diff --git a/bin/herald b/bin/herald new file mode 120000 index 0000000000..53f039ddf1 --- /dev/null +++ b/bin/herald @@ -0,0 +1 @@ +../scripts/setup/manage_herald.php \ No newline at end of file diff --git a/resources/celerity/map.php b/resources/celerity/map.php index f812e17517..e5eb50bb58 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -1,2361 +1,2361 @@ array( 'conpherence.pkg.css' => 'e68cf1fa', 'conpherence.pkg.js' => '15191c65', - 'core.pkg.css' => '2574c199', - 'core.pkg.js' => 'b5a949ca', + 'core.pkg.css' => 'cff4ff6f', + 'core.pkg.js' => '4bde473b', 'differential.pkg.css' => '06dc617c', - 'differential.pkg.js' => 'c1cfa143', + 'differential.pkg.js' => 'ef0b989b', 'diffusion.pkg.css' => 'a2d17c7d', 'diffusion.pkg.js' => '6134c5a1', 'maniphest.pkg.css' => '4845691a', 'maniphest.pkg.js' => '4d7e79c8', 'rsrc/audio/basic/alert.mp3' => '98461568', 'rsrc/audio/basic/bing.mp3' => 'ab8603a5', 'rsrc/audio/basic/pock.mp3' => '0cc772f5', 'rsrc/audio/basic/tap.mp3' => 'fc2fd796', 'rsrc/audio/basic/ting.mp3' => '17660001', 'rsrc/css/aphront/aphront-bars.css' => '231ac33c', 'rsrc/css/aphront/dark-console.css' => '0e14e8f6', 'rsrc/css/aphront/dialog-view.css' => '6bfc244b', 'rsrc/css/aphront/list-filter-view.css' => '5d6f0526', 'rsrc/css/aphront/multi-column.css' => '84cc6640', 'rsrc/css/aphront/notification.css' => '457861ec', 'rsrc/css/aphront/panel-view.css' => '8427b78d', 'rsrc/css/aphront/phabricator-nav-view.css' => '694d7723', 'rsrc/css/aphront/table-view.css' => '8c9bbafe', 'rsrc/css/aphront/tokenizer.css' => '15d5ff71', - 'rsrc/css/aphront/tooltip.css' => '173b9431', + 'rsrc/css/aphront/tooltip.css' => 'cb1397a4', 'rsrc/css/aphront/typeahead-browse.css' => 'f2818435', 'rsrc/css/aphront/typeahead.css' => 'a4a21016', 'rsrc/css/application/almanac/almanac.css' => 'dbb9b3af', 'rsrc/css/application/auth/auth.css' => '0877ed6e', 'rsrc/css/application/base/main-menu-view.css' => '1802a242', 'rsrc/css/application/base/notification-menu.css' => 'ef480927', 'rsrc/css/application/base/phui-theme.css' => '9f261c6b', 'rsrc/css/application/base/standard-page-view.css' => '34ee718b', 'rsrc/css/application/chatlog/chatlog.css' => 'd295b020', 'rsrc/css/application/conduit/conduit-api.css' => '7bc725c4', 'rsrc/css/application/config/config-options.css' => '4615667b', 'rsrc/css/application/config/config-template.css' => '8f18fa41', 'rsrc/css/application/config/setup-issue.css' => '30ee0173', 'rsrc/css/application/config/unhandled-exception.css' => '4c96257a', 'rsrc/css/application/conpherence/color.css' => 'abb4c358', 'rsrc/css/application/conpherence/durable-column.css' => '89ea6bef', 'rsrc/css/application/conpherence/header-pane.css' => 'cb6f4e19', 'rsrc/css/application/conpherence/menu.css' => '69368e97', 'rsrc/css/application/conpherence/message-pane.css' => 'b0f55ecc', 'rsrc/css/application/conpherence/notification.css' => 'cef0a3fc', 'rsrc/css/application/conpherence/participant-pane.css' => '26a3ce56', 'rsrc/css/application/conpherence/transaction.css' => '85129c68', 'rsrc/css/application/contentsource/content-source-view.css' => '4b8b05d4', 'rsrc/css/application/countdown/timer.css' => '16c52f5c', 'rsrc/css/application/daemon/bulk-job.css' => 'df9c1d4a', 'rsrc/css/application/dashboard/dashboard.css' => 'fe5b1869', 'rsrc/css/application/diff/inline-comment-summary.css' => 'f23d4e8f', 'rsrc/css/application/differential/add-comment.css' => 'c47f8c40', 'rsrc/css/application/differential/changeset-view.css' => 'db34a142', 'rsrc/css/application/differential/core.css' => '5b7b8ff4', 'rsrc/css/application/differential/phui-inline-comment.css' => '65ae3bc2', 'rsrc/css/application/differential/revision-comment.css' => '14b8565a', 'rsrc/css/application/differential/revision-history.css' => '0e8eb855', 'rsrc/css/application/differential/revision-list.css' => 'f3c47d33', 'rsrc/css/application/differential/table-of-contents.css' => 'ae4b7a55', 'rsrc/css/application/diffusion/diffusion-icons.css' => '0c15255e', 'rsrc/css/application/diffusion/diffusion-readme.css' => '419dd5b6', 'rsrc/css/application/diffusion/diffusion-repository.css' => 'ee6f20ec', 'rsrc/css/application/diffusion/diffusion.css' => '45727264', 'rsrc/css/application/feed/feed.css' => 'ecd4ec57', 'rsrc/css/application/files/global-drag-and-drop.css' => 'b556a948', 'rsrc/css/application/flag/flag.css' => 'bba8f811', 'rsrc/css/application/harbormaster/harbormaster.css' => '7446ce72', 'rsrc/css/application/herald/herald-test.css' => 'a52e323e', 'rsrc/css/application/herald/herald.css' => 'cd8d0134', 'rsrc/css/application/maniphest/report.css' => '9b9580b7', 'rsrc/css/application/maniphest/task-edit.css' => 'fda62a9b', 'rsrc/css/application/maniphest/task-summary.css' => '11cc5344', 'rsrc/css/application/objectselector/object-selector.css' => '85ee8ce6', 'rsrc/css/application/owners/owners-path-editor.css' => '9c136c29', 'rsrc/css/application/paste/paste.css' => '9fcc9773', 'rsrc/css/application/people/people-picture-menu-item.css' => 'a06f7f34', 'rsrc/css/application/people/people-profile.css' => '4df76faf', 'rsrc/css/application/phame/phame.css' => '8cb3afcd', 'rsrc/css/application/pholio/pholio-edit.css' => '07676f51', 'rsrc/css/application/pholio/pholio-inline-comments.css' => '8e545e49', 'rsrc/css/application/pholio/pholio.css' => 'ca89d380', 'rsrc/css/application/phortune/phortune-credit-card-form.css' => '8391eb02', 'rsrc/css/application/phortune/phortune-invoice.css' => '476055e2', 'rsrc/css/application/phortune/phortune.css' => '5b99dae0', 'rsrc/css/application/phrequent/phrequent.css' => 'ffc185ad', 'rsrc/css/application/phriction/phriction-document-css.css' => '4282e4ad', 'rsrc/css/application/policy/policy-edit.css' => '815c66f7', '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' => '0010bb52', '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', 'rsrc/css/application/releeph/releeph-request-differential-create-dialog.css' => '8d8b92cd', 'rsrc/css/application/releeph/releeph-request-typeahead.css' => '667a48ae', 'rsrc/css/application/search/application-search-view.css' => '787f5b76', 'rsrc/css/application/search/search-results.css' => '505dd8cf', 'rsrc/css/application/slowvote/slowvote.css' => 'a94b7230', 'rsrc/css/application/tokens/tokens.css' => '3d0f239e', 'rsrc/css/application/uiexample/example.css' => '528b19de', 'rsrc/css/core/core.css' => '62fa3ace', 'rsrc/css/core/remarkup.css' => 'b182076e', 'rsrc/css/core/syntax.css' => 'e9c95dd4', 'rsrc/css/core/z-index.css' => '9d8f7c4b', 'rsrc/css/diviner/diviner-shared.css' => '896f1d43', 'rsrc/css/font/font-awesome.css' => 'e838e088', 'rsrc/css/font/font-lato.css' => 'c7ccd872', 'rsrc/css/font/phui-font-icon-base.css' => '870a7360', 'rsrc/css/layout/phabricator-filetree-view.css' => 'b912ad97', 'rsrc/css/layout/phabricator-source-code-view.css' => '2ab25dfa', 'rsrc/css/phui/button/phui-button-bar.css' => 'f1ff5494', 'rsrc/css/phui/button/phui-button-simple.css' => '8e1baf68', 'rsrc/css/phui/button/phui-button.css' => '6ccb303c', 'rsrc/css/phui/calendar/phui-calendar-day.css' => '572b1893', 'rsrc/css/phui/calendar/phui-calendar-list.css' => '576be600', 'rsrc/css/phui/calendar/phui-calendar-month.css' => '21154caf', 'rsrc/css/phui/calendar/phui-calendar.css' => 'f1ddf11c', 'rsrc/css/phui/object-item/phui-oi-big-ui.css' => '628f59de', 'rsrc/css/phui/object-item/phui-oi-color.css' => 'cd2b9b77', 'rsrc/css/phui/object-item/phui-oi-drag-ui.css' => '08f4ccc3', 'rsrc/css/phui/object-item/phui-oi-flush-ui.css' => '9d9685d6', 'rsrc/css/phui/object-item/phui-oi-list-view.css' => '7c5c1291', 'rsrc/css/phui/object-item/phui-oi-simple-ui.css' => 'a8beebea', 'rsrc/css/phui/phui-action-list.css' => '0bcd9a45', 'rsrc/css/phui/phui-action-panel.css' => 'b4798122', 'rsrc/css/phui/phui-badge.css' => '22c0cf4f', 'rsrc/css/phui/phui-basic-nav-view.css' => '98c11ab3', 'rsrc/css/phui/phui-big-info-view.css' => 'acc3492c', 'rsrc/css/phui/phui-box.css' => '4bd6cdb9', 'rsrc/css/phui/phui-bulk-editor.css' => '9a81e5d5', 'rsrc/css/phui/phui-chart.css' => '6bf6f78e', 'rsrc/css/phui/phui-cms.css' => '504b4b23', 'rsrc/css/phui/phui-comment-form.css' => 'ac68149f', 'rsrc/css/phui/phui-comment-panel.css' => 'f50152ad', 'rsrc/css/phui/phui-crumbs-view.css' => '10728aaa', 'rsrc/css/phui/phui-curtain-view.css' => '2bdaf026', 'rsrc/css/phui/phui-document-pro.css' => 'dd79b5df', 'rsrc/css/phui/phui-document-summary.css' => '9ca48bdf', 'rsrc/css/phui/phui-document.css' => 'c4ac41f9', 'rsrc/css/phui/phui-feed-story.css' => '44a9c8e9', 'rsrc/css/phui/phui-fontkit.css' => '1320ed01', 'rsrc/css/phui/phui-form-view.css' => '2f43fae7', 'rsrc/css/phui/phui-form.css' => '7aaa04e3', 'rsrc/css/phui/phui-head-thing.css' => 'fd311e5f', 'rsrc/css/phui/phui-header-view.css' => '1ba8b707', - 'rsrc/css/phui/phui-hovercard.css' => 'f0592bcf', + 'rsrc/css/phui/phui-hovercard.css' => '4a484541', 'rsrc/css/phui/phui-icon-set-selector.css' => '87db8fee', 'rsrc/css/phui/phui-icon.css' => 'cf24ceec', 'rsrc/css/phui/phui-image-mask.css' => 'a8498f9c', 'rsrc/css/phui/phui-info-view.css' => 'e929f98c', 'rsrc/css/phui/phui-invisible-character-view.css' => '6993d9f0', 'rsrc/css/phui/phui-left-right.css' => '75227a4d', 'rsrc/css/phui/phui-lightbox.css' => '0a035e40', 'rsrc/css/phui/phui-list.css' => '38f8c9bd', 'rsrc/css/phui/phui-object-box.css' => '9cff003c', 'rsrc/css/phui/phui-pager.css' => 'edcbc226', 'rsrc/css/phui/phui-pinboard-view.css' => '2495140e', 'rsrc/css/phui/phui-property-list-view.css' => '546a04ae', 'rsrc/css/phui/phui-remarkup-preview.css' => '54a34863', 'rsrc/css/phui/phui-segment-bar-view.css' => 'b1d1b892', 'rsrc/css/phui/phui-spacing.css' => '042804d6', 'rsrc/css/phui/phui-status.css' => 'd5263e49', 'rsrc/css/phui/phui-tag-view.css' => 'b4719c50', 'rsrc/css/phui/phui-timeline-view.css' => '6ddf8126', 'rsrc/css/phui/phui-two-column-view.css' => '44ec4951', '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', 'rsrc/css/phui/workboards/phui-workpanel.css' => 'a3a63478', 'rsrc/css/sprite-login.css' => '396f3c3a', 'rsrc/css/sprite-tokens.css' => '9cdfd599', 'rsrc/css/syntax/syntax-default.css' => '9923583c', 'rsrc/externals/d3/d3.min.js' => 'a11a5ff2', 'rsrc/externals/font/fontawesome/fontawesome-webfont.eot' => '24a7064f', 'rsrc/externals/font/fontawesome/fontawesome-webfont.ttf' => '0039fe26', 'rsrc/externals/font/fontawesome/fontawesome-webfont.woff' => 'de978a43', 'rsrc/externals/font/fontawesome/fontawesome-webfont.woff2' => '2a832057', 'rsrc/externals/font/lato/lato-bold.eot' => '99fbcf8c', 'rsrc/externals/font/lato/lato-bold.svg' => '2aa83045', 'rsrc/externals/font/lato/lato-bold.ttf' => '0a7141f7', 'rsrc/externals/font/lato/lato-bold.woff' => 'f5db2061', 'rsrc/externals/font/lato/lato-bold.woff2' => '37a94ecd', 'rsrc/externals/font/lato/lato-bolditalic.eot' => 'b93389d0', 'rsrc/externals/font/lato/lato-bolditalic.svg' => '5442e1ef', 'rsrc/externals/font/lato/lato-bolditalic.ttf' => 'dad31252', 'rsrc/externals/font/lato/lato-bolditalic.woff' => 'e53bcf47', 'rsrc/externals/font/lato/lato-bolditalic.woff2' => 'd035007f', 'rsrc/externals/font/lato/lato-italic.eot' => '6a903f5d', 'rsrc/externals/font/lato/lato-italic.svg' => '0dc7cf2f', 'rsrc/externals/font/lato/lato-italic.ttf' => '629f64f0', 'rsrc/externals/font/lato/lato-italic.woff' => '678dc4bb', 'rsrc/externals/font/lato/lato-italic.woff2' => '7c8dd650', 'rsrc/externals/font/lato/lato-regular.eot' => '848dfb1e', 'rsrc/externals/font/lato/lato-regular.svg' => 'cbd5fd6b', 'rsrc/externals/font/lato/lato-regular.ttf' => 'e270165b', 'rsrc/externals/font/lato/lato-regular.woff' => '13d39fe2', 'rsrc/externals/font/lato/lato-regular.woff2' => '57a9f742', - 'rsrc/externals/javelin/core/Event.js' => '2ee659ce', + 'rsrc/externals/javelin/core/Event.js' => 'ef7e057f', 'rsrc/externals/javelin/core/Stratcom.js' => '327f418a', 'rsrc/externals/javelin/core/__tests__/event-stop-and-kill.js' => '717554e4', 'rsrc/externals/javelin/core/__tests__/install.js' => 'c432ee85', 'rsrc/externals/javelin/core/__tests__/stratcom.js' => '88bf7313', 'rsrc/externals/javelin/core/__tests__/util.js' => 'e251703d', - 'rsrc/externals/javelin/core/init.js' => '638a4e2b', + 'rsrc/externals/javelin/core/init.js' => '8d83d2a1', 'rsrc/externals/javelin/core/init_node.js' => 'c234aded', 'rsrc/externals/javelin/core/install.js' => '05270951', 'rsrc/externals/javelin/core/util.js' => '93cc50d6', 'rsrc/externals/javelin/docs/Base.js' => '74676256', 'rsrc/externals/javelin/docs/onload.js' => 'e819c479', 'rsrc/externals/javelin/ext/fx/Color.js' => '7e41274a', 'rsrc/externals/javelin/ext/fx/FX.js' => '54b612ba', 'rsrc/externals/javelin/ext/reactor/core/DynVal.js' => 'f6555212', 'rsrc/externals/javelin/ext/reactor/core/Reactor.js' => '2b8de964', 'rsrc/externals/javelin/ext/reactor/core/ReactorNode.js' => '1ad0a787', 'rsrc/externals/javelin/ext/reactor/core/ReactorNodeCalmer.js' => '76f4ebed', 'rsrc/externals/javelin/ext/reactor/dom/RDOM.js' => 'c90a04fc', 'rsrc/externals/javelin/ext/view/HTMLView.js' => 'fe287620', 'rsrc/externals/javelin/ext/view/View.js' => '0f764c35', 'rsrc/externals/javelin/ext/view/ViewInterpreter.js' => 'f829edb3', 'rsrc/externals/javelin/ext/view/ViewPlaceholder.js' => '47830651', 'rsrc/externals/javelin/ext/view/ViewRenderer.js' => '6c2b09a2', 'rsrc/externals/javelin/ext/view/ViewVisitor.js' => 'efe49472', 'rsrc/externals/javelin/ext/view/__tests__/HTMLView.js' => 'f92d7bcb', 'rsrc/externals/javelin/ext/view/__tests__/View.js' => '6450b38b', 'rsrc/externals/javelin/ext/view/__tests__/ViewInterpreter.js' => '7a94d6a5', 'rsrc/externals/javelin/ext/view/__tests__/ViewRenderer.js' => '6ea96ac9', 'rsrc/externals/javelin/lib/Cookie.js' => '62dfea03', 'rsrc/externals/javelin/lib/DOM.js' => '4976858c', 'rsrc/externals/javelin/lib/History.js' => 'd4505101', 'rsrc/externals/javelin/lib/JSON.js' => '69adf288', 'rsrc/externals/javelin/lib/Leader.js' => '7f243deb', 'rsrc/externals/javelin/lib/Mask.js' => '8a41885b', 'rsrc/externals/javelin/lib/Quicksand.js' => '6b8ef10b', 'rsrc/externals/javelin/lib/Request.js' => '94b750d2', 'rsrc/externals/javelin/lib/Resource.js' => '44959b73', 'rsrc/externals/javelin/lib/Routable.js' => 'b3e7d692', 'rsrc/externals/javelin/lib/Router.js' => '29274e2b', 'rsrc/externals/javelin/lib/Scrollbar.js' => '9065f639', 'rsrc/externals/javelin/lib/Sound.js' => '949c0fe5', 'rsrc/externals/javelin/lib/URI.js' => 'c989ade3', 'rsrc/externals/javelin/lib/Vector.js' => '2caa8fb8', 'rsrc/externals/javelin/lib/WebSocket.js' => '3ffe32d6', 'rsrc/externals/javelin/lib/Workflow.js' => '6a726c55', 'rsrc/externals/javelin/lib/__tests__/Cookie.js' => '5ed109e8', 'rsrc/externals/javelin/lib/__tests__/DOM.js' => 'c984504b', 'rsrc/externals/javelin/lib/__tests__/JSON.js' => '837a7d68', 'rsrc/externals/javelin/lib/__tests__/URI.js' => '1e45fda9', 'rsrc/externals/javelin/lib/__tests__/behavior.js' => '1ea62783', 'rsrc/externals/javelin/lib/behavior.js' => '61cbc29a', 'rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js' => 'bb6e5c16', 'rsrc/externals/javelin/lib/control/typeahead/Typeahead.js' => '70baed2f', 'rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js' => '185bbd53', 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadCompositeSource.js' => '503e17fd', 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js' => '013ffff9', 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js' => '54f314a0', 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => 'ab9e0a82', 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js' => '6c0e62fa', 'rsrc/favicons/favicon-16x16.png' => 'fc6275ba', 'rsrc/favicons/mask-icon.svg' => 'e132a80f', 'rsrc/image/BFCFDA.png' => 'd5ec91f4', 'rsrc/image/actions/edit.png' => '2fc41442', 'rsrc/image/avatar.png' => '17d346a4', 'rsrc/image/checker_dark.png' => 'd8e65881', 'rsrc/image/checker_light.png' => 'a0155918', 'rsrc/image/checker_lighter.png' => 'd5da91b6', 'rsrc/image/controls/checkbox-checked.png' => 'ad6441ea', 'rsrc/image/controls/checkbox-unchecked.png' => '8eb1f0ae', 'rsrc/image/d5d8e1.png' => '0c2a1497', 'rsrc/image/darkload.gif' => '1ffd3ec6', 'rsrc/image/divot.png' => '94dded62', 'rsrc/image/examples/hero.png' => '979a86ae', 'rsrc/image/grippy_texture.png' => 'aca81e2f', 'rsrc/image/icon/fatcow/arrow_branch.png' => '2537c01c', 'rsrc/image/icon/fatcow/arrow_merge.png' => '21b660e0', 'rsrc/image/icon/fatcow/calendar_edit.png' => '24632275', 'rsrc/image/icon/fatcow/document_black.png' => '45fe1c60', 'rsrc/image/icon/fatcow/flag_blue.png' => 'a01abb1d', 'rsrc/image/icon/fatcow/flag_finish.png' => '67825cee', 'rsrc/image/icon/fatcow/flag_ghost.png' => '20ca8783', 'rsrc/image/icon/fatcow/flag_green.png' => '7e0eaa7a', 'rsrc/image/icon/fatcow/flag_orange.png' => '9e73df66', 'rsrc/image/icon/fatcow/flag_pink.png' => '7e92f3b2', 'rsrc/image/icon/fatcow/flag_purple.png' => 'cc517522', 'rsrc/image/icon/fatcow/flag_red.png' => '04ec726f', 'rsrc/image/icon/fatcow/flag_yellow.png' => '73946fd4', 'rsrc/image/icon/fatcow/key_question.png' => '52a0c26a', 'rsrc/image/icon/fatcow/link.png' => '7afd4d5e', 'rsrc/image/icon/fatcow/page_white_edit.png' => '39a2eed8', 'rsrc/image/icon/fatcow/page_white_put.png' => '08c95a0c', 'rsrc/image/icon/fatcow/source/conduit.png' => '4ea01d2f', 'rsrc/image/icon/fatcow/source/email.png' => '9bab3239', 'rsrc/image/icon/fatcow/source/fax.png' => '04195e68', 'rsrc/image/icon/fatcow/source/mobile.png' => 'f1321264', 'rsrc/image/icon/fatcow/source/tablet.png' => '49396799', 'rsrc/image/icon/fatcow/source/web.png' => '136ccb5d', 'rsrc/image/icon/subscribe.png' => 'd03ed5a5', 'rsrc/image/icon/tango/attachment.png' => 'ecc8022e', 'rsrc/image/icon/tango/edit.png' => '929a1363', 'rsrc/image/icon/tango/go-down.png' => '96d95e43', 'rsrc/image/icon/tango/log.png' => 'b08cc63a', 'rsrc/image/icon/tango/upload.png' => '7bbb7984', 'rsrc/image/icon/unsubscribe.png' => '25725013', 'rsrc/image/lightblue-header.png' => '5c168b6d', 'rsrc/image/logo/light-eye.png' => '1a576ddd', 'rsrc/image/main_texture.png' => '29a2c5ad', 'rsrc/image/menu_texture.png' => '5a17580d', 'rsrc/image/people/harding.png' => '45aa614e', 'rsrc/image/people/jefferson.png' => 'afca0e53', 'rsrc/image/people/lincoln.png' => '9369126d', 'rsrc/image/people/mckinley.png' => 'fb8f16ce', 'rsrc/image/people/taft.png' => 'd7bc402c', 'rsrc/image/people/user0.png' => '03dacaea', 'rsrc/image/people/user1.png' => '4a4e7702', 'rsrc/image/people/user2.png' => '47a0ee40', 'rsrc/image/people/user3.png' => '835ff627', 'rsrc/image/people/user4.png' => 'b0e830f1', 'rsrc/image/people/user5.png' => '9c95b369', 'rsrc/image/people/user6.png' => 'ba3fbfb0', 'rsrc/image/people/user7.png' => 'da613924', 'rsrc/image/people/user8.png' => 'f1035edf', 'rsrc/image/people/user9.png' => '66730be3', 'rsrc/image/people/washington.png' => '40dd301c', 'rsrc/image/phrequent_active.png' => 'a466a8ed', 'rsrc/image/phrequent_inactive.png' => 'bfc15a69', 'rsrc/image/resize.png' => 'fd476de4', 'rsrc/image/sprite-login-X2.png' => '308c92c4', 'rsrc/image/sprite-login.png' => '9ec54245', 'rsrc/image/sprite-tokens-X2.png' => '804a5232', 'rsrc/image/sprite-tokens.png' => 'b41d03da', 'rsrc/image/texture/card-gradient.png' => '815f26e8', 'rsrc/image/texture/dark-menu-hover.png' => '5fa7ece8', 'rsrc/image/texture/dark-menu.png' => '7e22296e', 'rsrc/image/texture/grip.png' => '719404f3', 'rsrc/image/texture/panel-header-gradient.png' => 'e3b8dcfe', 'rsrc/image/texture/phlnx-bg.png' => '8d819209', 'rsrc/image/texture/pholio-background.gif' => 'ba29239c', 'rsrc/image/texture/table_header.png' => '5c433037', 'rsrc/image/texture/table_header_hover.png' => '038ec3b9', 'rsrc/image/texture/table_header_tall.png' => 'd56b434f', 'rsrc/js/application/aphlict/Aphlict.js' => 'e1d4b11a', 'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => 'caade6f2', 'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => '599a8f5f', 'rsrc/js/application/aphlict/behavior-aphlict-status.js' => '5e2634b9', 'rsrc/js/application/aphlict/behavior-desktop-notifications-control.js' => '27ca6289', 'rsrc/js/application/calendar/behavior-day-view.js' => '4b3c4443', 'rsrc/js/application/calendar/behavior-event-all-day.js' => 'b41537c9', 'rsrc/js/application/calendar/behavior-month-view.js' => 'fe33e256', 'rsrc/js/application/config/behavior-reorder-fields.js' => 'b6993408', 'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => '4d863052', 'rsrc/js/application/conpherence/behavior-conpherence-search.js' => '9bbf3762', 'rsrc/js/application/conpherence/behavior-durable-column.js' => '2ae077e1', 'rsrc/js/application/conpherence/behavior-menu.js' => '4047cd35', 'rsrc/js/application/conpherence/behavior-participant-pane.js' => 'd057e45a', 'rsrc/js/application/conpherence/behavior-pontificate.js' => '55616e04', 'rsrc/js/application/conpherence/behavior-quicksand-blacklist.js' => '7927a7d3', 'rsrc/js/application/conpherence/behavior-toggle-widget.js' => '3dbf94d5', 'rsrc/js/application/countdown/timer.js' => 'e4cc26b3', 'rsrc/js/application/daemon/behavior-bulk-job-reload.js' => 'edf8a145', 'rsrc/js/application/dashboard/behavior-dashboard-async-panel.js' => '469c0d9e', 'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => '408bf173', 'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => '453c5375', 'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => 'd4eecc63', 'rsrc/js/application/diff/DiffChangeset.js' => 'b49b59d6', - 'rsrc/js/application/diff/DiffChangesetList.js' => 'e0b984b5', + 'rsrc/js/application/diff/DiffChangesetList.js' => '0a84bcc1', 'rsrc/js/application/diff/DiffInline.js' => 'e83d28f3', 'rsrc/js/application/diff/behavior-preview-link.js' => '051c7832', 'rsrc/js/application/differential/behavior-comment-preview.js' => '51c5ad07', 'rsrc/js/application/differential/behavior-diff-radios.js' => 'e1ff79b1', 'rsrc/js/application/differential/behavior-populate.js' => 'f0eb6708', 'rsrc/js/application/differential/behavior-user-select.js' => 'a8d8459d', 'rsrc/js/application/diffusion/DiffusionLocateFileSource.js' => '00676f00', 'rsrc/js/application/diffusion/behavior-audit-preview.js' => 'd835b03a', 'rsrc/js/application/diffusion/behavior-commit-branches.js' => 'bdaf4d04', 'rsrc/js/application/diffusion/behavior-commit-graph.js' => '75b83cbb', 'rsrc/js/application/diffusion/behavior-locate-file.js' => '6d3e1947', 'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => 'f01586dc', 'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => '1db13e70', 'rsrc/js/application/drydock/drydock-live-operation-status.js' => '901935ef', 'rsrc/js/application/files/behavior-document-engine.js' => '3935d8c4', 'rsrc/js/application/files/behavior-icon-composer.js' => '8499b6ab', 'rsrc/js/application/files/behavior-launch-icon-composer.js' => '48086888', 'rsrc/js/application/harbormaster/behavior-harbormaster-log.js' => '549459b8', 'rsrc/js/application/herald/HeraldRuleEditor.js' => 'dca75c0e', 'rsrc/js/application/herald/PathTypeahead.js' => '6d8c7912', 'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3', 'rsrc/js/application/maniphest/behavior-batch-selector.js' => 'ad54037e', 'rsrc/js/application/maniphest/behavior-line-chart.js' => 'e4232876', 'rsrc/js/application/maniphest/behavior-list-edit.js' => 'a9f88de2', 'rsrc/js/application/maniphest/behavior-subpriorityeditor.js' => '71237763', 'rsrc/js/application/owners/OwnersPathEditor.js' => 'c96502cf', 'rsrc/js/application/owners/owners-path-editor.js' => '7a68dda3', 'rsrc/js/application/passphrase/passphrase-credential-control.js' => '3cb0b2fc', 'rsrc/js/application/pholio/behavior-pholio-mock-edit.js' => 'bee502c8', 'rsrc/js/application/pholio/behavior-pholio-mock-view.js' => 'ec1f3669', 'rsrc/js/application/phortune/behavior-stripe-payment-form.js' => 'a6b98425', 'rsrc/js/application/phortune/behavior-test-payment-form.js' => 'fc91ab6c', 'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef', 'rsrc/js/application/policy/behavior-policy-control.js' => 'd0c516d5', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c', 'rsrc/js/application/projects/WorkboardBoard.js' => '8935deef', 'rsrc/js/application/projects/WorkboardCard.js' => 'c587b80f', 'rsrc/js/application/projects/WorkboardColumn.js' => '758b4758', 'rsrc/js/application/projects/WorkboardController.js' => '26167537', 'rsrc/js/application/projects/behavior-project-boards.js' => '4250a34e', 'rsrc/js/application/projects/behavior-project-create.js' => '065227cc', 'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb', 'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf', 'rsrc/js/application/releeph/releeph-request-state-change.js' => 'a0b57eb8', 'rsrc/js/application/releeph/releeph-request-typeahead.js' => 'de2e896f', 'rsrc/js/application/repository/repository-crossreference.js' => '9a860428', 'rsrc/js/application/search/behavior-reorder-profile-menu-items.js' => 'e2e0a072', 'rsrc/js/application/search/behavior-reorder-queries.js' => 'e9581f08', - 'rsrc/js/application/transactions/behavior-comment-actions.js' => '038bf27f', + 'rsrc/js/application/transactions/behavior-comment-actions.js' => '59e27e74', 'rsrc/js/application/transactions/behavior-reorder-configs.js' => 'd7a74243', 'rsrc/js/application/transactions/behavior-reorder-fields.js' => 'b59e1e96', 'rsrc/js/application/transactions/behavior-show-older-transactions.js' => '8f29b364', 'rsrc/js/application/transactions/behavior-transaction-comment-form.js' => 'b23b49e6', 'rsrc/js/application/transactions/behavior-transaction-list.js' => '1f6794f6', 'rsrc/js/application/typeahead/behavior-typeahead-browse.js' => '635de1ec', 'rsrc/js/application/typeahead/behavior-typeahead-search.js' => '93d0c9e3', 'rsrc/js/application/uiexample/gesture-example.js' => '558829c2', 'rsrc/js/application/uiexample/notification-example.js' => '8ce821c5', 'rsrc/js/core/Busy.js' => '59a7976a', 'rsrc/js/core/DragAndDropFileUpload.js' => '58dea2fa', 'rsrc/js/core/DraggableList.js' => 'bea6e7f4', 'rsrc/js/core/Favicon.js' => '1fe2510c', 'rsrc/js/core/FileUpload.js' => '680ea2c8', 'rsrc/js/core/Hovercard.js' => '1bd28176', 'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2', 'rsrc/js/core/KeyboardShortcutManager.js' => 'c19dd9b9', 'rsrc/js/core/MultirowRowManager.js' => 'b5d57730', 'rsrc/js/core/Notification.js' => '4f774dac', 'rsrc/js/core/Prefab.js' => '77b0ae28', 'rsrc/js/core/ShapedRequest.js' => '7cbe244b', 'rsrc/js/core/TextAreaUtils.js' => '320810c8', 'rsrc/js/core/Title.js' => '485aaa6c', 'rsrc/js/core/ToolTip.js' => '358b8c04', 'rsrc/js/core/behavior-active-nav.js' => 'e379b58e', 'rsrc/js/core/behavior-audio-source.js' => '59b251eb', 'rsrc/js/core/behavior-autofocus.js' => '7319e029', 'rsrc/js/core/behavior-badge-view.js' => '8ff5e24c', 'rsrc/js/core/behavior-bulk-editor.js' => '66a6def1', 'rsrc/js/core/behavior-choose-control.js' => '327a00d1', 'rsrc/js/core/behavior-copy.js' => 'b0b8f86d', 'rsrc/js/core/behavior-detect-timezone.js' => '4c193c96', 'rsrc/js/core/behavior-device.js' => 'a3714c76', 'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '484a6e22', 'rsrc/js/core/behavior-fancy-datepicker.js' => 'ecf4e799', 'rsrc/js/core/behavior-file-tree.js' => '88236f00', 'rsrc/js/core/behavior-form.js' => '5c54cbf3', 'rsrc/js/core/behavior-gesture.js' => '3ab51e2c', 'rsrc/js/core/behavior-global-drag-and-drop.js' => '960f6a39', 'rsrc/js/core/behavior-high-security-warning.js' => 'a464fe03', 'rsrc/js/core/behavior-history-install.js' => '7ee2b591', 'rsrc/js/core/behavior-hovercard.js' => 'bcaccd64', 'rsrc/js/core/behavior-keyboard-pager.js' => 'a8da01f0', 'rsrc/js/core/behavior-keyboard-shortcuts.js' => '01fca1f0', 'rsrc/js/core/behavior-lightbox-attachments.js' => '6b31879a', 'rsrc/js/core/behavior-line-linker.js' => '66a62306', 'rsrc/js/core/behavior-more.js' => 'a80d0378', 'rsrc/js/core/behavior-object-selector.js' => '77c1f0b0', 'rsrc/js/core/behavior-oncopy.js' => '2926fff2', 'rsrc/js/core/behavior-phabricator-nav.js' => '9d32bc88', 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => 'acd29eee', 'rsrc/js/core/behavior-read-only-warning.js' => 'ba158207', 'rsrc/js/core/behavior-redirect.js' => '0213259f', 'rsrc/js/core/behavior-refresh-csrf.js' => 'ab2f381b', 'rsrc/js/core/behavior-remarkup-load-image.js' => '040fce04', 'rsrc/js/core/behavior-remarkup-preview.js' => '4b700e9e', 'rsrc/js/core/behavior-reorder-applications.js' => '76b9fc3e', 'rsrc/js/core/behavior-reveal-content.js' => '60821bc7', 'rsrc/js/core/behavior-scrollbar.js' => '834a1173', 'rsrc/js/core/behavior-search-typeahead.js' => 'c3e917d9', 'rsrc/js/core/behavior-select-content.js' => 'bf5374ef', 'rsrc/js/core/behavior-select-on-click.js' => '4e3e79a6', 'rsrc/js/core/behavior-setup-check-https.js' => '491416b3', 'rsrc/js/core/behavior-time-typeahead.js' => '522431f7', 'rsrc/js/core/behavior-toggle-class.js' => '92b9ec77', 'rsrc/js/core/behavior-tokenizer.js' => 'b3a4b884', 'rsrc/js/core/behavior-tooltip.js' => 'c420b0b9', 'rsrc/js/core/behavior-user-menu.js' => '31420f77', 'rsrc/js/core/behavior-watch-anchor.js' => '9f36c42d', 'rsrc/js/core/behavior-workflow.js' => '0a3f3021', 'rsrc/js/core/darkconsole/DarkLog.js' => 'c8e1ffe3', 'rsrc/js/core/darkconsole/DarkMessage.js' => 'c48cccdd', 'rsrc/js/core/darkconsole/behavior-dark-console.js' => '66888767', 'rsrc/js/core/phtize.js' => 'd254d646', 'rsrc/js/phui/behavior-phui-dropdown-menu.js' => 'b95d6f7d', 'rsrc/js/phui/behavior-phui-file-upload.js' => 'b003d4fb', 'rsrc/js/phui/behavior-phui-selectable-list.js' => '464259a2', 'rsrc/js/phui/behavior-phui-submenu.js' => 'a6f7a73b', 'rsrc/js/phui/behavior-phui-tab-group.js' => '0a0b10e9', 'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8', 'rsrc/js/phuix/PHUIXActionView.js' => '8d4a8c72', 'rsrc/js/phuix/PHUIXAutocomplete.js' => 'df1bbd34', 'rsrc/js/phuix/PHUIXButtonView.js' => '85ac9772', 'rsrc/js/phuix/PHUIXDropdownMenu.js' => '04b2ae03', 'rsrc/js/phuix/PHUIXExample.js' => '68af71ca', 'rsrc/js/phuix/PHUIXFormControl.js' => '210a16c1', 'rsrc/js/phuix/PHUIXIconView.js' => 'bff6884b', ), 'symbols' => array( 'almanac-css' => 'dbb9b3af', 'aphront-bars' => '231ac33c', 'aphront-dark-console-css' => '0e14e8f6', 'aphront-dialog-view-css' => '6bfc244b', 'aphront-list-filter-view-css' => '5d6f0526', 'aphront-multi-column-view-css' => '84cc6640', 'aphront-panel-view-css' => '8427b78d', 'aphront-table-view-css' => '8c9bbafe', 'aphront-tokenizer-control-css' => '15d5ff71', - 'aphront-tooltip-css' => '173b9431', + 'aphront-tooltip-css' => 'cb1397a4', 'aphront-typeahead-control-css' => 'a4a21016', 'application-search-view-css' => '787f5b76', 'auth-css' => '0877ed6e', 'bulk-job-css' => 'df9c1d4a', 'conduit-api-css' => '7bc725c4', 'config-options-css' => '4615667b', 'conpherence-color-css' => 'abb4c358', 'conpherence-durable-column-view' => '89ea6bef', 'conpherence-header-pane-css' => 'cb6f4e19', 'conpherence-menu-css' => '69368e97', 'conpherence-message-pane-css' => 'b0f55ecc', 'conpherence-notification-css' => 'cef0a3fc', 'conpherence-participant-pane-css' => '26a3ce56', 'conpherence-thread-manager' => '4d863052', 'conpherence-transaction-css' => '85129c68', 'd3' => 'a11a5ff2', 'differential-changeset-view-css' => 'db34a142', 'differential-core-view-css' => '5b7b8ff4', 'differential-revision-add-comment-css' => 'c47f8c40', 'differential-revision-comment-css' => '14b8565a', 'differential-revision-history-css' => '0e8eb855', 'differential-revision-list-css' => 'f3c47d33', 'differential-table-of-contents-css' => 'ae4b7a55', 'diffusion-css' => '45727264', 'diffusion-icons-css' => '0c15255e', 'diffusion-readme-css' => '419dd5b6', 'diffusion-repository-css' => 'ee6f20ec', 'diviner-shared-css' => '896f1d43', 'font-fontawesome' => 'e838e088', 'font-lato' => 'c7ccd872', 'global-drag-and-drop-css' => 'b556a948', 'harbormaster-css' => '7446ce72', 'herald-css' => 'cd8d0134', 'herald-rule-editor' => 'dca75c0e', 'herald-test-css' => 'a52e323e', 'inline-comment-summary-css' => 'f23d4e8f', 'javelin-aphlict' => 'e1d4b11a', 'javelin-behavior' => '61cbc29a', 'javelin-behavior-aphlict-dropdown' => 'caade6f2', 'javelin-behavior-aphlict-listen' => '599a8f5f', 'javelin-behavior-aphlict-status' => '5e2634b9', 'javelin-behavior-aphront-basic-tokenizer' => 'b3a4b884', 'javelin-behavior-aphront-drag-and-drop-textarea' => '484a6e22', 'javelin-behavior-aphront-form-disable-on-submit' => '5c54cbf3', 'javelin-behavior-aphront-more' => 'a80d0378', 'javelin-behavior-audio-source' => '59b251eb', 'javelin-behavior-audit-preview' => 'd835b03a', 'javelin-behavior-badge-view' => '8ff5e24c', 'javelin-behavior-bulk-editor' => '66a6def1', 'javelin-behavior-bulk-job-reload' => 'edf8a145', 'javelin-behavior-calendar-month-view' => 'fe33e256', 'javelin-behavior-choose-control' => '327a00d1', - 'javelin-behavior-comment-actions' => '038bf27f', + 'javelin-behavior-comment-actions' => '59e27e74', 'javelin-behavior-config-reorder-fields' => 'b6993408', 'javelin-behavior-conpherence-menu' => '4047cd35', 'javelin-behavior-conpherence-participant-pane' => 'd057e45a', 'javelin-behavior-conpherence-pontificate' => '55616e04', 'javelin-behavior-conpherence-search' => '9bbf3762', 'javelin-behavior-countdown-timer' => 'e4cc26b3', 'javelin-behavior-dark-console' => '66888767', 'javelin-behavior-dashboard-async-panel' => '469c0d9e', 'javelin-behavior-dashboard-move-panels' => '408bf173', 'javelin-behavior-dashboard-query-panel-select' => '453c5375', 'javelin-behavior-dashboard-tab-panel' => 'd4eecc63', 'javelin-behavior-day-view' => '4b3c4443', 'javelin-behavior-desktop-notifications-control' => '27ca6289', 'javelin-behavior-detect-timezone' => '4c193c96', 'javelin-behavior-device' => 'a3714c76', 'javelin-behavior-diff-preview-link' => '051c7832', 'javelin-behavior-differential-diff-radios' => 'e1ff79b1', 'javelin-behavior-differential-feedback-preview' => '51c5ad07', 'javelin-behavior-differential-populate' => 'f0eb6708', 'javelin-behavior-differential-user-select' => 'a8d8459d', 'javelin-behavior-diffusion-commit-branches' => 'bdaf4d04', 'javelin-behavior-diffusion-commit-graph' => '75b83cbb', 'javelin-behavior-diffusion-locate-file' => '6d3e1947', 'javelin-behavior-diffusion-pull-lastmodified' => 'f01586dc', 'javelin-behavior-document-engine' => '3935d8c4', 'javelin-behavior-doorkeeper-tag' => '1db13e70', 'javelin-behavior-drydock-live-operation-status' => '901935ef', 'javelin-behavior-durable-column' => '2ae077e1', 'javelin-behavior-editengine-reorder-configs' => 'd7a74243', 'javelin-behavior-editengine-reorder-fields' => 'b59e1e96', 'javelin-behavior-event-all-day' => 'b41537c9', 'javelin-behavior-fancy-datepicker' => 'ecf4e799', 'javelin-behavior-global-drag-and-drop' => '960f6a39', 'javelin-behavior-harbormaster-log' => '549459b8', 'javelin-behavior-herald-rule-editor' => '7ebaeed3', 'javelin-behavior-high-security-warning' => 'a464fe03', 'javelin-behavior-history-install' => '7ee2b591', 'javelin-behavior-icon-composer' => '8499b6ab', 'javelin-behavior-launch-icon-composer' => '48086888', 'javelin-behavior-lightbox-attachments' => '6b31879a', 'javelin-behavior-line-chart' => 'e4232876', 'javelin-behavior-maniphest-batch-selector' => 'ad54037e', 'javelin-behavior-maniphest-list-editor' => 'a9f88de2', 'javelin-behavior-maniphest-subpriority-editor' => '71237763', 'javelin-behavior-owners-path-editor' => '7a68dda3', 'javelin-behavior-passphrase-credential-control' => '3cb0b2fc', 'javelin-behavior-phabricator-active-nav' => 'e379b58e', 'javelin-behavior-phabricator-autofocus' => '7319e029', 'javelin-behavior-phabricator-clipboard-copy' => 'b0b8f86d', 'javelin-behavior-phabricator-file-tree' => '88236f00', 'javelin-behavior-phabricator-gesture' => '3ab51e2c', 'javelin-behavior-phabricator-gesture-example' => '558829c2', 'javelin-behavior-phabricator-keyboard-pager' => 'a8da01f0', 'javelin-behavior-phabricator-keyboard-shortcuts' => '01fca1f0', 'javelin-behavior-phabricator-line-linker' => '66a62306', 'javelin-behavior-phabricator-nav' => '9d32bc88', 'javelin-behavior-phabricator-notification-example' => '8ce821c5', 'javelin-behavior-phabricator-object-selector' => '77c1f0b0', 'javelin-behavior-phabricator-oncopy' => '2926fff2', 'javelin-behavior-phabricator-remarkup-assist' => 'acd29eee', 'javelin-behavior-phabricator-reveal-content' => '60821bc7', 'javelin-behavior-phabricator-search-typeahead' => 'c3e917d9', 'javelin-behavior-phabricator-show-older-transactions' => '8f29b364', 'javelin-behavior-phabricator-tooltips' => 'c420b0b9', 'javelin-behavior-phabricator-transaction-comment-form' => 'b23b49e6', 'javelin-behavior-phabricator-transaction-list' => '1f6794f6', 'javelin-behavior-phabricator-watch-anchor' => '9f36c42d', 'javelin-behavior-pholio-mock-edit' => 'bee502c8', 'javelin-behavior-pholio-mock-view' => 'ec1f3669', 'javelin-behavior-phui-dropdown-menu' => 'b95d6f7d', 'javelin-behavior-phui-file-upload' => 'b003d4fb', 'javelin-behavior-phui-hovercards' => 'bcaccd64', 'javelin-behavior-phui-selectable-list' => '464259a2', 'javelin-behavior-phui-submenu' => 'a6f7a73b', 'javelin-behavior-phui-tab-group' => '0a0b10e9', 'javelin-behavior-phuix-example' => '68af71ca', 'javelin-behavior-policy-control' => 'd0c516d5', 'javelin-behavior-policy-rule-editor' => '5e9f347c', 'javelin-behavior-project-boards' => '4250a34e', 'javelin-behavior-project-create' => '065227cc', 'javelin-behavior-quicksand-blacklist' => '7927a7d3', 'javelin-behavior-read-only-warning' => 'ba158207', 'javelin-behavior-redirect' => '0213259f', 'javelin-behavior-refresh-csrf' => 'ab2f381b', 'javelin-behavior-releeph-preview-branch' => 'b2b4fbaf', 'javelin-behavior-releeph-request-state-change' => 'a0b57eb8', 'javelin-behavior-releeph-request-typeahead' => 'de2e896f', 'javelin-behavior-remarkup-load-image' => '040fce04', 'javelin-behavior-remarkup-preview' => '4b700e9e', 'javelin-behavior-reorder-applications' => '76b9fc3e', 'javelin-behavior-reorder-columns' => 'e1d25dfb', 'javelin-behavior-reorder-profile-menu-items' => 'e2e0a072', 'javelin-behavior-repository-crossreference' => '9a860428', 'javelin-behavior-scrollbar' => '834a1173', 'javelin-behavior-search-reorder-queries' => 'e9581f08', 'javelin-behavior-select-content' => 'bf5374ef', 'javelin-behavior-select-on-click' => '4e3e79a6', 'javelin-behavior-setup-check-https' => '491416b3', 'javelin-behavior-stripe-payment-form' => 'a6b98425', 'javelin-behavior-test-payment-form' => 'fc91ab6c', 'javelin-behavior-time-typeahead' => '522431f7', 'javelin-behavior-toggle-class' => '92b9ec77', 'javelin-behavior-toggle-widget' => '3dbf94d5', 'javelin-behavior-typeahead-browse' => '635de1ec', 'javelin-behavior-typeahead-search' => '93d0c9e3', 'javelin-behavior-user-menu' => '31420f77', 'javelin-behavior-view-placeholder' => '47830651', 'javelin-behavior-workflow' => '0a3f3021', 'javelin-color' => '7e41274a', 'javelin-cookie' => '62dfea03', 'javelin-diffusion-locate-file-source' => '00676f00', 'javelin-dom' => '4976858c', 'javelin-dynval' => 'f6555212', - 'javelin-event' => '2ee659ce', + 'javelin-event' => 'ef7e057f', 'javelin-fx' => '54b612ba', 'javelin-history' => 'd4505101', 'javelin-install' => '05270951', 'javelin-json' => '69adf288', 'javelin-leader' => '7f243deb', - 'javelin-magical-init' => '638a4e2b', + 'javelin-magical-init' => '8d83d2a1', 'javelin-mask' => '8a41885b', 'javelin-quicksand' => '6b8ef10b', 'javelin-reactor' => '2b8de964', 'javelin-reactor-dom' => 'c90a04fc', 'javelin-reactor-node-calmer' => '76f4ebed', 'javelin-reactornode' => '1ad0a787', 'javelin-request' => '94b750d2', 'javelin-resource' => '44959b73', 'javelin-routable' => 'b3e7d692', 'javelin-router' => '29274e2b', 'javelin-scrollbar' => '9065f639', 'javelin-sound' => '949c0fe5', 'javelin-stratcom' => '327f418a', 'javelin-tokenizer' => 'bb6e5c16', 'javelin-typeahead' => '70baed2f', 'javelin-typeahead-composite-source' => '503e17fd', 'javelin-typeahead-normalizer' => '185bbd53', 'javelin-typeahead-ondemand-source' => '013ffff9', 'javelin-typeahead-preloaded-source' => '54f314a0', 'javelin-typeahead-source' => 'ab9e0a82', 'javelin-typeahead-static-source' => '6c0e62fa', 'javelin-uri' => 'c989ade3', 'javelin-util' => '93cc50d6', 'javelin-vector' => '2caa8fb8', 'javelin-view' => '0f764c35', 'javelin-view-html' => 'fe287620', 'javelin-view-interpreter' => 'f829edb3', 'javelin-view-renderer' => '6c2b09a2', 'javelin-view-visitor' => 'efe49472', 'javelin-websocket' => '3ffe32d6', 'javelin-workboard-board' => '8935deef', 'javelin-workboard-card' => 'c587b80f', 'javelin-workboard-column' => '758b4758', 'javelin-workboard-controller' => '26167537', 'javelin-workflow' => '6a726c55', 'maniphest-report-css' => '9b9580b7', 'maniphest-task-edit-css' => 'fda62a9b', 'maniphest-task-summary-css' => '11cc5344', 'multirow-row-manager' => 'b5d57730', 'owners-path-editor' => 'c96502cf', 'owners-path-editor-css' => '9c136c29', 'paste-css' => '9fcc9773', 'path-typeahead' => '6d8c7912', 'people-picture-menu-item-css' => 'a06f7f34', 'people-profile-css' => '4df76faf', 'phabricator-action-list-view-css' => '0bcd9a45', 'phabricator-busy' => '59a7976a', 'phabricator-chatlog-css' => 'd295b020', 'phabricator-content-source-view-css' => '4b8b05d4', 'phabricator-core-css' => '62fa3ace', 'phabricator-countdown-css' => '16c52f5c', 'phabricator-darklog' => 'c8e1ffe3', 'phabricator-darkmessage' => 'c48cccdd', 'phabricator-dashboard-css' => 'fe5b1869', 'phabricator-diff-changeset' => 'b49b59d6', - 'phabricator-diff-changeset-list' => 'e0b984b5', + 'phabricator-diff-changeset-list' => '0a84bcc1', 'phabricator-diff-inline' => 'e83d28f3', 'phabricator-drag-and-drop-file-upload' => '58dea2fa', 'phabricator-draggable-list' => 'bea6e7f4', 'phabricator-fatal-config-template-css' => '8f18fa41', 'phabricator-favicon' => '1fe2510c', 'phabricator-feed-css' => 'ecd4ec57', 'phabricator-file-upload' => '680ea2c8', 'phabricator-filetree-view-css' => 'b912ad97', 'phabricator-flag-css' => 'bba8f811', 'phabricator-keyboard-shortcut' => '1ae869f2', 'phabricator-keyboard-shortcut-manager' => 'c19dd9b9', 'phabricator-main-menu-view' => '1802a242', 'phabricator-nav-view-css' => '694d7723', 'phabricator-notification' => '4f774dac', 'phabricator-notification-css' => '457861ec', 'phabricator-notification-menu-css' => 'ef480927', 'phabricator-object-selector-css' => '85ee8ce6', 'phabricator-phtize' => 'd254d646', 'phabricator-prefab' => '77b0ae28', 'phabricator-remarkup-css' => 'b182076e', 'phabricator-search-results-css' => '505dd8cf', 'phabricator-shaped-request' => '7cbe244b', 'phabricator-slowvote-css' => 'a94b7230', 'phabricator-source-code-view-css' => '2ab25dfa', 'phabricator-standard-page-view' => '34ee718b', 'phabricator-textareautils' => '320810c8', 'phabricator-title' => '485aaa6c', 'phabricator-tooltip' => '358b8c04', 'phabricator-ui-example-css' => '528b19de', 'phabricator-zindex-css' => '9d8f7c4b', 'phame-css' => '8cb3afcd', 'pholio-css' => 'ca89d380', 'pholio-edit-css' => '07676f51', 'pholio-inline-comments-css' => '8e545e49', 'phortune-credit-card-form' => '2290aeef', 'phortune-credit-card-form-css' => '8391eb02', 'phortune-css' => '5b99dae0', 'phortune-invoice-css' => '476055e2', 'phrequent-css' => 'ffc185ad', 'phriction-document-css' => '4282e4ad', 'phui-action-panel-css' => 'b4798122', 'phui-badge-view-css' => '22c0cf4f', 'phui-basic-nav-view-css' => '98c11ab3', 'phui-big-info-view-css' => 'acc3492c', 'phui-box-css' => '4bd6cdb9', 'phui-bulk-editor-css' => '9a81e5d5', 'phui-button-bar-css' => 'f1ff5494', 'phui-button-css' => '6ccb303c', 'phui-button-simple-css' => '8e1baf68', 'phui-calendar-css' => 'f1ddf11c', 'phui-calendar-day-css' => '572b1893', 'phui-calendar-list-css' => '576be600', 'phui-calendar-month-css' => '21154caf', 'phui-chart-css' => '6bf6f78e', 'phui-cms-css' => '504b4b23', 'phui-comment-form-css' => 'ac68149f', 'phui-comment-panel-css' => 'f50152ad', 'phui-crumbs-view-css' => '10728aaa', 'phui-curtain-view-css' => '2bdaf026', 'phui-document-summary-view-css' => '9ca48bdf', 'phui-document-view-css' => 'c4ac41f9', 'phui-document-view-pro-css' => 'dd79b5df', 'phui-feed-story-css' => '44a9c8e9', 'phui-font-icon-base-css' => '870a7360', 'phui-fontkit-css' => '1320ed01', 'phui-form-css' => '7aaa04e3', 'phui-form-view-css' => '2f43fae7', 'phui-head-thing-view-css' => 'fd311e5f', 'phui-header-view-css' => '1ba8b707', 'phui-hovercard' => '1bd28176', - 'phui-hovercard-view-css' => 'f0592bcf', + 'phui-hovercard-view-css' => '4a484541', 'phui-icon-set-selector-css' => '87db8fee', 'phui-icon-view-css' => 'cf24ceec', 'phui-image-mask-css' => 'a8498f9c', 'phui-info-view-css' => 'e929f98c', 'phui-inline-comment-view-css' => '65ae3bc2', 'phui-invisible-character-view-css' => '6993d9f0', 'phui-left-right-css' => '75227a4d', 'phui-lightbox-css' => '0a035e40', 'phui-list-view-css' => '38f8c9bd', 'phui-object-box-css' => '9cff003c', 'phui-oi-big-ui-css' => '628f59de', 'phui-oi-color-css' => 'cd2b9b77', 'phui-oi-drag-ui-css' => '08f4ccc3', 'phui-oi-flush-ui-css' => '9d9685d6', 'phui-oi-list-view-css' => '7c5c1291', 'phui-oi-simple-ui-css' => 'a8beebea', 'phui-pager-css' => 'edcbc226', 'phui-pinboard-view-css' => '2495140e', 'phui-property-list-view-css' => '546a04ae', 'phui-remarkup-preview-css' => '54a34863', 'phui-segment-bar-view-css' => 'b1d1b892', 'phui-spacing-css' => '042804d6', 'phui-status-list-view-css' => 'd5263e49', 'phui-tag-view-css' => 'b4719c50', 'phui-theme-css' => '9f261c6b', 'phui-timeline-view-css' => '6ddf8126', 'phui-two-column-view-css' => '44ec4951', 'phui-workboard-color-css' => '783cdff5', 'phui-workboard-view-css' => '3bc85455', 'phui-workcard-view-css' => 'cca5fa92', 'phui-workpanel-view-css' => 'a3a63478', 'phuix-action-list-view' => 'b5c256b8', 'phuix-action-view' => '8d4a8c72', 'phuix-autocomplete' => 'df1bbd34', 'phuix-button-view' => '85ac9772', 'phuix-dropdown-menu' => '04b2ae03', 'phuix-form-control-view' => '210a16c1', 'phuix-icon-view' => 'bff6884b', 'policy-css' => '957ea14c', 'policy-edit-css' => '815c66f7', 'policy-transaction-detail-css' => '82100a43', 'ponder-view-css' => 'fbd45f96', 'project-card-view-css' => '0010bb52', 'project-view-css' => '792c9057', 'releeph-core' => '9b3c5733', 'releeph-preview-branch' => 'b7a6f4a5', 'releeph-request-differential-create-dialog' => '8d8b92cd', 'releeph-request-typeahead-css' => '667a48ae', 'setup-issue-css' => '30ee0173', 'sprite-login-css' => '396f3c3a', 'sprite-tokens-css' => '9cdfd599', 'syntax-default-css' => '9923583c', 'syntax-highlighting-css' => 'e9c95dd4', 'tokens-css' => '3d0f239e', 'typeahead-browse-css' => 'f2818435', 'unhandled-exception-css' => '4c96257a', ), 'requires' => array( '00676f00' => array( 'javelin-install', 'javelin-dom', 'javelin-typeahead-preloaded-source', 'javelin-util', ), '013ffff9' => array( 'javelin-install', 'javelin-util', 'javelin-request', 'javelin-typeahead-source', ), '01fca1f0' => array( 'javelin-behavior', 'javelin-workflow', 'javelin-json', 'javelin-dom', 'phabricator-keyboard-shortcut', ), '0213259f' => array( 'javelin-behavior', 'javelin-uri', ), - '038bf27f' => array( - 'javelin-behavior', - 'javelin-stratcom', - 'javelin-workflow', - 'javelin-dom', - 'phuix-form-control-view', - 'phuix-icon-view', - 'javelin-behavior-phabricator-gesture', - ), '040fce04' => array( 'javelin-behavior', 'javelin-request', ), '04b2ae03' => array( 'javelin-install', 'javelin-util', 'javelin-dom', 'javelin-vector', 'javelin-stratcom', ), '051c7832' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), '05270951' => array( 'javelin-util', 'javelin-magical-init', ), '065227cc' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-workflow', ), '08f4ccc3' => array( 'phui-oi-list-view-css', ), '0a0b10e9' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), '0a3f3021' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', 'javelin-router', ), + '0a84bcc1' => array( + 'javelin-install', + 'phuix-button-view', + ), '0f764c35' => array( 'javelin-install', 'javelin-util', ), '15d5ff71' => array( 'aphront-typeahead-control-css', 'phui-tag-view-css', ), '1802a242' => array( 'phui-theme-css', ), '185bbd53' => array( 'javelin-install', ), '1ad0a787' => array( 'javelin-install', 'javelin-reactor', 'javelin-util', 'javelin-reactor-node-calmer', ), '1ae869f2' => array( 'javelin-install', 'javelin-util', 'phabricator-keyboard-shortcut-manager', ), '1bd28176' => array( 'javelin-install', 'javelin-dom', 'javelin-vector', 'javelin-request', 'javelin-uri', ), '1db13e70' => array( 'javelin-behavior', 'javelin-dom', 'javelin-json', 'javelin-workflow', 'javelin-magical-init', ), '1f6794f6' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', 'javelin-uri', 'phabricator-textareautils', ), '1fe2510c' => array( 'javelin-install', 'javelin-dom', ), '210a16c1' => array( 'javelin-install', 'javelin-dom', ), '2290aeef' => array( 'javelin-install', 'javelin-dom', 'javelin-json', 'javelin-workflow', 'javelin-util', ), 26167537 => array( 'javelin-install', 'javelin-dom', 'javelin-util', 'javelin-vector', 'javelin-stratcom', 'javelin-workflow', 'phabricator-drag-and-drop-file-upload', 'javelin-workboard-board', ), '27ca6289' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-uri', 'phabricator-notification', ), '2926fff2' => array( 'javelin-behavior', 'javelin-dom', ), '29274e2b' => array( 'javelin-install', 'javelin-util', ), '2ae077e1' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-behavior-device', 'javelin-scrollbar', 'javelin-quicksand', 'phabricator-keyboard-shortcut', 'conpherence-thread-manager', ), '2b8de964' => array( 'javelin-install', 'javelin-util', ), '2caa8fb8' => array( 'javelin-install', 'javelin-event', ), - '2ee659ce' => array( - 'javelin-install', - ), '31420f77' => array( 'javelin-behavior', ), '320810c8' => array( 'javelin-install', 'javelin-dom', 'javelin-vector', ), '327a00d1' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-workflow', ), '327f418a' => array( 'javelin-install', 'javelin-event', 'javelin-util', 'javelin-magical-init', ), '358b8c04' => array( 'javelin-install', 'javelin-util', 'javelin-dom', 'javelin-vector', ), '3935d8c4' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', ), '3ab51e2c' => array( 'javelin-behavior', 'javelin-behavior-device', 'javelin-stratcom', 'javelin-vector', 'javelin-dom', 'javelin-magical-init', ), '3cb0b2fc' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-workflow', 'javelin-util', 'javelin-uri', ), '3dbf94d5' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-workflow', 'javelin-stratcom', ), '3ffe32d6' => array( 'javelin-install', ), '4047cd35' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-stratcom', 'javelin-workflow', 'javelin-behavior-device', 'javelin-history', 'javelin-vector', 'javelin-scrollbar', 'phabricator-title', 'phabricator-shaped-request', 'conpherence-thread-manager', ), '408bf173' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-stratcom', 'javelin-workflow', 'phabricator-draggable-list', ), '4250a34e' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-vector', 'javelin-stratcom', 'javelin-workflow', 'javelin-workboard-controller', ), '44959b73' => array( 'javelin-util', 'javelin-uri', 'javelin-install', ), '453c5375' => array( 'javelin-behavior', 'javelin-dom', ), '464259a2' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), '469c0d9e' => array( 'javelin-behavior', 'javelin-dom', 'javelin-workflow', ), 47830651 => array( 'javelin-behavior', 'javelin-dom', 'javelin-view-renderer', 'javelin-install', ), 48086888 => array( 'javelin-behavior', 'javelin-dom', 'javelin-workflow', ), '484a6e22' => array( 'javelin-behavior', 'javelin-dom', 'phabricator-drag-and-drop-file-upload', 'phabricator-textareautils', ), '485aaa6c' => array( 'javelin-install', ), '491416b3' => array( 'javelin-behavior', 'javelin-uri', 'phabricator-notification', ), '4976858c' => array( 'javelin-magical-init', 'javelin-install', 'javelin-util', 'javelin-vector', 'javelin-stratcom', ), '4b3c4443' => array( 'phuix-icon-view', ), '4b700e9e' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'phabricator-shaped-request', ), '4c193c96' => array( 'javelin-behavior', 'javelin-uri', 'phabricator-notification', ), '4d863052' => array( 'javelin-dom', 'javelin-util', 'javelin-stratcom', 'javelin-install', 'javelin-aphlict', 'javelin-workflow', 'javelin-router', 'javelin-behavior-device', 'javelin-vector', ), '4e3e79a6' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), '4f774dac' => array( 'javelin-install', 'javelin-dom', 'javelin-stratcom', 'javelin-util', 'phabricator-notification-css', ), '503e17fd' => array( 'javelin-install', 'javelin-typeahead-source', 'javelin-util', ), '51c5ad07' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-request', 'javelin-util', 'phabricator-shaped-request', ), '522431f7' => array( 'javelin-behavior', 'javelin-util', 'javelin-dom', 'javelin-stratcom', 'javelin-vector', 'javelin-typeahead-static-source', ), '549459b8' => array( 'javelin-behavior', ), '54b612ba' => array( 'javelin-color', 'javelin-install', 'javelin-util', ), '54f314a0' => array( 'javelin-install', 'javelin-util', 'javelin-request', 'javelin-typeahead-source', ), '55616e04' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-workflow', 'javelin-stratcom', 'conpherence-thread-manager', ), '558829c2' => array( 'javelin-stratcom', 'javelin-behavior', 'javelin-vector', 'javelin-dom', ), '58dea2fa' => array( 'javelin-install', 'javelin-util', 'javelin-request', 'javelin-dom', 'javelin-uri', 'phabricator-file-upload', ), '599a8f5f' => array( 'javelin-behavior', 'javelin-aphlict', 'javelin-stratcom', 'javelin-request', 'javelin-uri', 'javelin-dom', 'javelin-json', 'javelin-router', 'javelin-util', 'javelin-leader', 'javelin-sound', 'phabricator-notification', ), '59a7976a' => array( 'javelin-install', 'javelin-dom', 'javelin-fx', ), '59b251eb' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-vector', 'javelin-dom', ), + '59e27e74' => array( + 'javelin-behavior', + 'javelin-stratcom', + 'javelin-workflow', + 'javelin-dom', + 'phuix-form-control-view', + 'phuix-icon-view', + 'javelin-behavior-phabricator-gesture', + ), '5c54cbf3' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), '5e2634b9' => array( 'javelin-behavior', 'javelin-aphlict', 'phabricator-phtize', 'javelin-dom', ), '5e9f347c' => array( 'javelin-behavior', 'multirow-row-manager', 'javelin-dom', 'javelin-util', 'phabricator-prefab', 'javelin-json', ), '60821bc7' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), '61cbc29a' => array( 'javelin-magical-init', 'javelin-util', ), '628f59de' => array( 'phui-oi-list-view-css', ), '62dfea03' => array( 'javelin-install', 'javelin-util', ), '635de1ec' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', ), 66888767 => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-util', 'javelin-dom', 'javelin-request', 'phabricator-keyboard-shortcut', 'phabricator-darklog', 'phabricator-darkmessage', ), '66a62306' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-history', ), '66a6def1' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'multirow-row-manager', 'javelin-json', 'phuix-form-control-view', ), '680ea2c8' => array( 'javelin-install', 'javelin-dom', 'phabricator-notification', ), '68af71ca' => array( 'javelin-install', 'javelin-dom', 'phuix-button-view', ), '69adf288' => array( 'javelin-install', ), '6a726c55' => array( 'javelin-stratcom', 'javelin-request', 'javelin-dom', 'javelin-vector', 'javelin-install', 'javelin-util', 'javelin-mask', 'javelin-uri', 'javelin-routable', ), '6b31879a' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-mask', 'javelin-util', 'phuix-icon-view', 'phabricator-busy', ), '6b8ef10b' => array( 'javelin-install', ), '6c0e62fa' => array( 'javelin-install', 'javelin-typeahead-source', ), '6c2b09a2' => array( 'javelin-install', 'javelin-util', ), '6d3e1947' => array( 'javelin-behavior', 'javelin-diffusion-locate-file-source', 'javelin-dom', 'javelin-typeahead', 'javelin-uri', ), '6d8c7912' => array( 'javelin-install', 'javelin-typeahead', 'javelin-dom', 'javelin-request', 'javelin-typeahead-ondemand-source', 'javelin-util', ), '70baed2f' => array( 'javelin-install', 'javelin-dom', 'javelin-vector', 'javelin-util', ), 71237763 => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-workflow', 'phabricator-draggable-list', ), '7319e029' => array( 'javelin-behavior', 'javelin-dom', ), '758b4758' => array( 'javelin-install', 'javelin-workboard-card', ), '75b83cbb' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', ), '76b9fc3e' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', 'phabricator-draggable-list', ), '76f4ebed' => array( 'javelin-install', 'javelin-reactor', 'javelin-util', ), '77b0ae28' => array( 'javelin-install', 'javelin-util', 'javelin-dom', 'javelin-typeahead', 'javelin-tokenizer', 'javelin-typeahead-preloaded-source', 'javelin-typeahead-ondemand-source', 'javelin-dom', 'javelin-stratcom', 'javelin-util', ), '77c1f0b0' => array( 'javelin-behavior', 'javelin-dom', 'javelin-request', 'javelin-util', ), '7927a7d3' => array( 'javelin-behavior', 'javelin-quicksand', ), '7a68dda3' => array( 'owners-path-editor', 'javelin-behavior', ), '7cbe244b' => array( 'javelin-install', 'javelin-util', 'javelin-request', 'javelin-router', ), '7e41274a' => array( 'javelin-install', ), '7ebaeed3' => array( 'herald-rule-editor', 'javelin-behavior', ), '7ee2b591' => array( 'javelin-behavior', 'javelin-history', ), '7f243deb' => array( 'javelin-install', ), '834a1173' => array( 'javelin-behavior', 'javelin-scrollbar', ), '8499b6ab' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', ), '85ac9772' => array( 'javelin-install', 'javelin-dom', ), '85ee8ce6' => array( 'aphront-dialog-view-css', ), '88236f00' => array( 'javelin-behavior', 'phabricator-keyboard-shortcut', 'javelin-stratcom', ), '8935deef' => array( 'javelin-install', 'javelin-dom', 'javelin-util', 'javelin-stratcom', 'javelin-workflow', 'phabricator-draggable-list', 'javelin-workboard-column', ), '8a41885b' => array( 'javelin-install', 'javelin-dom', ), '8ce821c5' => array( 'phabricator-notification', 'javelin-stratcom', 'javelin-behavior', ), '8d4a8c72' => array( 'javelin-install', 'javelin-dom', 'javelin-util', ), '8e1baf68' => array( 'phui-button-css', ), '8f29b364' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'phabricator-busy', ), '8ff5e24c' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), '901935ef' => array( 'javelin-behavior', 'javelin-dom', 'javelin-request', ), '9065f639' => array( 'javelin-install', 'javelin-dom', 'javelin-stratcom', 'javelin-vector', ), '92b9ec77' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), '93d0c9e3' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', ), '949c0fe5' => array( 'javelin-install', ), '94b750d2' => array( 'javelin-install', 'javelin-stratcom', 'javelin-util', 'javelin-behavior', 'javelin-json', 'javelin-dom', 'javelin-resource', 'javelin-routable', ), '960f6a39' => array( 'javelin-behavior', 'javelin-dom', 'javelin-uri', 'javelin-mask', 'phabricator-drag-and-drop-file-upload', ), '9a860428' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-uri', ), '9bbf3762' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-workflow', 'javelin-stratcom', ), '9d32bc88' => array( 'javelin-behavior', 'javelin-behavior-device', 'javelin-stratcom', 'javelin-dom', 'javelin-magical-init', 'javelin-vector', 'javelin-request', 'javelin-util', ), '9d9685d6' => array( 'phui-oi-list-view-css', ), '9f36c42d' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-vector', ), 'a0b57eb8' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-workflow', 'javelin-util', 'phabricator-keyboard-shortcut', ), 'a3714c76' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-vector', 'javelin-install', ), 'a3a63478' => array( 'phui-workcard-view-css', ), 'a464fe03' => array( 'javelin-behavior', 'javelin-uri', 'phabricator-notification', ), 'a6b98425' => array( 'javelin-behavior', 'javelin-dom', 'phortune-credit-card-form', ), 'a6f7a73b' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), 'a80d0378' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), 'a8beebea' => array( 'phui-oi-list-view-css', ), 'a8d8459d' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', ), 'a8da01f0' => array( 'javelin-behavior', 'javelin-uri', 'phabricator-keyboard-shortcut', ), 'a9f88de2' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-workflow', 'javelin-fx', 'javelin-util', ), 'ab2f381b' => array( 'javelin-request', 'javelin-behavior', 'javelin-dom', 'javelin-router', 'javelin-util', 'phabricator-busy', ), 'ab9e0a82' => array( 'javelin-install', 'javelin-util', 'javelin-dom', 'javelin-typeahead-normalizer', ), 'acd29eee' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'phabricator-phtize', 'phabricator-textareautils', 'javelin-workflow', 'javelin-vector', 'phuix-autocomplete', 'javelin-mask', ), 'ad54037e' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-util', ), 'b003d4fb' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'phuix-dropdown-menu', ), 'b0b8f86d' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', ), 'b23b49e6' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-request', 'phabricator-shaped-request', ), 'b2b4fbaf' => array( 'javelin-behavior', 'javelin-dom', 'javelin-uri', 'javelin-request', ), 'b3a4b884' => array( 'javelin-behavior', 'phabricator-prefab', ), 'b3e7d692' => array( 'javelin-install', ), 'b49b59d6' => array( 'javelin-dom', 'javelin-util', 'javelin-stratcom', 'javelin-install', 'javelin-workflow', 'javelin-router', 'javelin-behavior-device', 'javelin-vector', 'phabricator-diff-inline', ), 'b59e1e96' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', 'phabricator-draggable-list', ), 'b5c256b8' => array( 'javelin-install', 'javelin-dom', ), 'b5d57730' => array( 'javelin-install', 'javelin-stratcom', 'javelin-dom', 'javelin-util', ), 'b6993408' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-json', 'phabricator-draggable-list', ), 'b95d6f7d' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'phuix-dropdown-menu', ), 'ba158207' => array( 'javelin-behavior', 'javelin-uri', 'phabricator-notification', ), 'bb6e5c16' => array( 'javelin-dom', 'javelin-util', 'javelin-stratcom', 'javelin-install', ), 'bcaccd64' => array( 'javelin-behavior', 'javelin-behavior-device', 'javelin-stratcom', 'javelin-vector', 'phui-hovercard', ), 'bdaf4d04' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-request', ), 'bea6e7f4' => array( 'javelin-install', 'javelin-dom', 'javelin-stratcom', 'javelin-util', 'javelin-vector', 'javelin-magical-init', ), 'bee502c8' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-workflow', 'javelin-quicksand', 'phabricator-phtize', 'phabricator-drag-and-drop-file-upload', 'phabricator-draggable-list', ), 'bf5374ef' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), 'bff6884b' => array( 'javelin-install', 'javelin-dom', ), 'c19dd9b9' => array( 'javelin-install', 'javelin-util', 'javelin-stratcom', 'javelin-dom', 'javelin-vector', ), 'c3e917d9' => array( 'javelin-behavior', 'javelin-typeahead-ondemand-source', 'javelin-typeahead', 'javelin-dom', 'javelin-uri', 'javelin-util', 'javelin-stratcom', 'phabricator-prefab', 'phuix-icon-view', ), 'c420b0b9' => array( 'javelin-behavior', 'javelin-behavior-device', 'javelin-stratcom', 'phabricator-tooltip', ), 'c587b80f' => array( 'javelin-install', ), 'c7ccd872' => array( 'phui-fontkit-css', ), 'c90a04fc' => array( 'javelin-dom', 'javelin-dynval', 'javelin-reactor', 'javelin-reactornode', 'javelin-install', 'javelin-util', ), 'c96502cf' => array( 'multirow-row-manager', 'javelin-install', 'path-typeahead', 'javelin-dom', 'javelin-util', 'phabricator-prefab', 'phuix-form-control-view', ), 'c989ade3' => array( 'javelin-install', 'javelin-util', 'javelin-stratcom', ), 'caade6f2' => array( 'javelin-behavior', 'javelin-request', 'javelin-stratcom', 'javelin-vector', 'javelin-dom', 'javelin-uri', 'javelin-behavior-device', 'phabricator-title', 'phabricator-favicon', ), 'cd2b9b77' => array( 'phui-oi-list-view-css', ), 'd057e45a' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-workflow', 'javelin-util', 'phabricator-notification', 'conpherence-thread-manager', ), 'd0c516d5' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'phuix-dropdown-menu', 'phuix-action-list-view', 'phuix-action-view', 'javelin-workflow', 'phuix-icon-view', ), 'd254d646' => array( 'javelin-util', ), 'd4505101' => array( 'javelin-stratcom', 'javelin-install', 'javelin-uri', 'javelin-util', ), 'd4eecc63' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', ), 'd7a74243' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', 'phabricator-draggable-list', ), 'd835b03a' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'phabricator-shaped-request', ), 'db34a142' => array( 'phui-inline-comment-view-css', ), 'dca75c0e' => array( 'multirow-row-manager', 'javelin-install', 'javelin-util', 'javelin-dom', 'javelin-stratcom', 'javelin-json', 'phabricator-prefab', ), 'de2e896f' => array( 'javelin-behavior', 'javelin-dom', 'javelin-typeahead', 'javelin-typeahead-ondemand-source', 'javelin-dom', ), 'df1bbd34' => array( 'javelin-install', 'javelin-dom', 'phuix-icon-view', 'phabricator-prefab', ), - 'e0b984b5' => array( - 'javelin-install', - 'phuix-button-view', - ), 'e1d25dfb' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', 'phabricator-draggable-list', ), 'e1d4b11a' => array( 'javelin-install', 'javelin-util', 'javelin-websocket', 'javelin-leader', 'javelin-json', ), 'e1ff79b1' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), 'e2e0a072' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', 'phabricator-draggable-list', ), 'e379b58e' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-vector', 'javelin-dom', 'javelin-uri', ), 'e4232876' => array( 'javelin-behavior', 'javelin-dom', 'javelin-vector', 'phui-chart-css', ), 'e4cc26b3' => array( 'javelin-behavior', 'javelin-dom', ), 'e83d28f3' => array( 'javelin-dom', ), 'e9581f08' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', 'phabricator-draggable-list', ), 'e9c95dd4' => array( 'syntax-default-css', ), 'ec1f3669' => array( 'javelin-behavior', 'javelin-util', 'javelin-stratcom', 'javelin-dom', 'javelin-vector', 'javelin-magical-init', 'javelin-request', 'javelin-history', 'javelin-workflow', 'javelin-mask', 'javelin-behavior-device', 'phabricator-keyboard-shortcut', ), 'ecf4e799' => array( 'javelin-behavior', 'javelin-util', 'javelin-dom', 'javelin-stratcom', 'javelin-vector', ), 'edf8a145' => array( 'javelin-behavior', 'javelin-uri', ), + 'ef7e057f' => array( + 'javelin-install', + ), 'efe49472' => array( 'javelin-install', 'javelin-util', ), 'f01586dc' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-workflow', 'javelin-json', ), 'f0eb6708' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'phabricator-tooltip', 'phabricator-diff-changeset-list', 'phabricator-diff-changeset', ), 'f1ff5494' => array( 'phui-button-css', 'phui-button-simple-css', ), 'f50152ad' => array( 'phui-timeline-view-css', ), 'f6555212' => array( 'javelin-install', 'javelin-reactornode', 'javelin-util', 'javelin-reactor', ), 'f829edb3' => array( 'javelin-view', 'javelin-install', 'javelin-dom', ), 'fc91ab6c' => array( 'javelin-behavior', 'javelin-dom', 'phortune-credit-card-form', ), 'fe287620' => array( 'javelin-install', 'javelin-dom', 'javelin-view-visitor', 'javelin-util', ), ), 'packages' => array( 'conpherence.pkg.css' => array( 'conpherence-durable-column-view', 'conpherence-menu-css', 'conpherence-color-css', 'conpherence-message-pane-css', 'conpherence-notification-css', 'conpherence-transaction-css', 'conpherence-participant-pane-css', 'conpherence-header-pane-css', ), 'conpherence.pkg.js' => array( 'javelin-behavior-conpherence-menu', 'javelin-behavior-conpherence-participant-pane', 'javelin-behavior-conpherence-pontificate', 'javelin-behavior-toggle-widget', ), 'core.pkg.css' => array( 'phabricator-core-css', 'phabricator-zindex-css', 'phui-button-css', 'phui-button-simple-css', 'phui-theme-css', 'phabricator-standard-page-view', 'aphront-dialog-view-css', 'phui-form-view-css', 'aphront-panel-view-css', 'aphront-table-view-css', 'aphront-tokenizer-control-css', 'aphront-typeahead-control-css', 'aphront-list-filter-view-css', 'application-search-view-css', 'phabricator-remarkup-css', 'syntax-highlighting-css', 'syntax-default-css', 'phui-pager-css', 'aphront-tooltip-css', 'phabricator-flag-css', 'phui-info-view-css', 'phabricator-main-menu-view', 'phabricator-notification-css', 'phabricator-notification-menu-css', 'phui-lightbox-css', 'phui-comment-panel-css', 'phui-header-view-css', 'phabricator-nav-view-css', 'phui-basic-nav-view-css', 'phui-crumbs-view-css', 'phui-oi-list-view-css', 'phui-oi-color-css', 'phui-oi-big-ui-css', 'phui-oi-drag-ui-css', 'phui-oi-simple-ui-css', 'phui-oi-flush-ui-css', 'global-drag-and-drop-css', 'phui-spacing-css', 'phui-form-css', 'phui-icon-view-css', 'phabricator-action-list-view-css', 'phui-property-list-view-css', 'phui-tag-view-css', 'phui-list-view-css', 'font-fontawesome', 'font-lato', 'phui-font-icon-base-css', 'phui-fontkit-css', 'phui-box-css', 'phui-object-box-css', 'phui-timeline-view-css', 'phui-two-column-view-css', 'phui-curtain-view-css', 'sprite-login-css', 'sprite-tokens-css', 'tokens-css', 'auth-css', 'phui-status-list-view-css', 'phui-feed-story-css', 'phabricator-feed-css', 'phabricator-dashboard-css', 'aphront-multi-column-view-css', ), 'core.pkg.js' => array( 'javelin-util', 'javelin-install', 'javelin-event', 'javelin-stratcom', 'javelin-behavior', 'javelin-resource', 'javelin-request', 'javelin-vector', 'javelin-dom', 'javelin-json', 'javelin-uri', 'javelin-workflow', 'javelin-mask', 'javelin-typeahead', 'javelin-typeahead-normalizer', 'javelin-typeahead-source', 'javelin-typeahead-preloaded-source', 'javelin-typeahead-ondemand-source', 'javelin-tokenizer', 'javelin-history', 'javelin-router', 'javelin-routable', 'javelin-behavior-aphront-basic-tokenizer', 'javelin-behavior-workflow', 'javelin-behavior-aphront-form-disable-on-submit', 'phabricator-keyboard-shortcut-manager', 'phabricator-keyboard-shortcut', 'javelin-behavior-phabricator-keyboard-shortcuts', 'javelin-behavior-refresh-csrf', 'javelin-behavior-phabricator-watch-anchor', 'javelin-behavior-phabricator-autofocus', 'phuix-dropdown-menu', 'phuix-action-list-view', 'phuix-action-view', 'phuix-icon-view', 'phabricator-phtize', 'javelin-behavior-phabricator-oncopy', 'phabricator-tooltip', 'javelin-behavior-phabricator-tooltips', 'phabricator-prefab', 'javelin-behavior-device', 'javelin-behavior-toggle-class', 'javelin-behavior-lightbox-attachments', 'phabricator-busy', 'javelin-sound', 'javelin-aphlict', 'phabricator-notification', 'javelin-behavior-aphlict-listen', 'javelin-behavior-phabricator-search-typeahead', 'javelin-behavior-aphlict-dropdown', 'javelin-behavior-history-install', 'javelin-behavior-phabricator-gesture', 'javelin-behavior-phabricator-active-nav', 'javelin-behavior-phabricator-nav', 'javelin-behavior-phabricator-remarkup-assist', 'phabricator-textareautils', 'phabricator-file-upload', 'javelin-behavior-global-drag-and-drop', 'javelin-behavior-phabricator-reveal-content', 'phui-hovercard', 'javelin-behavior-phui-hovercards', 'javelin-color', 'javelin-fx', 'phabricator-draggable-list', 'javelin-behavior-phabricator-transaction-list', 'javelin-behavior-phabricator-show-older-transactions', 'javelin-behavior-phui-dropdown-menu', 'javelin-behavior-doorkeeper-tag', 'phabricator-title', 'javelin-leader', 'javelin-websocket', 'javelin-behavior-dashboard-async-panel', 'javelin-behavior-dashboard-tab-panel', 'javelin-quicksand', 'javelin-behavior-quicksand-blacklist', 'javelin-behavior-high-security-warning', 'javelin-behavior-read-only-warning', 'javelin-scrollbar', 'javelin-behavior-scrollbar', 'javelin-behavior-durable-column', 'conpherence-thread-manager', 'javelin-behavior-detect-timezone', 'javelin-behavior-setup-check-https', 'javelin-behavior-aphlict-status', 'javelin-behavior-user-menu', 'phabricator-favicon', ), 'differential.pkg.css' => array( 'differential-core-view-css', 'differential-changeset-view-css', 'differential-revision-history-css', 'differential-revision-list-css', 'differential-table-of-contents-css', 'differential-revision-comment-css', 'differential-revision-add-comment-css', 'phabricator-object-selector-css', 'phabricator-content-source-view-css', 'inline-comment-summary-css', 'phui-inline-comment-view-css', 'phabricator-filetree-view-css', ), 'differential.pkg.js' => array( 'phabricator-drag-and-drop-file-upload', 'phabricator-shaped-request', 'javelin-behavior-differential-feedback-preview', 'javelin-behavior-differential-populate', 'javelin-behavior-differential-diff-radios', 'javelin-behavior-aphront-drag-and-drop-textarea', 'javelin-behavior-phabricator-object-selector', 'javelin-behavior-repository-crossreference', 'javelin-behavior-differential-user-select', 'javelin-behavior-aphront-more', 'phabricator-diff-inline', 'phabricator-diff-changeset', 'phabricator-diff-changeset-list', ), 'diffusion.pkg.css' => array( 'diffusion-icons-css', ), 'diffusion.pkg.js' => array( 'javelin-behavior-diffusion-pull-lastmodified', 'javelin-behavior-diffusion-commit-graph', 'javelin-behavior-audit-preview', ), 'maniphest.pkg.css' => array( 'maniphest-task-summary-css', ), 'maniphest.pkg.js' => array( 'javelin-behavior-maniphest-batch-selector', 'javelin-behavior-maniphest-subpriority-editor', 'javelin-behavior-maniphest-list-editor', ), ), ); diff --git a/resources/sql/autopatches/20140805.boardcol.2.php b/resources/sql/autopatches/20140805.boardcol.2.php index 317de4e370..40d6c46ec2 100644 --- a/resources/sql/autopatches/20140805.boardcol.2.php +++ b/resources/sql/autopatches/20140805.boardcol.2.php @@ -1,53 +1,53 @@ establishConnection('w'); $rows = queryfx_all( $conn_w, 'SELECT src, dst FROM %T WHERE type = %d', PhabricatorEdgeConfig::TABLE_NAME_EDGE, $type_has_object); $cols = array(); foreach ($rows as $row) { $cols[$row['src']][] = $row['dst']; } $sql = array(); foreach ($cols as $col_phid => $obj_phids) { echo pht("Migrating column '%s'...", $col_phid)."\n"; $column = id(new PhabricatorProjectColumn())->loadOneWhere( 'phid = %s', $col_phid); if (!$column) { echo pht("Column '%s' does not exist.", $col_phid)."\n"; continue; } $sequence = 0; foreach ($obj_phids as $obj_phid) { $sql[] = qsprintf( $conn_w, '(%s, %s, %s, %d)', $column->getProjectPHID(), $column->getPHID(), $obj_phid, $sequence++); } } echo pht('Inserting rows...')."\n"; foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) { queryfx( $conn_w, 'INSERT INTO %T (boardPHID, columnPHID, objectPHID, sequence) - VALUES %Q', + VALUES %LQ', id(new PhabricatorProjectColumnPosition())->getTableName(), $chunk); } echo pht('Done.')."\n"; diff --git a/resources/sql/autopatches/20180910.audit.03.status.php b/resources/sql/autopatches/20180910.audit.03.status.php index b7a0bf9f58..ab42d196f0 100644 --- a/resources/sql/autopatches/20180910.audit.03.status.php +++ b/resources/sql/autopatches/20180910.audit.03.status.php @@ -1,28 +1,22 @@ establishConnection('w'); $status_map = array( 0 => 'none', 1 => 'needs-audit', 2 => 'concern-raised', 3 => 'partially-audited', 4 => 'audited', 5 => 'needs-verification', ); -foreach (new LiskMigrationIterator($table) as $commit) { - $status = $commit->getAuditStatus(); - - if (!isset($status_map[$status])) { - continue; - } - +foreach ($status_map as $old_status => $new_status) { queryfx( $conn, - 'UPDATE %T SET auditStatus = %s WHERE id = %d', - $table->getTableName(), - $status_map[$status], - $commit->getID()); + 'UPDATE %R SET auditStatus = %s WHERE auditStatus = %s', + $table, + $new_status, + $old_status); } diff --git a/resources/sql/autopatches/20180914.audit.01.mailkey.php b/resources/sql/autopatches/20180914.audit.01.mailkey.php index de8419a3c4..60926857ee 100644 --- a/resources/sql/autopatches/20180914.audit.01.mailkey.php +++ b/resources/sql/autopatches/20180914.audit.01.mailkey.php @@ -1,26 +1,34 @@ establishConnection('w'); $commit_name = $commit_table->getTableName(); $properties_table = new PhabricatorMetaMTAMailProperties(); $conn = $properties_table->establishConnection('w'); $iterator = new LiskRawMigrationIterator($commit_conn, $commit_name); -foreach ($iterator as $commit) { +$chunks = new PhutilChunkedIterator($iterator, 100); +foreach ($chunks as $chunk) { + $sql = array(); + foreach ($chunk as $commit) { + $sql[] = qsprintf( + $conn, + '(%s, %s, %d, %d)', + $commit['phid'], + phutil_json_encode( + array( + 'mailKey' => $commit['mailKey'], + )), + PhabricatorTime::getNow(), + PhabricatorTime::getNow()); + } + queryfx( $conn, - 'INSERT IGNORE INTO %T + 'INSERT IGNORE INTO %R (objectPHID, mailProperties, dateCreated, dateModified) - VALUES - (%s, %s, %d, %d)', - $properties_table->getTableName(), - $commit['phid'], - phutil_json_encode( - array( - 'mailKey' => $commit['mailKey'], - )), - PhabricatorTime::getNow(), - PhabricatorTime::getNow()); + VALUES %LQ', + $properties_table, + $sql); } diff --git a/resources/sql/patches/20130820.file-mailkey-populate.php b/resources/sql/patches/20130820.file-mailkey-populate.php index ba4d6d1606..0db10bef58 100644 --- a/resources/sql/patches/20130820.file-mailkey-populate.php +++ b/resources/sql/patches/20130820.file-mailkey-populate.php @@ -1,38 +1,38 @@ getTableName(); $conn_w = $table->establishConnection('w'); $conn_w->openTransaction(); $sql = array(); foreach (new LiskRawMigrationIterator($conn_w, 'file') as $row) { // NOTE: MySQL requires that the INSERT specify all columns which don't // have default values when configured in strict mode. This query will // never actually insert rows, but we need to hand it values anyway. $sql[] = qsprintf( $conn_w, '(%d, %s, 0, 0, 0, 0, 0, 0, 0, 0)', $row['id'], Filesystem::readRandomCharacters(20)); } if ($sql) { - foreach (PhabricatorLiskDAO::chunkSQL($sql, ', ') as $chunk) { + foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) { queryfx( $conn_w, 'INSERT INTO %T (id, mailKey, phid, byteSize, storageEngine, storageFormat, - storageHandle, dateCreated, dateModified, metadata) VALUES %Q '. + storageHandle, dateCreated, dateModified, metadata) VALUES %LQ '. 'ON DUPLICATE KEY UPDATE mailKey = VALUES(mailKey)', $table_name, $chunk); } } $table->saveTransaction(); echo pht('Done.')."\n"; diff --git a/resources/sql/patches/20131106.diffphid.2.mig.php b/resources/sql/patches/20131106.diffphid.2.mig.php index 67fd14aad0..7976c910eb 100644 --- a/resources/sql/patches/20131106.diffphid.2.mig.php +++ b/resources/sql/patches/20131106.diffphid.2.mig.php @@ -1,47 +1,47 @@ establishConnection('w'); $size = 1000; $row_iter = id(new LiskMigrationIterator($diff_table))->setPageSize($size); $chunk_iter = new PhutilChunkedIterator($row_iter, $size); foreach ($chunk_iter as $chunk) { $sql = array(); foreach ($chunk as $diff) { $id = $diff->getID(); echo pht('Migrating diff ID %d...', $id)."\n"; $phid = $diff->getPHID(); if (strlen($phid)) { continue; } $type_diff = DifferentialDiffPHIDType::TYPECONST; $new_phid = PhabricatorPHID::generateNewPHID($type_diff); $sql[] = qsprintf( $conn_w, '(%d, %s)', $id, $new_phid); } if (!$sql) { continue; } - foreach (PhabricatorLiskDAO::chunkSQL($sql, ', ') as $sql_chunk) { + foreach (PhabricatorLiskDAO::chunkSQL($sql) as $sql_chunk) { queryfx( $conn_w, - 'INSERT IGNORE INTO %T (id, phid) VALUES %Q + 'INSERT IGNORE INTO %T (id, phid) VALUES %LQ ON DUPLICATE KEY UPDATE phid = VALUES(phid)', $diff_table->getTableName(), $sql_chunk); } } echo pht('Done.')."\n"; diff --git a/scripts/setup/manage_herald.php b/scripts/setup/manage_herald.php new file mode 100755 index 0000000000..4ebd94f820 --- /dev/null +++ b/scripts/setup/manage_herald.php @@ -0,0 +1,21 @@ +#!/usr/bin/env php +setTagline(pht('manage Herald')); +$args->setSynopsis(<<parseStandardArguments(); + +$workflows = id(new PhutilClassMapQuery()) + ->setAncestorClass('HeraldManagementWorkflow') + ->execute(); +$workflows[] = new PhutilHelpArgumentWorkflow(); +$args->parseWorkflows($workflows); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 4c784571b5..e4ffb74b0f 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1,11438 +1,11514 @@ 2, 'class' => array( 'AlmanacAddress' => 'applications/almanac/util/AlmanacAddress.php', 'AlmanacBinding' => 'applications/almanac/storage/AlmanacBinding.php', 'AlmanacBindingDeletePropertyTransaction' => 'applications/almanac/xaction/AlmanacBindingDeletePropertyTransaction.php', 'AlmanacBindingDisableController' => 'applications/almanac/controller/AlmanacBindingDisableController.php', 'AlmanacBindingDisableTransaction' => 'applications/almanac/xaction/AlmanacBindingDisableTransaction.php', 'AlmanacBindingEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacBindingEditConduitAPIMethod.php', 'AlmanacBindingEditController' => 'applications/almanac/controller/AlmanacBindingEditController.php', 'AlmanacBindingEditEngine' => 'applications/almanac/editor/AlmanacBindingEditEngine.php', 'AlmanacBindingEditor' => 'applications/almanac/editor/AlmanacBindingEditor.php', 'AlmanacBindingInterfaceTransaction' => 'applications/almanac/xaction/AlmanacBindingInterfaceTransaction.php', 'AlmanacBindingPHIDType' => 'applications/almanac/phid/AlmanacBindingPHIDType.php', 'AlmanacBindingPropertyEditEngine' => 'applications/almanac/editor/AlmanacBindingPropertyEditEngine.php', 'AlmanacBindingQuery' => 'applications/almanac/query/AlmanacBindingQuery.php', 'AlmanacBindingSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacBindingSearchConduitAPIMethod.php', 'AlmanacBindingSearchEngine' => 'applications/almanac/query/AlmanacBindingSearchEngine.php', 'AlmanacBindingServiceTransaction' => 'applications/almanac/xaction/AlmanacBindingServiceTransaction.php', 'AlmanacBindingSetPropertyTransaction' => 'applications/almanac/xaction/AlmanacBindingSetPropertyTransaction.php', 'AlmanacBindingTableView' => 'applications/almanac/view/AlmanacBindingTableView.php', 'AlmanacBindingTransaction' => 'applications/almanac/storage/AlmanacBindingTransaction.php', 'AlmanacBindingTransactionQuery' => 'applications/almanac/query/AlmanacBindingTransactionQuery.php', 'AlmanacBindingTransactionType' => 'applications/almanac/xaction/AlmanacBindingTransactionType.php', 'AlmanacBindingViewController' => 'applications/almanac/controller/AlmanacBindingViewController.php', 'AlmanacBindingsSearchEngineAttachment' => 'applications/almanac/engineextension/AlmanacBindingsSearchEngineAttachment.php', 'AlmanacCacheEngineExtension' => 'applications/almanac/engineextension/AlmanacCacheEngineExtension.php', 'AlmanacClusterDatabaseServiceType' => 'applications/almanac/servicetype/AlmanacClusterDatabaseServiceType.php', 'AlmanacClusterRepositoryServiceType' => 'applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php', 'AlmanacClusterServiceType' => 'applications/almanac/servicetype/AlmanacClusterServiceType.php', 'AlmanacConsoleController' => 'applications/almanac/controller/AlmanacConsoleController.php', 'AlmanacController' => 'applications/almanac/controller/AlmanacController.php', 'AlmanacCreateDevicesCapability' => 'applications/almanac/capability/AlmanacCreateDevicesCapability.php', 'AlmanacCreateNamespacesCapability' => 'applications/almanac/capability/AlmanacCreateNamespacesCapability.php', 'AlmanacCreateNetworksCapability' => 'applications/almanac/capability/AlmanacCreateNetworksCapability.php', 'AlmanacCreateServicesCapability' => 'applications/almanac/capability/AlmanacCreateServicesCapability.php', 'AlmanacCustomServiceType' => 'applications/almanac/servicetype/AlmanacCustomServiceType.php', 'AlmanacDAO' => 'applications/almanac/storage/AlmanacDAO.php', 'AlmanacDeletePropertyEditField' => 'applications/almanac/engineextension/AlmanacDeletePropertyEditField.php', 'AlmanacDeletePropertyEditType' => 'applications/almanac/engineextension/AlmanacDeletePropertyEditType.php', 'AlmanacDevice' => 'applications/almanac/storage/AlmanacDevice.php', 'AlmanacDeviceController' => 'applications/almanac/controller/AlmanacDeviceController.php', 'AlmanacDeviceDeletePropertyTransaction' => 'applications/almanac/xaction/AlmanacDeviceDeletePropertyTransaction.php', 'AlmanacDeviceEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacDeviceEditConduitAPIMethod.php', 'AlmanacDeviceEditController' => 'applications/almanac/controller/AlmanacDeviceEditController.php', 'AlmanacDeviceEditEngine' => 'applications/almanac/editor/AlmanacDeviceEditEngine.php', 'AlmanacDeviceEditor' => 'applications/almanac/editor/AlmanacDeviceEditor.php', 'AlmanacDeviceListController' => 'applications/almanac/controller/AlmanacDeviceListController.php', 'AlmanacDeviceNameNgrams' => 'applications/almanac/storage/AlmanacDeviceNameNgrams.php', 'AlmanacDeviceNameTransaction' => 'applications/almanac/xaction/AlmanacDeviceNameTransaction.php', 'AlmanacDevicePHIDType' => 'applications/almanac/phid/AlmanacDevicePHIDType.php', 'AlmanacDevicePropertyEditEngine' => 'applications/almanac/editor/AlmanacDevicePropertyEditEngine.php', 'AlmanacDeviceQuery' => 'applications/almanac/query/AlmanacDeviceQuery.php', 'AlmanacDeviceSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacDeviceSearchConduitAPIMethod.php', 'AlmanacDeviceSearchEngine' => 'applications/almanac/query/AlmanacDeviceSearchEngine.php', 'AlmanacDeviceSetPropertyTransaction' => 'applications/almanac/xaction/AlmanacDeviceSetPropertyTransaction.php', 'AlmanacDeviceTransaction' => 'applications/almanac/storage/AlmanacDeviceTransaction.php', 'AlmanacDeviceTransactionQuery' => 'applications/almanac/query/AlmanacDeviceTransactionQuery.php', 'AlmanacDeviceTransactionType' => 'applications/almanac/xaction/AlmanacDeviceTransactionType.php', 'AlmanacDeviceViewController' => 'applications/almanac/controller/AlmanacDeviceViewController.php', 'AlmanacDrydockPoolServiceType' => 'applications/almanac/servicetype/AlmanacDrydockPoolServiceType.php', 'AlmanacEditor' => 'applications/almanac/editor/AlmanacEditor.php', 'AlmanacInterface' => 'applications/almanac/storage/AlmanacInterface.php', 'AlmanacInterfaceAddressTransaction' => 'applications/almanac/xaction/AlmanacInterfaceAddressTransaction.php', 'AlmanacInterfaceDatasource' => 'applications/almanac/typeahead/AlmanacInterfaceDatasource.php', 'AlmanacInterfaceDeleteController' => 'applications/almanac/controller/AlmanacInterfaceDeleteController.php', 'AlmanacInterfaceDestroyTransaction' => 'applications/almanac/xaction/AlmanacInterfaceDestroyTransaction.php', 'AlmanacInterfaceDeviceTransaction' => 'applications/almanac/xaction/AlmanacInterfaceDeviceTransaction.php', 'AlmanacInterfaceEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacInterfaceEditConduitAPIMethod.php', 'AlmanacInterfaceEditController' => 'applications/almanac/controller/AlmanacInterfaceEditController.php', 'AlmanacInterfaceEditEngine' => 'applications/almanac/editor/AlmanacInterfaceEditEngine.php', 'AlmanacInterfaceEditor' => 'applications/almanac/editor/AlmanacInterfaceEditor.php', 'AlmanacInterfaceNetworkTransaction' => 'applications/almanac/xaction/AlmanacInterfaceNetworkTransaction.php', 'AlmanacInterfacePHIDType' => 'applications/almanac/phid/AlmanacInterfacePHIDType.php', 'AlmanacInterfacePortTransaction' => 'applications/almanac/xaction/AlmanacInterfacePortTransaction.php', 'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php', 'AlmanacInterfaceSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacInterfaceSearchConduitAPIMethod.php', 'AlmanacInterfaceSearchEngine' => 'applications/almanac/query/AlmanacInterfaceSearchEngine.php', 'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php', 'AlmanacInterfaceTransaction' => 'applications/almanac/storage/AlmanacInterfaceTransaction.php', 'AlmanacInterfaceTransactionType' => 'applications/almanac/xaction/AlmanacInterfaceTransactionType.php', 'AlmanacKeys' => 'applications/almanac/util/AlmanacKeys.php', 'AlmanacManageClusterServicesCapability' => 'applications/almanac/capability/AlmanacManageClusterServicesCapability.php', 'AlmanacManagementRegisterWorkflow' => 'applications/almanac/management/AlmanacManagementRegisterWorkflow.php', 'AlmanacManagementTrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php', 'AlmanacManagementUntrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php', 'AlmanacManagementWorkflow' => 'applications/almanac/management/AlmanacManagementWorkflow.php', 'AlmanacModularTransaction' => 'applications/almanac/storage/AlmanacModularTransaction.php', 'AlmanacNames' => 'applications/almanac/util/AlmanacNames.php', 'AlmanacNamesTestCase' => 'applications/almanac/util/__tests__/AlmanacNamesTestCase.php', 'AlmanacNamespace' => 'applications/almanac/storage/AlmanacNamespace.php', 'AlmanacNamespaceController' => 'applications/almanac/controller/AlmanacNamespaceController.php', 'AlmanacNamespaceEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacNamespaceEditConduitAPIMethod.php', 'AlmanacNamespaceEditController' => 'applications/almanac/controller/AlmanacNamespaceEditController.php', 'AlmanacNamespaceEditEngine' => 'applications/almanac/editor/AlmanacNamespaceEditEngine.php', 'AlmanacNamespaceEditor' => 'applications/almanac/editor/AlmanacNamespaceEditor.php', 'AlmanacNamespaceListController' => 'applications/almanac/controller/AlmanacNamespaceListController.php', 'AlmanacNamespaceNameNgrams' => 'applications/almanac/storage/AlmanacNamespaceNameNgrams.php', 'AlmanacNamespaceNameTransaction' => 'applications/almanac/xaction/AlmanacNamespaceNameTransaction.php', 'AlmanacNamespacePHIDType' => 'applications/almanac/phid/AlmanacNamespacePHIDType.php', 'AlmanacNamespaceQuery' => 'applications/almanac/query/AlmanacNamespaceQuery.php', 'AlmanacNamespaceSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacNamespaceSearchConduitAPIMethod.php', 'AlmanacNamespaceSearchEngine' => 'applications/almanac/query/AlmanacNamespaceSearchEngine.php', 'AlmanacNamespaceTransaction' => 'applications/almanac/storage/AlmanacNamespaceTransaction.php', 'AlmanacNamespaceTransactionQuery' => 'applications/almanac/query/AlmanacNamespaceTransactionQuery.php', 'AlmanacNamespaceTransactionType' => 'applications/almanac/xaction/AlmanacNamespaceTransactionType.php', 'AlmanacNamespaceViewController' => 'applications/almanac/controller/AlmanacNamespaceViewController.php', 'AlmanacNetwork' => 'applications/almanac/storage/AlmanacNetwork.php', 'AlmanacNetworkController' => 'applications/almanac/controller/AlmanacNetworkController.php', 'AlmanacNetworkEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacNetworkEditConduitAPIMethod.php', 'AlmanacNetworkEditController' => 'applications/almanac/controller/AlmanacNetworkEditController.php', 'AlmanacNetworkEditEngine' => 'applications/almanac/editor/AlmanacNetworkEditEngine.php', 'AlmanacNetworkEditor' => 'applications/almanac/editor/AlmanacNetworkEditor.php', 'AlmanacNetworkListController' => 'applications/almanac/controller/AlmanacNetworkListController.php', 'AlmanacNetworkNameNgrams' => 'applications/almanac/storage/AlmanacNetworkNameNgrams.php', 'AlmanacNetworkNameTransaction' => 'applications/almanac/xaction/AlmanacNetworkNameTransaction.php', 'AlmanacNetworkPHIDType' => 'applications/almanac/phid/AlmanacNetworkPHIDType.php', 'AlmanacNetworkQuery' => 'applications/almanac/query/AlmanacNetworkQuery.php', 'AlmanacNetworkSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacNetworkSearchConduitAPIMethod.php', 'AlmanacNetworkSearchEngine' => 'applications/almanac/query/AlmanacNetworkSearchEngine.php', 'AlmanacNetworkTransaction' => 'applications/almanac/storage/AlmanacNetworkTransaction.php', 'AlmanacNetworkTransactionQuery' => 'applications/almanac/query/AlmanacNetworkTransactionQuery.php', 'AlmanacNetworkTransactionType' => 'applications/almanac/xaction/AlmanacNetworkTransactionType.php', 'AlmanacNetworkViewController' => 'applications/almanac/controller/AlmanacNetworkViewController.php', 'AlmanacPropertiesDestructionEngineExtension' => 'applications/almanac/engineextension/AlmanacPropertiesDestructionEngineExtension.php', 'AlmanacPropertiesEditEngineExtension' => 'applications/almanac/engineextension/AlmanacPropertiesEditEngineExtension.php', 'AlmanacPropertiesSearchEngineAttachment' => 'applications/almanac/engineextension/AlmanacPropertiesSearchEngineAttachment.php', 'AlmanacProperty' => 'applications/almanac/storage/AlmanacProperty.php', 'AlmanacPropertyController' => 'applications/almanac/controller/AlmanacPropertyController.php', 'AlmanacPropertyDeleteController' => 'applications/almanac/controller/AlmanacPropertyDeleteController.php', 'AlmanacPropertyEditController' => 'applications/almanac/controller/AlmanacPropertyEditController.php', 'AlmanacPropertyEditEngine' => 'applications/almanac/editor/AlmanacPropertyEditEngine.php', 'AlmanacPropertyInterface' => 'applications/almanac/property/AlmanacPropertyInterface.php', 'AlmanacPropertyQuery' => 'applications/almanac/query/AlmanacPropertyQuery.php', 'AlmanacQuery' => 'applications/almanac/query/AlmanacQuery.php', 'AlmanacSchemaSpec' => 'applications/almanac/storage/AlmanacSchemaSpec.php', 'AlmanacSearchEngineAttachment' => 'applications/almanac/engineextension/AlmanacSearchEngineAttachment.php', 'AlmanacService' => 'applications/almanac/storage/AlmanacService.php', 'AlmanacServiceController' => 'applications/almanac/controller/AlmanacServiceController.php', 'AlmanacServiceDatasource' => 'applications/almanac/typeahead/AlmanacServiceDatasource.php', 'AlmanacServiceDeletePropertyTransaction' => 'applications/almanac/xaction/AlmanacServiceDeletePropertyTransaction.php', 'AlmanacServiceEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacServiceEditConduitAPIMethod.php', 'AlmanacServiceEditController' => 'applications/almanac/controller/AlmanacServiceEditController.php', 'AlmanacServiceEditEngine' => 'applications/almanac/editor/AlmanacServiceEditEngine.php', 'AlmanacServiceEditor' => 'applications/almanac/editor/AlmanacServiceEditor.php', 'AlmanacServiceListController' => 'applications/almanac/controller/AlmanacServiceListController.php', 'AlmanacServiceNameNgrams' => 'applications/almanac/storage/AlmanacServiceNameNgrams.php', 'AlmanacServiceNameTransaction' => 'applications/almanac/xaction/AlmanacServiceNameTransaction.php', 'AlmanacServicePHIDType' => 'applications/almanac/phid/AlmanacServicePHIDType.php', 'AlmanacServicePropertyEditEngine' => 'applications/almanac/editor/AlmanacServicePropertyEditEngine.php', 'AlmanacServiceQuery' => 'applications/almanac/query/AlmanacServiceQuery.php', 'AlmanacServiceSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacServiceSearchConduitAPIMethod.php', 'AlmanacServiceSearchEngine' => 'applications/almanac/query/AlmanacServiceSearchEngine.php', 'AlmanacServiceSetPropertyTransaction' => 'applications/almanac/xaction/AlmanacServiceSetPropertyTransaction.php', 'AlmanacServiceTransaction' => 'applications/almanac/storage/AlmanacServiceTransaction.php', 'AlmanacServiceTransactionQuery' => 'applications/almanac/query/AlmanacServiceTransactionQuery.php', 'AlmanacServiceTransactionType' => 'applications/almanac/xaction/AlmanacServiceTransactionType.php', 'AlmanacServiceType' => 'applications/almanac/servicetype/AlmanacServiceType.php', 'AlmanacServiceTypeDatasource' => 'applications/almanac/typeahead/AlmanacServiceTypeDatasource.php', 'AlmanacServiceTypeTestCase' => 'applications/almanac/servicetype/__tests__/AlmanacServiceTypeTestCase.php', 'AlmanacServiceTypeTransaction' => 'applications/almanac/xaction/AlmanacServiceTypeTransaction.php', 'AlmanacServiceViewController' => 'applications/almanac/controller/AlmanacServiceViewController.php', 'AlmanacSetPropertyEditField' => 'applications/almanac/engineextension/AlmanacSetPropertyEditField.php', 'AlmanacSetPropertyEditType' => 'applications/almanac/engineextension/AlmanacSetPropertyEditType.php', 'AlmanacTransactionType' => 'applications/almanac/xaction/AlmanacTransactionType.php', 'AphlictDropdownDataQuery' => 'applications/aphlict/query/AphlictDropdownDataQuery.php', 'Aphront304Response' => 'aphront/response/Aphront304Response.php', 'Aphront400Response' => 'aphront/response/Aphront400Response.php', 'Aphront403Response' => 'aphront/response/Aphront403Response.php', 'Aphront404Response' => 'aphront/response/Aphront404Response.php', 'AphrontAjaxResponse' => 'aphront/response/AphrontAjaxResponse.php', 'AphrontApplicationConfiguration' => 'aphront/configuration/AphrontApplicationConfiguration.php', 'AphrontBarView' => 'view/widget/bars/AphrontBarView.php', 'AphrontBoolHTTPParameterType' => 'aphront/httpparametertype/AphrontBoolHTTPParameterType.php', 'AphrontCalendarEventView' => 'applications/calendar/view/AphrontCalendarEventView.php', 'AphrontController' => 'aphront/AphrontController.php', 'AphrontCursorPagerView' => 'view/control/AphrontCursorPagerView.php', 'AphrontDefaultApplicationConfiguration' => 'aphront/configuration/AphrontDefaultApplicationConfiguration.php', 'AphrontDialogResponse' => 'aphront/response/AphrontDialogResponse.php', 'AphrontDialogView' => 'view/AphrontDialogView.php', 'AphrontEpochHTTPParameterType' => 'aphront/httpparametertype/AphrontEpochHTTPParameterType.php', 'AphrontException' => 'aphront/exception/AphrontException.php', 'AphrontFileHTTPParameterType' => 'aphront/httpparametertype/AphrontFileHTTPParameterType.php', 'AphrontFileResponse' => 'aphront/response/AphrontFileResponse.php', 'AphrontFormCheckboxControl' => 'view/form/control/AphrontFormCheckboxControl.php', 'AphrontFormControl' => 'view/form/control/AphrontFormControl.php', 'AphrontFormDateControl' => 'view/form/control/AphrontFormDateControl.php', 'AphrontFormDateControlValue' => 'view/form/control/AphrontFormDateControlValue.php', 'AphrontFormDividerControl' => 'view/form/control/AphrontFormDividerControl.php', 'AphrontFormFileControl' => 'view/form/control/AphrontFormFileControl.php', 'AphrontFormHandlesControl' => 'view/form/control/AphrontFormHandlesControl.php', 'AphrontFormMarkupControl' => 'view/form/control/AphrontFormMarkupControl.php', 'AphrontFormPasswordControl' => 'view/form/control/AphrontFormPasswordControl.php', 'AphrontFormPolicyControl' => 'view/form/control/AphrontFormPolicyControl.php', 'AphrontFormRadioButtonControl' => 'view/form/control/AphrontFormRadioButtonControl.php', 'AphrontFormRecaptchaControl' => 'view/form/control/AphrontFormRecaptchaControl.php', 'AphrontFormSelectControl' => 'view/form/control/AphrontFormSelectControl.php', 'AphrontFormStaticControl' => 'view/form/control/AphrontFormStaticControl.php', 'AphrontFormSubmitControl' => 'view/form/control/AphrontFormSubmitControl.php', 'AphrontFormTextAreaControl' => 'view/form/control/AphrontFormTextAreaControl.php', 'AphrontFormTextControl' => 'view/form/control/AphrontFormTextControl.php', 'AphrontFormTextWithSubmitControl' => 'view/form/control/AphrontFormTextWithSubmitControl.php', 'AphrontFormTokenizerControl' => 'view/form/control/AphrontFormTokenizerControl.php', 'AphrontFormTypeaheadControl' => 'view/form/control/AphrontFormTypeaheadControl.php', 'AphrontFormView' => 'view/form/AphrontFormView.php', 'AphrontGlyphBarView' => 'view/widget/bars/AphrontGlyphBarView.php', 'AphrontHTMLResponse' => 'aphront/response/AphrontHTMLResponse.php', 'AphrontHTTPParameterType' => 'aphront/httpparametertype/AphrontHTTPParameterType.php', 'AphrontHTTPProxyResponse' => 'aphront/response/AphrontHTTPProxyResponse.php', 'AphrontHTTPSink' => 'aphront/sink/AphrontHTTPSink.php', 'AphrontHTTPSinkTestCase' => 'aphront/sink/__tests__/AphrontHTTPSinkTestCase.php', 'AphrontIntHTTPParameterType' => 'aphront/httpparametertype/AphrontIntHTTPParameterType.php', 'AphrontIsolatedDatabaseConnectionTestCase' => 'infrastructure/storage/__tests__/AphrontIsolatedDatabaseConnectionTestCase.php', 'AphrontIsolatedHTTPSink' => 'aphront/sink/AphrontIsolatedHTTPSink.php', 'AphrontJSONResponse' => 'aphront/response/AphrontJSONResponse.php', 'AphrontJavelinView' => 'view/AphrontJavelinView.php', 'AphrontKeyboardShortcutsAvailableView' => 'view/widget/AphrontKeyboardShortcutsAvailableView.php', 'AphrontListFilterView' => 'view/layout/AphrontListFilterView.php', 'AphrontListHTTPParameterType' => 'aphront/httpparametertype/AphrontListHTTPParameterType.php', 'AphrontMalformedRequestException' => 'aphront/exception/AphrontMalformedRequestException.php', 'AphrontMoreView' => 'view/layout/AphrontMoreView.php', 'AphrontMultiColumnView' => 'view/layout/AphrontMultiColumnView.php', 'AphrontMySQLDatabaseConnectionTestCase' => 'infrastructure/storage/__tests__/AphrontMySQLDatabaseConnectionTestCase.php', 'AphrontNullView' => 'view/AphrontNullView.php', 'AphrontPHIDHTTPParameterType' => 'aphront/httpparametertype/AphrontPHIDHTTPParameterType.php', 'AphrontPHIDListHTTPParameterType' => 'aphront/httpparametertype/AphrontPHIDListHTTPParameterType.php', 'AphrontPHPHTTPSink' => 'aphront/sink/AphrontPHPHTTPSink.php', 'AphrontPageView' => 'view/page/AphrontPageView.php', 'AphrontPlainTextResponse' => 'aphront/response/AphrontPlainTextResponse.php', 'AphrontProgressBarView' => 'view/widget/bars/AphrontProgressBarView.php', 'AphrontProjectListHTTPParameterType' => 'aphront/httpparametertype/AphrontProjectListHTTPParameterType.php', 'AphrontProxyResponse' => 'aphront/response/AphrontProxyResponse.php', 'AphrontRedirectResponse' => 'aphront/response/AphrontRedirectResponse.php', 'AphrontRedirectResponseTestCase' => 'aphront/response/__tests__/AphrontRedirectResponseTestCase.php', 'AphrontReloadResponse' => 'aphront/response/AphrontReloadResponse.php', 'AphrontRequest' => 'aphront/AphrontRequest.php', 'AphrontRequestExceptionHandler' => 'aphront/handler/AphrontRequestExceptionHandler.php', 'AphrontRequestTestCase' => 'aphront/__tests__/AphrontRequestTestCase.php', 'AphrontResponse' => 'aphront/response/AphrontResponse.php', 'AphrontResponseProducerInterface' => 'aphront/interface/AphrontResponseProducerInterface.php', 'AphrontRoutingMap' => 'aphront/site/AphrontRoutingMap.php', 'AphrontRoutingResult' => 'aphront/site/AphrontRoutingResult.php', 'AphrontSelectHTTPParameterType' => 'aphront/httpparametertype/AphrontSelectHTTPParameterType.php', 'AphrontSideNavFilterView' => 'view/layout/AphrontSideNavFilterView.php', 'AphrontSite' => 'aphront/site/AphrontSite.php', 'AphrontStackTraceView' => 'view/widget/AphrontStackTraceView.php', 'AphrontStandaloneHTMLResponse' => 'aphront/response/AphrontStandaloneHTMLResponse.php', 'AphrontStringHTTPParameterType' => 'aphront/httpparametertype/AphrontStringHTTPParameterType.php', 'AphrontStringListHTTPParameterType' => 'aphront/httpparametertype/AphrontStringListHTTPParameterType.php', 'AphrontTableView' => 'view/control/AphrontTableView.php', 'AphrontTagView' => 'view/AphrontTagView.php', 'AphrontTokenizerTemplateView' => 'view/control/AphrontTokenizerTemplateView.php', 'AphrontTypeaheadTemplateView' => 'view/control/AphrontTypeaheadTemplateView.php', 'AphrontUnhandledExceptionResponse' => 'aphront/response/AphrontUnhandledExceptionResponse.php', 'AphrontUserListHTTPParameterType' => 'aphront/httpparametertype/AphrontUserListHTTPParameterType.php', 'AphrontView' => 'view/AphrontView.php', 'AphrontWebpageResponse' => 'aphront/response/AphrontWebpageResponse.php', 'ArcanistConduitAPIMethod' => 'applications/arcanist/conduit/ArcanistConduitAPIMethod.php', 'AuditConduitAPIMethod' => 'applications/audit/conduit/AuditConduitAPIMethod.php', 'AuditQueryConduitAPIMethod' => 'applications/audit/conduit/AuditQueryConduitAPIMethod.php', 'AuthManageProvidersCapability' => 'applications/auth/capability/AuthManageProvidersCapability.php', 'BulkParameterType' => 'applications/transactions/bulk/type/BulkParameterType.php', 'BulkPointsParameterType' => 'applications/transactions/bulk/type/BulkPointsParameterType.php', 'BulkRemarkupParameterType' => 'applications/transactions/bulk/type/BulkRemarkupParameterType.php', 'BulkSelectParameterType' => 'applications/transactions/bulk/type/BulkSelectParameterType.php', 'BulkStringParameterType' => 'applications/transactions/bulk/type/BulkStringParameterType.php', 'BulkTokenizerParameterType' => 'applications/transactions/bulk/type/BulkTokenizerParameterType.php', 'CalendarTimeUtil' => 'applications/calendar/util/CalendarTimeUtil.php', 'CalendarTimeUtilTestCase' => 'applications/calendar/__tests__/CalendarTimeUtilTestCase.php', 'CelerityAPI' => 'applications/celerity/CelerityAPI.php', 'CelerityDarkModePostprocessor' => 'applications/celerity/postprocessor/CelerityDarkModePostprocessor.php', 'CelerityDefaultPostprocessor' => 'applications/celerity/postprocessor/CelerityDefaultPostprocessor.php', 'CelerityHighContrastPostprocessor' => 'applications/celerity/postprocessor/CelerityHighContrastPostprocessor.php', 'CelerityLargeFontPostprocessor' => 'applications/celerity/postprocessor/CelerityLargeFontPostprocessor.php', 'CelerityManagementMapWorkflow' => 'applications/celerity/management/CelerityManagementMapWorkflow.php', 'CelerityManagementSyntaxWorkflow' => 'applications/celerity/management/CelerityManagementSyntaxWorkflow.php', 'CelerityManagementWorkflow' => 'applications/celerity/management/CelerityManagementWorkflow.php', 'CelerityPhabricatorResourceController' => 'applications/celerity/controller/CelerityPhabricatorResourceController.php', 'CelerityPhabricatorResources' => 'applications/celerity/resources/CelerityPhabricatorResources.php', 'CelerityPhysicalResources' => 'applications/celerity/resources/CelerityPhysicalResources.php', 'CelerityPhysicalResourcesTestCase' => 'applications/celerity/resources/__tests__/CelerityPhysicalResourcesTestCase.php', 'CelerityPostprocessor' => 'applications/celerity/postprocessor/CelerityPostprocessor.php', 'CelerityPostprocessorTestCase' => 'applications/celerity/__tests__/CelerityPostprocessorTestCase.php', 'CelerityRedGreenPostprocessor' => 'applications/celerity/postprocessor/CelerityRedGreenPostprocessor.php', 'CelerityResourceController' => 'applications/celerity/controller/CelerityResourceController.php', 'CelerityResourceGraph' => 'applications/celerity/CelerityResourceGraph.php', 'CelerityResourceMap' => 'applications/celerity/CelerityResourceMap.php', 'CelerityResourceMapGenerator' => 'applications/celerity/CelerityResourceMapGenerator.php', 'CelerityResourceTransformer' => 'applications/celerity/CelerityResourceTransformer.php', 'CelerityResourceTransformerTestCase' => 'applications/celerity/__tests__/CelerityResourceTransformerTestCase.php', 'CelerityResources' => 'applications/celerity/resources/CelerityResources.php', 'CelerityResourcesOnDisk' => 'applications/celerity/resources/CelerityResourcesOnDisk.php', 'CeleritySpriteGenerator' => 'applications/celerity/CeleritySpriteGenerator.php', 'CelerityStaticResourceResponse' => 'applications/celerity/CelerityStaticResourceResponse.php', 'ChatLogConduitAPIMethod' => 'applications/chatlog/conduit/ChatLogConduitAPIMethod.php', 'ChatLogQueryConduitAPIMethod' => 'applications/chatlog/conduit/ChatLogQueryConduitAPIMethod.php', 'ChatLogRecordConduitAPIMethod' => 'applications/chatlog/conduit/ChatLogRecordConduitAPIMethod.php', 'ConduitAPIMethod' => 'applications/conduit/method/ConduitAPIMethod.php', 'ConduitAPIMethodTestCase' => 'applications/conduit/method/__tests__/ConduitAPIMethodTestCase.php', 'ConduitAPIRequest' => 'applications/conduit/protocol/ConduitAPIRequest.php', 'ConduitAPIResponse' => 'applications/conduit/protocol/ConduitAPIResponse.php', 'ConduitApplicationNotInstalledException' => 'applications/conduit/protocol/exception/ConduitApplicationNotInstalledException.php', 'ConduitBoolParameterType' => 'applications/conduit/parametertype/ConduitBoolParameterType.php', 'ConduitCall' => 'applications/conduit/call/ConduitCall.php', 'ConduitCallTestCase' => 'applications/conduit/call/__tests__/ConduitCallTestCase.php', 'ConduitColumnsParameterType' => 'applications/conduit/parametertype/ConduitColumnsParameterType.php', 'ConduitConnectConduitAPIMethod' => 'applications/conduit/method/ConduitConnectConduitAPIMethod.php', 'ConduitConstantDescription' => 'applications/conduit/data/ConduitConstantDescription.php', 'ConduitEpochParameterType' => 'applications/conduit/parametertype/ConduitEpochParameterType.php', 'ConduitException' => 'applications/conduit/protocol/exception/ConduitException.php', 'ConduitGetCapabilitiesConduitAPIMethod' => 'applications/conduit/method/ConduitGetCapabilitiesConduitAPIMethod.php', 'ConduitGetCertificateConduitAPIMethod' => 'applications/conduit/method/ConduitGetCertificateConduitAPIMethod.php', 'ConduitIntListParameterType' => 'applications/conduit/parametertype/ConduitIntListParameterType.php', 'ConduitIntParameterType' => 'applications/conduit/parametertype/ConduitIntParameterType.php', 'ConduitListParameterType' => 'applications/conduit/parametertype/ConduitListParameterType.php', 'ConduitLogGarbageCollector' => 'applications/conduit/garbagecollector/ConduitLogGarbageCollector.php', 'ConduitMethodDoesNotExistException' => 'applications/conduit/protocol/exception/ConduitMethodDoesNotExistException.php', 'ConduitMethodNotFoundException' => 'applications/conduit/protocol/exception/ConduitMethodNotFoundException.php', 'ConduitPHIDListParameterType' => 'applications/conduit/parametertype/ConduitPHIDListParameterType.php', 'ConduitPHIDParameterType' => 'applications/conduit/parametertype/ConduitPHIDParameterType.php', 'ConduitParameterType' => 'applications/conduit/parametertype/ConduitParameterType.php', 'ConduitPingConduitAPIMethod' => 'applications/conduit/method/ConduitPingConduitAPIMethod.php', 'ConduitPointsParameterType' => 'applications/conduit/parametertype/ConduitPointsParameterType.php', 'ConduitProjectListParameterType' => 'applications/conduit/parametertype/ConduitProjectListParameterType.php', 'ConduitQueryConduitAPIMethod' => 'applications/conduit/method/ConduitQueryConduitAPIMethod.php', 'ConduitResultSearchEngineExtension' => 'applications/conduit/query/ConduitResultSearchEngineExtension.php', 'ConduitSSHWorkflow' => 'applications/conduit/ssh/ConduitSSHWorkflow.php', 'ConduitStringListParameterType' => 'applications/conduit/parametertype/ConduitStringListParameterType.php', 'ConduitStringParameterType' => 'applications/conduit/parametertype/ConduitStringParameterType.php', 'ConduitTokenGarbageCollector' => 'applications/conduit/garbagecollector/ConduitTokenGarbageCollector.php', 'ConduitUserListParameterType' => 'applications/conduit/parametertype/ConduitUserListParameterType.php', 'ConduitUserParameterType' => 'applications/conduit/parametertype/ConduitUserParameterType.php', 'ConduitWildParameterType' => 'applications/conduit/parametertype/ConduitWildParameterType.php', 'ConpherenceColumnViewController' => 'applications/conpherence/controller/ConpherenceColumnViewController.php', 'ConpherenceConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceConduitAPIMethod.php', 'ConpherenceConfigOptions' => 'applications/conpherence/config/ConpherenceConfigOptions.php', 'ConpherenceConstants' => 'applications/conpherence/constants/ConpherenceConstants.php', 'ConpherenceController' => 'applications/conpherence/controller/ConpherenceController.php', 'ConpherenceCreateThreadConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceCreateThreadConduitAPIMethod.php', 'ConpherenceDAO' => 'applications/conpherence/storage/ConpherenceDAO.php', 'ConpherenceDurableColumnView' => 'applications/conpherence/view/ConpherenceDurableColumnView.php', 'ConpherenceEditConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceEditConduitAPIMethod.php', 'ConpherenceEditEngine' => 'applications/conpherence/editor/ConpherenceEditEngine.php', 'ConpherenceEditor' => 'applications/conpherence/editor/ConpherenceEditor.php', 'ConpherenceFulltextQuery' => 'applications/conpherence/query/ConpherenceFulltextQuery.php', 'ConpherenceIndex' => 'applications/conpherence/storage/ConpherenceIndex.php', 'ConpherenceLayoutView' => 'applications/conpherence/view/ConpherenceLayoutView.php', 'ConpherenceListController' => 'applications/conpherence/controller/ConpherenceListController.php', 'ConpherenceMenuItemView' => 'applications/conpherence/view/ConpherenceMenuItemView.php', 'ConpherenceNotificationPanelController' => 'applications/conpherence/controller/ConpherenceNotificationPanelController.php', 'ConpherenceParticipant' => 'applications/conpherence/storage/ConpherenceParticipant.php', 'ConpherenceParticipantController' => 'applications/conpherence/controller/ConpherenceParticipantController.php', 'ConpherenceParticipantCountQuery' => 'applications/conpherence/query/ConpherenceParticipantCountQuery.php', 'ConpherenceParticipantQuery' => 'applications/conpherence/query/ConpherenceParticipantQuery.php', 'ConpherenceParticipantView' => 'applications/conpherence/view/ConpherenceParticipantView.php', 'ConpherenceQueryThreadConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceQueryThreadConduitAPIMethod.php', 'ConpherenceQueryTransactionConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceQueryTransactionConduitAPIMethod.php', 'ConpherenceReplyHandler' => 'applications/conpherence/mail/ConpherenceReplyHandler.php', 'ConpherenceRoomEditController' => 'applications/conpherence/controller/ConpherenceRoomEditController.php', 'ConpherenceRoomListController' => 'applications/conpherence/controller/ConpherenceRoomListController.php', 'ConpherenceRoomPictureController' => 'applications/conpherence/controller/ConpherenceRoomPictureController.php', 'ConpherenceRoomPreferencesController' => 'applications/conpherence/controller/ConpherenceRoomPreferencesController.php', 'ConpherenceRoomSettings' => 'applications/conpherence/constants/ConpherenceRoomSettings.php', 'ConpherenceRoomTestCase' => 'applications/conpherence/__tests__/ConpherenceRoomTestCase.php', 'ConpherenceSchemaSpec' => 'applications/conpherence/storage/ConpherenceSchemaSpec.php', 'ConpherenceTestCase' => 'applications/conpherence/__tests__/ConpherenceTestCase.php', 'ConpherenceThread' => 'applications/conpherence/storage/ConpherenceThread.php', 'ConpherenceThreadDatasource' => 'applications/conpherence/typeahead/ConpherenceThreadDatasource.php', 'ConpherenceThreadDateMarkerTransaction' => 'applications/conpherence/xaction/ConpherenceThreadDateMarkerTransaction.php', 'ConpherenceThreadIndexEngineExtension' => 'applications/conpherence/engineextension/ConpherenceThreadIndexEngineExtension.php', 'ConpherenceThreadListView' => 'applications/conpherence/view/ConpherenceThreadListView.php', 'ConpherenceThreadMailReceiver' => 'applications/conpherence/mail/ConpherenceThreadMailReceiver.php', 'ConpherenceThreadMembersPolicyRule' => 'applications/conpherence/policyrule/ConpherenceThreadMembersPolicyRule.php', 'ConpherenceThreadParticipantsTransaction' => 'applications/conpherence/xaction/ConpherenceThreadParticipantsTransaction.php', 'ConpherenceThreadPictureTransaction' => 'applications/conpherence/xaction/ConpherenceThreadPictureTransaction.php', 'ConpherenceThreadQuery' => 'applications/conpherence/query/ConpherenceThreadQuery.php', 'ConpherenceThreadRemarkupRule' => 'applications/conpherence/remarkup/ConpherenceThreadRemarkupRule.php', 'ConpherenceThreadSearchController' => 'applications/conpherence/controller/ConpherenceThreadSearchController.php', 'ConpherenceThreadSearchEngine' => 'applications/conpherence/query/ConpherenceThreadSearchEngine.php', 'ConpherenceThreadTitleNgrams' => 'applications/conpherence/storage/ConpherenceThreadTitleNgrams.php', 'ConpherenceThreadTitleTransaction' => 'applications/conpherence/xaction/ConpherenceThreadTitleTransaction.php', 'ConpherenceThreadTopicTransaction' => 'applications/conpherence/xaction/ConpherenceThreadTopicTransaction.php', 'ConpherenceThreadTransactionType' => 'applications/conpherence/xaction/ConpherenceThreadTransactionType.php', 'ConpherenceTransaction' => 'applications/conpherence/storage/ConpherenceTransaction.php', 'ConpherenceTransactionComment' => 'applications/conpherence/storage/ConpherenceTransactionComment.php', 'ConpherenceTransactionQuery' => 'applications/conpherence/query/ConpherenceTransactionQuery.php', 'ConpherenceTransactionRenderer' => 'applications/conpherence/ConpherenceTransactionRenderer.php', 'ConpherenceTransactionView' => 'applications/conpherence/view/ConpherenceTransactionView.php', 'ConpherenceUpdateActions' => 'applications/conpherence/constants/ConpherenceUpdateActions.php', 'ConpherenceUpdateController' => 'applications/conpherence/controller/ConpherenceUpdateController.php', 'ConpherenceUpdateThreadConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceUpdateThreadConduitAPIMethod.php', 'ConpherenceViewController' => 'applications/conpherence/controller/ConpherenceViewController.php', 'CountdownEditConduitAPIMethod' => 'applications/countdown/conduit/CountdownEditConduitAPIMethod.php', 'CountdownSearchConduitAPIMethod' => 'applications/countdown/conduit/CountdownSearchConduitAPIMethod.php', 'DarkConsoleController' => 'applications/console/controller/DarkConsoleController.php', 'DarkConsoleCore' => 'applications/console/core/DarkConsoleCore.php', 'DarkConsoleDataController' => 'applications/console/controller/DarkConsoleDataController.php', 'DarkConsoleErrorLogPlugin' => 'applications/console/plugin/DarkConsoleErrorLogPlugin.php', 'DarkConsoleErrorLogPluginAPI' => 'applications/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php', 'DarkConsoleEventPlugin' => 'applications/console/plugin/DarkConsoleEventPlugin.php', 'DarkConsoleEventPluginAPI' => 'applications/console/plugin/event/DarkConsoleEventPluginAPI.php', 'DarkConsolePlugin' => 'applications/console/plugin/DarkConsolePlugin.php', 'DarkConsoleRealtimePlugin' => 'applications/console/plugin/DarkConsoleRealtimePlugin.php', 'DarkConsoleRequestPlugin' => 'applications/console/plugin/DarkConsoleRequestPlugin.php', 'DarkConsoleServicesPlugin' => 'applications/console/plugin/DarkConsoleServicesPlugin.php', 'DarkConsoleStartupPlugin' => 'applications/console/plugin/DarkConsoleStartupPlugin.php', 'DarkConsoleXHProfPlugin' => 'applications/console/plugin/DarkConsoleXHProfPlugin.php', 'DarkConsoleXHProfPluginAPI' => 'applications/console/plugin/xhprof/DarkConsoleXHProfPluginAPI.php', 'DifferentialAction' => 'applications/differential/constants/DifferentialAction.php', 'DifferentialActionEmailCommand' => 'applications/differential/command/DifferentialActionEmailCommand.php', 'DifferentialAdjustmentMapTestCase' => 'applications/differential/storage/__tests__/DifferentialAdjustmentMapTestCase.php', 'DifferentialAffectedPath' => 'applications/differential/storage/DifferentialAffectedPath.php', 'DifferentialAsanaRepresentationField' => 'applications/differential/customfield/DifferentialAsanaRepresentationField.php', 'DifferentialAuditorsCommitMessageField' => 'applications/differential/field/DifferentialAuditorsCommitMessageField.php', 'DifferentialAuditorsField' => 'applications/differential/customfield/DifferentialAuditorsField.php', 'DifferentialBlameRevisionCommitMessageField' => 'applications/differential/field/DifferentialBlameRevisionCommitMessageField.php', 'DifferentialBlameRevisionField' => 'applications/differential/customfield/DifferentialBlameRevisionField.php', 'DifferentialBlockHeraldAction' => 'applications/differential/herald/DifferentialBlockHeraldAction.php', 'DifferentialBlockingReviewerDatasource' => 'applications/differential/typeahead/DifferentialBlockingReviewerDatasource.php', 'DifferentialBranchField' => 'applications/differential/customfield/DifferentialBranchField.php', 'DifferentialBuildableEngine' => 'applications/differential/harbormaster/DifferentialBuildableEngine.php', 'DifferentialChangeDetailMailView' => 'applications/differential/mail/DifferentialChangeDetailMailView.php', 'DifferentialChangeHeraldFieldGroup' => 'applications/differential/herald/DifferentialChangeHeraldFieldGroup.php', 'DifferentialChangeType' => 'applications/differential/constants/DifferentialChangeType.php', 'DifferentialChangesSinceLastUpdateField' => 'applications/differential/customfield/DifferentialChangesSinceLastUpdateField.php', 'DifferentialChangeset' => 'applications/differential/storage/DifferentialChangeset.php', 'DifferentialChangesetDetailView' => 'applications/differential/view/DifferentialChangesetDetailView.php', 'DifferentialChangesetEngine' => 'applications/differential/engine/DifferentialChangesetEngine.php', 'DifferentialChangesetFileTreeSideNavBuilder' => 'applications/differential/view/DifferentialChangesetFileTreeSideNavBuilder.php', 'DifferentialChangesetHTMLRenderer' => 'applications/differential/render/DifferentialChangesetHTMLRenderer.php', 'DifferentialChangesetListController' => 'applications/differential/controller/DifferentialChangesetListController.php', 'DifferentialChangesetListView' => 'applications/differential/view/DifferentialChangesetListView.php', 'DifferentialChangesetOneUpMailRenderer' => 'applications/differential/render/DifferentialChangesetOneUpMailRenderer.php', 'DifferentialChangesetOneUpRenderer' => 'applications/differential/render/DifferentialChangesetOneUpRenderer.php', 'DifferentialChangesetOneUpTestRenderer' => 'applications/differential/render/DifferentialChangesetOneUpTestRenderer.php', 'DifferentialChangesetParser' => 'applications/differential/parser/DifferentialChangesetParser.php', 'DifferentialChangesetParserTestCase' => 'applications/differential/parser/__tests__/DifferentialChangesetParserTestCase.php', 'DifferentialChangesetQuery' => 'applications/differential/query/DifferentialChangesetQuery.php', 'DifferentialChangesetRenderer' => 'applications/differential/render/DifferentialChangesetRenderer.php', 'DifferentialChangesetSearchEngine' => 'applications/differential/query/DifferentialChangesetSearchEngine.php', 'DifferentialChangesetTestRenderer' => 'applications/differential/render/DifferentialChangesetTestRenderer.php', 'DifferentialChangesetTwoUpRenderer' => 'applications/differential/render/DifferentialChangesetTwoUpRenderer.php', 'DifferentialChangesetTwoUpTestRenderer' => 'applications/differential/render/DifferentialChangesetTwoUpTestRenderer.php', 'DifferentialChangesetViewController' => 'applications/differential/controller/DifferentialChangesetViewController.php', 'DifferentialCloseConduitAPIMethod' => 'applications/differential/conduit/DifferentialCloseConduitAPIMethod.php', 'DifferentialCommitMessageCustomField' => 'applications/differential/field/DifferentialCommitMessageCustomField.php', 'DifferentialCommitMessageField' => 'applications/differential/field/DifferentialCommitMessageField.php', 'DifferentialCommitMessageFieldTestCase' => 'applications/differential/field/__tests__/DifferentialCommitMessageFieldTestCase.php', 'DifferentialCommitMessageParser' => 'applications/differential/parser/DifferentialCommitMessageParser.php', 'DifferentialCommitMessageParserTestCase' => 'applications/differential/parser/__tests__/DifferentialCommitMessageParserTestCase.php', 'DifferentialCommitsField' => 'applications/differential/customfield/DifferentialCommitsField.php', 'DifferentialCommitsSearchEngineAttachment' => 'applications/differential/engineextension/DifferentialCommitsSearchEngineAttachment.php', 'DifferentialConduitAPIMethod' => 'applications/differential/conduit/DifferentialConduitAPIMethod.php', 'DifferentialConflictsCommitMessageField' => 'applications/differential/field/DifferentialConflictsCommitMessageField.php', 'DifferentialController' => 'applications/differential/controller/DifferentialController.php', 'DifferentialCoreCustomField' => 'applications/differential/customfield/DifferentialCoreCustomField.php', 'DifferentialCreateCommentConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateCommentConduitAPIMethod.php', 'DifferentialCreateDiffConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php', 'DifferentialCreateInlineConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateInlineConduitAPIMethod.php', 'DifferentialCreateMailReceiver' => 'applications/differential/mail/DifferentialCreateMailReceiver.php', 'DifferentialCreateRawDiffConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateRawDiffConduitAPIMethod.php', 'DifferentialCreateRevisionConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateRevisionConduitAPIMethod.php', 'DifferentialCustomField' => 'applications/differential/customfield/DifferentialCustomField.php', 'DifferentialCustomFieldDependsOnParser' => 'applications/differential/parser/DifferentialCustomFieldDependsOnParser.php', 'DifferentialCustomFieldDependsOnParserTestCase' => 'applications/differential/parser/__tests__/DifferentialCustomFieldDependsOnParserTestCase.php', 'DifferentialCustomFieldNumericIndex' => 'applications/differential/storage/DifferentialCustomFieldNumericIndex.php', 'DifferentialCustomFieldRevertsParser' => 'applications/differential/parser/DifferentialCustomFieldRevertsParser.php', 'DifferentialCustomFieldRevertsParserTestCase' => 'applications/differential/parser/__tests__/DifferentialCustomFieldRevertsParserTestCase.php', 'DifferentialCustomFieldStorage' => 'applications/differential/storage/DifferentialCustomFieldStorage.php', 'DifferentialCustomFieldStringIndex' => 'applications/differential/storage/DifferentialCustomFieldStringIndex.php', 'DifferentialDAO' => 'applications/differential/storage/DifferentialDAO.php', 'DifferentialDefaultViewCapability' => 'applications/differential/capability/DifferentialDefaultViewCapability.php', 'DifferentialDiff' => 'applications/differential/storage/DifferentialDiff.php', 'DifferentialDiffAffectedFilesHeraldField' => 'applications/differential/herald/DifferentialDiffAffectedFilesHeraldField.php', 'DifferentialDiffAuthorHeraldField' => 'applications/differential/herald/DifferentialDiffAuthorHeraldField.php', 'DifferentialDiffAuthorProjectsHeraldField' => 'applications/differential/herald/DifferentialDiffAuthorProjectsHeraldField.php', 'DifferentialDiffContentAddedHeraldField' => 'applications/differential/herald/DifferentialDiffContentAddedHeraldField.php', 'DifferentialDiffContentHeraldField' => 'applications/differential/herald/DifferentialDiffContentHeraldField.php', 'DifferentialDiffContentRemovedHeraldField' => 'applications/differential/herald/DifferentialDiffContentRemovedHeraldField.php', 'DifferentialDiffCreateController' => 'applications/differential/controller/DifferentialDiffCreateController.php', 'DifferentialDiffEditor' => 'applications/differential/editor/DifferentialDiffEditor.php', 'DifferentialDiffExtractionEngine' => 'applications/differential/engine/DifferentialDiffExtractionEngine.php', 'DifferentialDiffHeraldField' => 'applications/differential/herald/DifferentialDiffHeraldField.php', 'DifferentialDiffHeraldFieldGroup' => 'applications/differential/herald/DifferentialDiffHeraldFieldGroup.php', 'DifferentialDiffInlineCommentQuery' => 'applications/differential/query/DifferentialDiffInlineCommentQuery.php', 'DifferentialDiffPHIDType' => 'applications/differential/phid/DifferentialDiffPHIDType.php', 'DifferentialDiffProperty' => 'applications/differential/storage/DifferentialDiffProperty.php', 'DifferentialDiffQuery' => 'applications/differential/query/DifferentialDiffQuery.php', 'DifferentialDiffRepositoryHeraldField' => 'applications/differential/herald/DifferentialDiffRepositoryHeraldField.php', 'DifferentialDiffRepositoryProjectsHeraldField' => 'applications/differential/herald/DifferentialDiffRepositoryProjectsHeraldField.php', 'DifferentialDiffSearchConduitAPIMethod' => 'applications/differential/conduit/DifferentialDiffSearchConduitAPIMethod.php', 'DifferentialDiffSearchEngine' => 'applications/differential/query/DifferentialDiffSearchEngine.php', 'DifferentialDiffTestCase' => 'applications/differential/storage/__tests__/DifferentialDiffTestCase.php', 'DifferentialDiffTransaction' => 'applications/differential/storage/DifferentialDiffTransaction.php', 'DifferentialDiffTransactionQuery' => 'applications/differential/query/DifferentialDiffTransactionQuery.php', 'DifferentialDiffViewController' => 'applications/differential/controller/DifferentialDiffViewController.php', 'DifferentialDoorkeeperRevisionFeedStoryPublisher' => 'applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php', 'DifferentialDraftField' => 'applications/differential/customfield/DifferentialDraftField.php', 'DifferentialExactUserFunctionDatasource' => 'applications/differential/typeahead/DifferentialExactUserFunctionDatasource.php', 'DifferentialFieldParseException' => 'applications/differential/exception/DifferentialFieldParseException.php', 'DifferentialFieldValidationException' => 'applications/differential/exception/DifferentialFieldValidationException.php', 'DifferentialGetAllDiffsConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetAllDiffsConduitAPIMethod.php', 'DifferentialGetCommitMessageConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetCommitMessageConduitAPIMethod.php', 'DifferentialGetCommitPathsConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetCommitPathsConduitAPIMethod.php', 'DifferentialGetDiffConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetDiffConduitAPIMethod.php', 'DifferentialGetRawDiffConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetRawDiffConduitAPIMethod.php', 'DifferentialGetRevisionCommentsConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetRevisionCommentsConduitAPIMethod.php', 'DifferentialGetRevisionConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetRevisionConduitAPIMethod.php', 'DifferentialGetWorkingCopy' => 'applications/differential/DifferentialGetWorkingCopy.php', 'DifferentialGitSVNIDCommitMessageField' => 'applications/differential/field/DifferentialGitSVNIDCommitMessageField.php', 'DifferentialHarbormasterField' => 'applications/differential/customfield/DifferentialHarbormasterField.php', 'DifferentialHeraldStateReasons' => 'applications/differential/herald/DifferentialHeraldStateReasons.php', 'DifferentialHiddenComment' => 'applications/differential/storage/DifferentialHiddenComment.php', 'DifferentialHostField' => 'applications/differential/customfield/DifferentialHostField.php', 'DifferentialHovercardEngineExtension' => 'applications/differential/engineextension/DifferentialHovercardEngineExtension.php', 'DifferentialHunk' => 'applications/differential/storage/DifferentialHunk.php', 'DifferentialHunkParser' => 'applications/differential/parser/DifferentialHunkParser.php', 'DifferentialHunkParserTestCase' => 'applications/differential/parser/__tests__/DifferentialHunkParserTestCase.php', 'DifferentialHunkQuery' => 'applications/differential/query/DifferentialHunkQuery.php', 'DifferentialHunkTestCase' => 'applications/differential/storage/__tests__/DifferentialHunkTestCase.php', 'DifferentialInlineComment' => 'applications/differential/storage/DifferentialInlineComment.php', 'DifferentialInlineCommentEditController' => 'applications/differential/controller/DifferentialInlineCommentEditController.php', 'DifferentialInlineCommentMailView' => 'applications/differential/mail/DifferentialInlineCommentMailView.php', 'DifferentialInlineCommentQuery' => 'applications/differential/query/DifferentialInlineCommentQuery.php', 'DifferentialJIRAIssuesCommitMessageField' => 'applications/differential/field/DifferentialJIRAIssuesCommitMessageField.php', 'DifferentialJIRAIssuesField' => 'applications/differential/customfield/DifferentialJIRAIssuesField.php', 'DifferentialLegacyQuery' => 'applications/differential/constants/DifferentialLegacyQuery.php', 'DifferentialLineAdjustmentMap' => 'applications/differential/parser/DifferentialLineAdjustmentMap.php', 'DifferentialLintField' => 'applications/differential/customfield/DifferentialLintField.php', 'DifferentialLintStatus' => 'applications/differential/constants/DifferentialLintStatus.php', 'DifferentialLocalCommitsView' => 'applications/differential/view/DifferentialLocalCommitsView.php', 'DifferentialMailEngineExtension' => 'applications/differential/engineextension/DifferentialMailEngineExtension.php', 'DifferentialMailView' => 'applications/differential/mail/DifferentialMailView.php', 'DifferentialManiphestTasksField' => 'applications/differential/customfield/DifferentialManiphestTasksField.php', 'DifferentialParseCacheGarbageCollector' => 'applications/differential/garbagecollector/DifferentialParseCacheGarbageCollector.php', 'DifferentialParseCommitMessageConduitAPIMethod' => 'applications/differential/conduit/DifferentialParseCommitMessageConduitAPIMethod.php', 'DifferentialParseRenderTestCase' => 'applications/differential/__tests__/DifferentialParseRenderTestCase.php', 'DifferentialPathField' => 'applications/differential/customfield/DifferentialPathField.php', 'DifferentialProjectReviewersField' => 'applications/differential/customfield/DifferentialProjectReviewersField.php', 'DifferentialQueryConduitAPIMethod' => 'applications/differential/conduit/DifferentialQueryConduitAPIMethod.php', 'DifferentialQueryDiffsConduitAPIMethod' => 'applications/differential/conduit/DifferentialQueryDiffsConduitAPIMethod.php', 'DifferentialRawDiffRenderer' => 'applications/differential/render/DifferentialRawDiffRenderer.php', 'DifferentialReleephRequestFieldSpecification' => 'applications/releeph/differential/DifferentialReleephRequestFieldSpecification.php', 'DifferentialRemarkupRule' => 'applications/differential/remarkup/DifferentialRemarkupRule.php', 'DifferentialReplyHandler' => 'applications/differential/mail/DifferentialReplyHandler.php', 'DifferentialRepositoryField' => 'applications/differential/customfield/DifferentialRepositoryField.php', 'DifferentialRepositoryLookup' => 'applications/differential/query/DifferentialRepositoryLookup.php', 'DifferentialRequiredSignaturesField' => 'applications/differential/customfield/DifferentialRequiredSignaturesField.php', 'DifferentialResponsibleDatasource' => 'applications/differential/typeahead/DifferentialResponsibleDatasource.php', 'DifferentialResponsibleUserDatasource' => 'applications/differential/typeahead/DifferentialResponsibleUserDatasource.php', 'DifferentialResponsibleViewerFunctionDatasource' => 'applications/differential/typeahead/DifferentialResponsibleViewerFunctionDatasource.php', 'DifferentialRevertPlanCommitMessageField' => 'applications/differential/field/DifferentialRevertPlanCommitMessageField.php', 'DifferentialRevertPlanField' => 'applications/differential/customfield/DifferentialRevertPlanField.php', 'DifferentialReviewedByCommitMessageField' => 'applications/differential/field/DifferentialReviewedByCommitMessageField.php', 'DifferentialReviewer' => 'applications/differential/storage/DifferentialReviewer.php', 'DifferentialReviewerDatasource' => 'applications/differential/typeahead/DifferentialReviewerDatasource.php', 'DifferentialReviewerForRevisionEdgeType' => 'applications/differential/edge/DifferentialReviewerForRevisionEdgeType.php', 'DifferentialReviewerStatus' => 'applications/differential/constants/DifferentialReviewerStatus.php', 'DifferentialReviewersAddBlockingReviewersHeraldAction' => 'applications/differential/herald/DifferentialReviewersAddBlockingReviewersHeraldAction.php', 'DifferentialReviewersAddBlockingSelfHeraldAction' => 'applications/differential/herald/DifferentialReviewersAddBlockingSelfHeraldAction.php', 'DifferentialReviewersAddReviewersHeraldAction' => 'applications/differential/herald/DifferentialReviewersAddReviewersHeraldAction.php', 'DifferentialReviewersAddSelfHeraldAction' => 'applications/differential/herald/DifferentialReviewersAddSelfHeraldAction.php', 'DifferentialReviewersCommitMessageField' => 'applications/differential/field/DifferentialReviewersCommitMessageField.php', 'DifferentialReviewersField' => 'applications/differential/customfield/DifferentialReviewersField.php', 'DifferentialReviewersHeraldAction' => 'applications/differential/herald/DifferentialReviewersHeraldAction.php', 'DifferentialReviewersSearchEngineAttachment' => 'applications/differential/engineextension/DifferentialReviewersSearchEngineAttachment.php', 'DifferentialReviewersView' => 'applications/differential/view/DifferentialReviewersView.php', 'DifferentialRevision' => 'applications/differential/storage/DifferentialRevision.php', 'DifferentialRevisionAbandonTransaction' => 'applications/differential/xaction/DifferentialRevisionAbandonTransaction.php', 'DifferentialRevisionAcceptTransaction' => 'applications/differential/xaction/DifferentialRevisionAcceptTransaction.php', 'DifferentialRevisionActionTransaction' => 'applications/differential/xaction/DifferentialRevisionActionTransaction.php', 'DifferentialRevisionAffectedFilesHeraldField' => 'applications/differential/herald/DifferentialRevisionAffectedFilesHeraldField.php', 'DifferentialRevisionAuthorHeraldField' => 'applications/differential/herald/DifferentialRevisionAuthorHeraldField.php', 'DifferentialRevisionAuthorProjectsHeraldField' => 'applications/differential/herald/DifferentialRevisionAuthorProjectsHeraldField.php', 'DifferentialRevisionBuildableTransaction' => 'applications/differential/xaction/DifferentialRevisionBuildableTransaction.php', 'DifferentialRevisionCloseDetailsController' => 'applications/differential/controller/DifferentialRevisionCloseDetailsController.php', 'DifferentialRevisionCloseTransaction' => 'applications/differential/xaction/DifferentialRevisionCloseTransaction.php', 'DifferentialRevisionClosedStatusDatasource' => 'applications/differential/typeahead/DifferentialRevisionClosedStatusDatasource.php', 'DifferentialRevisionCommandeerTransaction' => 'applications/differential/xaction/DifferentialRevisionCommandeerTransaction.php', 'DifferentialRevisionContentAddedHeraldField' => 'applications/differential/herald/DifferentialRevisionContentAddedHeraldField.php', 'DifferentialRevisionContentHeraldField' => 'applications/differential/herald/DifferentialRevisionContentHeraldField.php', 'DifferentialRevisionContentRemovedHeraldField' => 'applications/differential/herald/DifferentialRevisionContentRemovedHeraldField.php', 'DifferentialRevisionControlSystem' => 'applications/differential/constants/DifferentialRevisionControlSystem.php', 'DifferentialRevisionDependedOnByRevisionEdgeType' => 'applications/differential/edge/DifferentialRevisionDependedOnByRevisionEdgeType.php', 'DifferentialRevisionDependsOnRevisionEdgeType' => 'applications/differential/edge/DifferentialRevisionDependsOnRevisionEdgeType.php', 'DifferentialRevisionDraftEngine' => 'applications/differential/engine/DifferentialRevisionDraftEngine.php', 'DifferentialRevisionEditConduitAPIMethod' => 'applications/differential/conduit/DifferentialRevisionEditConduitAPIMethod.php', 'DifferentialRevisionEditController' => 'applications/differential/controller/DifferentialRevisionEditController.php', 'DifferentialRevisionEditEngine' => 'applications/differential/editor/DifferentialRevisionEditEngine.php', 'DifferentialRevisionFerretEngine' => 'applications/differential/search/DifferentialRevisionFerretEngine.php', 'DifferentialRevisionFulltextEngine' => 'applications/differential/search/DifferentialRevisionFulltextEngine.php', 'DifferentialRevisionGraph' => 'infrastructure/graph/DifferentialRevisionGraph.php', 'DifferentialRevisionHasChildRelationship' => 'applications/differential/relationships/DifferentialRevisionHasChildRelationship.php', 'DifferentialRevisionHasCommitEdgeType' => 'applications/differential/edge/DifferentialRevisionHasCommitEdgeType.php', 'DifferentialRevisionHasCommitRelationship' => 'applications/differential/relationships/DifferentialRevisionHasCommitRelationship.php', 'DifferentialRevisionHasParentRelationship' => 'applications/differential/relationships/DifferentialRevisionHasParentRelationship.php', 'DifferentialRevisionHasReviewerEdgeType' => 'applications/differential/edge/DifferentialRevisionHasReviewerEdgeType.php', 'DifferentialRevisionHasTaskEdgeType' => 'applications/differential/edge/DifferentialRevisionHasTaskEdgeType.php', 'DifferentialRevisionHasTaskRelationship' => 'applications/differential/relationships/DifferentialRevisionHasTaskRelationship.php', 'DifferentialRevisionHeraldField' => 'applications/differential/herald/DifferentialRevisionHeraldField.php', 'DifferentialRevisionHeraldFieldGroup' => 'applications/differential/herald/DifferentialRevisionHeraldFieldGroup.php', 'DifferentialRevisionHoldDraftTransaction' => 'applications/differential/xaction/DifferentialRevisionHoldDraftTransaction.php', 'DifferentialRevisionIDCommitMessageField' => 'applications/differential/field/DifferentialRevisionIDCommitMessageField.php', 'DifferentialRevisionInlineTransaction' => 'applications/differential/xaction/DifferentialRevisionInlineTransaction.php', 'DifferentialRevisionInlinesController' => 'applications/differential/controller/DifferentialRevisionInlinesController.php', 'DifferentialRevisionListController' => 'applications/differential/controller/DifferentialRevisionListController.php', 'DifferentialRevisionListView' => 'applications/differential/view/DifferentialRevisionListView.php', 'DifferentialRevisionMailReceiver' => 'applications/differential/mail/DifferentialRevisionMailReceiver.php', 'DifferentialRevisionOpenStatusDatasource' => 'applications/differential/typeahead/DifferentialRevisionOpenStatusDatasource.php', 'DifferentialRevisionOperationController' => 'applications/differential/controller/DifferentialRevisionOperationController.php', 'DifferentialRevisionPHIDType' => 'applications/differential/phid/DifferentialRevisionPHIDType.php', 'DifferentialRevisionPackageHeraldField' => 'applications/differential/herald/DifferentialRevisionPackageHeraldField.php', 'DifferentialRevisionPackageOwnerHeraldField' => 'applications/differential/herald/DifferentialRevisionPackageOwnerHeraldField.php', 'DifferentialRevisionPlanChangesTransaction' => 'applications/differential/xaction/DifferentialRevisionPlanChangesTransaction.php', 'DifferentialRevisionQuery' => 'applications/differential/query/DifferentialRevisionQuery.php', 'DifferentialRevisionReclaimTransaction' => 'applications/differential/xaction/DifferentialRevisionReclaimTransaction.php', 'DifferentialRevisionRejectTransaction' => 'applications/differential/xaction/DifferentialRevisionRejectTransaction.php', 'DifferentialRevisionRelationship' => 'applications/differential/relationships/DifferentialRevisionRelationship.php', 'DifferentialRevisionRelationshipSource' => 'applications/search/relationship/DifferentialRevisionRelationshipSource.php', 'DifferentialRevisionReopenTransaction' => 'applications/differential/xaction/DifferentialRevisionReopenTransaction.php', 'DifferentialRevisionRepositoryHeraldField' => 'applications/differential/herald/DifferentialRevisionRepositoryHeraldField.php', 'DifferentialRevisionRepositoryProjectsHeraldField' => 'applications/differential/herald/DifferentialRevisionRepositoryProjectsHeraldField.php', 'DifferentialRevisionRepositoryTransaction' => 'applications/differential/xaction/DifferentialRevisionRepositoryTransaction.php', 'DifferentialRevisionRequestReviewTransaction' => 'applications/differential/xaction/DifferentialRevisionRequestReviewTransaction.php', 'DifferentialRevisionRequiredActionResultBucket' => 'applications/differential/query/DifferentialRevisionRequiredActionResultBucket.php', 'DifferentialRevisionResignTransaction' => 'applications/differential/xaction/DifferentialRevisionResignTransaction.php', 'DifferentialRevisionResultBucket' => 'applications/differential/query/DifferentialRevisionResultBucket.php', 'DifferentialRevisionReviewTransaction' => 'applications/differential/xaction/DifferentialRevisionReviewTransaction.php', 'DifferentialRevisionReviewersHeraldField' => 'applications/differential/herald/DifferentialRevisionReviewersHeraldField.php', 'DifferentialRevisionReviewersTransaction' => 'applications/differential/xaction/DifferentialRevisionReviewersTransaction.php', 'DifferentialRevisionSearchConduitAPIMethod' => 'applications/differential/conduit/DifferentialRevisionSearchConduitAPIMethod.php', 'DifferentialRevisionSearchEngine' => 'applications/differential/query/DifferentialRevisionSearchEngine.php', 'DifferentialRevisionStatus' => 'applications/differential/constants/DifferentialRevisionStatus.php', 'DifferentialRevisionStatusDatasource' => 'applications/differential/typeahead/DifferentialRevisionStatusDatasource.php', 'DifferentialRevisionStatusFunctionDatasource' => 'applications/differential/typeahead/DifferentialRevisionStatusFunctionDatasource.php', 'DifferentialRevisionStatusHeraldField' => 'applications/differential/herald/DifferentialRevisionStatusHeraldField.php', 'DifferentialRevisionStatusTransaction' => 'applications/differential/xaction/DifferentialRevisionStatusTransaction.php', 'DifferentialRevisionSummaryHeraldField' => 'applications/differential/herald/DifferentialRevisionSummaryHeraldField.php', 'DifferentialRevisionSummaryTransaction' => 'applications/differential/xaction/DifferentialRevisionSummaryTransaction.php', 'DifferentialRevisionTestPlanHeraldField' => 'applications/differential/herald/DifferentialRevisionTestPlanHeraldField.php', 'DifferentialRevisionTestPlanTransaction' => 'applications/differential/xaction/DifferentialRevisionTestPlanTransaction.php', 'DifferentialRevisionTitleHeraldField' => 'applications/differential/herald/DifferentialRevisionTitleHeraldField.php', 'DifferentialRevisionTitleTransaction' => 'applications/differential/xaction/DifferentialRevisionTitleTransaction.php', 'DifferentialRevisionTransactionType' => 'applications/differential/xaction/DifferentialRevisionTransactionType.php', 'DifferentialRevisionUpdateHistoryView' => 'applications/differential/view/DifferentialRevisionUpdateHistoryView.php', 'DifferentialRevisionUpdateTransaction' => 'applications/differential/xaction/DifferentialRevisionUpdateTransaction.php', 'DifferentialRevisionViewController' => 'applications/differential/controller/DifferentialRevisionViewController.php', 'DifferentialRevisionVoidTransaction' => 'applications/differential/xaction/DifferentialRevisionVoidTransaction.php', 'DifferentialRevisionWrongStateTransaction' => 'applications/differential/xaction/DifferentialRevisionWrongStateTransaction.php', 'DifferentialSchemaSpec' => 'applications/differential/storage/DifferentialSchemaSpec.php', 'DifferentialSetDiffPropertyConduitAPIMethod' => 'applications/differential/conduit/DifferentialSetDiffPropertyConduitAPIMethod.php', 'DifferentialStoredCustomField' => 'applications/differential/customfield/DifferentialStoredCustomField.php', 'DifferentialSubscribersCommitMessageField' => 'applications/differential/field/DifferentialSubscribersCommitMessageField.php', 'DifferentialSummaryCommitMessageField' => 'applications/differential/field/DifferentialSummaryCommitMessageField.php', 'DifferentialSummaryField' => 'applications/differential/customfield/DifferentialSummaryField.php', 'DifferentialTagsCommitMessageField' => 'applications/differential/field/DifferentialTagsCommitMessageField.php', 'DifferentialTasksCommitMessageField' => 'applications/differential/field/DifferentialTasksCommitMessageField.php', 'DifferentialTestPlanCommitMessageField' => 'applications/differential/field/DifferentialTestPlanCommitMessageField.php', 'DifferentialTestPlanField' => 'applications/differential/customfield/DifferentialTestPlanField.php', 'DifferentialTitleCommitMessageField' => 'applications/differential/field/DifferentialTitleCommitMessageField.php', 'DifferentialTransaction' => 'applications/differential/storage/DifferentialTransaction.php', 'DifferentialTransactionComment' => 'applications/differential/storage/DifferentialTransactionComment.php', 'DifferentialTransactionEditor' => 'applications/differential/editor/DifferentialTransactionEditor.php', 'DifferentialTransactionQuery' => 'applications/differential/query/DifferentialTransactionQuery.php', 'DifferentialTransactionView' => 'applications/differential/view/DifferentialTransactionView.php', 'DifferentialUnitField' => 'applications/differential/customfield/DifferentialUnitField.php', 'DifferentialUnitStatus' => 'applications/differential/constants/DifferentialUnitStatus.php', 'DifferentialUnitTestResult' => 'applications/differential/constants/DifferentialUnitTestResult.php', 'DifferentialUpdateRevisionConduitAPIMethod' => 'applications/differential/conduit/DifferentialUpdateRevisionConduitAPIMethod.php', 'DiffusionAuditorDatasource' => 'applications/diffusion/typeahead/DiffusionAuditorDatasource.php', 'DiffusionAuditorFunctionDatasource' => 'applications/diffusion/typeahead/DiffusionAuditorFunctionDatasource.php', 'DiffusionAuditorsAddAuditorsHeraldAction' => 'applications/diffusion/herald/DiffusionAuditorsAddAuditorsHeraldAction.php', 'DiffusionAuditorsAddSelfHeraldAction' => 'applications/diffusion/herald/DiffusionAuditorsAddSelfHeraldAction.php', 'DiffusionAuditorsHeraldAction' => 'applications/diffusion/herald/DiffusionAuditorsHeraldAction.php', 'DiffusionBlameConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionBlameConduitAPIMethod.php', 'DiffusionBlameController' => 'applications/diffusion/controller/DiffusionBlameController.php', 'DiffusionBlameQuery' => 'applications/diffusion/query/blame/DiffusionBlameQuery.php', 'DiffusionBlockHeraldAction' => 'applications/diffusion/herald/DiffusionBlockHeraldAction.php', 'DiffusionBranchListView' => 'applications/diffusion/view/DiffusionBranchListView.php', 'DiffusionBranchQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionBranchQueryConduitAPIMethod.php', 'DiffusionBranchTableController' => 'applications/diffusion/controller/DiffusionBranchTableController.php', 'DiffusionBranchTableView' => 'applications/diffusion/view/DiffusionBranchTableView.php', 'DiffusionBrowseController' => 'applications/diffusion/controller/DiffusionBrowseController.php', 'DiffusionBrowseQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php', 'DiffusionBrowseResultSet' => 'applications/diffusion/data/DiffusionBrowseResultSet.php', 'DiffusionBrowseTableView' => 'applications/diffusion/view/DiffusionBrowseTableView.php', 'DiffusionBuildableEngine' => 'applications/diffusion/harbormaster/DiffusionBuildableEngine.php', 'DiffusionCacheEngineExtension' => 'applications/diffusion/engineextension/DiffusionCacheEngineExtension.php', 'DiffusionCachedResolveRefsQuery' => 'applications/diffusion/query/DiffusionCachedResolveRefsQuery.php', 'DiffusionChangeController' => 'applications/diffusion/controller/DiffusionChangeController.php', 'DiffusionChangeHeraldFieldGroup' => 'applications/diffusion/herald/DiffusionChangeHeraldFieldGroup.php', 'DiffusionCloneController' => 'applications/diffusion/controller/DiffusionCloneController.php', 'DiffusionCloneURIView' => 'applications/diffusion/view/DiffusionCloneURIView.php', 'DiffusionCommandEngine' => 'applications/diffusion/protocol/DiffusionCommandEngine.php', 'DiffusionCommandEngineTestCase' => 'applications/diffusion/protocol/__tests__/DiffusionCommandEngineTestCase.php', 'DiffusionCommitAcceptTransaction' => 'applications/diffusion/xaction/DiffusionCommitAcceptTransaction.php', 'DiffusionCommitActionTransaction' => 'applications/diffusion/xaction/DiffusionCommitActionTransaction.php', 'DiffusionCommitAffectedFilesHeraldField' => 'applications/diffusion/herald/DiffusionCommitAffectedFilesHeraldField.php', 'DiffusionCommitAuditStatus' => 'applications/diffusion/DiffusionCommitAuditStatus.php', 'DiffusionCommitAuditTransaction' => 'applications/diffusion/xaction/DiffusionCommitAuditTransaction.php', 'DiffusionCommitAuditorsHeraldField' => 'applications/diffusion/herald/DiffusionCommitAuditorsHeraldField.php', 'DiffusionCommitAuditorsTransaction' => 'applications/diffusion/xaction/DiffusionCommitAuditorsTransaction.php', 'DiffusionCommitAuthorHeraldField' => 'applications/diffusion/herald/DiffusionCommitAuthorHeraldField.php', 'DiffusionCommitAuthorProjectsHeraldField' => 'applications/diffusion/herald/DiffusionCommitAuthorProjectsHeraldField.php', 'DiffusionCommitAutocloseHeraldField' => 'applications/diffusion/herald/DiffusionCommitAutocloseHeraldField.php', 'DiffusionCommitBranchesController' => 'applications/diffusion/controller/DiffusionCommitBranchesController.php', 'DiffusionCommitBranchesHeraldField' => 'applications/diffusion/herald/DiffusionCommitBranchesHeraldField.php', 'DiffusionCommitBuildableTransaction' => 'applications/diffusion/xaction/DiffusionCommitBuildableTransaction.php', 'DiffusionCommitCommitterHeraldField' => 'applications/diffusion/herald/DiffusionCommitCommitterHeraldField.php', 'DiffusionCommitCommitterProjectsHeraldField' => 'applications/diffusion/herald/DiffusionCommitCommitterProjectsHeraldField.php', 'DiffusionCommitConcernTransaction' => 'applications/diffusion/xaction/DiffusionCommitConcernTransaction.php', 'DiffusionCommitController' => 'applications/diffusion/controller/DiffusionCommitController.php', 'DiffusionCommitDiffContentAddedHeraldField' => 'applications/diffusion/herald/DiffusionCommitDiffContentAddedHeraldField.php', 'DiffusionCommitDiffContentHeraldField' => 'applications/diffusion/herald/DiffusionCommitDiffContentHeraldField.php', 'DiffusionCommitDiffContentRemovedHeraldField' => 'applications/diffusion/herald/DiffusionCommitDiffContentRemovedHeraldField.php', 'DiffusionCommitDiffEnormousHeraldField' => 'applications/diffusion/herald/DiffusionCommitDiffEnormousHeraldField.php', 'DiffusionCommitDraftEngine' => 'applications/diffusion/engine/DiffusionCommitDraftEngine.php', 'DiffusionCommitEditConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionCommitEditConduitAPIMethod.php', 'DiffusionCommitEditController' => 'applications/diffusion/controller/DiffusionCommitEditController.php', 'DiffusionCommitEditEngine' => 'applications/diffusion/editor/DiffusionCommitEditEngine.php', 'DiffusionCommitFerretEngine' => 'applications/repository/search/DiffusionCommitFerretEngine.php', 'DiffusionCommitFulltextEngine' => 'applications/repository/search/DiffusionCommitFulltextEngine.php', 'DiffusionCommitHasPackageEdgeType' => 'applications/diffusion/edge/DiffusionCommitHasPackageEdgeType.php', 'DiffusionCommitHasRevisionEdgeType' => 'applications/diffusion/edge/DiffusionCommitHasRevisionEdgeType.php', 'DiffusionCommitHasRevisionRelationship' => 'applications/diffusion/relationships/DiffusionCommitHasRevisionRelationship.php', 'DiffusionCommitHasTaskEdgeType' => 'applications/diffusion/edge/DiffusionCommitHasTaskEdgeType.php', 'DiffusionCommitHasTaskRelationship' => 'applications/diffusion/relationships/DiffusionCommitHasTaskRelationship.php', 'DiffusionCommitHash' => 'applications/diffusion/data/DiffusionCommitHash.php', 'DiffusionCommitHeraldField' => 'applications/diffusion/herald/DiffusionCommitHeraldField.php', 'DiffusionCommitHeraldFieldGroup' => 'applications/diffusion/herald/DiffusionCommitHeraldFieldGroup.php', 'DiffusionCommitHintQuery' => 'applications/diffusion/query/DiffusionCommitHintQuery.php', 'DiffusionCommitHookEngine' => 'applications/diffusion/engine/DiffusionCommitHookEngine.php', 'DiffusionCommitHookRejectException' => 'applications/diffusion/exception/DiffusionCommitHookRejectException.php', 'DiffusionCommitListController' => 'applications/diffusion/controller/DiffusionCommitListController.php', 'DiffusionCommitListView' => 'applications/diffusion/view/DiffusionCommitListView.php', 'DiffusionCommitMergeHeraldField' => 'applications/diffusion/herald/DiffusionCommitMergeHeraldField.php', 'DiffusionCommitMessageHeraldField' => 'applications/diffusion/herald/DiffusionCommitMessageHeraldField.php', 'DiffusionCommitPackageAuditHeraldField' => 'applications/diffusion/herald/DiffusionCommitPackageAuditHeraldField.php', 'DiffusionCommitPackageHeraldField' => 'applications/diffusion/herald/DiffusionCommitPackageHeraldField.php', 'DiffusionCommitPackageOwnerHeraldField' => 'applications/diffusion/herald/DiffusionCommitPackageOwnerHeraldField.php', 'DiffusionCommitParentsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionCommitParentsQueryConduitAPIMethod.php', 'DiffusionCommitQuery' => 'applications/diffusion/query/DiffusionCommitQuery.php', 'DiffusionCommitRef' => 'applications/diffusion/data/DiffusionCommitRef.php', 'DiffusionCommitRelationship' => 'applications/diffusion/relationships/DiffusionCommitRelationship.php', 'DiffusionCommitRelationshipSource' => 'applications/search/relationship/DiffusionCommitRelationshipSource.php', 'DiffusionCommitRemarkupRule' => 'applications/diffusion/remarkup/DiffusionCommitRemarkupRule.php', 'DiffusionCommitRemarkupRuleTestCase' => 'applications/diffusion/remarkup/__tests__/DiffusionCommitRemarkupRuleTestCase.php', 'DiffusionCommitRepositoryHeraldField' => 'applications/diffusion/herald/DiffusionCommitRepositoryHeraldField.php', 'DiffusionCommitRepositoryProjectsHeraldField' => 'applications/diffusion/herald/DiffusionCommitRepositoryProjectsHeraldField.php', 'DiffusionCommitRequiredActionResultBucket' => 'applications/diffusion/query/DiffusionCommitRequiredActionResultBucket.php', 'DiffusionCommitResignTransaction' => 'applications/diffusion/xaction/DiffusionCommitResignTransaction.php', 'DiffusionCommitResultBucket' => 'applications/diffusion/query/DiffusionCommitResultBucket.php', 'DiffusionCommitRevertedByCommitEdgeType' => 'applications/diffusion/edge/DiffusionCommitRevertedByCommitEdgeType.php', 'DiffusionCommitRevertsCommitEdgeType' => 'applications/diffusion/edge/DiffusionCommitRevertsCommitEdgeType.php', 'DiffusionCommitReviewerHeraldField' => 'applications/diffusion/herald/DiffusionCommitReviewerHeraldField.php', 'DiffusionCommitRevisionAcceptedHeraldField' => 'applications/diffusion/herald/DiffusionCommitRevisionAcceptedHeraldField.php', 'DiffusionCommitRevisionAcceptingReviewersHeraldField' => 'applications/diffusion/herald/DiffusionCommitRevisionAcceptingReviewersHeraldField.php', 'DiffusionCommitRevisionHeraldField' => 'applications/diffusion/herald/DiffusionCommitRevisionHeraldField.php', 'DiffusionCommitRevisionReviewersHeraldField' => 'applications/diffusion/herald/DiffusionCommitRevisionReviewersHeraldField.php', 'DiffusionCommitRevisionSubscribersHeraldField' => 'applications/diffusion/herald/DiffusionCommitRevisionSubscribersHeraldField.php', 'DiffusionCommitSearchConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionCommitSearchConduitAPIMethod.php', 'DiffusionCommitStateTransaction' => 'applications/diffusion/xaction/DiffusionCommitStateTransaction.php', 'DiffusionCommitTagsController' => 'applications/diffusion/controller/DiffusionCommitTagsController.php', 'DiffusionCommitTransactionType' => 'applications/diffusion/xaction/DiffusionCommitTransactionType.php', 'DiffusionCommitVerifyTransaction' => 'applications/diffusion/xaction/DiffusionCommitVerifyTransaction.php', 'DiffusionCompareController' => 'applications/diffusion/controller/DiffusionCompareController.php', 'DiffusionConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionConduitAPIMethod.php', 'DiffusionController' => 'applications/diffusion/controller/DiffusionController.php', 'DiffusionCreateRepositoriesCapability' => 'applications/diffusion/capability/DiffusionCreateRepositoriesCapability.php', 'DiffusionDaemonLockException' => 'applications/diffusion/exception/DiffusionDaemonLockException.php', 'DiffusionDatasourceEngineExtension' => 'applications/diffusion/engineextension/DiffusionDatasourceEngineExtension.php', 'DiffusionDefaultEditCapability' => 'applications/diffusion/capability/DiffusionDefaultEditCapability.php', 'DiffusionDefaultPushCapability' => 'applications/diffusion/capability/DiffusionDefaultPushCapability.php', 'DiffusionDefaultViewCapability' => 'applications/diffusion/capability/DiffusionDefaultViewCapability.php', 'DiffusionDiffController' => 'applications/diffusion/controller/DiffusionDiffController.php', 'DiffusionDiffInlineCommentQuery' => 'applications/diffusion/query/DiffusionDiffInlineCommentQuery.php', 'DiffusionDiffQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionDiffQueryConduitAPIMethod.php', 'DiffusionDocumentController' => 'applications/diffusion/controller/DiffusionDocumentController.php', 'DiffusionDocumentRenderingEngine' => 'applications/diffusion/document/DiffusionDocumentRenderingEngine.php', 'DiffusionDoorkeeperCommitFeedStoryPublisher' => 'applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php', 'DiffusionEmptyResultView' => 'applications/diffusion/view/DiffusionEmptyResultView.php', 'DiffusionExistsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionExistsQueryConduitAPIMethod.php', 'DiffusionExternalController' => 'applications/diffusion/controller/DiffusionExternalController.php', 'DiffusionExternalSymbolQuery' => 'applications/diffusion/symbol/DiffusionExternalSymbolQuery.php', 'DiffusionExternalSymbolsSource' => 'applications/diffusion/symbol/DiffusionExternalSymbolsSource.php', 'DiffusionFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionFileContentQuery.php', 'DiffusionFileContentQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionFileContentQueryConduitAPIMethod.php', 'DiffusionFileFutureQuery' => 'applications/diffusion/query/DiffusionFileFutureQuery.php', 'DiffusionFindSymbolsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionFindSymbolsConduitAPIMethod.php', 'DiffusionGetLintMessagesConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionGetLintMessagesConduitAPIMethod.php', 'DiffusionGetRecentCommitsByPathConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionGetRecentCommitsByPathConduitAPIMethod.php', 'DiffusionGitBlameQuery' => 'applications/diffusion/query/blame/DiffusionGitBlameQuery.php', 'DiffusionGitBranch' => 'applications/diffusion/data/DiffusionGitBranch.php', 'DiffusionGitBranchTestCase' => 'applications/diffusion/data/__tests__/DiffusionGitBranchTestCase.php', 'DiffusionGitCommandEngine' => 'applications/diffusion/protocol/DiffusionGitCommandEngine.php', 'DiffusionGitFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php', 'DiffusionGitLFSAuthenticateWorkflow' => 'applications/diffusion/gitlfs/DiffusionGitLFSAuthenticateWorkflow.php', 'DiffusionGitLFSResponse' => 'applications/diffusion/response/DiffusionGitLFSResponse.php', 'DiffusionGitLFSTemporaryTokenType' => 'applications/diffusion/gitlfs/DiffusionGitLFSTemporaryTokenType.php', 'DiffusionGitRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionGitRawDiffQuery.php', 'DiffusionGitReceivePackSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php', 'DiffusionGitRequest' => 'applications/diffusion/request/DiffusionGitRequest.php', 'DiffusionGitResponse' => 'applications/diffusion/response/DiffusionGitResponse.php', 'DiffusionGitSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitSSHWorkflow.php', 'DiffusionGitUploadPackSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php', 'DiffusionGraphController' => 'applications/diffusion/controller/DiffusionGraphController.php', 'DiffusionHistoryController' => 'applications/diffusion/controller/DiffusionHistoryController.php', 'DiffusionHistoryListView' => 'applications/diffusion/view/DiffusionHistoryListView.php', 'DiffusionHistoryQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php', 'DiffusionHistoryTableView' => 'applications/diffusion/view/DiffusionHistoryTableView.php', 'DiffusionHistoryView' => 'applications/diffusion/view/DiffusionHistoryView.php', 'DiffusionHovercardEngineExtension' => 'applications/diffusion/engineextension/DiffusionHovercardEngineExtension.php', 'DiffusionIdentityAssigneeDatasource' => 'applications/diffusion/typeahead/DiffusionIdentityAssigneeDatasource.php', 'DiffusionIdentityAssigneeEditField' => 'applications/diffusion/editfield/DiffusionIdentityAssigneeEditField.php', 'DiffusionIdentityAssigneeSearchField' => 'applications/diffusion/searchfield/DiffusionIdentityAssigneeSearchField.php', 'DiffusionIdentityEditController' => 'applications/diffusion/controller/DiffusionIdentityEditController.php', 'DiffusionIdentityListController' => 'applications/diffusion/controller/DiffusionIdentityListController.php', 'DiffusionIdentityUnassignedDatasource' => 'applications/diffusion/typeahead/DiffusionIdentityUnassignedDatasource.php', 'DiffusionIdentityViewController' => 'applications/diffusion/controller/DiffusionIdentityViewController.php', 'DiffusionInlineCommentController' => 'applications/diffusion/controller/DiffusionInlineCommentController.php', 'DiffusionInlineCommentPreviewController' => 'applications/diffusion/controller/DiffusionInlineCommentPreviewController.php', 'DiffusionInternalAncestorsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionInternalAncestorsConduitAPIMethod.php', 'DiffusionInternalGitRawDiffQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionInternalGitRawDiffQueryConduitAPIMethod.php', 'DiffusionLastModifiedController' => 'applications/diffusion/controller/DiffusionLastModifiedController.php', 'DiffusionLastModifiedQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionLastModifiedQueryConduitAPIMethod.php', 'DiffusionLintController' => 'applications/diffusion/controller/DiffusionLintController.php', 'DiffusionLintCountQuery' => 'applications/diffusion/query/DiffusionLintCountQuery.php', 'DiffusionLintSaveRunner' => 'applications/diffusion/DiffusionLintSaveRunner.php', 'DiffusionLocalRepositoryFilter' => 'applications/diffusion/data/DiffusionLocalRepositoryFilter.php', 'DiffusionLogController' => 'applications/diffusion/controller/DiffusionLogController.php', 'DiffusionLookSoonConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionLookSoonConduitAPIMethod.php', 'DiffusionLowLevelCommitFieldsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelCommitFieldsQuery.php', 'DiffusionLowLevelCommitQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelCommitQuery.php', + 'DiffusionLowLevelFilesizeQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelFilesizeQuery.php', 'DiffusionLowLevelGitRefQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelGitRefQuery.php', 'DiffusionLowLevelMercurialBranchesQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelMercurialBranchesQuery.php', 'DiffusionLowLevelMercurialPathsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelMercurialPathsQuery.php', 'DiffusionLowLevelParentsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php', 'DiffusionLowLevelQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelQuery.php', 'DiffusionLowLevelResolveRefsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php', 'DiffusionMercurialBlameQuery' => 'applications/diffusion/query/blame/DiffusionMercurialBlameQuery.php', 'DiffusionMercurialCommandEngine' => 'applications/diffusion/protocol/DiffusionMercurialCommandEngine.php', 'DiffusionMercurialFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionMercurialFileContentQuery.php', 'DiffusionMercurialFlagInjectionException' => 'applications/diffusion/exception/DiffusionMercurialFlagInjectionException.php', 'DiffusionMercurialRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionMercurialRawDiffQuery.php', 'DiffusionMercurialRequest' => 'applications/diffusion/request/DiffusionMercurialRequest.php', 'DiffusionMercurialResponse' => 'applications/diffusion/response/DiffusionMercurialResponse.php', 'DiffusionMercurialSSHWorkflow' => 'applications/diffusion/ssh/DiffusionMercurialSSHWorkflow.php', 'DiffusionMercurialServeSSHWorkflow' => 'applications/diffusion/ssh/DiffusionMercurialServeSSHWorkflow.php', 'DiffusionMercurialWireClientSSHProtocolChannel' => 'applications/diffusion/ssh/DiffusionMercurialWireClientSSHProtocolChannel.php', 'DiffusionMercurialWireProtocol' => 'applications/diffusion/protocol/DiffusionMercurialWireProtocol.php', 'DiffusionMercurialWireProtocolTests' => 'applications/diffusion/protocol/__tests__/DiffusionMercurialWireProtocolTests.php', 'DiffusionMercurialWireSSHTestCase' => 'applications/diffusion/ssh/__tests__/DiffusionMercurialWireSSHTestCase.php', 'DiffusionMergedCommitsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionMergedCommitsQueryConduitAPIMethod.php', 'DiffusionPathChange' => 'applications/diffusion/data/DiffusionPathChange.php', 'DiffusionPathChangeQuery' => 'applications/diffusion/query/pathchange/DiffusionPathChangeQuery.php', 'DiffusionPathCompleteController' => 'applications/diffusion/controller/DiffusionPathCompleteController.php', 'DiffusionPathIDQuery' => 'applications/diffusion/query/pathid/DiffusionPathIDQuery.php', 'DiffusionPathQuery' => 'applications/diffusion/query/DiffusionPathQuery.php', 'DiffusionPathQueryTestCase' => 'applications/diffusion/query/pathid/__tests__/DiffusionPathQueryTestCase.php', 'DiffusionPathTreeController' => 'applications/diffusion/controller/DiffusionPathTreeController.php', 'DiffusionPathValidateController' => 'applications/diffusion/controller/DiffusionPathValidateController.php', 'DiffusionPatternSearchView' => 'applications/diffusion/view/DiffusionPatternSearchView.php', 'DiffusionPhpExternalSymbolsSource' => 'applications/diffusion/symbol/DiffusionPhpExternalSymbolsSource.php', 'DiffusionPreCommitContentAffectedFilesHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAffectedFilesHeraldField.php', 'DiffusionPreCommitContentAuthorHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAuthorHeraldField.php', 'DiffusionPreCommitContentAuthorProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAuthorProjectsHeraldField.php', 'DiffusionPreCommitContentAuthorRawHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAuthorRawHeraldField.php', 'DiffusionPreCommitContentBranchesHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentBranchesHeraldField.php', 'DiffusionPreCommitContentCommitterHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentCommitterHeraldField.php', 'DiffusionPreCommitContentCommitterProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentCommitterProjectsHeraldField.php', 'DiffusionPreCommitContentCommitterRawHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentCommitterRawHeraldField.php', 'DiffusionPreCommitContentDiffContentAddedHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentDiffContentAddedHeraldField.php', 'DiffusionPreCommitContentDiffContentHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentDiffContentHeraldField.php', 'DiffusionPreCommitContentDiffContentRemovedHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentDiffContentRemovedHeraldField.php', 'DiffusionPreCommitContentDiffEnormousHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentDiffEnormousHeraldField.php', 'DiffusionPreCommitContentHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentHeraldField.php', 'DiffusionPreCommitContentMergeHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentMergeHeraldField.php', 'DiffusionPreCommitContentMessageHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentMessageHeraldField.php', 'DiffusionPreCommitContentPackageHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentPackageHeraldField.php', 'DiffusionPreCommitContentPackageOwnerHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentPackageOwnerHeraldField.php', 'DiffusionPreCommitContentPusherHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentPusherHeraldField.php', 'DiffusionPreCommitContentPusherIsCommitterHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentPusherIsCommitterHeraldField.php', 'DiffusionPreCommitContentPusherProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentPusherProjectsHeraldField.php', 'DiffusionPreCommitContentRepositoryHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRepositoryHeraldField.php', 'DiffusionPreCommitContentRepositoryProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRepositoryProjectsHeraldField.php', 'DiffusionPreCommitContentRevisionAcceptedHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRevisionAcceptedHeraldField.php', 'DiffusionPreCommitContentRevisionAcceptingReviewersHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRevisionAcceptingReviewersHeraldField.php', 'DiffusionPreCommitContentRevisionHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRevisionHeraldField.php', 'DiffusionPreCommitContentRevisionReviewersHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRevisionReviewersHeraldField.php', 'DiffusionPreCommitContentRevisionSubscribersHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRevisionSubscribersHeraldField.php', 'DiffusionPreCommitRefChangeHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefChangeHeraldField.php', 'DiffusionPreCommitRefHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefHeraldField.php', 'DiffusionPreCommitRefHeraldFieldGroup' => 'applications/diffusion/herald/DiffusionPreCommitRefHeraldFieldGroup.php', 'DiffusionPreCommitRefNameHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefNameHeraldField.php', 'DiffusionPreCommitRefPusherHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefPusherHeraldField.php', 'DiffusionPreCommitRefPusherProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefPusherProjectsHeraldField.php', 'DiffusionPreCommitRefRepositoryHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefRepositoryHeraldField.php', 'DiffusionPreCommitRefRepositoryProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefRepositoryProjectsHeraldField.php', 'DiffusionPreCommitRefTypeHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefTypeHeraldField.php', 'DiffusionPreCommitUsesGitLFSHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitUsesGitLFSHeraldField.php', 'DiffusionPullEventGarbageCollector' => 'applications/diffusion/garbagecollector/DiffusionPullEventGarbageCollector.php', 'DiffusionPullLogListController' => 'applications/diffusion/controller/DiffusionPullLogListController.php', 'DiffusionPullLogListView' => 'applications/diffusion/view/DiffusionPullLogListView.php', 'DiffusionPullLogSearchEngine' => 'applications/diffusion/query/DiffusionPullLogSearchEngine.php', 'DiffusionPushCapability' => 'applications/diffusion/capability/DiffusionPushCapability.php', 'DiffusionPushEventViewController' => 'applications/diffusion/controller/DiffusionPushEventViewController.php', 'DiffusionPushLogListController' => 'applications/diffusion/controller/DiffusionPushLogListController.php', 'DiffusionPushLogListView' => 'applications/diffusion/view/DiffusionPushLogListView.php', 'DiffusionPythonExternalSymbolsSource' => 'applications/diffusion/symbol/DiffusionPythonExternalSymbolsSource.php', 'DiffusionQuery' => 'applications/diffusion/query/DiffusionQuery.php', 'DiffusionQueryCommitsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionQueryCommitsConduitAPIMethod.php', 'DiffusionQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionQueryConduitAPIMethod.php', 'DiffusionQueryPathsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionQueryPathsConduitAPIMethod.php', 'DiffusionRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionRawDiffQuery.php', 'DiffusionRawDiffQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRawDiffQueryConduitAPIMethod.php', 'DiffusionReadmeView' => 'applications/diffusion/view/DiffusionReadmeView.php', 'DiffusionRefDatasource' => 'applications/diffusion/typeahead/DiffusionRefDatasource.php', 'DiffusionRefNotFoundException' => 'applications/diffusion/exception/DiffusionRefNotFoundException.php', 'DiffusionRefTableController' => 'applications/diffusion/controller/DiffusionRefTableController.php', 'DiffusionRefsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRefsQueryConduitAPIMethod.php', 'DiffusionRenameHistoryQuery' => 'applications/diffusion/query/DiffusionRenameHistoryQuery.php', 'DiffusionRepositoryActionsManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryActionsManagementPanel.php', 'DiffusionRepositoryAutomationManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryAutomationManagementPanel.php', 'DiffusionRepositoryBasicsManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php', 'DiffusionRepositoryBranchesManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryBranchesManagementPanel.php', 'DiffusionRepositoryByIDRemarkupRule' => 'applications/diffusion/remarkup/DiffusionRepositoryByIDRemarkupRule.php', 'DiffusionRepositoryClusterEngine' => 'applications/diffusion/protocol/DiffusionRepositoryClusterEngine.php', 'DiffusionRepositoryClusterEngineLogInterface' => 'applications/diffusion/protocol/DiffusionRepositoryClusterEngineLogInterface.php', 'DiffusionRepositoryController' => 'applications/diffusion/controller/DiffusionRepositoryController.php', 'DiffusionRepositoryDatasource' => 'applications/diffusion/typeahead/DiffusionRepositoryDatasource.php', 'DiffusionRepositoryDefaultController' => 'applications/diffusion/controller/DiffusionRepositoryDefaultController.php', 'DiffusionRepositoryEditActivateController' => 'applications/diffusion/controller/DiffusionRepositoryEditActivateController.php', 'DiffusionRepositoryEditConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRepositoryEditConduitAPIMethod.php', 'DiffusionRepositoryEditController' => 'applications/diffusion/controller/DiffusionRepositoryEditController.php', 'DiffusionRepositoryEditDangerousController' => 'applications/diffusion/controller/DiffusionRepositoryEditDangerousController.php', 'DiffusionRepositoryEditDeleteController' => 'applications/diffusion/controller/DiffusionRepositoryEditDeleteController.php', 'DiffusionRepositoryEditEngine' => 'applications/diffusion/editor/DiffusionRepositoryEditEngine.php', 'DiffusionRepositoryEditEnormousController' => 'applications/diffusion/controller/DiffusionRepositoryEditEnormousController.php', 'DiffusionRepositoryEditUpdateController' => 'applications/diffusion/controller/DiffusionRepositoryEditUpdateController.php', 'DiffusionRepositoryFunctionDatasource' => 'applications/diffusion/typeahead/DiffusionRepositoryFunctionDatasource.php', 'DiffusionRepositoryHistoryManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryHistoryManagementPanel.php', 'DiffusionRepositoryIdentityEditor' => 'applications/diffusion/editor/DiffusionRepositoryIdentityEditor.php', 'DiffusionRepositoryIdentitySearchEngine' => 'applications/diffusion/query/DiffusionRepositoryIdentitySearchEngine.php', + 'DiffusionRepositoryLimitsManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryLimitsManagementPanel.php', 'DiffusionRepositoryListController' => 'applications/diffusion/controller/DiffusionRepositoryListController.php', 'DiffusionRepositoryManageController' => 'applications/diffusion/controller/DiffusionRepositoryManageController.php', 'DiffusionRepositoryManagePanelsController' => 'applications/diffusion/controller/DiffusionRepositoryManagePanelsController.php', + 'DiffusionRepositoryManagementBuildsPanelGroup' => 'applications/diffusion/management/DiffusionRepositoryManagementBuildsPanelGroup.php', + 'DiffusionRepositoryManagementIntegrationsPanelGroup' => 'applications/diffusion/management/DiffusionRepositoryManagementIntegrationsPanelGroup.php', + 'DiffusionRepositoryManagementMainPanelGroup' => 'applications/diffusion/management/DiffusionRepositoryManagementMainPanelGroup.php', + 'DiffusionRepositoryManagementOtherPanelGroup' => 'applications/diffusion/management/DiffusionRepositoryManagementOtherPanelGroup.php', 'DiffusionRepositoryManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryManagementPanel.php', + 'DiffusionRepositoryManagementPanelGroup' => 'applications/diffusion/management/DiffusionRepositoryManagementPanelGroup.php', 'DiffusionRepositoryPath' => 'applications/diffusion/data/DiffusionRepositoryPath.php', 'DiffusionRepositoryPoliciesManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryPoliciesManagementPanel.php', 'DiffusionRepositoryProfilePictureController' => 'applications/diffusion/controller/DiffusionRepositoryProfilePictureController.php', 'DiffusionRepositoryRef' => 'applications/diffusion/data/DiffusionRepositoryRef.php', 'DiffusionRepositoryRemarkupRule' => 'applications/diffusion/remarkup/DiffusionRepositoryRemarkupRule.php', 'DiffusionRepositorySearchConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRepositorySearchConduitAPIMethod.php', 'DiffusionRepositoryStagingManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryStagingManagementPanel.php', 'DiffusionRepositoryStorageManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryStorageManagementPanel.php', 'DiffusionRepositorySubversionManagementPanel' => 'applications/diffusion/management/DiffusionRepositorySubversionManagementPanel.php', 'DiffusionRepositorySymbolsManagementPanel' => 'applications/diffusion/management/DiffusionRepositorySymbolsManagementPanel.php', 'DiffusionRepositoryTag' => 'applications/diffusion/data/DiffusionRepositoryTag.php', 'DiffusionRepositoryTestAutomationController' => 'applications/diffusion/controller/DiffusionRepositoryTestAutomationController.php', 'DiffusionRepositoryURICredentialController' => 'applications/diffusion/controller/DiffusionRepositoryURICredentialController.php', 'DiffusionRepositoryURIDisableController' => 'applications/diffusion/controller/DiffusionRepositoryURIDisableController.php', 'DiffusionRepositoryURIEditController' => 'applications/diffusion/controller/DiffusionRepositoryURIEditController.php', 'DiffusionRepositoryURIViewController' => 'applications/diffusion/controller/DiffusionRepositoryURIViewController.php', 'DiffusionRepositoryURIsIndexEngineExtension' => 'applications/diffusion/engineextension/DiffusionRepositoryURIsIndexEngineExtension.php', 'DiffusionRepositoryURIsManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryURIsManagementPanel.php', 'DiffusionRepositoryURIsSearchEngineAttachment' => 'applications/diffusion/engineextension/DiffusionRepositoryURIsSearchEngineAttachment.php', 'DiffusionRequest' => 'applications/diffusion/request/DiffusionRequest.php', 'DiffusionResolveRefsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionResolveRefsConduitAPIMethod.php', 'DiffusionResolveUserQuery' => 'applications/diffusion/query/DiffusionResolveUserQuery.php', 'DiffusionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSSHWorkflow.php', 'DiffusionSearchQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionSearchQueryConduitAPIMethod.php', 'DiffusionServeController' => 'applications/diffusion/controller/DiffusionServeController.php', 'DiffusionSetPasswordSettingsPanel' => 'applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php', 'DiffusionSetupException' => 'applications/diffusion/exception/DiffusionSetupException.php', 'DiffusionSubversionCommandEngine' => 'applications/diffusion/protocol/DiffusionSubversionCommandEngine.php', 'DiffusionSubversionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSubversionSSHWorkflow.php', 'DiffusionSubversionServeSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php', 'DiffusionSubversionWireProtocol' => 'applications/diffusion/protocol/DiffusionSubversionWireProtocol.php', 'DiffusionSubversionWireProtocolTestCase' => 'applications/diffusion/protocol/__tests__/DiffusionSubversionWireProtocolTestCase.php', 'DiffusionSvnBlameQuery' => 'applications/diffusion/query/blame/DiffusionSvnBlameQuery.php', 'DiffusionSvnFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionSvnFileContentQuery.php', 'DiffusionSvnRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionSvnRawDiffQuery.php', 'DiffusionSvnRequest' => 'applications/diffusion/request/DiffusionSvnRequest.php', 'DiffusionSymbolController' => 'applications/diffusion/controller/DiffusionSymbolController.php', 'DiffusionSymbolDatasource' => 'applications/diffusion/typeahead/DiffusionSymbolDatasource.php', 'DiffusionSymbolQuery' => 'applications/diffusion/query/DiffusionSymbolQuery.php', 'DiffusionSyncLogListController' => 'applications/diffusion/controller/DiffusionSyncLogListController.php', 'DiffusionSyncLogListView' => 'applications/diffusion/view/DiffusionSyncLogListView.php', 'DiffusionSyncLogSearchEngine' => 'applications/diffusion/query/DiffusionSyncLogSearchEngine.php', 'DiffusionTagListController' => 'applications/diffusion/controller/DiffusionTagListController.php', 'DiffusionTagListView' => 'applications/diffusion/view/DiffusionTagListView.php', 'DiffusionTagTableView' => 'applications/diffusion/view/DiffusionTagTableView.php', 'DiffusionTaggedRepositoriesFunctionDatasource' => 'applications/diffusion/typeahead/DiffusionTaggedRepositoriesFunctionDatasource.php', 'DiffusionTagsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionTagsQueryConduitAPIMethod.php', 'DiffusionURIEditConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionURIEditConduitAPIMethod.php', 'DiffusionURIEditEngine' => 'applications/diffusion/editor/DiffusionURIEditEngine.php', 'DiffusionURIEditor' => 'applications/diffusion/editor/DiffusionURIEditor.php', 'DiffusionURITestCase' => 'applications/diffusion/request/__tests__/DiffusionURITestCase.php', 'DiffusionUpdateCoverageConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionUpdateCoverageConduitAPIMethod.php', 'DiffusionView' => 'applications/diffusion/view/DiffusionView.php', 'DivinerArticleAtomizer' => 'applications/diviner/atomizer/DivinerArticleAtomizer.php', 'DivinerAtom' => 'applications/diviner/atom/DivinerAtom.php', 'DivinerAtomCache' => 'applications/diviner/cache/DivinerAtomCache.php', 'DivinerAtomController' => 'applications/diviner/controller/DivinerAtomController.php', 'DivinerAtomListController' => 'applications/diviner/controller/DivinerAtomListController.php', 'DivinerAtomPHIDType' => 'applications/diviner/phid/DivinerAtomPHIDType.php', 'DivinerAtomQuery' => 'applications/diviner/query/DivinerAtomQuery.php', 'DivinerAtomRef' => 'applications/diviner/atom/DivinerAtomRef.php', 'DivinerAtomSearchEngine' => 'applications/diviner/query/DivinerAtomSearchEngine.php', 'DivinerAtomizeWorkflow' => 'applications/diviner/workflow/DivinerAtomizeWorkflow.php', 'DivinerAtomizer' => 'applications/diviner/atomizer/DivinerAtomizer.php', 'DivinerBookController' => 'applications/diviner/controller/DivinerBookController.php', 'DivinerBookDatasource' => 'applications/diviner/typeahead/DivinerBookDatasource.php', 'DivinerBookEditController' => 'applications/diviner/controller/DivinerBookEditController.php', 'DivinerBookItemView' => 'applications/diviner/view/DivinerBookItemView.php', 'DivinerBookPHIDType' => 'applications/diviner/phid/DivinerBookPHIDType.php', 'DivinerBookQuery' => 'applications/diviner/query/DivinerBookQuery.php', 'DivinerController' => 'applications/diviner/controller/DivinerController.php', 'DivinerDAO' => 'applications/diviner/storage/DivinerDAO.php', 'DivinerDefaultEditCapability' => 'applications/diviner/capability/DivinerDefaultEditCapability.php', 'DivinerDefaultRenderer' => 'applications/diviner/renderer/DivinerDefaultRenderer.php', 'DivinerDefaultViewCapability' => 'applications/diviner/capability/DivinerDefaultViewCapability.php', 'DivinerDiskCache' => 'applications/diviner/cache/DivinerDiskCache.php', 'DivinerFileAtomizer' => 'applications/diviner/atomizer/DivinerFileAtomizer.php', 'DivinerFindController' => 'applications/diviner/controller/DivinerFindController.php', 'DivinerGenerateWorkflow' => 'applications/diviner/workflow/DivinerGenerateWorkflow.php', 'DivinerLiveAtom' => 'applications/diviner/storage/DivinerLiveAtom.php', 'DivinerLiveBook' => 'applications/diviner/storage/DivinerLiveBook.php', 'DivinerLiveBookEditor' => 'applications/diviner/editor/DivinerLiveBookEditor.php', 'DivinerLiveBookFulltextEngine' => 'applications/diviner/search/DivinerLiveBookFulltextEngine.php', 'DivinerLiveBookTransaction' => 'applications/diviner/storage/DivinerLiveBookTransaction.php', 'DivinerLiveBookTransactionQuery' => 'applications/diviner/query/DivinerLiveBookTransactionQuery.php', 'DivinerLivePublisher' => 'applications/diviner/publisher/DivinerLivePublisher.php', 'DivinerLiveSymbol' => 'applications/diviner/storage/DivinerLiveSymbol.php', 'DivinerLiveSymbolFulltextEngine' => 'applications/diviner/search/DivinerLiveSymbolFulltextEngine.php', 'DivinerMainController' => 'applications/diviner/controller/DivinerMainController.php', 'DivinerPHPAtomizer' => 'applications/diviner/atomizer/DivinerPHPAtomizer.php', 'DivinerParameterTableView' => 'applications/diviner/view/DivinerParameterTableView.php', 'DivinerPublishCache' => 'applications/diviner/cache/DivinerPublishCache.php', 'DivinerPublisher' => 'applications/diviner/publisher/DivinerPublisher.php', 'DivinerRenderer' => 'applications/diviner/renderer/DivinerRenderer.php', 'DivinerReturnTableView' => 'applications/diviner/view/DivinerReturnTableView.php', 'DivinerSchemaSpec' => 'applications/diviner/storage/DivinerSchemaSpec.php', 'DivinerSectionView' => 'applications/diviner/view/DivinerSectionView.php', 'DivinerStaticPublisher' => 'applications/diviner/publisher/DivinerStaticPublisher.php', 'DivinerSymbolRemarkupRule' => 'applications/diviner/markup/DivinerSymbolRemarkupRule.php', 'DivinerWorkflow' => 'applications/diviner/workflow/DivinerWorkflow.php', 'DoorkeeperAsanaFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperAsanaFeedWorker.php', 'DoorkeeperAsanaRemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperAsanaRemarkupRule.php', 'DoorkeeperBridge' => 'applications/doorkeeper/bridge/DoorkeeperBridge.php', 'DoorkeeperBridgeAsana' => 'applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php', 'DoorkeeperBridgeGitHub' => 'applications/doorkeeper/bridge/DoorkeeperBridgeGitHub.php', 'DoorkeeperBridgeGitHubIssue' => 'applications/doorkeeper/bridge/DoorkeeperBridgeGitHubIssue.php', 'DoorkeeperBridgeGitHubUser' => 'applications/doorkeeper/bridge/DoorkeeperBridgeGitHubUser.php', 'DoorkeeperBridgeJIRA' => 'applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php', 'DoorkeeperBridgeJIRATestCase' => 'applications/doorkeeper/bridge/__tests__/DoorkeeperBridgeJIRATestCase.php', 'DoorkeeperBridgedObjectCurtainExtension' => 'applications/doorkeeper/engineextension/DoorkeeperBridgedObjectCurtainExtension.php', 'DoorkeeperBridgedObjectInterface' => 'applications/doorkeeper/bridge/DoorkeeperBridgedObjectInterface.php', 'DoorkeeperDAO' => 'applications/doorkeeper/storage/DoorkeeperDAO.php', 'DoorkeeperExternalObject' => 'applications/doorkeeper/storage/DoorkeeperExternalObject.php', 'DoorkeeperExternalObjectPHIDType' => 'applications/doorkeeper/phid/DoorkeeperExternalObjectPHIDType.php', 'DoorkeeperExternalObjectQuery' => 'applications/doorkeeper/query/DoorkeeperExternalObjectQuery.php', 'DoorkeeperFeedStoryPublisher' => 'applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php', 'DoorkeeperFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperFeedWorker.php', 'DoorkeeperImportEngine' => 'applications/doorkeeper/engine/DoorkeeperImportEngine.php', 'DoorkeeperJIRAFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperJIRAFeedWorker.php', 'DoorkeeperJIRARemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperJIRARemarkupRule.php', 'DoorkeeperMissingLinkException' => 'applications/doorkeeper/exception/DoorkeeperMissingLinkException.php', 'DoorkeeperObjectRef' => 'applications/doorkeeper/engine/DoorkeeperObjectRef.php', 'DoorkeeperRemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php', 'DoorkeeperSchemaSpec' => 'applications/doorkeeper/storage/DoorkeeperSchemaSpec.php', 'DoorkeeperTagView' => 'applications/doorkeeper/view/DoorkeeperTagView.php', 'DoorkeeperTagsController' => 'applications/doorkeeper/controller/DoorkeeperTagsController.php', 'DrydockAcquiredBrokenResourceException' => 'applications/drydock/exception/DrydockAcquiredBrokenResourceException.php', 'DrydockAlmanacServiceHostBlueprintImplementation' => 'applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php', 'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php', 'DrydockAuthorization' => 'applications/drydock/storage/DrydockAuthorization.php', 'DrydockAuthorizationAuthorizeController' => 'applications/drydock/controller/DrydockAuthorizationAuthorizeController.php', 'DrydockAuthorizationListController' => 'applications/drydock/controller/DrydockAuthorizationListController.php', 'DrydockAuthorizationListView' => 'applications/drydock/view/DrydockAuthorizationListView.php', 'DrydockAuthorizationPHIDType' => 'applications/drydock/phid/DrydockAuthorizationPHIDType.php', 'DrydockAuthorizationQuery' => 'applications/drydock/query/DrydockAuthorizationQuery.php', 'DrydockAuthorizationSearchConduitAPIMethod' => 'applications/drydock/conduit/DrydockAuthorizationSearchConduitAPIMethod.php', 'DrydockAuthorizationSearchEngine' => 'applications/drydock/query/DrydockAuthorizationSearchEngine.php', 'DrydockAuthorizationViewController' => 'applications/drydock/controller/DrydockAuthorizationViewController.php', 'DrydockBlueprint' => 'applications/drydock/storage/DrydockBlueprint.php', 'DrydockBlueprintController' => 'applications/drydock/controller/DrydockBlueprintController.php', 'DrydockBlueprintCoreCustomField' => 'applications/drydock/customfield/DrydockBlueprintCoreCustomField.php', 'DrydockBlueprintCustomField' => 'applications/drydock/customfield/DrydockBlueprintCustomField.php', 'DrydockBlueprintDatasource' => 'applications/drydock/typeahead/DrydockBlueprintDatasource.php', 'DrydockBlueprintDisableController' => 'applications/drydock/controller/DrydockBlueprintDisableController.php', 'DrydockBlueprintDisableTransaction' => 'applications/drydock/xaction/DrydockBlueprintDisableTransaction.php', 'DrydockBlueprintEditConduitAPIMethod' => 'applications/drydock/conduit/DrydockBlueprintEditConduitAPIMethod.php', 'DrydockBlueprintEditController' => 'applications/drydock/controller/DrydockBlueprintEditController.php', 'DrydockBlueprintEditEngine' => 'applications/drydock/editor/DrydockBlueprintEditEngine.php', 'DrydockBlueprintEditor' => 'applications/drydock/editor/DrydockBlueprintEditor.php', 'DrydockBlueprintImplementation' => 'applications/drydock/blueprint/DrydockBlueprintImplementation.php', 'DrydockBlueprintImplementationTestCase' => 'applications/drydock/blueprint/__tests__/DrydockBlueprintImplementationTestCase.php', 'DrydockBlueprintListController' => 'applications/drydock/controller/DrydockBlueprintListController.php', 'DrydockBlueprintNameNgrams' => 'applications/drydock/storage/DrydockBlueprintNameNgrams.php', 'DrydockBlueprintNameTransaction' => 'applications/drydock/xaction/DrydockBlueprintNameTransaction.php', 'DrydockBlueprintPHIDType' => 'applications/drydock/phid/DrydockBlueprintPHIDType.php', 'DrydockBlueprintQuery' => 'applications/drydock/query/DrydockBlueprintQuery.php', 'DrydockBlueprintSearchConduitAPIMethod' => 'applications/drydock/conduit/DrydockBlueprintSearchConduitAPIMethod.php', 'DrydockBlueprintSearchEngine' => 'applications/drydock/query/DrydockBlueprintSearchEngine.php', 'DrydockBlueprintTransaction' => 'applications/drydock/storage/DrydockBlueprintTransaction.php', 'DrydockBlueprintTransactionQuery' => 'applications/drydock/query/DrydockBlueprintTransactionQuery.php', 'DrydockBlueprintTransactionType' => 'applications/drydock/xaction/DrydockBlueprintTransactionType.php', 'DrydockBlueprintTypeTransaction' => 'applications/drydock/xaction/DrydockBlueprintTypeTransaction.php', 'DrydockBlueprintViewController' => 'applications/drydock/controller/DrydockBlueprintViewController.php', 'DrydockCommand' => 'applications/drydock/storage/DrydockCommand.php', 'DrydockCommandError' => 'applications/drydock/exception/DrydockCommandError.php', 'DrydockCommandInterface' => 'applications/drydock/interface/command/DrydockCommandInterface.php', 'DrydockCommandQuery' => 'applications/drydock/query/DrydockCommandQuery.php', 'DrydockConsoleController' => 'applications/drydock/controller/DrydockConsoleController.php', 'DrydockController' => 'applications/drydock/controller/DrydockController.php', 'DrydockCreateBlueprintsCapability' => 'applications/drydock/capability/DrydockCreateBlueprintsCapability.php', 'DrydockDAO' => 'applications/drydock/storage/DrydockDAO.php', 'DrydockDefaultEditCapability' => 'applications/drydock/capability/DrydockDefaultEditCapability.php', 'DrydockDefaultViewCapability' => 'applications/drydock/capability/DrydockDefaultViewCapability.php', 'DrydockFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockFilesystemInterface.php', 'DrydockInterface' => 'applications/drydock/interface/DrydockInterface.php', 'DrydockLandRepositoryOperation' => 'applications/drydock/operation/DrydockLandRepositoryOperation.php', 'DrydockLease' => 'applications/drydock/storage/DrydockLease.php', 'DrydockLeaseAcquiredLogType' => 'applications/drydock/logtype/DrydockLeaseAcquiredLogType.php', 'DrydockLeaseActivatedLogType' => 'applications/drydock/logtype/DrydockLeaseActivatedLogType.php', 'DrydockLeaseActivationFailureLogType' => 'applications/drydock/logtype/DrydockLeaseActivationFailureLogType.php', 'DrydockLeaseActivationYieldLogType' => 'applications/drydock/logtype/DrydockLeaseActivationYieldLogType.php', 'DrydockLeaseAllocationFailureLogType' => 'applications/drydock/logtype/DrydockLeaseAllocationFailureLogType.php', 'DrydockLeaseController' => 'applications/drydock/controller/DrydockLeaseController.php', 'DrydockLeaseDatasource' => 'applications/drydock/typeahead/DrydockLeaseDatasource.php', 'DrydockLeaseDestroyedLogType' => 'applications/drydock/logtype/DrydockLeaseDestroyedLogType.php', 'DrydockLeaseListController' => 'applications/drydock/controller/DrydockLeaseListController.php', 'DrydockLeaseListView' => 'applications/drydock/view/DrydockLeaseListView.php', 'DrydockLeaseNoAuthorizationsLogType' => 'applications/drydock/logtype/DrydockLeaseNoAuthorizationsLogType.php', 'DrydockLeaseNoBlueprintsLogType' => 'applications/drydock/logtype/DrydockLeaseNoBlueprintsLogType.php', 'DrydockLeasePHIDType' => 'applications/drydock/phid/DrydockLeasePHIDType.php', 'DrydockLeaseQuery' => 'applications/drydock/query/DrydockLeaseQuery.php', 'DrydockLeaseQueuedLogType' => 'applications/drydock/logtype/DrydockLeaseQueuedLogType.php', 'DrydockLeaseReacquireLogType' => 'applications/drydock/logtype/DrydockLeaseReacquireLogType.php', 'DrydockLeaseReclaimLogType' => 'applications/drydock/logtype/DrydockLeaseReclaimLogType.php', 'DrydockLeaseReleaseController' => 'applications/drydock/controller/DrydockLeaseReleaseController.php', 'DrydockLeaseReleasedLogType' => 'applications/drydock/logtype/DrydockLeaseReleasedLogType.php', 'DrydockLeaseSearchConduitAPIMethod' => 'applications/drydock/conduit/DrydockLeaseSearchConduitAPIMethod.php', 'DrydockLeaseSearchEngine' => 'applications/drydock/query/DrydockLeaseSearchEngine.php', 'DrydockLeaseStatus' => 'applications/drydock/constants/DrydockLeaseStatus.php', 'DrydockLeaseUpdateWorker' => 'applications/drydock/worker/DrydockLeaseUpdateWorker.php', 'DrydockLeaseViewController' => 'applications/drydock/controller/DrydockLeaseViewController.php', 'DrydockLeaseWaitingForResourcesLogType' => 'applications/drydock/logtype/DrydockLeaseWaitingForResourcesLogType.php', 'DrydockLog' => 'applications/drydock/storage/DrydockLog.php', 'DrydockLogController' => 'applications/drydock/controller/DrydockLogController.php', 'DrydockLogGarbageCollector' => 'applications/drydock/garbagecollector/DrydockLogGarbageCollector.php', 'DrydockLogListController' => 'applications/drydock/controller/DrydockLogListController.php', 'DrydockLogListView' => 'applications/drydock/view/DrydockLogListView.php', 'DrydockLogQuery' => 'applications/drydock/query/DrydockLogQuery.php', 'DrydockLogSearchEngine' => 'applications/drydock/query/DrydockLogSearchEngine.php', 'DrydockLogType' => 'applications/drydock/logtype/DrydockLogType.php', 'DrydockManagementCommandWorkflow' => 'applications/drydock/management/DrydockManagementCommandWorkflow.php', 'DrydockManagementLeaseWorkflow' => 'applications/drydock/management/DrydockManagementLeaseWorkflow.php', 'DrydockManagementReclaimWorkflow' => 'applications/drydock/management/DrydockManagementReclaimWorkflow.php', 'DrydockManagementReleaseLeaseWorkflow' => 'applications/drydock/management/DrydockManagementReleaseLeaseWorkflow.php', 'DrydockManagementReleaseResourceWorkflow' => 'applications/drydock/management/DrydockManagementReleaseResourceWorkflow.php', 'DrydockManagementUpdateLeaseWorkflow' => 'applications/drydock/management/DrydockManagementUpdateLeaseWorkflow.php', 'DrydockManagementUpdateResourceWorkflow' => 'applications/drydock/management/DrydockManagementUpdateResourceWorkflow.php', 'DrydockManagementWorkflow' => 'applications/drydock/management/DrydockManagementWorkflow.php', 'DrydockObjectAuthorizationView' => 'applications/drydock/view/DrydockObjectAuthorizationView.php', 'DrydockOperationWorkLogType' => 'applications/drydock/logtype/DrydockOperationWorkLogType.php', 'DrydockQuery' => 'applications/drydock/query/DrydockQuery.php', 'DrydockRepositoryOperation' => 'applications/drydock/storage/DrydockRepositoryOperation.php', 'DrydockRepositoryOperationController' => 'applications/drydock/controller/DrydockRepositoryOperationController.php', 'DrydockRepositoryOperationDismissController' => 'applications/drydock/controller/DrydockRepositoryOperationDismissController.php', 'DrydockRepositoryOperationListController' => 'applications/drydock/controller/DrydockRepositoryOperationListController.php', 'DrydockRepositoryOperationPHIDType' => 'applications/drydock/phid/DrydockRepositoryOperationPHIDType.php', 'DrydockRepositoryOperationQuery' => 'applications/drydock/query/DrydockRepositoryOperationQuery.php', 'DrydockRepositoryOperationSearchEngine' => 'applications/drydock/query/DrydockRepositoryOperationSearchEngine.php', 'DrydockRepositoryOperationStatusController' => 'applications/drydock/controller/DrydockRepositoryOperationStatusController.php', 'DrydockRepositoryOperationStatusView' => 'applications/drydock/view/DrydockRepositoryOperationStatusView.php', 'DrydockRepositoryOperationType' => 'applications/drydock/operation/DrydockRepositoryOperationType.php', 'DrydockRepositoryOperationUpdateWorker' => 'applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php', 'DrydockRepositoryOperationViewController' => 'applications/drydock/controller/DrydockRepositoryOperationViewController.php', 'DrydockResource' => 'applications/drydock/storage/DrydockResource.php', 'DrydockResourceActivationFailureLogType' => 'applications/drydock/logtype/DrydockResourceActivationFailureLogType.php', 'DrydockResourceActivationYieldLogType' => 'applications/drydock/logtype/DrydockResourceActivationYieldLogType.php', 'DrydockResourceAllocationFailureLogType' => 'applications/drydock/logtype/DrydockResourceAllocationFailureLogType.php', 'DrydockResourceController' => 'applications/drydock/controller/DrydockResourceController.php', 'DrydockResourceDatasource' => 'applications/drydock/typeahead/DrydockResourceDatasource.php', 'DrydockResourceListController' => 'applications/drydock/controller/DrydockResourceListController.php', 'DrydockResourceListView' => 'applications/drydock/view/DrydockResourceListView.php', 'DrydockResourceLockException' => 'applications/drydock/exception/DrydockResourceLockException.php', 'DrydockResourcePHIDType' => 'applications/drydock/phid/DrydockResourcePHIDType.php', 'DrydockResourceQuery' => 'applications/drydock/query/DrydockResourceQuery.php', 'DrydockResourceReclaimLogType' => 'applications/drydock/logtype/DrydockResourceReclaimLogType.php', 'DrydockResourceReleaseController' => 'applications/drydock/controller/DrydockResourceReleaseController.php', 'DrydockResourceSearchEngine' => 'applications/drydock/query/DrydockResourceSearchEngine.php', 'DrydockResourceStatus' => 'applications/drydock/constants/DrydockResourceStatus.php', 'DrydockResourceUpdateWorker' => 'applications/drydock/worker/DrydockResourceUpdateWorker.php', 'DrydockResourceViewController' => 'applications/drydock/controller/DrydockResourceViewController.php', 'DrydockSFTPFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockSFTPFilesystemInterface.php', 'DrydockSSHCommandInterface' => 'applications/drydock/interface/command/DrydockSSHCommandInterface.php', 'DrydockSchemaSpec' => 'applications/drydock/storage/DrydockSchemaSpec.php', 'DrydockSlotLock' => 'applications/drydock/storage/DrydockSlotLock.php', 'DrydockSlotLockException' => 'applications/drydock/exception/DrydockSlotLockException.php', 'DrydockSlotLockFailureLogType' => 'applications/drydock/logtype/DrydockSlotLockFailureLogType.php', 'DrydockTestRepositoryOperation' => 'applications/drydock/operation/DrydockTestRepositoryOperation.php', 'DrydockTextLogType' => 'applications/drydock/logtype/DrydockTextLogType.php', 'DrydockWebrootInterface' => 'applications/drydock/interface/webroot/DrydockWebrootInterface.php', 'DrydockWorker' => 'applications/drydock/worker/DrydockWorker.php', 'DrydockWorkingCopyBlueprintImplementation' => 'applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php', 'EdgeSearchConduitAPIMethod' => 'infrastructure/edges/conduit/EdgeSearchConduitAPIMethod.php', 'FeedConduitAPIMethod' => 'applications/feed/conduit/FeedConduitAPIMethod.php', 'FeedPublishConduitAPIMethod' => 'applications/feed/conduit/FeedPublishConduitAPIMethod.php', 'FeedPublisherHTTPWorker' => 'applications/feed/worker/FeedPublisherHTTPWorker.php', 'FeedPublisherWorker' => 'applications/feed/worker/FeedPublisherWorker.php', 'FeedPushWorker' => 'applications/feed/worker/FeedPushWorker.php', 'FeedQueryConduitAPIMethod' => 'applications/feed/conduit/FeedQueryConduitAPIMethod.php', 'FeedStoryNotificationGarbageCollector' => 'applications/notification/garbagecollector/FeedStoryNotificationGarbageCollector.php', 'FileAllocateConduitAPIMethod' => 'applications/files/conduit/FileAllocateConduitAPIMethod.php', 'FileConduitAPIMethod' => 'applications/files/conduit/FileConduitAPIMethod.php', 'FileCreateMailReceiver' => 'applications/files/mail/FileCreateMailReceiver.php', 'FileDeletionWorker' => 'applications/files/worker/FileDeletionWorker.php', 'FileDownloadConduitAPIMethod' => 'applications/files/conduit/FileDownloadConduitAPIMethod.php', 'FileInfoConduitAPIMethod' => 'applications/files/conduit/FileInfoConduitAPIMethod.php', 'FileMailReceiver' => 'applications/files/mail/FileMailReceiver.php', 'FileQueryChunksConduitAPIMethod' => 'applications/files/conduit/FileQueryChunksConduitAPIMethod.php', 'FileReplyHandler' => 'applications/files/mail/FileReplyHandler.php', 'FileTypeIcon' => 'applications/files/constants/FileTypeIcon.php', 'FileUploadChunkConduitAPIMethod' => 'applications/files/conduit/FileUploadChunkConduitAPIMethod.php', 'FileUploadConduitAPIMethod' => 'applications/files/conduit/FileUploadConduitAPIMethod.php', 'FileUploadHashConduitAPIMethod' => 'applications/files/conduit/FileUploadHashConduitAPIMethod.php', 'FilesDefaultViewCapability' => 'applications/files/capability/FilesDefaultViewCapability.php', 'FlagConduitAPIMethod' => 'applications/flag/conduit/FlagConduitAPIMethod.php', 'FlagDeleteConduitAPIMethod' => 'applications/flag/conduit/FlagDeleteConduitAPIMethod.php', 'FlagEditConduitAPIMethod' => 'applications/flag/conduit/FlagEditConduitAPIMethod.php', 'FlagQueryConduitAPIMethod' => 'applications/flag/conduit/FlagQueryConduitAPIMethod.php', 'FundBacker' => 'applications/fund/storage/FundBacker.php', 'FundBackerCart' => 'applications/fund/phortune/FundBackerCart.php', 'FundBackerEditor' => 'applications/fund/editor/FundBackerEditor.php', 'FundBackerListController' => 'applications/fund/controller/FundBackerListController.php', 'FundBackerPHIDType' => 'applications/fund/phid/FundBackerPHIDType.php', 'FundBackerProduct' => 'applications/fund/phortune/FundBackerProduct.php', 'FundBackerQuery' => 'applications/fund/query/FundBackerQuery.php', 'FundBackerRefundTransaction' => 'applications/fund/xaction/FundBackerRefundTransaction.php', 'FundBackerSearchEngine' => 'applications/fund/query/FundBackerSearchEngine.php', 'FundBackerStatusTransaction' => 'applications/fund/xaction/FundBackerStatusTransaction.php', 'FundBackerTransaction' => 'applications/fund/storage/FundBackerTransaction.php', 'FundBackerTransactionQuery' => 'applications/fund/query/FundBackerTransactionQuery.php', 'FundBackerTransactionType' => 'applications/fund/xaction/FundBackerTransactionType.php', 'FundController' => 'applications/fund/controller/FundController.php', 'FundCreateInitiativesCapability' => 'applications/fund/capability/FundCreateInitiativesCapability.php', 'FundDAO' => 'applications/fund/storage/FundDAO.php', 'FundDefaultViewCapability' => 'applications/fund/capability/FundDefaultViewCapability.php', 'FundInitiative' => 'applications/fund/storage/FundInitiative.php', 'FundInitiativeBackController' => 'applications/fund/controller/FundInitiativeBackController.php', 'FundInitiativeBackerTransaction' => 'applications/fund/xaction/FundInitiativeBackerTransaction.php', 'FundInitiativeCloseController' => 'applications/fund/controller/FundInitiativeCloseController.php', 'FundInitiativeDescriptionTransaction' => 'applications/fund/xaction/FundInitiativeDescriptionTransaction.php', 'FundInitiativeEditController' => 'applications/fund/controller/FundInitiativeEditController.php', 'FundInitiativeEditEngine' => 'applications/fund/editor/FundInitiativeEditEngine.php', 'FundInitiativeEditor' => 'applications/fund/editor/FundInitiativeEditor.php', 'FundInitiativeFerretEngine' => 'applications/fund/search/FundInitiativeFerretEngine.php', 'FundInitiativeFulltextEngine' => 'applications/fund/search/FundInitiativeFulltextEngine.php', 'FundInitiativeListController' => 'applications/fund/controller/FundInitiativeListController.php', 'FundInitiativeMerchantTransaction' => 'applications/fund/xaction/FundInitiativeMerchantTransaction.php', 'FundInitiativeNameTransaction' => 'applications/fund/xaction/FundInitiativeNameTransaction.php', 'FundInitiativePHIDType' => 'applications/fund/phid/FundInitiativePHIDType.php', 'FundInitiativeQuery' => 'applications/fund/query/FundInitiativeQuery.php', 'FundInitiativeRefundTransaction' => 'applications/fund/xaction/FundInitiativeRefundTransaction.php', 'FundInitiativeRemarkupRule' => 'applications/fund/remarkup/FundInitiativeRemarkupRule.php', 'FundInitiativeReplyHandler' => 'applications/fund/mail/FundInitiativeReplyHandler.php', 'FundInitiativeRisksTransaction' => 'applications/fund/xaction/FundInitiativeRisksTransaction.php', 'FundInitiativeSearchEngine' => 'applications/fund/query/FundInitiativeSearchEngine.php', 'FundInitiativeStatusTransaction' => 'applications/fund/xaction/FundInitiativeStatusTransaction.php', 'FundInitiativeTransaction' => 'applications/fund/storage/FundInitiativeTransaction.php', 'FundInitiativeTransactionComment' => 'applications/fund/storage/FundInitiativeTransactionComment.php', 'FundInitiativeTransactionQuery' => 'applications/fund/query/FundInitiativeTransactionQuery.php', 'FundInitiativeTransactionType' => 'applications/fund/xaction/FundInitiativeTransactionType.php', 'FundInitiativeViewController' => 'applications/fund/controller/FundInitiativeViewController.php', 'FundSchemaSpec' => 'applications/fund/storage/FundSchemaSpec.php', 'HarbormasterAbortOlderBuildsBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterAbortOlderBuildsBuildStepImplementation.php', 'HarbormasterArcLintBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterArcLintBuildStepImplementation.php', 'HarbormasterArcUnitBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterArcUnitBuildStepImplementation.php', 'HarbormasterArtifact' => 'applications/harbormaster/artifact/HarbormasterArtifact.php', 'HarbormasterAutotargetsTestCase' => 'applications/harbormaster/__tests__/HarbormasterAutotargetsTestCase.php', 'HarbormasterBuild' => 'applications/harbormaster/storage/build/HarbormasterBuild.php', 'HarbormasterBuildAbortedException' => 'applications/harbormaster/exception/HarbormasterBuildAbortedException.php', 'HarbormasterBuildActionController' => 'applications/harbormaster/controller/HarbormasterBuildActionController.php', 'HarbormasterBuildArcanistAutoplan' => 'applications/harbormaster/autoplan/HarbormasterBuildArcanistAutoplan.php', 'HarbormasterBuildArtifact' => 'applications/harbormaster/storage/build/HarbormasterBuildArtifact.php', 'HarbormasterBuildArtifactPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildArtifactPHIDType.php', 'HarbormasterBuildArtifactQuery' => 'applications/harbormaster/query/HarbormasterBuildArtifactQuery.php', 'HarbormasterBuildAutoplan' => 'applications/harbormaster/autoplan/HarbormasterBuildAutoplan.php', 'HarbormasterBuildCommand' => 'applications/harbormaster/storage/HarbormasterBuildCommand.php', 'HarbormasterBuildDependencyDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildDependencyDatasource.php', 'HarbormasterBuildEngine' => 'applications/harbormaster/engine/HarbormasterBuildEngine.php', 'HarbormasterBuildFailureException' => 'applications/harbormaster/exception/HarbormasterBuildFailureException.php', 'HarbormasterBuildGraph' => 'applications/harbormaster/engine/HarbormasterBuildGraph.php', 'HarbormasterBuildInitiatorDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildInitiatorDatasource.php', 'HarbormasterBuildLintMessage' => 'applications/harbormaster/storage/build/HarbormasterBuildLintMessage.php', 'HarbormasterBuildListController' => 'applications/harbormaster/controller/HarbormasterBuildListController.php', 'HarbormasterBuildLog' => 'applications/harbormaster/storage/build/HarbormasterBuildLog.php', 'HarbormasterBuildLogChunk' => 'applications/harbormaster/storage/build/HarbormasterBuildLogChunk.php', 'HarbormasterBuildLogChunkIterator' => 'applications/harbormaster/storage/build/HarbormasterBuildLogChunkIterator.php', 'HarbormasterBuildLogDownloadController' => 'applications/harbormaster/controller/HarbormasterBuildLogDownloadController.php', 'HarbormasterBuildLogPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildLogPHIDType.php', 'HarbormasterBuildLogQuery' => 'applications/harbormaster/query/HarbormasterBuildLogQuery.php', 'HarbormasterBuildLogRenderController' => 'applications/harbormaster/controller/HarbormasterBuildLogRenderController.php', 'HarbormasterBuildLogSearchConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterBuildLogSearchConduitAPIMethod.php', 'HarbormasterBuildLogSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildLogSearchEngine.php', 'HarbormasterBuildLogTestCase' => 'applications/harbormaster/__tests__/HarbormasterBuildLogTestCase.php', 'HarbormasterBuildLogView' => 'applications/harbormaster/view/HarbormasterBuildLogView.php', 'HarbormasterBuildLogViewController' => 'applications/harbormaster/controller/HarbormasterBuildLogViewController.php', 'HarbormasterBuildMessage' => 'applications/harbormaster/storage/HarbormasterBuildMessage.php', 'HarbormasterBuildMessageQuery' => 'applications/harbormaster/query/HarbormasterBuildMessageQuery.php', 'HarbormasterBuildPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildPHIDType.php', 'HarbormasterBuildPlan' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php', 'HarbormasterBuildPlanDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildPlanDatasource.php', 'HarbormasterBuildPlanDefaultEditCapability' => 'applications/harbormaster/capability/HarbormasterBuildPlanDefaultEditCapability.php', 'HarbormasterBuildPlanDefaultViewCapability' => 'applications/harbormaster/capability/HarbormasterBuildPlanDefaultViewCapability.php', 'HarbormasterBuildPlanEditEngine' => 'applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php', 'HarbormasterBuildPlanEditor' => 'applications/harbormaster/editor/HarbormasterBuildPlanEditor.php', 'HarbormasterBuildPlanNameNgrams' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlanNameNgrams.php', 'HarbormasterBuildPlanPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildPlanPHIDType.php', 'HarbormasterBuildPlanQuery' => 'applications/harbormaster/query/HarbormasterBuildPlanQuery.php', 'HarbormasterBuildPlanSearchAPIMethod' => 'applications/harbormaster/conduit/HarbormasterBuildPlanSearchAPIMethod.php', 'HarbormasterBuildPlanSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildPlanSearchEngine.php', 'HarbormasterBuildPlanTransaction' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlanTransaction.php', 'HarbormasterBuildPlanTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildPlanTransactionQuery.php', 'HarbormasterBuildQuery' => 'applications/harbormaster/query/HarbormasterBuildQuery.php', 'HarbormasterBuildRequest' => 'applications/harbormaster/engine/HarbormasterBuildRequest.php', 'HarbormasterBuildSearchConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterBuildSearchConduitAPIMethod.php', 'HarbormasterBuildSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildSearchEngine.php', 'HarbormasterBuildStatus' => 'applications/harbormaster/constants/HarbormasterBuildStatus.php', 'HarbormasterBuildStatusDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildStatusDatasource.php', 'HarbormasterBuildStep' => 'applications/harbormaster/storage/configuration/HarbormasterBuildStep.php', 'HarbormasterBuildStepCoreCustomField' => 'applications/harbormaster/customfield/HarbormasterBuildStepCoreCustomField.php', 'HarbormasterBuildStepCustomField' => 'applications/harbormaster/customfield/HarbormasterBuildStepCustomField.php', 'HarbormasterBuildStepEditor' => 'applications/harbormaster/editor/HarbormasterBuildStepEditor.php', 'HarbormasterBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterBuildStepGroup.php', 'HarbormasterBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterBuildStepImplementation.php', 'HarbormasterBuildStepImplementationTestCase' => 'applications/harbormaster/step/__tests__/HarbormasterBuildStepImplementationTestCase.php', 'HarbormasterBuildStepPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildStepPHIDType.php', 'HarbormasterBuildStepQuery' => 'applications/harbormaster/query/HarbormasterBuildStepQuery.php', 'HarbormasterBuildStepTransaction' => 'applications/harbormaster/storage/configuration/HarbormasterBuildStepTransaction.php', 'HarbormasterBuildStepTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildStepTransactionQuery.php', 'HarbormasterBuildTarget' => 'applications/harbormaster/storage/build/HarbormasterBuildTarget.php', 'HarbormasterBuildTargetPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildTargetPHIDType.php', 'HarbormasterBuildTargetQuery' => 'applications/harbormaster/query/HarbormasterBuildTargetQuery.php', + 'HarbormasterBuildTargetSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildTargetSearchEngine.php', 'HarbormasterBuildTransaction' => 'applications/harbormaster/storage/HarbormasterBuildTransaction.php', 'HarbormasterBuildTransactionEditor' => 'applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php', 'HarbormasterBuildTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildTransactionQuery.php', 'HarbormasterBuildUnitMessage' => 'applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php', 'HarbormasterBuildViewController' => 'applications/harbormaster/controller/HarbormasterBuildViewController.php', 'HarbormasterBuildWorker' => 'applications/harbormaster/worker/HarbormasterBuildWorker.php', 'HarbormasterBuildable' => 'applications/harbormaster/storage/HarbormasterBuildable.php', 'HarbormasterBuildableActionController' => 'applications/harbormaster/controller/HarbormasterBuildableActionController.php', 'HarbormasterBuildableAdapterInterface' => 'applications/harbormaster/herald/HarbormasterBuildableAdapterInterface.php', 'HarbormasterBuildableEngine' => 'applications/harbormaster/engine/HarbormasterBuildableEngine.php', 'HarbormasterBuildableInterface' => 'applications/harbormaster/interface/HarbormasterBuildableInterface.php', 'HarbormasterBuildableListController' => 'applications/harbormaster/controller/HarbormasterBuildableListController.php', 'HarbormasterBuildablePHIDType' => 'applications/harbormaster/phid/HarbormasterBuildablePHIDType.php', 'HarbormasterBuildableQuery' => 'applications/harbormaster/query/HarbormasterBuildableQuery.php', + 'HarbormasterBuildableSearchAPIMethod' => 'applications/harbormaster/conduit/HarbormasterBuildableSearchAPIMethod.php', 'HarbormasterBuildableSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildableSearchEngine.php', 'HarbormasterBuildableStatus' => 'applications/harbormaster/constants/HarbormasterBuildableStatus.php', 'HarbormasterBuildableTransaction' => 'applications/harbormaster/storage/HarbormasterBuildableTransaction.php', 'HarbormasterBuildableTransactionEditor' => 'applications/harbormaster/editor/HarbormasterBuildableTransactionEditor.php', 'HarbormasterBuildableTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildableTransactionQuery.php', 'HarbormasterBuildableViewController' => 'applications/harbormaster/controller/HarbormasterBuildableViewController.php', 'HarbormasterBuildkiteBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterBuildkiteBuildStepImplementation.php', 'HarbormasterBuildkiteBuildableInterface' => 'applications/harbormaster/interface/HarbormasterBuildkiteBuildableInterface.php', 'HarbormasterBuildkiteHookController' => 'applications/harbormaster/controller/HarbormasterBuildkiteHookController.php', 'HarbormasterBuiltinBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterBuiltinBuildStepGroup.php', 'HarbormasterCircleCIBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterCircleCIBuildStepImplementation.php', 'HarbormasterCircleCIBuildableInterface' => 'applications/harbormaster/interface/HarbormasterCircleCIBuildableInterface.php', 'HarbormasterCircleCIHookController' => 'applications/harbormaster/controller/HarbormasterCircleCIHookController.php', 'HarbormasterConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php', 'HarbormasterControlBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterControlBuildStepGroup.php', 'HarbormasterController' => 'applications/harbormaster/controller/HarbormasterController.php', 'HarbormasterCreateArtifactConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterCreateArtifactConduitAPIMethod.php', 'HarbormasterCreatePlansCapability' => 'applications/harbormaster/capability/HarbormasterCreatePlansCapability.php', 'HarbormasterDAO' => 'applications/harbormaster/storage/HarbormasterDAO.php', 'HarbormasterDrydockBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterDrydockBuildStepGroup.php', 'HarbormasterDrydockCommandBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterDrydockCommandBuildStepImplementation.php', 'HarbormasterDrydockLeaseArtifact' => 'applications/harbormaster/artifact/HarbormasterDrydockLeaseArtifact.php', 'HarbormasterExecFuture' => 'applications/harbormaster/future/HarbormasterExecFuture.php', 'HarbormasterExternalBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterExternalBuildStepGroup.php', 'HarbormasterFileArtifact' => 'applications/harbormaster/artifact/HarbormasterFileArtifact.php', 'HarbormasterHTTPRequestBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php', 'HarbormasterHostArtifact' => 'applications/harbormaster/artifact/HarbormasterHostArtifact.php', 'HarbormasterLeaseWorkingCopyBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterLeaseWorkingCopyBuildStepImplementation.php', 'HarbormasterLintMessagesController' => 'applications/harbormaster/controller/HarbormasterLintMessagesController.php', 'HarbormasterLintPropertyView' => 'applications/harbormaster/view/HarbormasterLintPropertyView.php', 'HarbormasterLogWorker' => 'applications/harbormaster/worker/HarbormasterLogWorker.php', 'HarbormasterManagementArchiveLogsWorkflow' => 'applications/harbormaster/management/HarbormasterManagementArchiveLogsWorkflow.php', 'HarbormasterManagementBuildWorkflow' => 'applications/harbormaster/management/HarbormasterManagementBuildWorkflow.php', 'HarbormasterManagementPublishWorkflow' => 'applications/harbormaster/management/HarbormasterManagementPublishWorkflow.php', 'HarbormasterManagementRebuildLogWorkflow' => 'applications/harbormaster/management/HarbormasterManagementRebuildLogWorkflow.php', 'HarbormasterManagementRestartWorkflow' => 'applications/harbormaster/management/HarbormasterManagementRestartWorkflow.php', 'HarbormasterManagementUpdateWorkflow' => 'applications/harbormaster/management/HarbormasterManagementUpdateWorkflow.php', 'HarbormasterManagementWorkflow' => 'applications/harbormaster/management/HarbormasterManagementWorkflow.php', 'HarbormasterManagementWriteLogWorkflow' => 'applications/harbormaster/management/HarbormasterManagementWriteLogWorkflow.php', 'HarbormasterMessageType' => 'applications/harbormaster/engine/HarbormasterMessageType.php', 'HarbormasterObject' => 'applications/harbormaster/storage/HarbormasterObject.php', 'HarbormasterOtherBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterOtherBuildStepGroup.php', 'HarbormasterPlanController' => 'applications/harbormaster/controller/HarbormasterPlanController.php', 'HarbormasterPlanDisableController' => 'applications/harbormaster/controller/HarbormasterPlanDisableController.php', 'HarbormasterPlanEditController' => 'applications/harbormaster/controller/HarbormasterPlanEditController.php', 'HarbormasterPlanListController' => 'applications/harbormaster/controller/HarbormasterPlanListController.php', 'HarbormasterPlanRunController' => 'applications/harbormaster/controller/HarbormasterPlanRunController.php', 'HarbormasterPlanViewController' => 'applications/harbormaster/controller/HarbormasterPlanViewController.php', 'HarbormasterPrototypeBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterPrototypeBuildStepGroup.php', 'HarbormasterPublishFragmentBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterPublishFragmentBuildStepImplementation.php', 'HarbormasterQueryAutotargetsConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterQueryAutotargetsConduitAPIMethod.php', 'HarbormasterQueryBuildablesConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterQueryBuildablesConduitAPIMethod.php', 'HarbormasterQueryBuildsConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterQueryBuildsConduitAPIMethod.php', 'HarbormasterQueryBuildsSearchEngineAttachment' => 'applications/harbormaster/engineextension/HarbormasterQueryBuildsSearchEngineAttachment.php', 'HarbormasterRemarkupRule' => 'applications/harbormaster/remarkup/HarbormasterRemarkupRule.php', 'HarbormasterRunBuildPlansHeraldAction' => 'applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php', 'HarbormasterSchemaSpec' => 'applications/harbormaster/storage/HarbormasterSchemaSpec.php', 'HarbormasterScratchTable' => 'applications/harbormaster/storage/HarbormasterScratchTable.php', 'HarbormasterSendMessageConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterSendMessageConduitAPIMethod.php', 'HarbormasterSleepBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterSleepBuildStepImplementation.php', 'HarbormasterStepAddController' => 'applications/harbormaster/controller/HarbormasterStepAddController.php', 'HarbormasterStepDeleteController' => 'applications/harbormaster/controller/HarbormasterStepDeleteController.php', 'HarbormasterStepEditController' => 'applications/harbormaster/controller/HarbormasterStepEditController.php', 'HarbormasterStepViewController' => 'applications/harbormaster/controller/HarbormasterStepViewController.php', 'HarbormasterTargetEngine' => 'applications/harbormaster/engine/HarbormasterTargetEngine.php', + 'HarbormasterTargetSearchAPIMethod' => 'applications/harbormaster/conduit/HarbormasterTargetSearchAPIMethod.php', 'HarbormasterTargetWorker' => 'applications/harbormaster/worker/HarbormasterTargetWorker.php', 'HarbormasterTestBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterTestBuildStepGroup.php', 'HarbormasterThrowExceptionBuildStep' => 'applications/harbormaster/step/HarbormasterThrowExceptionBuildStep.php', 'HarbormasterUIEventListener' => 'applications/harbormaster/event/HarbormasterUIEventListener.php', 'HarbormasterURIArtifact' => 'applications/harbormaster/artifact/HarbormasterURIArtifact.php', 'HarbormasterUnitMessageListController' => 'applications/harbormaster/controller/HarbormasterUnitMessageListController.php', 'HarbormasterUnitMessageViewController' => 'applications/harbormaster/controller/HarbormasterUnitMessageViewController.php', 'HarbormasterUnitPropertyView' => 'applications/harbormaster/view/HarbormasterUnitPropertyView.php', 'HarbormasterUnitStatus' => 'applications/harbormaster/constants/HarbormasterUnitStatus.php', 'HarbormasterUnitSummaryView' => 'applications/harbormaster/view/HarbormasterUnitSummaryView.php', 'HarbormasterUploadArtifactBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterUploadArtifactBuildStepImplementation.php', 'HarbormasterWaitForPreviousBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterWaitForPreviousBuildStepImplementation.php', 'HarbormasterWorker' => 'applications/harbormaster/worker/HarbormasterWorker.php', 'HarbormasterWorkingCopyArtifact' => 'applications/harbormaster/artifact/HarbormasterWorkingCopyArtifact.php', 'HeraldActingUserField' => 'applications/herald/field/HeraldActingUserField.php', 'HeraldAction' => 'applications/herald/action/HeraldAction.php', 'HeraldActionGroup' => 'applications/herald/action/HeraldActionGroup.php', 'HeraldActionRecord' => 'applications/herald/storage/HeraldActionRecord.php', 'HeraldAdapter' => 'applications/herald/adapter/HeraldAdapter.php', 'HeraldAdapterDatasource' => 'applications/herald/typeahead/HeraldAdapterDatasource.php', 'HeraldAlwaysField' => 'applications/herald/field/HeraldAlwaysField.php', 'HeraldAnotherRuleField' => 'applications/herald/field/HeraldAnotherRuleField.php', 'HeraldApplicationActionGroup' => 'applications/herald/action/HeraldApplicationActionGroup.php', 'HeraldApplyTranscript' => 'applications/herald/storage/transcript/HeraldApplyTranscript.php', 'HeraldBasicFieldGroup' => 'applications/herald/field/HeraldBasicFieldGroup.php', 'HeraldBuildableState' => 'applications/herald/state/HeraldBuildableState.php', 'HeraldCallWebhookAction' => 'applications/herald/action/HeraldCallWebhookAction.php', 'HeraldCommentAction' => 'applications/herald/action/HeraldCommentAction.php', 'HeraldCommitAdapter' => 'applications/diffusion/herald/HeraldCommitAdapter.php', 'HeraldCondition' => 'applications/herald/storage/HeraldCondition.php', 'HeraldConditionTranscript' => 'applications/herald/storage/transcript/HeraldConditionTranscript.php', 'HeraldContentSourceField' => 'applications/herald/field/HeraldContentSourceField.php', 'HeraldController' => 'applications/herald/controller/HeraldController.php', 'HeraldCoreStateReasons' => 'applications/herald/state/HeraldCoreStateReasons.php', 'HeraldCreateWebhooksCapability' => 'applications/herald/capability/HeraldCreateWebhooksCapability.php', 'HeraldDAO' => 'applications/herald/storage/HeraldDAO.php', 'HeraldDeprecatedFieldGroup' => 'applications/herald/field/HeraldDeprecatedFieldGroup.php', 'HeraldDifferentialAdapter' => 'applications/differential/herald/HeraldDifferentialAdapter.php', 'HeraldDifferentialDiffAdapter' => 'applications/differential/herald/HeraldDifferentialDiffAdapter.php', 'HeraldDifferentialRevisionAdapter' => 'applications/differential/herald/HeraldDifferentialRevisionAdapter.php', 'HeraldDisableController' => 'applications/herald/controller/HeraldDisableController.php', 'HeraldDoNothingAction' => 'applications/herald/action/HeraldDoNothingAction.php', 'HeraldEditFieldGroup' => 'applications/herald/field/HeraldEditFieldGroup.php', 'HeraldEffect' => 'applications/herald/engine/HeraldEffect.php', 'HeraldEmptyFieldValue' => 'applications/herald/value/HeraldEmptyFieldValue.php', 'HeraldEngine' => 'applications/herald/engine/HeraldEngine.php', 'HeraldExactProjectsField' => 'applications/project/herald/HeraldExactProjectsField.php', 'HeraldField' => 'applications/herald/field/HeraldField.php', 'HeraldFieldGroup' => 'applications/herald/field/HeraldFieldGroup.php', 'HeraldFieldTestCase' => 'applications/herald/field/__tests__/HeraldFieldTestCase.php', 'HeraldFieldValue' => 'applications/herald/value/HeraldFieldValue.php', 'HeraldGroup' => 'applications/herald/group/HeraldGroup.php', 'HeraldInvalidActionException' => 'applications/herald/engine/exception/HeraldInvalidActionException.php', 'HeraldInvalidConditionException' => 'applications/herald/engine/exception/HeraldInvalidConditionException.php', 'HeraldMailableState' => 'applications/herald/state/HeraldMailableState.php', 'HeraldManageGlobalRulesCapability' => 'applications/herald/capability/HeraldManageGlobalRulesCapability.php', + 'HeraldManagementWorkflow' => 'applications/herald/management/HeraldManagementWorkflow.php', 'HeraldManiphestTaskAdapter' => 'applications/maniphest/herald/HeraldManiphestTaskAdapter.php', 'HeraldNewController' => 'applications/herald/controller/HeraldNewController.php', 'HeraldNewObjectField' => 'applications/herald/field/HeraldNewObjectField.php', 'HeraldNotifyActionGroup' => 'applications/herald/action/HeraldNotifyActionGroup.php', 'HeraldObjectTranscript' => 'applications/herald/storage/transcript/HeraldObjectTranscript.php', 'HeraldPhameBlogAdapter' => 'applications/phame/herald/HeraldPhameBlogAdapter.php', 'HeraldPhamePostAdapter' => 'applications/phame/herald/HeraldPhamePostAdapter.php', 'HeraldPholioMockAdapter' => 'applications/pholio/herald/HeraldPholioMockAdapter.php', 'HeraldPonderQuestionAdapter' => 'applications/ponder/herald/HeraldPonderQuestionAdapter.php', 'HeraldPreCommitAdapter' => 'applications/diffusion/herald/HeraldPreCommitAdapter.php', 'HeraldPreCommitContentAdapter' => 'applications/diffusion/herald/HeraldPreCommitContentAdapter.php', 'HeraldPreCommitRefAdapter' => 'applications/diffusion/herald/HeraldPreCommitRefAdapter.php', 'HeraldPreventActionGroup' => 'applications/herald/action/HeraldPreventActionGroup.php', 'HeraldProjectsField' => 'applications/project/herald/HeraldProjectsField.php', 'HeraldRecursiveConditionsException' => 'applications/herald/engine/exception/HeraldRecursiveConditionsException.php', 'HeraldRelatedFieldGroup' => 'applications/herald/field/HeraldRelatedFieldGroup.php', 'HeraldRemarkupFieldValue' => 'applications/herald/value/HeraldRemarkupFieldValue.php', 'HeraldRemarkupRule' => 'applications/herald/remarkup/HeraldRemarkupRule.php', 'HeraldRule' => 'applications/herald/storage/HeraldRule.php', 'HeraldRuleAdapter' => 'applications/herald/adapter/HeraldRuleAdapter.php', 'HeraldRuleAdapterField' => 'applications/herald/field/rule/HeraldRuleAdapterField.php', 'HeraldRuleController' => 'applications/herald/controller/HeraldRuleController.php', 'HeraldRuleDatasource' => 'applications/herald/typeahead/HeraldRuleDatasource.php', 'HeraldRuleEditor' => 'applications/herald/editor/HeraldRuleEditor.php', 'HeraldRuleField' => 'applications/herald/field/rule/HeraldRuleField.php', 'HeraldRuleFieldGroup' => 'applications/herald/field/rule/HeraldRuleFieldGroup.php', 'HeraldRuleListController' => 'applications/herald/controller/HeraldRuleListController.php', 'HeraldRulePHIDType' => 'applications/herald/phid/HeraldRulePHIDType.php', 'HeraldRuleQuery' => 'applications/herald/query/HeraldRuleQuery.php', 'HeraldRuleReplyHandler' => 'applications/herald/mail/HeraldRuleReplyHandler.php', 'HeraldRuleSearchEngine' => 'applications/herald/query/HeraldRuleSearchEngine.php', 'HeraldRuleSerializer' => 'applications/herald/editor/HeraldRuleSerializer.php', 'HeraldRuleTestCase' => 'applications/herald/storage/__tests__/HeraldRuleTestCase.php', 'HeraldRuleTransaction' => 'applications/herald/storage/HeraldRuleTransaction.php', 'HeraldRuleTransactionComment' => 'applications/herald/storage/HeraldRuleTransactionComment.php', 'HeraldRuleTranscript' => 'applications/herald/storage/transcript/HeraldRuleTranscript.php', 'HeraldRuleTypeConfig' => 'applications/herald/config/HeraldRuleTypeConfig.php', 'HeraldRuleTypeDatasource' => 'applications/herald/typeahead/HeraldRuleTypeDatasource.php', 'HeraldRuleTypeField' => 'applications/herald/field/rule/HeraldRuleTypeField.php', 'HeraldRuleViewController' => 'applications/herald/controller/HeraldRuleViewController.php', 'HeraldSchemaSpec' => 'applications/herald/storage/HeraldSchemaSpec.php', 'HeraldSelectFieldValue' => 'applications/herald/value/HeraldSelectFieldValue.php', 'HeraldSpaceField' => 'applications/spaces/herald/HeraldSpaceField.php', 'HeraldState' => 'applications/herald/state/HeraldState.php', 'HeraldStateReasons' => 'applications/herald/state/HeraldStateReasons.php', 'HeraldSubscribersField' => 'applications/subscriptions/herald/HeraldSubscribersField.php', 'HeraldSupportActionGroup' => 'applications/herald/action/HeraldSupportActionGroup.php', 'HeraldSupportFieldGroup' => 'applications/herald/field/HeraldSupportFieldGroup.php', 'HeraldTestConsoleController' => 'applications/herald/controller/HeraldTestConsoleController.php', + 'HeraldTestManagementWorkflow' => 'applications/herald/management/HeraldTestManagementWorkflow.php', 'HeraldTextFieldValue' => 'applications/herald/value/HeraldTextFieldValue.php', 'HeraldTokenizerFieldValue' => 'applications/herald/value/HeraldTokenizerFieldValue.php', 'HeraldTransactionQuery' => 'applications/herald/query/HeraldTransactionQuery.php', 'HeraldTranscript' => 'applications/herald/storage/transcript/HeraldTranscript.php', 'HeraldTranscriptController' => 'applications/herald/controller/HeraldTranscriptController.php', 'HeraldTranscriptDestructionEngineExtension' => 'applications/herald/engineextension/HeraldTranscriptDestructionEngineExtension.php', 'HeraldTranscriptGarbageCollector' => 'applications/herald/garbagecollector/HeraldTranscriptGarbageCollector.php', 'HeraldTranscriptListController' => 'applications/herald/controller/HeraldTranscriptListController.php', 'HeraldTranscriptPHIDType' => 'applications/herald/phid/HeraldTranscriptPHIDType.php', 'HeraldTranscriptQuery' => 'applications/herald/query/HeraldTranscriptQuery.php', 'HeraldTranscriptSearchEngine' => 'applications/herald/query/HeraldTranscriptSearchEngine.php', 'HeraldTranscriptTestCase' => 'applications/herald/storage/__tests__/HeraldTranscriptTestCase.php', 'HeraldUtilityActionGroup' => 'applications/herald/action/HeraldUtilityActionGroup.php', 'HeraldWebhook' => 'applications/herald/storage/HeraldWebhook.php', 'HeraldWebhookCallManagementWorkflow' => 'applications/herald/management/HeraldWebhookCallManagementWorkflow.php', 'HeraldWebhookController' => 'applications/herald/controller/HeraldWebhookController.php', 'HeraldWebhookDatasource' => 'applications/herald/typeahead/HeraldWebhookDatasource.php', 'HeraldWebhookEditController' => 'applications/herald/controller/HeraldWebhookEditController.php', 'HeraldWebhookEditEngine' => 'applications/herald/editor/HeraldWebhookEditEngine.php', 'HeraldWebhookEditor' => 'applications/herald/editor/HeraldWebhookEditor.php', 'HeraldWebhookKeyController' => 'applications/herald/controller/HeraldWebhookKeyController.php', 'HeraldWebhookListController' => 'applications/herald/controller/HeraldWebhookListController.php', 'HeraldWebhookManagementWorkflow' => 'applications/herald/management/HeraldWebhookManagementWorkflow.php', 'HeraldWebhookNameTransaction' => 'applications/herald/xaction/HeraldWebhookNameTransaction.php', 'HeraldWebhookPHIDType' => 'applications/herald/phid/HeraldWebhookPHIDType.php', 'HeraldWebhookQuery' => 'applications/herald/query/HeraldWebhookQuery.php', 'HeraldWebhookRequest' => 'applications/herald/storage/HeraldWebhookRequest.php', 'HeraldWebhookRequestGarbageCollector' => 'applications/herald/garbagecollector/HeraldWebhookRequestGarbageCollector.php', 'HeraldWebhookRequestListView' => 'applications/herald/view/HeraldWebhookRequestListView.php', 'HeraldWebhookRequestPHIDType' => 'applications/herald/phid/HeraldWebhookRequestPHIDType.php', 'HeraldWebhookRequestQuery' => 'applications/herald/query/HeraldWebhookRequestQuery.php', 'HeraldWebhookSearchEngine' => 'applications/herald/query/HeraldWebhookSearchEngine.php', 'HeraldWebhookStatusTransaction' => 'applications/herald/xaction/HeraldWebhookStatusTransaction.php', 'HeraldWebhookTestController' => 'applications/herald/controller/HeraldWebhookTestController.php', 'HeraldWebhookTransaction' => 'applications/herald/storage/HeraldWebhookTransaction.php', 'HeraldWebhookTransactionQuery' => 'applications/herald/query/HeraldWebhookTransactionQuery.php', 'HeraldWebhookTransactionType' => 'applications/herald/xaction/HeraldWebhookTransactionType.php', 'HeraldWebhookURITransaction' => 'applications/herald/xaction/HeraldWebhookURITransaction.php', 'HeraldWebhookViewController' => 'applications/herald/controller/HeraldWebhookViewController.php', 'HeraldWebhookWorker' => 'applications/herald/worker/HeraldWebhookWorker.php', 'Javelin' => 'infrastructure/javelin/Javelin.php', 'LegalpadController' => 'applications/legalpad/controller/LegalpadController.php', 'LegalpadCreateDocumentsCapability' => 'applications/legalpad/capability/LegalpadCreateDocumentsCapability.php', 'LegalpadDAO' => 'applications/legalpad/storage/LegalpadDAO.php', 'LegalpadDefaultEditCapability' => 'applications/legalpad/capability/LegalpadDefaultEditCapability.php', 'LegalpadDefaultViewCapability' => 'applications/legalpad/capability/LegalpadDefaultViewCapability.php', 'LegalpadDocument' => 'applications/legalpad/storage/LegalpadDocument.php', 'LegalpadDocumentBody' => 'applications/legalpad/storage/LegalpadDocumentBody.php', 'LegalpadDocumentDatasource' => 'applications/legalpad/typeahead/LegalpadDocumentDatasource.php', 'LegalpadDocumentDoneController' => 'applications/legalpad/controller/LegalpadDocumentDoneController.php', 'LegalpadDocumentEditController' => 'applications/legalpad/controller/LegalpadDocumentEditController.php', 'LegalpadDocumentEditEngine' => 'applications/legalpad/editor/LegalpadDocumentEditEngine.php', 'LegalpadDocumentEditor' => 'applications/legalpad/editor/LegalpadDocumentEditor.php', 'LegalpadDocumentListController' => 'applications/legalpad/controller/LegalpadDocumentListController.php', 'LegalpadDocumentManageController' => 'applications/legalpad/controller/LegalpadDocumentManageController.php', 'LegalpadDocumentPreambleTransaction' => 'applications/legalpad/xaction/LegalpadDocumentPreambleTransaction.php', 'LegalpadDocumentQuery' => 'applications/legalpad/query/LegalpadDocumentQuery.php', 'LegalpadDocumentRemarkupRule' => 'applications/legalpad/remarkup/LegalpadDocumentRemarkupRule.php', 'LegalpadDocumentRequireSignatureTransaction' => 'applications/legalpad/xaction/LegalpadDocumentRequireSignatureTransaction.php', 'LegalpadDocumentSearchEngine' => 'applications/legalpad/query/LegalpadDocumentSearchEngine.php', 'LegalpadDocumentSignController' => 'applications/legalpad/controller/LegalpadDocumentSignController.php', 'LegalpadDocumentSignature' => 'applications/legalpad/storage/LegalpadDocumentSignature.php', 'LegalpadDocumentSignatureAddController' => 'applications/legalpad/controller/LegalpadDocumentSignatureAddController.php', 'LegalpadDocumentSignatureListController' => 'applications/legalpad/controller/LegalpadDocumentSignatureListController.php', 'LegalpadDocumentSignatureQuery' => 'applications/legalpad/query/LegalpadDocumentSignatureQuery.php', 'LegalpadDocumentSignatureSearchEngine' => 'applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php', 'LegalpadDocumentSignatureTypeTransaction' => 'applications/legalpad/xaction/LegalpadDocumentSignatureTypeTransaction.php', 'LegalpadDocumentSignatureVerificationController' => 'applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php', 'LegalpadDocumentSignatureViewController' => 'applications/legalpad/controller/LegalpadDocumentSignatureViewController.php', 'LegalpadDocumentTextTransaction' => 'applications/legalpad/xaction/LegalpadDocumentTextTransaction.php', 'LegalpadDocumentTitleTransaction' => 'applications/legalpad/xaction/LegalpadDocumentTitleTransaction.php', 'LegalpadDocumentTransactionType' => 'applications/legalpad/xaction/LegalpadDocumentTransactionType.php', 'LegalpadMailReceiver' => 'applications/legalpad/mail/LegalpadMailReceiver.php', 'LegalpadObjectNeedsSignatureEdgeType' => 'applications/legalpad/edge/LegalpadObjectNeedsSignatureEdgeType.php', 'LegalpadReplyHandler' => 'applications/legalpad/mail/LegalpadReplyHandler.php', 'LegalpadRequireSignatureHeraldAction' => 'applications/legalpad/herald/LegalpadRequireSignatureHeraldAction.php', 'LegalpadSchemaSpec' => 'applications/legalpad/storage/LegalpadSchemaSpec.php', 'LegalpadSignatureNeededByObjectEdgeType' => 'applications/legalpad/edge/LegalpadSignatureNeededByObjectEdgeType.php', 'LegalpadTransaction' => 'applications/legalpad/storage/LegalpadTransaction.php', 'LegalpadTransactionComment' => 'applications/legalpad/storage/LegalpadTransactionComment.php', 'LegalpadTransactionQuery' => 'applications/legalpad/query/LegalpadTransactionQuery.php', 'LegalpadTransactionView' => 'applications/legalpad/view/LegalpadTransactionView.php', 'LiskChunkTestCase' => 'infrastructure/storage/lisk/__tests__/LiskChunkTestCase.php', 'LiskDAO' => 'infrastructure/storage/lisk/LiskDAO.php', 'LiskDAOSet' => 'infrastructure/storage/lisk/LiskDAOSet.php', 'LiskDAOTestCase' => 'infrastructure/storage/lisk/__tests__/LiskDAOTestCase.php', 'LiskEphemeralObjectException' => 'infrastructure/storage/lisk/LiskEphemeralObjectException.php', 'LiskFixtureTestCase' => 'infrastructure/storage/lisk/__tests__/LiskFixtureTestCase.php', 'LiskIsolationTestCase' => 'infrastructure/storage/lisk/__tests__/LiskIsolationTestCase.php', 'LiskIsolationTestDAO' => 'infrastructure/storage/lisk/__tests__/LiskIsolationTestDAO.php', 'LiskIsolationTestDAOException' => 'infrastructure/storage/lisk/__tests__/LiskIsolationTestDAOException.php', 'LiskMigrationIterator' => 'infrastructure/storage/lisk/LiskMigrationIterator.php', 'LiskRawMigrationIterator' => 'infrastructure/storage/lisk/LiskRawMigrationIterator.php', 'MacroConduitAPIMethod' => 'applications/macro/conduit/MacroConduitAPIMethod.php', 'MacroCreateMemeConduitAPIMethod' => 'applications/macro/conduit/MacroCreateMemeConduitAPIMethod.php', 'MacroEditConduitAPIMethod' => 'applications/macro/conduit/MacroEditConduitAPIMethod.php', 'MacroEmojiExample' => 'applications/uiexample/examples/MacroEmojiExample.php', 'MacroQueryConduitAPIMethod' => 'applications/macro/conduit/MacroQueryConduitAPIMethod.php', 'ManiphestAssignEmailCommand' => 'applications/maniphest/command/ManiphestAssignEmailCommand.php', 'ManiphestAssigneeDatasource' => 'applications/maniphest/typeahead/ManiphestAssigneeDatasource.php', 'ManiphestBulkEditCapability' => 'applications/maniphest/capability/ManiphestBulkEditCapability.php', 'ManiphestBulkEditController' => 'applications/maniphest/controller/ManiphestBulkEditController.php', 'ManiphestClaimEmailCommand' => 'applications/maniphest/command/ManiphestClaimEmailCommand.php', 'ManiphestCloseEmailCommand' => 'applications/maniphest/command/ManiphestCloseEmailCommand.php', 'ManiphestConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestConduitAPIMethod.php', 'ManiphestConfiguredCustomField' => 'applications/maniphest/field/ManiphestConfiguredCustomField.php', 'ManiphestConstants' => 'applications/maniphest/constants/ManiphestConstants.php', 'ManiphestController' => 'applications/maniphest/controller/ManiphestController.php', 'ManiphestCreateMailReceiver' => 'applications/maniphest/mail/ManiphestCreateMailReceiver.php', 'ManiphestCreateTaskConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestCreateTaskConduitAPIMethod.php', 'ManiphestCustomField' => 'applications/maniphest/field/ManiphestCustomField.php', 'ManiphestCustomFieldNumericIndex' => 'applications/maniphest/storage/ManiphestCustomFieldNumericIndex.php', 'ManiphestCustomFieldStatusParser' => 'applications/maniphest/field/parser/ManiphestCustomFieldStatusParser.php', 'ManiphestCustomFieldStatusParserTestCase' => 'applications/maniphest/field/parser/__tests__/ManiphestCustomFieldStatusParserTestCase.php', 'ManiphestCustomFieldStorage' => 'applications/maniphest/storage/ManiphestCustomFieldStorage.php', 'ManiphestCustomFieldStringIndex' => 'applications/maniphest/storage/ManiphestCustomFieldStringIndex.php', 'ManiphestDAO' => 'applications/maniphest/storage/ManiphestDAO.php', 'ManiphestDefaultEditCapability' => 'applications/maniphest/capability/ManiphestDefaultEditCapability.php', 'ManiphestDefaultViewCapability' => 'applications/maniphest/capability/ManiphestDefaultViewCapability.php', 'ManiphestEditConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestEditConduitAPIMethod.php', 'ManiphestEditEngine' => 'applications/maniphest/editor/ManiphestEditEngine.php', 'ManiphestEmailCommand' => 'applications/maniphest/command/ManiphestEmailCommand.php', 'ManiphestGetTaskTransactionsConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php', 'ManiphestHovercardEngineExtension' => 'applications/maniphest/engineextension/ManiphestHovercardEngineExtension.php', 'ManiphestInfoConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestInfoConduitAPIMethod.php', 'ManiphestMailEngineExtension' => 'applications/maniphest/engineextension/ManiphestMailEngineExtension.php', 'ManiphestNameIndex' => 'applications/maniphest/storage/ManiphestNameIndex.php', 'ManiphestPointsConfigType' => 'applications/maniphest/config/ManiphestPointsConfigType.php', 'ManiphestPrioritiesConfigType' => 'applications/maniphest/config/ManiphestPrioritiesConfigType.php', 'ManiphestPriorityEmailCommand' => 'applications/maniphest/command/ManiphestPriorityEmailCommand.php', 'ManiphestPrioritySearchConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestPrioritySearchConduitAPIMethod.php', 'ManiphestProjectNameFulltextEngineExtension' => 'applications/maniphest/engineextension/ManiphestProjectNameFulltextEngineExtension.php', 'ManiphestQueryConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestQueryConduitAPIMethod.php', 'ManiphestQueryStatusesConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestQueryStatusesConduitAPIMethod.php', 'ManiphestRemarkupRule' => 'applications/maniphest/remarkup/ManiphestRemarkupRule.php', 'ManiphestReplyHandler' => 'applications/maniphest/mail/ManiphestReplyHandler.php', 'ManiphestReportController' => 'applications/maniphest/controller/ManiphestReportController.php', 'ManiphestSchemaSpec' => 'applications/maniphest/storage/ManiphestSchemaSpec.php', 'ManiphestSearchConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestSearchConduitAPIMethod.php', 'ManiphestStatusEmailCommand' => 'applications/maniphest/command/ManiphestStatusEmailCommand.php', 'ManiphestStatusSearchConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestStatusSearchConduitAPIMethod.php', 'ManiphestStatusesConfigType' => 'applications/maniphest/config/ManiphestStatusesConfigType.php', 'ManiphestSubpriorityController' => 'applications/maniphest/controller/ManiphestSubpriorityController.php', 'ManiphestSubtypesConfigType' => 'applications/maniphest/config/ManiphestSubtypesConfigType.php', 'ManiphestTask' => 'applications/maniphest/storage/ManiphestTask.php', 'ManiphestTaskAssignHeraldAction' => 'applications/maniphest/herald/ManiphestTaskAssignHeraldAction.php', 'ManiphestTaskAssignOtherHeraldAction' => 'applications/maniphest/herald/ManiphestTaskAssignOtherHeraldAction.php', 'ManiphestTaskAssignSelfHeraldAction' => 'applications/maniphest/herald/ManiphestTaskAssignSelfHeraldAction.php', 'ManiphestTaskAssigneeHeraldField' => 'applications/maniphest/herald/ManiphestTaskAssigneeHeraldField.php', 'ManiphestTaskAttachTransaction' => 'applications/maniphest/xaction/ManiphestTaskAttachTransaction.php', 'ManiphestTaskAuthorHeraldField' => 'applications/maniphest/herald/ManiphestTaskAuthorHeraldField.php', 'ManiphestTaskAuthorPolicyRule' => 'applications/maniphest/policyrule/ManiphestTaskAuthorPolicyRule.php', 'ManiphestTaskBulkEngine' => 'applications/maniphest/bulk/ManiphestTaskBulkEngine.php', 'ManiphestTaskCloseAsDuplicateRelationship' => 'applications/maniphest/relationship/ManiphestTaskCloseAsDuplicateRelationship.php', 'ManiphestTaskClosedStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskClosedStatusDatasource.php', 'ManiphestTaskCoverImageTransaction' => 'applications/maniphest/xaction/ManiphestTaskCoverImageTransaction.php', 'ManiphestTaskDependedOnByTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependedOnByTaskEdgeType.php', 'ManiphestTaskDependsOnTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependsOnTaskEdgeType.php', 'ManiphestTaskDescriptionHeraldField' => 'applications/maniphest/herald/ManiphestTaskDescriptionHeraldField.php', 'ManiphestTaskDescriptionTransaction' => 'applications/maniphest/xaction/ManiphestTaskDescriptionTransaction.php', 'ManiphestTaskDetailController' => 'applications/maniphest/controller/ManiphestTaskDetailController.php', 'ManiphestTaskEdgeTransaction' => 'applications/maniphest/xaction/ManiphestTaskEdgeTransaction.php', 'ManiphestTaskEditController' => 'applications/maniphest/controller/ManiphestTaskEditController.php', 'ManiphestTaskEditEngineLock' => 'applications/maniphest/editor/ManiphestTaskEditEngineLock.php', 'ManiphestTaskFerretEngine' => 'applications/maniphest/search/ManiphestTaskFerretEngine.php', 'ManiphestTaskFulltextEngine' => 'applications/maniphest/search/ManiphestTaskFulltextEngine.php', 'ManiphestTaskGraph' => 'infrastructure/graph/ManiphestTaskGraph.php', 'ManiphestTaskHasCommitEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasCommitEdgeType.php', 'ManiphestTaskHasCommitRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasCommitRelationship.php', 'ManiphestTaskHasDuplicateTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasDuplicateTaskEdgeType.php', 'ManiphestTaskHasMockEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasMockEdgeType.php', 'ManiphestTaskHasMockRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasMockRelationship.php', 'ManiphestTaskHasParentRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasParentRelationship.php', 'ManiphestTaskHasRevisionEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasRevisionEdgeType.php', 'ManiphestTaskHasRevisionRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasRevisionRelationship.php', 'ManiphestTaskHasSubtaskRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasSubtaskRelationship.php', 'ManiphestTaskHeraldField' => 'applications/maniphest/herald/ManiphestTaskHeraldField.php', 'ManiphestTaskHeraldFieldGroup' => 'applications/maniphest/herald/ManiphestTaskHeraldFieldGroup.php', 'ManiphestTaskIsDuplicateOfTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskIsDuplicateOfTaskEdgeType.php', 'ManiphestTaskListController' => 'applications/maniphest/controller/ManiphestTaskListController.php', 'ManiphestTaskListHTTPParameterType' => 'applications/maniphest/httpparametertype/ManiphestTaskListHTTPParameterType.php', 'ManiphestTaskListView' => 'applications/maniphest/view/ManiphestTaskListView.php', 'ManiphestTaskMailReceiver' => 'applications/maniphest/mail/ManiphestTaskMailReceiver.php', 'ManiphestTaskMergeInRelationship' => 'applications/maniphest/relationship/ManiphestTaskMergeInRelationship.php', 'ManiphestTaskMergedFromTransaction' => 'applications/maniphest/xaction/ManiphestTaskMergedFromTransaction.php', 'ManiphestTaskMergedIntoTransaction' => 'applications/maniphest/xaction/ManiphestTaskMergedIntoTransaction.php', 'ManiphestTaskOpenStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskOpenStatusDatasource.php', 'ManiphestTaskOwnerTransaction' => 'applications/maniphest/xaction/ManiphestTaskOwnerTransaction.php', 'ManiphestTaskPHIDResolver' => 'applications/maniphest/httpparametertype/ManiphestTaskPHIDResolver.php', 'ManiphestTaskPHIDType' => 'applications/maniphest/phid/ManiphestTaskPHIDType.php', 'ManiphestTaskParentTransaction' => 'applications/maniphest/xaction/ManiphestTaskParentTransaction.php', 'ManiphestTaskPoints' => 'applications/maniphest/constants/ManiphestTaskPoints.php', 'ManiphestTaskPointsTransaction' => 'applications/maniphest/xaction/ManiphestTaskPointsTransaction.php', 'ManiphestTaskPriority' => 'applications/maniphest/constants/ManiphestTaskPriority.php', 'ManiphestTaskPriorityDatasource' => 'applications/maniphest/typeahead/ManiphestTaskPriorityDatasource.php', 'ManiphestTaskPriorityHeraldAction' => 'applications/maniphest/herald/ManiphestTaskPriorityHeraldAction.php', 'ManiphestTaskPriorityHeraldField' => 'applications/maniphest/herald/ManiphestTaskPriorityHeraldField.php', 'ManiphestTaskPriorityTransaction' => 'applications/maniphest/xaction/ManiphestTaskPriorityTransaction.php', 'ManiphestTaskQuery' => 'applications/maniphest/query/ManiphestTaskQuery.php', 'ManiphestTaskRelationship' => 'applications/maniphest/relationship/ManiphestTaskRelationship.php', 'ManiphestTaskRelationshipSource' => 'applications/search/relationship/ManiphestTaskRelationshipSource.php', 'ManiphestTaskResultListView' => 'applications/maniphest/view/ManiphestTaskResultListView.php', 'ManiphestTaskSearchEngine' => 'applications/maniphest/query/ManiphestTaskSearchEngine.php', 'ManiphestTaskStatus' => 'applications/maniphest/constants/ManiphestTaskStatus.php', 'ManiphestTaskStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskStatusDatasource.php', 'ManiphestTaskStatusFunctionDatasource' => 'applications/maniphest/typeahead/ManiphestTaskStatusFunctionDatasource.php', 'ManiphestTaskStatusHeraldAction' => 'applications/maniphest/herald/ManiphestTaskStatusHeraldAction.php', 'ManiphestTaskStatusHeraldField' => 'applications/maniphest/herald/ManiphestTaskStatusHeraldField.php', 'ManiphestTaskStatusTestCase' => 'applications/maniphest/constants/__tests__/ManiphestTaskStatusTestCase.php', 'ManiphestTaskStatusTransaction' => 'applications/maniphest/xaction/ManiphestTaskStatusTransaction.php', 'ManiphestTaskSubpriorityTransaction' => 'applications/maniphest/xaction/ManiphestTaskSubpriorityTransaction.php', 'ManiphestTaskSubtypeDatasource' => 'applications/maniphest/typeahead/ManiphestTaskSubtypeDatasource.php', 'ManiphestTaskTestCase' => 'applications/maniphest/__tests__/ManiphestTaskTestCase.php', 'ManiphestTaskTitleHeraldField' => 'applications/maniphest/herald/ManiphestTaskTitleHeraldField.php', 'ManiphestTaskTitleTransaction' => 'applications/maniphest/xaction/ManiphestTaskTitleTransaction.php', 'ManiphestTaskTransactionType' => 'applications/maniphest/xaction/ManiphestTaskTransactionType.php', 'ManiphestTaskUnblockTransaction' => 'applications/maniphest/xaction/ManiphestTaskUnblockTransaction.php', 'ManiphestTransaction' => 'applications/maniphest/storage/ManiphestTransaction.php', 'ManiphestTransactionComment' => 'applications/maniphest/storage/ManiphestTransactionComment.php', 'ManiphestTransactionEditor' => 'applications/maniphest/editor/ManiphestTransactionEditor.php', 'ManiphestTransactionQuery' => 'applications/maniphest/query/ManiphestTransactionQuery.php', 'ManiphestUpdateConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestUpdateConduitAPIMethod.php', 'ManiphestView' => 'applications/maniphest/view/ManiphestView.php', 'MetaMTAEmailTransactionCommand' => 'applications/metamta/command/MetaMTAEmailTransactionCommand.php', 'MetaMTAEmailTransactionCommandTestCase' => 'applications/metamta/command/__tests__/MetaMTAEmailTransactionCommandTestCase.php', 'MetaMTAMailReceivedGarbageCollector' => 'applications/metamta/garbagecollector/MetaMTAMailReceivedGarbageCollector.php', 'MetaMTAMailSentGarbageCollector' => 'applications/metamta/garbagecollector/MetaMTAMailSentGarbageCollector.php', 'MetaMTAReceivedMailStatus' => 'applications/metamta/constants/MetaMTAReceivedMailStatus.php', 'MultimeterContext' => 'applications/multimeter/storage/MultimeterContext.php', 'MultimeterControl' => 'applications/multimeter/data/MultimeterControl.php', 'MultimeterController' => 'applications/multimeter/controller/MultimeterController.php', 'MultimeterDAO' => 'applications/multimeter/storage/MultimeterDAO.php', 'MultimeterDimension' => 'applications/multimeter/storage/MultimeterDimension.php', 'MultimeterEvent' => 'applications/multimeter/storage/MultimeterEvent.php', 'MultimeterEventGarbageCollector' => 'applications/multimeter/garbagecollector/MultimeterEventGarbageCollector.php', 'MultimeterHost' => 'applications/multimeter/storage/MultimeterHost.php', 'MultimeterLabel' => 'applications/multimeter/storage/MultimeterLabel.php', 'MultimeterSampleController' => 'applications/multimeter/controller/MultimeterSampleController.php', 'MultimeterViewer' => 'applications/multimeter/storage/MultimeterViewer.php', 'NuanceCommandImplementation' => 'applications/nuance/command/NuanceCommandImplementation.php', 'NuanceConduitAPIMethod' => 'applications/nuance/conduit/NuanceConduitAPIMethod.php', 'NuanceConsoleController' => 'applications/nuance/controller/NuanceConsoleController.php', 'NuanceContentSource' => 'applications/nuance/contentsource/NuanceContentSource.php', 'NuanceController' => 'applications/nuance/controller/NuanceController.php', 'NuanceDAO' => 'applications/nuance/storage/NuanceDAO.php', 'NuanceFormItemType' => 'applications/nuance/item/NuanceFormItemType.php', 'NuanceGitHubEventItemType' => 'applications/nuance/item/NuanceGitHubEventItemType.php', 'NuanceGitHubImportCursor' => 'applications/nuance/cursor/NuanceGitHubImportCursor.php', 'NuanceGitHubIssuesImportCursor' => 'applications/nuance/cursor/NuanceGitHubIssuesImportCursor.php', 'NuanceGitHubRawEvent' => 'applications/nuance/github/NuanceGitHubRawEvent.php', 'NuanceGitHubRawEventTestCase' => 'applications/nuance/github/__tests__/NuanceGitHubRawEventTestCase.php', 'NuanceGitHubRepositoryImportCursor' => 'applications/nuance/cursor/NuanceGitHubRepositoryImportCursor.php', 'NuanceGitHubRepositorySourceDefinition' => 'applications/nuance/source/NuanceGitHubRepositorySourceDefinition.php', 'NuanceImportCursor' => 'applications/nuance/cursor/NuanceImportCursor.php', 'NuanceImportCursorData' => 'applications/nuance/storage/NuanceImportCursorData.php', 'NuanceImportCursorDataQuery' => 'applications/nuance/query/NuanceImportCursorDataQuery.php', 'NuanceImportCursorPHIDType' => 'applications/nuance/phid/NuanceImportCursorPHIDType.php', 'NuanceItem' => 'applications/nuance/storage/NuanceItem.php', 'NuanceItemActionController' => 'applications/nuance/controller/NuanceItemActionController.php', 'NuanceItemCommand' => 'applications/nuance/storage/NuanceItemCommand.php', 'NuanceItemCommandQuery' => 'applications/nuance/query/NuanceItemCommandQuery.php', 'NuanceItemCommandSpec' => 'applications/nuance/command/NuanceItemCommandSpec.php', 'NuanceItemCommandTransaction' => 'applications/nuance/xaction/NuanceItemCommandTransaction.php', 'NuanceItemController' => 'applications/nuance/controller/NuanceItemController.php', 'NuanceItemEditor' => 'applications/nuance/editor/NuanceItemEditor.php', 'NuanceItemListController' => 'applications/nuance/controller/NuanceItemListController.php', 'NuanceItemManageController' => 'applications/nuance/controller/NuanceItemManageController.php', 'NuanceItemOwnerTransaction' => 'applications/nuance/xaction/NuanceItemOwnerTransaction.php', 'NuanceItemPHIDType' => 'applications/nuance/phid/NuanceItemPHIDType.php', 'NuanceItemPropertyTransaction' => 'applications/nuance/xaction/NuanceItemPropertyTransaction.php', 'NuanceItemQuery' => 'applications/nuance/query/NuanceItemQuery.php', 'NuanceItemQueueTransaction' => 'applications/nuance/xaction/NuanceItemQueueTransaction.php', 'NuanceItemRequestorTransaction' => 'applications/nuance/xaction/NuanceItemRequestorTransaction.php', 'NuanceItemSearchEngine' => 'applications/nuance/query/NuanceItemSearchEngine.php', 'NuanceItemSourceTransaction' => 'applications/nuance/xaction/NuanceItemSourceTransaction.php', 'NuanceItemStatusTransaction' => 'applications/nuance/xaction/NuanceItemStatusTransaction.php', 'NuanceItemTransaction' => 'applications/nuance/storage/NuanceItemTransaction.php', 'NuanceItemTransactionComment' => 'applications/nuance/storage/NuanceItemTransactionComment.php', 'NuanceItemTransactionQuery' => 'applications/nuance/query/NuanceItemTransactionQuery.php', 'NuanceItemTransactionType' => 'applications/nuance/xaction/NuanceItemTransactionType.php', 'NuanceItemType' => 'applications/nuance/item/NuanceItemType.php', 'NuanceItemUpdateWorker' => 'applications/nuance/worker/NuanceItemUpdateWorker.php', 'NuanceItemViewController' => 'applications/nuance/controller/NuanceItemViewController.php', 'NuanceManagementImportWorkflow' => 'applications/nuance/management/NuanceManagementImportWorkflow.php', 'NuanceManagementUpdateWorkflow' => 'applications/nuance/management/NuanceManagementUpdateWorkflow.php', 'NuanceManagementWorkflow' => 'applications/nuance/management/NuanceManagementWorkflow.php', 'NuancePhabricatorFormSourceDefinition' => 'applications/nuance/source/NuancePhabricatorFormSourceDefinition.php', 'NuanceQuery' => 'applications/nuance/query/NuanceQuery.php', 'NuanceQueue' => 'applications/nuance/storage/NuanceQueue.php', 'NuanceQueueController' => 'applications/nuance/controller/NuanceQueueController.php', 'NuanceQueueDatasource' => 'applications/nuance/typeahead/NuanceQueueDatasource.php', 'NuanceQueueEditController' => 'applications/nuance/controller/NuanceQueueEditController.php', 'NuanceQueueEditEngine' => 'applications/nuance/editor/NuanceQueueEditEngine.php', 'NuanceQueueEditor' => 'applications/nuance/editor/NuanceQueueEditor.php', 'NuanceQueueListController' => 'applications/nuance/controller/NuanceQueueListController.php', 'NuanceQueueNameTransaction' => 'applications/nuance/xaction/NuanceQueueNameTransaction.php', 'NuanceQueuePHIDType' => 'applications/nuance/phid/NuanceQueuePHIDType.php', 'NuanceQueueQuery' => 'applications/nuance/query/NuanceQueueQuery.php', 'NuanceQueueSearchEngine' => 'applications/nuance/query/NuanceQueueSearchEngine.php', 'NuanceQueueTransaction' => 'applications/nuance/storage/NuanceQueueTransaction.php', 'NuanceQueueTransactionComment' => 'applications/nuance/storage/NuanceQueueTransactionComment.php', 'NuanceQueueTransactionQuery' => 'applications/nuance/query/NuanceQueueTransactionQuery.php', 'NuanceQueueTransactionType' => 'applications/nuance/xaction/NuanceQueueTransactionType.php', 'NuanceQueueViewController' => 'applications/nuance/controller/NuanceQueueViewController.php', 'NuanceQueueWorkController' => 'applications/nuance/controller/NuanceQueueWorkController.php', 'NuanceSchemaSpec' => 'applications/nuance/storage/NuanceSchemaSpec.php', 'NuanceSource' => 'applications/nuance/storage/NuanceSource.php', 'NuanceSourceActionController' => 'applications/nuance/controller/NuanceSourceActionController.php', 'NuanceSourceController' => 'applications/nuance/controller/NuanceSourceController.php', 'NuanceSourceDefaultEditCapability' => 'applications/nuance/capability/NuanceSourceDefaultEditCapability.php', 'NuanceSourceDefaultQueueTransaction' => 'applications/nuance/xaction/NuanceSourceDefaultQueueTransaction.php', 'NuanceSourceDefaultViewCapability' => 'applications/nuance/capability/NuanceSourceDefaultViewCapability.php', 'NuanceSourceDefinition' => 'applications/nuance/source/NuanceSourceDefinition.php', 'NuanceSourceDefinitionTestCase' => 'applications/nuance/source/__tests__/NuanceSourceDefinitionTestCase.php', 'NuanceSourceEditController' => 'applications/nuance/controller/NuanceSourceEditController.php', 'NuanceSourceEditEngine' => 'applications/nuance/editor/NuanceSourceEditEngine.php', 'NuanceSourceEditor' => 'applications/nuance/editor/NuanceSourceEditor.php', 'NuanceSourceListController' => 'applications/nuance/controller/NuanceSourceListController.php', 'NuanceSourceManageCapability' => 'applications/nuance/capability/NuanceSourceManageCapability.php', 'NuanceSourceNameNgrams' => 'applications/nuance/storage/NuanceSourceNameNgrams.php', 'NuanceSourceNameTransaction' => 'applications/nuance/xaction/NuanceSourceNameTransaction.php', 'NuanceSourcePHIDType' => 'applications/nuance/phid/NuanceSourcePHIDType.php', 'NuanceSourceQuery' => 'applications/nuance/query/NuanceSourceQuery.php', 'NuanceSourceSearchEngine' => 'applications/nuance/query/NuanceSourceSearchEngine.php', 'NuanceSourceTransaction' => 'applications/nuance/storage/NuanceSourceTransaction.php', 'NuanceSourceTransactionComment' => 'applications/nuance/storage/NuanceSourceTransactionComment.php', 'NuanceSourceTransactionQuery' => 'applications/nuance/query/NuanceSourceTransactionQuery.php', 'NuanceSourceTransactionType' => 'applications/nuance/xaction/NuanceSourceTransactionType.php', 'NuanceSourceViewController' => 'applications/nuance/controller/NuanceSourceViewController.php', 'NuanceTransaction' => 'applications/nuance/storage/NuanceTransaction.php', 'NuanceTrashCommand' => 'applications/nuance/command/NuanceTrashCommand.php', 'NuanceWorker' => 'applications/nuance/worker/NuanceWorker.php', 'OwnersConduitAPIMethod' => 'applications/owners/conduit/OwnersConduitAPIMethod.php', 'OwnersEditConduitAPIMethod' => 'applications/owners/conduit/OwnersEditConduitAPIMethod.php', 'OwnersPackageReplyHandler' => 'applications/owners/mail/OwnersPackageReplyHandler.php', 'OwnersQueryConduitAPIMethod' => 'applications/owners/conduit/OwnersQueryConduitAPIMethod.php', 'OwnersSearchConduitAPIMethod' => 'applications/owners/conduit/OwnersSearchConduitAPIMethod.php', 'PHIDConduitAPIMethod' => 'applications/phid/conduit/PHIDConduitAPIMethod.php', 'PHIDInfoConduitAPIMethod' => 'applications/phid/conduit/PHIDInfoConduitAPIMethod.php', 'PHIDLookupConduitAPIMethod' => 'applications/phid/conduit/PHIDLookupConduitAPIMethod.php', 'PHIDQueryConduitAPIMethod' => 'applications/phid/conduit/PHIDQueryConduitAPIMethod.php', 'PHUI' => 'view/phui/PHUI.php', 'PHUIActionPanelExample' => 'applications/uiexample/examples/PHUIActionPanelExample.php', 'PHUIActionPanelView' => 'view/phui/PHUIActionPanelView.php', 'PHUIApplicationMenuView' => 'view/layout/PHUIApplicationMenuView.php', 'PHUIBadgeBoxView' => 'view/phui/PHUIBadgeBoxView.php', 'PHUIBadgeExample' => 'applications/uiexample/examples/PHUIBadgeExample.php', 'PHUIBadgeMiniView' => 'view/phui/PHUIBadgeMiniView.php', 'PHUIBadgeView' => 'view/phui/PHUIBadgeView.php', 'PHUIBigInfoExample' => 'applications/uiexample/examples/PHUIBigInfoExample.php', 'PHUIBigInfoView' => 'view/phui/PHUIBigInfoView.php', 'PHUIBoxExample' => 'applications/uiexample/examples/PHUIBoxExample.php', 'PHUIBoxView' => 'view/phui/PHUIBoxView.php', 'PHUIButtonBarExample' => 'applications/uiexample/examples/PHUIButtonBarExample.php', 'PHUIButtonBarView' => 'view/phui/PHUIButtonBarView.php', 'PHUIButtonExample' => 'applications/uiexample/examples/PHUIButtonExample.php', 'PHUIButtonView' => 'view/phui/PHUIButtonView.php', 'PHUICMSView' => 'view/phui/PHUICMSView.php', 'PHUICalendarDayView' => 'view/phui/calendar/PHUICalendarDayView.php', 'PHUICalendarListView' => 'view/phui/calendar/PHUICalendarListView.php', 'PHUICalendarMonthView' => 'view/phui/calendar/PHUICalendarMonthView.php', 'PHUICalendarWeekView' => 'view/phui/calendar/PHUICalendarWeekView.php', 'PHUICalendarWidgetView' => 'view/phui/calendar/PHUICalendarWidgetView.php', 'PHUIColorPalletteExample' => 'applications/uiexample/examples/PHUIColorPalletteExample.php', 'PHUICrumbView' => 'view/phui/PHUICrumbView.php', 'PHUICrumbsView' => 'view/phui/PHUICrumbsView.php', 'PHUICurtainExtension' => 'view/extension/PHUICurtainExtension.php', 'PHUICurtainPanelView' => 'view/layout/PHUICurtainPanelView.php', 'PHUICurtainView' => 'view/layout/PHUICurtainView.php', 'PHUIDiffGraphView' => 'infrastructure/diff/view/PHUIDiffGraphView.php', 'PHUIDiffGraphViewTestCase' => 'infrastructure/diff/view/__tests__/PHUIDiffGraphViewTestCase.php', 'PHUIDiffInlineCommentDetailView' => 'infrastructure/diff/view/PHUIDiffInlineCommentDetailView.php', 'PHUIDiffInlineCommentEditView' => 'infrastructure/diff/view/PHUIDiffInlineCommentEditView.php', 'PHUIDiffInlineCommentPreviewListView' => 'infrastructure/diff/view/PHUIDiffInlineCommentPreviewListView.php', 'PHUIDiffInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffInlineCommentRowScaffold.php', 'PHUIDiffInlineCommentTableScaffold' => 'infrastructure/diff/view/PHUIDiffInlineCommentTableScaffold.php', 'PHUIDiffInlineCommentUndoView' => 'infrastructure/diff/view/PHUIDiffInlineCommentUndoView.php', 'PHUIDiffInlineCommentView' => 'infrastructure/diff/view/PHUIDiffInlineCommentView.php', 'PHUIDiffInlineThreader' => 'infrastructure/diff/view/PHUIDiffInlineThreader.php', 'PHUIDiffOneUpInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffOneUpInlineCommentRowScaffold.php', 'PHUIDiffRevealIconView' => 'infrastructure/diff/view/PHUIDiffRevealIconView.php', 'PHUIDiffTableOfContentsItemView' => 'infrastructure/diff/view/PHUIDiffTableOfContentsItemView.php', 'PHUIDiffTableOfContentsListView' => 'infrastructure/diff/view/PHUIDiffTableOfContentsListView.php', 'PHUIDiffTwoUpInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffTwoUpInlineCommentRowScaffold.php', 'PHUIDocumentSummaryView' => 'view/phui/PHUIDocumentSummaryView.php', 'PHUIDocumentView' => 'view/phui/PHUIDocumentView.php', 'PHUIFeedStoryExample' => 'applications/uiexample/examples/PHUIFeedStoryExample.php', 'PHUIFeedStoryView' => 'view/phui/PHUIFeedStoryView.php', 'PHUIFormDividerControl' => 'view/form/control/PHUIFormDividerControl.php', 'PHUIFormFileControl' => 'view/form/control/PHUIFormFileControl.php', 'PHUIFormFreeformDateControl' => 'view/form/control/PHUIFormFreeformDateControl.php', 'PHUIFormIconSetControl' => 'view/form/control/PHUIFormIconSetControl.php', 'PHUIFormInsetView' => 'view/form/PHUIFormInsetView.php', 'PHUIFormLayoutView' => 'view/form/PHUIFormLayoutView.php', 'PHUIFormNumberControl' => 'view/form/control/PHUIFormNumberControl.php', 'PHUIHandleListView' => 'applications/phid/view/PHUIHandleListView.php', 'PHUIHandleTagListView' => 'applications/phid/view/PHUIHandleTagListView.php', 'PHUIHandleView' => 'applications/phid/view/PHUIHandleView.php', 'PHUIHeadThingView' => 'view/phui/PHUIHeadThingView.php', 'PHUIHeaderView' => 'view/phui/PHUIHeaderView.php', 'PHUIHomeView' => 'applications/home/view/PHUIHomeView.php', 'PHUIHovercardUIExample' => 'applications/uiexample/examples/PHUIHovercardUIExample.php', 'PHUIHovercardView' => 'view/phui/PHUIHovercardView.php', 'PHUIIconCircleView' => 'view/phui/PHUIIconCircleView.php', 'PHUIIconExample' => 'applications/uiexample/examples/PHUIIconExample.php', 'PHUIIconView' => 'view/phui/PHUIIconView.php', 'PHUIImageMaskExample' => 'applications/uiexample/examples/PHUIImageMaskExample.php', 'PHUIImageMaskView' => 'view/phui/PHUIImageMaskView.php', 'PHUIInfoExample' => 'applications/uiexample/examples/PHUIInfoExample.php', 'PHUIInfoView' => 'view/phui/PHUIInfoView.php', 'PHUIInvisibleCharacterTestCase' => 'view/phui/__tests__/PHUIInvisibleCharacterTestCase.php', 'PHUIInvisibleCharacterView' => 'view/phui/PHUIInvisibleCharacterView.php', 'PHUILeftRightExample' => 'applications/uiexample/examples/PHUILeftRightExample.php', 'PHUILeftRightView' => 'view/phui/PHUILeftRightView.php', 'PHUIListExample' => 'applications/uiexample/examples/PHUIListExample.php', 'PHUIListItemView' => 'view/phui/PHUIListItemView.php', 'PHUIListView' => 'view/phui/PHUIListView.php', 'PHUIListViewTestCase' => 'view/layout/__tests__/PHUIListViewTestCase.php', 'PHUIObjectBoxView' => 'view/phui/PHUIObjectBoxView.php', 'PHUIObjectItemListExample' => 'applications/uiexample/examples/PHUIObjectItemListExample.php', 'PHUIObjectItemListView' => 'view/phui/PHUIObjectItemListView.php', 'PHUIObjectItemView' => 'view/phui/PHUIObjectItemView.php', 'PHUIPagerView' => 'view/phui/PHUIPagerView.php', 'PHUIPinboardItemView' => 'view/phui/PHUIPinboardItemView.php', 'PHUIPinboardView' => 'view/phui/PHUIPinboardView.php', 'PHUIPolicySectionView' => 'applications/policy/view/PHUIPolicySectionView.php', 'PHUIPropertyGroupView' => 'view/phui/PHUIPropertyGroupView.php', 'PHUIPropertyListExample' => 'applications/uiexample/examples/PHUIPropertyListExample.php', 'PHUIPropertyListView' => 'view/phui/PHUIPropertyListView.php', 'PHUIRemarkupImageView' => 'infrastructure/markup/view/PHUIRemarkupImageView.php', 'PHUIRemarkupPreviewPanel' => 'view/phui/PHUIRemarkupPreviewPanel.php', 'PHUIRemarkupView' => 'infrastructure/markup/view/PHUIRemarkupView.php', 'PHUISegmentBarSegmentView' => 'view/phui/PHUISegmentBarSegmentView.php', 'PHUISegmentBarView' => 'view/phui/PHUISegmentBarView.php', 'PHUISpacesNamespaceContextView' => 'applications/spaces/view/PHUISpacesNamespaceContextView.php', 'PHUIStatusItemView' => 'view/phui/PHUIStatusItemView.php', 'PHUIStatusListView' => 'view/phui/PHUIStatusListView.php', 'PHUITabGroupView' => 'view/phui/PHUITabGroupView.php', 'PHUITabView' => 'view/phui/PHUITabView.php', 'PHUITagExample' => 'applications/uiexample/examples/PHUITagExample.php', 'PHUITagView' => 'view/phui/PHUITagView.php', 'PHUITimelineEventView' => 'view/phui/PHUITimelineEventView.php', 'PHUITimelineExample' => 'applications/uiexample/examples/PHUITimelineExample.php', 'PHUITimelineView' => 'view/phui/PHUITimelineView.php', 'PHUITwoColumnView' => 'view/phui/PHUITwoColumnView.php', 'PHUITypeaheadExample' => 'applications/uiexample/examples/PHUITypeaheadExample.php', 'PHUIUserAvailabilityView' => 'applications/calendar/view/PHUIUserAvailabilityView.php', 'PHUIWorkboardView' => 'view/phui/PHUIWorkboardView.php', 'PHUIWorkpanelView' => 'view/phui/PHUIWorkpanelView.php', 'PHUIXComponentsExample' => 'applications/uiexample/examples/PHUIXComponentsExample.php', 'PassphraseAbstractKey' => 'applications/passphrase/keys/PassphraseAbstractKey.php', 'PassphraseConduitAPIMethod' => 'applications/passphrase/conduit/PassphraseConduitAPIMethod.php', 'PassphraseController' => 'applications/passphrase/controller/PassphraseController.php', 'PassphraseCredential' => 'applications/passphrase/storage/PassphraseCredential.php', 'PassphraseCredentialAuthorPolicyRule' => 'applications/passphrase/policyrule/PassphraseCredentialAuthorPolicyRule.php', 'PassphraseCredentialConduitController' => 'applications/passphrase/controller/PassphraseCredentialConduitController.php', 'PassphraseCredentialConduitTransaction' => 'applications/passphrase/xaction/PassphraseCredentialConduitTransaction.php', 'PassphraseCredentialControl' => 'applications/passphrase/view/PassphraseCredentialControl.php', 'PassphraseCredentialCreateController' => 'applications/passphrase/controller/PassphraseCredentialCreateController.php', 'PassphraseCredentialDescriptionTransaction' => 'applications/passphrase/xaction/PassphraseCredentialDescriptionTransaction.php', 'PassphraseCredentialDestroyController' => 'applications/passphrase/controller/PassphraseCredentialDestroyController.php', 'PassphraseCredentialDestroyTransaction' => 'applications/passphrase/xaction/PassphraseCredentialDestroyTransaction.php', 'PassphraseCredentialEditController' => 'applications/passphrase/controller/PassphraseCredentialEditController.php', 'PassphraseCredentialFerretEngine' => 'applications/passphrase/search/PassphraseCredentialFerretEngine.php', 'PassphraseCredentialFulltextEngine' => 'applications/passphrase/search/PassphraseCredentialFulltextEngine.php', 'PassphraseCredentialListController' => 'applications/passphrase/controller/PassphraseCredentialListController.php', 'PassphraseCredentialLockController' => 'applications/passphrase/controller/PassphraseCredentialLockController.php', 'PassphraseCredentialLockTransaction' => 'applications/passphrase/xaction/PassphraseCredentialLockTransaction.php', 'PassphraseCredentialLookedAtTransaction' => 'applications/passphrase/xaction/PassphraseCredentialLookedAtTransaction.php', 'PassphraseCredentialNameTransaction' => 'applications/passphrase/xaction/PassphraseCredentialNameTransaction.php', 'PassphraseCredentialPHIDType' => 'applications/passphrase/phid/PassphraseCredentialPHIDType.php', 'PassphraseCredentialPublicController' => 'applications/passphrase/controller/PassphraseCredentialPublicController.php', 'PassphraseCredentialQuery' => 'applications/passphrase/query/PassphraseCredentialQuery.php', 'PassphraseCredentialRevealController' => 'applications/passphrase/controller/PassphraseCredentialRevealController.php', 'PassphraseCredentialSearchEngine' => 'applications/passphrase/query/PassphraseCredentialSearchEngine.php', 'PassphraseCredentialSecretIDTransaction' => 'applications/passphrase/xaction/PassphraseCredentialSecretIDTransaction.php', 'PassphraseCredentialTransaction' => 'applications/passphrase/storage/PassphraseCredentialTransaction.php', 'PassphraseCredentialTransactionEditor' => 'applications/passphrase/editor/PassphraseCredentialTransactionEditor.php', 'PassphraseCredentialTransactionQuery' => 'applications/passphrase/query/PassphraseCredentialTransactionQuery.php', 'PassphraseCredentialTransactionType' => 'applications/passphrase/xaction/PassphraseCredentialTransactionType.php', 'PassphraseCredentialType' => 'applications/passphrase/credentialtype/PassphraseCredentialType.php', 'PassphraseCredentialTypeTestCase' => 'applications/passphrase/credentialtype/__tests__/PassphraseCredentialTypeTestCase.php', 'PassphraseCredentialUsernameTransaction' => 'applications/passphrase/xaction/PassphraseCredentialUsernameTransaction.php', 'PassphraseCredentialViewController' => 'applications/passphrase/controller/PassphraseCredentialViewController.php', 'PassphraseDAO' => 'applications/passphrase/storage/PassphraseDAO.php', 'PassphraseDefaultEditCapability' => 'applications/passphrase/capability/PassphraseDefaultEditCapability.php', 'PassphraseDefaultViewCapability' => 'applications/passphrase/capability/PassphraseDefaultViewCapability.php', 'PassphraseNoteCredentialType' => 'applications/passphrase/credentialtype/PassphraseNoteCredentialType.php', 'PassphrasePasswordCredentialType' => 'applications/passphrase/credentialtype/PassphrasePasswordCredentialType.php', 'PassphrasePasswordKey' => 'applications/passphrase/keys/PassphrasePasswordKey.php', 'PassphraseQueryConduitAPIMethod' => 'applications/passphrase/conduit/PassphraseQueryConduitAPIMethod.php', 'PassphraseRemarkupRule' => 'applications/passphrase/remarkup/PassphraseRemarkupRule.php', 'PassphraseSSHGeneratedKeyCredentialType' => 'applications/passphrase/credentialtype/PassphraseSSHGeneratedKeyCredentialType.php', 'PassphraseSSHKey' => 'applications/passphrase/keys/PassphraseSSHKey.php', 'PassphraseSSHPrivateKeyCredentialType' => 'applications/passphrase/credentialtype/PassphraseSSHPrivateKeyCredentialType.php', 'PassphraseSSHPrivateKeyFileCredentialType' => 'applications/passphrase/credentialtype/PassphraseSSHPrivateKeyFileCredentialType.php', 'PassphraseSSHPrivateKeyTextCredentialType' => 'applications/passphrase/credentialtype/PassphraseSSHPrivateKeyTextCredentialType.php', 'PassphraseSchemaSpec' => 'applications/passphrase/storage/PassphraseSchemaSpec.php', 'PassphraseSecret' => 'applications/passphrase/storage/PassphraseSecret.php', 'PassphraseTokenCredentialType' => 'applications/passphrase/credentialtype/PassphraseTokenCredentialType.php', 'PasteConduitAPIMethod' => 'applications/paste/conduit/PasteConduitAPIMethod.php', 'PasteCreateConduitAPIMethod' => 'applications/paste/conduit/PasteCreateConduitAPIMethod.php', 'PasteCreateMailReceiver' => 'applications/paste/mail/PasteCreateMailReceiver.php', 'PasteDefaultEditCapability' => 'applications/paste/capability/PasteDefaultEditCapability.php', 'PasteDefaultViewCapability' => 'applications/paste/capability/PasteDefaultViewCapability.php', 'PasteEditConduitAPIMethod' => 'applications/paste/conduit/PasteEditConduitAPIMethod.php', 'PasteEmbedView' => 'applications/paste/view/PasteEmbedView.php', 'PasteInfoConduitAPIMethod' => 'applications/paste/conduit/PasteInfoConduitAPIMethod.php', 'PasteLanguageSelectDatasource' => 'applications/paste/typeahead/PasteLanguageSelectDatasource.php', 'PasteMailReceiver' => 'applications/paste/mail/PasteMailReceiver.php', 'PasteQueryConduitAPIMethod' => 'applications/paste/conduit/PasteQueryConduitAPIMethod.php', 'PasteReplyHandler' => 'applications/paste/mail/PasteReplyHandler.php', 'PasteSearchConduitAPIMethod' => 'applications/paste/conduit/PasteSearchConduitAPIMethod.php', 'PeopleBrowseUserDirectoryCapability' => 'applications/people/capability/PeopleBrowseUserDirectoryCapability.php', 'PeopleCreateUsersCapability' => 'applications/people/capability/PeopleCreateUsersCapability.php', 'PeopleDisableUsersCapability' => 'applications/people/capability/PeopleDisableUsersCapability.php', 'PeopleHovercardEngineExtension' => 'applications/people/engineextension/PeopleHovercardEngineExtension.php', 'PeopleMainMenuBarExtension' => 'applications/people/engineextension/PeopleMainMenuBarExtension.php', 'PeopleUserLogGarbageCollector' => 'applications/people/garbagecollector/PeopleUserLogGarbageCollector.php', 'Phabricator404Controller' => 'applications/base/controller/Phabricator404Controller.php', 'PhabricatorAWSConfigOptions' => 'applications/config/option/PhabricatorAWSConfigOptions.php', 'PhabricatorAccessControlTestCase' => 'applications/base/controller/__tests__/PhabricatorAccessControlTestCase.php', 'PhabricatorAccessLog' => 'infrastructure/log/PhabricatorAccessLog.php', 'PhabricatorAccessLogConfigOptions' => 'applications/config/option/PhabricatorAccessLogConfigOptions.php', 'PhabricatorAccessibilitySetting' => 'applications/settings/setting/PhabricatorAccessibilitySetting.php', 'PhabricatorAccountSettingsPanel' => 'applications/settings/panel/PhabricatorAccountSettingsPanel.php', 'PhabricatorActionListView' => 'view/layout/PhabricatorActionListView.php', 'PhabricatorActionView' => 'view/layout/PhabricatorActionView.php', 'PhabricatorActivitySettingsPanel' => 'applications/settings/panel/PhabricatorActivitySettingsPanel.php', 'PhabricatorAdministratorsPolicyRule' => 'applications/people/policyrule/PhabricatorAdministratorsPolicyRule.php', 'PhabricatorAjaxRequestExceptionHandler' => 'aphront/handler/PhabricatorAjaxRequestExceptionHandler.php', 'PhabricatorAlmanacApplication' => 'applications/almanac/application/PhabricatorAlmanacApplication.php', 'PhabricatorAmazonAuthProvider' => 'applications/auth/provider/PhabricatorAmazonAuthProvider.php', 'PhabricatorAnchorView' => 'view/layout/PhabricatorAnchorView.php', 'PhabricatorAphlictManagementDebugWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementDebugWorkflow.php', 'PhabricatorAphlictManagementRestartWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementRestartWorkflow.php', 'PhabricatorAphlictManagementStartWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementStartWorkflow.php', 'PhabricatorAphlictManagementStatusWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementStatusWorkflow.php', 'PhabricatorAphlictManagementStopWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementStopWorkflow.php', 'PhabricatorAphlictManagementWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php', 'PhabricatorAphlictSetupCheck' => 'applications/notification/setup/PhabricatorAphlictSetupCheck.php', 'PhabricatorAphrontBarUIExample' => 'applications/uiexample/examples/PhabricatorAphrontBarUIExample.php', 'PhabricatorAphrontViewTestCase' => 'view/__tests__/PhabricatorAphrontViewTestCase.php', 'PhabricatorAppSearchEngine' => 'applications/meta/query/PhabricatorAppSearchEngine.php', 'PhabricatorApplication' => 'applications/base/PhabricatorApplication.php', 'PhabricatorApplicationApplicationPHIDType' => 'applications/meta/phid/PhabricatorApplicationApplicationPHIDType.php', 'PhabricatorApplicationApplicationTransaction' => 'applications/meta/storage/PhabricatorApplicationApplicationTransaction.php', 'PhabricatorApplicationApplicationTransactionQuery' => 'applications/meta/query/PhabricatorApplicationApplicationTransactionQuery.php', 'PhabricatorApplicationConfigOptions' => 'applications/config/option/PhabricatorApplicationConfigOptions.php', 'PhabricatorApplicationConfigurationPanel' => 'applications/meta/panel/PhabricatorApplicationConfigurationPanel.php', 'PhabricatorApplicationConfigurationPanelTestCase' => 'applications/meta/panel/__tests__/PhabricatorApplicationConfigurationPanelTestCase.php', 'PhabricatorApplicationDatasource' => 'applications/meta/typeahead/PhabricatorApplicationDatasource.php', 'PhabricatorApplicationDetailViewController' => 'applications/meta/controller/PhabricatorApplicationDetailViewController.php', 'PhabricatorApplicationEditController' => 'applications/meta/controller/PhabricatorApplicationEditController.php', 'PhabricatorApplicationEditEngine' => 'applications/meta/editor/PhabricatorApplicationEditEngine.php', 'PhabricatorApplicationEditHTTPParameterHelpView' => 'applications/transactions/view/PhabricatorApplicationEditHTTPParameterHelpView.php', 'PhabricatorApplicationEditor' => 'applications/meta/editor/PhabricatorApplicationEditor.php', 'PhabricatorApplicationEmailCommandsController' => 'applications/meta/controller/PhabricatorApplicationEmailCommandsController.php', 'PhabricatorApplicationObjectMailEngineExtension' => 'applications/transactions/engineextension/PhabricatorApplicationObjectMailEngineExtension.php', 'PhabricatorApplicationPanelController' => 'applications/meta/controller/PhabricatorApplicationPanelController.php', 'PhabricatorApplicationPolicyChangeTransaction' => 'applications/meta/xactions/PhabricatorApplicationPolicyChangeTransaction.php', 'PhabricatorApplicationProfileMenuItem' => 'applications/search/menuitem/PhabricatorApplicationProfileMenuItem.php', 'PhabricatorApplicationQuery' => 'applications/meta/query/PhabricatorApplicationQuery.php', 'PhabricatorApplicationSchemaSpec' => 'applications/meta/storage/PhabricatorApplicationSchemaSpec.php', 'PhabricatorApplicationSearchController' => 'applications/search/controller/PhabricatorApplicationSearchController.php', 'PhabricatorApplicationSearchEngine' => 'applications/search/engine/PhabricatorApplicationSearchEngine.php', 'PhabricatorApplicationSearchEngineTestCase' => 'applications/search/engine/__tests__/PhabricatorApplicationSearchEngineTestCase.php', 'PhabricatorApplicationSearchResultView' => 'applications/search/view/PhabricatorApplicationSearchResultView.php', 'PhabricatorApplicationTestCase' => 'applications/base/__tests__/PhabricatorApplicationTestCase.php', 'PhabricatorApplicationTransaction' => 'applications/transactions/storage/PhabricatorApplicationTransaction.php', 'PhabricatorApplicationTransactionComment' => 'applications/transactions/storage/PhabricatorApplicationTransactionComment.php', 'PhabricatorApplicationTransactionCommentEditController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php', 'PhabricatorApplicationTransactionCommentEditor' => 'applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php', 'PhabricatorApplicationTransactionCommentHistoryController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentHistoryController.php', 'PhabricatorApplicationTransactionCommentQuery' => 'applications/transactions/query/PhabricatorApplicationTransactionCommentQuery.php', 'PhabricatorApplicationTransactionCommentQuoteController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentQuoteController.php', 'PhabricatorApplicationTransactionCommentRawController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentRawController.php', 'PhabricatorApplicationTransactionCommentRemoveController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php', 'PhabricatorApplicationTransactionCommentView' => 'applications/transactions/view/PhabricatorApplicationTransactionCommentView.php', 'PhabricatorApplicationTransactionController' => 'applications/transactions/controller/PhabricatorApplicationTransactionController.php', 'PhabricatorApplicationTransactionDetailController' => 'applications/transactions/controller/PhabricatorApplicationTransactionDetailController.php', 'PhabricatorApplicationTransactionEditor' => 'applications/transactions/editor/PhabricatorApplicationTransactionEditor.php', 'PhabricatorApplicationTransactionFeedStory' => 'applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php', 'PhabricatorApplicationTransactionInterface' => 'applications/transactions/interface/PhabricatorApplicationTransactionInterface.php', 'PhabricatorApplicationTransactionNoEffectException' => 'applications/transactions/exception/PhabricatorApplicationTransactionNoEffectException.php', 'PhabricatorApplicationTransactionNoEffectResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionNoEffectResponse.php', 'PhabricatorApplicationTransactionPublishWorker' => 'applications/transactions/worker/PhabricatorApplicationTransactionPublishWorker.php', 'PhabricatorApplicationTransactionQuery' => 'applications/transactions/query/PhabricatorApplicationTransactionQuery.php', 'PhabricatorApplicationTransactionRemarkupPreviewController' => 'applications/transactions/controller/PhabricatorApplicationTransactionRemarkupPreviewController.php', 'PhabricatorApplicationTransactionReplyHandler' => 'applications/transactions/replyhandler/PhabricatorApplicationTransactionReplyHandler.php', 'PhabricatorApplicationTransactionResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionResponse.php', 'PhabricatorApplicationTransactionShowOlderController' => 'applications/transactions/controller/PhabricatorApplicationTransactionShowOlderController.php', 'PhabricatorApplicationTransactionStructureException' => 'applications/transactions/exception/PhabricatorApplicationTransactionStructureException.php', 'PhabricatorApplicationTransactionTemplatedCommentQuery' => 'applications/transactions/query/PhabricatorApplicationTransactionTemplatedCommentQuery.php', 'PhabricatorApplicationTransactionTextDiffDetailView' => 'applications/transactions/view/PhabricatorApplicationTransactionTextDiffDetailView.php', 'PhabricatorApplicationTransactionTransactionPHIDType' => 'applications/transactions/phid/PhabricatorApplicationTransactionTransactionPHIDType.php', 'PhabricatorApplicationTransactionType' => 'applications/meta/xactions/PhabricatorApplicationTransactionType.php', 'PhabricatorApplicationTransactionValidationError' => 'applications/transactions/error/PhabricatorApplicationTransactionValidationError.php', 'PhabricatorApplicationTransactionValidationException' => 'applications/transactions/exception/PhabricatorApplicationTransactionValidationException.php', 'PhabricatorApplicationTransactionValidationResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionValidationResponse.php', 'PhabricatorApplicationTransactionValueController' => 'applications/transactions/controller/PhabricatorApplicationTransactionValueController.php', 'PhabricatorApplicationTransactionView' => 'applications/transactions/view/PhabricatorApplicationTransactionView.php', 'PhabricatorApplicationTransactionWarningException' => 'applications/transactions/exception/PhabricatorApplicationTransactionWarningException.php', 'PhabricatorApplicationTransactionWarningResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionWarningResponse.php', 'PhabricatorApplicationUninstallController' => 'applications/meta/controller/PhabricatorApplicationUninstallController.php', 'PhabricatorApplicationUninstallTransaction' => 'applications/meta/xactions/PhabricatorApplicationUninstallTransaction.php', 'PhabricatorApplicationsApplication' => 'applications/meta/application/PhabricatorApplicationsApplication.php', 'PhabricatorApplicationsController' => 'applications/meta/controller/PhabricatorApplicationsController.php', 'PhabricatorApplicationsListController' => 'applications/meta/controller/PhabricatorApplicationsListController.php', 'PhabricatorApplyEditField' => 'applications/transactions/editfield/PhabricatorApplyEditField.php', 'PhabricatorAsanaAuthProvider' => 'applications/auth/provider/PhabricatorAsanaAuthProvider.php', 'PhabricatorAsanaConfigOptions' => 'applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php', 'PhabricatorAsanaSubtaskHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorAsanaSubtaskHasObjectEdgeType.php', 'PhabricatorAsanaTaskHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorAsanaTaskHasObjectEdgeType.php', 'PhabricatorAudioDocumentEngine' => 'applications/files/document/PhabricatorAudioDocumentEngine.php', 'PhabricatorAuditActionConstants' => 'applications/audit/constants/PhabricatorAuditActionConstants.php', 'PhabricatorAuditApplication' => 'applications/audit/application/PhabricatorAuditApplication.php', 'PhabricatorAuditCommentEditor' => 'applications/audit/editor/PhabricatorAuditCommentEditor.php', 'PhabricatorAuditController' => 'applications/audit/controller/PhabricatorAuditController.php', 'PhabricatorAuditEditor' => 'applications/audit/editor/PhabricatorAuditEditor.php', 'PhabricatorAuditInlineComment' => 'applications/audit/storage/PhabricatorAuditInlineComment.php', 'PhabricatorAuditListView' => 'applications/audit/view/PhabricatorAuditListView.php', 'PhabricatorAuditMailReceiver' => 'applications/audit/mail/PhabricatorAuditMailReceiver.php', 'PhabricatorAuditManagementDeleteWorkflow' => 'applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php', 'PhabricatorAuditManagementWorkflow' => 'applications/audit/management/PhabricatorAuditManagementWorkflow.php', 'PhabricatorAuditReplyHandler' => 'applications/audit/mail/PhabricatorAuditReplyHandler.php', 'PhabricatorAuditStatusConstants' => 'applications/audit/constants/PhabricatorAuditStatusConstants.php', 'PhabricatorAuditSynchronizeManagementWorkflow' => 'applications/audit/management/PhabricatorAuditSynchronizeManagementWorkflow.php', 'PhabricatorAuditTransaction' => 'applications/audit/storage/PhabricatorAuditTransaction.php', 'PhabricatorAuditTransactionComment' => 'applications/audit/storage/PhabricatorAuditTransactionComment.php', 'PhabricatorAuditTransactionQuery' => 'applications/audit/query/PhabricatorAuditTransactionQuery.php', 'PhabricatorAuditTransactionView' => 'applications/audit/view/PhabricatorAuditTransactionView.php', 'PhabricatorAuditUpdateOwnersManagementWorkflow' => 'applications/audit/management/PhabricatorAuditUpdateOwnersManagementWorkflow.php', 'PhabricatorAuthAccountView' => 'applications/auth/view/PhabricatorAuthAccountView.php', 'PhabricatorAuthApplication' => 'applications/auth/application/PhabricatorAuthApplication.php', 'PhabricatorAuthAuthFactorPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthFactorPHIDType.php', 'PhabricatorAuthAuthProviderPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthProviderPHIDType.php', 'PhabricatorAuthChangePasswordAction' => 'applications/auth/action/PhabricatorAuthChangePasswordAction.php', 'PhabricatorAuthConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthConduitAPIMethod.php', 'PhabricatorAuthConduitTokenRevoker' => 'applications/auth/revoker/PhabricatorAuthConduitTokenRevoker.php', 'PhabricatorAuthConfirmLinkController' => 'applications/auth/controller/PhabricatorAuthConfirmLinkController.php', 'PhabricatorAuthController' => 'applications/auth/controller/PhabricatorAuthController.php', 'PhabricatorAuthDAO' => 'applications/auth/storage/PhabricatorAuthDAO.php', 'PhabricatorAuthDisableController' => 'applications/auth/controller/config/PhabricatorAuthDisableController.php', 'PhabricatorAuthDowngradeSessionController' => 'applications/auth/controller/PhabricatorAuthDowngradeSessionController.php', 'PhabricatorAuthEditController' => 'applications/auth/controller/config/PhabricatorAuthEditController.php', 'PhabricatorAuthFactor' => 'applications/auth/factor/PhabricatorAuthFactor.php', 'PhabricatorAuthFactorConfig' => 'applications/auth/storage/PhabricatorAuthFactorConfig.php', 'PhabricatorAuthFactorTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthFactorTestCase.php', 'PhabricatorAuthFinishController' => 'applications/auth/controller/PhabricatorAuthFinishController.php', 'PhabricatorAuthHMACKey' => 'applications/auth/storage/PhabricatorAuthHMACKey.php', 'PhabricatorAuthHighSecurityRequiredException' => 'applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php', 'PhabricatorAuthHighSecurityToken' => 'applications/auth/data/PhabricatorAuthHighSecurityToken.php', 'PhabricatorAuthInvite' => 'applications/auth/storage/PhabricatorAuthInvite.php', 'PhabricatorAuthInviteAccountException' => 'applications/auth/exception/PhabricatorAuthInviteAccountException.php', 'PhabricatorAuthInviteAction' => 'applications/auth/data/PhabricatorAuthInviteAction.php', 'PhabricatorAuthInviteActionTableView' => 'applications/auth/view/PhabricatorAuthInviteActionTableView.php', 'PhabricatorAuthInviteController' => 'applications/auth/controller/PhabricatorAuthInviteController.php', 'PhabricatorAuthInviteDialogException' => 'applications/auth/exception/PhabricatorAuthInviteDialogException.php', 'PhabricatorAuthInviteEngine' => 'applications/auth/engine/PhabricatorAuthInviteEngine.php', 'PhabricatorAuthInviteException' => 'applications/auth/exception/PhabricatorAuthInviteException.php', 'PhabricatorAuthInviteInvalidException' => 'applications/auth/exception/PhabricatorAuthInviteInvalidException.php', 'PhabricatorAuthInviteLoginException' => 'applications/auth/exception/PhabricatorAuthInviteLoginException.php', 'PhabricatorAuthInvitePHIDType' => 'applications/auth/phid/PhabricatorAuthInvitePHIDType.php', 'PhabricatorAuthInviteQuery' => 'applications/auth/query/PhabricatorAuthInviteQuery.php', 'PhabricatorAuthInviteRegisteredException' => 'applications/auth/exception/PhabricatorAuthInviteRegisteredException.php', 'PhabricatorAuthInviteSearchEngine' => 'applications/auth/query/PhabricatorAuthInviteSearchEngine.php', 'PhabricatorAuthInviteTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthInviteTestCase.php', 'PhabricatorAuthInviteVerifyException' => 'applications/auth/exception/PhabricatorAuthInviteVerifyException.php', 'PhabricatorAuthInviteWorker' => 'applications/auth/worker/PhabricatorAuthInviteWorker.php', 'PhabricatorAuthLinkController' => 'applications/auth/controller/PhabricatorAuthLinkController.php', 'PhabricatorAuthListController' => 'applications/auth/controller/config/PhabricatorAuthListController.php', 'PhabricatorAuthLoginController' => 'applications/auth/controller/PhabricatorAuthLoginController.php', 'PhabricatorAuthLoginHandler' => 'applications/auth/handler/PhabricatorAuthLoginHandler.php', 'PhabricatorAuthLogoutConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthLogoutConduitAPIMethod.php', 'PhabricatorAuthMainMenuBarExtension' => 'applications/auth/extension/PhabricatorAuthMainMenuBarExtension.php', 'PhabricatorAuthManagementCachePKCS8Workflow' => 'applications/auth/management/PhabricatorAuthManagementCachePKCS8Workflow.php', 'PhabricatorAuthManagementLDAPWorkflow' => 'applications/auth/management/PhabricatorAuthManagementLDAPWorkflow.php', 'PhabricatorAuthManagementListFactorsWorkflow' => 'applications/auth/management/PhabricatorAuthManagementListFactorsWorkflow.php', 'PhabricatorAuthManagementRecoverWorkflow' => 'applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php', 'PhabricatorAuthManagementRefreshWorkflow' => 'applications/auth/management/PhabricatorAuthManagementRefreshWorkflow.php', 'PhabricatorAuthManagementRevokeWorkflow' => 'applications/auth/management/PhabricatorAuthManagementRevokeWorkflow.php', 'PhabricatorAuthManagementStripWorkflow' => 'applications/auth/management/PhabricatorAuthManagementStripWorkflow.php', 'PhabricatorAuthManagementTrustOAuthClientWorkflow' => 'applications/auth/management/PhabricatorAuthManagementTrustOAuthClientWorkflow.php', 'PhabricatorAuthManagementUnlimitWorkflow' => 'applications/auth/management/PhabricatorAuthManagementUnlimitWorkflow.php', 'PhabricatorAuthManagementUntrustOAuthClientWorkflow' => 'applications/auth/management/PhabricatorAuthManagementUntrustOAuthClientWorkflow.php', 'PhabricatorAuthManagementVerifyWorkflow' => 'applications/auth/management/PhabricatorAuthManagementVerifyWorkflow.php', 'PhabricatorAuthManagementWorkflow' => 'applications/auth/management/PhabricatorAuthManagementWorkflow.php', 'PhabricatorAuthNeedsApprovalController' => 'applications/auth/controller/PhabricatorAuthNeedsApprovalController.php', 'PhabricatorAuthNeedsMultiFactorController' => 'applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php', 'PhabricatorAuthNewController' => 'applications/auth/controller/config/PhabricatorAuthNewController.php', 'PhabricatorAuthOldOAuthRedirectController' => 'applications/auth/controller/PhabricatorAuthOldOAuthRedirectController.php', 'PhabricatorAuthOneTimeLoginController' => 'applications/auth/controller/PhabricatorAuthOneTimeLoginController.php', 'PhabricatorAuthOneTimeLoginTemporaryTokenType' => 'applications/auth/tokentype/PhabricatorAuthOneTimeLoginTemporaryTokenType.php', 'PhabricatorAuthPassword' => 'applications/auth/storage/PhabricatorAuthPassword.php', 'PhabricatorAuthPasswordEditor' => 'applications/auth/editor/PhabricatorAuthPasswordEditor.php', 'PhabricatorAuthPasswordEngine' => 'applications/auth/engine/PhabricatorAuthPasswordEngine.php', 'PhabricatorAuthPasswordException' => 'applications/auth/password/PhabricatorAuthPasswordException.php', 'PhabricatorAuthPasswordHashInterface' => 'applications/auth/password/PhabricatorAuthPasswordHashInterface.php', 'PhabricatorAuthPasswordPHIDType' => 'applications/auth/phid/PhabricatorAuthPasswordPHIDType.php', 'PhabricatorAuthPasswordQuery' => 'applications/auth/query/PhabricatorAuthPasswordQuery.php', 'PhabricatorAuthPasswordResetTemporaryTokenType' => 'applications/auth/tokentype/PhabricatorAuthPasswordResetTemporaryTokenType.php', 'PhabricatorAuthPasswordRevokeTransaction' => 'applications/auth/xaction/PhabricatorAuthPasswordRevokeTransaction.php', 'PhabricatorAuthPasswordRevoker' => 'applications/auth/revoker/PhabricatorAuthPasswordRevoker.php', 'PhabricatorAuthPasswordTestCase' => 'applications/auth/__tests__/PhabricatorAuthPasswordTestCase.php', 'PhabricatorAuthPasswordTransaction' => 'applications/auth/storage/PhabricatorAuthPasswordTransaction.php', 'PhabricatorAuthPasswordTransactionQuery' => 'applications/auth/query/PhabricatorAuthPasswordTransactionQuery.php', 'PhabricatorAuthPasswordTransactionType' => 'applications/auth/xaction/PhabricatorAuthPasswordTransactionType.php', 'PhabricatorAuthPasswordUpgradeTransaction' => 'applications/auth/xaction/PhabricatorAuthPasswordUpgradeTransaction.php', 'PhabricatorAuthProvider' => 'applications/auth/provider/PhabricatorAuthProvider.php', 'PhabricatorAuthProviderConfig' => 'applications/auth/storage/PhabricatorAuthProviderConfig.php', 'PhabricatorAuthProviderConfigController' => 'applications/auth/controller/config/PhabricatorAuthProviderConfigController.php', 'PhabricatorAuthProviderConfigEditor' => 'applications/auth/editor/PhabricatorAuthProviderConfigEditor.php', 'PhabricatorAuthProviderConfigQuery' => 'applications/auth/query/PhabricatorAuthProviderConfigQuery.php', 'PhabricatorAuthProviderConfigTransaction' => 'applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php', 'PhabricatorAuthProviderConfigTransactionQuery' => 'applications/auth/query/PhabricatorAuthProviderConfigTransactionQuery.php', 'PhabricatorAuthProvidersGuidanceContext' => 'applications/auth/guidance/PhabricatorAuthProvidersGuidanceContext.php', 'PhabricatorAuthProvidersGuidanceEngineExtension' => 'applications/auth/guidance/PhabricatorAuthProvidersGuidanceEngineExtension.php', 'PhabricatorAuthQueryPublicKeysConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthQueryPublicKeysConduitAPIMethod.php', 'PhabricatorAuthRegisterController' => 'applications/auth/controller/PhabricatorAuthRegisterController.php', 'PhabricatorAuthRevokeTokenController' => 'applications/auth/controller/PhabricatorAuthRevokeTokenController.php', 'PhabricatorAuthRevoker' => 'applications/auth/revoker/PhabricatorAuthRevoker.php', 'PhabricatorAuthSSHKey' => 'applications/auth/storage/PhabricatorAuthSSHKey.php', 'PhabricatorAuthSSHKeyController' => 'applications/auth/controller/PhabricatorAuthSSHKeyController.php', 'PhabricatorAuthSSHKeyEditController' => 'applications/auth/controller/PhabricatorAuthSSHKeyEditController.php', 'PhabricatorAuthSSHKeyEditor' => 'applications/auth/editor/PhabricatorAuthSSHKeyEditor.php', 'PhabricatorAuthSSHKeyGenerateController' => 'applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php', 'PhabricatorAuthSSHKeyListController' => 'applications/auth/controller/PhabricatorAuthSSHKeyListController.php', 'PhabricatorAuthSSHKeyPHIDType' => 'applications/auth/phid/PhabricatorAuthSSHKeyPHIDType.php', 'PhabricatorAuthSSHKeyQuery' => 'applications/auth/query/PhabricatorAuthSSHKeyQuery.php', 'PhabricatorAuthSSHKeyReplyHandler' => 'applications/auth/mail/PhabricatorAuthSSHKeyReplyHandler.php', 'PhabricatorAuthSSHKeyRevokeController' => 'applications/auth/controller/PhabricatorAuthSSHKeyRevokeController.php', 'PhabricatorAuthSSHKeySearchEngine' => 'applications/auth/query/PhabricatorAuthSSHKeySearchEngine.php', 'PhabricatorAuthSSHKeyTableView' => 'applications/auth/view/PhabricatorAuthSSHKeyTableView.php', 'PhabricatorAuthSSHKeyTestCase' => 'applications/auth/__tests__/PhabricatorAuthSSHKeyTestCase.php', 'PhabricatorAuthSSHKeyTransaction' => 'applications/auth/storage/PhabricatorAuthSSHKeyTransaction.php', 'PhabricatorAuthSSHKeyTransactionQuery' => 'applications/auth/query/PhabricatorAuthSSHKeyTransactionQuery.php', 'PhabricatorAuthSSHKeyViewController' => 'applications/auth/controller/PhabricatorAuthSSHKeyViewController.php', 'PhabricatorAuthSSHPublicKey' => 'applications/auth/sshkey/PhabricatorAuthSSHPublicKey.php', 'PhabricatorAuthSSHRevoker' => 'applications/auth/revoker/PhabricatorAuthSSHRevoker.php', 'PhabricatorAuthSession' => 'applications/auth/storage/PhabricatorAuthSession.php', 'PhabricatorAuthSessionEngine' => 'applications/auth/engine/PhabricatorAuthSessionEngine.php', 'PhabricatorAuthSessionEngineExtension' => 'applications/auth/engine/PhabricatorAuthSessionEngineExtension.php', 'PhabricatorAuthSessionEngineExtensionModule' => 'applications/auth/engine/PhabricatorAuthSessionEngineExtensionModule.php', 'PhabricatorAuthSessionGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthSessionGarbageCollector.php', 'PhabricatorAuthSessionInfo' => 'applications/auth/data/PhabricatorAuthSessionInfo.php', 'PhabricatorAuthSessionQuery' => 'applications/auth/query/PhabricatorAuthSessionQuery.php', 'PhabricatorAuthSessionRevoker' => 'applications/auth/revoker/PhabricatorAuthSessionRevoker.php', 'PhabricatorAuthSetPasswordController' => 'applications/auth/controller/PhabricatorAuthSetPasswordController.php', 'PhabricatorAuthSetupCheck' => 'applications/config/check/PhabricatorAuthSetupCheck.php', 'PhabricatorAuthStartController' => 'applications/auth/controller/PhabricatorAuthStartController.php', 'PhabricatorAuthTOTPKeyTemporaryTokenType' => 'applications/auth/factor/PhabricatorAuthTOTPKeyTemporaryTokenType.php', 'PhabricatorAuthTemporaryToken' => 'applications/auth/storage/PhabricatorAuthTemporaryToken.php', 'PhabricatorAuthTemporaryTokenGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthTemporaryTokenGarbageCollector.php', 'PhabricatorAuthTemporaryTokenQuery' => 'applications/auth/query/PhabricatorAuthTemporaryTokenQuery.php', 'PhabricatorAuthTemporaryTokenRevoker' => 'applications/auth/revoker/PhabricatorAuthTemporaryTokenRevoker.php', 'PhabricatorAuthTemporaryTokenType' => 'applications/auth/tokentype/PhabricatorAuthTemporaryTokenType.php', 'PhabricatorAuthTemporaryTokenTypeModule' => 'applications/auth/tokentype/PhabricatorAuthTemporaryTokenTypeModule.php', 'PhabricatorAuthTerminateSessionController' => 'applications/auth/controller/PhabricatorAuthTerminateSessionController.php', 'PhabricatorAuthTryFactorAction' => 'applications/auth/action/PhabricatorAuthTryFactorAction.php', 'PhabricatorAuthUnlinkController' => 'applications/auth/controller/PhabricatorAuthUnlinkController.php', 'PhabricatorAuthValidateController' => 'applications/auth/controller/PhabricatorAuthValidateController.php', 'PhabricatorAuthenticationConfigOptions' => 'applications/config/option/PhabricatorAuthenticationConfigOptions.php', 'PhabricatorAutoEventListener' => 'infrastructure/events/PhabricatorAutoEventListener.php', 'PhabricatorBadgesApplication' => 'applications/badges/application/PhabricatorBadgesApplication.php', 'PhabricatorBadgesArchiveController' => 'applications/badges/controller/PhabricatorBadgesArchiveController.php', 'PhabricatorBadgesAward' => 'applications/badges/storage/PhabricatorBadgesAward.php', 'PhabricatorBadgesAwardController' => 'applications/badges/controller/PhabricatorBadgesAwardController.php', 'PhabricatorBadgesAwardQuery' => 'applications/badges/query/PhabricatorBadgesAwardQuery.php', 'PhabricatorBadgesAwardTestDataGenerator' => 'applications/badges/lipsum/PhabricatorBadgesAwardTestDataGenerator.php', 'PhabricatorBadgesBadge' => 'applications/badges/storage/PhabricatorBadgesBadge.php', 'PhabricatorBadgesBadgeAwardTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeAwardTransaction.php', 'PhabricatorBadgesBadgeDescriptionTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeDescriptionTransaction.php', 'PhabricatorBadgesBadgeFlavorTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeFlavorTransaction.php', 'PhabricatorBadgesBadgeIconTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeIconTransaction.php', 'PhabricatorBadgesBadgeNameNgrams' => 'applications/badges/storage/PhabricatorBadgesBadgeNameNgrams.php', 'PhabricatorBadgesBadgeNameTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeNameTransaction.php', 'PhabricatorBadgesBadgeQualityTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeQualityTransaction.php', 'PhabricatorBadgesBadgeRevokeTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeRevokeTransaction.php', 'PhabricatorBadgesBadgeStatusTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeStatusTransaction.php', 'PhabricatorBadgesBadgeTestDataGenerator' => 'applications/badges/lipsum/PhabricatorBadgesBadgeTestDataGenerator.php', 'PhabricatorBadgesBadgeTransactionType' => 'applications/badges/xaction/PhabricatorBadgesBadgeTransactionType.php', 'PhabricatorBadgesCommentController' => 'applications/badges/controller/PhabricatorBadgesCommentController.php', 'PhabricatorBadgesController' => 'applications/badges/controller/PhabricatorBadgesController.php', 'PhabricatorBadgesCreateCapability' => 'applications/badges/capability/PhabricatorBadgesCreateCapability.php', 'PhabricatorBadgesDAO' => 'applications/badges/storage/PhabricatorBadgesDAO.php', 'PhabricatorBadgesDatasource' => 'applications/badges/typeahead/PhabricatorBadgesDatasource.php', 'PhabricatorBadgesDefaultEditCapability' => 'applications/badges/capability/PhabricatorBadgesDefaultEditCapability.php', 'PhabricatorBadgesEditConduitAPIMethod' => 'applications/badges/conduit/PhabricatorBadgesEditConduitAPIMethod.php', 'PhabricatorBadgesEditController' => 'applications/badges/controller/PhabricatorBadgesEditController.php', 'PhabricatorBadgesEditEngine' => 'applications/badges/editor/PhabricatorBadgesEditEngine.php', 'PhabricatorBadgesEditRecipientsController' => 'applications/badges/controller/PhabricatorBadgesEditRecipientsController.php', 'PhabricatorBadgesEditor' => 'applications/badges/editor/PhabricatorBadgesEditor.php', 'PhabricatorBadgesIconSet' => 'applications/badges/icon/PhabricatorBadgesIconSet.php', 'PhabricatorBadgesListController' => 'applications/badges/controller/PhabricatorBadgesListController.php', 'PhabricatorBadgesLootContextFreeGrammar' => 'applications/badges/lipsum/PhabricatorBadgesLootContextFreeGrammar.php', 'PhabricatorBadgesMailReceiver' => 'applications/badges/mail/PhabricatorBadgesMailReceiver.php', 'PhabricatorBadgesPHIDType' => 'applications/badges/phid/PhabricatorBadgesPHIDType.php', 'PhabricatorBadgesProfileController' => 'applications/badges/controller/PhabricatorBadgesProfileController.php', 'PhabricatorBadgesQuality' => 'applications/badges/constants/PhabricatorBadgesQuality.php', 'PhabricatorBadgesQuery' => 'applications/badges/query/PhabricatorBadgesQuery.php', 'PhabricatorBadgesRecipientsController' => 'applications/badges/controller/PhabricatorBadgesRecipientsController.php', 'PhabricatorBadgesRecipientsListView' => 'applications/badges/view/PhabricatorBadgesRecipientsListView.php', 'PhabricatorBadgesRemoveRecipientsController' => 'applications/badges/controller/PhabricatorBadgesRemoveRecipientsController.php', 'PhabricatorBadgesReplyHandler' => 'applications/badges/mail/PhabricatorBadgesReplyHandler.php', 'PhabricatorBadgesSchemaSpec' => 'applications/badges/storage/PhabricatorBadgesSchemaSpec.php', 'PhabricatorBadgesSearchConduitAPIMethod' => 'applications/badges/conduit/PhabricatorBadgesSearchConduitAPIMethod.php', 'PhabricatorBadgesSearchEngine' => 'applications/badges/query/PhabricatorBadgesSearchEngine.php', 'PhabricatorBadgesTransaction' => 'applications/badges/storage/PhabricatorBadgesTransaction.php', 'PhabricatorBadgesTransactionComment' => 'applications/badges/storage/PhabricatorBadgesTransactionComment.php', 'PhabricatorBadgesTransactionQuery' => 'applications/badges/query/PhabricatorBadgesTransactionQuery.php', 'PhabricatorBadgesViewController' => 'applications/badges/controller/PhabricatorBadgesViewController.php', 'PhabricatorBarePageView' => 'view/page/PhabricatorBarePageView.php', 'PhabricatorBaseURISetupCheck' => 'applications/config/check/PhabricatorBaseURISetupCheck.php', 'PhabricatorBcryptPasswordHasher' => 'infrastructure/util/password/PhabricatorBcryptPasswordHasher.php', 'PhabricatorBinariesSetupCheck' => 'applications/config/check/PhabricatorBinariesSetupCheck.php', 'PhabricatorBitbucketAuthProvider' => 'applications/auth/provider/PhabricatorBitbucketAuthProvider.php', 'PhabricatorBoardColumnsSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorBoardColumnsSearchEngineAttachment.php', 'PhabricatorBoardLayoutEngine' => 'applications/project/engine/PhabricatorBoardLayoutEngine.php', 'PhabricatorBoardRenderingEngine' => 'applications/project/engine/PhabricatorBoardRenderingEngine.php', 'PhabricatorBoardResponseEngine' => 'applications/project/engine/PhabricatorBoardResponseEngine.php', 'PhabricatorBoolConfigType' => 'applications/config/type/PhabricatorBoolConfigType.php', 'PhabricatorBoolEditField' => 'applications/transactions/editfield/PhabricatorBoolEditField.php', 'PhabricatorBoolMailStamp' => 'applications/metamta/stamp/PhabricatorBoolMailStamp.php', 'PhabricatorBritishEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorBritishEnglishTranslation.php', 'PhabricatorBuiltinDraftEngine' => 'applications/transactions/draft/PhabricatorBuiltinDraftEngine.php', 'PhabricatorBuiltinFileCachePurger' => 'applications/cache/purger/PhabricatorBuiltinFileCachePurger.php', 'PhabricatorBuiltinPatchList' => 'infrastructure/storage/patch/PhabricatorBuiltinPatchList.php', 'PhabricatorBulkContentSource' => 'infrastructure/daemon/contentsource/PhabricatorBulkContentSource.php', 'PhabricatorBulkEditGroup' => 'applications/transactions/bulk/PhabricatorBulkEditGroup.php', 'PhabricatorBulkEngine' => 'applications/transactions/bulk/PhabricatorBulkEngine.php', 'PhabricatorBulkManagementExportWorkflow' => 'applications/transactions/bulk/management/PhabricatorBulkManagementExportWorkflow.php', 'PhabricatorBulkManagementMakeSilentWorkflow' => 'applications/transactions/bulk/management/PhabricatorBulkManagementMakeSilentWorkflow.php', 'PhabricatorBulkManagementWorkflow' => 'applications/transactions/bulk/management/PhabricatorBulkManagementWorkflow.php', 'PhabricatorCSVExportFormat' => 'infrastructure/export/format/PhabricatorCSVExportFormat.php', 'PhabricatorCacheDAO' => 'applications/cache/storage/PhabricatorCacheDAO.php', 'PhabricatorCacheEngine' => 'applications/system/engine/PhabricatorCacheEngine.php', 'PhabricatorCacheEngineExtension' => 'applications/system/engine/PhabricatorCacheEngineExtension.php', 'PhabricatorCacheGeneralGarbageCollector' => 'applications/cache/garbagecollector/PhabricatorCacheGeneralGarbageCollector.php', 'PhabricatorCacheManagementPurgeWorkflow' => 'applications/cache/management/PhabricatorCacheManagementPurgeWorkflow.php', 'PhabricatorCacheManagementWorkflow' => 'applications/cache/management/PhabricatorCacheManagementWorkflow.php', 'PhabricatorCacheMarkupGarbageCollector' => 'applications/cache/garbagecollector/PhabricatorCacheMarkupGarbageCollector.php', 'PhabricatorCachePurger' => 'applications/cache/purger/PhabricatorCachePurger.php', 'PhabricatorCacheSchemaSpec' => 'applications/cache/storage/PhabricatorCacheSchemaSpec.php', 'PhabricatorCacheSetupCheck' => 'applications/config/check/PhabricatorCacheSetupCheck.php', 'PhabricatorCacheSpec' => 'applications/cache/spec/PhabricatorCacheSpec.php', 'PhabricatorCacheTTLGarbageCollector' => 'applications/cache/garbagecollector/PhabricatorCacheTTLGarbageCollector.php', 'PhabricatorCachedClassMapQuery' => 'applications/cache/PhabricatorCachedClassMapQuery.php', 'PhabricatorCaches' => 'applications/cache/PhabricatorCaches.php', 'PhabricatorCachesTestCase' => 'applications/cache/__tests__/PhabricatorCachesTestCase.php', 'PhabricatorCalendarApplication' => 'applications/calendar/application/PhabricatorCalendarApplication.php', 'PhabricatorCalendarController' => 'applications/calendar/controller/PhabricatorCalendarController.php', 'PhabricatorCalendarDAO' => 'applications/calendar/storage/PhabricatorCalendarDAO.php', 'PhabricatorCalendarEvent' => 'applications/calendar/storage/PhabricatorCalendarEvent.php', 'PhabricatorCalendarEventAcceptTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventAcceptTransaction.php', 'PhabricatorCalendarEventAllDayTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventAllDayTransaction.php', 'PhabricatorCalendarEventAvailabilityController' => 'applications/calendar/controller/PhabricatorCalendarEventAvailabilityController.php', 'PhabricatorCalendarEventCancelController' => 'applications/calendar/controller/PhabricatorCalendarEventCancelController.php', 'PhabricatorCalendarEventCancelTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventCancelTransaction.php', 'PhabricatorCalendarEventDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventDateTransaction.php', 'PhabricatorCalendarEventDeclineTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventDeclineTransaction.php', 'PhabricatorCalendarEventDefaultEditCapability' => 'applications/calendar/capability/PhabricatorCalendarEventDefaultEditCapability.php', 'PhabricatorCalendarEventDefaultViewCapability' => 'applications/calendar/capability/PhabricatorCalendarEventDefaultViewCapability.php', 'PhabricatorCalendarEventDescriptionTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventDescriptionTransaction.php', 'PhabricatorCalendarEventDragController' => 'applications/calendar/controller/PhabricatorCalendarEventDragController.php', 'PhabricatorCalendarEventEditConduitAPIMethod' => 'applications/calendar/conduit/PhabricatorCalendarEventEditConduitAPIMethod.php', 'PhabricatorCalendarEventEditController' => 'applications/calendar/controller/PhabricatorCalendarEventEditController.php', 'PhabricatorCalendarEventEditEngine' => 'applications/calendar/editor/PhabricatorCalendarEventEditEngine.php', 'PhabricatorCalendarEventEditor' => 'applications/calendar/editor/PhabricatorCalendarEventEditor.php', 'PhabricatorCalendarEventEmailCommand' => 'applications/calendar/command/PhabricatorCalendarEventEmailCommand.php', 'PhabricatorCalendarEventEndDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventEndDateTransaction.php', 'PhabricatorCalendarEventExportController' => 'applications/calendar/controller/PhabricatorCalendarEventExportController.php', 'PhabricatorCalendarEventFerretEngine' => 'applications/calendar/search/PhabricatorCalendarEventFerretEngine.php', 'PhabricatorCalendarEventForkTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventForkTransaction.php', 'PhabricatorCalendarEventFrequencyTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventFrequencyTransaction.php', 'PhabricatorCalendarEventFulltextEngine' => 'applications/calendar/search/PhabricatorCalendarEventFulltextEngine.php', 'PhabricatorCalendarEventHeraldAdapter' => 'applications/calendar/herald/PhabricatorCalendarEventHeraldAdapter.php', 'PhabricatorCalendarEventHeraldField' => 'applications/calendar/herald/PhabricatorCalendarEventHeraldField.php', 'PhabricatorCalendarEventHeraldFieldGroup' => 'applications/calendar/herald/PhabricatorCalendarEventHeraldFieldGroup.php', 'PhabricatorCalendarEventHostPolicyRule' => 'applications/calendar/policyrule/PhabricatorCalendarEventHostPolicyRule.php', 'PhabricatorCalendarEventHostTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventHostTransaction.php', 'PhabricatorCalendarEventIconTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventIconTransaction.php', 'PhabricatorCalendarEventInviteTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventInviteTransaction.php', 'PhabricatorCalendarEventInvitee' => 'applications/calendar/storage/PhabricatorCalendarEventInvitee.php', 'PhabricatorCalendarEventInviteeQuery' => 'applications/calendar/query/PhabricatorCalendarEventInviteeQuery.php', 'PhabricatorCalendarEventInviteesPolicyRule' => 'applications/calendar/policyrule/PhabricatorCalendarEventInviteesPolicyRule.php', 'PhabricatorCalendarEventJoinController' => 'applications/calendar/controller/PhabricatorCalendarEventJoinController.php', 'PhabricatorCalendarEventListController' => 'applications/calendar/controller/PhabricatorCalendarEventListController.php', 'PhabricatorCalendarEventMailReceiver' => 'applications/calendar/mail/PhabricatorCalendarEventMailReceiver.php', 'PhabricatorCalendarEventNameHeraldField' => 'applications/calendar/herald/PhabricatorCalendarEventNameHeraldField.php', 'PhabricatorCalendarEventNameTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventNameTransaction.php', 'PhabricatorCalendarEventNotificationView' => 'applications/calendar/notifications/PhabricatorCalendarEventNotificationView.php', 'PhabricatorCalendarEventPHIDType' => 'applications/calendar/phid/PhabricatorCalendarEventPHIDType.php', 'PhabricatorCalendarEventPolicyCodex' => 'applications/calendar/codex/PhabricatorCalendarEventPolicyCodex.php', 'PhabricatorCalendarEventQuery' => 'applications/calendar/query/PhabricatorCalendarEventQuery.php', 'PhabricatorCalendarEventRSVPEmailCommand' => 'applications/calendar/command/PhabricatorCalendarEventRSVPEmailCommand.php', 'PhabricatorCalendarEventRecurringTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventRecurringTransaction.php', 'PhabricatorCalendarEventReplyTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventReplyTransaction.php', 'PhabricatorCalendarEventSearchConduitAPIMethod' => 'applications/calendar/conduit/PhabricatorCalendarEventSearchConduitAPIMethod.php', 'PhabricatorCalendarEventSearchEngine' => 'applications/calendar/query/PhabricatorCalendarEventSearchEngine.php', 'PhabricatorCalendarEventStartDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventStartDateTransaction.php', 'PhabricatorCalendarEventTransaction' => 'applications/calendar/storage/PhabricatorCalendarEventTransaction.php', 'PhabricatorCalendarEventTransactionComment' => 'applications/calendar/storage/PhabricatorCalendarEventTransactionComment.php', 'PhabricatorCalendarEventTransactionQuery' => 'applications/calendar/query/PhabricatorCalendarEventTransactionQuery.php', 'PhabricatorCalendarEventTransactionType' => 'applications/calendar/xaction/PhabricatorCalendarEventTransactionType.php', 'PhabricatorCalendarEventUntilDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventUntilDateTransaction.php', 'PhabricatorCalendarEventViewController' => 'applications/calendar/controller/PhabricatorCalendarEventViewController.php', 'PhabricatorCalendarExport' => 'applications/calendar/storage/PhabricatorCalendarExport.php', 'PhabricatorCalendarExportDisableController' => 'applications/calendar/controller/PhabricatorCalendarExportDisableController.php', 'PhabricatorCalendarExportDisableTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportDisableTransaction.php', 'PhabricatorCalendarExportEditController' => 'applications/calendar/controller/PhabricatorCalendarExportEditController.php', 'PhabricatorCalendarExportEditEngine' => 'applications/calendar/editor/PhabricatorCalendarExportEditEngine.php', 'PhabricatorCalendarExportEditor' => 'applications/calendar/editor/PhabricatorCalendarExportEditor.php', 'PhabricatorCalendarExportICSController' => 'applications/calendar/controller/PhabricatorCalendarExportICSController.php', 'PhabricatorCalendarExportListController' => 'applications/calendar/controller/PhabricatorCalendarExportListController.php', 'PhabricatorCalendarExportModeTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportModeTransaction.php', 'PhabricatorCalendarExportNameTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportNameTransaction.php', 'PhabricatorCalendarExportPHIDType' => 'applications/calendar/phid/PhabricatorCalendarExportPHIDType.php', 'PhabricatorCalendarExportQuery' => 'applications/calendar/query/PhabricatorCalendarExportQuery.php', 'PhabricatorCalendarExportQueryKeyTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportQueryKeyTransaction.php', 'PhabricatorCalendarExportSearchEngine' => 'applications/calendar/query/PhabricatorCalendarExportSearchEngine.php', 'PhabricatorCalendarExportTransaction' => 'applications/calendar/storage/PhabricatorCalendarExportTransaction.php', 'PhabricatorCalendarExportTransactionQuery' => 'applications/calendar/query/PhabricatorCalendarExportTransactionQuery.php', 'PhabricatorCalendarExportTransactionType' => 'applications/calendar/xaction/PhabricatorCalendarExportTransactionType.php', 'PhabricatorCalendarExportViewController' => 'applications/calendar/controller/PhabricatorCalendarExportViewController.php', 'PhabricatorCalendarExternalInvitee' => 'applications/calendar/storage/PhabricatorCalendarExternalInvitee.php', 'PhabricatorCalendarExternalInviteePHIDType' => 'applications/calendar/phid/PhabricatorCalendarExternalInviteePHIDType.php', 'PhabricatorCalendarExternalInviteeQuery' => 'applications/calendar/query/PhabricatorCalendarExternalInviteeQuery.php', 'PhabricatorCalendarICSFileImportEngine' => 'applications/calendar/import/PhabricatorCalendarICSFileImportEngine.php', 'PhabricatorCalendarICSImportEngine' => 'applications/calendar/import/PhabricatorCalendarICSImportEngine.php', 'PhabricatorCalendarICSURIImportEngine' => 'applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php', 'PhabricatorCalendarICSWriter' => 'applications/calendar/util/PhabricatorCalendarICSWriter.php', 'PhabricatorCalendarIconSet' => 'applications/calendar/icon/PhabricatorCalendarIconSet.php', 'PhabricatorCalendarImport' => 'applications/calendar/storage/PhabricatorCalendarImport.php', 'PhabricatorCalendarImportDefaultLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportDefaultLogType.php', 'PhabricatorCalendarImportDeleteController' => 'applications/calendar/controller/PhabricatorCalendarImportDeleteController.php', 'PhabricatorCalendarImportDeleteLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportDeleteLogType.php', 'PhabricatorCalendarImportDeleteTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportDeleteTransaction.php', 'PhabricatorCalendarImportDisableController' => 'applications/calendar/controller/PhabricatorCalendarImportDisableController.php', 'PhabricatorCalendarImportDisableTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportDisableTransaction.php', 'PhabricatorCalendarImportDropController' => 'applications/calendar/controller/PhabricatorCalendarImportDropController.php', 'PhabricatorCalendarImportDuplicateLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportDuplicateLogType.php', 'PhabricatorCalendarImportEditController' => 'applications/calendar/controller/PhabricatorCalendarImportEditController.php', 'PhabricatorCalendarImportEditEngine' => 'applications/calendar/editor/PhabricatorCalendarImportEditEngine.php', 'PhabricatorCalendarImportEditor' => 'applications/calendar/editor/PhabricatorCalendarImportEditor.php', 'PhabricatorCalendarImportEmptyLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportEmptyLogType.php', 'PhabricatorCalendarImportEngine' => 'applications/calendar/import/PhabricatorCalendarImportEngine.php', 'PhabricatorCalendarImportEpochLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportEpochLogType.php', 'PhabricatorCalendarImportFetchLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportFetchLogType.php', 'PhabricatorCalendarImportFrequencyLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportFrequencyLogType.php', 'PhabricatorCalendarImportFrequencyTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportFrequencyTransaction.php', 'PhabricatorCalendarImportICSFileTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportICSFileTransaction.php', 'PhabricatorCalendarImportICSLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportICSLogType.php', 'PhabricatorCalendarImportICSURITransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportICSURITransaction.php', 'PhabricatorCalendarImportICSWarningLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportICSWarningLogType.php', 'PhabricatorCalendarImportIgnoredNodeLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportIgnoredNodeLogType.php', 'PhabricatorCalendarImportListController' => 'applications/calendar/controller/PhabricatorCalendarImportListController.php', 'PhabricatorCalendarImportLog' => 'applications/calendar/storage/PhabricatorCalendarImportLog.php', 'PhabricatorCalendarImportLogListController' => 'applications/calendar/controller/PhabricatorCalendarImportLogListController.php', 'PhabricatorCalendarImportLogQuery' => 'applications/calendar/query/PhabricatorCalendarImportLogQuery.php', 'PhabricatorCalendarImportLogSearchEngine' => 'applications/calendar/query/PhabricatorCalendarImportLogSearchEngine.php', 'PhabricatorCalendarImportLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportLogType.php', 'PhabricatorCalendarImportLogView' => 'applications/calendar/view/PhabricatorCalendarImportLogView.php', 'PhabricatorCalendarImportNameTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportNameTransaction.php', 'PhabricatorCalendarImportOriginalLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportOriginalLogType.php', 'PhabricatorCalendarImportOrphanLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportOrphanLogType.php', 'PhabricatorCalendarImportPHIDType' => 'applications/calendar/phid/PhabricatorCalendarImportPHIDType.php', 'PhabricatorCalendarImportQuery' => 'applications/calendar/query/PhabricatorCalendarImportQuery.php', 'PhabricatorCalendarImportQueueLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportQueueLogType.php', 'PhabricatorCalendarImportReloadController' => 'applications/calendar/controller/PhabricatorCalendarImportReloadController.php', 'PhabricatorCalendarImportReloadTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportReloadTransaction.php', 'PhabricatorCalendarImportReloadWorker' => 'applications/calendar/worker/PhabricatorCalendarImportReloadWorker.php', 'PhabricatorCalendarImportSearchEngine' => 'applications/calendar/query/PhabricatorCalendarImportSearchEngine.php', 'PhabricatorCalendarImportTransaction' => 'applications/calendar/storage/PhabricatorCalendarImportTransaction.php', 'PhabricatorCalendarImportTransactionQuery' => 'applications/calendar/query/PhabricatorCalendarImportTransactionQuery.php', 'PhabricatorCalendarImportTransactionType' => 'applications/calendar/xaction/PhabricatorCalendarImportTransactionType.php', 'PhabricatorCalendarImportTriggerLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportTriggerLogType.php', 'PhabricatorCalendarImportUpdateLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportUpdateLogType.php', 'PhabricatorCalendarImportViewController' => 'applications/calendar/controller/PhabricatorCalendarImportViewController.php', 'PhabricatorCalendarInviteeDatasource' => 'applications/calendar/typeahead/PhabricatorCalendarInviteeDatasource.php', 'PhabricatorCalendarInviteeUserDatasource' => 'applications/calendar/typeahead/PhabricatorCalendarInviteeUserDatasource.php', 'PhabricatorCalendarInviteeViewerFunctionDatasource' => 'applications/calendar/typeahead/PhabricatorCalendarInviteeViewerFunctionDatasource.php', 'PhabricatorCalendarManagementNotifyWorkflow' => 'applications/calendar/management/PhabricatorCalendarManagementNotifyWorkflow.php', 'PhabricatorCalendarManagementReloadWorkflow' => 'applications/calendar/management/PhabricatorCalendarManagementReloadWorkflow.php', 'PhabricatorCalendarManagementWorkflow' => 'applications/calendar/management/PhabricatorCalendarManagementWorkflow.php', 'PhabricatorCalendarNotification' => 'applications/calendar/storage/PhabricatorCalendarNotification.php', 'PhabricatorCalendarNotificationEngine' => 'applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php', 'PhabricatorCalendarRemarkupRule' => 'applications/calendar/remarkup/PhabricatorCalendarRemarkupRule.php', 'PhabricatorCalendarReplyHandler' => 'applications/calendar/mail/PhabricatorCalendarReplyHandler.php', 'PhabricatorCalendarSchemaSpec' => 'applications/calendar/storage/PhabricatorCalendarSchemaSpec.php', 'PhabricatorCelerityApplication' => 'applications/celerity/application/PhabricatorCelerityApplication.php', 'PhabricatorCelerityTestCase' => '__tests__/PhabricatorCelerityTestCase.php', 'PhabricatorChangeParserTestCase' => 'applications/repository/worker/__tests__/PhabricatorChangeParserTestCase.php', 'PhabricatorChangesetCachePurger' => 'applications/cache/purger/PhabricatorChangesetCachePurger.php', 'PhabricatorChangesetResponse' => 'infrastructure/diff/PhabricatorChangesetResponse.php', 'PhabricatorChatLogApplication' => 'applications/chatlog/application/PhabricatorChatLogApplication.php', 'PhabricatorChatLogChannel' => 'applications/chatlog/storage/PhabricatorChatLogChannel.php', 'PhabricatorChatLogChannelListController' => 'applications/chatlog/controller/PhabricatorChatLogChannelListController.php', 'PhabricatorChatLogChannelLogController' => 'applications/chatlog/controller/PhabricatorChatLogChannelLogController.php', 'PhabricatorChatLogChannelQuery' => 'applications/chatlog/query/PhabricatorChatLogChannelQuery.php', 'PhabricatorChatLogController' => 'applications/chatlog/controller/PhabricatorChatLogController.php', 'PhabricatorChatLogDAO' => 'applications/chatlog/storage/PhabricatorChatLogDAO.php', 'PhabricatorChatLogEvent' => 'applications/chatlog/storage/PhabricatorChatLogEvent.php', 'PhabricatorChatLogQuery' => 'applications/chatlog/query/PhabricatorChatLogQuery.php', 'PhabricatorCheckboxesEditField' => 'applications/transactions/editfield/PhabricatorCheckboxesEditField.php', 'PhabricatorChunkedFileStorageEngine' => 'applications/files/engine/PhabricatorChunkedFileStorageEngine.php', 'PhabricatorClassConfigType' => 'applications/config/type/PhabricatorClassConfigType.php', 'PhabricatorClusterConfigOptions' => 'applications/config/option/PhabricatorClusterConfigOptions.php', 'PhabricatorClusterDatabasesConfigType' => 'infrastructure/cluster/config/PhabricatorClusterDatabasesConfigType.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', 'PhabricatorClusterMailersConfigType' => 'infrastructure/cluster/config/PhabricatorClusterMailersConfigType.php', 'PhabricatorClusterNoHostForRoleException' => 'infrastructure/cluster/exception/PhabricatorClusterNoHostForRoleException.php', 'PhabricatorClusterSearchConfigType' => 'infrastructure/cluster/config/PhabricatorClusterSearchConfigType.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', 'PhabricatorCommentEditField' => 'applications/transactions/editfield/PhabricatorCommentEditField.php', 'PhabricatorCommentEditType' => 'applications/transactions/edittype/PhabricatorCommentEditType.php', 'PhabricatorCommitBranchesField' => 'applications/repository/customfield/PhabricatorCommitBranchesField.php', 'PhabricatorCommitCustomField' => 'applications/repository/customfield/PhabricatorCommitCustomField.php', 'PhabricatorCommitMergedCommitsField' => 'applications/repository/customfield/PhabricatorCommitMergedCommitsField.php', 'PhabricatorCommitRepositoryField' => 'applications/repository/customfield/PhabricatorCommitRepositoryField.php', 'PhabricatorCommitSearchEngine' => 'applications/audit/query/PhabricatorCommitSearchEngine.php', 'PhabricatorCommitTagsField' => 'applications/repository/customfield/PhabricatorCommitTagsField.php', 'PhabricatorCommonPasswords' => 'applications/auth/constants/PhabricatorCommonPasswords.php', 'PhabricatorConduitAPIController' => 'applications/conduit/controller/PhabricatorConduitAPIController.php', 'PhabricatorConduitApplication' => 'applications/conduit/application/PhabricatorConduitApplication.php', 'PhabricatorConduitCallManagementWorkflow' => 'applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php', 'PhabricatorConduitCertificateToken' => 'applications/conduit/storage/PhabricatorConduitCertificateToken.php', 'PhabricatorConduitConsoleController' => 'applications/conduit/controller/PhabricatorConduitConsoleController.php', 'PhabricatorConduitContentSource' => 'infrastructure/contentsource/PhabricatorConduitContentSource.php', 'PhabricatorConduitController' => 'applications/conduit/controller/PhabricatorConduitController.php', 'PhabricatorConduitDAO' => 'applications/conduit/storage/PhabricatorConduitDAO.php', 'PhabricatorConduitEditField' => 'applications/transactions/editfield/PhabricatorConduitEditField.php', 'PhabricatorConduitListController' => 'applications/conduit/controller/PhabricatorConduitListController.php', 'PhabricatorConduitLogController' => 'applications/conduit/controller/PhabricatorConduitLogController.php', 'PhabricatorConduitLogQuery' => 'applications/conduit/query/PhabricatorConduitLogQuery.php', 'PhabricatorConduitLogSearchEngine' => 'applications/conduit/query/PhabricatorConduitLogSearchEngine.php', 'PhabricatorConduitManagementWorkflow' => 'applications/conduit/management/PhabricatorConduitManagementWorkflow.php', 'PhabricatorConduitMethodCallLog' => 'applications/conduit/storage/PhabricatorConduitMethodCallLog.php', 'PhabricatorConduitMethodQuery' => 'applications/conduit/query/PhabricatorConduitMethodQuery.php', 'PhabricatorConduitRequestExceptionHandler' => 'aphront/handler/PhabricatorConduitRequestExceptionHandler.php', 'PhabricatorConduitResultInterface' => 'applications/conduit/interface/PhabricatorConduitResultInterface.php', 'PhabricatorConduitSearchEngine' => 'applications/conduit/query/PhabricatorConduitSearchEngine.php', 'PhabricatorConduitSearchFieldSpecification' => 'applications/conduit/interface/PhabricatorConduitSearchFieldSpecification.php', 'PhabricatorConduitTestCase' => '__tests__/PhabricatorConduitTestCase.php', 'PhabricatorConduitToken' => 'applications/conduit/storage/PhabricatorConduitToken.php', 'PhabricatorConduitTokenController' => 'applications/conduit/controller/PhabricatorConduitTokenController.php', 'PhabricatorConduitTokenEditController' => 'applications/conduit/controller/PhabricatorConduitTokenEditController.php', 'PhabricatorConduitTokenHandshakeController' => 'applications/conduit/controller/PhabricatorConduitTokenHandshakeController.php', 'PhabricatorConduitTokenQuery' => 'applications/conduit/query/PhabricatorConduitTokenQuery.php', 'PhabricatorConduitTokenTerminateController' => 'applications/conduit/controller/PhabricatorConduitTokenTerminateController.php', 'PhabricatorConduitTokensSettingsPanel' => 'applications/conduit/settings/PhabricatorConduitTokensSettingsPanel.php', 'PhabricatorConfigAllController' => 'applications/config/controller/PhabricatorConfigAllController.php', 'PhabricatorConfigApplication' => 'applications/config/application/PhabricatorConfigApplication.php', 'PhabricatorConfigApplicationController' => 'applications/config/controller/PhabricatorConfigApplicationController.php', 'PhabricatorConfigCacheController' => 'applications/config/controller/PhabricatorConfigCacheController.php', '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', 'PhabricatorConfigConstants' => 'applications/config/constants/PhabricatorConfigConstants.php', 'PhabricatorConfigController' => 'applications/config/controller/PhabricatorConfigController.php', 'PhabricatorConfigCoreSchemaSpec' => 'applications/config/schema/PhabricatorConfigCoreSchemaSpec.php', 'PhabricatorConfigDatabaseController' => 'applications/config/controller/PhabricatorConfigDatabaseController.php', 'PhabricatorConfigDatabaseIssueController' => 'applications/config/controller/PhabricatorConfigDatabaseIssueController.php', 'PhabricatorConfigDatabaseSchema' => 'applications/config/schema/PhabricatorConfigDatabaseSchema.php', 'PhabricatorConfigDatabaseSource' => 'infrastructure/env/PhabricatorConfigDatabaseSource.php', 'PhabricatorConfigDatabaseStatusController' => 'applications/config/controller/PhabricatorConfigDatabaseStatusController.php', 'PhabricatorConfigDefaultSource' => 'infrastructure/env/PhabricatorConfigDefaultSource.php', 'PhabricatorConfigDictionarySource' => 'infrastructure/env/PhabricatorConfigDictionarySource.php', 'PhabricatorConfigEdgeModule' => 'applications/config/module/PhabricatorConfigEdgeModule.php', 'PhabricatorConfigEditController' => 'applications/config/controller/PhabricatorConfigEditController.php', 'PhabricatorConfigEditor' => 'applications/config/editor/PhabricatorConfigEditor.php', 'PhabricatorConfigEntry' => 'applications/config/storage/PhabricatorConfigEntry.php', 'PhabricatorConfigEntryDAO' => 'applications/config/storage/PhabricatorConfigEntryDAO.php', 'PhabricatorConfigEntryQuery' => 'applications/config/query/PhabricatorConfigEntryQuery.php', 'PhabricatorConfigFileSource' => 'infrastructure/env/PhabricatorConfigFileSource.php', 'PhabricatorConfigGroupConstants' => 'applications/config/constants/PhabricatorConfigGroupConstants.php', 'PhabricatorConfigGroupController' => 'applications/config/controller/PhabricatorConfigGroupController.php', 'PhabricatorConfigHTTPParameterTypesModule' => 'applications/config/module/PhabricatorConfigHTTPParameterTypesModule.php', 'PhabricatorConfigHistoryController' => 'applications/config/controller/PhabricatorConfigHistoryController.php', 'PhabricatorConfigIgnoreController' => 'applications/config/controller/PhabricatorConfigIgnoreController.php', 'PhabricatorConfigIssueListController' => 'applications/config/controller/PhabricatorConfigIssueListController.php', 'PhabricatorConfigIssuePanelController' => 'applications/config/controller/PhabricatorConfigIssuePanelController.php', 'PhabricatorConfigIssueViewController' => 'applications/config/controller/PhabricatorConfigIssueViewController.php', 'PhabricatorConfigJSON' => 'applications/config/json/PhabricatorConfigJSON.php', 'PhabricatorConfigJSONOptionType' => 'applications/config/custom/PhabricatorConfigJSONOptionType.php', 'PhabricatorConfigKeySchema' => 'applications/config/schema/PhabricatorConfigKeySchema.php', 'PhabricatorConfigListController' => 'applications/config/controller/PhabricatorConfigListController.php', 'PhabricatorConfigLocalSource' => 'infrastructure/env/PhabricatorConfigLocalSource.php', 'PhabricatorConfigManagementDeleteWorkflow' => 'applications/config/management/PhabricatorConfigManagementDeleteWorkflow.php', 'PhabricatorConfigManagementDoneWorkflow' => 'applications/config/management/PhabricatorConfigManagementDoneWorkflow.php', 'PhabricatorConfigManagementGetWorkflow' => 'applications/config/management/PhabricatorConfigManagementGetWorkflow.php', 'PhabricatorConfigManagementListWorkflow' => 'applications/config/management/PhabricatorConfigManagementListWorkflow.php', 'PhabricatorConfigManagementMigrateWorkflow' => 'applications/config/management/PhabricatorConfigManagementMigrateWorkflow.php', 'PhabricatorConfigManagementSetWorkflow' => 'applications/config/management/PhabricatorConfigManagementSetWorkflow.php', 'PhabricatorConfigManagementWorkflow' => 'applications/config/management/PhabricatorConfigManagementWorkflow.php', 'PhabricatorConfigManualActivity' => 'applications/config/storage/PhabricatorConfigManualActivity.php', 'PhabricatorConfigModule' => 'applications/config/module/PhabricatorConfigModule.php', 'PhabricatorConfigModuleController' => 'applications/config/controller/PhabricatorConfigModuleController.php', 'PhabricatorConfigOption' => 'applications/config/option/PhabricatorConfigOption.php', 'PhabricatorConfigOptionType' => 'applications/config/custom/PhabricatorConfigOptionType.php', 'PhabricatorConfigPHIDModule' => 'applications/config/module/PhabricatorConfigPHIDModule.php', 'PhabricatorConfigProxySource' => 'infrastructure/env/PhabricatorConfigProxySource.php', 'PhabricatorConfigPurgeCacheController' => 'applications/config/controller/PhabricatorConfigPurgeCacheController.php', 'PhabricatorConfigRegexOptionType' => 'applications/config/custom/PhabricatorConfigRegexOptionType.php', 'PhabricatorConfigRequestExceptionHandlerModule' => 'applications/config/module/PhabricatorConfigRequestExceptionHandlerModule.php', 'PhabricatorConfigResponse' => 'applications/config/response/PhabricatorConfigResponse.php', 'PhabricatorConfigSchemaQuery' => 'applications/config/schema/PhabricatorConfigSchemaQuery.php', 'PhabricatorConfigSchemaSpec' => 'applications/config/schema/PhabricatorConfigSchemaSpec.php', 'PhabricatorConfigServerSchema' => 'applications/config/schema/PhabricatorConfigServerSchema.php', 'PhabricatorConfigSetupCheckModule' => 'applications/config/module/PhabricatorConfigSetupCheckModule.php', 'PhabricatorConfigSiteModule' => 'applications/config/module/PhabricatorConfigSiteModule.php', 'PhabricatorConfigSiteSource' => 'infrastructure/env/PhabricatorConfigSiteSource.php', 'PhabricatorConfigSource' => 'infrastructure/env/PhabricatorConfigSource.php', 'PhabricatorConfigStackSource' => 'infrastructure/env/PhabricatorConfigStackSource.php', 'PhabricatorConfigStorageSchema' => 'applications/config/schema/PhabricatorConfigStorageSchema.php', 'PhabricatorConfigTableSchema' => 'applications/config/schema/PhabricatorConfigTableSchema.php', 'PhabricatorConfigTransaction' => 'applications/config/storage/PhabricatorConfigTransaction.php', 'PhabricatorConfigTransactionQuery' => 'applications/config/query/PhabricatorConfigTransactionQuery.php', 'PhabricatorConfigType' => 'applications/config/type/PhabricatorConfigType.php', 'PhabricatorConfigValidationException' => 'applications/config/exception/PhabricatorConfigValidationException.php', 'PhabricatorConfigVersionController' => 'applications/config/controller/PhabricatorConfigVersionController.php', 'PhabricatorConpherenceApplication' => 'applications/conpherence/application/PhabricatorConpherenceApplication.php', 'PhabricatorConpherenceColumnMinimizeSetting' => 'applications/settings/setting/PhabricatorConpherenceColumnMinimizeSetting.php', 'PhabricatorConpherenceColumnVisibleSetting' => 'applications/settings/setting/PhabricatorConpherenceColumnVisibleSetting.php', 'PhabricatorConpherenceNotificationsSetting' => 'applications/settings/setting/PhabricatorConpherenceNotificationsSetting.php', 'PhabricatorConpherencePreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorConpherencePreferencesSettingsPanel.php', 'PhabricatorConpherenceProfileMenuItem' => 'applications/search/menuitem/PhabricatorConpherenceProfileMenuItem.php', 'PhabricatorConpherenceRoomContextFreeGrammar' => 'applications/conpherence/lipsum/PhabricatorConpherenceRoomContextFreeGrammar.php', 'PhabricatorConpherenceRoomTestDataGenerator' => 'applications/conpherence/lipsum/PhabricatorConpherenceRoomTestDataGenerator.php', 'PhabricatorConpherenceSoundSetting' => 'applications/settings/setting/PhabricatorConpherenceSoundSetting.php', 'PhabricatorConpherenceThreadPHIDType' => 'applications/conpherence/phid/PhabricatorConpherenceThreadPHIDType.php', 'PhabricatorConpherenceWidgetVisibleSetting' => 'applications/settings/setting/PhabricatorConpherenceWidgetVisibleSetting.php', 'PhabricatorConsoleApplication' => 'applications/console/application/PhabricatorConsoleApplication.php', 'PhabricatorConsoleContentSource' => 'infrastructure/contentsource/PhabricatorConsoleContentSource.php', 'PhabricatorContentSource' => 'infrastructure/contentsource/PhabricatorContentSource.php', 'PhabricatorContentSourceModule' => 'infrastructure/contentsource/PhabricatorContentSourceModule.php', 'PhabricatorContentSourceView' => 'infrastructure/contentsource/PhabricatorContentSourceView.php', 'PhabricatorContributedToObjectEdgeType' => 'applications/transactions/edges/PhabricatorContributedToObjectEdgeType.php', 'PhabricatorController' => 'applications/base/controller/PhabricatorController.php', 'PhabricatorCookies' => 'applications/auth/constants/PhabricatorCookies.php', 'PhabricatorCoreConfigOptions' => 'applications/config/option/PhabricatorCoreConfigOptions.php', 'PhabricatorCoreCreateTransaction' => 'applications/transactions/xaction/PhabricatorCoreCreateTransaction.php', 'PhabricatorCoreTransactionType' => 'applications/transactions/xaction/PhabricatorCoreTransactionType.php', 'PhabricatorCoreVoidTransaction' => 'applications/transactions/xaction/PhabricatorCoreVoidTransaction.php', 'PhabricatorCountFact' => 'applications/fact/fact/PhabricatorCountFact.php', 'PhabricatorCountdown' => 'applications/countdown/storage/PhabricatorCountdown.php', 'PhabricatorCountdownApplication' => 'applications/countdown/application/PhabricatorCountdownApplication.php', 'PhabricatorCountdownController' => 'applications/countdown/controller/PhabricatorCountdownController.php', 'PhabricatorCountdownCountdownPHIDType' => 'applications/countdown/phid/PhabricatorCountdownCountdownPHIDType.php', 'PhabricatorCountdownDAO' => 'applications/countdown/storage/PhabricatorCountdownDAO.php', 'PhabricatorCountdownDefaultEditCapability' => 'applications/countdown/capability/PhabricatorCountdownDefaultEditCapability.php', 'PhabricatorCountdownDefaultViewCapability' => 'applications/countdown/capability/PhabricatorCountdownDefaultViewCapability.php', 'PhabricatorCountdownDescriptionTransaction' => 'applications/countdown/xaction/PhabricatorCountdownDescriptionTransaction.php', 'PhabricatorCountdownEditController' => 'applications/countdown/controller/PhabricatorCountdownEditController.php', 'PhabricatorCountdownEditEngine' => 'applications/countdown/editor/PhabricatorCountdownEditEngine.php', 'PhabricatorCountdownEditor' => 'applications/countdown/editor/PhabricatorCountdownEditor.php', 'PhabricatorCountdownEpochTransaction' => 'applications/countdown/xaction/PhabricatorCountdownEpochTransaction.php', 'PhabricatorCountdownListController' => 'applications/countdown/controller/PhabricatorCountdownListController.php', 'PhabricatorCountdownMailReceiver' => 'applications/countdown/mail/PhabricatorCountdownMailReceiver.php', 'PhabricatorCountdownQuery' => 'applications/countdown/query/PhabricatorCountdownQuery.php', 'PhabricatorCountdownRemarkupRule' => 'applications/countdown/remarkup/PhabricatorCountdownRemarkupRule.php', 'PhabricatorCountdownReplyHandler' => 'applications/countdown/mail/PhabricatorCountdownReplyHandler.php', 'PhabricatorCountdownSchemaSpec' => 'applications/countdown/storage/PhabricatorCountdownSchemaSpec.php', 'PhabricatorCountdownSearchEngine' => 'applications/countdown/query/PhabricatorCountdownSearchEngine.php', 'PhabricatorCountdownTitleTransaction' => 'applications/countdown/xaction/PhabricatorCountdownTitleTransaction.php', 'PhabricatorCountdownTransaction' => 'applications/countdown/storage/PhabricatorCountdownTransaction.php', 'PhabricatorCountdownTransactionComment' => 'applications/countdown/storage/PhabricatorCountdownTransactionComment.php', 'PhabricatorCountdownTransactionQuery' => 'applications/countdown/query/PhabricatorCountdownTransactionQuery.php', 'PhabricatorCountdownTransactionType' => 'applications/countdown/xaction/PhabricatorCountdownTransactionType.php', 'PhabricatorCountdownView' => 'applications/countdown/view/PhabricatorCountdownView.php', 'PhabricatorCountdownViewController' => 'applications/countdown/controller/PhabricatorCountdownViewController.php', 'PhabricatorCursorPagedPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php', 'PhabricatorCustomField' => 'infrastructure/customfield/field/PhabricatorCustomField.php', 'PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource' => 'infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource.php', 'PhabricatorCustomFieldApplicationSearchDatasource' => 'infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchDatasource.php', 'PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource' => 'infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource.php', 'PhabricatorCustomFieldAttachment' => 'infrastructure/customfield/field/PhabricatorCustomFieldAttachment.php', 'PhabricatorCustomFieldConfigOptionType' => 'infrastructure/customfield/config/PhabricatorCustomFieldConfigOptionType.php', 'PhabricatorCustomFieldDataNotAvailableException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldDataNotAvailableException.php', 'PhabricatorCustomFieldEditEngineExtension' => 'infrastructure/customfield/engineextension/PhabricatorCustomFieldEditEngineExtension.php', 'PhabricatorCustomFieldEditField' => 'infrastructure/customfield/editor/PhabricatorCustomFieldEditField.php', 'PhabricatorCustomFieldEditType' => 'infrastructure/customfield/editor/PhabricatorCustomFieldEditType.php', 'PhabricatorCustomFieldExportEngineExtension' => 'infrastructure/export/engine/PhabricatorCustomFieldExportEngineExtension.php', 'PhabricatorCustomFieldFulltextEngineExtension' => 'infrastructure/customfield/engineextension/PhabricatorCustomFieldFulltextEngineExtension.php', 'PhabricatorCustomFieldHeraldAction' => 'infrastructure/customfield/herald/PhabricatorCustomFieldHeraldAction.php', 'PhabricatorCustomFieldHeraldActionGroup' => 'infrastructure/customfield/herald/PhabricatorCustomFieldHeraldActionGroup.php', 'PhabricatorCustomFieldHeraldField' => 'infrastructure/customfield/herald/PhabricatorCustomFieldHeraldField.php', 'PhabricatorCustomFieldHeraldFieldGroup' => 'infrastructure/customfield/herald/PhabricatorCustomFieldHeraldFieldGroup.php', 'PhabricatorCustomFieldImplementationIncompleteException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldImplementationIncompleteException.php', 'PhabricatorCustomFieldIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldIndexStorage.php', 'PhabricatorCustomFieldInterface' => 'infrastructure/customfield/interface/PhabricatorCustomFieldInterface.php', 'PhabricatorCustomFieldList' => 'infrastructure/customfield/field/PhabricatorCustomFieldList.php', 'PhabricatorCustomFieldMonogramParser' => 'infrastructure/customfield/parser/PhabricatorCustomFieldMonogramParser.php', 'PhabricatorCustomFieldNotAttachedException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldNotAttachedException.php', 'PhabricatorCustomFieldNotProxyException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldNotProxyException.php', 'PhabricatorCustomFieldNumericIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldNumericIndexStorage.php', 'PhabricatorCustomFieldSearchEngineExtension' => 'infrastructure/customfield/engineextension/PhabricatorCustomFieldSearchEngineExtension.php', 'PhabricatorCustomFieldStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldStorage.php', 'PhabricatorCustomFieldStorageQuery' => 'infrastructure/customfield/query/PhabricatorCustomFieldStorageQuery.php', 'PhabricatorCustomFieldStringIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldStringIndexStorage.php', 'PhabricatorCustomLogoConfigType' => 'applications/config/custom/PhabricatorCustomLogoConfigType.php', 'PhabricatorCustomUIFooterConfigType' => 'applications/config/custom/PhabricatorCustomUIFooterConfigType.php', 'PhabricatorDaemon' => 'infrastructure/daemon/PhabricatorDaemon.php', 'PhabricatorDaemonBulkJobController' => 'applications/daemon/controller/PhabricatorDaemonBulkJobController.php', 'PhabricatorDaemonBulkJobListController' => 'applications/daemon/controller/PhabricatorDaemonBulkJobListController.php', 'PhabricatorDaemonBulkJobMonitorController' => 'applications/daemon/controller/PhabricatorDaemonBulkJobMonitorController.php', 'PhabricatorDaemonBulkJobViewController' => 'applications/daemon/controller/PhabricatorDaemonBulkJobViewController.php', 'PhabricatorDaemonConsoleController' => 'applications/daemon/controller/PhabricatorDaemonConsoleController.php', 'PhabricatorDaemonContentSource' => 'infrastructure/daemon/contentsource/PhabricatorDaemonContentSource.php', 'PhabricatorDaemonController' => 'applications/daemon/controller/PhabricatorDaemonController.php', 'PhabricatorDaemonDAO' => 'applications/daemon/storage/PhabricatorDaemonDAO.php', 'PhabricatorDaemonEventListener' => 'applications/daemon/event/PhabricatorDaemonEventListener.php', 'PhabricatorDaemonLockLog' => 'applications/daemon/storage/PhabricatorDaemonLockLog.php', 'PhabricatorDaemonLockLogGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonLockLogGarbageCollector.php', 'PhabricatorDaemonLog' => 'applications/daemon/storage/PhabricatorDaemonLog.php', 'PhabricatorDaemonLogEvent' => 'applications/daemon/storage/PhabricatorDaemonLogEvent.php', 'PhabricatorDaemonLogEventGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonLogEventGarbageCollector.php', 'PhabricatorDaemonLogEventViewController' => 'applications/daemon/controller/PhabricatorDaemonLogEventViewController.php', 'PhabricatorDaemonLogEventsView' => 'applications/daemon/view/PhabricatorDaemonLogEventsView.php', 'PhabricatorDaemonLogGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonLogGarbageCollector.php', 'PhabricatorDaemonLogListController' => 'applications/daemon/controller/PhabricatorDaemonLogListController.php', 'PhabricatorDaemonLogListView' => 'applications/daemon/view/PhabricatorDaemonLogListView.php', 'PhabricatorDaemonLogQuery' => 'applications/daemon/query/PhabricatorDaemonLogQuery.php', 'PhabricatorDaemonLogViewController' => 'applications/daemon/controller/PhabricatorDaemonLogViewController.php', 'PhabricatorDaemonManagementDebugWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementDebugWorkflow.php', 'PhabricatorDaemonManagementLaunchWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementLaunchWorkflow.php', 'PhabricatorDaemonManagementListWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementListWorkflow.php', 'PhabricatorDaemonManagementLogWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementLogWorkflow.php', 'PhabricatorDaemonManagementReloadWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementReloadWorkflow.php', 'PhabricatorDaemonManagementRestartWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php', 'PhabricatorDaemonManagementStartWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementStartWorkflow.php', 'PhabricatorDaemonManagementStatusWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php', 'PhabricatorDaemonManagementStopWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php', 'PhabricatorDaemonManagementWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementWorkflow.php', 'PhabricatorDaemonOverseerModule' => 'infrastructure/daemon/overseer/PhabricatorDaemonOverseerModule.php', 'PhabricatorDaemonReference' => 'infrastructure/daemon/control/PhabricatorDaemonReference.php', 'PhabricatorDaemonTaskGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonTaskGarbageCollector.php', 'PhabricatorDaemonTasksTableView' => 'applications/daemon/view/PhabricatorDaemonTasksTableView.php', 'PhabricatorDaemonsApplication' => 'applications/daemon/application/PhabricatorDaemonsApplication.php', 'PhabricatorDaemonsSetupCheck' => 'applications/config/check/PhabricatorDaemonsSetupCheck.php', 'PhabricatorDailyRoutineTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorDailyRoutineTriggerClock.php', 'PhabricatorDarkConsoleSetting' => 'applications/settings/setting/PhabricatorDarkConsoleSetting.php', 'PhabricatorDarkConsoleTabSetting' => 'applications/settings/setting/PhabricatorDarkConsoleTabSetting.php', 'PhabricatorDarkConsoleVisibleSetting' => 'applications/settings/setting/PhabricatorDarkConsoleVisibleSetting.php', 'PhabricatorDashboard' => 'applications/dashboard/storage/PhabricatorDashboard.php', 'PhabricatorDashboardAddPanelController' => 'applications/dashboard/controller/PhabricatorDashboardAddPanelController.php', 'PhabricatorDashboardApplication' => 'applications/dashboard/application/PhabricatorDashboardApplication.php', 'PhabricatorDashboardArchiveController' => 'applications/dashboard/controller/PhabricatorDashboardArchiveController.php', 'PhabricatorDashboardArrangeController' => 'applications/dashboard/controller/PhabricatorDashboardArrangeController.php', 'PhabricatorDashboardController' => 'applications/dashboard/controller/PhabricatorDashboardController.php', 'PhabricatorDashboardDAO' => 'applications/dashboard/storage/PhabricatorDashboardDAO.php', 'PhabricatorDashboardDashboardHasPanelEdgeType' => 'applications/dashboard/edge/PhabricatorDashboardDashboardHasPanelEdgeType.php', 'PhabricatorDashboardDashboardPHIDType' => 'applications/dashboard/phid/PhabricatorDashboardDashboardPHIDType.php', 'PhabricatorDashboardDatasource' => 'applications/dashboard/typeahead/PhabricatorDashboardDatasource.php', 'PhabricatorDashboardEditController' => 'applications/dashboard/controller/PhabricatorDashboardEditController.php', 'PhabricatorDashboardIconSet' => 'applications/dashboard/icon/PhabricatorDashboardIconSet.php', 'PhabricatorDashboardInstall' => 'applications/dashboard/storage/PhabricatorDashboardInstall.php', 'PhabricatorDashboardInstallController' => 'applications/dashboard/controller/PhabricatorDashboardInstallController.php', 'PhabricatorDashboardLayoutConfig' => 'applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php', 'PhabricatorDashboardListController' => 'applications/dashboard/controller/PhabricatorDashboardListController.php', 'PhabricatorDashboardManageController' => 'applications/dashboard/controller/PhabricatorDashboardManageController.php', 'PhabricatorDashboardMovePanelController' => 'applications/dashboard/controller/PhabricatorDashboardMovePanelController.php', 'PhabricatorDashboardNgrams' => 'applications/dashboard/storage/PhabricatorDashboardNgrams.php', 'PhabricatorDashboardPanel' => 'applications/dashboard/storage/PhabricatorDashboardPanel.php', 'PhabricatorDashboardPanelArchiveController' => 'applications/dashboard/controller/PhabricatorDashboardPanelArchiveController.php', 'PhabricatorDashboardPanelCoreCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelCoreCustomField.php', 'PhabricatorDashboardPanelCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelCustomField.php', 'PhabricatorDashboardPanelDatasource' => 'applications/dashboard/typeahead/PhabricatorDashboardPanelDatasource.php', 'PhabricatorDashboardPanelEditConduitAPIMethod' => 'applications/dashboard/conduit/PhabricatorDashboardPanelEditConduitAPIMethod.php', 'PhabricatorDashboardPanelEditController' => 'applications/dashboard/controller/PhabricatorDashboardPanelEditController.php', 'PhabricatorDashboardPanelEditEngine' => 'applications/dashboard/editor/PhabricatorDashboardPanelEditEngine.php', 'PhabricatorDashboardPanelEditproController' => 'applications/dashboard/controller/PhabricatorDashboardPanelEditproController.php', 'PhabricatorDashboardPanelHasDashboardEdgeType' => 'applications/dashboard/edge/PhabricatorDashboardPanelHasDashboardEdgeType.php', 'PhabricatorDashboardPanelListController' => 'applications/dashboard/controller/PhabricatorDashboardPanelListController.php', 'PhabricatorDashboardPanelNgrams' => 'applications/dashboard/storage/PhabricatorDashboardPanelNgrams.php', 'PhabricatorDashboardPanelPHIDType' => 'applications/dashboard/phid/PhabricatorDashboardPanelPHIDType.php', 'PhabricatorDashboardPanelQuery' => 'applications/dashboard/query/PhabricatorDashboardPanelQuery.php', 'PhabricatorDashboardPanelRenderController' => 'applications/dashboard/controller/PhabricatorDashboardPanelRenderController.php', 'PhabricatorDashboardPanelRenderingEngine' => 'applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php', 'PhabricatorDashboardPanelSearchApplicationCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelSearchApplicationCustomField.php', 'PhabricatorDashboardPanelSearchEngine' => 'applications/dashboard/query/PhabricatorDashboardPanelSearchEngine.php', 'PhabricatorDashboardPanelSearchQueryCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelSearchQueryCustomField.php', 'PhabricatorDashboardPanelTabsCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelTabsCustomField.php', 'PhabricatorDashboardPanelTransaction' => 'applications/dashboard/storage/PhabricatorDashboardPanelTransaction.php', 'PhabricatorDashboardPanelTransactionEditor' => 'applications/dashboard/editor/PhabricatorDashboardPanelTransactionEditor.php', 'PhabricatorDashboardPanelTransactionQuery' => 'applications/dashboard/query/PhabricatorDashboardPanelTransactionQuery.php', 'PhabricatorDashboardPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardPanelType.php', 'PhabricatorDashboardPanelViewController' => 'applications/dashboard/controller/PhabricatorDashboardPanelViewController.php', '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', 'PhabricatorDashboardRenderingEngine' => 'applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php', 'PhabricatorDashboardSchemaSpec' => 'applications/dashboard/storage/PhabricatorDashboardSchemaSpec.php', 'PhabricatorDashboardSearchEngine' => 'applications/dashboard/query/PhabricatorDashboardSearchEngine.php', 'PhabricatorDashboardTabsPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardTabsPanelType.php', 'PhabricatorDashboardTextPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardTextPanelType.php', 'PhabricatorDashboardTransaction' => 'applications/dashboard/storage/PhabricatorDashboardTransaction.php', 'PhabricatorDashboardTransactionEditor' => 'applications/dashboard/editor/PhabricatorDashboardTransactionEditor.php', 'PhabricatorDashboardTransactionQuery' => 'applications/dashboard/query/PhabricatorDashboardTransactionQuery.php', 'PhabricatorDashboardViewController' => 'applications/dashboard/controller/PhabricatorDashboardViewController.php', 'PhabricatorDataCacheSpec' => 'applications/cache/spec/PhabricatorDataCacheSpec.php', 'PhabricatorDataNotAttachedException' => 'infrastructure/storage/lisk/PhabricatorDataNotAttachedException.php', 'PhabricatorDatabaseRef' => 'infrastructure/cluster/PhabricatorDatabaseRef.php', 'PhabricatorDatabaseRefParser' => 'infrastructure/cluster/PhabricatorDatabaseRefParser.php', 'PhabricatorDatabaseSetupCheck' => 'applications/config/check/PhabricatorDatabaseSetupCheck.php', 'PhabricatorDatasourceApplicationEngineExtension' => 'applications/meta/engineextension/PhabricatorDatasourceApplicationEngineExtension.php', 'PhabricatorDatasourceEditField' => 'applications/transactions/editfield/PhabricatorDatasourceEditField.php', 'PhabricatorDatasourceEditType' => 'applications/transactions/edittype/PhabricatorDatasourceEditType.php', 'PhabricatorDatasourceEngine' => 'applications/search/engine/PhabricatorDatasourceEngine.php', 'PhabricatorDatasourceEngineExtension' => 'applications/search/engineextension/PhabricatorDatasourceEngineExtension.php', 'PhabricatorDateFormatSetting' => 'applications/settings/setting/PhabricatorDateFormatSetting.php', 'PhabricatorDateTimeSettingsPanel' => 'applications/settings/panel/PhabricatorDateTimeSettingsPanel.php', 'PhabricatorDebugController' => 'applications/system/controller/PhabricatorDebugController.php', 'PhabricatorDefaultRequestExceptionHandler' => 'aphront/handler/PhabricatorDefaultRequestExceptionHandler.php', 'PhabricatorDefaultSyntaxStyle' => 'infrastructure/syntax/PhabricatorDefaultSyntaxStyle.php', 'PhabricatorDestructibleCodex' => 'applications/system/codex/PhabricatorDestructibleCodex.php', 'PhabricatorDestructibleCodexInterface' => 'applications/system/interface/PhabricatorDestructibleCodexInterface.php', 'PhabricatorDestructibleInterface' => 'applications/system/interface/PhabricatorDestructibleInterface.php', 'PhabricatorDestructionEngine' => 'applications/system/engine/PhabricatorDestructionEngine.php', 'PhabricatorDestructionEngineExtension' => 'applications/system/engine/PhabricatorDestructionEngineExtension.php', 'PhabricatorDestructionEngineExtensionModule' => 'applications/system/engine/PhabricatorDestructionEngineExtensionModule.php', 'PhabricatorDeveloperConfigOptions' => 'applications/config/option/PhabricatorDeveloperConfigOptions.php', 'PhabricatorDeveloperPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDeveloperPreferencesSettingsPanel.php', 'PhabricatorDiffInlineCommentQuery' => 'infrastructure/diff/query/PhabricatorDiffInlineCommentQuery.php', 'PhabricatorDiffPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDiffPreferencesSettingsPanel.php', 'PhabricatorDifferenceEngine' => 'infrastructure/diff/PhabricatorDifferenceEngine.php', 'PhabricatorDifferentialApplication' => 'applications/differential/application/PhabricatorDifferentialApplication.php', 'PhabricatorDifferentialAttachCommitWorkflow' => 'applications/differential/management/PhabricatorDifferentialAttachCommitWorkflow.php', 'PhabricatorDifferentialConfigOptions' => 'applications/differential/config/PhabricatorDifferentialConfigOptions.php', 'PhabricatorDifferentialExtractWorkflow' => 'applications/differential/management/PhabricatorDifferentialExtractWorkflow.php', 'PhabricatorDifferentialManagementWorkflow' => 'applications/differential/management/PhabricatorDifferentialManagementWorkflow.php', 'PhabricatorDifferentialMigrateHunkWorkflow' => 'applications/differential/management/PhabricatorDifferentialMigrateHunkWorkflow.php', 'PhabricatorDifferentialRebuildChangesetsWorkflow' => 'applications/differential/management/PhabricatorDifferentialRebuildChangesetsWorkflow.php', 'PhabricatorDifferentialRevisionTestDataGenerator' => 'applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php', 'PhabricatorDiffusionApplication' => 'applications/diffusion/application/PhabricatorDiffusionApplication.php', 'PhabricatorDiffusionBlameSetting' => 'applications/settings/setting/PhabricatorDiffusionBlameSetting.php', 'PhabricatorDiffusionConfigOptions' => 'applications/diffusion/config/PhabricatorDiffusionConfigOptions.php', 'PhabricatorDisabledUserController' => 'applications/auth/controller/PhabricatorDisabledUserController.php', 'PhabricatorDisplayPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDisplayPreferencesSettingsPanel.php', 'PhabricatorDisqusAuthProvider' => 'applications/auth/provider/PhabricatorDisqusAuthProvider.php', 'PhabricatorDividerEditField' => 'applications/transactions/editfield/PhabricatorDividerEditField.php', 'PhabricatorDividerProfileMenuItem' => 'applications/search/menuitem/PhabricatorDividerProfileMenuItem.php', 'PhabricatorDivinerApplication' => 'applications/diviner/application/PhabricatorDivinerApplication.php', 'PhabricatorDocumentEngine' => 'applications/files/document/PhabricatorDocumentEngine.php', 'PhabricatorDocumentRef' => 'applications/files/document/PhabricatorDocumentRef.php', 'PhabricatorDocumentRenderingEngine' => 'applications/files/document/render/PhabricatorDocumentRenderingEngine.php', 'PhabricatorDoorkeeperApplication' => 'applications/doorkeeper/application/PhabricatorDoorkeeperApplication.php', 'PhabricatorDoubleExportField' => 'infrastructure/export/field/PhabricatorDoubleExportField.php', 'PhabricatorDraft' => 'applications/draft/storage/PhabricatorDraft.php', 'PhabricatorDraftDAO' => 'applications/draft/storage/PhabricatorDraftDAO.php', 'PhabricatorDraftEngine' => 'applications/transactions/draft/PhabricatorDraftEngine.php', 'PhabricatorDraftInterface' => 'applications/transactions/draft/PhabricatorDraftInterface.php', 'PhabricatorDrydockApplication' => 'applications/drydock/application/PhabricatorDrydockApplication.php', 'PhabricatorEdgeChangeRecord' => 'infrastructure/edges/util/PhabricatorEdgeChangeRecord.php', 'PhabricatorEdgeChangeRecordTestCase' => 'infrastructure/edges/__tests__/PhabricatorEdgeChangeRecordTestCase.php', 'PhabricatorEdgeConfig' => 'infrastructure/edges/constants/PhabricatorEdgeConfig.php', 'PhabricatorEdgeConstants' => 'infrastructure/edges/constants/PhabricatorEdgeConstants.php', 'PhabricatorEdgeCycleException' => 'infrastructure/edges/exception/PhabricatorEdgeCycleException.php', 'PhabricatorEdgeEditType' => 'applications/transactions/edittype/PhabricatorEdgeEditType.php', 'PhabricatorEdgeEditor' => 'infrastructure/edges/editor/PhabricatorEdgeEditor.php', 'PhabricatorEdgeGraph' => 'infrastructure/edges/util/PhabricatorEdgeGraph.php', 'PhabricatorEdgeObject' => 'infrastructure/edges/conduit/PhabricatorEdgeObject.php', 'PhabricatorEdgeObjectQuery' => 'infrastructure/edges/query/PhabricatorEdgeObjectQuery.php', 'PhabricatorEdgeQuery' => 'infrastructure/edges/query/PhabricatorEdgeQuery.php', 'PhabricatorEdgeTestCase' => 'infrastructure/edges/__tests__/PhabricatorEdgeTestCase.php', 'PhabricatorEdgeType' => 'infrastructure/edges/type/PhabricatorEdgeType.php', 'PhabricatorEdgeTypeTestCase' => 'infrastructure/edges/type/__tests__/PhabricatorEdgeTypeTestCase.php', 'PhabricatorEdgesDestructionEngineExtension' => 'infrastructure/edges/engineextension/PhabricatorEdgesDestructionEngineExtension.php', 'PhabricatorEditEngine' => 'applications/transactions/editengine/PhabricatorEditEngine.php', 'PhabricatorEditEngineAPIMethod' => 'applications/transactions/editengine/PhabricatorEditEngineAPIMethod.php', 'PhabricatorEditEngineBulkJobType' => 'applications/transactions/bulk/PhabricatorEditEngineBulkJobType.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', 'PhabricatorEditEngineConfiguration' => 'applications/transactions/storage/PhabricatorEditEngineConfiguration.php', 'PhabricatorEditEngineConfigurationDefaultCreateController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultCreateController.php', 'PhabricatorEditEngineConfigurationDefaultsController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultsController.php', 'PhabricatorEditEngineConfigurationDisableController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationDisableController.php', 'PhabricatorEditEngineConfigurationEditController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationEditController.php', 'PhabricatorEditEngineConfigurationEditEngine' => 'applications/transactions/editor/PhabricatorEditEngineConfigurationEditEngine.php', 'PhabricatorEditEngineConfigurationEditor' => 'applications/transactions/editor/PhabricatorEditEngineConfigurationEditor.php', 'PhabricatorEditEngineConfigurationIsEditController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationIsEditController.php', 'PhabricatorEditEngineConfigurationListController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationListController.php', 'PhabricatorEditEngineConfigurationLockController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationLockController.php', 'PhabricatorEditEngineConfigurationPHIDType' => 'applications/transactions/phid/PhabricatorEditEngineConfigurationPHIDType.php', 'PhabricatorEditEngineConfigurationQuery' => 'applications/transactions/query/PhabricatorEditEngineConfigurationQuery.php', 'PhabricatorEditEngineConfigurationReorderController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationReorderController.php', 'PhabricatorEditEngineConfigurationSaveController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationSaveController.php', 'PhabricatorEditEngineConfigurationSearchEngine' => 'applications/transactions/query/PhabricatorEditEngineConfigurationSearchEngine.php', 'PhabricatorEditEngineConfigurationSortController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationSortController.php', 'PhabricatorEditEngineConfigurationSubtypeController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationSubtypeController.php', 'PhabricatorEditEngineConfigurationTransaction' => 'applications/transactions/storage/PhabricatorEditEngineConfigurationTransaction.php', 'PhabricatorEditEngineConfigurationTransactionQuery' => 'applications/transactions/query/PhabricatorEditEngineConfigurationTransactionQuery.php', 'PhabricatorEditEngineConfigurationViewController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationViewController.php', 'PhabricatorEditEngineController' => 'applications/transactions/controller/PhabricatorEditEngineController.php', 'PhabricatorEditEngineDatasource' => 'applications/transactions/typeahead/PhabricatorEditEngineDatasource.php', 'PhabricatorEditEngineDefaultLock' => 'applications/transactions/editengine/PhabricatorEditEngineDefaultLock.php', 'PhabricatorEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorEditEngineExtension.php', 'PhabricatorEditEngineExtensionModule' => 'applications/transactions/engineextension/PhabricatorEditEngineExtensionModule.php', 'PhabricatorEditEngineListController' => 'applications/transactions/controller/PhabricatorEditEngineListController.php', 'PhabricatorEditEngineLock' => 'applications/transactions/editengine/PhabricatorEditEngineLock.php', 'PhabricatorEditEngineLockableInterface' => 'applications/transactions/editengine/PhabricatorEditEngineLockableInterface.php', 'PhabricatorEditEnginePointsCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEnginePointsCommentAction.php', 'PhabricatorEditEngineProfileMenuItem' => 'applications/search/menuitem/PhabricatorEditEngineProfileMenuItem.php', 'PhabricatorEditEngineQuery' => 'applications/transactions/query/PhabricatorEditEngineQuery.php', 'PhabricatorEditEngineSearchEngine' => 'applications/transactions/query/PhabricatorEditEngineSearchEngine.php', 'PhabricatorEditEngineSelectCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineSelectCommentAction.php', 'PhabricatorEditEngineSettingsPanel' => 'applications/settings/panel/PhabricatorEditEngineSettingsPanel.php', 'PhabricatorEditEngineStaticCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineStaticCommentAction.php', 'PhabricatorEditEngineSubtype' => 'applications/transactions/editengine/PhabricatorEditEngineSubtype.php', 'PhabricatorEditEngineSubtypeInterface' => 'applications/transactions/editengine/PhabricatorEditEngineSubtypeInterface.php', 'PhabricatorEditEngineSubtypeTestCase' => 'applications/transactions/editengine/__tests__/PhabricatorEditEngineSubtypeTestCase.php', 'PhabricatorEditEngineTokenizerCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineTokenizerCommentAction.php', 'PhabricatorEditField' => 'applications/transactions/editfield/PhabricatorEditField.php', 'PhabricatorEditPage' => 'applications/transactions/editengine/PhabricatorEditPage.php', 'PhabricatorEditType' => 'applications/transactions/edittype/PhabricatorEditType.php', 'PhabricatorEditor' => 'infrastructure/PhabricatorEditor.php', 'PhabricatorEditorMailEngineExtension' => 'applications/transactions/engineextension/PhabricatorEditorMailEngineExtension.php', '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', 'PhabricatorEmailDeliverySettingsPanel' => 'applications/settings/panel/PhabricatorEmailDeliverySettingsPanel.php', 'PhabricatorEmailFormatSetting' => 'applications/settings/setting/PhabricatorEmailFormatSetting.php', 'PhabricatorEmailFormatSettingsPanel' => 'applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php', 'PhabricatorEmailLoginController' => 'applications/auth/controller/PhabricatorEmailLoginController.php', 'PhabricatorEmailNotificationsSetting' => 'applications/settings/setting/PhabricatorEmailNotificationsSetting.php', 'PhabricatorEmailPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorEmailPreferencesSettingsPanel.php', 'PhabricatorEmailRePrefixSetting' => 'applications/settings/setting/PhabricatorEmailRePrefixSetting.php', 'PhabricatorEmailSelfActionsSetting' => 'applications/settings/setting/PhabricatorEmailSelfActionsSetting.php', 'PhabricatorEmailStampsSetting' => 'applications/settings/setting/PhabricatorEmailStampsSetting.php', 'PhabricatorEmailTagsSetting' => 'applications/settings/setting/PhabricatorEmailTagsSetting.php', 'PhabricatorEmailVarySubjectsSetting' => 'applications/settings/setting/PhabricatorEmailVarySubjectsSetting.php', 'PhabricatorEmailVerificationController' => 'applications/auth/controller/PhabricatorEmailVerificationController.php', 'PhabricatorEmbedFileRemarkupRule' => 'applications/files/markup/PhabricatorEmbedFileRemarkupRule.php', 'PhabricatorEmojiDatasource' => 'applications/macro/typeahead/PhabricatorEmojiDatasource.php', 'PhabricatorEmojiRemarkupRule' => 'applications/macro/markup/PhabricatorEmojiRemarkupRule.php', 'PhabricatorEmojiTranslation' => 'infrastructure/internationalization/translation/PhabricatorEmojiTranslation.php', 'PhabricatorEmptyQueryException' => 'infrastructure/query/PhabricatorEmptyQueryException.php', 'PhabricatorEnumConfigType' => 'applications/config/type/PhabricatorEnumConfigType.php', 'PhabricatorEnv' => 'infrastructure/env/PhabricatorEnv.php', 'PhabricatorEnvTestCase' => 'infrastructure/env/__tests__/PhabricatorEnvTestCase.php', 'PhabricatorEpochEditField' => 'applications/transactions/editfield/PhabricatorEpochEditField.php', 'PhabricatorEpochExportField' => 'infrastructure/export/field/PhabricatorEpochExportField.php', 'PhabricatorEvent' => 'infrastructure/events/PhabricatorEvent.php', 'PhabricatorEventEngine' => 'infrastructure/events/PhabricatorEventEngine.php', 'PhabricatorEventListener' => 'infrastructure/events/PhabricatorEventListener.php', 'PhabricatorEventType' => 'infrastructure/events/constant/PhabricatorEventType.php', 'PhabricatorExampleEventListener' => 'infrastructure/events/PhabricatorExampleEventListener.php', 'PhabricatorExcelExportFormat' => 'infrastructure/export/format/PhabricatorExcelExportFormat.php', 'PhabricatorExecFutureFileUploadSource' => 'applications/files/uploadsource/PhabricatorExecFutureFileUploadSource.php', 'PhabricatorExportEngine' => 'infrastructure/export/engine/PhabricatorExportEngine.php', 'PhabricatorExportEngineBulkJobType' => 'infrastructure/export/engine/PhabricatorExportEngineBulkJobType.php', 'PhabricatorExportEngineExtension' => 'infrastructure/export/engine/PhabricatorExportEngineExtension.php', 'PhabricatorExportField' => 'infrastructure/export/field/PhabricatorExportField.php', 'PhabricatorExportFormat' => 'infrastructure/export/format/PhabricatorExportFormat.php', 'PhabricatorExportFormatSetting' => 'infrastructure/export/engine/PhabricatorExportFormatSetting.php', 'PhabricatorExtendedPolicyInterface' => 'applications/policy/interface/PhabricatorExtendedPolicyInterface.php', 'PhabricatorExtendingPhabricatorConfigOptions' => 'applications/config/option/PhabricatorExtendingPhabricatorConfigOptions.php', 'PhabricatorExtensionsSetupCheck' => 'applications/config/check/PhabricatorExtensionsSetupCheck.php', 'PhabricatorExternalAccount' => 'applications/people/storage/PhabricatorExternalAccount.php', 'PhabricatorExternalAccountQuery' => 'applications/auth/query/PhabricatorExternalAccountQuery.php', 'PhabricatorExternalAccountsSettingsPanel' => 'applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php', 'PhabricatorExtraConfigSetupCheck' => 'applications/config/check/PhabricatorExtraConfigSetupCheck.php', 'PhabricatorFacebookAuthProvider' => 'applications/auth/provider/PhabricatorFacebookAuthProvider.php', 'PhabricatorFact' => 'applications/fact/fact/PhabricatorFact.php', 'PhabricatorFactAggregate' => 'applications/fact/storage/PhabricatorFactAggregate.php', 'PhabricatorFactApplication' => 'applications/fact/application/PhabricatorFactApplication.php', 'PhabricatorFactChartController' => 'applications/fact/controller/PhabricatorFactChartController.php', 'PhabricatorFactController' => 'applications/fact/controller/PhabricatorFactController.php', 'PhabricatorFactCursor' => 'applications/fact/storage/PhabricatorFactCursor.php', 'PhabricatorFactDAO' => 'applications/fact/storage/PhabricatorFactDAO.php', 'PhabricatorFactDaemon' => 'applications/fact/daemon/PhabricatorFactDaemon.php', 'PhabricatorFactDatapointQuery' => 'applications/fact/query/PhabricatorFactDatapointQuery.php', 'PhabricatorFactDimension' => 'applications/fact/storage/PhabricatorFactDimension.php', 'PhabricatorFactEngine' => 'applications/fact/engine/PhabricatorFactEngine.php', 'PhabricatorFactEngineTestCase' => 'applications/fact/engine/__tests__/PhabricatorFactEngineTestCase.php', 'PhabricatorFactHomeController' => 'applications/fact/controller/PhabricatorFactHomeController.php', 'PhabricatorFactIntDatapoint' => 'applications/fact/storage/PhabricatorFactIntDatapoint.php', 'PhabricatorFactKeyDimension' => 'applications/fact/storage/PhabricatorFactKeyDimension.php', 'PhabricatorFactManagementAnalyzeWorkflow' => 'applications/fact/management/PhabricatorFactManagementAnalyzeWorkflow.php', 'PhabricatorFactManagementCursorsWorkflow' => 'applications/fact/management/PhabricatorFactManagementCursorsWorkflow.php', 'PhabricatorFactManagementDestroyWorkflow' => 'applications/fact/management/PhabricatorFactManagementDestroyWorkflow.php', 'PhabricatorFactManagementListWorkflow' => 'applications/fact/management/PhabricatorFactManagementListWorkflow.php', 'PhabricatorFactManagementWorkflow' => 'applications/fact/management/PhabricatorFactManagementWorkflow.php', 'PhabricatorFactObjectController' => 'applications/fact/controller/PhabricatorFactObjectController.php', 'PhabricatorFactObjectDimension' => 'applications/fact/storage/PhabricatorFactObjectDimension.php', 'PhabricatorFactRaw' => 'applications/fact/storage/PhabricatorFactRaw.php', 'PhabricatorFactUpdateIterator' => 'applications/fact/extract/PhabricatorFactUpdateIterator.php', 'PhabricatorFaviconRef' => 'applications/files/favicon/PhabricatorFaviconRef.php', 'PhabricatorFaviconRefQuery' => 'applications/files/favicon/PhabricatorFaviconRefQuery.php', 'PhabricatorFavoritesApplication' => 'applications/favorites/application/PhabricatorFavoritesApplication.php', 'PhabricatorFavoritesController' => 'applications/favorites/controller/PhabricatorFavoritesController.php', 'PhabricatorFavoritesMainMenuBarExtension' => 'applications/favorites/engineextension/PhabricatorFavoritesMainMenuBarExtension.php', 'PhabricatorFavoritesMenuItemController' => 'applications/favorites/controller/PhabricatorFavoritesMenuItemController.php', 'PhabricatorFavoritesProfileMenuEngine' => 'applications/favorites/engine/PhabricatorFavoritesProfileMenuEngine.php', 'PhabricatorFaxContentSource' => 'infrastructure/contentsource/PhabricatorFaxContentSource.php', 'PhabricatorFeedApplication' => 'applications/feed/application/PhabricatorFeedApplication.php', 'PhabricatorFeedBuilder' => 'applications/feed/builder/PhabricatorFeedBuilder.php', 'PhabricatorFeedConfigOptions' => 'applications/feed/config/PhabricatorFeedConfigOptions.php', 'PhabricatorFeedController' => 'applications/feed/controller/PhabricatorFeedController.php', 'PhabricatorFeedDAO' => 'applications/feed/storage/PhabricatorFeedDAO.php', 'PhabricatorFeedDetailController' => 'applications/feed/controller/PhabricatorFeedDetailController.php', 'PhabricatorFeedListController' => 'applications/feed/controller/PhabricatorFeedListController.php', 'PhabricatorFeedManagementRepublishWorkflow' => 'applications/feed/management/PhabricatorFeedManagementRepublishWorkflow.php', 'PhabricatorFeedManagementWorkflow' => 'applications/feed/management/PhabricatorFeedManagementWorkflow.php', 'PhabricatorFeedQuery' => 'applications/feed/query/PhabricatorFeedQuery.php', 'PhabricatorFeedSearchEngine' => 'applications/feed/query/PhabricatorFeedSearchEngine.php', 'PhabricatorFeedStory' => 'applications/feed/story/PhabricatorFeedStory.php', 'PhabricatorFeedStoryData' => 'applications/feed/storage/PhabricatorFeedStoryData.php', 'PhabricatorFeedStoryNotification' => 'applications/notification/storage/PhabricatorFeedStoryNotification.php', 'PhabricatorFeedStoryPublisher' => 'applications/feed/PhabricatorFeedStoryPublisher.php', 'PhabricatorFeedStoryReference' => 'applications/feed/storage/PhabricatorFeedStoryReference.php', 'PhabricatorFerretEngine' => 'applications/search/ferret/PhabricatorFerretEngine.php', 'PhabricatorFerretEngineTestCase' => 'applications/search/ferret/__tests__/PhabricatorFerretEngineTestCase.php', 'PhabricatorFerretFulltextEngineExtension' => 'applications/search/engineextension/PhabricatorFerretFulltextEngineExtension.php', 'PhabricatorFerretFulltextStorageEngine' => 'applications/search/fulltextstorage/PhabricatorFerretFulltextStorageEngine.php', 'PhabricatorFerretInterface' => 'applications/search/ferret/PhabricatorFerretInterface.php', 'PhabricatorFerretMetadata' => 'applications/search/ferret/PhabricatorFerretMetadata.php', 'PhabricatorFerretSearchEngineExtension' => 'applications/search/engineextension/PhabricatorFerretSearchEngineExtension.php', 'PhabricatorFile' => 'applications/files/storage/PhabricatorFile.php', 'PhabricatorFileAES256StorageFormat' => 'applications/files/format/PhabricatorFileAES256StorageFormat.php', 'PhabricatorFileBundleLoader' => 'applications/files/query/PhabricatorFileBundleLoader.php', 'PhabricatorFileChunk' => 'applications/files/storage/PhabricatorFileChunk.php', 'PhabricatorFileChunkIterator' => 'applications/files/engine/PhabricatorFileChunkIterator.php', 'PhabricatorFileChunkQuery' => 'applications/files/query/PhabricatorFileChunkQuery.php', 'PhabricatorFileComposeController' => 'applications/files/controller/PhabricatorFileComposeController.php', 'PhabricatorFileController' => 'applications/files/controller/PhabricatorFileController.php', 'PhabricatorFileDAO' => 'applications/files/storage/PhabricatorFileDAO.php', 'PhabricatorFileDataController' => 'applications/files/controller/PhabricatorFileDataController.php', 'PhabricatorFileDeleteController' => 'applications/files/controller/PhabricatorFileDeleteController.php', 'PhabricatorFileDeleteTransaction' => 'applications/files/xaction/PhabricatorFileDeleteTransaction.php', 'PhabricatorFileDocumentController' => 'applications/files/controller/PhabricatorFileDocumentController.php', 'PhabricatorFileDocumentRenderingEngine' => 'applications/files/document/render/PhabricatorFileDocumentRenderingEngine.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', 'PhabricatorFileExternalRequestGarbageCollector' => 'applications/files/garbagecollector/PhabricatorFileExternalRequestGarbageCollector.php', 'PhabricatorFileFilePHIDType' => 'applications/files/phid/PhabricatorFileFilePHIDType.php', 'PhabricatorFileHasObjectEdgeType' => 'applications/files/edge/PhabricatorFileHasObjectEdgeType.php', 'PhabricatorFileIconSetSelectController' => 'applications/files/controller/PhabricatorFileIconSetSelectController.php', 'PhabricatorFileImageMacro' => 'applications/macro/storage/PhabricatorFileImageMacro.php', 'PhabricatorFileImageProxyController' => 'applications/files/controller/PhabricatorFileImageProxyController.php', 'PhabricatorFileImageTransform' => 'applications/files/transform/PhabricatorFileImageTransform.php', 'PhabricatorFileIntegrityException' => 'applications/files/exception/PhabricatorFileIntegrityException.php', 'PhabricatorFileLightboxController' => 'applications/files/controller/PhabricatorFileLightboxController.php', 'PhabricatorFileLinkView' => 'view/layout/PhabricatorFileLinkView.php', 'PhabricatorFileListController' => 'applications/files/controller/PhabricatorFileListController.php', 'PhabricatorFileNameNgrams' => 'applications/files/storage/PhabricatorFileNameNgrams.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', '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', 'PhabricatorFileStorageEngine' => 'applications/files/engine/PhabricatorFileStorageEngine.php', 'PhabricatorFileStorageEngineTestCase' => 'applications/files/engine/__tests__/PhabricatorFileStorageEngineTestCase.php', 'PhabricatorFileStorageFormat' => 'applications/files/format/PhabricatorFileStorageFormat.php', 'PhabricatorFileStorageFormatTestCase' => 'applications/files/format/__tests__/PhabricatorFileStorageFormatTestCase.php', 'PhabricatorFileTemporaryGarbageCollector' => 'applications/files/garbagecollector/PhabricatorFileTemporaryGarbageCollector.php', 'PhabricatorFileTestCase' => 'applications/files/storage/__tests__/PhabricatorFileTestCase.php', 'PhabricatorFileTestDataGenerator' => 'applications/files/lipsum/PhabricatorFileTestDataGenerator.php', 'PhabricatorFileThumbnailTransform' => 'applications/files/transform/PhabricatorFileThumbnailTransform.php', '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', 'PhabricatorFileTransformTestCase' => 'applications/files/transform/__tests__/PhabricatorFileTransformTestCase.php', 'PhabricatorFileUploadController' => 'applications/files/controller/PhabricatorFileUploadController.php', 'PhabricatorFileUploadDialogController' => 'applications/files/controller/PhabricatorFileUploadDialogController.php', 'PhabricatorFileUploadException' => 'applications/files/exception/PhabricatorFileUploadException.php', 'PhabricatorFileUploadSource' => 'applications/files/uploadsource/PhabricatorFileUploadSource.php', 'PhabricatorFileUploadSourceByteLimitException' => 'applications/files/uploadsource/PhabricatorFileUploadSourceByteLimitException.php', 'PhabricatorFileViewController' => 'applications/files/controller/PhabricatorFileViewController.php', 'PhabricatorFileinfoSetupCheck' => 'applications/config/check/PhabricatorFileinfoSetupCheck.php', 'PhabricatorFilesApplication' => 'applications/files/application/PhabricatorFilesApplication.php', 'PhabricatorFilesApplicationStorageEnginePanel' => 'applications/files/applicationpanel/PhabricatorFilesApplicationStorageEnginePanel.php', 'PhabricatorFilesBuiltinFile' => 'applications/files/builtin/PhabricatorFilesBuiltinFile.php', 'PhabricatorFilesComposeAvatarBuiltinFile' => 'applications/files/builtin/PhabricatorFilesComposeAvatarBuiltinFile.php', 'PhabricatorFilesComposeIconBuiltinFile' => 'applications/files/builtin/PhabricatorFilesComposeIconBuiltinFile.php', 'PhabricatorFilesConfigOptions' => 'applications/files/config/PhabricatorFilesConfigOptions.php', 'PhabricatorFilesManagementCatWorkflow' => 'applications/files/management/PhabricatorFilesManagementCatWorkflow.php', 'PhabricatorFilesManagementCompactWorkflow' => 'applications/files/management/PhabricatorFilesManagementCompactWorkflow.php', 'PhabricatorFilesManagementCycleWorkflow' => 'applications/files/management/PhabricatorFilesManagementCycleWorkflow.php', 'PhabricatorFilesManagementEncodeWorkflow' => 'applications/files/management/PhabricatorFilesManagementEncodeWorkflow.php', 'PhabricatorFilesManagementEnginesWorkflow' => 'applications/files/management/PhabricatorFilesManagementEnginesWorkflow.php', 'PhabricatorFilesManagementGenerateKeyWorkflow' => 'applications/files/management/PhabricatorFilesManagementGenerateKeyWorkflow.php', 'PhabricatorFilesManagementIntegrityWorkflow' => 'applications/files/management/PhabricatorFilesManagementIntegrityWorkflow.php', 'PhabricatorFilesManagementMigrateWorkflow' => 'applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php', 'PhabricatorFilesManagementRebuildWorkflow' => 'applications/files/management/PhabricatorFilesManagementRebuildWorkflow.php', 'PhabricatorFilesManagementWorkflow' => 'applications/files/management/PhabricatorFilesManagementWorkflow.php', 'PhabricatorFilesOnDiskBuiltinFile' => 'applications/files/builtin/PhabricatorFilesOnDiskBuiltinFile.php', 'PhabricatorFilesOutboundRequestAction' => 'applications/files/action/PhabricatorFilesOutboundRequestAction.php', 'PhabricatorFiletreeVisibleSetting' => 'applications/settings/setting/PhabricatorFiletreeVisibleSetting.php', 'PhabricatorFiletreeWidthSetting' => 'applications/settings/setting/PhabricatorFiletreeWidthSetting.php', 'PhabricatorFlag' => 'applications/flag/storage/PhabricatorFlag.php', 'PhabricatorFlagAddFlagHeraldAction' => 'applications/flag/herald/PhabricatorFlagAddFlagHeraldAction.php', 'PhabricatorFlagColor' => 'applications/flag/constants/PhabricatorFlagColor.php', 'PhabricatorFlagConstants' => 'applications/flag/constants/PhabricatorFlagConstants.php', 'PhabricatorFlagController' => 'applications/flag/controller/PhabricatorFlagController.php', 'PhabricatorFlagDAO' => 'applications/flag/storage/PhabricatorFlagDAO.php', 'PhabricatorFlagDeleteController' => 'applications/flag/controller/PhabricatorFlagDeleteController.php', 'PhabricatorFlagDestructionEngineExtension' => 'applications/flag/engineextension/PhabricatorFlagDestructionEngineExtension.php', 'PhabricatorFlagEditController' => 'applications/flag/controller/PhabricatorFlagEditController.php', 'PhabricatorFlagListController' => 'applications/flag/controller/PhabricatorFlagListController.php', 'PhabricatorFlagQuery' => 'applications/flag/query/PhabricatorFlagQuery.php', 'PhabricatorFlagSearchEngine' => 'applications/flag/query/PhabricatorFlagSearchEngine.php', 'PhabricatorFlagSelectControl' => 'applications/flag/view/PhabricatorFlagSelectControl.php', 'PhabricatorFlaggableInterface' => 'applications/flag/interface/PhabricatorFlaggableInterface.php', 'PhabricatorFlagsApplication' => 'applications/flag/application/PhabricatorFlagsApplication.php', 'PhabricatorFlagsUIEventListener' => 'applications/flag/events/PhabricatorFlagsUIEventListener.php', 'PhabricatorFulltextEngine' => 'applications/search/index/PhabricatorFulltextEngine.php', 'PhabricatorFulltextEngineExtension' => 'applications/search/index/PhabricatorFulltextEngineExtension.php', 'PhabricatorFulltextEngineExtensionModule' => 'applications/search/index/PhabricatorFulltextEngineExtensionModule.php', 'PhabricatorFulltextIndexEngineExtension' => 'applications/search/engineextension/PhabricatorFulltextIndexEngineExtension.php', 'PhabricatorFulltextInterface' => 'applications/search/interface/PhabricatorFulltextInterface.php', 'PhabricatorFulltextResultSet' => 'applications/search/query/PhabricatorFulltextResultSet.php', 'PhabricatorFulltextStorageEngine' => 'applications/search/fulltextstorage/PhabricatorFulltextStorageEngine.php', 'PhabricatorFulltextToken' => 'applications/search/query/PhabricatorFulltextToken.php', 'PhabricatorFundApplication' => 'applications/fund/application/PhabricatorFundApplication.php', 'PhabricatorGDSetupCheck' => 'applications/config/check/PhabricatorGDSetupCheck.php', 'PhabricatorGarbageCollector' => 'infrastructure/daemon/garbagecollector/PhabricatorGarbageCollector.php', 'PhabricatorGarbageCollectorManagementCollectWorkflow' => 'infrastructure/daemon/garbagecollector/management/PhabricatorGarbageCollectorManagementCollectWorkflow.php', 'PhabricatorGarbageCollectorManagementCompactEdgesWorkflow' => 'infrastructure/daemon/garbagecollector/management/PhabricatorGarbageCollectorManagementCompactEdgesWorkflow.php', 'PhabricatorGarbageCollectorManagementSetPolicyWorkflow' => 'infrastructure/daemon/garbagecollector/management/PhabricatorGarbageCollectorManagementSetPolicyWorkflow.php', 'PhabricatorGarbageCollectorManagementWorkflow' => 'infrastructure/daemon/garbagecollector/management/PhabricatorGarbageCollectorManagementWorkflow.php', 'PhabricatorGeneralCachePurger' => 'applications/cache/purger/PhabricatorGeneralCachePurger.php', 'PhabricatorGestureUIExample' => 'applications/uiexample/examples/PhabricatorGestureUIExample.php', 'PhabricatorGitGraphStream' => 'applications/repository/daemon/PhabricatorGitGraphStream.php', 'PhabricatorGitHubAuthProvider' => 'applications/auth/provider/PhabricatorGitHubAuthProvider.php', 'PhabricatorGlobalLock' => 'infrastructure/util/PhabricatorGlobalLock.php', 'PhabricatorGlobalUploadTargetView' => 'applications/files/view/PhabricatorGlobalUploadTargetView.php', 'PhabricatorGoogleAuthProvider' => 'applications/auth/provider/PhabricatorGoogleAuthProvider.php', 'PhabricatorGuidanceContext' => 'applications/guides/guidance/PhabricatorGuidanceContext.php', 'PhabricatorGuidanceEngine' => 'applications/guides/guidance/PhabricatorGuidanceEngine.php', 'PhabricatorGuidanceEngineExtension' => 'applications/guides/guidance/PhabricatorGuidanceEngineExtension.php', 'PhabricatorGuidanceMessage' => 'applications/guides/guidance/PhabricatorGuidanceMessage.php', 'PhabricatorGuideApplication' => 'applications/guides/application/PhabricatorGuideApplication.php', 'PhabricatorGuideController' => 'applications/guides/controller/PhabricatorGuideController.php', 'PhabricatorGuideInstallModule' => 'applications/guides/module/PhabricatorGuideInstallModule.php', 'PhabricatorGuideItemView' => 'applications/guides/view/PhabricatorGuideItemView.php', 'PhabricatorGuideListView' => 'applications/guides/view/PhabricatorGuideListView.php', 'PhabricatorGuideModule' => 'applications/guides/module/PhabricatorGuideModule.php', 'PhabricatorGuideModuleController' => 'applications/guides/controller/PhabricatorGuideModuleController.php', 'PhabricatorGuideQuickStartModule' => 'applications/guides/module/PhabricatorGuideQuickStartModule.php', 'PhabricatorHMACTestCase' => 'infrastructure/util/__tests__/PhabricatorHMACTestCase.php', 'PhabricatorHTTPParameterTypeTableView' => 'applications/config/view/PhabricatorHTTPParameterTypeTableView.php', 'PhabricatorHandleList' => 'applications/phid/handle/pool/PhabricatorHandleList.php', 'PhabricatorHandleObjectSelectorDataView' => 'applications/phid/handle/view/PhabricatorHandleObjectSelectorDataView.php', 'PhabricatorHandlePool' => 'applications/phid/handle/pool/PhabricatorHandlePool.php', 'PhabricatorHandlePoolTestCase' => 'applications/phid/handle/pool/__tests__/PhabricatorHandlePoolTestCase.php', 'PhabricatorHandleQuery' => 'applications/phid/query/PhabricatorHandleQuery.php', 'PhabricatorHandleRemarkupRule' => 'applications/phid/remarkup/PhabricatorHandleRemarkupRule.php', 'PhabricatorHandlesEditField' => 'applications/transactions/editfield/PhabricatorHandlesEditField.php', 'PhabricatorHarbormasterApplication' => 'applications/harbormaster/application/PhabricatorHarbormasterApplication.php', 'PhabricatorHash' => 'infrastructure/util/PhabricatorHash.php', 'PhabricatorHashTestCase' => 'infrastructure/util/__tests__/PhabricatorHashTestCase.php', 'PhabricatorHelpApplication' => 'applications/help/application/PhabricatorHelpApplication.php', 'PhabricatorHelpController' => 'applications/help/controller/PhabricatorHelpController.php', 'PhabricatorHelpDocumentationController' => 'applications/help/controller/PhabricatorHelpDocumentationController.php', 'PhabricatorHelpEditorProtocolController' => 'applications/help/controller/PhabricatorHelpEditorProtocolController.php', 'PhabricatorHelpKeyboardShortcutController' => 'applications/help/controller/PhabricatorHelpKeyboardShortcutController.php', 'PhabricatorHeraldApplication' => 'applications/herald/application/PhabricatorHeraldApplication.php', 'PhabricatorHeraldContentSource' => 'applications/herald/contentsource/PhabricatorHeraldContentSource.php', 'PhabricatorHexdumpDocumentEngine' => 'applications/files/document/PhabricatorHexdumpDocumentEngine.php', 'PhabricatorHighSecurityRequestExceptionHandler' => 'aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php', 'PhabricatorHomeApplication' => 'applications/home/application/PhabricatorHomeApplication.php', 'PhabricatorHomeConstants' => 'applications/home/constants/PhabricatorHomeConstants.php', 'PhabricatorHomeController' => 'applications/home/controller/PhabricatorHomeController.php', 'PhabricatorHomeLauncherProfileMenuItem' => 'applications/home/menuitem/PhabricatorHomeLauncherProfileMenuItem.php', 'PhabricatorHomeMenuItemController' => 'applications/home/controller/PhabricatorHomeMenuItemController.php', 'PhabricatorHomeProfileMenuEngine' => 'applications/home/engine/PhabricatorHomeProfileMenuEngine.php', 'PhabricatorHomeProfileMenuItem' => 'applications/home/menuitem/PhabricatorHomeProfileMenuItem.php', 'PhabricatorHovercardEngineExtension' => 'applications/search/engineextension/PhabricatorHovercardEngineExtension.php', 'PhabricatorHovercardEngineExtensionModule' => 'applications/search/engineextension/PhabricatorHovercardEngineExtensionModule.php', 'PhabricatorIDExportField' => 'infrastructure/export/field/PhabricatorIDExportField.php', 'PhabricatorIDsSearchEngineExtension' => 'applications/search/engineextension/PhabricatorIDsSearchEngineExtension.php', 'PhabricatorIDsSearchField' => 'applications/search/field/PhabricatorIDsSearchField.php', 'PhabricatorIconDatasource' => 'applications/files/typeahead/PhabricatorIconDatasource.php', 'PhabricatorIconRemarkupRule' => 'applications/macro/markup/PhabricatorIconRemarkupRule.php', 'PhabricatorIconSet' => 'applications/files/iconset/PhabricatorIconSet.php', 'PhabricatorIconSetEditField' => 'applications/transactions/editfield/PhabricatorIconSetEditField.php', 'PhabricatorIconSetIcon' => 'applications/files/iconset/PhabricatorIconSetIcon.php', 'PhabricatorImageDocumentEngine' => 'applications/files/document/PhabricatorImageDocumentEngine.php', 'PhabricatorImageMacroRemarkupRule' => 'applications/macro/markup/PhabricatorImageMacroRemarkupRule.php', 'PhabricatorImageRemarkupRule' => 'applications/files/markup/PhabricatorImageRemarkupRule.php', 'PhabricatorImageTransformer' => 'applications/files/PhabricatorImageTransformer.php', 'PhabricatorImagemagickSetupCheck' => 'applications/config/check/PhabricatorImagemagickSetupCheck.php', 'PhabricatorInFlightErrorView' => 'applications/config/view/PhabricatorInFlightErrorView.php', 'PhabricatorIndexEngine' => 'applications/search/index/PhabricatorIndexEngine.php', 'PhabricatorIndexEngineExtension' => 'applications/search/index/PhabricatorIndexEngineExtension.php', 'PhabricatorIndexEngineExtensionModule' => 'applications/search/index/PhabricatorIndexEngineExtensionModule.php', 'PhabricatorIndexableInterface' => 'applications/search/interface/PhabricatorIndexableInterface.php', 'PhabricatorInfrastructureTestCase' => '__tests__/PhabricatorInfrastructureTestCase.php', 'PhabricatorInlineCommentController' => 'infrastructure/diff/PhabricatorInlineCommentController.php', 'PhabricatorInlineCommentInterface' => 'infrastructure/diff/interface/PhabricatorInlineCommentInterface.php', 'PhabricatorInlineCommentPreviewController' => 'infrastructure/diff/PhabricatorInlineCommentPreviewController.php', 'PhabricatorInlineSummaryView' => 'infrastructure/diff/view/PhabricatorInlineSummaryView.php', 'PhabricatorInstructionsEditField' => 'applications/transactions/editfield/PhabricatorInstructionsEditField.php', 'PhabricatorIntConfigType' => 'applications/config/type/PhabricatorIntConfigType.php', 'PhabricatorIntEditField' => 'applications/transactions/editfield/PhabricatorIntEditField.php', 'PhabricatorIntExportField' => 'infrastructure/export/field/PhabricatorIntExportField.php', 'PhabricatorInternalSetting' => 'applications/settings/setting/PhabricatorInternalSetting.php', 'PhabricatorInternationalizationManagementExtractWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementExtractWorkflow.php', 'PhabricatorInternationalizationManagementWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementWorkflow.php', 'PhabricatorInvalidConfigSetupCheck' => 'applications/config/check/PhabricatorInvalidConfigSetupCheck.php', 'PhabricatorIteratedMD5PasswordHasher' => 'infrastructure/util/password/PhabricatorIteratedMD5PasswordHasher.php', 'PhabricatorIteratedMD5PasswordHasherTestCase' => 'infrastructure/util/password/__tests__/PhabricatorIteratedMD5PasswordHasherTestCase.php', 'PhabricatorIteratorFileUploadSource' => 'applications/files/uploadsource/PhabricatorIteratorFileUploadSource.php', 'PhabricatorJIRAAuthProvider' => 'applications/auth/provider/PhabricatorJIRAAuthProvider.php', 'PhabricatorJSONConfigType' => 'applications/config/type/PhabricatorJSONConfigType.php', 'PhabricatorJSONDocumentEngine' => 'applications/files/document/PhabricatorJSONDocumentEngine.php', 'PhabricatorJSONExportFormat' => 'infrastructure/export/format/PhabricatorJSONExportFormat.php', 'PhabricatorJavelinLinter' => 'infrastructure/lint/linter/PhabricatorJavelinLinter.php', 'PhabricatorJiraIssueHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorJiraIssueHasObjectEdgeType.php', 'PhabricatorJupyterDocumentEngine' => 'applications/files/document/PhabricatorJupyterDocumentEngine.php', 'PhabricatorKeyValueDatabaseCache' => 'applications/cache/PhabricatorKeyValueDatabaseCache.php', 'PhabricatorKeyValueSerializingCacheProxy' => 'applications/cache/PhabricatorKeyValueSerializingCacheProxy.php', 'PhabricatorKeyboardRemarkupRule' => 'infrastructure/markup/rule/PhabricatorKeyboardRemarkupRule.php', 'PhabricatorKeyring' => 'applications/files/keyring/PhabricatorKeyring.php', 'PhabricatorKeyringConfigOptionType' => 'applications/files/keyring/PhabricatorKeyringConfigOptionType.php', 'PhabricatorLDAPAuthProvider' => 'applications/auth/provider/PhabricatorLDAPAuthProvider.php', 'PhabricatorLabelProfileMenuItem' => 'applications/search/menuitem/PhabricatorLabelProfileMenuItem.php', 'PhabricatorLegalpadApplication' => 'applications/legalpad/application/PhabricatorLegalpadApplication.php', 'PhabricatorLegalpadConfigOptions' => 'applications/legalpad/config/PhabricatorLegalpadConfigOptions.php', 'PhabricatorLegalpadDocumentPHIDType' => 'applications/legalpad/phid/PhabricatorLegalpadDocumentPHIDType.php', 'PhabricatorLegalpadSignaturePolicyRule' => 'applications/legalpad/policyrule/PhabricatorLegalpadSignaturePolicyRule.php', 'PhabricatorLibraryTestCase' => '__tests__/PhabricatorLibraryTestCase.php', 'PhabricatorLinkProfileMenuItem' => 'applications/search/menuitem/PhabricatorLinkProfileMenuItem.php', 'PhabricatorLipsumArtist' => 'applications/lipsum/image/PhabricatorLipsumArtist.php', 'PhabricatorLipsumContentSource' => 'infrastructure/contentsource/PhabricatorLipsumContentSource.php', 'PhabricatorLipsumGenerateWorkflow' => 'applications/lipsum/management/PhabricatorLipsumGenerateWorkflow.php', 'PhabricatorLipsumManagementWorkflow' => 'applications/lipsum/management/PhabricatorLipsumManagementWorkflow.php', 'PhabricatorLipsumMondrianArtist' => 'applications/lipsum/image/PhabricatorLipsumMondrianArtist.php', 'PhabricatorLiskDAO' => 'infrastructure/storage/lisk/PhabricatorLiskDAO.php', 'PhabricatorLiskExportEngineExtension' => 'infrastructure/export/engine/PhabricatorLiskExportEngineExtension.php', 'PhabricatorLiskFulltextEngineExtension' => 'applications/search/engineextension/PhabricatorLiskFulltextEngineExtension.php', 'PhabricatorLiskSearchEngineExtension' => 'applications/search/engineextension/PhabricatorLiskSearchEngineExtension.php', 'PhabricatorLiskSerializer' => 'infrastructure/storage/lisk/PhabricatorLiskSerializer.php', 'PhabricatorListExportField' => 'infrastructure/export/field/PhabricatorListExportField.php', 'PhabricatorLocalDiskFileStorageEngine' => 'applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php', 'PhabricatorLocalTimeTestCase' => 'view/__tests__/PhabricatorLocalTimeTestCase.php', 'PhabricatorLocaleScopeGuard' => 'infrastructure/internationalization/scope/PhabricatorLocaleScopeGuard.php', 'PhabricatorLocaleScopeGuardTestCase' => 'infrastructure/internationalization/scope/__tests__/PhabricatorLocaleScopeGuardTestCase.php', 'PhabricatorLockLogManagementWorkflow' => 'applications/daemon/management/PhabricatorLockLogManagementWorkflow.php', 'PhabricatorLockManagementWorkflow' => 'applications/daemon/management/PhabricatorLockManagementWorkflow.php', 'PhabricatorLogTriggerAction' => 'infrastructure/daemon/workers/action/PhabricatorLogTriggerAction.php', 'PhabricatorLogoutController' => 'applications/auth/controller/PhabricatorLogoutController.php', 'PhabricatorLunarPhasePolicyRule' => 'applications/policy/rule/PhabricatorLunarPhasePolicyRule.php', 'PhabricatorMacroApplication' => 'applications/macro/application/PhabricatorMacroApplication.php', 'PhabricatorMacroAudioBehaviorTransaction' => 'applications/macro/xaction/PhabricatorMacroAudioBehaviorTransaction.php', 'PhabricatorMacroAudioController' => 'applications/macro/controller/PhabricatorMacroAudioController.php', 'PhabricatorMacroAudioTransaction' => 'applications/macro/xaction/PhabricatorMacroAudioTransaction.php', 'PhabricatorMacroConfigOptions' => 'applications/macro/config/PhabricatorMacroConfigOptions.php', 'PhabricatorMacroController' => 'applications/macro/controller/PhabricatorMacroController.php', 'PhabricatorMacroDatasource' => 'applications/macro/typeahead/PhabricatorMacroDatasource.php', 'PhabricatorMacroDisableController' => 'applications/macro/controller/PhabricatorMacroDisableController.php', 'PhabricatorMacroDisabledTransaction' => 'applications/macro/xaction/PhabricatorMacroDisabledTransaction.php', 'PhabricatorMacroEditController' => 'applications/macro/controller/PhabricatorMacroEditController.php', 'PhabricatorMacroEditEngine' => 'applications/macro/editor/PhabricatorMacroEditEngine.php', 'PhabricatorMacroEditor' => 'applications/macro/editor/PhabricatorMacroEditor.php', 'PhabricatorMacroFileTransaction' => 'applications/macro/xaction/PhabricatorMacroFileTransaction.php', 'PhabricatorMacroListController' => 'applications/macro/controller/PhabricatorMacroListController.php', 'PhabricatorMacroMacroPHIDType' => 'applications/macro/phid/PhabricatorMacroMacroPHIDType.php', 'PhabricatorMacroMailReceiver' => 'applications/macro/mail/PhabricatorMacroMailReceiver.php', 'PhabricatorMacroManageCapability' => 'applications/macro/capability/PhabricatorMacroManageCapability.php', 'PhabricatorMacroMemeController' => 'applications/macro/controller/PhabricatorMacroMemeController.php', 'PhabricatorMacroMemeDialogController' => 'applications/macro/controller/PhabricatorMacroMemeDialogController.php', 'PhabricatorMacroNameTransaction' => 'applications/macro/xaction/PhabricatorMacroNameTransaction.php', 'PhabricatorMacroQuery' => 'applications/macro/query/PhabricatorMacroQuery.php', 'PhabricatorMacroReplyHandler' => 'applications/macro/mail/PhabricatorMacroReplyHandler.php', 'PhabricatorMacroSearchEngine' => 'applications/macro/query/PhabricatorMacroSearchEngine.php', 'PhabricatorMacroTestCase' => 'applications/macro/xaction/__tests__/PhabricatorMacroTestCase.php', 'PhabricatorMacroTransaction' => 'applications/macro/storage/PhabricatorMacroTransaction.php', 'PhabricatorMacroTransactionComment' => 'applications/macro/storage/PhabricatorMacroTransactionComment.php', 'PhabricatorMacroTransactionQuery' => 'applications/macro/query/PhabricatorMacroTransactionQuery.php', 'PhabricatorMacroTransactionType' => 'applications/macro/xaction/PhabricatorMacroTransactionType.php', 'PhabricatorMacroViewController' => 'applications/macro/controller/PhabricatorMacroViewController.php', 'PhabricatorMailConfigTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMailConfigTestCase.php', 'PhabricatorMailEmailHeraldField' => 'applications/metamta/herald/PhabricatorMailEmailHeraldField.php', 'PhabricatorMailEmailHeraldFieldGroup' => 'applications/metamta/herald/PhabricatorMailEmailHeraldFieldGroup.php', 'PhabricatorMailEmailSubjectHeraldField' => 'applications/metamta/herald/PhabricatorMailEmailSubjectHeraldField.php', 'PhabricatorMailEngineExtension' => 'applications/metamta/engine/PhabricatorMailEngineExtension.php', 'PhabricatorMailImplementationAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationAdapter.php', 'PhabricatorMailImplementationAmazonSESAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php', 'PhabricatorMailImplementationMailgunAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php', 'PhabricatorMailImplementationPHPMailerAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php', 'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php', 'PhabricatorMailImplementationPostmarkAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationPostmarkAdapter.php', 'PhabricatorMailImplementationSendGridAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php', 'PhabricatorMailImplementationTestAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php', 'PhabricatorMailManagementListInboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementListInboundWorkflow.php', 'PhabricatorMailManagementListOutboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php', 'PhabricatorMailManagementReceiveTestWorkflow' => 'applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php', 'PhabricatorMailManagementResendWorkflow' => 'applications/metamta/management/PhabricatorMailManagementResendWorkflow.php', 'PhabricatorMailManagementSendTestWorkflow' => 'applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php', 'PhabricatorMailManagementShowInboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementShowInboundWorkflow.php', 'PhabricatorMailManagementShowOutboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php', 'PhabricatorMailManagementUnverifyWorkflow' => 'applications/metamta/management/PhabricatorMailManagementUnverifyWorkflow.php', 'PhabricatorMailManagementVolumeWorkflow' => 'applications/metamta/management/PhabricatorMailManagementVolumeWorkflow.php', 'PhabricatorMailManagementWorkflow' => 'applications/metamta/management/PhabricatorMailManagementWorkflow.php', 'PhabricatorMailMustEncryptHeraldAction' => 'applications/metamta/herald/PhabricatorMailMustEncryptHeraldAction.php', 'PhabricatorMailOutboundMailHeraldAdapter' => 'applications/metamta/herald/PhabricatorMailOutboundMailHeraldAdapter.php', 'PhabricatorMailOutboundRoutingHeraldAction' => 'applications/metamta/herald/PhabricatorMailOutboundRoutingHeraldAction.php', 'PhabricatorMailOutboundRoutingSelfEmailHeraldAction' => 'applications/metamta/herald/PhabricatorMailOutboundRoutingSelfEmailHeraldAction.php', 'PhabricatorMailOutboundRoutingSelfNotificationHeraldAction' => 'applications/metamta/herald/PhabricatorMailOutboundRoutingSelfNotificationHeraldAction.php', 'PhabricatorMailOutboundStatus' => 'applications/metamta/constants/PhabricatorMailOutboundStatus.php', 'PhabricatorMailPropertiesDestructionEngineExtension' => 'applications/metamta/engineextension/PhabricatorMailPropertiesDestructionEngineExtension.php', 'PhabricatorMailReceiver' => 'applications/metamta/receiver/PhabricatorMailReceiver.php', 'PhabricatorMailReceiverTestCase' => 'applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php', 'PhabricatorMailReplyHandler' => 'applications/metamta/replyhandler/PhabricatorMailReplyHandler.php', 'PhabricatorMailRoutingRule' => 'applications/metamta/constants/PhabricatorMailRoutingRule.php', 'PhabricatorMailSetupCheck' => 'applications/config/check/PhabricatorMailSetupCheck.php', 'PhabricatorMailStamp' => 'applications/metamta/stamp/PhabricatorMailStamp.php', 'PhabricatorMailTarget' => 'applications/metamta/replyhandler/PhabricatorMailTarget.php', 'PhabricatorMailgunConfigOptions' => 'applications/config/option/PhabricatorMailgunConfigOptions.php', 'PhabricatorMainMenuBarExtension' => 'view/page/menu/PhabricatorMainMenuBarExtension.php', 'PhabricatorMainMenuSearchView' => 'view/page/menu/PhabricatorMainMenuSearchView.php', 'PhabricatorMainMenuView' => 'view/page/menu/PhabricatorMainMenuView.php', 'PhabricatorManageProfileMenuItem' => 'applications/search/menuitem/PhabricatorManageProfileMenuItem.php', 'PhabricatorManagementWorkflow' => 'infrastructure/management/PhabricatorManagementWorkflow.php', 'PhabricatorManiphestApplication' => 'applications/maniphest/application/PhabricatorManiphestApplication.php', 'PhabricatorManiphestConfigOptions' => 'applications/maniphest/config/PhabricatorManiphestConfigOptions.php', 'PhabricatorManiphestTaskFactEngine' => 'applications/fact/engine/PhabricatorManiphestTaskFactEngine.php', 'PhabricatorManiphestTaskTestDataGenerator' => 'applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php', 'PhabricatorManualActivitySetupCheck' => 'applications/config/check/PhabricatorManualActivitySetupCheck.php', 'PhabricatorMarkupCache' => 'applications/cache/storage/PhabricatorMarkupCache.php', 'PhabricatorMarkupEngine' => 'infrastructure/markup/PhabricatorMarkupEngine.php', 'PhabricatorMarkupEngineTestCase' => 'infrastructure/markup/__tests__/PhabricatorMarkupEngineTestCase.php', 'PhabricatorMarkupInterface' => 'infrastructure/markup/PhabricatorMarkupInterface.php', 'PhabricatorMarkupOneOff' => 'infrastructure/markup/PhabricatorMarkupOneOff.php', 'PhabricatorMarkupPreviewController' => 'infrastructure/markup/PhabricatorMarkupPreviewController.php', 'PhabricatorMemeEngine' => 'applications/macro/engine/PhabricatorMemeEngine.php', 'PhabricatorMemeRemarkupRule' => 'applications/macro/markup/PhabricatorMemeRemarkupRule.php', 'PhabricatorMentionRemarkupRule' => 'applications/people/markup/PhabricatorMentionRemarkupRule.php', 'PhabricatorMentionableInterface' => 'applications/transactions/interface/PhabricatorMentionableInterface.php', 'PhabricatorMercurialGraphStream' => 'applications/repository/daemon/PhabricatorMercurialGraphStream.php', 'PhabricatorMetaMTAActor' => 'applications/metamta/query/PhabricatorMetaMTAActor.php', 'PhabricatorMetaMTAActorQuery' => 'applications/metamta/query/PhabricatorMetaMTAActorQuery.php', 'PhabricatorMetaMTAApplication' => 'applications/metamta/application/PhabricatorMetaMTAApplication.php', 'PhabricatorMetaMTAApplicationEmail' => 'applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php', 'PhabricatorMetaMTAApplicationEmailDatasource' => 'applications/metamta/typeahead/PhabricatorMetaMTAApplicationEmailDatasource.php', 'PhabricatorMetaMTAApplicationEmailEditor' => 'applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php', 'PhabricatorMetaMTAApplicationEmailHeraldField' => 'applications/metamta/herald/PhabricatorMetaMTAApplicationEmailHeraldField.php', 'PhabricatorMetaMTAApplicationEmailPHIDType' => 'applications/phid/PhabricatorMetaMTAApplicationEmailPHIDType.php', 'PhabricatorMetaMTAApplicationEmailPanel' => 'applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php', 'PhabricatorMetaMTAApplicationEmailQuery' => 'applications/metamta/query/PhabricatorMetaMTAApplicationEmailQuery.php', 'PhabricatorMetaMTAApplicationEmailTransaction' => 'applications/metamta/storage/PhabricatorMetaMTAApplicationEmailTransaction.php', 'PhabricatorMetaMTAApplicationEmailTransactionQuery' => 'applications/metamta/query/PhabricatorMetaMTAApplicationEmailTransactionQuery.php', 'PhabricatorMetaMTAAttachment' => 'applications/metamta/storage/PhabricatorMetaMTAAttachment.php', 'PhabricatorMetaMTAConfigOptions' => 'applications/config/option/PhabricatorMetaMTAConfigOptions.php', 'PhabricatorMetaMTAController' => 'applications/metamta/controller/PhabricatorMetaMTAController.php', 'PhabricatorMetaMTADAO' => 'applications/metamta/storage/PhabricatorMetaMTADAO.php', 'PhabricatorMetaMTAEmailBodyParser' => 'applications/metamta/parser/PhabricatorMetaMTAEmailBodyParser.php', 'PhabricatorMetaMTAEmailBodyParserTestCase' => 'applications/metamta/parser/__tests__/PhabricatorMetaMTAEmailBodyParserTestCase.php', 'PhabricatorMetaMTAEmailHeraldAction' => 'applications/metamta/herald/PhabricatorMetaMTAEmailHeraldAction.php', 'PhabricatorMetaMTAEmailOthersHeraldAction' => 'applications/metamta/herald/PhabricatorMetaMTAEmailOthersHeraldAction.php', 'PhabricatorMetaMTAEmailSelfHeraldAction' => 'applications/metamta/herald/PhabricatorMetaMTAEmailSelfHeraldAction.php', 'PhabricatorMetaMTAErrorMailAction' => 'applications/metamta/action/PhabricatorMetaMTAErrorMailAction.php', 'PhabricatorMetaMTAMail' => 'applications/metamta/storage/PhabricatorMetaMTAMail.php', 'PhabricatorMetaMTAMailBody' => 'applications/metamta/view/PhabricatorMetaMTAMailBody.php', 'PhabricatorMetaMTAMailBodyTestCase' => 'applications/metamta/view/__tests__/PhabricatorMetaMTAMailBodyTestCase.php', 'PhabricatorMetaMTAMailHasRecipientEdgeType' => 'applications/metamta/edge/PhabricatorMetaMTAMailHasRecipientEdgeType.php', 'PhabricatorMetaMTAMailListController' => 'applications/metamta/controller/PhabricatorMetaMTAMailListController.php', 'PhabricatorMetaMTAMailPHIDType' => 'applications/metamta/phid/PhabricatorMetaMTAMailPHIDType.php', 'PhabricatorMetaMTAMailProperties' => 'applications/metamta/storage/PhabricatorMetaMTAMailProperties.php', 'PhabricatorMetaMTAMailPropertiesQuery' => 'applications/metamta/query/PhabricatorMetaMTAMailPropertiesQuery.php', 'PhabricatorMetaMTAMailQuery' => 'applications/metamta/query/PhabricatorMetaMTAMailQuery.php', 'PhabricatorMetaMTAMailSearchEngine' => 'applications/metamta/query/PhabricatorMetaMTAMailSearchEngine.php', 'PhabricatorMetaMTAMailSection' => 'applications/metamta/view/PhabricatorMetaMTAMailSection.php', 'PhabricatorMetaMTAMailTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php', 'PhabricatorMetaMTAMailViewController' => 'applications/metamta/controller/PhabricatorMetaMTAMailViewController.php', 'PhabricatorMetaMTAMailableDatasource' => 'applications/metamta/typeahead/PhabricatorMetaMTAMailableDatasource.php', 'PhabricatorMetaMTAMailableFunctionDatasource' => 'applications/metamta/typeahead/PhabricatorMetaMTAMailableFunctionDatasource.php', 'PhabricatorMetaMTAMailgunReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php', 'PhabricatorMetaMTAMemberQuery' => 'applications/metamta/query/PhabricatorMetaMTAMemberQuery.php', 'PhabricatorMetaMTAPermanentFailureException' => 'applications/metamta/exception/PhabricatorMetaMTAPermanentFailureException.php', 'PhabricatorMetaMTAPostmarkReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTAPostmarkReceiveController.php', 'PhabricatorMetaMTAReceivedMail' => 'applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php', 'PhabricatorMetaMTAReceivedMailProcessingException' => 'applications/metamta/exception/PhabricatorMetaMTAReceivedMailProcessingException.php', 'PhabricatorMetaMTAReceivedMailTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMetaMTAReceivedMailTestCase.php', 'PhabricatorMetaMTASchemaSpec' => 'applications/metamta/storage/PhabricatorMetaMTASchemaSpec.php', 'PhabricatorMetaMTASendGridReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php', 'PhabricatorMetaMTAWorker' => 'applications/metamta/PhabricatorMetaMTAWorker.php', 'PhabricatorMetronomicTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorMetronomicTriggerClock.php', 'PhabricatorModularTransaction' => 'applications/transactions/storage/PhabricatorModularTransaction.php', 'PhabricatorModularTransactionType' => 'applications/transactions/storage/PhabricatorModularTransactionType.php', 'PhabricatorMonogramDatasourceEngineExtension' => 'applications/typeahead/engineextension/PhabricatorMonogramDatasourceEngineExtension.php', 'PhabricatorMonospacedFontSetting' => 'applications/settings/setting/PhabricatorMonospacedFontSetting.php', 'PhabricatorMonospacedTextareasSetting' => 'applications/settings/setting/PhabricatorMonospacedTextareasSetting.php', 'PhabricatorMotivatorProfileMenuItem' => 'applications/search/menuitem/PhabricatorMotivatorProfileMenuItem.php', 'PhabricatorMultiColumnUIExample' => 'applications/uiexample/examples/PhabricatorMultiColumnUIExample.php', 'PhabricatorMultiFactorSettingsPanel' => 'applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php', 'PhabricatorMultimeterApplication' => 'applications/multimeter/application/PhabricatorMultimeterApplication.php', 'PhabricatorMustVerifyEmailController' => 'applications/auth/controller/PhabricatorMustVerifyEmailController.php', 'PhabricatorMutedByEdgeType' => 'applications/transactions/edges/PhabricatorMutedByEdgeType.php', 'PhabricatorMutedEdgeType' => 'applications/transactions/edges/PhabricatorMutedEdgeType.php', 'PhabricatorMySQLConfigOptions' => 'applications/config/option/PhabricatorMySQLConfigOptions.php', 'PhabricatorMySQLFileStorageEngine' => 'applications/files/engine/PhabricatorMySQLFileStorageEngine.php', 'PhabricatorMySQLSearchHost' => 'infrastructure/cluster/search/PhabricatorMySQLSearchHost.php', 'PhabricatorMySQLSetupCheck' => 'applications/config/check/PhabricatorMySQLSetupCheck.php', 'PhabricatorNamedQuery' => 'applications/search/storage/PhabricatorNamedQuery.php', 'PhabricatorNamedQueryConfig' => 'applications/search/storage/PhabricatorNamedQueryConfig.php', 'PhabricatorNamedQueryConfigQuery' => 'applications/search/query/PhabricatorNamedQueryConfigQuery.php', 'PhabricatorNamedQueryQuery' => 'applications/search/query/PhabricatorNamedQueryQuery.php', 'PhabricatorNavigationRemarkupRule' => 'infrastructure/markup/rule/PhabricatorNavigationRemarkupRule.php', 'PhabricatorNeverTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorNeverTriggerClock.php', 'PhabricatorNgramsIndexEngineExtension' => 'applications/search/engineextension/PhabricatorNgramsIndexEngineExtension.php', 'PhabricatorNgramsInterface' => 'applications/search/interface/PhabricatorNgramsInterface.php', 'PhabricatorNotificationBuilder' => 'applications/notification/builder/PhabricatorNotificationBuilder.php', 'PhabricatorNotificationClearController' => 'applications/notification/controller/PhabricatorNotificationClearController.php', 'PhabricatorNotificationClient' => 'applications/notification/client/PhabricatorNotificationClient.php', 'PhabricatorNotificationConfigOptions' => 'applications/config/option/PhabricatorNotificationConfigOptions.php', 'PhabricatorNotificationController' => 'applications/notification/controller/PhabricatorNotificationController.php', 'PhabricatorNotificationDestructionEngineExtension' => 'applications/notification/engineextension/PhabricatorNotificationDestructionEngineExtension.php', 'PhabricatorNotificationIndividualController' => 'applications/notification/controller/PhabricatorNotificationIndividualController.php', 'PhabricatorNotificationListController' => 'applications/notification/controller/PhabricatorNotificationListController.php', 'PhabricatorNotificationPanelController' => 'applications/notification/controller/PhabricatorNotificationPanelController.php', 'PhabricatorNotificationQuery' => 'applications/notification/query/PhabricatorNotificationQuery.php', 'PhabricatorNotificationSearchEngine' => 'applications/notification/query/PhabricatorNotificationSearchEngine.php', 'PhabricatorNotificationServerRef' => 'applications/notification/client/PhabricatorNotificationServerRef.php', 'PhabricatorNotificationServersConfigType' => 'applications/notification/config/PhabricatorNotificationServersConfigType.php', 'PhabricatorNotificationStatusView' => 'applications/notification/view/PhabricatorNotificationStatusView.php', 'PhabricatorNotificationTestController' => 'applications/notification/controller/PhabricatorNotificationTestController.php', 'PhabricatorNotificationTestFeedStory' => 'applications/notification/feed/PhabricatorNotificationTestFeedStory.php', 'PhabricatorNotificationUIExample' => 'applications/uiexample/examples/PhabricatorNotificationUIExample.php', 'PhabricatorNotificationsApplication' => 'applications/notification/application/PhabricatorNotificationsApplication.php', 'PhabricatorNotificationsSetting' => 'applications/settings/setting/PhabricatorNotificationsSetting.php', 'PhabricatorNotificationsSettingsPanel' => 'applications/settings/panel/PhabricatorNotificationsSettingsPanel.php', 'PhabricatorNuanceApplication' => 'applications/nuance/application/PhabricatorNuanceApplication.php', 'PhabricatorOAuth1AuthProvider' => 'applications/auth/provider/PhabricatorOAuth1AuthProvider.php', 'PhabricatorOAuth1SecretTemporaryTokenType' => 'applications/auth/provider/PhabricatorOAuth1SecretTemporaryTokenType.php', 'PhabricatorOAuth2AuthProvider' => 'applications/auth/provider/PhabricatorOAuth2AuthProvider.php', 'PhabricatorOAuthAuthProvider' => 'applications/auth/provider/PhabricatorOAuthAuthProvider.php', 'PhabricatorOAuthClientAuthorization' => 'applications/oauthserver/storage/PhabricatorOAuthClientAuthorization.php', 'PhabricatorOAuthClientAuthorizationQuery' => 'applications/oauthserver/query/PhabricatorOAuthClientAuthorizationQuery.php', 'PhabricatorOAuthClientController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientController.php', 'PhabricatorOAuthClientDisableController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientDisableController.php', 'PhabricatorOAuthClientEditController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientEditController.php', 'PhabricatorOAuthClientListController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientListController.php', 'PhabricatorOAuthClientSecretController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientSecretController.php', 'PhabricatorOAuthClientTestController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientTestController.php', 'PhabricatorOAuthClientViewController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientViewController.php', 'PhabricatorOAuthResponse' => 'applications/oauthserver/PhabricatorOAuthResponse.php', 'PhabricatorOAuthServer' => 'applications/oauthserver/PhabricatorOAuthServer.php', 'PhabricatorOAuthServerAccessToken' => 'applications/oauthserver/storage/PhabricatorOAuthServerAccessToken.php', 'PhabricatorOAuthServerApplication' => 'applications/oauthserver/application/PhabricatorOAuthServerApplication.php', 'PhabricatorOAuthServerAuthController' => 'applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php', 'PhabricatorOAuthServerAuthorizationCode' => 'applications/oauthserver/storage/PhabricatorOAuthServerAuthorizationCode.php', 'PhabricatorOAuthServerAuthorizationsSettingsPanel' => 'applications/oauthserver/panel/PhabricatorOAuthServerAuthorizationsSettingsPanel.php', 'PhabricatorOAuthServerClient' => 'applications/oauthserver/storage/PhabricatorOAuthServerClient.php', 'PhabricatorOAuthServerClientAuthorizationPHIDType' => 'applications/oauthserver/phid/PhabricatorOAuthServerClientAuthorizationPHIDType.php', 'PhabricatorOAuthServerClientPHIDType' => 'applications/oauthserver/phid/PhabricatorOAuthServerClientPHIDType.php', 'PhabricatorOAuthServerClientQuery' => 'applications/oauthserver/query/PhabricatorOAuthServerClientQuery.php', 'PhabricatorOAuthServerClientSearchEngine' => 'applications/oauthserver/query/PhabricatorOAuthServerClientSearchEngine.php', 'PhabricatorOAuthServerController' => 'applications/oauthserver/controller/PhabricatorOAuthServerController.php', 'PhabricatorOAuthServerCreateClientsCapability' => 'applications/oauthserver/capability/PhabricatorOAuthServerCreateClientsCapability.php', 'PhabricatorOAuthServerDAO' => 'applications/oauthserver/storage/PhabricatorOAuthServerDAO.php', 'PhabricatorOAuthServerEditEngine' => 'applications/oauthserver/editor/PhabricatorOAuthServerEditEngine.php', 'PhabricatorOAuthServerEditor' => 'applications/oauthserver/editor/PhabricatorOAuthServerEditor.php', 'PhabricatorOAuthServerSchemaSpec' => 'applications/oauthserver/query/PhabricatorOAuthServerSchemaSpec.php', 'PhabricatorOAuthServerScope' => 'applications/oauthserver/PhabricatorOAuthServerScope.php', 'PhabricatorOAuthServerTestCase' => 'applications/oauthserver/__tests__/PhabricatorOAuthServerTestCase.php', 'PhabricatorOAuthServerTokenController' => 'applications/oauthserver/controller/PhabricatorOAuthServerTokenController.php', 'PhabricatorOAuthServerTransaction' => 'applications/oauthserver/storage/PhabricatorOAuthServerTransaction.php', 'PhabricatorOAuthServerTransactionQuery' => 'applications/oauthserver/query/PhabricatorOAuthServerTransactionQuery.php', 'PhabricatorObjectGraph' => 'infrastructure/graph/PhabricatorObjectGraph.php', 'PhabricatorObjectHandle' => 'applications/phid/PhabricatorObjectHandle.php', 'PhabricatorObjectHasAsanaSubtaskEdgeType' => 'applications/doorkeeper/edge/PhabricatorObjectHasAsanaSubtaskEdgeType.php', 'PhabricatorObjectHasAsanaTaskEdgeType' => 'applications/doorkeeper/edge/PhabricatorObjectHasAsanaTaskEdgeType.php', 'PhabricatorObjectHasContributorEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasContributorEdgeType.php', 'PhabricatorObjectHasDraftEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasDraftEdgeType.php', 'PhabricatorObjectHasFileEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasFileEdgeType.php', 'PhabricatorObjectHasJiraIssueEdgeType' => 'applications/doorkeeper/edge/PhabricatorObjectHasJiraIssueEdgeType.php', 'PhabricatorObjectHasSubscriberEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasSubscriberEdgeType.php', 'PhabricatorObjectHasUnsubscriberEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasUnsubscriberEdgeType.php', 'PhabricatorObjectHasWatcherEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasWatcherEdgeType.php', 'PhabricatorObjectListQuery' => 'applications/phid/query/PhabricatorObjectListQuery.php', 'PhabricatorObjectListQueryTestCase' => 'applications/phid/query/__tests__/PhabricatorObjectListQueryTestCase.php', 'PhabricatorObjectMailReceiver' => 'applications/metamta/receiver/PhabricatorObjectMailReceiver.php', 'PhabricatorObjectMailReceiverTestCase' => 'applications/metamta/receiver/__tests__/PhabricatorObjectMailReceiverTestCase.php', 'PhabricatorObjectMentionedByObjectEdgeType' => 'applications/transactions/edges/PhabricatorObjectMentionedByObjectEdgeType.php', 'PhabricatorObjectMentionsObjectEdgeType' => 'applications/transactions/edges/PhabricatorObjectMentionsObjectEdgeType.php', 'PhabricatorObjectQuery' => 'applications/phid/query/PhabricatorObjectQuery.php', 'PhabricatorObjectRelationship' => 'applications/search/relationship/PhabricatorObjectRelationship.php', 'PhabricatorObjectRelationshipList' => 'applications/search/relationship/PhabricatorObjectRelationshipList.php', 'PhabricatorObjectRelationshipSource' => 'applications/search/relationship/PhabricatorObjectRelationshipSource.php', 'PhabricatorObjectRemarkupRule' => 'infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php', 'PhabricatorObjectSelectorDialog' => 'view/control/PhabricatorObjectSelectorDialog.php', 'PhabricatorObjectStatus' => 'infrastructure/status/PhabricatorObjectStatus.php', 'PhabricatorOffsetPagedQuery' => 'infrastructure/query/PhabricatorOffsetPagedQuery.php', 'PhabricatorOldWorldContentSource' => 'infrastructure/contentsource/PhabricatorOldWorldContentSource.php', 'PhabricatorOlderInlinesSetting' => 'applications/settings/setting/PhabricatorOlderInlinesSetting.php', 'PhabricatorOneTimeTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorOneTimeTriggerClock.php', 'PhabricatorOpcodeCacheSpec' => 'applications/cache/spec/PhabricatorOpcodeCacheSpec.php', 'PhabricatorOptionGroupSetting' => 'applications/settings/setting/PhabricatorOptionGroupSetting.php', 'PhabricatorOwnerPathQuery' => 'applications/owners/query/PhabricatorOwnerPathQuery.php', 'PhabricatorOwnersApplication' => 'applications/owners/application/PhabricatorOwnersApplication.php', 'PhabricatorOwnersArchiveController' => 'applications/owners/controller/PhabricatorOwnersArchiveController.php', 'PhabricatorOwnersConfigOptions' => 'applications/owners/config/PhabricatorOwnersConfigOptions.php', 'PhabricatorOwnersConfiguredCustomField' => 'applications/owners/customfield/PhabricatorOwnersConfiguredCustomField.php', 'PhabricatorOwnersController' => 'applications/owners/controller/PhabricatorOwnersController.php', 'PhabricatorOwnersCustomField' => 'applications/owners/customfield/PhabricatorOwnersCustomField.php', 'PhabricatorOwnersCustomFieldNumericIndex' => 'applications/owners/storage/PhabricatorOwnersCustomFieldNumericIndex.php', 'PhabricatorOwnersCustomFieldStorage' => 'applications/owners/storage/PhabricatorOwnersCustomFieldStorage.php', 'PhabricatorOwnersCustomFieldStringIndex' => 'applications/owners/storage/PhabricatorOwnersCustomFieldStringIndex.php', 'PhabricatorOwnersDAO' => 'applications/owners/storage/PhabricatorOwnersDAO.php', 'PhabricatorOwnersDefaultEditCapability' => 'applications/owners/capability/PhabricatorOwnersDefaultEditCapability.php', 'PhabricatorOwnersDefaultViewCapability' => 'applications/owners/capability/PhabricatorOwnersDefaultViewCapability.php', 'PhabricatorOwnersDetailController' => 'applications/owners/controller/PhabricatorOwnersDetailController.php', 'PhabricatorOwnersEditController' => 'applications/owners/controller/PhabricatorOwnersEditController.php', 'PhabricatorOwnersHovercardEngineExtension' => 'applications/owners/engineextension/PhabricatorOwnersHovercardEngineExtension.php', 'PhabricatorOwnersListController' => 'applications/owners/controller/PhabricatorOwnersListController.php', 'PhabricatorOwnersOwner' => 'applications/owners/storage/PhabricatorOwnersOwner.php', 'PhabricatorOwnersPackage' => 'applications/owners/storage/PhabricatorOwnersPackage.php', 'PhabricatorOwnersPackageAuditingTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageAuditingTransaction.php', 'PhabricatorOwnersPackageAutoreviewTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageAutoreviewTransaction.php', 'PhabricatorOwnersPackageContextFreeGrammar' => 'applications/owners/lipsum/PhabricatorOwnersPackageContextFreeGrammar.php', 'PhabricatorOwnersPackageDatasource' => 'applications/owners/typeahead/PhabricatorOwnersPackageDatasource.php', 'PhabricatorOwnersPackageDescriptionTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageDescriptionTransaction.php', 'PhabricatorOwnersPackageDominionTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageDominionTransaction.php', 'PhabricatorOwnersPackageEditEngine' => 'applications/owners/editor/PhabricatorOwnersPackageEditEngine.php', 'PhabricatorOwnersPackageFerretEngine' => 'applications/owners/search/PhabricatorOwnersPackageFerretEngine.php', 'PhabricatorOwnersPackageFulltextEngine' => 'applications/owners/search/PhabricatorOwnersPackageFulltextEngine.php', 'PhabricatorOwnersPackageFunctionDatasource' => 'applications/owners/typeahead/PhabricatorOwnersPackageFunctionDatasource.php', 'PhabricatorOwnersPackageIgnoredTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageIgnoredTransaction.php', 'PhabricatorOwnersPackageNameNgrams' => 'applications/owners/storage/PhabricatorOwnersPackageNameNgrams.php', 'PhabricatorOwnersPackageNameTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageNameTransaction.php', 'PhabricatorOwnersPackageOwnerDatasource' => 'applications/owners/typeahead/PhabricatorOwnersPackageOwnerDatasource.php', 'PhabricatorOwnersPackageOwnersTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageOwnersTransaction.php', 'PhabricatorOwnersPackagePHIDType' => 'applications/owners/phid/PhabricatorOwnersPackagePHIDType.php', 'PhabricatorOwnersPackagePathsTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackagePathsTransaction.php', 'PhabricatorOwnersPackagePrimaryTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackagePrimaryTransaction.php', 'PhabricatorOwnersPackageQuery' => 'applications/owners/query/PhabricatorOwnersPackageQuery.php', 'PhabricatorOwnersPackageRemarkupRule' => 'applications/owners/remarkup/PhabricatorOwnersPackageRemarkupRule.php', 'PhabricatorOwnersPackageSearchEngine' => 'applications/owners/query/PhabricatorOwnersPackageSearchEngine.php', 'PhabricatorOwnersPackageStatusTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageStatusTransaction.php', 'PhabricatorOwnersPackageTestCase' => 'applications/owners/storage/__tests__/PhabricatorOwnersPackageTestCase.php', 'PhabricatorOwnersPackageTestDataGenerator' => 'applications/owners/lipsum/PhabricatorOwnersPackageTestDataGenerator.php', 'PhabricatorOwnersPackageTransaction' => 'applications/owners/storage/PhabricatorOwnersPackageTransaction.php', 'PhabricatorOwnersPackageTransactionEditor' => 'applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php', 'PhabricatorOwnersPackageTransactionQuery' => 'applications/owners/query/PhabricatorOwnersPackageTransactionQuery.php', 'PhabricatorOwnersPackageTransactionType' => 'applications/owners/xaction/PhabricatorOwnersPackageTransactionType.php', 'PhabricatorOwnersPath' => 'applications/owners/storage/PhabricatorOwnersPath.php', 'PhabricatorOwnersPathContextFreeGrammar' => 'applications/owners/lipsum/PhabricatorOwnersPathContextFreeGrammar.php', 'PhabricatorOwnersPathsController' => 'applications/owners/controller/PhabricatorOwnersPathsController.php', 'PhabricatorOwnersPathsSearchEngineAttachment' => 'applications/owners/engineextension/PhabricatorOwnersPathsSearchEngineAttachment.php', 'PhabricatorOwnersSchemaSpec' => 'applications/owners/storage/PhabricatorOwnersSchemaSpec.php', 'PhabricatorOwnersSearchField' => 'applications/owners/searchfield/PhabricatorOwnersSearchField.php', 'PhabricatorPDFDocumentEngine' => 'applications/files/document/PhabricatorPDFDocumentEngine.php', 'PhabricatorPHDConfigOptions' => 'applications/config/option/PhabricatorPHDConfigOptions.php', 'PhabricatorPHID' => 'applications/phid/storage/PhabricatorPHID.php', 'PhabricatorPHIDConstants' => 'applications/phid/PhabricatorPHIDConstants.php', 'PhabricatorPHIDExportField' => 'infrastructure/export/field/PhabricatorPHIDExportField.php', 'PhabricatorPHIDInterface' => 'applications/phid/interface/PhabricatorPHIDInterface.php', 'PhabricatorPHIDListEditField' => 'applications/transactions/editfield/PhabricatorPHIDListEditField.php', 'PhabricatorPHIDListEditType' => 'applications/transactions/edittype/PhabricatorPHIDListEditType.php', 'PhabricatorPHIDListExportField' => 'infrastructure/export/field/PhabricatorPHIDListExportField.php', 'PhabricatorPHIDMailStamp' => 'applications/metamta/stamp/PhabricatorPHIDMailStamp.php', 'PhabricatorPHIDResolver' => 'applications/phid/resolver/PhabricatorPHIDResolver.php', 'PhabricatorPHIDType' => 'applications/phid/type/PhabricatorPHIDType.php', 'PhabricatorPHIDTypeTestCase' => 'applications/phid/type/__tests__/PhabricatorPHIDTypeTestCase.php', 'PhabricatorPHIDsSearchField' => 'applications/search/field/PhabricatorPHIDsSearchField.php', 'PhabricatorPHPASTApplication' => 'applications/phpast/application/PhabricatorPHPASTApplication.php', 'PhabricatorPHPConfigSetupCheck' => 'applications/config/check/PhabricatorPHPConfigSetupCheck.php', 'PhabricatorPHPMailerConfigOptions' => 'applications/config/option/PhabricatorPHPMailerConfigOptions.php', 'PhabricatorPHPPreflightSetupCheck' => 'applications/config/check/PhabricatorPHPPreflightSetupCheck.php', 'PhabricatorPackagesApplication' => 'applications/packages/application/PhabricatorPackagesApplication.php', 'PhabricatorPackagesController' => 'applications/packages/controller/PhabricatorPackagesController.php', 'PhabricatorPackagesCreatePublisherCapability' => 'applications/packages/capability/PhabricatorPackagesCreatePublisherCapability.php', 'PhabricatorPackagesDAO' => 'applications/packages/storage/PhabricatorPackagesDAO.php', 'PhabricatorPackagesEditEngine' => 'applications/packages/editor/PhabricatorPackagesEditEngine.php', 'PhabricatorPackagesEditor' => 'applications/packages/editor/PhabricatorPackagesEditor.php', 'PhabricatorPackagesNgrams' => 'applications/packages/storage/PhabricatorPackagesNgrams.php', 'PhabricatorPackagesPackage' => 'applications/packages/storage/PhabricatorPackagesPackage.php', 'PhabricatorPackagesPackageController' => 'applications/packages/controller/PhabricatorPackagesPackageController.php', 'PhabricatorPackagesPackageDatasource' => 'applications/packages/typeahead/PhabricatorPackagesPackageDatasource.php', 'PhabricatorPackagesPackageDefaultEditCapability' => 'applications/packages/capability/PhabricatorPackagesPackageDefaultEditCapability.php', 'PhabricatorPackagesPackageDefaultViewCapability' => 'applications/packages/capability/PhabricatorPackagesPackageDefaultViewCapability.php', 'PhabricatorPackagesPackageEditConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesPackageEditConduitAPIMethod.php', 'PhabricatorPackagesPackageEditController' => 'applications/packages/controller/PhabricatorPackagesPackageEditController.php', 'PhabricatorPackagesPackageEditEngine' => 'applications/packages/editor/PhabricatorPackagesPackageEditEngine.php', 'PhabricatorPackagesPackageEditor' => 'applications/packages/editor/PhabricatorPackagesPackageEditor.php', 'PhabricatorPackagesPackageKeyTransaction' => 'applications/packages/xaction/package/PhabricatorPackagesPackageKeyTransaction.php', 'PhabricatorPackagesPackageListController' => 'applications/packages/controller/PhabricatorPackagesPackageListController.php', 'PhabricatorPackagesPackageListView' => 'applications/packages/view/PhabricatorPackagesPackageListView.php', 'PhabricatorPackagesPackageNameNgrams' => 'applications/packages/storage/PhabricatorPackagesPackageNameNgrams.php', 'PhabricatorPackagesPackageNameTransaction' => 'applications/packages/xaction/package/PhabricatorPackagesPackageNameTransaction.php', 'PhabricatorPackagesPackagePHIDType' => 'applications/packages/phid/PhabricatorPackagesPackagePHIDType.php', 'PhabricatorPackagesPackagePublisherTransaction' => 'applications/packages/xaction/package/PhabricatorPackagesPackagePublisherTransaction.php', 'PhabricatorPackagesPackageQuery' => 'applications/packages/query/PhabricatorPackagesPackageQuery.php', 'PhabricatorPackagesPackageSearchConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesPackageSearchConduitAPIMethod.php', 'PhabricatorPackagesPackageSearchEngine' => 'applications/packages/query/PhabricatorPackagesPackageSearchEngine.php', 'PhabricatorPackagesPackageTransaction' => 'applications/packages/storage/PhabricatorPackagesPackageTransaction.php', 'PhabricatorPackagesPackageTransactionQuery' => 'applications/packages/query/PhabricatorPackagesPackageTransactionQuery.php', 'PhabricatorPackagesPackageTransactionType' => 'applications/packages/xaction/package/PhabricatorPackagesPackageTransactionType.php', 'PhabricatorPackagesPackageViewController' => 'applications/packages/controller/PhabricatorPackagesPackageViewController.php', 'PhabricatorPackagesPublisher' => 'applications/packages/storage/PhabricatorPackagesPublisher.php', 'PhabricatorPackagesPublisherController' => 'applications/packages/controller/PhabricatorPackagesPublisherController.php', 'PhabricatorPackagesPublisherDatasource' => 'applications/packages/typeahead/PhabricatorPackagesPublisherDatasource.php', 'PhabricatorPackagesPublisherDefaultEditCapability' => 'applications/packages/capability/PhabricatorPackagesPublisherDefaultEditCapability.php', 'PhabricatorPackagesPublisherEditConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesPublisherEditConduitAPIMethod.php', 'PhabricatorPackagesPublisherEditController' => 'applications/packages/controller/PhabricatorPackagesPublisherEditController.php', 'PhabricatorPackagesPublisherEditEngine' => 'applications/packages/editor/PhabricatorPackagesPublisherEditEngine.php', 'PhabricatorPackagesPublisherEditor' => 'applications/packages/editor/PhabricatorPackagesPublisherEditor.php', 'PhabricatorPackagesPublisherKeyTransaction' => 'applications/packages/xaction/publisher/PhabricatorPackagesPublisherKeyTransaction.php', 'PhabricatorPackagesPublisherListController' => 'applications/packages/controller/PhabricatorPackagesPublisherListController.php', 'PhabricatorPackagesPublisherListView' => 'applications/packages/view/PhabricatorPackagesPublisherListView.php', 'PhabricatorPackagesPublisherNameNgrams' => 'applications/packages/storage/PhabricatorPackagesPublisherNameNgrams.php', 'PhabricatorPackagesPublisherNameTransaction' => 'applications/packages/xaction/publisher/PhabricatorPackagesPublisherNameTransaction.php', 'PhabricatorPackagesPublisherPHIDType' => 'applications/packages/phid/PhabricatorPackagesPublisherPHIDType.php', 'PhabricatorPackagesPublisherQuery' => 'applications/packages/query/PhabricatorPackagesPublisherQuery.php', 'PhabricatorPackagesPublisherSearchConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesPublisherSearchConduitAPIMethod.php', 'PhabricatorPackagesPublisherSearchEngine' => 'applications/packages/query/PhabricatorPackagesPublisherSearchEngine.php', 'PhabricatorPackagesPublisherTransaction' => 'applications/packages/storage/PhabricatorPackagesPublisherTransaction.php', 'PhabricatorPackagesPublisherTransactionQuery' => 'applications/packages/query/PhabricatorPackagesPublisherTransactionQuery.php', 'PhabricatorPackagesPublisherTransactionType' => 'applications/packages/xaction/publisher/PhabricatorPackagesPublisherTransactionType.php', 'PhabricatorPackagesPublisherViewController' => 'applications/packages/controller/PhabricatorPackagesPublisherViewController.php', 'PhabricatorPackagesQuery' => 'applications/packages/query/PhabricatorPackagesQuery.php', 'PhabricatorPackagesSchemaSpec' => 'applications/packages/storage/PhabricatorPackagesSchemaSpec.php', 'PhabricatorPackagesTransactionType' => 'applications/packages/xaction/PhabricatorPackagesTransactionType.php', 'PhabricatorPackagesVersion' => 'applications/packages/storage/PhabricatorPackagesVersion.php', 'PhabricatorPackagesVersionController' => 'applications/packages/controller/PhabricatorPackagesVersionController.php', 'PhabricatorPackagesVersionEditConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesVersionEditConduitAPIMethod.php', 'PhabricatorPackagesVersionEditController' => 'applications/packages/controller/PhabricatorPackagesVersionEditController.php', 'PhabricatorPackagesVersionEditEngine' => 'applications/packages/editor/PhabricatorPackagesVersionEditEngine.php', 'PhabricatorPackagesVersionEditor' => 'applications/packages/editor/PhabricatorPackagesVersionEditor.php', 'PhabricatorPackagesVersionListController' => 'applications/packages/controller/PhabricatorPackagesVersionListController.php', 'PhabricatorPackagesVersionListView' => 'applications/packages/view/PhabricatorPackagesVersionListView.php', 'PhabricatorPackagesVersionNameNgrams' => 'applications/packages/storage/PhabricatorPackagesVersionNameNgrams.php', 'PhabricatorPackagesVersionNameTransaction' => 'applications/packages/xaction/version/PhabricatorPackagesVersionNameTransaction.php', 'PhabricatorPackagesVersionPHIDType' => 'applications/packages/phid/PhabricatorPackagesVersionPHIDType.php', 'PhabricatorPackagesVersionPackageTransaction' => 'applications/packages/xaction/version/PhabricatorPackagesVersionPackageTransaction.php', 'PhabricatorPackagesVersionQuery' => 'applications/packages/query/PhabricatorPackagesVersionQuery.php', 'PhabricatorPackagesVersionSearchConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesVersionSearchConduitAPIMethod.php', 'PhabricatorPackagesVersionSearchEngine' => 'applications/packages/query/PhabricatorPackagesVersionSearchEngine.php', 'PhabricatorPackagesVersionTransaction' => 'applications/packages/storage/PhabricatorPackagesVersionTransaction.php', 'PhabricatorPackagesVersionTransactionQuery' => 'applications/packages/query/PhabricatorPackagesVersionTransactionQuery.php', 'PhabricatorPackagesVersionTransactionType' => 'applications/packages/xaction/version/PhabricatorPackagesVersionTransactionType.php', 'PhabricatorPackagesVersionViewController' => 'applications/packages/controller/PhabricatorPackagesVersionViewController.php', 'PhabricatorPackagesView' => 'applications/packages/view/PhabricatorPackagesView.php', 'PhabricatorPagerUIExample' => 'applications/uiexample/examples/PhabricatorPagerUIExample.php', 'PhabricatorPassphraseApplication' => 'applications/passphrase/application/PhabricatorPassphraseApplication.php', 'PhabricatorPasswordAuthProvider' => 'applications/auth/provider/PhabricatorPasswordAuthProvider.php', 'PhabricatorPasswordDestructionEngineExtension' => 'applications/auth/extension/PhabricatorPasswordDestructionEngineExtension.php', 'PhabricatorPasswordHasher' => 'infrastructure/util/password/PhabricatorPasswordHasher.php', 'PhabricatorPasswordHasherTestCase' => 'infrastructure/util/password/__tests__/PhabricatorPasswordHasherTestCase.php', 'PhabricatorPasswordHasherUnavailableException' => 'infrastructure/util/password/PhabricatorPasswordHasherUnavailableException.php', 'PhabricatorPasswordSettingsPanel' => 'applications/settings/panel/PhabricatorPasswordSettingsPanel.php', 'PhabricatorPaste' => 'applications/paste/storage/PhabricatorPaste.php', 'PhabricatorPasteApplication' => 'applications/paste/application/PhabricatorPasteApplication.php', 'PhabricatorPasteArchiveController' => 'applications/paste/controller/PhabricatorPasteArchiveController.php', 'PhabricatorPasteConfigOptions' => 'applications/paste/config/PhabricatorPasteConfigOptions.php', 'PhabricatorPasteContentSearchEngineAttachment' => 'applications/paste/engineextension/PhabricatorPasteContentSearchEngineAttachment.php', 'PhabricatorPasteContentTransaction' => 'applications/paste/xaction/PhabricatorPasteContentTransaction.php', 'PhabricatorPasteController' => 'applications/paste/controller/PhabricatorPasteController.php', 'PhabricatorPasteDAO' => 'applications/paste/storage/PhabricatorPasteDAO.php', 'PhabricatorPasteEditController' => 'applications/paste/controller/PhabricatorPasteEditController.php', 'PhabricatorPasteEditEngine' => 'applications/paste/editor/PhabricatorPasteEditEngine.php', 'PhabricatorPasteEditor' => 'applications/paste/editor/PhabricatorPasteEditor.php', 'PhabricatorPasteFilenameContextFreeGrammar' => 'applications/paste/lipsum/PhabricatorPasteFilenameContextFreeGrammar.php', 'PhabricatorPasteLanguageTransaction' => 'applications/paste/xaction/PhabricatorPasteLanguageTransaction.php', 'PhabricatorPasteListController' => 'applications/paste/controller/PhabricatorPasteListController.php', 'PhabricatorPastePastePHIDType' => 'applications/paste/phid/PhabricatorPastePastePHIDType.php', 'PhabricatorPasteQuery' => 'applications/paste/query/PhabricatorPasteQuery.php', 'PhabricatorPasteRawController' => 'applications/paste/controller/PhabricatorPasteRawController.php', 'PhabricatorPasteRemarkupRule' => 'applications/paste/remarkup/PhabricatorPasteRemarkupRule.php', 'PhabricatorPasteSchemaSpec' => 'applications/paste/storage/PhabricatorPasteSchemaSpec.php', 'PhabricatorPasteSearchEngine' => 'applications/paste/query/PhabricatorPasteSearchEngine.php', 'PhabricatorPasteSnippet' => 'applications/paste/snippet/PhabricatorPasteSnippet.php', 'PhabricatorPasteStatusTransaction' => 'applications/paste/xaction/PhabricatorPasteStatusTransaction.php', 'PhabricatorPasteTestDataGenerator' => 'applications/paste/lipsum/PhabricatorPasteTestDataGenerator.php', 'PhabricatorPasteTitleTransaction' => 'applications/paste/xaction/PhabricatorPasteTitleTransaction.php', 'PhabricatorPasteTransaction' => 'applications/paste/storage/PhabricatorPasteTransaction.php', 'PhabricatorPasteTransactionComment' => 'applications/paste/storage/PhabricatorPasteTransactionComment.php', 'PhabricatorPasteTransactionQuery' => 'applications/paste/query/PhabricatorPasteTransactionQuery.php', 'PhabricatorPasteTransactionType' => 'applications/paste/xaction/PhabricatorPasteTransactionType.php', 'PhabricatorPasteViewController' => 'applications/paste/controller/PhabricatorPasteViewController.php', 'PhabricatorPathSetupCheck' => 'applications/config/check/PhabricatorPathSetupCheck.php', 'PhabricatorPeopleAnyOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleAnyOwnerDatasource.php', 'PhabricatorPeopleApplication' => 'applications/people/application/PhabricatorPeopleApplication.php', 'PhabricatorPeopleApproveController' => 'applications/people/controller/PhabricatorPeopleApproveController.php', 'PhabricatorPeopleBadgesProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleBadgesProfileMenuItem.php', 'PhabricatorPeopleCommitsProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleCommitsProfileMenuItem.php', 'PhabricatorPeopleController' => 'applications/people/controller/PhabricatorPeopleController.php', 'PhabricatorPeopleCreateController' => 'applications/people/controller/PhabricatorPeopleCreateController.php', 'PhabricatorPeopleCreateGuidanceContext' => 'applications/people/guidance/PhabricatorPeopleCreateGuidanceContext.php', 'PhabricatorPeopleDatasource' => 'applications/people/typeahead/PhabricatorPeopleDatasource.php', 'PhabricatorPeopleDatasourceEngineExtension' => 'applications/people/engineextension/PhabricatorPeopleDatasourceEngineExtension.php', 'PhabricatorPeopleDeleteController' => 'applications/people/controller/PhabricatorPeopleDeleteController.php', 'PhabricatorPeopleDetailsProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleDetailsProfileMenuItem.php', 'PhabricatorPeopleDisableController' => 'applications/people/controller/PhabricatorPeopleDisableController.php', 'PhabricatorPeopleEmpowerController' => 'applications/people/controller/PhabricatorPeopleEmpowerController.php', 'PhabricatorPeopleExternalPHIDType' => 'applications/people/phid/PhabricatorPeopleExternalPHIDType.php', 'PhabricatorPeopleIconSet' => 'applications/people/icon/PhabricatorPeopleIconSet.php', 'PhabricatorPeopleInviteController' => 'applications/people/controller/PhabricatorPeopleInviteController.php', 'PhabricatorPeopleInviteListController' => 'applications/people/controller/PhabricatorPeopleInviteListController.php', 'PhabricatorPeopleInviteSendController' => 'applications/people/controller/PhabricatorPeopleInviteSendController.php', 'PhabricatorPeopleLdapController' => 'applications/people/controller/PhabricatorPeopleLdapController.php', 'PhabricatorPeopleListController' => 'applications/people/controller/PhabricatorPeopleListController.php', 'PhabricatorPeopleLogQuery' => 'applications/people/query/PhabricatorPeopleLogQuery.php', 'PhabricatorPeopleLogSearchEngine' => 'applications/people/query/PhabricatorPeopleLogSearchEngine.php', 'PhabricatorPeopleLogsController' => 'applications/people/controller/PhabricatorPeopleLogsController.php', 'PhabricatorPeopleManageProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleManageProfileMenuItem.php', 'PhabricatorPeopleManagementWorkflow' => 'applications/people/management/PhabricatorPeopleManagementWorkflow.php', 'PhabricatorPeopleNewController' => 'applications/people/controller/PhabricatorPeopleNewController.php', 'PhabricatorPeopleNoOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleNoOwnerDatasource.php', 'PhabricatorPeopleOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleOwnerDatasource.php', 'PhabricatorPeoplePictureProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeoplePictureProfileMenuItem.php', 'PhabricatorPeopleProfileBadgesController' => 'applications/people/controller/PhabricatorPeopleProfileBadgesController.php', 'PhabricatorPeopleProfileCommitsController' => 'applications/people/controller/PhabricatorPeopleProfileCommitsController.php', 'PhabricatorPeopleProfileController' => 'applications/people/controller/PhabricatorPeopleProfileController.php', 'PhabricatorPeopleProfileEditController' => 'applications/people/controller/PhabricatorPeopleProfileEditController.php', 'PhabricatorPeopleProfileImageWorkflow' => 'applications/people/management/PhabricatorPeopleProfileImageWorkflow.php', 'PhabricatorPeopleProfileManageController' => 'applications/people/controller/PhabricatorPeopleProfileManageController.php', 'PhabricatorPeopleProfileMenuEngine' => 'applications/people/engine/PhabricatorPeopleProfileMenuEngine.php', 'PhabricatorPeopleProfilePictureController' => 'applications/people/controller/PhabricatorPeopleProfilePictureController.php', 'PhabricatorPeopleProfileRevisionsController' => 'applications/people/controller/PhabricatorPeopleProfileRevisionsController.php', 'PhabricatorPeopleProfileTasksController' => 'applications/people/controller/PhabricatorPeopleProfileTasksController.php', 'PhabricatorPeopleProfileViewController' => 'applications/people/controller/PhabricatorPeopleProfileViewController.php', 'PhabricatorPeopleQuery' => 'applications/people/query/PhabricatorPeopleQuery.php', 'PhabricatorPeopleRenameController' => 'applications/people/controller/PhabricatorPeopleRenameController.php', 'PhabricatorPeopleRevisionsProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleRevisionsProfileMenuItem.php', 'PhabricatorPeopleSearchEngine' => 'applications/people/query/PhabricatorPeopleSearchEngine.php', 'PhabricatorPeopleTasksProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleTasksProfileMenuItem.php', 'PhabricatorPeopleTestDataGenerator' => 'applications/people/lipsum/PhabricatorPeopleTestDataGenerator.php', 'PhabricatorPeopleTransactionQuery' => 'applications/people/query/PhabricatorPeopleTransactionQuery.php', 'PhabricatorPeopleUserFunctionDatasource' => 'applications/people/typeahead/PhabricatorPeopleUserFunctionDatasource.php', 'PhabricatorPeopleUserPHIDType' => 'applications/people/phid/PhabricatorPeopleUserPHIDType.php', 'PhabricatorPeopleWelcomeController' => 'applications/people/controller/PhabricatorPeopleWelcomeController.php', 'PhabricatorPhabricatorAuthProvider' => 'applications/auth/provider/PhabricatorPhabricatorAuthProvider.php', 'PhabricatorPhameApplication' => 'applications/phame/application/PhabricatorPhameApplication.php', 'PhabricatorPhameBlogPHIDType' => 'applications/phame/phid/PhabricatorPhameBlogPHIDType.php', 'PhabricatorPhamePostPHIDType' => 'applications/phame/phid/PhabricatorPhamePostPHIDType.php', 'PhabricatorPhluxApplication' => 'applications/phlux/application/PhabricatorPhluxApplication.php', 'PhabricatorPholioApplication' => 'applications/pholio/application/PhabricatorPholioApplication.php', 'PhabricatorPholioConfigOptions' => 'applications/pholio/config/PhabricatorPholioConfigOptions.php', 'PhabricatorPholioMockTestDataGenerator' => 'applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php', 'PhabricatorPhortuneApplication' => 'applications/phortune/application/PhabricatorPhortuneApplication.php', 'PhabricatorPhortuneContentSource' => 'applications/phortune/contentsource/PhabricatorPhortuneContentSource.php', 'PhabricatorPhortuneManagementInvoiceWorkflow' => 'applications/phortune/management/PhabricatorPhortuneManagementInvoiceWorkflow.php', 'PhabricatorPhortuneManagementWorkflow' => 'applications/phortune/management/PhabricatorPhortuneManagementWorkflow.php', 'PhabricatorPhortuneTestCase' => 'applications/phortune/__tests__/PhabricatorPhortuneTestCase.php', 'PhabricatorPhragmentApplication' => 'applications/phragment/application/PhabricatorPhragmentApplication.php', 'PhabricatorPhrequentApplication' => 'applications/phrequent/application/PhabricatorPhrequentApplication.php', 'PhabricatorPhrictionApplication' => 'applications/phriction/application/PhabricatorPhrictionApplication.php', 'PhabricatorPhrictionConfigOptions' => 'applications/phriction/config/PhabricatorPhrictionConfigOptions.php', 'PhabricatorPhurlApplication' => 'applications/phurl/application/PhabricatorPhurlApplication.php', 'PhabricatorPhurlConfigOptions' => 'applications/config/option/PhabricatorPhurlConfigOptions.php', 'PhabricatorPhurlController' => 'applications/phurl/controller/PhabricatorPhurlController.php', 'PhabricatorPhurlDAO' => 'applications/phurl/storage/PhabricatorPhurlDAO.php', 'PhabricatorPhurlLinkRemarkupRule' => 'applications/phurl/remarkup/PhabricatorPhurlLinkRemarkupRule.php', 'PhabricatorPhurlRemarkupRule' => 'applications/phurl/remarkup/PhabricatorPhurlRemarkupRule.php', 'PhabricatorPhurlSchemaSpec' => 'applications/phurl/storage/PhabricatorPhurlSchemaSpec.php', 'PhabricatorPhurlShortURLController' => 'applications/phurl/controller/PhabricatorPhurlShortURLController.php', 'PhabricatorPhurlShortURLDefaultController' => 'applications/phurl/controller/PhabricatorPhurlShortURLDefaultController.php', 'PhabricatorPhurlURL' => 'applications/phurl/storage/PhabricatorPhurlURL.php', 'PhabricatorPhurlURLAccessController' => 'applications/phurl/controller/PhabricatorPhurlURLAccessController.php', 'PhabricatorPhurlURLAliasTransaction' => 'applications/phurl/xaction/PhabricatorPhurlURLAliasTransaction.php', 'PhabricatorPhurlURLCreateCapability' => 'applications/phurl/capability/PhabricatorPhurlURLCreateCapability.php', 'PhabricatorPhurlURLDatasource' => 'applications/phurl/typeahead/PhabricatorPhurlURLDatasource.php', 'PhabricatorPhurlURLDescriptionTransaction' => 'applications/phurl/xaction/PhabricatorPhurlURLDescriptionTransaction.php', 'PhabricatorPhurlURLEditConduitAPIMethod' => 'applications/phurl/conduit/PhabricatorPhurlURLEditConduitAPIMethod.php', 'PhabricatorPhurlURLEditController' => 'applications/phurl/controller/PhabricatorPhurlURLEditController.php', 'PhabricatorPhurlURLEditEngine' => 'applications/phurl/editor/PhabricatorPhurlURLEditEngine.php', 'PhabricatorPhurlURLEditor' => 'applications/phurl/editor/PhabricatorPhurlURLEditor.php', 'PhabricatorPhurlURLListController' => 'applications/phurl/controller/PhabricatorPhurlURLListController.php', 'PhabricatorPhurlURLLongURLTransaction' => 'applications/phurl/xaction/PhabricatorPhurlURLLongURLTransaction.php', 'PhabricatorPhurlURLMailReceiver' => 'applications/phurl/mail/PhabricatorPhurlURLMailReceiver.php', 'PhabricatorPhurlURLNameNgrams' => 'applications/phurl/storage/PhabricatorPhurlURLNameNgrams.php', 'PhabricatorPhurlURLNameTransaction' => 'applications/phurl/xaction/PhabricatorPhurlURLNameTransaction.php', 'PhabricatorPhurlURLPHIDType' => 'applications/phurl/phid/PhabricatorPhurlURLPHIDType.php', 'PhabricatorPhurlURLQuery' => 'applications/phurl/query/PhabricatorPhurlURLQuery.php', 'PhabricatorPhurlURLReplyHandler' => 'applications/phurl/mail/PhabricatorPhurlURLReplyHandler.php', 'PhabricatorPhurlURLSearchConduitAPIMethod' => 'applications/phurl/conduit/PhabricatorPhurlURLSearchConduitAPIMethod.php', 'PhabricatorPhurlURLSearchEngine' => 'applications/phurl/query/PhabricatorPhurlURLSearchEngine.php', 'PhabricatorPhurlURLTransaction' => 'applications/phurl/storage/PhabricatorPhurlURLTransaction.php', 'PhabricatorPhurlURLTransactionComment' => 'applications/phurl/storage/PhabricatorPhurlURLTransactionComment.php', 'PhabricatorPhurlURLTransactionQuery' => 'applications/phurl/query/PhabricatorPhurlURLTransactionQuery.php', 'PhabricatorPhurlURLTransactionType' => 'applications/phurl/xaction/PhabricatorPhurlURLTransactionType.php', 'PhabricatorPhurlURLViewController' => 'applications/phurl/controller/PhabricatorPhurlURLViewController.php', 'PhabricatorPinnedApplicationsSetting' => 'applications/settings/setting/PhabricatorPinnedApplicationsSetting.php', 'PhabricatorPirateEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorPirateEnglishTranslation.php', 'PhabricatorPlatformSite' => 'aphront/site/PhabricatorPlatformSite.php', 'PhabricatorPointsEditField' => 'applications/transactions/editfield/PhabricatorPointsEditField.php', 'PhabricatorPointsFact' => 'applications/fact/fact/PhabricatorPointsFact.php', 'PhabricatorPolicies' => 'applications/policy/constants/PhabricatorPolicies.php', 'PhabricatorPolicy' => 'applications/policy/storage/PhabricatorPolicy.php', 'PhabricatorPolicyApplication' => 'applications/policy/application/PhabricatorPolicyApplication.php', 'PhabricatorPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorPolicyAwareQuery.php', 'PhabricatorPolicyAwareTestQuery' => 'applications/policy/__tests__/PhabricatorPolicyAwareTestQuery.php', 'PhabricatorPolicyCanEditCapability' => 'applications/policy/capability/PhabricatorPolicyCanEditCapability.php', 'PhabricatorPolicyCanInteractCapability' => 'applications/policy/capability/PhabricatorPolicyCanInteractCapability.php', 'PhabricatorPolicyCanJoinCapability' => 'applications/policy/capability/PhabricatorPolicyCanJoinCapability.php', 'PhabricatorPolicyCanViewCapability' => 'applications/policy/capability/PhabricatorPolicyCanViewCapability.php', 'PhabricatorPolicyCapability' => 'applications/policy/capability/PhabricatorPolicyCapability.php', 'PhabricatorPolicyCapabilityTestCase' => 'applications/policy/capability/__tests__/PhabricatorPolicyCapabilityTestCase.php', 'PhabricatorPolicyCodex' => 'applications/policy/codex/PhabricatorPolicyCodex.php', 'PhabricatorPolicyCodexInterface' => 'applications/policy/codex/PhabricatorPolicyCodexInterface.php', 'PhabricatorPolicyCodexRuleDescription' => 'applications/policy/codex/PhabricatorPolicyCodexRuleDescription.php', 'PhabricatorPolicyConfigOptions' => 'applications/policy/config/PhabricatorPolicyConfigOptions.php', 'PhabricatorPolicyConstants' => 'applications/policy/constants/PhabricatorPolicyConstants.php', 'PhabricatorPolicyController' => 'applications/policy/controller/PhabricatorPolicyController.php', 'PhabricatorPolicyDAO' => 'applications/policy/storage/PhabricatorPolicyDAO.php', 'PhabricatorPolicyDataTestCase' => 'applications/policy/__tests__/PhabricatorPolicyDataTestCase.php', 'PhabricatorPolicyEditController' => 'applications/policy/controller/PhabricatorPolicyEditController.php', 'PhabricatorPolicyEditEngineExtension' => 'applications/policy/editor/PhabricatorPolicyEditEngineExtension.php', 'PhabricatorPolicyEditField' => 'applications/transactions/editfield/PhabricatorPolicyEditField.php', 'PhabricatorPolicyException' => 'applications/policy/exception/PhabricatorPolicyException.php', 'PhabricatorPolicyExplainController' => 'applications/policy/controller/PhabricatorPolicyExplainController.php', 'PhabricatorPolicyFavoritesSetting' => 'applications/settings/setting/PhabricatorPolicyFavoritesSetting.php', 'PhabricatorPolicyFilter' => 'applications/policy/filter/PhabricatorPolicyFilter.php', 'PhabricatorPolicyInterface' => 'applications/policy/interface/PhabricatorPolicyInterface.php', 'PhabricatorPolicyManagementShowWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementShowWorkflow.php', 'PhabricatorPolicyManagementUnlockWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php', 'PhabricatorPolicyManagementWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementWorkflow.php', 'PhabricatorPolicyPHIDTypePolicy' => 'applications/policy/phid/PhabricatorPolicyPHIDTypePolicy.php', 'PhabricatorPolicyQuery' => 'applications/policy/query/PhabricatorPolicyQuery.php', 'PhabricatorPolicyRequestExceptionHandler' => 'aphront/handler/PhabricatorPolicyRequestExceptionHandler.php', 'PhabricatorPolicyRule' => 'applications/policy/rule/PhabricatorPolicyRule.php', 'PhabricatorPolicySearchEngineExtension' => 'applications/policy/engineextension/PhabricatorPolicySearchEngineExtension.php', 'PhabricatorPolicyStrengthConstants' => 'applications/policy/constants/PhabricatorPolicyStrengthConstants.php', 'PhabricatorPolicyTestCase' => 'applications/policy/__tests__/PhabricatorPolicyTestCase.php', 'PhabricatorPolicyTestObject' => 'applications/policy/__tests__/PhabricatorPolicyTestObject.php', 'PhabricatorPolicyType' => 'applications/policy/constants/PhabricatorPolicyType.php', 'PhabricatorPonderApplication' => 'applications/ponder/application/PhabricatorPonderApplication.php', 'PhabricatorProfileMenuEditEngine' => 'applications/search/editor/PhabricatorProfileMenuEditEngine.php', 'PhabricatorProfileMenuEditor' => 'applications/search/editor/PhabricatorProfileMenuEditor.php', 'PhabricatorProfileMenuEngine' => 'applications/search/engine/PhabricatorProfileMenuEngine.php', 'PhabricatorProfileMenuItem' => 'applications/search/menuitem/PhabricatorProfileMenuItem.php', 'PhabricatorProfileMenuItemConfiguration' => 'applications/search/storage/PhabricatorProfileMenuItemConfiguration.php', 'PhabricatorProfileMenuItemConfigurationQuery' => 'applications/search/query/PhabricatorProfileMenuItemConfigurationQuery.php', 'PhabricatorProfileMenuItemConfigurationTransaction' => 'applications/search/storage/PhabricatorProfileMenuItemConfigurationTransaction.php', 'PhabricatorProfileMenuItemConfigurationTransactionQuery' => 'applications/search/query/PhabricatorProfileMenuItemConfigurationTransactionQuery.php', 'PhabricatorProfileMenuItemIconSet' => 'applications/search/menuitem/PhabricatorProfileMenuItemIconSet.php', 'PhabricatorProfileMenuItemPHIDType' => 'applications/search/phidtype/PhabricatorProfileMenuItemPHIDType.php', 'PhabricatorProject' => 'applications/project/storage/PhabricatorProject.php', 'PhabricatorProjectAddHeraldAction' => 'applications/project/herald/PhabricatorProjectAddHeraldAction.php', 'PhabricatorProjectApplication' => 'applications/project/application/PhabricatorProjectApplication.php', 'PhabricatorProjectArchiveController' => 'applications/project/controller/PhabricatorProjectArchiveController.php', 'PhabricatorProjectBoardBackgroundController' => 'applications/project/controller/PhabricatorProjectBoardBackgroundController.php', 'PhabricatorProjectBoardController' => 'applications/project/controller/PhabricatorProjectBoardController.php', 'PhabricatorProjectBoardDisableController' => 'applications/project/controller/PhabricatorProjectBoardDisableController.php', 'PhabricatorProjectBoardImportController' => 'applications/project/controller/PhabricatorProjectBoardImportController.php', 'PhabricatorProjectBoardManageController' => 'applications/project/controller/PhabricatorProjectBoardManageController.php', 'PhabricatorProjectBoardReorderController' => 'applications/project/controller/PhabricatorProjectBoardReorderController.php', 'PhabricatorProjectBoardViewController' => 'applications/project/controller/PhabricatorProjectBoardViewController.php', 'PhabricatorProjectBuiltinsExample' => 'applications/uiexample/examples/PhabricatorProjectBuiltinsExample.php', 'PhabricatorProjectCardView' => 'applications/project/view/PhabricatorProjectCardView.php', 'PhabricatorProjectColorTransaction' => 'applications/project/xaction/PhabricatorProjectColorTransaction.php', 'PhabricatorProjectColorsConfigType' => 'applications/project/config/PhabricatorProjectColorsConfigType.php', 'PhabricatorProjectColumn' => 'applications/project/storage/PhabricatorProjectColumn.php', 'PhabricatorProjectColumnDetailController' => 'applications/project/controller/PhabricatorProjectColumnDetailController.php', 'PhabricatorProjectColumnEditController' => 'applications/project/controller/PhabricatorProjectColumnEditController.php', 'PhabricatorProjectColumnHideController' => 'applications/project/controller/PhabricatorProjectColumnHideController.php', 'PhabricatorProjectColumnPHIDType' => 'applications/project/phid/PhabricatorProjectColumnPHIDType.php', 'PhabricatorProjectColumnPosition' => 'applications/project/storage/PhabricatorProjectColumnPosition.php', 'PhabricatorProjectColumnPositionQuery' => 'applications/project/query/PhabricatorProjectColumnPositionQuery.php', 'PhabricatorProjectColumnQuery' => 'applications/project/query/PhabricatorProjectColumnQuery.php', 'PhabricatorProjectColumnSearchEngine' => 'applications/project/query/PhabricatorProjectColumnSearchEngine.php', 'PhabricatorProjectColumnTransaction' => 'applications/project/storage/PhabricatorProjectColumnTransaction.php', 'PhabricatorProjectColumnTransactionEditor' => 'applications/project/editor/PhabricatorProjectColumnTransactionEditor.php', 'PhabricatorProjectColumnTransactionQuery' => 'applications/project/query/PhabricatorProjectColumnTransactionQuery.php', 'PhabricatorProjectConfigOptions' => 'applications/project/config/PhabricatorProjectConfigOptions.php', 'PhabricatorProjectConfiguredCustomField' => 'applications/project/customfield/PhabricatorProjectConfiguredCustomField.php', 'PhabricatorProjectController' => 'applications/project/controller/PhabricatorProjectController.php', 'PhabricatorProjectCoreTestCase' => 'applications/project/__tests__/PhabricatorProjectCoreTestCase.php', 'PhabricatorProjectCoverController' => 'applications/project/controller/PhabricatorProjectCoverController.php', 'PhabricatorProjectCustomField' => 'applications/project/customfield/PhabricatorProjectCustomField.php', 'PhabricatorProjectCustomFieldNumericIndex' => 'applications/project/storage/PhabricatorProjectCustomFieldNumericIndex.php', 'PhabricatorProjectCustomFieldStorage' => 'applications/project/storage/PhabricatorProjectCustomFieldStorage.php', 'PhabricatorProjectCustomFieldStringIndex' => 'applications/project/storage/PhabricatorProjectCustomFieldStringIndex.php', 'PhabricatorProjectDAO' => 'applications/project/storage/PhabricatorProjectDAO.php', 'PhabricatorProjectDatasource' => 'applications/project/typeahead/PhabricatorProjectDatasource.php', 'PhabricatorProjectDefaultController' => 'applications/project/controller/PhabricatorProjectDefaultController.php', 'PhabricatorProjectDescriptionField' => 'applications/project/customfield/PhabricatorProjectDescriptionField.php', 'PhabricatorProjectDetailsProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectDetailsProfileMenuItem.php', 'PhabricatorProjectEditController' => 'applications/project/controller/PhabricatorProjectEditController.php', 'PhabricatorProjectEditEngine' => 'applications/project/engine/PhabricatorProjectEditEngine.php', 'PhabricatorProjectEditPictureController' => 'applications/project/controller/PhabricatorProjectEditPictureController.php', 'PhabricatorProjectFerretEngine' => 'applications/project/search/PhabricatorProjectFerretEngine.php', 'PhabricatorProjectFilterTransaction' => 'applications/project/xaction/PhabricatorProjectFilterTransaction.php', 'PhabricatorProjectFulltextEngine' => 'applications/project/search/PhabricatorProjectFulltextEngine.php', 'PhabricatorProjectHeraldAction' => 'applications/project/herald/PhabricatorProjectHeraldAction.php', 'PhabricatorProjectHeraldAdapter' => 'applications/project/herald/PhabricatorProjectHeraldAdapter.php', 'PhabricatorProjectHeraldFieldGroup' => 'applications/project/herald/PhabricatorProjectHeraldFieldGroup.php', 'PhabricatorProjectHovercardEngineExtension' => 'applications/project/engineextension/PhabricatorProjectHovercardEngineExtension.php', 'PhabricatorProjectIconSet' => 'applications/project/icon/PhabricatorProjectIconSet.php', 'PhabricatorProjectIconTransaction' => 'applications/project/xaction/PhabricatorProjectIconTransaction.php', 'PhabricatorProjectIconsConfigType' => 'applications/project/config/PhabricatorProjectIconsConfigType.php', 'PhabricatorProjectImageTransaction' => 'applications/project/xaction/PhabricatorProjectImageTransaction.php', 'PhabricatorProjectInterface' => 'applications/project/interface/PhabricatorProjectInterface.php', 'PhabricatorProjectListController' => 'applications/project/controller/PhabricatorProjectListController.php', 'PhabricatorProjectListView' => 'applications/project/view/PhabricatorProjectListView.php', 'PhabricatorProjectLockController' => 'applications/project/controller/PhabricatorProjectLockController.php', 'PhabricatorProjectLockTransaction' => 'applications/project/xaction/PhabricatorProjectLockTransaction.php', 'PhabricatorProjectLogicalAncestorDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalAncestorDatasource.php', 'PhabricatorProjectLogicalDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalDatasource.php', 'PhabricatorProjectLogicalOnlyDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalOnlyDatasource.php', 'PhabricatorProjectLogicalOrNotDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalOrNotDatasource.php', 'PhabricatorProjectLogicalUserDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalUserDatasource.php', 'PhabricatorProjectLogicalViewerDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalViewerDatasource.php', 'PhabricatorProjectManageController' => 'applications/project/controller/PhabricatorProjectManageController.php', 'PhabricatorProjectManageProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectManageProfileMenuItem.php', 'PhabricatorProjectMaterializedMemberEdgeType' => 'applications/project/edge/PhabricatorProjectMaterializedMemberEdgeType.php', 'PhabricatorProjectMemberListView' => 'applications/project/view/PhabricatorProjectMemberListView.php', 'PhabricatorProjectMemberOfProjectEdgeType' => 'applications/project/edge/PhabricatorProjectMemberOfProjectEdgeType.php', 'PhabricatorProjectMembersAddController' => 'applications/project/controller/PhabricatorProjectMembersAddController.php', 'PhabricatorProjectMembersDatasource' => 'applications/project/typeahead/PhabricatorProjectMembersDatasource.php', 'PhabricatorProjectMembersPolicyRule' => 'applications/project/policyrule/PhabricatorProjectMembersPolicyRule.php', 'PhabricatorProjectMembersProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectMembersProfileMenuItem.php', 'PhabricatorProjectMembersRemoveController' => 'applications/project/controller/PhabricatorProjectMembersRemoveController.php', 'PhabricatorProjectMembersViewController' => 'applications/project/controller/PhabricatorProjectMembersViewController.php', 'PhabricatorProjectMenuItemController' => 'applications/project/controller/PhabricatorProjectMenuItemController.php', 'PhabricatorProjectMilestoneTransaction' => 'applications/project/xaction/PhabricatorProjectMilestoneTransaction.php', 'PhabricatorProjectMoveController' => 'applications/project/controller/PhabricatorProjectMoveController.php', 'PhabricatorProjectNameContextFreeGrammar' => 'applications/project/lipsum/PhabricatorProjectNameContextFreeGrammar.php', 'PhabricatorProjectNameTransaction' => 'applications/project/xaction/PhabricatorProjectNameTransaction.php', 'PhabricatorProjectNoProjectsDatasource' => 'applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php', 'PhabricatorProjectObjectHasProjectEdgeType' => 'applications/project/edge/PhabricatorProjectObjectHasProjectEdgeType.php', 'PhabricatorProjectOrUserDatasource' => 'applications/project/typeahead/PhabricatorProjectOrUserDatasource.php', 'PhabricatorProjectOrUserFunctionDatasource' => 'applications/project/typeahead/PhabricatorProjectOrUserFunctionDatasource.php', 'PhabricatorProjectPHIDResolver' => 'applications/phid/resolver/PhabricatorProjectPHIDResolver.php', 'PhabricatorProjectParentTransaction' => 'applications/project/xaction/PhabricatorProjectParentTransaction.php', 'PhabricatorProjectPictureProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectPictureProfileMenuItem.php', 'PhabricatorProjectPointsProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectPointsProfileMenuItem.php', 'PhabricatorProjectProfileController' => 'applications/project/controller/PhabricatorProjectProfileController.php', 'PhabricatorProjectProfileMenuEngine' => 'applications/project/engine/PhabricatorProjectProfileMenuEngine.php', 'PhabricatorProjectProfileMenuItem' => 'applications/search/menuitem/PhabricatorProjectProfileMenuItem.php', 'PhabricatorProjectProjectHasMemberEdgeType' => 'applications/project/edge/PhabricatorProjectProjectHasMemberEdgeType.php', 'PhabricatorProjectProjectHasObjectEdgeType' => 'applications/project/edge/PhabricatorProjectProjectHasObjectEdgeType.php', 'PhabricatorProjectProjectPHIDType' => 'applications/project/phid/PhabricatorProjectProjectPHIDType.php', 'PhabricatorProjectQuery' => 'applications/project/query/PhabricatorProjectQuery.php', 'PhabricatorProjectRemoveHeraldAction' => 'applications/project/herald/PhabricatorProjectRemoveHeraldAction.php', 'PhabricatorProjectSchemaSpec' => 'applications/project/storage/PhabricatorProjectSchemaSpec.php', 'PhabricatorProjectSearchEngine' => 'applications/project/query/PhabricatorProjectSearchEngine.php', 'PhabricatorProjectSearchField' => 'applications/project/searchfield/PhabricatorProjectSearchField.php', 'PhabricatorProjectSilenceController' => 'applications/project/controller/PhabricatorProjectSilenceController.php', 'PhabricatorProjectSilencedEdgeType' => 'applications/project/edge/PhabricatorProjectSilencedEdgeType.php', 'PhabricatorProjectSlug' => 'applications/project/storage/PhabricatorProjectSlug.php', 'PhabricatorProjectSlugsTransaction' => 'applications/project/xaction/PhabricatorProjectSlugsTransaction.php', 'PhabricatorProjectSortTransaction' => 'applications/project/xaction/PhabricatorProjectSortTransaction.php', 'PhabricatorProjectStandardCustomField' => 'applications/project/customfield/PhabricatorProjectStandardCustomField.php', 'PhabricatorProjectStatus' => 'applications/project/constants/PhabricatorProjectStatus.php', 'PhabricatorProjectStatusTransaction' => 'applications/project/xaction/PhabricatorProjectStatusTransaction.php', 'PhabricatorProjectSubprojectWarningController' => 'applications/project/controller/PhabricatorProjectSubprojectWarningController.php', 'PhabricatorProjectSubprojectsController' => 'applications/project/controller/PhabricatorProjectSubprojectsController.php', 'PhabricatorProjectSubprojectsProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectSubprojectsProfileMenuItem.php', 'PhabricatorProjectTestDataGenerator' => 'applications/project/lipsum/PhabricatorProjectTestDataGenerator.php', 'PhabricatorProjectTransaction' => 'applications/project/storage/PhabricatorProjectTransaction.php', 'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php', 'PhabricatorProjectTransactionQuery' => 'applications/project/query/PhabricatorProjectTransactionQuery.php', 'PhabricatorProjectTransactionType' => 'applications/project/xaction/PhabricatorProjectTransactionType.php', 'PhabricatorProjectTypeTransaction' => 'applications/project/xaction/PhabricatorProjectTypeTransaction.php', 'PhabricatorProjectUIEventListener' => 'applications/project/events/PhabricatorProjectUIEventListener.php', 'PhabricatorProjectUpdateController' => 'applications/project/controller/PhabricatorProjectUpdateController.php', 'PhabricatorProjectUserFunctionDatasource' => 'applications/project/typeahead/PhabricatorProjectUserFunctionDatasource.php', 'PhabricatorProjectUserListView' => 'applications/project/view/PhabricatorProjectUserListView.php', 'PhabricatorProjectViewController' => 'applications/project/controller/PhabricatorProjectViewController.php', 'PhabricatorProjectWatchController' => 'applications/project/controller/PhabricatorProjectWatchController.php', 'PhabricatorProjectWatcherListView' => 'applications/project/view/PhabricatorProjectWatcherListView.php', 'PhabricatorProjectWorkboardBackgroundColor' => 'applications/project/constants/PhabricatorProjectWorkboardBackgroundColor.php', 'PhabricatorProjectWorkboardBackgroundTransaction' => 'applications/project/xaction/PhabricatorProjectWorkboardBackgroundTransaction.php', 'PhabricatorProjectWorkboardProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectWorkboardProfileMenuItem.php', 'PhabricatorProjectWorkboardTransaction' => 'applications/project/xaction/PhabricatorProjectWorkboardTransaction.php', 'PhabricatorProjectsAllPolicyRule' => 'applications/project/policyrule/PhabricatorProjectsAllPolicyRule.php', 'PhabricatorProjectsAncestorsSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsAncestorsSearchEngineAttachment.php', 'PhabricatorProjectsBasePolicyRule' => 'applications/project/policyrule/PhabricatorProjectsBasePolicyRule.php', 'PhabricatorProjectsCurtainExtension' => 'applications/project/engineextension/PhabricatorProjectsCurtainExtension.php', 'PhabricatorProjectsEditEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsEditEngineExtension.php', 'PhabricatorProjectsEditField' => 'applications/transactions/editfield/PhabricatorProjectsEditField.php', 'PhabricatorProjectsExportEngineExtension' => 'infrastructure/export/engine/PhabricatorProjectsExportEngineExtension.php', 'PhabricatorProjectsFulltextEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsFulltextEngineExtension.php', 'PhabricatorProjectsMailEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsMailEngineExtension.php', 'PhabricatorProjectsMembersSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsMembersSearchEngineAttachment.php', 'PhabricatorProjectsMembershipIndexEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsMembershipIndexEngineExtension.php', 'PhabricatorProjectsPolicyRule' => 'applications/project/policyrule/PhabricatorProjectsPolicyRule.php', 'PhabricatorProjectsSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsSearchEngineAttachment.php', 'PhabricatorProjectsSearchEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsSearchEngineExtension.php', 'PhabricatorProjectsWatchersSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsWatchersSearchEngineAttachment.php', 'PhabricatorPronounSetting' => 'applications/settings/setting/PhabricatorPronounSetting.php', 'PhabricatorPygmentSetupCheck' => 'applications/config/check/PhabricatorPygmentSetupCheck.php', 'PhabricatorQuery' => 'infrastructure/query/PhabricatorQuery.php', 'PhabricatorQueryConstraint' => 'infrastructure/query/constraint/PhabricatorQueryConstraint.php', 'PhabricatorQueryIterator' => 'infrastructure/storage/lisk/PhabricatorQueryIterator.php', 'PhabricatorQueryOrderItem' => 'infrastructure/query/order/PhabricatorQueryOrderItem.php', 'PhabricatorQueryOrderTestCase' => 'infrastructure/query/order/__tests__/PhabricatorQueryOrderTestCase.php', 'PhabricatorQueryOrderVector' => 'infrastructure/query/order/PhabricatorQueryOrderVector.php', 'PhabricatorQuickSearchEngineExtension' => 'applications/search/engineextension/PhabricatorQuickSearchEngineExtension.php', 'PhabricatorRateLimitRequestExceptionHandler' => 'aphront/handler/PhabricatorRateLimitRequestExceptionHandler.php', 'PhabricatorRecaptchaConfigOptions' => 'applications/config/option/PhabricatorRecaptchaConfigOptions.php', 'PhabricatorRedirectController' => 'applications/base/controller/PhabricatorRedirectController.php', 'PhabricatorRefreshCSRFController' => 'applications/auth/controller/PhabricatorRefreshCSRFController.php', 'PhabricatorRegexListConfigType' => 'applications/config/type/PhabricatorRegexListConfigType.php', 'PhabricatorRegistrationProfile' => 'applications/people/storage/PhabricatorRegistrationProfile.php', 'PhabricatorReleephApplication' => 'applications/releeph/application/PhabricatorReleephApplication.php', 'PhabricatorReleephApplicationConfigOptions' => 'applications/releeph/config/PhabricatorReleephApplicationConfigOptions.php', 'PhabricatorRemarkupCachePurger' => 'applications/cache/purger/PhabricatorRemarkupCachePurger.php', 'PhabricatorRemarkupControl' => 'view/form/control/PhabricatorRemarkupControl.php', 'PhabricatorRemarkupCowsayBlockInterpreter' => 'infrastructure/markup/interpreter/PhabricatorRemarkupCowsayBlockInterpreter.php', 'PhabricatorRemarkupCustomBlockRule' => 'infrastructure/markup/rule/PhabricatorRemarkupCustomBlockRule.php', 'PhabricatorRemarkupCustomInlineRule' => 'infrastructure/markup/rule/PhabricatorRemarkupCustomInlineRule.php', 'PhabricatorRemarkupDocumentEngine' => 'applications/files/document/PhabricatorRemarkupDocumentEngine.php', 'PhabricatorRemarkupEditField' => 'applications/transactions/editfield/PhabricatorRemarkupEditField.php', 'PhabricatorRemarkupFigletBlockInterpreter' => 'infrastructure/markup/interpreter/PhabricatorRemarkupFigletBlockInterpreter.php', 'PhabricatorRemarkupUIExample' => 'applications/uiexample/examples/PhabricatorRemarkupUIExample.php', 'PhabricatorRepositoriesSetupCheck' => 'applications/config/check/PhabricatorRepositoriesSetupCheck.php', 'PhabricatorRepository' => 'applications/repository/storage/PhabricatorRepository.php', + 'PhabricatorRepositoryActivateTransaction' => 'applications/repository/xaction/PhabricatorRepositoryActivateTransaction.php', 'PhabricatorRepositoryAuditRequest' => 'applications/repository/storage/PhabricatorRepositoryAuditRequest.php', + 'PhabricatorRepositoryAutocloseOnlyTransaction' => 'applications/repository/xaction/PhabricatorRepositoryAutocloseOnlyTransaction.php', + 'PhabricatorRepositoryAutocloseTransaction' => 'applications/repository/xaction/PhabricatorRepositoryAutocloseTransaction.php', + 'PhabricatorRepositoryBlueprintsTransaction' => 'applications/repository/xaction/PhabricatorRepositoryBlueprintsTransaction.php', 'PhabricatorRepositoryBranch' => 'applications/repository/storage/PhabricatorRepositoryBranch.php', + 'PhabricatorRepositoryCallsignTransaction' => 'applications/repository/xaction/PhabricatorRepositoryCallsignTransaction.php', 'PhabricatorRepositoryCommit' => 'applications/repository/storage/PhabricatorRepositoryCommit.php', 'PhabricatorRepositoryCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php', 'PhabricatorRepositoryCommitData' => 'applications/repository/storage/PhabricatorRepositoryCommitData.php', 'PhabricatorRepositoryCommitHeraldWorker' => 'applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php', 'PhabricatorRepositoryCommitHint' => 'applications/repository/storage/PhabricatorRepositoryCommitHint.php', 'PhabricatorRepositoryCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php', 'PhabricatorRepositoryCommitOwnersWorker' => 'applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php', 'PhabricatorRepositoryCommitPHIDType' => 'applications/repository/phid/PhabricatorRepositoryCommitPHIDType.php', 'PhabricatorRepositoryCommitParserWorker' => 'applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php', 'PhabricatorRepositoryCommitRef' => 'applications/repository/engine/PhabricatorRepositoryCommitRef.php', 'PhabricatorRepositoryCommitTestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryCommitTestCase.php', 'PhabricatorRepositoryConfigOptions' => 'applications/repository/config/PhabricatorRepositoryConfigOptions.php', + 'PhabricatorRepositoryCopyTimeLimitTransaction' => 'applications/repository/xaction/PhabricatorRepositoryCopyTimeLimitTransaction.php', 'PhabricatorRepositoryDAO' => 'applications/repository/storage/PhabricatorRepositoryDAO.php', + 'PhabricatorRepositoryDangerousTransaction' => 'applications/repository/xaction/PhabricatorRepositoryDangerousTransaction.php', + 'PhabricatorRepositoryDefaultBranchTransaction' => 'applications/repository/xaction/PhabricatorRepositoryDefaultBranchTransaction.php', + 'PhabricatorRepositoryDescriptionTransaction' => 'applications/repository/xaction/PhabricatorRepositoryDescriptionTransaction.php', 'PhabricatorRepositoryDestructibleCodex' => 'applications/repository/codex/PhabricatorRepositoryDestructibleCodex.php', 'PhabricatorRepositoryDiscoveryEngine' => 'applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php', 'PhabricatorRepositoryEditor' => 'applications/repository/editor/PhabricatorRepositoryEditor.php', + 'PhabricatorRepositoryEncodingTransaction' => 'applications/repository/xaction/PhabricatorRepositoryEncodingTransaction.php', 'PhabricatorRepositoryEngine' => 'applications/repository/engine/PhabricatorRepositoryEngine.php', + 'PhabricatorRepositoryEnormousTransaction' => 'applications/repository/xaction/PhabricatorRepositoryEnormousTransaction.php', 'PhabricatorRepositoryFerretEngine' => 'applications/repository/search/PhabricatorRepositoryFerretEngine.php', + 'PhabricatorRepositoryFilesizeLimitTransaction' => 'applications/repository/xaction/PhabricatorRepositoryFilesizeLimitTransaction.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', 'PhabricatorRepositoryGitLFSRefQuery' => 'applications/repository/query/PhabricatorRepositoryGitLFSRefQuery.php', 'PhabricatorRepositoryGraphCache' => 'applications/repository/graphcache/PhabricatorRepositoryGraphCache.php', 'PhabricatorRepositoryGraphStream' => 'applications/repository/daemon/PhabricatorRepositoryGraphStream.php', 'PhabricatorRepositoryIdentity' => 'applications/repository/storage/PhabricatorRepositoryIdentity.php', 'PhabricatorRepositoryIdentityAssignTransaction' => 'applications/repository/xaction/PhabricatorRepositoryIdentityAssignTransaction.php', 'PhabricatorRepositoryIdentityChangeWorker' => 'applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php', 'PhabricatorRepositoryIdentityEditEngine' => 'applications/repository/engine/PhabricatorRepositoryIdentityEditEngine.php', 'PhabricatorRepositoryIdentityFerretEngine' => 'applications/repository/search/PhabricatorRepositoryIdentityFerretEngine.php', 'PhabricatorRepositoryIdentityPHIDType' => 'applications/repository/phid/PhabricatorRepositoryIdentityPHIDType.php', 'PhabricatorRepositoryIdentityQuery' => 'applications/repository/query/PhabricatorRepositoryIdentityQuery.php', 'PhabricatorRepositoryIdentityTransaction' => 'applications/repository/storage/PhabricatorRepositoryIdentityTransaction.php', 'PhabricatorRepositoryIdentityTransactionQuery' => 'applications/repository/query/PhabricatorRepositoryIdentityTransactionQuery.php', 'PhabricatorRepositoryIdentityTransactionType' => 'applications/repository/xaction/PhabricatorRepositoryIdentityTransactionType.php', 'PhabricatorRepositoryManagementCacheWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementCacheWorkflow.php', 'PhabricatorRepositoryManagementClusterizeWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementClusterizeWorkflow.php', 'PhabricatorRepositoryManagementDiscoverWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementDiscoverWorkflow.php', 'PhabricatorRepositoryManagementHintWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementHintWorkflow.php', 'PhabricatorRepositoryManagementImportingWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementImportingWorkflow.php', 'PhabricatorRepositoryManagementListPathsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementListPathsWorkflow.php', 'PhabricatorRepositoryManagementListWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementListWorkflow.php', 'PhabricatorRepositoryManagementLookupUsersWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementLookupUsersWorkflow.php', 'PhabricatorRepositoryManagementMarkImportedWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMarkImportedWorkflow.php', 'PhabricatorRepositoryManagementMarkReachableWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMarkReachableWorkflow.php', 'PhabricatorRepositoryManagementMirrorWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMirrorWorkflow.php', 'PhabricatorRepositoryManagementMovePathsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMovePathsWorkflow.php', 'PhabricatorRepositoryManagementParentsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementParentsWorkflow.php', 'PhabricatorRepositoryManagementPullWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementPullWorkflow.php', 'PhabricatorRepositoryManagementRebuildIdentitiesWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php', 'PhabricatorRepositoryManagementRefsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementRefsWorkflow.php', 'PhabricatorRepositoryManagementReparseWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementReparseWorkflow.php', 'PhabricatorRepositoryManagementThawWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementThawWorkflow.php', 'PhabricatorRepositoryManagementUnpublishWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementUnpublishWorkflow.php', 'PhabricatorRepositoryManagementUpdateWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementUpdateWorkflow.php', 'PhabricatorRepositoryManagementWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementWorkflow.php', 'PhabricatorRepositoryMercurialCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryMercurialCommitChangeParserWorker.php', 'PhabricatorRepositoryMercurialCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryMercurialCommitMessageParserWorker.php', 'PhabricatorRepositoryMirror' => 'applications/repository/storage/PhabricatorRepositoryMirror.php', 'PhabricatorRepositoryMirrorEngine' => 'applications/repository/engine/PhabricatorRepositoryMirrorEngine.php', + 'PhabricatorRepositoryNameTransaction' => 'applications/repository/xaction/PhabricatorRepositoryNameTransaction.php', + 'PhabricatorRepositoryNotifyTransaction' => 'applications/repository/xaction/PhabricatorRepositoryNotifyTransaction.php', 'PhabricatorRepositoryOldRef' => 'applications/repository/storage/PhabricatorRepositoryOldRef.php', 'PhabricatorRepositoryParsedChange' => 'applications/repository/data/PhabricatorRepositoryParsedChange.php', 'PhabricatorRepositoryPullEngine' => 'applications/repository/engine/PhabricatorRepositoryPullEngine.php', 'PhabricatorRepositoryPullEvent' => 'applications/repository/storage/PhabricatorRepositoryPullEvent.php', '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', 'PhabricatorRepositoryPushLog' => 'applications/repository/storage/PhabricatorRepositoryPushLog.php', 'PhabricatorRepositoryPushLogPHIDType' => 'applications/repository/phid/PhabricatorRepositoryPushLogPHIDType.php', 'PhabricatorRepositoryPushLogQuery' => 'applications/repository/query/PhabricatorRepositoryPushLogQuery.php', 'PhabricatorRepositoryPushLogSearchEngine' => 'applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php', 'PhabricatorRepositoryPushMailWorker' => 'applications/repository/worker/PhabricatorRepositoryPushMailWorker.php', + 'PhabricatorRepositoryPushPolicyTransaction' => 'applications/repository/xaction/PhabricatorRepositoryPushPolicyTransaction.php', 'PhabricatorRepositoryPushReplyHandler' => 'applications/repository/mail/PhabricatorRepositoryPushReplyHandler.php', 'PhabricatorRepositoryQuery' => 'applications/repository/query/PhabricatorRepositoryQuery.php', 'PhabricatorRepositoryRefCursor' => 'applications/repository/storage/PhabricatorRepositoryRefCursor.php', 'PhabricatorRepositoryRefCursorPHIDType' => 'applications/repository/phid/PhabricatorRepositoryRefCursorPHIDType.php', 'PhabricatorRepositoryRefCursorQuery' => 'applications/repository/query/PhabricatorRepositoryRefCursorQuery.php', 'PhabricatorRepositoryRefEngine' => 'applications/repository/engine/PhabricatorRepositoryRefEngine.php', 'PhabricatorRepositoryRefPosition' => 'applications/repository/storage/PhabricatorRepositoryRefPosition.php', 'PhabricatorRepositoryRepositoryPHIDType' => 'applications/repository/phid/PhabricatorRepositoryRepositoryPHIDType.php', + 'PhabricatorRepositorySVNSubpathTransaction' => 'applications/repository/xaction/PhabricatorRepositorySVNSubpathTransaction.php', 'PhabricatorRepositorySchemaSpec' => 'applications/repository/storage/PhabricatorRepositorySchemaSpec.php', 'PhabricatorRepositorySearchEngine' => 'applications/repository/query/PhabricatorRepositorySearchEngine.php', + 'PhabricatorRepositoryServiceTransaction' => 'applications/repository/xaction/PhabricatorRepositoryServiceTransaction.php', + 'PhabricatorRepositorySlugTransaction' => 'applications/repository/xaction/PhabricatorRepositorySlugTransaction.php', + 'PhabricatorRepositoryStagingURITransaction' => 'applications/repository/xaction/PhabricatorRepositoryStagingURITransaction.php', 'PhabricatorRepositoryStatusMessage' => 'applications/repository/storage/PhabricatorRepositoryStatusMessage.php', 'PhabricatorRepositorySvnCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositorySvnCommitChangeParserWorker.php', 'PhabricatorRepositorySvnCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositorySvnCommitMessageParserWorker.php', 'PhabricatorRepositorySymbol' => 'applications/repository/storage/PhabricatorRepositorySymbol.php', + 'PhabricatorRepositorySymbolLanguagesTransaction' => 'applications/repository/xaction/PhabricatorRepositorySymbolLanguagesTransaction.php', + 'PhabricatorRepositorySymbolSourcesTransaction' => 'applications/repository/xaction/PhabricatorRepositorySymbolSourcesTransaction.php', 'PhabricatorRepositorySyncEvent' => 'applications/repository/storage/PhabricatorRepositorySyncEvent.php', 'PhabricatorRepositorySyncEventPHIDType' => 'applications/repository/phid/PhabricatorRepositorySyncEventPHIDType.php', 'PhabricatorRepositorySyncEventQuery' => 'applications/repository/query/PhabricatorRepositorySyncEventQuery.php', 'PhabricatorRepositoryTestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php', + 'PhabricatorRepositoryTouchLimitTransaction' => 'applications/repository/xaction/PhabricatorRepositoryTouchLimitTransaction.php', + 'PhabricatorRepositoryTrackOnlyTransaction' => 'applications/repository/xaction/PhabricatorRepositoryTrackOnlyTransaction.php', 'PhabricatorRepositoryTransaction' => 'applications/repository/storage/PhabricatorRepositoryTransaction.php', 'PhabricatorRepositoryTransactionQuery' => 'applications/repository/query/PhabricatorRepositoryTransactionQuery.php', + 'PhabricatorRepositoryTransactionType' => 'applications/repository/xaction/PhabricatorRepositoryTransactionType.php', 'PhabricatorRepositoryType' => 'applications/repository/constants/PhabricatorRepositoryType.php', 'PhabricatorRepositoryURI' => 'applications/repository/storage/PhabricatorRepositoryURI.php', 'PhabricatorRepositoryURIIndex' => 'applications/repository/storage/PhabricatorRepositoryURIIndex.php', 'PhabricatorRepositoryURINormalizer' => 'applications/repository/data/PhabricatorRepositoryURINormalizer.php', 'PhabricatorRepositoryURINormalizerTestCase' => 'applications/repository/data/__tests__/PhabricatorRepositoryURINormalizerTestCase.php', 'PhabricatorRepositoryURIPHIDType' => 'applications/repository/phid/PhabricatorRepositoryURIPHIDType.php', 'PhabricatorRepositoryURIQuery' => 'applications/repository/query/PhabricatorRepositoryURIQuery.php', 'PhabricatorRepositoryURITestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryURITestCase.php', 'PhabricatorRepositoryURITransaction' => 'applications/repository/storage/PhabricatorRepositoryURITransaction.php', 'PhabricatorRepositoryURITransactionQuery' => 'applications/repository/query/PhabricatorRepositoryURITransactionQuery.php', + 'PhabricatorRepositoryVCSTransaction' => 'applications/repository/xaction/PhabricatorRepositoryVCSTransaction.php', 'PhabricatorRepositoryWorkingCopyVersion' => 'applications/repository/storage/PhabricatorRepositoryWorkingCopyVersion.php', 'PhabricatorRequestExceptionHandler' => 'aphront/handler/PhabricatorRequestExceptionHandler.php', 'PhabricatorResourceSite' => 'aphront/site/PhabricatorResourceSite.php', 'PhabricatorRobotsController' => 'applications/system/controller/PhabricatorRobotsController.php', 'PhabricatorS3FileStorageEngine' => 'applications/files/engine/PhabricatorS3FileStorageEngine.php', 'PhabricatorSMS' => 'infrastructure/sms/storage/PhabricatorSMS.php', 'PhabricatorSMSConfigOptions' => 'applications/config/option/PhabricatorSMSConfigOptions.php', 'PhabricatorSMSDAO' => 'infrastructure/sms/storage/PhabricatorSMSDAO.php', 'PhabricatorSMSDemultiplexWorker' => 'infrastructure/sms/worker/PhabricatorSMSDemultiplexWorker.php', 'PhabricatorSMSImplementationAdapter' => 'infrastructure/sms/adapter/PhabricatorSMSImplementationAdapter.php', 'PhabricatorSMSImplementationTestBlackholeAdapter' => 'infrastructure/sms/adapter/PhabricatorSMSImplementationTestBlackholeAdapter.php', 'PhabricatorSMSImplementationTwilioAdapter' => 'infrastructure/sms/adapter/PhabricatorSMSImplementationTwilioAdapter.php', 'PhabricatorSMSManagementListOutboundWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementListOutboundWorkflow.php', 'PhabricatorSMSManagementSendTestWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementSendTestWorkflow.php', 'PhabricatorSMSManagementShowOutboundWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementShowOutboundWorkflow.php', 'PhabricatorSMSManagementWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementWorkflow.php', 'PhabricatorSMSSendWorker' => 'infrastructure/sms/worker/PhabricatorSMSSendWorker.php', 'PhabricatorSMSWorker' => 'infrastructure/sms/worker/PhabricatorSMSWorker.php', 'PhabricatorSQLPatchList' => 'infrastructure/storage/patch/PhabricatorSQLPatchList.php', 'PhabricatorSSHKeyGenerator' => 'infrastructure/util/PhabricatorSSHKeyGenerator.php', 'PhabricatorSSHKeysSettingsPanel' => 'applications/settings/panel/PhabricatorSSHKeysSettingsPanel.php', 'PhabricatorSSHLog' => 'infrastructure/log/PhabricatorSSHLog.php', 'PhabricatorSSHPassthruCommand' => 'infrastructure/ssh/PhabricatorSSHPassthruCommand.php', 'PhabricatorSSHPublicKeyInterface' => 'applications/auth/sshkey/PhabricatorSSHPublicKeyInterface.php', 'PhabricatorSSHWorkflow' => 'infrastructure/ssh/PhabricatorSSHWorkflow.php', 'PhabricatorSavedQuery' => 'applications/search/storage/PhabricatorSavedQuery.php', 'PhabricatorSavedQueryQuery' => 'applications/search/query/PhabricatorSavedQueryQuery.php', 'PhabricatorScheduleTaskTriggerAction' => 'infrastructure/daemon/workers/action/PhabricatorScheduleTaskTriggerAction.php', 'PhabricatorScopedEnv' => 'infrastructure/env/PhabricatorScopedEnv.php', 'PhabricatorSearchAbstractDocument' => 'applications/search/index/PhabricatorSearchAbstractDocument.php', 'PhabricatorSearchApplication' => 'applications/search/application/PhabricatorSearchApplication.php', 'PhabricatorSearchApplicationSearchEngine' => 'applications/search/query/PhabricatorSearchApplicationSearchEngine.php', 'PhabricatorSearchApplicationStorageEnginePanel' => 'applications/search/applicationpanel/PhabricatorSearchApplicationStorageEnginePanel.php', 'PhabricatorSearchBaseController' => 'applications/search/controller/PhabricatorSearchBaseController.php', 'PhabricatorSearchCheckboxesField' => 'applications/search/field/PhabricatorSearchCheckboxesField.php', 'PhabricatorSearchConstraintException' => 'applications/search/exception/PhabricatorSearchConstraintException.php', 'PhabricatorSearchController' => 'applications/search/controller/PhabricatorSearchController.php', 'PhabricatorSearchCustomFieldProxyField' => 'applications/search/field/PhabricatorSearchCustomFieldProxyField.php', 'PhabricatorSearchDAO' => 'applications/search/storage/PhabricatorSearchDAO.php', 'PhabricatorSearchDatasource' => 'applications/search/typeahead/PhabricatorSearchDatasource.php', 'PhabricatorSearchDatasourceField' => 'applications/search/field/PhabricatorSearchDatasourceField.php', 'PhabricatorSearchDateControlField' => 'applications/search/field/PhabricatorSearchDateControlField.php', 'PhabricatorSearchDateField' => 'applications/search/field/PhabricatorSearchDateField.php', 'PhabricatorSearchDefaultController' => 'applications/search/controller/PhabricatorSearchDefaultController.php', 'PhabricatorSearchDeleteController' => 'applications/search/controller/PhabricatorSearchDeleteController.php', 'PhabricatorSearchDocument' => 'applications/search/storage/document/PhabricatorSearchDocument.php', 'PhabricatorSearchDocumentField' => 'applications/search/storage/document/PhabricatorSearchDocumentField.php', 'PhabricatorSearchDocumentFieldType' => 'applications/search/constants/PhabricatorSearchDocumentFieldType.php', 'PhabricatorSearchDocumentQuery' => 'applications/search/query/PhabricatorSearchDocumentQuery.php', 'PhabricatorSearchDocumentRelationship' => 'applications/search/storage/document/PhabricatorSearchDocumentRelationship.php', 'PhabricatorSearchDocumentTypeDatasource' => 'applications/search/typeahead/PhabricatorSearchDocumentTypeDatasource.php', 'PhabricatorSearchEditController' => 'applications/search/controller/PhabricatorSearchEditController.php', 'PhabricatorSearchEngineAPIMethod' => 'applications/search/engine/PhabricatorSearchEngineAPIMethod.php', 'PhabricatorSearchEngineAttachment' => 'applications/search/engineextension/PhabricatorSearchEngineAttachment.php', 'PhabricatorSearchEngineExtension' => 'applications/search/engineextension/PhabricatorSearchEngineExtension.php', 'PhabricatorSearchEngineExtensionModule' => 'applications/search/engineextension/PhabricatorSearchEngineExtensionModule.php', 'PhabricatorSearchFerretNgramGarbageCollector' => 'applications/search/garbagecollector/PhabricatorSearchFerretNgramGarbageCollector.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', 'PhabricatorSearchManagementIndexWorkflow' => 'applications/search/management/PhabricatorSearchManagementIndexWorkflow.php', 'PhabricatorSearchManagementInitWorkflow' => 'applications/search/management/PhabricatorSearchManagementInitWorkflow.php', 'PhabricatorSearchManagementNgramsWorkflow' => 'applications/search/management/PhabricatorSearchManagementNgramsWorkflow.php', 'PhabricatorSearchManagementQueryWorkflow' => 'applications/search/management/PhabricatorSearchManagementQueryWorkflow.php', 'PhabricatorSearchManagementWorkflow' => 'applications/search/management/PhabricatorSearchManagementWorkflow.php', 'PhabricatorSearchNgrams' => 'applications/search/ngrams/PhabricatorSearchNgrams.php', 'PhabricatorSearchNgramsDestructionEngineExtension' => 'applications/search/engineextension/PhabricatorSearchNgramsDestructionEngineExtension.php', 'PhabricatorSearchOrderController' => 'applications/search/controller/PhabricatorSearchOrderController.php', 'PhabricatorSearchOrderField' => 'applications/search/field/PhabricatorSearchOrderField.php', 'PhabricatorSearchRelationship' => 'applications/search/constants/PhabricatorSearchRelationship.php', 'PhabricatorSearchRelationshipController' => 'applications/search/controller/PhabricatorSearchRelationshipController.php', 'PhabricatorSearchRelationshipSourceController' => 'applications/search/controller/PhabricatorSearchRelationshipSourceController.php', 'PhabricatorSearchResultBucket' => 'applications/search/buckets/PhabricatorSearchResultBucket.php', 'PhabricatorSearchResultBucketGroup' => 'applications/search/buckets/PhabricatorSearchResultBucketGroup.php', 'PhabricatorSearchResultView' => 'applications/search/view/PhabricatorSearchResultView.php', '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', 'PhabricatorSearchThreeStateField' => 'applications/search/field/PhabricatorSearchThreeStateField.php', 'PhabricatorSearchTokenizerField' => 'applications/search/field/PhabricatorSearchTokenizerField.php', 'PhabricatorSearchWorker' => 'applications/search/worker/PhabricatorSearchWorker.php', 'PhabricatorSecurityConfigOptions' => 'applications/config/option/PhabricatorSecurityConfigOptions.php', 'PhabricatorSecuritySetupCheck' => 'applications/config/check/PhabricatorSecuritySetupCheck.php', 'PhabricatorSelectEditField' => 'applications/transactions/editfield/PhabricatorSelectEditField.php', 'PhabricatorSelectSetting' => 'applications/settings/setting/PhabricatorSelectSetting.php', 'PhabricatorSendGridConfigOptions' => 'applications/config/option/PhabricatorSendGridConfigOptions.php', 'PhabricatorSessionsSettingsPanel' => 'applications/settings/panel/PhabricatorSessionsSettingsPanel.php', 'PhabricatorSetConfigType' => 'applications/config/type/PhabricatorSetConfigType.php', 'PhabricatorSetting' => 'applications/settings/setting/PhabricatorSetting.php', 'PhabricatorSettingsAccountPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsAccountPanelGroup.php', 'PhabricatorSettingsAddEmailAction' => 'applications/settings/action/PhabricatorSettingsAddEmailAction.php', 'PhabricatorSettingsAdjustController' => 'applications/settings/controller/PhabricatorSettingsAdjustController.php', 'PhabricatorSettingsApplication' => 'applications/settings/application/PhabricatorSettingsApplication.php', 'PhabricatorSettingsApplicationsPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsApplicationsPanelGroup.php', 'PhabricatorSettingsAuthenticationPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsAuthenticationPanelGroup.php', 'PhabricatorSettingsDeveloperPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsDeveloperPanelGroup.php', 'PhabricatorSettingsEditEngine' => 'applications/settings/editor/PhabricatorSettingsEditEngine.php', 'PhabricatorSettingsEmailPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsEmailPanelGroup.php', 'PhabricatorSettingsIssueController' => 'applications/settings/controller/PhabricatorSettingsIssueController.php', 'PhabricatorSettingsListController' => 'applications/settings/controller/PhabricatorSettingsListController.php', 'PhabricatorSettingsLogsPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsLogsPanelGroup.php', 'PhabricatorSettingsMainController' => 'applications/settings/controller/PhabricatorSettingsMainController.php', 'PhabricatorSettingsPanel' => 'applications/settings/panel/PhabricatorSettingsPanel.php', 'PhabricatorSettingsPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsPanelGroup.php', 'PhabricatorSettingsTimezoneController' => 'applications/settings/controller/PhabricatorSettingsTimezoneController.php', 'PhabricatorSetupCheck' => 'applications/config/check/PhabricatorSetupCheck.php', 'PhabricatorSetupCheckTestCase' => 'applications/config/check/__tests__/PhabricatorSetupCheckTestCase.php', 'PhabricatorSetupEngine' => 'applications/config/engine/PhabricatorSetupEngine.php', 'PhabricatorSetupIssue' => 'applications/config/issue/PhabricatorSetupIssue.php', 'PhabricatorSetupIssueUIExample' => 'applications/uiexample/examples/PhabricatorSetupIssueUIExample.php', 'PhabricatorSetupIssueView' => 'applications/config/view/PhabricatorSetupIssueView.php', 'PhabricatorShortSite' => 'aphront/site/PhabricatorShortSite.php', 'PhabricatorShowFiletreeSetting' => 'applications/settings/setting/PhabricatorShowFiletreeSetting.php', 'PhabricatorSimpleEditType' => 'applications/transactions/edittype/PhabricatorSimpleEditType.php', 'PhabricatorSite' => 'aphront/site/PhabricatorSite.php', 'PhabricatorSlackAuthProvider' => 'applications/auth/provider/PhabricatorSlackAuthProvider.php', 'PhabricatorSlowvoteApplication' => 'applications/slowvote/application/PhabricatorSlowvoteApplication.php', 'PhabricatorSlowvoteChoice' => 'applications/slowvote/storage/PhabricatorSlowvoteChoice.php', 'PhabricatorSlowvoteCloseController' => 'applications/slowvote/controller/PhabricatorSlowvoteCloseController.php', 'PhabricatorSlowvoteCloseTransaction' => 'applications/slowvote/xaction/PhabricatorSlowvoteCloseTransaction.php', 'PhabricatorSlowvoteCommentController' => 'applications/slowvote/controller/PhabricatorSlowvoteCommentController.php', 'PhabricatorSlowvoteController' => 'applications/slowvote/controller/PhabricatorSlowvoteController.php', 'PhabricatorSlowvoteDAO' => 'applications/slowvote/storage/PhabricatorSlowvoteDAO.php', 'PhabricatorSlowvoteDefaultViewCapability' => 'applications/slowvote/capability/PhabricatorSlowvoteDefaultViewCapability.php', 'PhabricatorSlowvoteDescriptionTransaction' => 'applications/slowvote/xaction/PhabricatorSlowvoteDescriptionTransaction.php', 'PhabricatorSlowvoteEditController' => 'applications/slowvote/controller/PhabricatorSlowvoteEditController.php', 'PhabricatorSlowvoteEditor' => 'applications/slowvote/editor/PhabricatorSlowvoteEditor.php', 'PhabricatorSlowvoteListController' => 'applications/slowvote/controller/PhabricatorSlowvoteListController.php', 'PhabricatorSlowvoteMailReceiver' => 'applications/slowvote/mail/PhabricatorSlowvoteMailReceiver.php', 'PhabricatorSlowvoteOption' => 'applications/slowvote/storage/PhabricatorSlowvoteOption.php', 'PhabricatorSlowvotePoll' => 'applications/slowvote/storage/PhabricatorSlowvotePoll.php', 'PhabricatorSlowvotePollController' => 'applications/slowvote/controller/PhabricatorSlowvotePollController.php', 'PhabricatorSlowvotePollPHIDType' => 'applications/slowvote/phid/PhabricatorSlowvotePollPHIDType.php', 'PhabricatorSlowvoteQuery' => 'applications/slowvote/query/PhabricatorSlowvoteQuery.php', 'PhabricatorSlowvoteQuestionTransaction' => 'applications/slowvote/xaction/PhabricatorSlowvoteQuestionTransaction.php', 'PhabricatorSlowvoteReplyHandler' => 'applications/slowvote/mail/PhabricatorSlowvoteReplyHandler.php', 'PhabricatorSlowvoteResponsesTransaction' => 'applications/slowvote/xaction/PhabricatorSlowvoteResponsesTransaction.php', 'PhabricatorSlowvoteSchemaSpec' => 'applications/slowvote/storage/PhabricatorSlowvoteSchemaSpec.php', 'PhabricatorSlowvoteSearchEngine' => 'applications/slowvote/query/PhabricatorSlowvoteSearchEngine.php', 'PhabricatorSlowvoteShuffleTransaction' => 'applications/slowvote/xaction/PhabricatorSlowvoteShuffleTransaction.php', 'PhabricatorSlowvoteTransaction' => 'applications/slowvote/storage/PhabricatorSlowvoteTransaction.php', 'PhabricatorSlowvoteTransactionComment' => 'applications/slowvote/storage/PhabricatorSlowvoteTransactionComment.php', 'PhabricatorSlowvoteTransactionQuery' => 'applications/slowvote/query/PhabricatorSlowvoteTransactionQuery.php', 'PhabricatorSlowvoteTransactionType' => 'applications/slowvote/xaction/PhabricatorSlowvoteTransactionType.php', 'PhabricatorSlowvoteVoteController' => 'applications/slowvote/controller/PhabricatorSlowvoteVoteController.php', 'PhabricatorSlug' => 'infrastructure/util/PhabricatorSlug.php', 'PhabricatorSlugTestCase' => 'infrastructure/util/__tests__/PhabricatorSlugTestCase.php', 'PhabricatorSourceCodeView' => 'view/layout/PhabricatorSourceCodeView.php', 'PhabricatorSourceDocumentEngine' => 'applications/files/document/PhabricatorSourceDocumentEngine.php', 'PhabricatorSpaceEditField' => 'applications/transactions/editfield/PhabricatorSpaceEditField.php', 'PhabricatorSpacesApplication' => 'applications/spaces/application/PhabricatorSpacesApplication.php', 'PhabricatorSpacesArchiveController' => 'applications/spaces/controller/PhabricatorSpacesArchiveController.php', 'PhabricatorSpacesCapabilityCreateSpaces' => 'applications/spaces/capability/PhabricatorSpacesCapabilityCreateSpaces.php', 'PhabricatorSpacesCapabilityDefaultEdit' => 'applications/spaces/capability/PhabricatorSpacesCapabilityDefaultEdit.php', 'PhabricatorSpacesCapabilityDefaultView' => 'applications/spaces/capability/PhabricatorSpacesCapabilityDefaultView.php', 'PhabricatorSpacesController' => 'applications/spaces/controller/PhabricatorSpacesController.php', 'PhabricatorSpacesDAO' => 'applications/spaces/storage/PhabricatorSpacesDAO.php', 'PhabricatorSpacesEditController' => 'applications/spaces/controller/PhabricatorSpacesEditController.php', 'PhabricatorSpacesExportEngineExtension' => 'infrastructure/export/engine/PhabricatorSpacesExportEngineExtension.php', 'PhabricatorSpacesInterface' => 'applications/spaces/interface/PhabricatorSpacesInterface.php', 'PhabricatorSpacesListController' => 'applications/spaces/controller/PhabricatorSpacesListController.php', 'PhabricatorSpacesMailEngineExtension' => 'applications/spaces/engineextension/PhabricatorSpacesMailEngineExtension.php', 'PhabricatorSpacesNamespace' => 'applications/spaces/storage/PhabricatorSpacesNamespace.php', 'PhabricatorSpacesNamespaceArchiveTransaction' => 'applications/spaces/xaction/PhabricatorSpacesNamespaceArchiveTransaction.php', 'PhabricatorSpacesNamespaceDatasource' => 'applications/spaces/typeahead/PhabricatorSpacesNamespaceDatasource.php', 'PhabricatorSpacesNamespaceDefaultTransaction' => 'applications/spaces/xaction/PhabricatorSpacesNamespaceDefaultTransaction.php', 'PhabricatorSpacesNamespaceDescriptionTransaction' => 'applications/spaces/xaction/PhabricatorSpacesNamespaceDescriptionTransaction.php', 'PhabricatorSpacesNamespaceEditor' => 'applications/spaces/editor/PhabricatorSpacesNamespaceEditor.php', 'PhabricatorSpacesNamespaceNameTransaction' => 'applications/spaces/xaction/PhabricatorSpacesNamespaceNameTransaction.php', 'PhabricatorSpacesNamespacePHIDType' => 'applications/spaces/phid/PhabricatorSpacesNamespacePHIDType.php', 'PhabricatorSpacesNamespaceQuery' => 'applications/spaces/query/PhabricatorSpacesNamespaceQuery.php', 'PhabricatorSpacesNamespaceSearchEngine' => 'applications/spaces/query/PhabricatorSpacesNamespaceSearchEngine.php', 'PhabricatorSpacesNamespaceTransaction' => 'applications/spaces/storage/PhabricatorSpacesNamespaceTransaction.php', 'PhabricatorSpacesNamespaceTransactionQuery' => 'applications/spaces/query/PhabricatorSpacesNamespaceTransactionQuery.php', 'PhabricatorSpacesNamespaceTransactionType' => 'applications/spaces/xaction/PhabricatorSpacesNamespaceTransactionType.php', 'PhabricatorSpacesNoAccessController' => 'applications/spaces/controller/PhabricatorSpacesNoAccessController.php', 'PhabricatorSpacesRemarkupRule' => 'applications/spaces/remarkup/PhabricatorSpacesRemarkupRule.php', 'PhabricatorSpacesSchemaSpec' => 'applications/spaces/storage/PhabricatorSpacesSchemaSpec.php', 'PhabricatorSpacesSearchEngineExtension' => 'applications/spaces/engineextension/PhabricatorSpacesSearchEngineExtension.php', 'PhabricatorSpacesSearchField' => 'applications/spaces/searchfield/PhabricatorSpacesSearchField.php', 'PhabricatorSpacesTestCase' => 'applications/spaces/__tests__/PhabricatorSpacesTestCase.php', 'PhabricatorSpacesViewController' => 'applications/spaces/controller/PhabricatorSpacesViewController.php', 'PhabricatorStandardCustomField' => 'infrastructure/customfield/standard/PhabricatorStandardCustomField.php', 'PhabricatorStandardCustomFieldBlueprints' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldBlueprints.php', 'PhabricatorStandardCustomFieldBool' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php', 'PhabricatorStandardCustomFieldCredential' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldCredential.php', 'PhabricatorStandardCustomFieldDatasource' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldDatasource.php', 'PhabricatorStandardCustomFieldDate' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php', 'PhabricatorStandardCustomFieldHeader' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldHeader.php', 'PhabricatorStandardCustomFieldInt' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php', 'PhabricatorStandardCustomFieldInterface' => 'infrastructure/customfield/interface/PhabricatorStandardCustomFieldInterface.php', 'PhabricatorStandardCustomFieldLink' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php', 'PhabricatorStandardCustomFieldPHIDs' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php', 'PhabricatorStandardCustomFieldRemarkup' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php', 'PhabricatorStandardCustomFieldSelect' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php', 'PhabricatorStandardCustomFieldText' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php', 'PhabricatorStandardCustomFieldTokenizer' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php', 'PhabricatorStandardCustomFieldUsers' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldUsers.php', 'PhabricatorStandardPageView' => 'view/page/PhabricatorStandardPageView.php', 'PhabricatorStandardSelectCustomFieldDatasource' => 'infrastructure/customfield/datasource/PhabricatorStandardSelectCustomFieldDatasource.php', 'PhabricatorStaticEditField' => 'applications/transactions/editfield/PhabricatorStaticEditField.php', 'PhabricatorStatusController' => 'applications/system/controller/PhabricatorStatusController.php', 'PhabricatorStatusUIExample' => 'applications/uiexample/examples/PhabricatorStatusUIExample.php', 'PhabricatorStorageFixtureScopeGuard' => 'infrastructure/testing/fixture/PhabricatorStorageFixtureScopeGuard.php', 'PhabricatorStorageManagementAPI' => 'infrastructure/storage/management/PhabricatorStorageManagementAPI.php', 'PhabricatorStorageManagementAdjustWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementAdjustWorkflow.php', 'PhabricatorStorageManagementAnalyzeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementAnalyzeWorkflow.php', 'PhabricatorStorageManagementDatabasesWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDatabasesWorkflow.php', 'PhabricatorStorageManagementDestroyWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php', 'PhabricatorStorageManagementDumpWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php', 'PhabricatorStorageManagementOptimizeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementOptimizeWorkflow.php', 'PhabricatorStorageManagementPartitionWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementPartitionWorkflow.php', 'PhabricatorStorageManagementProbeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementProbeWorkflow.php', 'PhabricatorStorageManagementQuickstartWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementQuickstartWorkflow.php', 'PhabricatorStorageManagementRenamespaceWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementRenamespaceWorkflow.php', 'PhabricatorStorageManagementShellWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementShellWorkflow.php', 'PhabricatorStorageManagementStatusWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementStatusWorkflow.php', 'PhabricatorStorageManagementUpgradeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php', 'PhabricatorStorageManagementWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php', 'PhabricatorStoragePatch' => 'infrastructure/storage/management/PhabricatorStoragePatch.php', 'PhabricatorStorageSchemaSpec' => 'infrastructure/storage/schema/PhabricatorStorageSchemaSpec.php', 'PhabricatorStorageSetupCheck' => 'applications/config/check/PhabricatorStorageSetupCheck.php', 'PhabricatorStringConfigType' => 'applications/config/type/PhabricatorStringConfigType.php', 'PhabricatorStringExportField' => 'infrastructure/export/field/PhabricatorStringExportField.php', 'PhabricatorStringListConfigType' => 'applications/config/type/PhabricatorStringListConfigType.php', 'PhabricatorStringListEditField' => 'applications/transactions/editfield/PhabricatorStringListEditField.php', 'PhabricatorStringListExportField' => 'infrastructure/export/field/PhabricatorStringListExportField.php', 'PhabricatorStringMailStamp' => 'applications/metamta/stamp/PhabricatorStringMailStamp.php', 'PhabricatorStringSetting' => 'applications/settings/setting/PhabricatorStringSetting.php', 'PhabricatorSubmitEditField' => 'applications/transactions/editfield/PhabricatorSubmitEditField.php', 'PhabricatorSubscribableInterface' => 'applications/subscriptions/interface/PhabricatorSubscribableInterface.php', 'PhabricatorSubscribedToObjectEdgeType' => 'applications/transactions/edges/PhabricatorSubscribedToObjectEdgeType.php', 'PhabricatorSubscribersEditField' => 'applications/transactions/editfield/PhabricatorSubscribersEditField.php', 'PhabricatorSubscribersQuery' => 'applications/subscriptions/query/PhabricatorSubscribersQuery.php', 'PhabricatorSubscriptionTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorSubscriptionTriggerClock.php', 'PhabricatorSubscriptionsAddSelfHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsAddSelfHeraldAction.php', 'PhabricatorSubscriptionsAddSubscribersHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsAddSubscribersHeraldAction.php', 'PhabricatorSubscriptionsApplication' => 'applications/subscriptions/application/PhabricatorSubscriptionsApplication.php', 'PhabricatorSubscriptionsCurtainExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsCurtainExtension.php', 'PhabricatorSubscriptionsEditController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php', 'PhabricatorSubscriptionsEditEngineExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsEditEngineExtension.php', 'PhabricatorSubscriptionsEditor' => 'applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php', 'PhabricatorSubscriptionsExportEngineExtension' => 'infrastructure/export/engine/PhabricatorSubscriptionsExportEngineExtension.php', 'PhabricatorSubscriptionsFulltextEngineExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsFulltextEngineExtension.php', 'PhabricatorSubscriptionsHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsHeraldAction.php', 'PhabricatorSubscriptionsListController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsListController.php', 'PhabricatorSubscriptionsMailEngineExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsMailEngineExtension.php', 'PhabricatorSubscriptionsMuteController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php', 'PhabricatorSubscriptionsRemoveSelfHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsRemoveSelfHeraldAction.php', 'PhabricatorSubscriptionsRemoveSubscribersHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsRemoveSubscribersHeraldAction.php', 'PhabricatorSubscriptionsSearchEngineAttachment' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsSearchEngineAttachment.php', 'PhabricatorSubscriptionsSearchEngineExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsSearchEngineExtension.php', 'PhabricatorSubscriptionsSubscribeEmailCommand' => 'applications/subscriptions/command/PhabricatorSubscriptionsSubscribeEmailCommand.php', 'PhabricatorSubscriptionsSubscribersPolicyRule' => 'applications/subscriptions/policyrule/PhabricatorSubscriptionsSubscribersPolicyRule.php', 'PhabricatorSubscriptionsTransactionController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsTransactionController.php', 'PhabricatorSubscriptionsUIEventListener' => 'applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php', 'PhabricatorSubscriptionsUnsubscribeEmailCommand' => 'applications/subscriptions/command/PhabricatorSubscriptionsUnsubscribeEmailCommand.php', 'PhabricatorSubtypeEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php', 'PhabricatorSupportApplication' => 'applications/support/application/PhabricatorSupportApplication.php', 'PhabricatorSyntaxHighlighter' => 'infrastructure/markup/PhabricatorSyntaxHighlighter.php', 'PhabricatorSyntaxHighlightingConfigOptions' => 'applications/config/option/PhabricatorSyntaxHighlightingConfigOptions.php', 'PhabricatorSyntaxStyle' => 'infrastructure/syntax/PhabricatorSyntaxStyle.php', 'PhabricatorSystemAction' => 'applications/system/action/PhabricatorSystemAction.php', 'PhabricatorSystemActionEngine' => 'applications/system/engine/PhabricatorSystemActionEngine.php', 'PhabricatorSystemActionGarbageCollector' => 'applications/system/garbagecollector/PhabricatorSystemActionGarbageCollector.php', 'PhabricatorSystemActionLog' => 'applications/system/storage/PhabricatorSystemActionLog.php', 'PhabricatorSystemActionRateLimitException' => 'applications/system/exception/PhabricatorSystemActionRateLimitException.php', 'PhabricatorSystemApplication' => 'applications/system/application/PhabricatorSystemApplication.php', 'PhabricatorSystemDAO' => 'applications/system/storage/PhabricatorSystemDAO.php', 'PhabricatorSystemDestructionGarbageCollector' => 'applications/system/garbagecollector/PhabricatorSystemDestructionGarbageCollector.php', 'PhabricatorSystemDestructionLog' => 'applications/system/storage/PhabricatorSystemDestructionLog.php', 'PhabricatorSystemObjectController' => 'applications/system/controller/PhabricatorSystemObjectController.php', 'PhabricatorSystemReadOnlyController' => 'applications/system/controller/PhabricatorSystemReadOnlyController.php', 'PhabricatorSystemRemoveDestroyWorkflow' => 'applications/system/management/PhabricatorSystemRemoveDestroyWorkflow.php', 'PhabricatorSystemRemoveLogWorkflow' => 'applications/system/management/PhabricatorSystemRemoveLogWorkflow.php', 'PhabricatorSystemRemoveWorkflow' => 'applications/system/management/PhabricatorSystemRemoveWorkflow.php', 'PhabricatorSystemSelectEncodingController' => 'applications/system/controller/PhabricatorSystemSelectEncodingController.php', 'PhabricatorSystemSelectHighlightController' => 'applications/system/controller/PhabricatorSystemSelectHighlightController.php', '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', 'PhabricatorTestDataGenerator' => 'applications/lipsum/generator/PhabricatorTestDataGenerator.php', 'PhabricatorTestNoCycleEdgeType' => 'applications/transactions/edges/PhabricatorTestNoCycleEdgeType.php', 'PhabricatorTestStorageEngine' => 'applications/files/engine/PhabricatorTestStorageEngine.php', 'PhabricatorTestWorker' => 'infrastructure/daemon/workers/__tests__/PhabricatorTestWorker.php', 'PhabricatorTextAreaEditField' => 'applications/transactions/editfield/PhabricatorTextAreaEditField.php', 'PhabricatorTextConfigType' => 'applications/config/type/PhabricatorTextConfigType.php', 'PhabricatorTextDocumentEngine' => 'applications/files/document/PhabricatorTextDocumentEngine.php', 'PhabricatorTextEditField' => 'applications/transactions/editfield/PhabricatorTextEditField.php', 'PhabricatorTextExportFormat' => 'infrastructure/export/format/PhabricatorTextExportFormat.php', 'PhabricatorTextListConfigType' => 'applications/config/type/PhabricatorTextListConfigType.php', 'PhabricatorTime' => 'infrastructure/time/PhabricatorTime.php', 'PhabricatorTimeFormatSetting' => 'applications/settings/setting/PhabricatorTimeFormatSetting.php', 'PhabricatorTimeGuard' => 'infrastructure/time/PhabricatorTimeGuard.php', 'PhabricatorTimeTestCase' => 'infrastructure/time/__tests__/PhabricatorTimeTestCase.php', 'PhabricatorTimezoneIgnoreOffsetSetting' => 'applications/settings/setting/PhabricatorTimezoneIgnoreOffsetSetting.php', 'PhabricatorTimezoneSetting' => 'applications/settings/setting/PhabricatorTimezoneSetting.php', 'PhabricatorTimezoneSetupCheck' => 'applications/config/check/PhabricatorTimezoneSetupCheck.php', 'PhabricatorTitleGlyphsSetting' => 'applications/settings/setting/PhabricatorTitleGlyphsSetting.php', 'PhabricatorToken' => 'applications/tokens/storage/PhabricatorToken.php', 'PhabricatorTokenController' => 'applications/tokens/controller/PhabricatorTokenController.php', 'PhabricatorTokenCount' => 'applications/tokens/storage/PhabricatorTokenCount.php', 'PhabricatorTokenCountQuery' => 'applications/tokens/query/PhabricatorTokenCountQuery.php', 'PhabricatorTokenDAO' => 'applications/tokens/storage/PhabricatorTokenDAO.php', 'PhabricatorTokenDestructionEngineExtension' => 'applications/tokens/engineextension/PhabricatorTokenDestructionEngineExtension.php', 'PhabricatorTokenGiveController' => 'applications/tokens/controller/PhabricatorTokenGiveController.php', 'PhabricatorTokenGiven' => 'applications/tokens/storage/PhabricatorTokenGiven.php', 'PhabricatorTokenGivenController' => 'applications/tokens/controller/PhabricatorTokenGivenController.php', 'PhabricatorTokenGivenEditor' => 'applications/tokens/editor/PhabricatorTokenGivenEditor.php', 'PhabricatorTokenGivenFeedStory' => 'applications/tokens/feed/PhabricatorTokenGivenFeedStory.php', 'PhabricatorTokenGivenQuery' => 'applications/tokens/query/PhabricatorTokenGivenQuery.php', 'PhabricatorTokenLeaderController' => 'applications/tokens/controller/PhabricatorTokenLeaderController.php', 'PhabricatorTokenQuery' => 'applications/tokens/query/PhabricatorTokenQuery.php', 'PhabricatorTokenReceiverInterface' => 'applications/tokens/interface/PhabricatorTokenReceiverInterface.php', 'PhabricatorTokenReceiverQuery' => 'applications/tokens/query/PhabricatorTokenReceiverQuery.php', 'PhabricatorTokenTokenPHIDType' => 'applications/tokens/phid/PhabricatorTokenTokenPHIDType.php', 'PhabricatorTokenUIEventListener' => 'applications/tokens/event/PhabricatorTokenUIEventListener.php', 'PhabricatorTokenizerEditField' => 'applications/transactions/editfield/PhabricatorTokenizerEditField.php', 'PhabricatorTokensApplication' => 'applications/tokens/application/PhabricatorTokensApplication.php', 'PhabricatorTokensCurtainExtension' => 'applications/tokens/engineextension/PhabricatorTokensCurtainExtension.php', 'PhabricatorTokensSettingsPanel' => 'applications/settings/panel/PhabricatorTokensSettingsPanel.php', 'PhabricatorTokensToken' => 'applications/tokens/storage/PhabricatorTokensToken.php', 'PhabricatorTransactionChange' => 'applications/transactions/data/PhabricatorTransactionChange.php', 'PhabricatorTransactionFactEngine' => 'applications/fact/engine/PhabricatorTransactionFactEngine.php', 'PhabricatorTransactionRemarkupChange' => 'applications/transactions/data/PhabricatorTransactionRemarkupChange.php', 'PhabricatorTransactions' => 'applications/transactions/constants/PhabricatorTransactions.php', 'PhabricatorTransactionsApplication' => 'applications/transactions/application/PhabricatorTransactionsApplication.php', 'PhabricatorTransactionsDestructionEngineExtension' => 'applications/transactions/engineextension/PhabricatorTransactionsDestructionEngineExtension.php', 'PhabricatorTransactionsFulltextEngineExtension' => 'applications/transactions/engineextension/PhabricatorTransactionsFulltextEngineExtension.php', 'PhabricatorTransformedFile' => 'applications/files/storage/PhabricatorTransformedFile.php', 'PhabricatorTranslationSetting' => 'applications/settings/setting/PhabricatorTranslationSetting.php', 'PhabricatorTranslationsConfigOptions' => 'applications/config/option/PhabricatorTranslationsConfigOptions.php', 'PhabricatorTriggerAction' => 'infrastructure/daemon/workers/action/PhabricatorTriggerAction.php', 'PhabricatorTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorTriggerClock.php', 'PhabricatorTriggerClockTestCase' => 'infrastructure/daemon/workers/clock/__tests__/PhabricatorTriggerClockTestCase.php', 'PhabricatorTriggerDaemon' => 'infrastructure/daemon/workers/PhabricatorTriggerDaemon.php', 'PhabricatorTrivialTestCase' => 'infrastructure/testing/__tests__/PhabricatorTrivialTestCase.php', 'PhabricatorTwitchAuthProvider' => 'applications/auth/provider/PhabricatorTwitchAuthProvider.php', 'PhabricatorTwitterAuthProvider' => 'applications/auth/provider/PhabricatorTwitterAuthProvider.php', 'PhabricatorTypeaheadApplication' => 'applications/typeahead/application/PhabricatorTypeaheadApplication.php', 'PhabricatorTypeaheadCompositeDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php', 'PhabricatorTypeaheadDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php', 'PhabricatorTypeaheadDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadDatasourceController.php', 'PhabricatorTypeaheadDatasourceTestCase' => 'applications/typeahead/datasource/__tests__/PhabricatorTypeaheadDatasourceTestCase.php', 'PhabricatorTypeaheadFunctionHelpController' => 'applications/typeahead/controller/PhabricatorTypeaheadFunctionHelpController.php', 'PhabricatorTypeaheadInvalidTokenException' => 'applications/typeahead/exception/PhabricatorTypeaheadInvalidTokenException.php', 'PhabricatorTypeaheadModularDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php', 'PhabricatorTypeaheadMonogramDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadMonogramDatasource.php', 'PhabricatorTypeaheadProxyDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadProxyDatasource.php', 'PhabricatorTypeaheadResult' => 'applications/typeahead/storage/PhabricatorTypeaheadResult.php', 'PhabricatorTypeaheadRuntimeCompositeDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadRuntimeCompositeDatasource.php', 'PhabricatorTypeaheadTestNumbersDatasource' => 'applications/typeahead/datasource/__tests__/PhabricatorTypeaheadTestNumbersDatasource.php', 'PhabricatorTypeaheadTokenView' => 'applications/typeahead/view/PhabricatorTypeaheadTokenView.php', 'PhabricatorUIConfigOptions' => 'applications/config/option/PhabricatorUIConfigOptions.php', 'PhabricatorUIExample' => 'applications/uiexample/examples/PhabricatorUIExample.php', 'PhabricatorUIExampleRenderController' => 'applications/uiexample/controller/PhabricatorUIExampleRenderController.php', 'PhabricatorUIExamplesApplication' => 'applications/uiexample/application/PhabricatorUIExamplesApplication.php', 'PhabricatorURIExportField' => 'infrastructure/export/field/PhabricatorURIExportField.php', 'PhabricatorUSEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php', 'PhabricatorUnifiedDiffsSetting' => 'applications/settings/setting/PhabricatorUnifiedDiffsSetting.php', 'PhabricatorUnitTestContentSource' => 'infrastructure/contentsource/PhabricatorUnitTestContentSource.php', 'PhabricatorUnitsTestCase' => 'view/__tests__/PhabricatorUnitsTestCase.php', 'PhabricatorUnknownContentSource' => 'infrastructure/contentsource/PhabricatorUnknownContentSource.php', 'PhabricatorUnsubscribedFromObjectEdgeType' => 'applications/transactions/edges/PhabricatorUnsubscribedFromObjectEdgeType.php', 'PhabricatorUser' => 'applications/people/storage/PhabricatorUser.php', 'PhabricatorUserBadgesCacheType' => 'applications/people/cache/PhabricatorUserBadgesCacheType.php', 'PhabricatorUserBlurbField' => 'applications/people/customfield/PhabricatorUserBlurbField.php', 'PhabricatorUserCache' => 'applications/people/storage/PhabricatorUserCache.php', 'PhabricatorUserCachePurger' => 'applications/cache/purger/PhabricatorUserCachePurger.php', 'PhabricatorUserCacheType' => 'applications/people/cache/PhabricatorUserCacheType.php', 'PhabricatorUserCardView' => 'applications/people/view/PhabricatorUserCardView.php', 'PhabricatorUserConfigOptions' => 'applications/people/config/PhabricatorUserConfigOptions.php', 'PhabricatorUserConfiguredCustomField' => 'applications/people/customfield/PhabricatorUserConfiguredCustomField.php', 'PhabricatorUserConfiguredCustomFieldStorage' => 'applications/people/storage/PhabricatorUserConfiguredCustomFieldStorage.php', 'PhabricatorUserCustomField' => 'applications/people/customfield/PhabricatorUserCustomField.php', 'PhabricatorUserCustomFieldNumericIndex' => 'applications/people/storage/PhabricatorUserCustomFieldNumericIndex.php', 'PhabricatorUserCustomFieldStringIndex' => 'applications/people/storage/PhabricatorUserCustomFieldStringIndex.php', 'PhabricatorUserDAO' => 'applications/people/storage/PhabricatorUserDAO.php', 'PhabricatorUserDisableTransaction' => 'applications/people/xaction/PhabricatorUserDisableTransaction.php', 'PhabricatorUserEditEngine' => 'applications/people/editor/PhabricatorUserEditEngine.php', 'PhabricatorUserEditor' => 'applications/people/editor/PhabricatorUserEditor.php', 'PhabricatorUserEditorTestCase' => 'applications/people/editor/__tests__/PhabricatorUserEditorTestCase.php', 'PhabricatorUserEmail' => 'applications/people/storage/PhabricatorUserEmail.php', 'PhabricatorUserEmailTestCase' => 'applications/people/storage/__tests__/PhabricatorUserEmailTestCase.php', 'PhabricatorUserFerretEngine' => 'applications/people/search/PhabricatorUserFerretEngine.php', 'PhabricatorUserFulltextEngine' => 'applications/people/search/PhabricatorUserFulltextEngine.php', 'PhabricatorUserIconField' => 'applications/people/customfield/PhabricatorUserIconField.php', 'PhabricatorUserLog' => 'applications/people/storage/PhabricatorUserLog.php', 'PhabricatorUserLogView' => 'applications/people/view/PhabricatorUserLogView.php', 'PhabricatorUserMessageCountCacheType' => 'applications/people/cache/PhabricatorUserMessageCountCacheType.php', 'PhabricatorUserNotificationCountCacheType' => 'applications/people/cache/PhabricatorUserNotificationCountCacheType.php', 'PhabricatorUserPHIDResolver' => 'applications/phid/resolver/PhabricatorUserPHIDResolver.php', 'PhabricatorUserPreferences' => 'applications/settings/storage/PhabricatorUserPreferences.php', 'PhabricatorUserPreferencesCacheType' => 'applications/people/cache/PhabricatorUserPreferencesCacheType.php', 'PhabricatorUserPreferencesEditor' => 'applications/settings/editor/PhabricatorUserPreferencesEditor.php', 'PhabricatorUserPreferencesPHIDType' => 'applications/settings/phid/PhabricatorUserPreferencesPHIDType.php', 'PhabricatorUserPreferencesQuery' => 'applications/settings/query/PhabricatorUserPreferencesQuery.php', 'PhabricatorUserPreferencesSearchEngine' => 'applications/settings/query/PhabricatorUserPreferencesSearchEngine.php', 'PhabricatorUserPreferencesTransaction' => 'applications/settings/storage/PhabricatorUserPreferencesTransaction.php', 'PhabricatorUserPreferencesTransactionQuery' => 'applications/settings/query/PhabricatorUserPreferencesTransactionQuery.php', 'PhabricatorUserProfile' => 'applications/people/storage/PhabricatorUserProfile.php', 'PhabricatorUserProfileImageCacheType' => 'applications/people/cache/PhabricatorUserProfileImageCacheType.php', 'PhabricatorUserRealNameField' => 'applications/people/customfield/PhabricatorUserRealNameField.php', 'PhabricatorUserRolesField' => 'applications/people/customfield/PhabricatorUserRolesField.php', 'PhabricatorUserSchemaSpec' => 'applications/people/storage/PhabricatorUserSchemaSpec.php', 'PhabricatorUserSinceField' => 'applications/people/customfield/PhabricatorUserSinceField.php', 'PhabricatorUserStatusField' => 'applications/people/customfield/PhabricatorUserStatusField.php', 'PhabricatorUserTestCase' => 'applications/people/storage/__tests__/PhabricatorUserTestCase.php', 'PhabricatorUserTitleField' => 'applications/people/customfield/PhabricatorUserTitleField.php', 'PhabricatorUserTransaction' => 'applications/people/storage/PhabricatorUserTransaction.php', 'PhabricatorUserTransactionEditor' => 'applications/people/editor/PhabricatorUserTransactionEditor.php', 'PhabricatorUserTransactionType' => 'applications/people/xaction/PhabricatorUserTransactionType.php', 'PhabricatorUsersEditField' => 'applications/transactions/editfield/PhabricatorUsersEditField.php', 'PhabricatorUsersPolicyRule' => 'applications/people/policyrule/PhabricatorUsersPolicyRule.php', 'PhabricatorUsersSearchField' => 'applications/people/searchfield/PhabricatorUsersSearchField.php', 'PhabricatorVCSResponse' => 'applications/repository/response/PhabricatorVCSResponse.php', 'PhabricatorVersionedDraft' => 'applications/draft/storage/PhabricatorVersionedDraft.php', 'PhabricatorVeryWowEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorVeryWowEnglishTranslation.php', 'PhabricatorVideoDocumentEngine' => 'applications/files/document/PhabricatorVideoDocumentEngine.php', 'PhabricatorViewerDatasource' => 'applications/people/typeahead/PhabricatorViewerDatasource.php', 'PhabricatorVoidDocumentEngine' => 'applications/files/document/PhabricatorVoidDocumentEngine.php', 'PhabricatorWatcherHasObjectEdgeType' => 'applications/transactions/edges/PhabricatorWatcherHasObjectEdgeType.php', 'PhabricatorWebContentSource' => 'infrastructure/contentsource/PhabricatorWebContentSource.php', 'PhabricatorWebServerSetupCheck' => 'applications/config/check/PhabricatorWebServerSetupCheck.php', 'PhabricatorWeekStartDaySetting' => 'applications/settings/setting/PhabricatorWeekStartDaySetting.php', 'PhabricatorWildConfigType' => 'applications/config/type/PhabricatorWildConfigType.php', 'PhabricatorWordPressAuthProvider' => 'applications/auth/provider/PhabricatorWordPressAuthProvider.php', 'PhabricatorWorker' => 'infrastructure/daemon/workers/PhabricatorWorker.php', 'PhabricatorWorkerActiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php', 'PhabricatorWorkerActiveTaskQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerActiveTaskQuery.php', 'PhabricatorWorkerArchiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php', 'PhabricatorWorkerArchiveTaskQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerArchiveTaskQuery.php', 'PhabricatorWorkerBulkJob' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php', 'PhabricatorWorkerBulkJobCreateWorker' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobCreateWorker.php', 'PhabricatorWorkerBulkJobEditor' => 'infrastructure/daemon/workers/editor/PhabricatorWorkerBulkJobEditor.php', 'PhabricatorWorkerBulkJobPHIDType' => 'infrastructure/daemon/workers/phid/PhabricatorWorkerBulkJobPHIDType.php', 'PhabricatorWorkerBulkJobQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerBulkJobQuery.php', 'PhabricatorWorkerBulkJobSearchEngine' => 'infrastructure/daemon/workers/query/PhabricatorWorkerBulkJobSearchEngine.php', 'PhabricatorWorkerBulkJobTaskWorker' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobTaskWorker.php', 'PhabricatorWorkerBulkJobTestCase' => 'infrastructure/daemon/workers/__tests__/PhabricatorWorkerBulkJobTestCase.php', 'PhabricatorWorkerBulkJobTransaction' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJobTransaction.php', 'PhabricatorWorkerBulkJobTransactionQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerBulkJobTransactionQuery.php', 'PhabricatorWorkerBulkJobType' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobType.php', 'PhabricatorWorkerBulkJobWorker' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobWorker.php', 'PhabricatorWorkerBulkTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerBulkTask.php', 'PhabricatorWorkerDAO' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerDAO.php', 'PhabricatorWorkerDestructionEngineExtension' => 'infrastructure/daemon/workers/engineextension/PhabricatorWorkerDestructionEngineExtension.php', 'PhabricatorWorkerLeaseQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php', 'PhabricatorWorkerManagementCancelWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementCancelWorkflow.php', 'PhabricatorWorkerManagementExecuteWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementExecuteWorkflow.php', 'PhabricatorWorkerManagementFloodWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementFloodWorkflow.php', 'PhabricatorWorkerManagementFreeWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementFreeWorkflow.php', 'PhabricatorWorkerManagementRetryWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementRetryWorkflow.php', 'PhabricatorWorkerManagementWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementWorkflow.php', 'PhabricatorWorkerPermanentFailureException' => 'infrastructure/daemon/workers/exception/PhabricatorWorkerPermanentFailureException.php', 'PhabricatorWorkerSchemaSpec' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerSchemaSpec.php', 'PhabricatorWorkerSingleBulkJobType' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerSingleBulkJobType.php', 'PhabricatorWorkerTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTask.php', 'PhabricatorWorkerTaskData' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTaskData.php', 'PhabricatorWorkerTaskDetailController' => 'applications/daemon/controller/PhabricatorWorkerTaskDetailController.php', 'PhabricatorWorkerTaskQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerTaskQuery.php', 'PhabricatorWorkerTestCase' => 'infrastructure/daemon/workers/__tests__/PhabricatorWorkerTestCase.php', 'PhabricatorWorkerTrigger' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTrigger.php', 'PhabricatorWorkerTriggerEvent' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTriggerEvent.php', 'PhabricatorWorkerTriggerManagementFireWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementFireWorkflow.php', 'PhabricatorWorkerTriggerManagementWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementWorkflow.php', 'PhabricatorWorkerTriggerPHIDType' => 'infrastructure/daemon/workers/phid/PhabricatorWorkerTriggerPHIDType.php', 'PhabricatorWorkerTriggerQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php', 'PhabricatorWorkerYieldException' => 'infrastructure/daemon/workers/exception/PhabricatorWorkerYieldException.php', 'PhabricatorWorkingCopyDiscoveryTestCase' => 'applications/repository/engine/__tests__/PhabricatorWorkingCopyDiscoveryTestCase.php', 'PhabricatorWorkingCopyPullTestCase' => 'applications/repository/engine/__tests__/PhabricatorWorkingCopyPullTestCase.php', 'PhabricatorWorkingCopyTestCase' => 'applications/repository/engine/__tests__/PhabricatorWorkingCopyTestCase.php', 'PhabricatorXHPASTDAO' => 'applications/phpast/storage/PhabricatorXHPASTDAO.php', 'PhabricatorXHPASTParseTree' => 'applications/phpast/storage/PhabricatorXHPASTParseTree.php', 'PhabricatorXHPASTViewController' => 'applications/phpast/controller/PhabricatorXHPASTViewController.php', 'PhabricatorXHPASTViewFrameController' => 'applications/phpast/controller/PhabricatorXHPASTViewFrameController.php', 'PhabricatorXHPASTViewFramesetController' => 'applications/phpast/controller/PhabricatorXHPASTViewFramesetController.php', 'PhabricatorXHPASTViewInputController' => 'applications/phpast/controller/PhabricatorXHPASTViewInputController.php', 'PhabricatorXHPASTViewPanelController' => 'applications/phpast/controller/PhabricatorXHPASTViewPanelController.php', 'PhabricatorXHPASTViewRunController' => 'applications/phpast/controller/PhabricatorXHPASTViewRunController.php', 'PhabricatorXHPASTViewStreamController' => 'applications/phpast/controller/PhabricatorXHPASTViewStreamController.php', 'PhabricatorXHPASTViewTreeController' => 'applications/phpast/controller/PhabricatorXHPASTViewTreeController.php', 'PhabricatorXHProfApplication' => 'applications/xhprof/application/PhabricatorXHProfApplication.php', 'PhabricatorXHProfController' => 'applications/xhprof/controller/PhabricatorXHProfController.php', 'PhabricatorXHProfDAO' => 'applications/xhprof/storage/PhabricatorXHProfDAO.php', 'PhabricatorXHProfDropController' => 'applications/xhprof/controller/PhabricatorXHProfDropController.php', 'PhabricatorXHProfProfileController' => 'applications/xhprof/controller/PhabricatorXHProfProfileController.php', 'PhabricatorXHProfProfileSymbolView' => 'applications/xhprof/view/PhabricatorXHProfProfileSymbolView.php', 'PhabricatorXHProfProfileTopLevelView' => 'applications/xhprof/view/PhabricatorXHProfProfileTopLevelView.php', 'PhabricatorXHProfProfileView' => 'applications/xhprof/view/PhabricatorXHProfProfileView.php', 'PhabricatorXHProfSample' => 'applications/xhprof/storage/PhabricatorXHProfSample.php', 'PhabricatorXHProfSampleListController' => 'applications/xhprof/controller/PhabricatorXHProfSampleListController.php', 'PhabricatorXHProfSampleQuery' => 'applications/xhprof/query/PhabricatorXHProfSampleQuery.php', 'PhabricatorXHProfSampleSearchEngine' => 'applications/xhprof/query/PhabricatorXHProfSampleSearchEngine.php', 'PhabricatorYoutubeRemarkupRule' => 'infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php', 'Phame404Response' => 'applications/phame/site/Phame404Response.php', 'PhameBlog' => 'applications/phame/storage/PhameBlog.php', 'PhameBlog404Controller' => 'applications/phame/controller/blog/PhameBlog404Controller.php', 'PhameBlogArchiveController' => 'applications/phame/controller/blog/PhameBlogArchiveController.php', 'PhameBlogController' => 'applications/phame/controller/blog/PhameBlogController.php', 'PhameBlogCreateCapability' => 'applications/phame/capability/PhameBlogCreateCapability.php', 'PhameBlogDatasource' => 'applications/phame/typeahead/PhameBlogDatasource.php', 'PhameBlogDescriptionTransaction' => 'applications/phame/xaction/PhameBlogDescriptionTransaction.php', 'PhameBlogEditConduitAPIMethod' => 'applications/phame/conduit/PhameBlogEditConduitAPIMethod.php', 'PhameBlogEditController' => 'applications/phame/controller/blog/PhameBlogEditController.php', 'PhameBlogEditEngine' => 'applications/phame/editor/PhameBlogEditEngine.php', 'PhameBlogEditor' => 'applications/phame/editor/PhameBlogEditor.php', 'PhameBlogFeedController' => 'applications/phame/controller/blog/PhameBlogFeedController.php', 'PhameBlogFerretEngine' => 'applications/phame/search/PhameBlogFerretEngine.php', 'PhameBlogFullDomainTransaction' => 'applications/phame/xaction/PhameBlogFullDomainTransaction.php', 'PhameBlogFulltextEngine' => 'applications/phame/search/PhameBlogFulltextEngine.php', 'PhameBlogHeaderImageTransaction' => 'applications/phame/xaction/PhameBlogHeaderImageTransaction.php', 'PhameBlogHeaderPictureController' => 'applications/phame/controller/blog/PhameBlogHeaderPictureController.php', 'PhameBlogListController' => 'applications/phame/controller/blog/PhameBlogListController.php', 'PhameBlogListView' => 'applications/phame/view/PhameBlogListView.php', 'PhameBlogManageController' => 'applications/phame/controller/blog/PhameBlogManageController.php', 'PhameBlogNameTransaction' => 'applications/phame/xaction/PhameBlogNameTransaction.php', 'PhameBlogParentDomainTransaction' => 'applications/phame/xaction/PhameBlogParentDomainTransaction.php', 'PhameBlogParentSiteTransaction' => 'applications/phame/xaction/PhameBlogParentSiteTransaction.php', 'PhameBlogProfileImageTransaction' => 'applications/phame/xaction/PhameBlogProfileImageTransaction.php', 'PhameBlogProfilePictureController' => 'applications/phame/controller/blog/PhameBlogProfilePictureController.php', 'PhameBlogQuery' => 'applications/phame/query/PhameBlogQuery.php', 'PhameBlogReplyHandler' => 'applications/phame/mail/PhameBlogReplyHandler.php', 'PhameBlogSearchConduitAPIMethod' => 'applications/phame/conduit/PhameBlogSearchConduitAPIMethod.php', 'PhameBlogSearchEngine' => 'applications/phame/query/PhameBlogSearchEngine.php', 'PhameBlogSite' => 'applications/phame/site/PhameBlogSite.php', 'PhameBlogStatusTransaction' => 'applications/phame/xaction/PhameBlogStatusTransaction.php', 'PhameBlogSubtitleTransaction' => 'applications/phame/xaction/PhameBlogSubtitleTransaction.php', 'PhameBlogTransaction' => 'applications/phame/storage/PhameBlogTransaction.php', 'PhameBlogTransactionQuery' => 'applications/phame/query/PhameBlogTransactionQuery.php', 'PhameBlogTransactionType' => 'applications/phame/xaction/PhameBlogTransactionType.php', 'PhameBlogViewController' => 'applications/phame/controller/blog/PhameBlogViewController.php', 'PhameConstants' => 'applications/phame/constants/PhameConstants.php', 'PhameController' => 'applications/phame/controller/PhameController.php', 'PhameDAO' => 'applications/phame/storage/PhameDAO.php', 'PhameDescriptionView' => 'applications/phame/view/PhameDescriptionView.php', 'PhameDraftListView' => 'applications/phame/view/PhameDraftListView.php', 'PhameHomeController' => 'applications/phame/controller/PhameHomeController.php', 'PhameLiveController' => 'applications/phame/controller/PhameLiveController.php', 'PhameNextPostView' => 'applications/phame/view/PhameNextPostView.php', 'PhamePost' => 'applications/phame/storage/PhamePost.php', 'PhamePostArchiveController' => 'applications/phame/controller/post/PhamePostArchiveController.php', 'PhamePostBlogTransaction' => 'applications/phame/xaction/PhamePostBlogTransaction.php', 'PhamePostBodyTransaction' => 'applications/phame/xaction/PhamePostBodyTransaction.php', 'PhamePostController' => 'applications/phame/controller/post/PhamePostController.php', 'PhamePostEditConduitAPIMethod' => 'applications/phame/conduit/PhamePostEditConduitAPIMethod.php', 'PhamePostEditController' => 'applications/phame/controller/post/PhamePostEditController.php', 'PhamePostEditEngine' => 'applications/phame/editor/PhamePostEditEngine.php', 'PhamePostEditor' => 'applications/phame/editor/PhamePostEditor.php', 'PhamePostFerretEngine' => 'applications/phame/search/PhamePostFerretEngine.php', 'PhamePostFulltextEngine' => 'applications/phame/search/PhamePostFulltextEngine.php', 'PhamePostHeaderImageTransaction' => 'applications/phame/xaction/PhamePostHeaderImageTransaction.php', 'PhamePostHeaderPictureController' => 'applications/phame/controller/post/PhamePostHeaderPictureController.php', 'PhamePostHistoryController' => 'applications/phame/controller/post/PhamePostHistoryController.php', 'PhamePostListController' => 'applications/phame/controller/post/PhamePostListController.php', 'PhamePostListView' => 'applications/phame/view/PhamePostListView.php', 'PhamePostMailReceiver' => 'applications/phame/mail/PhamePostMailReceiver.php', 'PhamePostMoveController' => 'applications/phame/controller/post/PhamePostMoveController.php', 'PhamePostPublishController' => 'applications/phame/controller/post/PhamePostPublishController.php', 'PhamePostQuery' => 'applications/phame/query/PhamePostQuery.php', 'PhamePostRemarkupRule' => 'applications/phame/remarkup/PhamePostRemarkupRule.php', 'PhamePostReplyHandler' => 'applications/phame/mail/PhamePostReplyHandler.php', 'PhamePostSearchConduitAPIMethod' => 'applications/phame/conduit/PhamePostSearchConduitAPIMethod.php', 'PhamePostSearchEngine' => 'applications/phame/query/PhamePostSearchEngine.php', 'PhamePostSubtitleTransaction' => 'applications/phame/xaction/PhamePostSubtitleTransaction.php', 'PhamePostTitleTransaction' => 'applications/phame/xaction/PhamePostTitleTransaction.php', 'PhamePostTransaction' => 'applications/phame/storage/PhamePostTransaction.php', 'PhamePostTransactionComment' => 'applications/phame/storage/PhamePostTransactionComment.php', 'PhamePostTransactionQuery' => 'applications/phame/query/PhamePostTransactionQuery.php', 'PhamePostTransactionType' => 'applications/phame/xaction/PhamePostTransactionType.php', 'PhamePostViewController' => 'applications/phame/controller/post/PhamePostViewController.php', 'PhamePostVisibilityTransaction' => 'applications/phame/xaction/PhamePostVisibilityTransaction.php', 'PhameSchemaSpec' => 'applications/phame/storage/PhameSchemaSpec.php', 'PhameSite' => 'applications/phame/site/PhameSite.php', 'PhluxController' => 'applications/phlux/controller/PhluxController.php', 'PhluxDAO' => 'applications/phlux/storage/PhluxDAO.php', 'PhluxEditController' => 'applications/phlux/controller/PhluxEditController.php', 'PhluxListController' => 'applications/phlux/controller/PhluxListController.php', 'PhluxSchemaSpec' => 'applications/phlux/storage/PhluxSchemaSpec.php', 'PhluxTransaction' => 'applications/phlux/storage/PhluxTransaction.php', 'PhluxTransactionQuery' => 'applications/phlux/query/PhluxTransactionQuery.php', 'PhluxVariable' => 'applications/phlux/storage/PhluxVariable.php', 'PhluxVariableEditor' => 'applications/phlux/editor/PhluxVariableEditor.php', 'PhluxVariablePHIDType' => 'applications/phlux/phid/PhluxVariablePHIDType.php', 'PhluxVariableQuery' => 'applications/phlux/query/PhluxVariableQuery.php', 'PhluxViewController' => 'applications/phlux/controller/PhluxViewController.php', 'PholioController' => 'applications/pholio/controller/PholioController.php', 'PholioDAO' => 'applications/pholio/storage/PholioDAO.php', 'PholioDefaultEditCapability' => 'applications/pholio/capability/PholioDefaultEditCapability.php', 'PholioDefaultViewCapability' => 'applications/pholio/capability/PholioDefaultViewCapability.php', 'PholioImage' => 'applications/pholio/storage/PholioImage.php', 'PholioImageDescriptionTransaction' => 'applications/pholio/xaction/PholioImageDescriptionTransaction.php', 'PholioImageFileTransaction' => 'applications/pholio/xaction/PholioImageFileTransaction.php', 'PholioImageNameTransaction' => 'applications/pholio/xaction/PholioImageNameTransaction.php', 'PholioImagePHIDType' => 'applications/pholio/phid/PholioImagePHIDType.php', 'PholioImageQuery' => 'applications/pholio/query/PholioImageQuery.php', 'PholioImageReplaceTransaction' => 'applications/pholio/xaction/PholioImageReplaceTransaction.php', 'PholioImageSequenceTransaction' => 'applications/pholio/xaction/PholioImageSequenceTransaction.php', 'PholioImageTransactionType' => 'applications/pholio/xaction/PholioImageTransactionType.php', 'PholioImageUploadController' => 'applications/pholio/controller/PholioImageUploadController.php', 'PholioInlineController' => 'applications/pholio/controller/PholioInlineController.php', 'PholioInlineListController' => 'applications/pholio/controller/PholioInlineListController.php', 'PholioMock' => 'applications/pholio/storage/PholioMock.php', 'PholioMockArchiveController' => 'applications/pholio/controller/PholioMockArchiveController.php', 'PholioMockAuthorHeraldField' => 'applications/pholio/herald/PholioMockAuthorHeraldField.php', 'PholioMockCommentController' => 'applications/pholio/controller/PholioMockCommentController.php', 'PholioMockDescriptionHeraldField' => 'applications/pholio/herald/PholioMockDescriptionHeraldField.php', 'PholioMockDescriptionTransaction' => 'applications/pholio/xaction/PholioMockDescriptionTransaction.php', 'PholioMockEditController' => 'applications/pholio/controller/PholioMockEditController.php', 'PholioMockEditor' => 'applications/pholio/editor/PholioMockEditor.php', 'PholioMockEmbedView' => 'applications/pholio/view/PholioMockEmbedView.php', 'PholioMockFerretEngine' => 'applications/pholio/search/PholioMockFerretEngine.php', 'PholioMockFulltextEngine' => 'applications/pholio/search/PholioMockFulltextEngine.php', 'PholioMockHasTaskEdgeType' => 'applications/pholio/edge/PholioMockHasTaskEdgeType.php', 'PholioMockHasTaskRelationship' => 'applications/pholio/relationships/PholioMockHasTaskRelationship.php', 'PholioMockHeraldField' => 'applications/pholio/herald/PholioMockHeraldField.php', 'PholioMockHeraldFieldGroup' => 'applications/pholio/herald/PholioMockHeraldFieldGroup.php', 'PholioMockImagesView' => 'applications/pholio/view/PholioMockImagesView.php', 'PholioMockInlineTransaction' => 'applications/pholio/xaction/PholioMockInlineTransaction.php', 'PholioMockListController' => 'applications/pholio/controller/PholioMockListController.php', 'PholioMockMailReceiver' => 'applications/pholio/mail/PholioMockMailReceiver.php', 'PholioMockNameHeraldField' => 'applications/pholio/herald/PholioMockNameHeraldField.php', 'PholioMockNameTransaction' => 'applications/pholio/xaction/PholioMockNameTransaction.php', 'PholioMockPHIDType' => 'applications/pholio/phid/PholioMockPHIDType.php', 'PholioMockQuery' => 'applications/pholio/query/PholioMockQuery.php', 'PholioMockRelationship' => 'applications/pholio/relationships/PholioMockRelationship.php', 'PholioMockRelationshipSource' => 'applications/search/relationship/PholioMockRelationshipSource.php', 'PholioMockSearchEngine' => 'applications/pholio/query/PholioMockSearchEngine.php', 'PholioMockStatusTransaction' => 'applications/pholio/xaction/PholioMockStatusTransaction.php', 'PholioMockThumbGridView' => 'applications/pholio/view/PholioMockThumbGridView.php', 'PholioMockTransactionType' => 'applications/pholio/xaction/PholioMockTransactionType.php', 'PholioMockViewController' => 'applications/pholio/controller/PholioMockViewController.php', 'PholioRemarkupRule' => 'applications/pholio/remarkup/PholioRemarkupRule.php', 'PholioReplyHandler' => 'applications/pholio/mail/PholioReplyHandler.php', 'PholioSchemaSpec' => 'applications/pholio/storage/PholioSchemaSpec.php', 'PholioTransaction' => 'applications/pholio/storage/PholioTransaction.php', 'PholioTransactionComment' => 'applications/pholio/storage/PholioTransactionComment.php', 'PholioTransactionQuery' => 'applications/pholio/query/PholioTransactionQuery.php', 'PholioTransactionType' => 'applications/pholio/xaction/PholioTransactionType.php', 'PholioTransactionView' => 'applications/pholio/view/PholioTransactionView.php', 'PholioUploadedImageView' => 'applications/pholio/view/PholioUploadedImageView.php', 'PhortuneAccount' => 'applications/phortune/storage/PhortuneAccount.php', 'PhortuneAccountAddManagerController' => 'applications/phortune/controller/account/PhortuneAccountAddManagerController.php', 'PhortuneAccountBillingController' => 'applications/phortune/controller/account/PhortuneAccountBillingController.php', 'PhortuneAccountChargeListController' => 'applications/phortune/controller/account/PhortuneAccountChargeListController.php', 'PhortuneAccountController' => 'applications/phortune/controller/account/PhortuneAccountController.php', 'PhortuneAccountEditController' => 'applications/phortune/controller/account/PhortuneAccountEditController.php', 'PhortuneAccountEditEngine' => 'applications/phortune/editor/PhortuneAccountEditEngine.php', 'PhortuneAccountEditor' => 'applications/phortune/editor/PhortuneAccountEditor.php', 'PhortuneAccountHasMemberEdgeType' => 'applications/phortune/edge/PhortuneAccountHasMemberEdgeType.php', 'PhortuneAccountListController' => 'applications/phortune/controller/account/PhortuneAccountListController.php', 'PhortuneAccountManagerController' => 'applications/phortune/controller/account/PhortuneAccountManagerController.php', 'PhortuneAccountNameTransaction' => 'applications/phortune/xaction/PhortuneAccountNameTransaction.php', 'PhortuneAccountPHIDType' => 'applications/phortune/phid/PhortuneAccountPHIDType.php', 'PhortuneAccountProfileController' => 'applications/phortune/controller/account/PhortuneAccountProfileController.php', 'PhortuneAccountQuery' => 'applications/phortune/query/PhortuneAccountQuery.php', 'PhortuneAccountSubscriptionController' => 'applications/phortune/controller/account/PhortuneAccountSubscriptionController.php', 'PhortuneAccountTransaction' => 'applications/phortune/storage/PhortuneAccountTransaction.php', 'PhortuneAccountTransactionQuery' => 'applications/phortune/query/PhortuneAccountTransactionQuery.php', 'PhortuneAccountTransactionType' => 'applications/phortune/xaction/PhortuneAccountTransactionType.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/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/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/cart/PhortuneCartUpdateController.php', 'PhortuneCartViewController' => 'applications/phortune/controller/cart/PhortuneCartViewController.php', 'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php', 'PhortuneChargePHIDType' => 'applications/phortune/phid/PhortuneChargePHIDType.php', 'PhortuneChargeQuery' => 'applications/phortune/query/PhortuneChargeQuery.php', 'PhortuneChargeSearchEngine' => 'applications/phortune/query/PhortuneChargeSearchEngine.php', 'PhortuneChargeTableView' => 'applications/phortune/view/PhortuneChargeTableView.php', 'PhortuneConstants' => 'applications/phortune/constants/PhortuneConstants.php', 'PhortuneController' => 'applications/phortune/controller/PhortuneController.php', 'PhortuneCreditCardForm' => 'applications/phortune/view/PhortuneCreditCardForm.php', 'PhortuneCurrency' => 'applications/phortune/currency/PhortuneCurrency.php', 'PhortuneCurrencySerializer' => 'applications/phortune/currency/PhortuneCurrencySerializer.php', 'PhortuneCurrencyTestCase' => 'applications/phortune/currency/__tests__/PhortuneCurrencyTestCase.php', 'PhortuneDAO' => 'applications/phortune/storage/PhortuneDAO.php', 'PhortuneErrCode' => 'applications/phortune/constants/PhortuneErrCode.php', 'PhortuneInvoiceView' => 'applications/phortune/view/PhortuneInvoiceView.php', 'PhortuneLandingController' => 'applications/phortune/controller/PhortuneLandingController.php', 'PhortuneMemberHasAccountEdgeType' => 'applications/phortune/edge/PhortuneMemberHasAccountEdgeType.php', 'PhortuneMemberHasMerchantEdgeType' => 'applications/phortune/edge/PhortuneMemberHasMerchantEdgeType.php', 'PhortuneMerchant' => 'applications/phortune/storage/PhortuneMerchant.php', 'PhortuneMerchantAddManagerController' => 'applications/phortune/controller/merchant/PhortuneMerchantAddManagerController.php', 'PhortuneMerchantCapability' => 'applications/phortune/capability/PhortuneMerchantCapability.php', 'PhortuneMerchantContactInfoTransaction' => 'applications/phortune/xaction/PhortuneMerchantContactInfoTransaction.php', 'PhortuneMerchantController' => 'applications/phortune/controller/merchant/PhortuneMerchantController.php', 'PhortuneMerchantDescriptionTransaction' => 'applications/phortune/xaction/PhortuneMerchantDescriptionTransaction.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/merchant/PhortuneMerchantInvoiceCreateController.php', 'PhortuneMerchantInvoiceEmailTransaction' => 'applications/phortune/xaction/PhortuneMerchantInvoiceEmailTransaction.php', 'PhortuneMerchantInvoiceFooterTransaction' => 'applications/phortune/xaction/PhortuneMerchantInvoiceFooterTransaction.php', 'PhortuneMerchantListController' => 'applications/phortune/controller/merchant/PhortuneMerchantListController.php', 'PhortuneMerchantManagerController' => 'applications/phortune/controller/merchant/PhortuneMerchantManagerController.php', 'PhortuneMerchantNameTransaction' => 'applications/phortune/xaction/PhortuneMerchantNameTransaction.php', 'PhortuneMerchantPHIDType' => 'applications/phortune/phid/PhortuneMerchantPHIDType.php', 'PhortuneMerchantPictureController' => 'applications/phortune/controller/merchant/PhortuneMerchantPictureController.php', 'PhortuneMerchantPictureTransaction' => 'applications/phortune/xaction/PhortuneMerchantPictureTransaction.php', 'PhortuneMerchantProfileController' => 'applications/phortune/controller/merchant/PhortuneMerchantProfileController.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', 'PhortuneMerchantTransactionType' => 'applications/phortune/xaction/PhortuneMerchantTransactionType.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/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', 'PhortunePaymentProviderConfig' => 'applications/phortune/storage/PhortunePaymentProviderConfig.php', 'PhortunePaymentProviderConfigEditor' => 'applications/phortune/editor/PhortunePaymentProviderConfigEditor.php', 'PhortunePaymentProviderConfigQuery' => 'applications/phortune/query/PhortunePaymentProviderConfigQuery.php', 'PhortunePaymentProviderConfigTransaction' => 'applications/phortune/storage/PhortunePaymentProviderConfigTransaction.php', 'PhortunePaymentProviderConfigTransactionQuery' => 'applications/phortune/query/PhortunePaymentProviderConfigTransactionQuery.php', 'PhortunePaymentProviderPHIDType' => 'applications/phortune/phid/PhortunePaymentProviderPHIDType.php', 'PhortunePaymentProviderTestCase' => 'applications/phortune/provider/__tests__/PhortunePaymentProviderTestCase.php', 'PhortuneProduct' => 'applications/phortune/storage/PhortuneProduct.php', 'PhortuneProductImplementation' => 'applications/phortune/product/PhortuneProductImplementation.php', 'PhortuneProductListController' => 'applications/phortune/controller/product/PhortuneProductListController.php', 'PhortuneProductPHIDType' => 'applications/phortune/phid/PhortuneProductPHIDType.php', 'PhortuneProductQuery' => 'applications/phortune/query/PhortuneProductQuery.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', 'PhortuneSchemaSpec' => 'applications/phortune/storage/PhortuneSchemaSpec.php', 'PhortuneStripePaymentProvider' => 'applications/phortune/provider/PhortuneStripePaymentProvider.php', 'PhortuneSubscription' => 'applications/phortune/storage/PhortuneSubscription.php', 'PhortuneSubscriptionCart' => 'applications/phortune/cart/PhortuneSubscriptionCart.php', 'PhortuneSubscriptionEditController' => 'applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php', 'PhortuneSubscriptionImplementation' => 'applications/phortune/subscription/PhortuneSubscriptionImplementation.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/subscription/PhortuneSubscriptionViewController.php', 'PhortuneSubscriptionWorker' => 'applications/phortune/worker/PhortuneSubscriptionWorker.php', 'PhortuneTestPaymentProvider' => 'applications/phortune/provider/PhortuneTestPaymentProvider.php', 'PhortuneWePayPaymentProvider' => 'applications/phortune/provider/PhortuneWePayPaymentProvider.php', 'PhragmentBrowseController' => 'applications/phragment/controller/PhragmentBrowseController.php', 'PhragmentCanCreateCapability' => 'applications/phragment/capability/PhragmentCanCreateCapability.php', 'PhragmentConduitAPIMethod' => 'applications/phragment/conduit/PhragmentConduitAPIMethod.php', 'PhragmentController' => 'applications/phragment/controller/PhragmentController.php', 'PhragmentCreateController' => 'applications/phragment/controller/PhragmentCreateController.php', 'PhragmentDAO' => 'applications/phragment/storage/PhragmentDAO.php', 'PhragmentFragment' => 'applications/phragment/storage/PhragmentFragment.php', 'PhragmentFragmentPHIDType' => 'applications/phragment/phid/PhragmentFragmentPHIDType.php', 'PhragmentFragmentQuery' => 'applications/phragment/query/PhragmentFragmentQuery.php', 'PhragmentFragmentVersion' => 'applications/phragment/storage/PhragmentFragmentVersion.php', 'PhragmentFragmentVersionPHIDType' => 'applications/phragment/phid/PhragmentFragmentVersionPHIDType.php', 'PhragmentFragmentVersionQuery' => 'applications/phragment/query/PhragmentFragmentVersionQuery.php', 'PhragmentGetPatchConduitAPIMethod' => 'applications/phragment/conduit/PhragmentGetPatchConduitAPIMethod.php', 'PhragmentHistoryController' => 'applications/phragment/controller/PhragmentHistoryController.php', 'PhragmentPatchController' => 'applications/phragment/controller/PhragmentPatchController.php', 'PhragmentPatchUtil' => 'applications/phragment/util/PhragmentPatchUtil.php', 'PhragmentPolicyController' => 'applications/phragment/controller/PhragmentPolicyController.php', 'PhragmentQueryFragmentsConduitAPIMethod' => 'applications/phragment/conduit/PhragmentQueryFragmentsConduitAPIMethod.php', 'PhragmentRevertController' => 'applications/phragment/controller/PhragmentRevertController.php', 'PhragmentSchemaSpec' => 'applications/phragment/storage/PhragmentSchemaSpec.php', 'PhragmentSnapshot' => 'applications/phragment/storage/PhragmentSnapshot.php', 'PhragmentSnapshotChild' => 'applications/phragment/storage/PhragmentSnapshotChild.php', 'PhragmentSnapshotChildQuery' => 'applications/phragment/query/PhragmentSnapshotChildQuery.php', 'PhragmentSnapshotCreateController' => 'applications/phragment/controller/PhragmentSnapshotCreateController.php', 'PhragmentSnapshotDeleteController' => 'applications/phragment/controller/PhragmentSnapshotDeleteController.php', 'PhragmentSnapshotPHIDType' => 'applications/phragment/phid/PhragmentSnapshotPHIDType.php', 'PhragmentSnapshotPromoteController' => 'applications/phragment/controller/PhragmentSnapshotPromoteController.php', 'PhragmentSnapshotQuery' => 'applications/phragment/query/PhragmentSnapshotQuery.php', 'PhragmentSnapshotViewController' => 'applications/phragment/controller/PhragmentSnapshotViewController.php', 'PhragmentUpdateController' => 'applications/phragment/controller/PhragmentUpdateController.php', 'PhragmentVersionController' => 'applications/phragment/controller/PhragmentVersionController.php', 'PhragmentZIPController' => 'applications/phragment/controller/PhragmentZIPController.php', 'PhrequentConduitAPIMethod' => 'applications/phrequent/conduit/PhrequentConduitAPIMethod.php', 'PhrequentController' => 'applications/phrequent/controller/PhrequentController.php', 'PhrequentCurtainExtension' => 'applications/phrequent/engineextension/PhrequentCurtainExtension.php', 'PhrequentDAO' => 'applications/phrequent/storage/PhrequentDAO.php', 'PhrequentListController' => 'applications/phrequent/controller/PhrequentListController.php', 'PhrequentPopConduitAPIMethod' => 'applications/phrequent/conduit/PhrequentPopConduitAPIMethod.php', 'PhrequentPushConduitAPIMethod' => 'applications/phrequent/conduit/PhrequentPushConduitAPIMethod.php', 'PhrequentSearchEngine' => 'applications/phrequent/query/PhrequentSearchEngine.php', 'PhrequentTimeBlock' => 'applications/phrequent/storage/PhrequentTimeBlock.php', 'PhrequentTimeBlockTestCase' => 'applications/phrequent/storage/__tests__/PhrequentTimeBlockTestCase.php', 'PhrequentTimeSlices' => 'applications/phrequent/storage/PhrequentTimeSlices.php', 'PhrequentTrackController' => 'applications/phrequent/controller/PhrequentTrackController.php', 'PhrequentTrackableInterface' => 'applications/phrequent/interface/PhrequentTrackableInterface.php', 'PhrequentTrackingConduitAPIMethod' => 'applications/phrequent/conduit/PhrequentTrackingConduitAPIMethod.php', 'PhrequentTrackingEditor' => 'applications/phrequent/editor/PhrequentTrackingEditor.php', 'PhrequentUIEventListener' => 'applications/phrequent/event/PhrequentUIEventListener.php', 'PhrequentUserTime' => 'applications/phrequent/storage/PhrequentUserTime.php', 'PhrequentUserTimeQuery' => 'applications/phrequent/query/PhrequentUserTimeQuery.php', 'PhrictionChangeType' => 'applications/phriction/constants/PhrictionChangeType.php', 'PhrictionConduitAPIMethod' => 'applications/phriction/conduit/PhrictionConduitAPIMethod.php', 'PhrictionConstants' => 'applications/phriction/constants/PhrictionConstants.php', 'PhrictionContent' => 'applications/phriction/storage/PhrictionContent.php', 'PhrictionContentPHIDType' => 'applications/phriction/phid/PhrictionContentPHIDType.php', 'PhrictionContentQuery' => 'applications/phriction/query/PhrictionContentQuery.php', 'PhrictionContentSearchConduitAPIMethod' => 'applications/phriction/conduit/PhrictionContentSearchConduitAPIMethod.php', 'PhrictionContentSearchEngine' => 'applications/phriction/query/PhrictionContentSearchEngine.php', 'PhrictionContentSearchEngineAttachment' => 'applications/phriction/engineextension/PhrictionContentSearchEngineAttachment.php', 'PhrictionController' => 'applications/phriction/controller/PhrictionController.php', 'PhrictionCreateConduitAPIMethod' => 'applications/phriction/conduit/PhrictionCreateConduitAPIMethod.php', 'PhrictionDAO' => 'applications/phriction/storage/PhrictionDAO.php', 'PhrictionDatasourceEngineExtension' => 'applications/phriction/engineextension/PhrictionDatasourceEngineExtension.php', 'PhrictionDeleteController' => 'applications/phriction/controller/PhrictionDeleteController.php', 'PhrictionDiffController' => 'applications/phriction/controller/PhrictionDiffController.php', 'PhrictionDocument' => 'applications/phriction/storage/PhrictionDocument.php', 'PhrictionDocumentAuthorHeraldField' => 'applications/phriction/herald/PhrictionDocumentAuthorHeraldField.php', 'PhrictionDocumentContentHeraldField' => 'applications/phriction/herald/PhrictionDocumentContentHeraldField.php', 'PhrictionDocumentContentTransaction' => 'applications/phriction/xaction/PhrictionDocumentContentTransaction.php', 'PhrictionDocumentController' => 'applications/phriction/controller/PhrictionDocumentController.php', 'PhrictionDocumentDatasource' => 'applications/phriction/typeahead/PhrictionDocumentDatasource.php', 'PhrictionDocumentDeleteTransaction' => 'applications/phriction/xaction/PhrictionDocumentDeleteTransaction.php', 'PhrictionDocumentDraftTransaction' => 'applications/phriction/xaction/PhrictionDocumentDraftTransaction.php', 'PhrictionDocumentEditEngine' => 'applications/phriction/editor/PhrictionDocumentEditEngine.php', 'PhrictionDocumentEditTransaction' => 'applications/phriction/xaction/PhrictionDocumentEditTransaction.php', 'PhrictionDocumentFerretEngine' => 'applications/phriction/search/PhrictionDocumentFerretEngine.php', 'PhrictionDocumentFulltextEngine' => 'applications/phriction/search/PhrictionDocumentFulltextEngine.php', 'PhrictionDocumentHeraldAdapter' => 'applications/phriction/herald/PhrictionDocumentHeraldAdapter.php', 'PhrictionDocumentHeraldField' => 'applications/phriction/herald/PhrictionDocumentHeraldField.php', 'PhrictionDocumentHeraldFieldGroup' => 'applications/phriction/herald/PhrictionDocumentHeraldFieldGroup.php', 'PhrictionDocumentMoveAwayTransaction' => 'applications/phriction/xaction/PhrictionDocumentMoveAwayTransaction.php', 'PhrictionDocumentMoveToTransaction' => 'applications/phriction/xaction/PhrictionDocumentMoveToTransaction.php', 'PhrictionDocumentPHIDType' => 'applications/phriction/phid/PhrictionDocumentPHIDType.php', 'PhrictionDocumentPathHeraldField' => 'applications/phriction/herald/PhrictionDocumentPathHeraldField.php', 'PhrictionDocumentPolicyCodex' => 'applications/phriction/codex/PhrictionDocumentPolicyCodex.php', 'PhrictionDocumentPublishTransaction' => 'applications/phriction/xaction/PhrictionDocumentPublishTransaction.php', 'PhrictionDocumentQuery' => 'applications/phriction/query/PhrictionDocumentQuery.php', 'PhrictionDocumentSearchConduitAPIMethod' => 'applications/phriction/conduit/PhrictionDocumentSearchConduitAPIMethod.php', 'PhrictionDocumentSearchEngine' => 'applications/phriction/query/PhrictionDocumentSearchEngine.php', 'PhrictionDocumentStatus' => 'applications/phriction/constants/PhrictionDocumentStatus.php', 'PhrictionDocumentTitleHeraldField' => 'applications/phriction/herald/PhrictionDocumentTitleHeraldField.php', 'PhrictionDocumentTitleTransaction' => 'applications/phriction/xaction/PhrictionDocumentTitleTransaction.php', 'PhrictionDocumentTransactionType' => 'applications/phriction/xaction/PhrictionDocumentTransactionType.php', 'PhrictionDocumentVersionTransaction' => 'applications/phriction/xaction/PhrictionDocumentVersionTransaction.php', 'PhrictionEditConduitAPIMethod' => 'applications/phriction/conduit/PhrictionEditConduitAPIMethod.php', 'PhrictionEditController' => 'applications/phriction/controller/PhrictionEditController.php', 'PhrictionEditEngineController' => 'applications/phriction/controller/PhrictionEditEngineController.php', 'PhrictionHistoryConduitAPIMethod' => 'applications/phriction/conduit/PhrictionHistoryConduitAPIMethod.php', 'PhrictionHistoryController' => 'applications/phriction/controller/PhrictionHistoryController.php', 'PhrictionInfoConduitAPIMethod' => 'applications/phriction/conduit/PhrictionInfoConduitAPIMethod.php', 'PhrictionListController' => 'applications/phriction/controller/PhrictionListController.php', 'PhrictionMarkupPreviewController' => 'applications/phriction/controller/PhrictionMarkupPreviewController.php', 'PhrictionMoveController' => 'applications/phriction/controller/PhrictionMoveController.php', 'PhrictionNewController' => 'applications/phriction/controller/PhrictionNewController.php', 'PhrictionPublishController' => 'applications/phriction/controller/PhrictionPublishController.php', 'PhrictionRemarkupRule' => 'applications/phriction/markup/PhrictionRemarkupRule.php', 'PhrictionReplyHandler' => 'applications/phriction/mail/PhrictionReplyHandler.php', 'PhrictionSchemaSpec' => 'applications/phriction/storage/PhrictionSchemaSpec.php', 'PhrictionTransaction' => 'applications/phriction/storage/PhrictionTransaction.php', 'PhrictionTransactionComment' => 'applications/phriction/storage/PhrictionTransactionComment.php', 'PhrictionTransactionEditor' => 'applications/phriction/editor/PhrictionTransactionEditor.php', 'PhrictionTransactionQuery' => 'applications/phriction/query/PhrictionTransactionQuery.php', 'PolicyLockOptionType' => 'applications/policy/config/PolicyLockOptionType.php', 'PonderAddAnswerView' => 'applications/ponder/view/PonderAddAnswerView.php', 'PonderAnswer' => 'applications/ponder/storage/PonderAnswer.php', 'PonderAnswerCommentController' => 'applications/ponder/controller/PonderAnswerCommentController.php', 'PonderAnswerContentTransaction' => 'applications/ponder/xaction/PonderAnswerContentTransaction.php', 'PonderAnswerEditController' => 'applications/ponder/controller/PonderAnswerEditController.php', 'PonderAnswerEditor' => 'applications/ponder/editor/PonderAnswerEditor.php', 'PonderAnswerHistoryController' => 'applications/ponder/controller/PonderAnswerHistoryController.php', 'PonderAnswerMailReceiver' => 'applications/ponder/mail/PonderAnswerMailReceiver.php', 'PonderAnswerPHIDType' => 'applications/ponder/phid/PonderAnswerPHIDType.php', 'PonderAnswerQuery' => 'applications/ponder/query/PonderAnswerQuery.php', 'PonderAnswerQuestionIDTransaction' => 'applications/ponder/xaction/PonderAnswerQuestionIDTransaction.php', 'PonderAnswerReplyHandler' => 'applications/ponder/mail/PonderAnswerReplyHandler.php', 'PonderAnswerSaveController' => 'applications/ponder/controller/PonderAnswerSaveController.php', 'PonderAnswerStatus' => 'applications/ponder/constants/PonderAnswerStatus.php', 'PonderAnswerStatusTransaction' => 'applications/ponder/xaction/PonderAnswerStatusTransaction.php', 'PonderAnswerTransaction' => 'applications/ponder/storage/PonderAnswerTransaction.php', 'PonderAnswerTransactionComment' => 'applications/ponder/storage/PonderAnswerTransactionComment.php', 'PonderAnswerTransactionQuery' => 'applications/ponder/query/PonderAnswerTransactionQuery.php', 'PonderAnswerTransactionType' => 'applications/ponder/xaction/PonderAnswerTransactionType.php', 'PonderAnswerView' => 'applications/ponder/view/PonderAnswerView.php', 'PonderConstants' => 'applications/ponder/constants/PonderConstants.php', 'PonderController' => 'applications/ponder/controller/PonderController.php', 'PonderDAO' => 'applications/ponder/storage/PonderDAO.php', 'PonderDefaultViewCapability' => 'applications/ponder/capability/PonderDefaultViewCapability.php', 'PonderEditor' => 'applications/ponder/editor/PonderEditor.php', 'PonderFooterView' => 'applications/ponder/view/PonderFooterView.php', 'PonderModerateCapability' => 'applications/ponder/capability/PonderModerateCapability.php', 'PonderQuestion' => 'applications/ponder/storage/PonderQuestion.php', 'PonderQuestionAnswerTransaction' => 'applications/ponder/xaction/PonderQuestionAnswerTransaction.php', 'PonderQuestionAnswerWikiTransaction' => 'applications/ponder/xaction/PonderQuestionAnswerWikiTransaction.php', 'PonderQuestionCommentController' => 'applications/ponder/controller/PonderQuestionCommentController.php', 'PonderQuestionContentTransaction' => 'applications/ponder/xaction/PonderQuestionContentTransaction.php', 'PonderQuestionCreateMailReceiver' => 'applications/ponder/mail/PonderQuestionCreateMailReceiver.php', 'PonderQuestionEditController' => 'applications/ponder/controller/PonderQuestionEditController.php', 'PonderQuestionEditEngine' => 'applications/ponder/editor/PonderQuestionEditEngine.php', 'PonderQuestionEditor' => 'applications/ponder/editor/PonderQuestionEditor.php', 'PonderQuestionFerretEngine' => 'applications/ponder/search/PonderQuestionFerretEngine.php', 'PonderQuestionFulltextEngine' => 'applications/ponder/search/PonderQuestionFulltextEngine.php', 'PonderQuestionHistoryController' => 'applications/ponder/controller/PonderQuestionHistoryController.php', 'PonderQuestionListController' => 'applications/ponder/controller/PonderQuestionListController.php', 'PonderQuestionMailReceiver' => 'applications/ponder/mail/PonderQuestionMailReceiver.php', 'PonderQuestionPHIDType' => 'applications/ponder/phid/PonderQuestionPHIDType.php', 'PonderQuestionQuery' => 'applications/ponder/query/PonderQuestionQuery.php', 'PonderQuestionReplyHandler' => 'applications/ponder/mail/PonderQuestionReplyHandler.php', 'PonderQuestionSearchEngine' => 'applications/ponder/query/PonderQuestionSearchEngine.php', 'PonderQuestionStatus' => 'applications/ponder/constants/PonderQuestionStatus.php', 'PonderQuestionStatusController' => 'applications/ponder/controller/PonderQuestionStatusController.php', 'PonderQuestionStatusTransaction' => 'applications/ponder/xaction/PonderQuestionStatusTransaction.php', 'PonderQuestionTitleTransaction' => 'applications/ponder/xaction/PonderQuestionTitleTransaction.php', 'PonderQuestionTransaction' => 'applications/ponder/storage/PonderQuestionTransaction.php', 'PonderQuestionTransactionComment' => 'applications/ponder/storage/PonderQuestionTransactionComment.php', 'PonderQuestionTransactionQuery' => 'applications/ponder/query/PonderQuestionTransactionQuery.php', 'PonderQuestionTransactionType' => 'applications/ponder/xaction/PonderQuestionTransactionType.php', 'PonderQuestionViewController' => 'applications/ponder/controller/PonderQuestionViewController.php', 'PonderRemarkupRule' => 'applications/ponder/remarkup/PonderRemarkupRule.php', 'PonderSchemaSpec' => 'applications/ponder/storage/PonderSchemaSpec.php', 'ProjectAddProjectsEmailCommand' => 'applications/project/command/ProjectAddProjectsEmailCommand.php', 'ProjectBoardTaskCard' => 'applications/project/view/ProjectBoardTaskCard.php', 'ProjectCanLockProjectsCapability' => 'applications/project/capability/ProjectCanLockProjectsCapability.php', 'ProjectColumnSearchConduitAPIMethod' => 'applications/project/conduit/ProjectColumnSearchConduitAPIMethod.php', 'ProjectConduitAPIMethod' => 'applications/project/conduit/ProjectConduitAPIMethod.php', 'ProjectCreateConduitAPIMethod' => 'applications/project/conduit/ProjectCreateConduitAPIMethod.php', 'ProjectCreateProjectsCapability' => 'applications/project/capability/ProjectCreateProjectsCapability.php', 'ProjectDatasourceEngineExtension' => 'applications/project/engineextension/ProjectDatasourceEngineExtension.php', 'ProjectDefaultEditCapability' => 'applications/project/capability/ProjectDefaultEditCapability.php', 'ProjectDefaultJoinCapability' => 'applications/project/capability/ProjectDefaultJoinCapability.php', 'ProjectDefaultViewCapability' => 'applications/project/capability/ProjectDefaultViewCapability.php', 'ProjectEditConduitAPIMethod' => 'applications/project/conduit/ProjectEditConduitAPIMethod.php', 'ProjectQueryConduitAPIMethod' => 'applications/project/conduit/ProjectQueryConduitAPIMethod.php', 'ProjectRemarkupRule' => 'applications/project/remarkup/ProjectRemarkupRule.php', 'ProjectRemarkupRuleTestCase' => 'applications/project/remarkup/__tests__/ProjectRemarkupRuleTestCase.php', 'ProjectReplyHandler' => 'applications/project/mail/ProjectReplyHandler.php', 'ProjectSearchConduitAPIMethod' => 'applications/project/conduit/ProjectSearchConduitAPIMethod.php', 'QueryFormattingTestCase' => 'infrastructure/storage/__tests__/QueryFormattingTestCase.php', 'ReleephAuthorFieldSpecification' => 'applications/releeph/field/specification/ReleephAuthorFieldSpecification.php', 'ReleephBranch' => 'applications/releeph/storage/ReleephBranch.php', 'ReleephBranchAccessController' => 'applications/releeph/controller/branch/ReleephBranchAccessController.php', 'ReleephBranchCommitFieldSpecification' => 'applications/releeph/field/specification/ReleephBranchCommitFieldSpecification.php', 'ReleephBranchController' => 'applications/releeph/controller/branch/ReleephBranchController.php', 'ReleephBranchCreateController' => 'applications/releeph/controller/branch/ReleephBranchCreateController.php', 'ReleephBranchEditController' => 'applications/releeph/controller/branch/ReleephBranchEditController.php', 'ReleephBranchEditor' => 'applications/releeph/editor/ReleephBranchEditor.php', 'ReleephBranchHistoryController' => 'applications/releeph/controller/branch/ReleephBranchHistoryController.php', 'ReleephBranchNamePreviewController' => 'applications/releeph/controller/branch/ReleephBranchNamePreviewController.php', 'ReleephBranchPHIDType' => 'applications/releeph/phid/ReleephBranchPHIDType.php', 'ReleephBranchPreviewView' => 'applications/releeph/view/branch/ReleephBranchPreviewView.php', 'ReleephBranchQuery' => 'applications/releeph/query/ReleephBranchQuery.php', 'ReleephBranchSearchEngine' => 'applications/releeph/query/ReleephBranchSearchEngine.php', 'ReleephBranchTemplate' => 'applications/releeph/view/branch/ReleephBranchTemplate.php', 'ReleephBranchTransaction' => 'applications/releeph/storage/ReleephBranchTransaction.php', 'ReleephBranchTransactionQuery' => 'applications/releeph/query/ReleephBranchTransactionQuery.php', 'ReleephBranchViewController' => 'applications/releeph/controller/branch/ReleephBranchViewController.php', 'ReleephCommitFinder' => 'applications/releeph/commitfinder/ReleephCommitFinder.php', 'ReleephCommitFinderException' => 'applications/releeph/commitfinder/ReleephCommitFinderException.php', 'ReleephCommitMessageFieldSpecification' => 'applications/releeph/field/specification/ReleephCommitMessageFieldSpecification.php', 'ReleephConduitAPIMethod' => 'applications/releeph/conduit/ReleephConduitAPIMethod.php', 'ReleephController' => 'applications/releeph/controller/ReleephController.php', 'ReleephDAO' => 'applications/releeph/storage/ReleephDAO.php', 'ReleephDefaultFieldSelector' => 'applications/releeph/field/selector/ReleephDefaultFieldSelector.php', 'ReleephDependsOnFieldSpecification' => 'applications/releeph/field/specification/ReleephDependsOnFieldSpecification.php', 'ReleephDiffChurnFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffChurnFieldSpecification.php', 'ReleephDiffMessageFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffMessageFieldSpecification.php', 'ReleephDiffSizeFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffSizeFieldSpecification.php', 'ReleephFieldParseException' => 'applications/releeph/field/exception/ReleephFieldParseException.php', 'ReleephFieldSelector' => 'applications/releeph/field/selector/ReleephFieldSelector.php', 'ReleephFieldSpecification' => 'applications/releeph/field/specification/ReleephFieldSpecification.php', 'ReleephGetBranchesConduitAPIMethod' => 'applications/releeph/conduit/ReleephGetBranchesConduitAPIMethod.php', 'ReleephIntentFieldSpecification' => 'applications/releeph/field/specification/ReleephIntentFieldSpecification.php', 'ReleephLevelFieldSpecification' => 'applications/releeph/field/specification/ReleephLevelFieldSpecification.php', 'ReleephOriginalCommitFieldSpecification' => 'applications/releeph/field/specification/ReleephOriginalCommitFieldSpecification.php', 'ReleephProductActionController' => 'applications/releeph/controller/product/ReleephProductActionController.php', 'ReleephProductController' => 'applications/releeph/controller/product/ReleephProductController.php', 'ReleephProductCreateController' => 'applications/releeph/controller/product/ReleephProductCreateController.php', 'ReleephProductEditController' => 'applications/releeph/controller/product/ReleephProductEditController.php', 'ReleephProductEditor' => 'applications/releeph/editor/ReleephProductEditor.php', 'ReleephProductHistoryController' => 'applications/releeph/controller/product/ReleephProductHistoryController.php', 'ReleephProductListController' => 'applications/releeph/controller/product/ReleephProductListController.php', 'ReleephProductPHIDType' => 'applications/releeph/phid/ReleephProductPHIDType.php', 'ReleephProductQuery' => 'applications/releeph/query/ReleephProductQuery.php', 'ReleephProductSearchEngine' => 'applications/releeph/query/ReleephProductSearchEngine.php', 'ReleephProductTransaction' => 'applications/releeph/storage/ReleephProductTransaction.php', 'ReleephProductTransactionQuery' => 'applications/releeph/query/ReleephProductTransactionQuery.php', 'ReleephProductViewController' => 'applications/releeph/controller/product/ReleephProductViewController.php', 'ReleephProject' => 'applications/releeph/storage/ReleephProject.php', 'ReleephQueryBranchesConduitAPIMethod' => 'applications/releeph/conduit/ReleephQueryBranchesConduitAPIMethod.php', 'ReleephQueryProductsConduitAPIMethod' => 'applications/releeph/conduit/ReleephQueryProductsConduitAPIMethod.php', 'ReleephQueryRequestsConduitAPIMethod' => 'applications/releeph/conduit/ReleephQueryRequestsConduitAPIMethod.php', 'ReleephReasonFieldSpecification' => 'applications/releeph/field/specification/ReleephReasonFieldSpecification.php', 'ReleephRequest' => 'applications/releeph/storage/ReleephRequest.php', 'ReleephRequestActionController' => 'applications/releeph/controller/request/ReleephRequestActionController.php', 'ReleephRequestCommentController' => 'applications/releeph/controller/request/ReleephRequestCommentController.php', 'ReleephRequestConduitAPIMethod' => 'applications/releeph/conduit/ReleephRequestConduitAPIMethod.php', 'ReleephRequestController' => 'applications/releeph/controller/request/ReleephRequestController.php', 'ReleephRequestDifferentialCreateController' => 'applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php', 'ReleephRequestEditController' => 'applications/releeph/controller/request/ReleephRequestEditController.php', 'ReleephRequestMailReceiver' => 'applications/releeph/mail/ReleephRequestMailReceiver.php', 'ReleephRequestPHIDType' => 'applications/releeph/phid/ReleephRequestPHIDType.php', 'ReleephRequestQuery' => 'applications/releeph/query/ReleephRequestQuery.php', 'ReleephRequestReplyHandler' => 'applications/releeph/mail/ReleephRequestReplyHandler.php', 'ReleephRequestSearchEngine' => 'applications/releeph/query/ReleephRequestSearchEngine.php', 'ReleephRequestStatus' => 'applications/releeph/constants/ReleephRequestStatus.php', 'ReleephRequestTransaction' => 'applications/releeph/storage/ReleephRequestTransaction.php', 'ReleephRequestTransactionComment' => 'applications/releeph/storage/ReleephRequestTransactionComment.php', 'ReleephRequestTransactionQuery' => 'applications/releeph/query/ReleephRequestTransactionQuery.php', 'ReleephRequestTransactionalEditor' => 'applications/releeph/editor/ReleephRequestTransactionalEditor.php', 'ReleephRequestTypeaheadControl' => 'applications/releeph/view/request/ReleephRequestTypeaheadControl.php', 'ReleephRequestTypeaheadController' => 'applications/releeph/controller/request/ReleephRequestTypeaheadController.php', 'ReleephRequestView' => 'applications/releeph/view/ReleephRequestView.php', 'ReleephRequestViewController' => 'applications/releeph/controller/request/ReleephRequestViewController.php', 'ReleephRequestorFieldSpecification' => 'applications/releeph/field/specification/ReleephRequestorFieldSpecification.php', 'ReleephRevisionFieldSpecification' => 'applications/releeph/field/specification/ReleephRevisionFieldSpecification.php', 'ReleephSeverityFieldSpecification' => 'applications/releeph/field/specification/ReleephSeverityFieldSpecification.php', 'ReleephSummaryFieldSpecification' => 'applications/releeph/field/specification/ReleephSummaryFieldSpecification.php', 'ReleephWorkCanPushConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkCanPushConduitAPIMethod.php', 'ReleephWorkGetAuthorInfoConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkGetAuthorInfoConduitAPIMethod.php', 'ReleephWorkGetBranchCommitMessageConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkGetBranchCommitMessageConduitAPIMethod.php', 'ReleephWorkGetBranchConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkGetBranchConduitAPIMethod.php', 'ReleephWorkGetCommitMessageConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkGetCommitMessageConduitAPIMethod.php', 'ReleephWorkNextRequestConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkNextRequestConduitAPIMethod.php', 'ReleephWorkRecordConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkRecordConduitAPIMethod.php', 'ReleephWorkRecordPickStatusConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkRecordPickStatusConduitAPIMethod.php', 'RemarkupProcessConduitAPIMethod' => 'applications/remarkup/conduit/RemarkupProcessConduitAPIMethod.php', 'RepositoryConduitAPIMethod' => 'applications/repository/conduit/RepositoryConduitAPIMethod.php', 'RepositoryQueryConduitAPIMethod' => 'applications/repository/conduit/RepositoryQueryConduitAPIMethod.php', 'ShellLogView' => 'applications/harbormaster/view/ShellLogView.php', 'SlowvoteConduitAPIMethod' => 'applications/slowvote/conduit/SlowvoteConduitAPIMethod.php', 'SlowvoteEmbedView' => 'applications/slowvote/view/SlowvoteEmbedView.php', 'SlowvoteInfoConduitAPIMethod' => 'applications/slowvote/conduit/SlowvoteInfoConduitAPIMethod.php', 'SlowvoteRemarkupRule' => 'applications/slowvote/remarkup/SlowvoteRemarkupRule.php', 'SubscriptionListDialogBuilder' => 'applications/subscriptions/view/SubscriptionListDialogBuilder.php', 'SubscriptionListStringBuilder' => 'applications/subscriptions/view/SubscriptionListStringBuilder.php', 'TokenConduitAPIMethod' => 'applications/tokens/conduit/TokenConduitAPIMethod.php', 'TokenGiveConduitAPIMethod' => 'applications/tokens/conduit/TokenGiveConduitAPIMethod.php', 'TokenGivenConduitAPIMethod' => 'applications/tokens/conduit/TokenGivenConduitAPIMethod.php', 'TokenQueryConduitAPIMethod' => 'applications/tokens/conduit/TokenQueryConduitAPIMethod.php', 'TransactionSearchConduitAPIMethod' => 'applications/transactions/conduit/TransactionSearchConduitAPIMethod.php', 'UserConduitAPIMethod' => 'applications/people/conduit/UserConduitAPIMethod.php', 'UserDisableConduitAPIMethod' => 'applications/people/conduit/UserDisableConduitAPIMethod.php', 'UserEditConduitAPIMethod' => 'applications/people/conduit/UserEditConduitAPIMethod.php', 'UserEnableConduitAPIMethod' => 'applications/people/conduit/UserEnableConduitAPIMethod.php', 'UserFindConduitAPIMethod' => 'applications/people/conduit/UserFindConduitAPIMethod.php', 'UserQueryConduitAPIMethod' => 'applications/people/conduit/UserQueryConduitAPIMethod.php', 'UserSearchConduitAPIMethod' => 'applications/people/conduit/UserSearchConduitAPIMethod.php', 'UserWhoAmIConduitAPIMethod' => 'applications/people/conduit/UserWhoAmIConduitAPIMethod.php', ), 'function' => array( 'celerity_generate_unique_node_id' => 'applications/celerity/api.php', 'celerity_get_resource_uri' => 'applications/celerity/api.php', 'javelin_tag' => 'infrastructure/javelin/markup.php', 'phabricator_date' => 'view/viewutils.php', 'phabricator_datetime' => 'view/viewutils.php', 'phabricator_datetimezone' => 'view/viewutils.php', 'phabricator_form' => 'infrastructure/javelin/markup.php', 'phabricator_format_local_time' => 'view/viewutils.php', 'phabricator_relative_date' => 'view/viewutils.php', 'phabricator_time' => 'view/viewutils.php', 'phid_get_subtype' => 'applications/phid/utils.php', 'phid_get_type' => 'applications/phid/utils.php', 'phid_group_by_type' => 'applications/phid/utils.php', 'require_celerity_resource' => 'applications/celerity/api.php', ), 'xmap' => array( 'AlmanacAddress' => 'Phobject', 'AlmanacBinding' => array( 'AlmanacDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', 'AlmanacPropertyInterface', 'PhabricatorDestructibleInterface', 'PhabricatorExtendedPolicyInterface', 'PhabricatorConduitResultInterface', ), 'AlmanacBindingDeletePropertyTransaction' => 'AlmanacBindingTransactionType', 'AlmanacBindingDisableController' => 'AlmanacServiceController', 'AlmanacBindingDisableTransaction' => 'AlmanacBindingTransactionType', 'AlmanacBindingEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'AlmanacBindingEditController' => 'AlmanacServiceController', 'AlmanacBindingEditEngine' => 'PhabricatorEditEngine', 'AlmanacBindingEditor' => 'AlmanacEditor', 'AlmanacBindingInterfaceTransaction' => 'AlmanacBindingTransactionType', 'AlmanacBindingPHIDType' => 'PhabricatorPHIDType', 'AlmanacBindingPropertyEditEngine' => 'AlmanacPropertyEditEngine', 'AlmanacBindingQuery' => 'AlmanacQuery', 'AlmanacBindingSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'AlmanacBindingSearchEngine' => 'PhabricatorApplicationSearchEngine', 'AlmanacBindingServiceTransaction' => 'AlmanacBindingTransactionType', 'AlmanacBindingSetPropertyTransaction' => 'AlmanacBindingTransactionType', 'AlmanacBindingTableView' => 'AphrontView', 'AlmanacBindingTransaction' => 'AlmanacModularTransaction', 'AlmanacBindingTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'AlmanacBindingTransactionType' => 'AlmanacTransactionType', 'AlmanacBindingViewController' => 'AlmanacServiceController', 'AlmanacBindingsSearchEngineAttachment' => 'AlmanacSearchEngineAttachment', 'AlmanacCacheEngineExtension' => 'PhabricatorCacheEngineExtension', 'AlmanacClusterDatabaseServiceType' => 'AlmanacClusterServiceType', 'AlmanacClusterRepositoryServiceType' => 'AlmanacClusterServiceType', 'AlmanacClusterServiceType' => 'AlmanacServiceType', 'AlmanacConsoleController' => 'AlmanacController', 'AlmanacController' => 'PhabricatorController', 'AlmanacCreateDevicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateNamespacesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateNetworksCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateServicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCustomServiceType' => 'AlmanacServiceType', 'AlmanacDAO' => 'PhabricatorLiskDAO', 'AlmanacDeletePropertyEditField' => 'PhabricatorEditField', 'AlmanacDeletePropertyEditType' => 'PhabricatorEditType', 'AlmanacDevice' => array( 'AlmanacDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorProjectInterface', 'PhabricatorSSHPublicKeyInterface', 'AlmanacPropertyInterface', 'PhabricatorDestructibleInterface', 'PhabricatorNgramsInterface', 'PhabricatorConduitResultInterface', 'PhabricatorExtendedPolicyInterface', ), 'AlmanacDeviceController' => 'AlmanacController', 'AlmanacDeviceDeletePropertyTransaction' => 'AlmanacDeviceTransactionType', 'AlmanacDeviceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'AlmanacDeviceEditController' => 'AlmanacDeviceController', 'AlmanacDeviceEditEngine' => 'PhabricatorEditEngine', 'AlmanacDeviceEditor' => 'AlmanacEditor', 'AlmanacDeviceListController' => 'AlmanacDeviceController', 'AlmanacDeviceNameNgrams' => 'PhabricatorSearchNgrams', 'AlmanacDeviceNameTransaction' => 'AlmanacDeviceTransactionType', 'AlmanacDevicePHIDType' => 'PhabricatorPHIDType', 'AlmanacDevicePropertyEditEngine' => 'AlmanacPropertyEditEngine', 'AlmanacDeviceQuery' => 'AlmanacQuery', 'AlmanacDeviceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'AlmanacDeviceSearchEngine' => 'PhabricatorApplicationSearchEngine', 'AlmanacDeviceSetPropertyTransaction' => 'AlmanacDeviceTransactionType', 'AlmanacDeviceTransaction' => 'AlmanacModularTransaction', 'AlmanacDeviceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'AlmanacDeviceTransactionType' => 'AlmanacTransactionType', 'AlmanacDeviceViewController' => 'AlmanacDeviceController', 'AlmanacDrydockPoolServiceType' => 'AlmanacServiceType', 'AlmanacEditor' => 'PhabricatorApplicationTransactionEditor', 'AlmanacInterface' => array( 'AlmanacDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', 'PhabricatorExtendedPolicyInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorConduitResultInterface', ), 'AlmanacInterfaceAddressTransaction' => 'AlmanacInterfaceTransactionType', 'AlmanacInterfaceDatasource' => 'PhabricatorTypeaheadDatasource', 'AlmanacInterfaceDeleteController' => 'AlmanacDeviceController', 'AlmanacInterfaceDestroyTransaction' => 'AlmanacInterfaceTransactionType', 'AlmanacInterfaceDeviceTransaction' => 'AlmanacInterfaceTransactionType', 'AlmanacInterfaceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'AlmanacInterfaceEditController' => 'AlmanacDeviceController', 'AlmanacInterfaceEditEngine' => 'PhabricatorEditEngine', 'AlmanacInterfaceEditor' => 'AlmanacEditor', 'AlmanacInterfaceNetworkTransaction' => 'AlmanacInterfaceTransactionType', 'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType', 'AlmanacInterfacePortTransaction' => 'AlmanacInterfaceTransactionType', 'AlmanacInterfaceQuery' => 'AlmanacQuery', 'AlmanacInterfaceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'AlmanacInterfaceSearchEngine' => 'PhabricatorApplicationSearchEngine', 'AlmanacInterfaceTableView' => 'AphrontView', 'AlmanacInterfaceTransaction' => 'AlmanacModularTransaction', 'AlmanacInterfaceTransactionType' => 'AlmanacTransactionType', 'AlmanacKeys' => 'Phobject', 'AlmanacManageClusterServicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacManagementRegisterWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementWorkflow' => 'PhabricatorManagementWorkflow', 'AlmanacModularTransaction' => 'PhabricatorModularTransaction', 'AlmanacNames' => 'Phobject', 'AlmanacNamesTestCase' => 'PhabricatorTestCase', 'AlmanacNamespace' => array( 'AlmanacDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorProjectInterface', 'AlmanacPropertyInterface', 'PhabricatorDestructibleInterface', 'PhabricatorNgramsInterface', 'PhabricatorConduitResultInterface', ), 'AlmanacNamespaceController' => 'AlmanacController', 'AlmanacNamespaceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'AlmanacNamespaceEditController' => 'AlmanacNamespaceController', 'AlmanacNamespaceEditEngine' => 'PhabricatorEditEngine', 'AlmanacNamespaceEditor' => 'AlmanacEditor', 'AlmanacNamespaceListController' => 'AlmanacNamespaceController', 'AlmanacNamespaceNameNgrams' => 'PhabricatorSearchNgrams', 'AlmanacNamespaceNameTransaction' => 'AlmanacNamespaceTransactionType', 'AlmanacNamespacePHIDType' => 'PhabricatorPHIDType', 'AlmanacNamespaceQuery' => 'AlmanacQuery', 'AlmanacNamespaceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'AlmanacNamespaceSearchEngine' => 'PhabricatorApplicationSearchEngine', 'AlmanacNamespaceTransaction' => 'AlmanacModularTransaction', 'AlmanacNamespaceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'AlmanacNamespaceTransactionType' => 'AlmanacTransactionType', 'AlmanacNamespaceViewController' => 'AlmanacNamespaceController', 'AlmanacNetwork' => array( 'AlmanacDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', 'PhabricatorNgramsInterface', 'PhabricatorConduitResultInterface', ), 'AlmanacNetworkController' => 'AlmanacController', 'AlmanacNetworkEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'AlmanacNetworkEditController' => 'AlmanacNetworkController', 'AlmanacNetworkEditEngine' => 'PhabricatorEditEngine', 'AlmanacNetworkEditor' => 'AlmanacEditor', 'AlmanacNetworkListController' => 'AlmanacNetworkController', 'AlmanacNetworkNameNgrams' => 'PhabricatorSearchNgrams', 'AlmanacNetworkNameTransaction' => 'AlmanacNetworkTransactionType', 'AlmanacNetworkPHIDType' => 'PhabricatorPHIDType', 'AlmanacNetworkQuery' => 'AlmanacQuery', 'AlmanacNetworkSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'AlmanacNetworkSearchEngine' => 'PhabricatorApplicationSearchEngine', 'AlmanacNetworkTransaction' => 'AlmanacModularTransaction', 'AlmanacNetworkTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'AlmanacNetworkTransactionType' => 'AlmanacTransactionType', 'AlmanacNetworkViewController' => 'AlmanacNetworkController', 'AlmanacPropertiesDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', 'AlmanacPropertiesEditEngineExtension' => 'PhabricatorEditEngineExtension', 'AlmanacPropertiesSearchEngineAttachment' => 'AlmanacSearchEngineAttachment', 'AlmanacProperty' => array( 'AlmanacDAO', 'PhabricatorPolicyInterface', ), 'AlmanacPropertyController' => 'AlmanacController', 'AlmanacPropertyDeleteController' => 'AlmanacPropertyController', 'AlmanacPropertyEditController' => 'AlmanacPropertyController', 'AlmanacPropertyEditEngine' => 'PhabricatorEditEngine', 'AlmanacPropertyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'AlmanacQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'AlmanacSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'AlmanacSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'AlmanacService' => array( 'AlmanacDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorProjectInterface', 'AlmanacPropertyInterface', 'PhabricatorDestructibleInterface', 'PhabricatorNgramsInterface', 'PhabricatorConduitResultInterface', 'PhabricatorExtendedPolicyInterface', ), 'AlmanacServiceController' => 'AlmanacController', 'AlmanacServiceDatasource' => 'PhabricatorTypeaheadDatasource', 'AlmanacServiceDeletePropertyTransaction' => 'AlmanacServiceTransactionType', 'AlmanacServiceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'AlmanacServiceEditController' => 'AlmanacServiceController', 'AlmanacServiceEditEngine' => 'PhabricatorEditEngine', 'AlmanacServiceEditor' => 'AlmanacEditor', 'AlmanacServiceListController' => 'AlmanacServiceController', 'AlmanacServiceNameNgrams' => 'PhabricatorSearchNgrams', 'AlmanacServiceNameTransaction' => 'AlmanacServiceTransactionType', 'AlmanacServicePHIDType' => 'PhabricatorPHIDType', 'AlmanacServicePropertyEditEngine' => 'AlmanacPropertyEditEngine', 'AlmanacServiceQuery' => 'AlmanacQuery', 'AlmanacServiceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'AlmanacServiceSearchEngine' => 'PhabricatorApplicationSearchEngine', 'AlmanacServiceSetPropertyTransaction' => 'AlmanacServiceTransactionType', 'AlmanacServiceTransaction' => 'AlmanacModularTransaction', 'AlmanacServiceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'AlmanacServiceTransactionType' => 'AlmanacTransactionType', 'AlmanacServiceType' => 'Phobject', 'AlmanacServiceTypeDatasource' => 'PhabricatorTypeaheadDatasource', 'AlmanacServiceTypeTestCase' => 'PhabricatorTestCase', 'AlmanacServiceTypeTransaction' => 'AlmanacServiceTransactionType', 'AlmanacServiceViewController' => 'AlmanacServiceController', 'AlmanacSetPropertyEditField' => 'PhabricatorEditField', 'AlmanacSetPropertyEditType' => 'PhabricatorEditType', 'AlmanacTransactionType' => 'PhabricatorModularTransactionType', 'AphlictDropdownDataQuery' => 'Phobject', 'Aphront304Response' => 'AphrontResponse', 'Aphront400Response' => 'AphrontResponse', 'Aphront403Response' => 'AphrontHTMLResponse', 'Aphront404Response' => 'AphrontHTMLResponse', 'AphrontAjaxResponse' => 'AphrontResponse', 'AphrontApplicationConfiguration' => 'Phobject', 'AphrontBarView' => 'AphrontView', 'AphrontBoolHTTPParameterType' => 'AphrontHTTPParameterType', 'AphrontCalendarEventView' => 'AphrontView', 'AphrontController' => 'Phobject', 'AphrontCursorPagerView' => 'AphrontView', 'AphrontDefaultApplicationConfiguration' => 'AphrontApplicationConfiguration', 'AphrontDialogResponse' => 'AphrontResponse', 'AphrontDialogView' => array( 'AphrontView', 'AphrontResponseProducerInterface', ), 'AphrontEpochHTTPParameterType' => 'AphrontHTTPParameterType', 'AphrontException' => 'Exception', 'AphrontFileHTTPParameterType' => 'AphrontHTTPParameterType', 'AphrontFileResponse' => 'AphrontResponse', 'AphrontFormCheckboxControl' => 'AphrontFormControl', 'AphrontFormControl' => 'AphrontView', 'AphrontFormDateControl' => 'AphrontFormControl', 'AphrontFormDateControlValue' => 'Phobject', 'AphrontFormDividerControl' => 'AphrontFormControl', 'AphrontFormFileControl' => 'AphrontFormControl', 'AphrontFormHandlesControl' => 'AphrontFormControl', 'AphrontFormMarkupControl' => 'AphrontFormControl', 'AphrontFormPasswordControl' => 'AphrontFormControl', 'AphrontFormPolicyControl' => 'AphrontFormControl', 'AphrontFormRadioButtonControl' => 'AphrontFormControl', 'AphrontFormRecaptchaControl' => 'AphrontFormControl', 'AphrontFormSelectControl' => 'AphrontFormControl', 'AphrontFormStaticControl' => 'AphrontFormControl', 'AphrontFormSubmitControl' => 'AphrontFormControl', 'AphrontFormTextAreaControl' => 'AphrontFormControl', 'AphrontFormTextControl' => 'AphrontFormControl', 'AphrontFormTextWithSubmitControl' => 'AphrontFormControl', 'AphrontFormTokenizerControl' => 'AphrontFormControl', 'AphrontFormTypeaheadControl' => 'AphrontFormControl', 'AphrontFormView' => 'AphrontView', 'AphrontGlyphBarView' => 'AphrontBarView', 'AphrontHTMLResponse' => 'AphrontResponse', 'AphrontHTTPParameterType' => 'Phobject', 'AphrontHTTPProxyResponse' => 'AphrontResponse', 'AphrontHTTPSink' => 'Phobject', 'AphrontHTTPSinkTestCase' => 'PhabricatorTestCase', 'AphrontIntHTTPParameterType' => 'AphrontHTTPParameterType', 'AphrontIsolatedDatabaseConnectionTestCase' => 'PhabricatorTestCase', 'AphrontIsolatedHTTPSink' => 'AphrontHTTPSink', 'AphrontJSONResponse' => 'AphrontResponse', 'AphrontJavelinView' => 'AphrontView', 'AphrontKeyboardShortcutsAvailableView' => 'AphrontView', 'AphrontListFilterView' => 'AphrontView', 'AphrontListHTTPParameterType' => 'AphrontHTTPParameterType', 'AphrontMalformedRequestException' => 'AphrontException', 'AphrontMoreView' => 'AphrontView', 'AphrontMultiColumnView' => 'AphrontView', 'AphrontMySQLDatabaseConnectionTestCase' => 'PhabricatorTestCase', 'AphrontNullView' => 'AphrontView', 'AphrontPHIDHTTPParameterType' => 'AphrontHTTPParameterType', 'AphrontPHIDListHTTPParameterType' => 'AphrontListHTTPParameterType', 'AphrontPHPHTTPSink' => 'AphrontHTTPSink', 'AphrontPageView' => 'AphrontView', 'AphrontPlainTextResponse' => 'AphrontResponse', 'AphrontProgressBarView' => 'AphrontBarView', 'AphrontProjectListHTTPParameterType' => 'AphrontListHTTPParameterType', 'AphrontProxyResponse' => array( 'AphrontResponse', 'AphrontResponseProducerInterface', ), 'AphrontRedirectResponse' => 'AphrontResponse', 'AphrontRedirectResponseTestCase' => 'PhabricatorTestCase', 'AphrontReloadResponse' => 'AphrontRedirectResponse', 'AphrontRequest' => 'Phobject', 'AphrontRequestExceptionHandler' => 'Phobject', 'AphrontRequestTestCase' => 'PhabricatorTestCase', 'AphrontResponse' => 'Phobject', 'AphrontRoutingMap' => 'Phobject', 'AphrontRoutingResult' => 'Phobject', 'AphrontSelectHTTPParameterType' => 'AphrontHTTPParameterType', 'AphrontSideNavFilterView' => 'AphrontView', 'AphrontSite' => 'Phobject', 'AphrontStackTraceView' => 'AphrontView', 'AphrontStandaloneHTMLResponse' => 'AphrontHTMLResponse', 'AphrontStringHTTPParameterType' => 'AphrontHTTPParameterType', 'AphrontStringListHTTPParameterType' => 'AphrontListHTTPParameterType', 'AphrontTableView' => 'AphrontView', 'AphrontTagView' => 'AphrontView', 'AphrontTokenizerTemplateView' => 'AphrontView', 'AphrontTypeaheadTemplateView' => 'AphrontView', 'AphrontUnhandledExceptionResponse' => 'AphrontStandaloneHTMLResponse', 'AphrontUserListHTTPParameterType' => 'AphrontListHTTPParameterType', 'AphrontView' => array( 'Phobject', 'PhutilSafeHTMLProducerInterface', ), 'AphrontWebpageResponse' => 'AphrontHTMLResponse', 'ArcanistConduitAPIMethod' => 'ConduitAPIMethod', 'AuditConduitAPIMethod' => 'ConduitAPIMethod', 'AuditQueryConduitAPIMethod' => 'AuditConduitAPIMethod', 'AuthManageProvidersCapability' => 'PhabricatorPolicyCapability', 'BulkParameterType' => 'Phobject', 'BulkPointsParameterType' => 'BulkParameterType', 'BulkRemarkupParameterType' => 'BulkParameterType', 'BulkSelectParameterType' => 'BulkParameterType', 'BulkStringParameterType' => 'BulkParameterType', 'BulkTokenizerParameterType' => 'BulkParameterType', 'CalendarTimeUtil' => 'Phobject', 'CalendarTimeUtilTestCase' => 'PhabricatorTestCase', 'CelerityAPI' => 'Phobject', 'CelerityDarkModePostprocessor' => 'CelerityPostprocessor', 'CelerityDefaultPostprocessor' => 'CelerityPostprocessor', 'CelerityHighContrastPostprocessor' => 'CelerityPostprocessor', 'CelerityLargeFontPostprocessor' => 'CelerityPostprocessor', 'CelerityManagementMapWorkflow' => 'CelerityManagementWorkflow', 'CelerityManagementSyntaxWorkflow' => 'CelerityManagementWorkflow', 'CelerityManagementWorkflow' => 'PhabricatorManagementWorkflow', 'CelerityPhabricatorResourceController' => 'CelerityResourceController', 'CelerityPhabricatorResources' => 'CelerityResourcesOnDisk', 'CelerityPhysicalResources' => 'CelerityResources', 'CelerityPhysicalResourcesTestCase' => 'PhabricatorTestCase', 'CelerityPostprocessor' => 'Phobject', 'CelerityPostprocessorTestCase' => 'PhabricatorTestCase', 'CelerityRedGreenPostprocessor' => 'CelerityPostprocessor', 'CelerityResourceController' => 'PhabricatorController', 'CelerityResourceGraph' => 'AbstractDirectedGraph', 'CelerityResourceMap' => 'Phobject', 'CelerityResourceMapGenerator' => 'Phobject', 'CelerityResourceTransformer' => 'Phobject', 'CelerityResourceTransformerTestCase' => 'PhabricatorTestCase', 'CelerityResources' => 'Phobject', 'CelerityResourcesOnDisk' => 'CelerityPhysicalResources', 'CeleritySpriteGenerator' => 'Phobject', 'CelerityStaticResourceResponse' => 'Phobject', 'ChatLogConduitAPIMethod' => 'ConduitAPIMethod', 'ChatLogQueryConduitAPIMethod' => 'ChatLogConduitAPIMethod', 'ChatLogRecordConduitAPIMethod' => 'ChatLogConduitAPIMethod', 'ConduitAPIMethod' => array( 'Phobject', 'PhabricatorPolicyInterface', ), 'ConduitAPIMethodTestCase' => 'PhabricatorTestCase', 'ConduitAPIRequest' => 'Phobject', 'ConduitAPIResponse' => 'Phobject', 'ConduitApplicationNotInstalledException' => 'ConduitMethodNotFoundException', 'ConduitBoolParameterType' => 'ConduitParameterType', 'ConduitCall' => 'Phobject', 'ConduitCallTestCase' => 'PhabricatorTestCase', 'ConduitColumnsParameterType' => 'ConduitParameterType', 'ConduitConnectConduitAPIMethod' => 'ConduitAPIMethod', 'ConduitConstantDescription' => 'Phobject', 'ConduitEpochParameterType' => 'ConduitParameterType', 'ConduitException' => 'Exception', 'ConduitGetCapabilitiesConduitAPIMethod' => 'ConduitAPIMethod', 'ConduitGetCertificateConduitAPIMethod' => 'ConduitAPIMethod', 'ConduitIntListParameterType' => 'ConduitListParameterType', 'ConduitIntParameterType' => 'ConduitParameterType', 'ConduitListParameterType' => 'ConduitParameterType', 'ConduitLogGarbageCollector' => 'PhabricatorGarbageCollector', 'ConduitMethodDoesNotExistException' => 'ConduitMethodNotFoundException', 'ConduitMethodNotFoundException' => 'ConduitException', 'ConduitPHIDListParameterType' => 'ConduitListParameterType', 'ConduitPHIDParameterType' => 'ConduitParameterType', 'ConduitParameterType' => 'Phobject', 'ConduitPingConduitAPIMethod' => 'ConduitAPIMethod', 'ConduitPointsParameterType' => 'ConduitParameterType', 'ConduitProjectListParameterType' => 'ConduitListParameterType', 'ConduitQueryConduitAPIMethod' => 'ConduitAPIMethod', 'ConduitResultSearchEngineExtension' => 'PhabricatorSearchEngineExtension', 'ConduitSSHWorkflow' => 'PhabricatorSSHWorkflow', 'ConduitStringListParameterType' => 'ConduitListParameterType', 'ConduitStringParameterType' => 'ConduitParameterType', 'ConduitTokenGarbageCollector' => 'PhabricatorGarbageCollector', 'ConduitUserListParameterType' => 'ConduitListParameterType', 'ConduitUserParameterType' => 'ConduitParameterType', 'ConduitWildParameterType' => 'ConduitParameterType', 'ConpherenceColumnViewController' => 'ConpherenceController', 'ConpherenceConduitAPIMethod' => 'ConduitAPIMethod', 'ConpherenceConfigOptions' => 'PhabricatorApplicationConfigOptions', 'ConpherenceConstants' => 'Phobject', 'ConpherenceController' => 'PhabricatorController', 'ConpherenceCreateThreadConduitAPIMethod' => 'ConpherenceConduitAPIMethod', 'ConpherenceDAO' => 'PhabricatorLiskDAO', 'ConpherenceDurableColumnView' => 'AphrontTagView', 'ConpherenceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'ConpherenceEditEngine' => 'PhabricatorEditEngine', 'ConpherenceEditor' => 'PhabricatorApplicationTransactionEditor', 'ConpherenceFulltextQuery' => 'PhabricatorOffsetPagedQuery', 'ConpherenceIndex' => 'ConpherenceDAO', 'ConpherenceLayoutView' => 'AphrontTagView', 'ConpherenceListController' => 'ConpherenceController', 'ConpherenceMenuItemView' => 'AphrontTagView', 'ConpherenceNotificationPanelController' => 'ConpherenceController', 'ConpherenceParticipant' => 'ConpherenceDAO', 'ConpherenceParticipantController' => 'ConpherenceController', 'ConpherenceParticipantCountQuery' => 'PhabricatorOffsetPagedQuery', 'ConpherenceParticipantQuery' => 'PhabricatorOffsetPagedQuery', 'ConpherenceParticipantView' => 'AphrontView', 'ConpherenceQueryThreadConduitAPIMethod' => 'ConpherenceConduitAPIMethod', 'ConpherenceQueryTransactionConduitAPIMethod' => 'ConpherenceConduitAPIMethod', 'ConpherenceReplyHandler' => 'PhabricatorMailReplyHandler', 'ConpherenceRoomEditController' => 'ConpherenceController', 'ConpherenceRoomListController' => 'ConpherenceController', 'ConpherenceRoomPictureController' => 'ConpherenceController', 'ConpherenceRoomPreferencesController' => 'ConpherenceController', 'ConpherenceRoomSettings' => 'ConpherenceConstants', 'ConpherenceRoomTestCase' => 'ConpherenceTestCase', 'ConpherenceSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'ConpherenceTestCase' => 'PhabricatorTestCase', 'ConpherenceThread' => array( 'ConpherenceDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorMentionableInterface', 'PhabricatorDestructibleInterface', 'PhabricatorNgramsInterface', ), 'ConpherenceThreadDatasource' => 'PhabricatorTypeaheadDatasource', 'ConpherenceThreadDateMarkerTransaction' => 'ConpherenceThreadTransactionType', 'ConpherenceThreadIndexEngineExtension' => 'PhabricatorIndexEngineExtension', 'ConpherenceThreadListView' => 'AphrontView', 'ConpherenceThreadMailReceiver' => 'PhabricatorObjectMailReceiver', 'ConpherenceThreadMembersPolicyRule' => 'PhabricatorPolicyRule', 'ConpherenceThreadParticipantsTransaction' => 'ConpherenceThreadTransactionType', 'ConpherenceThreadPictureTransaction' => 'ConpherenceThreadTransactionType', 'ConpherenceThreadQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'ConpherenceThreadRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'ConpherenceThreadSearchController' => 'ConpherenceController', 'ConpherenceThreadSearchEngine' => 'PhabricatorApplicationSearchEngine', 'ConpherenceThreadTitleNgrams' => 'PhabricatorSearchNgrams', 'ConpherenceThreadTitleTransaction' => 'ConpherenceThreadTransactionType', 'ConpherenceThreadTopicTransaction' => 'ConpherenceThreadTransactionType', 'ConpherenceThreadTransactionType' => 'PhabricatorModularTransactionType', 'ConpherenceTransaction' => 'PhabricatorModularTransaction', 'ConpherenceTransactionComment' => 'PhabricatorApplicationTransactionComment', 'ConpherenceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'ConpherenceTransactionRenderer' => 'Phobject', 'ConpherenceTransactionView' => 'AphrontView', 'ConpherenceUpdateActions' => 'ConpherenceConstants', 'ConpherenceUpdateController' => 'ConpherenceController', 'ConpherenceUpdateThreadConduitAPIMethod' => 'ConpherenceConduitAPIMethod', 'ConpherenceViewController' => 'ConpherenceController', 'CountdownEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'CountdownSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'DarkConsoleController' => 'PhabricatorController', 'DarkConsoleCore' => 'Phobject', 'DarkConsoleDataController' => 'PhabricatorController', 'DarkConsoleErrorLogPlugin' => 'DarkConsolePlugin', 'DarkConsoleErrorLogPluginAPI' => 'Phobject', 'DarkConsoleEventPlugin' => 'DarkConsolePlugin', 'DarkConsoleEventPluginAPI' => 'PhabricatorEventListener', 'DarkConsolePlugin' => 'Phobject', 'DarkConsoleRealtimePlugin' => 'DarkConsolePlugin', 'DarkConsoleRequestPlugin' => 'DarkConsolePlugin', 'DarkConsoleServicesPlugin' => 'DarkConsolePlugin', 'DarkConsoleStartupPlugin' => 'DarkConsolePlugin', 'DarkConsoleXHProfPlugin' => 'DarkConsolePlugin', 'DarkConsoleXHProfPluginAPI' => 'Phobject', 'DifferentialAction' => 'Phobject', 'DifferentialActionEmailCommand' => 'MetaMTAEmailTransactionCommand', 'DifferentialAdjustmentMapTestCase' => 'PhutilTestCase', 'DifferentialAffectedPath' => 'DifferentialDAO', 'DifferentialAsanaRepresentationField' => 'DifferentialCustomField', 'DifferentialAuditorsCommitMessageField' => 'DifferentialCommitMessageCustomField', 'DifferentialAuditorsField' => 'DifferentialStoredCustomField', 'DifferentialBlameRevisionCommitMessageField' => 'DifferentialCommitMessageCustomField', 'DifferentialBlameRevisionField' => 'DifferentialStoredCustomField', 'DifferentialBlockHeraldAction' => 'HeraldAction', 'DifferentialBlockingReviewerDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'DifferentialBranchField' => 'DifferentialCustomField', 'DifferentialBuildableEngine' => 'HarbormasterBuildableEngine', 'DifferentialChangeDetailMailView' => 'DifferentialMailView', 'DifferentialChangeHeraldFieldGroup' => 'HeraldFieldGroup', 'DifferentialChangeType' => 'Phobject', 'DifferentialChangesSinceLastUpdateField' => 'DifferentialCustomField', 'DifferentialChangeset' => array( 'DifferentialDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', ), 'DifferentialChangesetDetailView' => 'AphrontView', 'DifferentialChangesetEngine' => 'Phobject', 'DifferentialChangesetFileTreeSideNavBuilder' => 'Phobject', 'DifferentialChangesetHTMLRenderer' => 'DifferentialChangesetRenderer', 'DifferentialChangesetListController' => 'DifferentialController', 'DifferentialChangesetListView' => 'AphrontView', 'DifferentialChangesetOneUpMailRenderer' => 'DifferentialChangesetRenderer', 'DifferentialChangesetOneUpRenderer' => 'DifferentialChangesetHTMLRenderer', 'DifferentialChangesetOneUpTestRenderer' => 'DifferentialChangesetTestRenderer', 'DifferentialChangesetParser' => 'Phobject', 'DifferentialChangesetParserTestCase' => 'PhabricatorTestCase', 'DifferentialChangesetQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DifferentialChangesetRenderer' => 'Phobject', 'DifferentialChangesetSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DifferentialChangesetTestRenderer' => 'DifferentialChangesetRenderer', 'DifferentialChangesetTwoUpRenderer' => 'DifferentialChangesetHTMLRenderer', 'DifferentialChangesetTwoUpTestRenderer' => 'DifferentialChangesetTestRenderer', 'DifferentialChangesetViewController' => 'DifferentialController', 'DifferentialCloseConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialCommitMessageCustomField' => 'DifferentialCommitMessageField', 'DifferentialCommitMessageField' => 'Phobject', 'DifferentialCommitMessageFieldTestCase' => 'PhabricatorTestCase', 'DifferentialCommitMessageParser' => 'Phobject', 'DifferentialCommitMessageParserTestCase' => 'PhabricatorTestCase', 'DifferentialCommitsField' => 'DifferentialCustomField', 'DifferentialCommitsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'DifferentialConduitAPIMethod' => 'ConduitAPIMethod', 'DifferentialConflictsCommitMessageField' => 'DifferentialCommitMessageField', 'DifferentialController' => 'PhabricatorController', 'DifferentialCoreCustomField' => 'DifferentialCustomField', 'DifferentialCreateCommentConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialCreateDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialCreateInlineConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialCreateMailReceiver' => 'PhabricatorMailReceiver', 'DifferentialCreateRawDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialCreateRevisionConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialCustomField' => 'PhabricatorCustomField', 'DifferentialCustomFieldDependsOnParser' => 'PhabricatorCustomFieldMonogramParser', 'DifferentialCustomFieldDependsOnParserTestCase' => 'PhabricatorTestCase', 'DifferentialCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage', 'DifferentialCustomFieldRevertsParser' => 'PhabricatorCustomFieldMonogramParser', 'DifferentialCustomFieldRevertsParserTestCase' => 'PhabricatorTestCase', 'DifferentialCustomFieldStorage' => 'PhabricatorCustomFieldStorage', 'DifferentialCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage', 'DifferentialDAO' => 'PhabricatorLiskDAO', 'DifferentialDefaultViewCapability' => 'PhabricatorPolicyCapability', 'DifferentialDiff' => array( 'DifferentialDAO', 'PhabricatorPolicyInterface', 'PhabricatorExtendedPolicyInterface', 'HarbormasterBuildableInterface', 'HarbormasterCircleCIBuildableInterface', 'HarbormasterBuildkiteBuildableInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorDestructibleInterface', 'PhabricatorConduitResultInterface', ), 'DifferentialDiffAffectedFilesHeraldField' => 'DifferentialDiffHeraldField', 'DifferentialDiffAuthorHeraldField' => 'DifferentialDiffHeraldField', 'DifferentialDiffAuthorProjectsHeraldField' => 'DifferentialDiffHeraldField', 'DifferentialDiffContentAddedHeraldField' => 'DifferentialDiffHeraldField', 'DifferentialDiffContentHeraldField' => 'DifferentialDiffHeraldField', 'DifferentialDiffContentRemovedHeraldField' => 'DifferentialDiffHeraldField', 'DifferentialDiffCreateController' => 'DifferentialController', 'DifferentialDiffEditor' => 'PhabricatorApplicationTransactionEditor', 'DifferentialDiffExtractionEngine' => 'Phobject', 'DifferentialDiffHeraldField' => 'HeraldField', 'DifferentialDiffHeraldFieldGroup' => 'HeraldFieldGroup', 'DifferentialDiffInlineCommentQuery' => 'PhabricatorDiffInlineCommentQuery', 'DifferentialDiffPHIDType' => 'PhabricatorPHIDType', 'DifferentialDiffProperty' => 'DifferentialDAO', 'DifferentialDiffQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DifferentialDiffRepositoryHeraldField' => 'DifferentialDiffHeraldField', 'DifferentialDiffRepositoryProjectsHeraldField' => 'DifferentialDiffHeraldField', 'DifferentialDiffSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'DifferentialDiffSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DifferentialDiffTestCase' => 'PhutilTestCase', 'DifferentialDiffTransaction' => 'PhabricatorApplicationTransaction', 'DifferentialDiffTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'DifferentialDiffViewController' => 'DifferentialController', 'DifferentialDoorkeeperRevisionFeedStoryPublisher' => 'DoorkeeperFeedStoryPublisher', 'DifferentialDraftField' => 'DifferentialCoreCustomField', 'DifferentialExactUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'DifferentialFieldParseException' => 'Exception', 'DifferentialFieldValidationException' => 'Exception', 'DifferentialGetAllDiffsConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialGetCommitMessageConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialGetCommitPathsConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialGetDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialGetRawDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialGetRevisionCommentsConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialGetRevisionConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialGetWorkingCopy' => 'Phobject', 'DifferentialGitSVNIDCommitMessageField' => 'DifferentialCommitMessageField', 'DifferentialHarbormasterField' => 'DifferentialCustomField', 'DifferentialHeraldStateReasons' => 'HeraldStateReasons', 'DifferentialHiddenComment' => 'DifferentialDAO', 'DifferentialHostField' => 'DifferentialCustomField', 'DifferentialHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension', 'DifferentialHunk' => array( 'DifferentialDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', ), 'DifferentialHunkParser' => 'Phobject', 'DifferentialHunkParserTestCase' => 'PhabricatorTestCase', 'DifferentialHunkQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DifferentialHunkTestCase' => 'PhutilTestCase', 'DifferentialInlineComment' => array( 'Phobject', 'PhabricatorInlineCommentInterface', ), 'DifferentialInlineCommentEditController' => 'PhabricatorInlineCommentController', 'DifferentialInlineCommentMailView' => 'DifferentialMailView', 'DifferentialInlineCommentQuery' => 'PhabricatorOffsetPagedQuery', 'DifferentialJIRAIssuesCommitMessageField' => 'DifferentialCommitMessageCustomField', 'DifferentialJIRAIssuesField' => 'DifferentialStoredCustomField', 'DifferentialLegacyQuery' => 'Phobject', 'DifferentialLineAdjustmentMap' => 'Phobject', 'DifferentialLintField' => 'DifferentialHarbormasterField', 'DifferentialLintStatus' => 'Phobject', 'DifferentialLocalCommitsView' => 'AphrontView', 'DifferentialMailEngineExtension' => 'PhabricatorMailEngineExtension', 'DifferentialMailView' => 'Phobject', 'DifferentialManiphestTasksField' => 'DifferentialCoreCustomField', 'DifferentialParseCacheGarbageCollector' => 'PhabricatorGarbageCollector', 'DifferentialParseCommitMessageConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialParseRenderTestCase' => 'PhabricatorTestCase', 'DifferentialPathField' => 'DifferentialCustomField', 'DifferentialProjectReviewersField' => 'DifferentialCustomField', 'DifferentialQueryConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialQueryDiffsConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialRawDiffRenderer' => 'Phobject', 'DifferentialReleephRequestFieldSpecification' => 'Phobject', 'DifferentialRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'DifferentialReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'DifferentialRepositoryField' => 'DifferentialCoreCustomField', 'DifferentialRepositoryLookup' => 'Phobject', 'DifferentialRequiredSignaturesField' => 'DifferentialCoreCustomField', 'DifferentialResponsibleDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'DifferentialResponsibleUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'DifferentialResponsibleViewerFunctionDatasource' => 'PhabricatorTypeaheadDatasource', 'DifferentialRevertPlanCommitMessageField' => 'DifferentialCommitMessageCustomField', 'DifferentialRevertPlanField' => 'DifferentialStoredCustomField', 'DifferentialReviewedByCommitMessageField' => 'DifferentialCommitMessageField', 'DifferentialReviewer' => 'DifferentialDAO', 'DifferentialReviewerDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'DifferentialReviewerForRevisionEdgeType' => 'PhabricatorEdgeType', 'DifferentialReviewerStatus' => 'Phobject', 'DifferentialReviewersAddBlockingReviewersHeraldAction' => 'DifferentialReviewersHeraldAction', 'DifferentialReviewersAddBlockingSelfHeraldAction' => 'DifferentialReviewersHeraldAction', 'DifferentialReviewersAddReviewersHeraldAction' => 'DifferentialReviewersHeraldAction', 'DifferentialReviewersAddSelfHeraldAction' => 'DifferentialReviewersHeraldAction', 'DifferentialReviewersCommitMessageField' => 'DifferentialCommitMessageField', 'DifferentialReviewersField' => 'DifferentialCoreCustomField', 'DifferentialReviewersHeraldAction' => 'HeraldAction', 'DifferentialReviewersSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'DifferentialReviewersView' => 'AphrontView', 'DifferentialRevision' => array( 'DifferentialDAO', 'PhabricatorTokenReceiverInterface', 'PhabricatorPolicyInterface', 'PhabricatorExtendedPolicyInterface', 'PhabricatorFlaggableInterface', 'PhrequentTrackableInterface', 'HarbormasterBuildableInterface', 'PhabricatorSubscribableInterface', 'PhabricatorCustomFieldInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorMentionableInterface', 'PhabricatorDestructibleInterface', 'PhabricatorProjectInterface', 'PhabricatorFulltextInterface', 'PhabricatorFerretInterface', 'PhabricatorConduitResultInterface', 'PhabricatorDraftInterface', ), 'DifferentialRevisionAbandonTransaction' => 'DifferentialRevisionActionTransaction', 'DifferentialRevisionAcceptTransaction' => 'DifferentialRevisionReviewTransaction', 'DifferentialRevisionActionTransaction' => 'DifferentialRevisionTransactionType', 'DifferentialRevisionAffectedFilesHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionAuthorHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionAuthorProjectsHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionBuildableTransaction' => 'DifferentialRevisionTransactionType', 'DifferentialRevisionCloseDetailsController' => 'DifferentialController', 'DifferentialRevisionCloseTransaction' => 'DifferentialRevisionActionTransaction', 'DifferentialRevisionClosedStatusDatasource' => 'PhabricatorTypeaheadDatasource', 'DifferentialRevisionCommandeerTransaction' => 'DifferentialRevisionActionTransaction', 'DifferentialRevisionContentAddedHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionContentHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionContentRemovedHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionControlSystem' => 'Phobject', 'DifferentialRevisionDependedOnByRevisionEdgeType' => 'PhabricatorEdgeType', 'DifferentialRevisionDependsOnRevisionEdgeType' => 'PhabricatorEdgeType', 'DifferentialRevisionDraftEngine' => 'PhabricatorDraftEngine', 'DifferentialRevisionEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'DifferentialRevisionEditController' => 'DifferentialController', 'DifferentialRevisionEditEngine' => 'PhabricatorEditEngine', 'DifferentialRevisionFerretEngine' => 'PhabricatorFerretEngine', 'DifferentialRevisionFulltextEngine' => 'PhabricatorFulltextEngine', 'DifferentialRevisionGraph' => 'PhabricatorObjectGraph', 'DifferentialRevisionHasChildRelationship' => 'DifferentialRevisionRelationship', 'DifferentialRevisionHasCommitEdgeType' => 'PhabricatorEdgeType', 'DifferentialRevisionHasCommitRelationship' => 'DifferentialRevisionRelationship', 'DifferentialRevisionHasParentRelationship' => 'DifferentialRevisionRelationship', 'DifferentialRevisionHasReviewerEdgeType' => 'PhabricatorEdgeType', 'DifferentialRevisionHasTaskEdgeType' => 'PhabricatorEdgeType', 'DifferentialRevisionHasTaskRelationship' => 'DifferentialRevisionRelationship', 'DifferentialRevisionHeraldField' => 'HeraldField', 'DifferentialRevisionHeraldFieldGroup' => 'HeraldFieldGroup', 'DifferentialRevisionHoldDraftTransaction' => 'DifferentialRevisionTransactionType', 'DifferentialRevisionIDCommitMessageField' => 'DifferentialCommitMessageField', 'DifferentialRevisionInlineTransaction' => 'PhabricatorModularTransactionType', 'DifferentialRevisionInlinesController' => 'DifferentialController', 'DifferentialRevisionListController' => 'DifferentialController', 'DifferentialRevisionListView' => 'AphrontView', 'DifferentialRevisionMailReceiver' => 'PhabricatorObjectMailReceiver', 'DifferentialRevisionOpenStatusDatasource' => 'PhabricatorTypeaheadDatasource', 'DifferentialRevisionOperationController' => 'DifferentialController', 'DifferentialRevisionPHIDType' => 'PhabricatorPHIDType', 'DifferentialRevisionPackageHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionPackageOwnerHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionPlanChangesTransaction' => 'DifferentialRevisionActionTransaction', 'DifferentialRevisionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DifferentialRevisionReclaimTransaction' => 'DifferentialRevisionActionTransaction', 'DifferentialRevisionRejectTransaction' => 'DifferentialRevisionReviewTransaction', 'DifferentialRevisionRelationship' => 'PhabricatorObjectRelationship', 'DifferentialRevisionRelationshipSource' => 'PhabricatorObjectRelationshipSource', 'DifferentialRevisionReopenTransaction' => 'DifferentialRevisionActionTransaction', 'DifferentialRevisionRepositoryHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionRepositoryProjectsHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionRepositoryTransaction' => 'DifferentialRevisionTransactionType', 'DifferentialRevisionRequestReviewTransaction' => 'DifferentialRevisionActionTransaction', 'DifferentialRevisionRequiredActionResultBucket' => 'DifferentialRevisionResultBucket', 'DifferentialRevisionResignTransaction' => 'DifferentialRevisionReviewTransaction', 'DifferentialRevisionResultBucket' => 'PhabricatorSearchResultBucket', 'DifferentialRevisionReviewTransaction' => 'DifferentialRevisionActionTransaction', 'DifferentialRevisionReviewersHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionReviewersTransaction' => 'DifferentialRevisionTransactionType', 'DifferentialRevisionSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'DifferentialRevisionSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DifferentialRevisionStatus' => 'Phobject', 'DifferentialRevisionStatusDatasource' => 'PhabricatorTypeaheadDatasource', 'DifferentialRevisionStatusFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'DifferentialRevisionStatusHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionStatusTransaction' => 'DifferentialRevisionTransactionType', 'DifferentialRevisionSummaryHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionSummaryTransaction' => 'DifferentialRevisionTransactionType', 'DifferentialRevisionTestPlanHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionTestPlanTransaction' => 'DifferentialRevisionTransactionType', 'DifferentialRevisionTitleHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionTitleTransaction' => 'DifferentialRevisionTransactionType', 'DifferentialRevisionTransactionType' => 'PhabricatorModularTransactionType', 'DifferentialRevisionUpdateHistoryView' => 'AphrontView', 'DifferentialRevisionUpdateTransaction' => 'DifferentialRevisionTransactionType', 'DifferentialRevisionViewController' => 'DifferentialController', 'DifferentialRevisionVoidTransaction' => 'DifferentialRevisionTransactionType', 'DifferentialRevisionWrongStateTransaction' => 'DifferentialRevisionTransactionType', 'DifferentialSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'DifferentialSetDiffPropertyConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialStoredCustomField' => 'DifferentialCustomField', 'DifferentialSubscribersCommitMessageField' => 'DifferentialCommitMessageField', 'DifferentialSummaryCommitMessageField' => 'DifferentialCommitMessageField', 'DifferentialSummaryField' => 'DifferentialCoreCustomField', 'DifferentialTagsCommitMessageField' => 'DifferentialCommitMessageField', 'DifferentialTasksCommitMessageField' => 'DifferentialCommitMessageField', 'DifferentialTestPlanCommitMessageField' => 'DifferentialCommitMessageField', 'DifferentialTestPlanField' => 'DifferentialCoreCustomField', 'DifferentialTitleCommitMessageField' => 'DifferentialCommitMessageField', 'DifferentialTransaction' => 'PhabricatorModularTransaction', 'DifferentialTransactionComment' => 'PhabricatorApplicationTransactionComment', 'DifferentialTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'DifferentialTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'DifferentialTransactionView' => 'PhabricatorApplicationTransactionView', 'DifferentialUnitField' => 'DifferentialCustomField', 'DifferentialUnitStatus' => 'Phobject', 'DifferentialUnitTestResult' => 'Phobject', 'DifferentialUpdateRevisionConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DiffusionAuditorDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'DiffusionAuditorFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'DiffusionAuditorsAddAuditorsHeraldAction' => 'DiffusionAuditorsHeraldAction', 'DiffusionAuditorsAddSelfHeraldAction' => 'DiffusionAuditorsHeraldAction', 'DiffusionAuditorsHeraldAction' => 'HeraldAction', 'DiffusionBlameConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionBlameController' => 'DiffusionController', 'DiffusionBlameQuery' => 'DiffusionQuery', 'DiffusionBlockHeraldAction' => 'HeraldAction', 'DiffusionBranchListView' => 'DiffusionView', 'DiffusionBranchQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionBranchTableController' => 'DiffusionController', 'DiffusionBranchTableView' => 'DiffusionView', 'DiffusionBrowseController' => 'DiffusionController', 'DiffusionBrowseQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionBrowseResultSet' => 'Phobject', 'DiffusionBrowseTableView' => 'DiffusionView', 'DiffusionBuildableEngine' => 'HarbormasterBuildableEngine', 'DiffusionCacheEngineExtension' => 'PhabricatorCacheEngineExtension', 'DiffusionCachedResolveRefsQuery' => 'DiffusionLowLevelQuery', 'DiffusionChangeController' => 'DiffusionController', 'DiffusionChangeHeraldFieldGroup' => 'HeraldFieldGroup', 'DiffusionCloneController' => 'DiffusionController', 'DiffusionCloneURIView' => 'AphrontView', 'DiffusionCommandEngine' => 'Phobject', 'DiffusionCommandEngineTestCase' => 'PhabricatorTestCase', 'DiffusionCommitAcceptTransaction' => 'DiffusionCommitAuditTransaction', 'DiffusionCommitActionTransaction' => 'DiffusionCommitTransactionType', 'DiffusionCommitAffectedFilesHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitAuditStatus' => 'Phobject', 'DiffusionCommitAuditTransaction' => 'DiffusionCommitActionTransaction', 'DiffusionCommitAuditorsHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitAuditorsTransaction' => 'DiffusionCommitTransactionType', 'DiffusionCommitAuthorHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitAuthorProjectsHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitAutocloseHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitBranchesController' => 'DiffusionController', 'DiffusionCommitBranchesHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitBuildableTransaction' => 'DiffusionCommitTransactionType', 'DiffusionCommitCommitterHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitCommitterProjectsHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitConcernTransaction' => 'DiffusionCommitAuditTransaction', 'DiffusionCommitController' => 'DiffusionController', 'DiffusionCommitDiffContentAddedHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitDiffContentHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitDiffContentRemovedHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitDiffEnormousHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitDraftEngine' => 'PhabricatorDraftEngine', 'DiffusionCommitEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'DiffusionCommitEditController' => 'DiffusionController', 'DiffusionCommitEditEngine' => 'PhabricatorEditEngine', 'DiffusionCommitFerretEngine' => 'PhabricatorFerretEngine', 'DiffusionCommitFulltextEngine' => 'PhabricatorFulltextEngine', 'DiffusionCommitHasPackageEdgeType' => 'PhabricatorEdgeType', 'DiffusionCommitHasRevisionEdgeType' => 'PhabricatorEdgeType', 'DiffusionCommitHasRevisionRelationship' => 'DiffusionCommitRelationship', 'DiffusionCommitHasTaskEdgeType' => 'PhabricatorEdgeType', 'DiffusionCommitHasTaskRelationship' => 'DiffusionCommitRelationship', 'DiffusionCommitHash' => 'Phobject', 'DiffusionCommitHeraldField' => 'HeraldField', 'DiffusionCommitHeraldFieldGroup' => 'HeraldFieldGroup', 'DiffusionCommitHintQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DiffusionCommitHookEngine' => 'Phobject', 'DiffusionCommitHookRejectException' => 'Exception', 'DiffusionCommitListController' => 'DiffusionController', 'DiffusionCommitListView' => 'AphrontView', 'DiffusionCommitMergeHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitMessageHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitPackageAuditHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitPackageHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitPackageOwnerHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitParentsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionCommitQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DiffusionCommitRef' => 'Phobject', 'DiffusionCommitRelationship' => 'PhabricatorObjectRelationship', 'DiffusionCommitRelationshipSource' => 'PhabricatorObjectRelationshipSource', 'DiffusionCommitRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'DiffusionCommitRemarkupRuleTestCase' => 'PhabricatorTestCase', 'DiffusionCommitRepositoryHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitRepositoryProjectsHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitRequiredActionResultBucket' => 'DiffusionCommitResultBucket', 'DiffusionCommitResignTransaction' => 'DiffusionCommitAuditTransaction', 'DiffusionCommitResultBucket' => 'PhabricatorSearchResultBucket', 'DiffusionCommitRevertedByCommitEdgeType' => 'PhabricatorEdgeType', 'DiffusionCommitRevertsCommitEdgeType' => 'PhabricatorEdgeType', 'DiffusionCommitReviewerHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitRevisionAcceptedHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitRevisionAcceptingReviewersHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitRevisionHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitRevisionReviewersHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitRevisionSubscribersHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'DiffusionCommitStateTransaction' => 'DiffusionCommitTransactionType', 'DiffusionCommitTagsController' => 'DiffusionController', 'DiffusionCommitTransactionType' => 'PhabricatorModularTransactionType', 'DiffusionCommitVerifyTransaction' => 'DiffusionCommitAuditTransaction', 'DiffusionCompareController' => 'DiffusionController', 'DiffusionConduitAPIMethod' => 'ConduitAPIMethod', 'DiffusionController' => 'PhabricatorController', 'DiffusionCreateRepositoriesCapability' => 'PhabricatorPolicyCapability', 'DiffusionDaemonLockException' => 'Exception', 'DiffusionDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension', 'DiffusionDefaultEditCapability' => 'PhabricatorPolicyCapability', 'DiffusionDefaultPushCapability' => 'PhabricatorPolicyCapability', 'DiffusionDefaultViewCapability' => 'PhabricatorPolicyCapability', 'DiffusionDiffController' => 'DiffusionController', 'DiffusionDiffInlineCommentQuery' => 'PhabricatorDiffInlineCommentQuery', 'DiffusionDiffQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionDocumentController' => 'DiffusionController', 'DiffusionDocumentRenderingEngine' => 'PhabricatorDocumentRenderingEngine', 'DiffusionDoorkeeperCommitFeedStoryPublisher' => 'DoorkeeperFeedStoryPublisher', 'DiffusionEmptyResultView' => 'DiffusionView', 'DiffusionExistsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionExternalController' => 'DiffusionController', 'DiffusionExternalSymbolQuery' => 'Phobject', 'DiffusionExternalSymbolsSource' => 'Phobject', 'DiffusionFileContentQuery' => 'DiffusionFileFutureQuery', 'DiffusionFileContentQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionFileFutureQuery' => 'DiffusionQuery', 'DiffusionFindSymbolsConduitAPIMethod' => 'DiffusionConduitAPIMethod', 'DiffusionGetLintMessagesConduitAPIMethod' => 'DiffusionConduitAPIMethod', 'DiffusionGetRecentCommitsByPathConduitAPIMethod' => 'DiffusionConduitAPIMethod', 'DiffusionGitBlameQuery' => 'DiffusionBlameQuery', 'DiffusionGitBranch' => 'Phobject', 'DiffusionGitBranchTestCase' => 'PhabricatorTestCase', 'DiffusionGitCommandEngine' => 'DiffusionCommandEngine', 'DiffusionGitFileContentQuery' => 'DiffusionFileContentQuery', 'DiffusionGitLFSAuthenticateWorkflow' => 'DiffusionGitSSHWorkflow', 'DiffusionGitLFSResponse' => 'AphrontResponse', 'DiffusionGitLFSTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType', 'DiffusionGitRawDiffQuery' => 'DiffusionRawDiffQuery', 'DiffusionGitReceivePackSSHWorkflow' => 'DiffusionGitSSHWorkflow', 'DiffusionGitRequest' => 'DiffusionRequest', 'DiffusionGitResponse' => 'AphrontResponse', 'DiffusionGitSSHWorkflow' => array( 'DiffusionSSHWorkflow', 'DiffusionRepositoryClusterEngineLogInterface', ), 'DiffusionGitUploadPackSSHWorkflow' => 'DiffusionGitSSHWorkflow', 'DiffusionGraphController' => 'DiffusionController', 'DiffusionHistoryController' => 'DiffusionController', 'DiffusionHistoryListView' => 'DiffusionHistoryView', 'DiffusionHistoryQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionHistoryTableView' => 'DiffusionHistoryView', 'DiffusionHistoryView' => 'DiffusionView', 'DiffusionHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension', 'DiffusionIdentityAssigneeDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'DiffusionIdentityAssigneeEditField' => 'PhabricatorTokenizerEditField', 'DiffusionIdentityAssigneeSearchField' => 'PhabricatorSearchTokenizerField', 'DiffusionIdentityEditController' => 'DiffusionController', 'DiffusionIdentityListController' => 'DiffusionController', 'DiffusionIdentityUnassignedDatasource' => 'PhabricatorTypeaheadDatasource', 'DiffusionIdentityViewController' => 'DiffusionController', 'DiffusionInlineCommentController' => 'PhabricatorInlineCommentController', 'DiffusionInlineCommentPreviewController' => 'PhabricatorInlineCommentPreviewController', 'DiffusionInternalAncestorsConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionInternalGitRawDiffQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionLastModifiedController' => 'DiffusionController', 'DiffusionLastModifiedQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionLintController' => 'DiffusionController', 'DiffusionLintCountQuery' => 'PhabricatorQuery', 'DiffusionLintSaveRunner' => 'Phobject', 'DiffusionLocalRepositoryFilter' => 'Phobject', 'DiffusionLogController' => 'DiffusionController', 'DiffusionLookSoonConduitAPIMethod' => 'DiffusionConduitAPIMethod', 'DiffusionLowLevelCommitFieldsQuery' => 'DiffusionLowLevelQuery', 'DiffusionLowLevelCommitQuery' => 'DiffusionLowLevelQuery', + 'DiffusionLowLevelFilesizeQuery' => 'DiffusionLowLevelQuery', 'DiffusionLowLevelGitRefQuery' => 'DiffusionLowLevelQuery', 'DiffusionLowLevelMercurialBranchesQuery' => 'DiffusionLowLevelQuery', 'DiffusionLowLevelMercurialPathsQuery' => 'DiffusionLowLevelQuery', 'DiffusionLowLevelParentsQuery' => 'DiffusionLowLevelQuery', 'DiffusionLowLevelQuery' => 'Phobject', 'DiffusionLowLevelResolveRefsQuery' => 'DiffusionLowLevelQuery', 'DiffusionMercurialBlameQuery' => 'DiffusionBlameQuery', 'DiffusionMercurialCommandEngine' => 'DiffusionCommandEngine', 'DiffusionMercurialFileContentQuery' => 'DiffusionFileContentQuery', 'DiffusionMercurialFlagInjectionException' => 'Exception', 'DiffusionMercurialRawDiffQuery' => 'DiffusionRawDiffQuery', 'DiffusionMercurialRequest' => 'DiffusionRequest', 'DiffusionMercurialResponse' => 'AphrontResponse', 'DiffusionMercurialSSHWorkflow' => 'DiffusionSSHWorkflow', 'DiffusionMercurialServeSSHWorkflow' => 'DiffusionMercurialSSHWorkflow', 'DiffusionMercurialWireClientSSHProtocolChannel' => 'PhutilProtocolChannel', 'DiffusionMercurialWireProtocol' => 'Phobject', 'DiffusionMercurialWireProtocolTests' => 'PhabricatorTestCase', 'DiffusionMercurialWireSSHTestCase' => 'PhabricatorTestCase', 'DiffusionMergedCommitsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionPathChange' => 'Phobject', 'DiffusionPathChangeQuery' => 'Phobject', 'DiffusionPathCompleteController' => 'DiffusionController', 'DiffusionPathIDQuery' => 'Phobject', 'DiffusionPathQuery' => 'Phobject', 'DiffusionPathQueryTestCase' => 'PhabricatorTestCase', 'DiffusionPathTreeController' => 'DiffusionController', 'DiffusionPathValidateController' => 'DiffusionController', 'DiffusionPatternSearchView' => 'DiffusionView', 'DiffusionPhpExternalSymbolsSource' => 'DiffusionExternalSymbolsSource', 'DiffusionPreCommitContentAffectedFilesHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentAuthorHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentAuthorProjectsHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentAuthorRawHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentBranchesHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentCommitterHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentCommitterProjectsHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentCommitterRawHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentDiffContentAddedHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentDiffContentHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentDiffContentRemovedHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentDiffEnormousHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentHeraldField' => 'HeraldField', 'DiffusionPreCommitContentMergeHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentMessageHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentPackageHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentPackageOwnerHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentPusherHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentPusherIsCommitterHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentPusherProjectsHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentRepositoryHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentRepositoryProjectsHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentRevisionAcceptedHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentRevisionAcceptingReviewersHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentRevisionHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentRevisionReviewersHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentRevisionSubscribersHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitRefChangeHeraldField' => 'DiffusionPreCommitRefHeraldField', 'DiffusionPreCommitRefHeraldField' => 'HeraldField', 'DiffusionPreCommitRefHeraldFieldGroup' => 'HeraldFieldGroup', 'DiffusionPreCommitRefNameHeraldField' => 'DiffusionPreCommitRefHeraldField', 'DiffusionPreCommitRefPusherHeraldField' => 'DiffusionPreCommitRefHeraldField', 'DiffusionPreCommitRefPusherProjectsHeraldField' => 'DiffusionPreCommitRefHeraldField', 'DiffusionPreCommitRefRepositoryHeraldField' => 'DiffusionPreCommitRefHeraldField', 'DiffusionPreCommitRefRepositoryProjectsHeraldField' => 'DiffusionPreCommitRefHeraldField', 'DiffusionPreCommitRefTypeHeraldField' => 'DiffusionPreCommitRefHeraldField', 'DiffusionPreCommitUsesGitLFSHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPullEventGarbageCollector' => 'PhabricatorGarbageCollector', 'DiffusionPullLogListController' => 'DiffusionLogController', 'DiffusionPullLogListView' => 'AphrontView', 'DiffusionPullLogSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DiffusionPushCapability' => 'PhabricatorPolicyCapability', 'DiffusionPushEventViewController' => 'DiffusionLogController', 'DiffusionPushLogListController' => 'DiffusionLogController', 'DiffusionPushLogListView' => 'AphrontView', 'DiffusionPythonExternalSymbolsSource' => 'DiffusionExternalSymbolsSource', 'DiffusionQuery' => 'PhabricatorQuery', 'DiffusionQueryCommitsConduitAPIMethod' => 'DiffusionConduitAPIMethod', 'DiffusionQueryConduitAPIMethod' => 'DiffusionConduitAPIMethod', 'DiffusionQueryPathsConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionRawDiffQuery' => 'DiffusionFileFutureQuery', 'DiffusionRawDiffQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionReadmeView' => 'DiffusionView', 'DiffusionRefDatasource' => 'PhabricatorTypeaheadDatasource', 'DiffusionRefNotFoundException' => 'Exception', 'DiffusionRefTableController' => 'DiffusionController', 'DiffusionRefsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionRenameHistoryQuery' => 'Phobject', 'DiffusionRepositoryActionsManagementPanel' => 'DiffusionRepositoryManagementPanel', 'DiffusionRepositoryAutomationManagementPanel' => 'DiffusionRepositoryManagementPanel', 'DiffusionRepositoryBasicsManagementPanel' => 'DiffusionRepositoryManagementPanel', 'DiffusionRepositoryBranchesManagementPanel' => 'DiffusionRepositoryManagementPanel', 'DiffusionRepositoryByIDRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'DiffusionRepositoryClusterEngine' => 'Phobject', 'DiffusionRepositoryController' => 'DiffusionController', 'DiffusionRepositoryDatasource' => 'PhabricatorTypeaheadDatasource', 'DiffusionRepositoryDefaultController' => 'DiffusionController', 'DiffusionRepositoryEditActivateController' => 'DiffusionRepositoryManageController', 'DiffusionRepositoryEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'DiffusionRepositoryEditController' => 'DiffusionRepositoryManageController', 'DiffusionRepositoryEditDangerousController' => 'DiffusionRepositoryManageController', 'DiffusionRepositoryEditDeleteController' => 'DiffusionRepositoryManageController', 'DiffusionRepositoryEditEngine' => 'PhabricatorEditEngine', 'DiffusionRepositoryEditEnormousController' => 'DiffusionRepositoryManageController', 'DiffusionRepositoryEditUpdateController' => 'DiffusionRepositoryManageController', 'DiffusionRepositoryFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'DiffusionRepositoryHistoryManagementPanel' => 'DiffusionRepositoryManagementPanel', 'DiffusionRepositoryIdentityEditor' => 'PhabricatorApplicationTransactionEditor', 'DiffusionRepositoryIdentitySearchEngine' => 'PhabricatorApplicationSearchEngine', + 'DiffusionRepositoryLimitsManagementPanel' => 'DiffusionRepositoryManagementPanel', 'DiffusionRepositoryListController' => 'DiffusionController', 'DiffusionRepositoryManageController' => 'DiffusionController', 'DiffusionRepositoryManagePanelsController' => 'DiffusionRepositoryManageController', + 'DiffusionRepositoryManagementBuildsPanelGroup' => 'DiffusionRepositoryManagementPanelGroup', + 'DiffusionRepositoryManagementIntegrationsPanelGroup' => 'DiffusionRepositoryManagementPanelGroup', + 'DiffusionRepositoryManagementMainPanelGroup' => 'DiffusionRepositoryManagementPanelGroup', + 'DiffusionRepositoryManagementOtherPanelGroup' => 'DiffusionRepositoryManagementPanelGroup', 'DiffusionRepositoryManagementPanel' => 'Phobject', + 'DiffusionRepositoryManagementPanelGroup' => 'Phobject', 'DiffusionRepositoryPath' => 'Phobject', 'DiffusionRepositoryPoliciesManagementPanel' => 'DiffusionRepositoryManagementPanel', 'DiffusionRepositoryProfilePictureController' => 'DiffusionController', 'DiffusionRepositoryRef' => 'Phobject', 'DiffusionRepositoryRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'DiffusionRepositorySearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'DiffusionRepositoryStagingManagementPanel' => 'DiffusionRepositoryManagementPanel', 'DiffusionRepositoryStorageManagementPanel' => 'DiffusionRepositoryManagementPanel', 'DiffusionRepositorySubversionManagementPanel' => 'DiffusionRepositoryManagementPanel', 'DiffusionRepositorySymbolsManagementPanel' => 'DiffusionRepositoryManagementPanel', 'DiffusionRepositoryTag' => 'Phobject', 'DiffusionRepositoryTestAutomationController' => 'DiffusionRepositoryManageController', 'DiffusionRepositoryURICredentialController' => 'DiffusionController', 'DiffusionRepositoryURIDisableController' => 'DiffusionController', 'DiffusionRepositoryURIEditController' => 'DiffusionController', 'DiffusionRepositoryURIViewController' => 'DiffusionController', 'DiffusionRepositoryURIsIndexEngineExtension' => 'PhabricatorIndexEngineExtension', 'DiffusionRepositoryURIsManagementPanel' => 'DiffusionRepositoryManagementPanel', 'DiffusionRepositoryURIsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'DiffusionRequest' => 'Phobject', 'DiffusionResolveRefsConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionResolveUserQuery' => 'Phobject', 'DiffusionSSHWorkflow' => 'PhabricatorSSHWorkflow', 'DiffusionSearchQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionServeController' => 'DiffusionController', 'DiffusionSetPasswordSettingsPanel' => 'PhabricatorSettingsPanel', 'DiffusionSetupException' => 'Exception', 'DiffusionSubversionCommandEngine' => 'DiffusionCommandEngine', 'DiffusionSubversionSSHWorkflow' => 'DiffusionSSHWorkflow', 'DiffusionSubversionServeSSHWorkflow' => 'DiffusionSubversionSSHWorkflow', 'DiffusionSubversionWireProtocol' => 'Phobject', 'DiffusionSubversionWireProtocolTestCase' => 'PhabricatorTestCase', 'DiffusionSvnBlameQuery' => 'DiffusionBlameQuery', 'DiffusionSvnFileContentQuery' => 'DiffusionFileContentQuery', 'DiffusionSvnRawDiffQuery' => 'DiffusionRawDiffQuery', 'DiffusionSvnRequest' => 'DiffusionRequest', 'DiffusionSymbolController' => 'DiffusionController', 'DiffusionSymbolDatasource' => 'PhabricatorTypeaheadDatasource', 'DiffusionSymbolQuery' => 'PhabricatorOffsetPagedQuery', 'DiffusionSyncLogListController' => 'DiffusionLogController', 'DiffusionSyncLogListView' => 'AphrontView', 'DiffusionSyncLogSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DiffusionTagListController' => 'DiffusionController', 'DiffusionTagListView' => 'DiffusionView', 'DiffusionTagTableView' => 'DiffusionView', 'DiffusionTaggedRepositoriesFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'DiffusionTagsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionURIEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'DiffusionURIEditEngine' => 'PhabricatorEditEngine', 'DiffusionURIEditor' => 'PhabricatorApplicationTransactionEditor', 'DiffusionURITestCase' => 'PhutilTestCase', 'DiffusionUpdateCoverageConduitAPIMethod' => 'DiffusionConduitAPIMethod', 'DiffusionView' => 'AphrontView', 'DivinerArticleAtomizer' => 'DivinerAtomizer', 'DivinerAtom' => 'Phobject', 'DivinerAtomCache' => 'DivinerDiskCache', 'DivinerAtomController' => 'DivinerController', 'DivinerAtomListController' => 'DivinerController', 'DivinerAtomPHIDType' => 'PhabricatorPHIDType', 'DivinerAtomQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DivinerAtomRef' => 'Phobject', 'DivinerAtomSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DivinerAtomizeWorkflow' => 'DivinerWorkflow', 'DivinerAtomizer' => 'Phobject', 'DivinerBookController' => 'DivinerController', 'DivinerBookDatasource' => 'PhabricatorTypeaheadDatasource', 'DivinerBookEditController' => 'DivinerController', 'DivinerBookItemView' => 'AphrontTagView', 'DivinerBookPHIDType' => 'PhabricatorPHIDType', 'DivinerBookQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DivinerController' => 'PhabricatorController', 'DivinerDAO' => 'PhabricatorLiskDAO', 'DivinerDefaultEditCapability' => 'PhabricatorPolicyCapability', 'DivinerDefaultRenderer' => 'DivinerRenderer', 'DivinerDefaultViewCapability' => 'PhabricatorPolicyCapability', 'DivinerDiskCache' => 'Phobject', 'DivinerFileAtomizer' => 'DivinerAtomizer', 'DivinerFindController' => 'DivinerController', 'DivinerGenerateWorkflow' => 'DivinerWorkflow', 'DivinerLiveAtom' => 'DivinerDAO', 'DivinerLiveBook' => array( 'DivinerDAO', 'PhabricatorPolicyInterface', 'PhabricatorProjectInterface', 'PhabricatorDestructibleInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorFulltextInterface', ), 'DivinerLiveBookEditor' => 'PhabricatorApplicationTransactionEditor', 'DivinerLiveBookFulltextEngine' => 'PhabricatorFulltextEngine', 'DivinerLiveBookTransaction' => 'PhabricatorApplicationTransaction', 'DivinerLiveBookTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'DivinerLivePublisher' => 'DivinerPublisher', 'DivinerLiveSymbol' => array( 'DivinerDAO', 'PhabricatorPolicyInterface', 'PhabricatorMarkupInterface', 'PhabricatorDestructibleInterface', 'PhabricatorFulltextInterface', ), 'DivinerLiveSymbolFulltextEngine' => 'PhabricatorFulltextEngine', 'DivinerMainController' => 'DivinerController', 'DivinerPHPAtomizer' => 'DivinerAtomizer', 'DivinerParameterTableView' => 'AphrontTagView', 'DivinerPublishCache' => 'DivinerDiskCache', 'DivinerPublisher' => 'Phobject', 'DivinerRenderer' => 'Phobject', 'DivinerReturnTableView' => 'AphrontTagView', 'DivinerSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'DivinerSectionView' => 'AphrontTagView', 'DivinerStaticPublisher' => 'DivinerPublisher', 'DivinerSymbolRemarkupRule' => 'PhutilRemarkupRule', 'DivinerWorkflow' => 'PhabricatorManagementWorkflow', 'DoorkeeperAsanaFeedWorker' => 'DoorkeeperFeedWorker', 'DoorkeeperAsanaRemarkupRule' => 'DoorkeeperRemarkupRule', 'DoorkeeperBridge' => 'Phobject', 'DoorkeeperBridgeAsana' => 'DoorkeeperBridge', 'DoorkeeperBridgeGitHub' => 'DoorkeeperBridge', 'DoorkeeperBridgeGitHubIssue' => 'DoorkeeperBridgeGitHub', 'DoorkeeperBridgeGitHubUser' => 'DoorkeeperBridgeGitHub', 'DoorkeeperBridgeJIRA' => 'DoorkeeperBridge', 'DoorkeeperBridgeJIRATestCase' => 'PhabricatorTestCase', 'DoorkeeperBridgedObjectCurtainExtension' => 'PHUICurtainExtension', 'DoorkeeperDAO' => 'PhabricatorLiskDAO', 'DoorkeeperExternalObject' => array( 'DoorkeeperDAO', 'PhabricatorPolicyInterface', ), 'DoorkeeperExternalObjectPHIDType' => 'PhabricatorPHIDType', 'DoorkeeperExternalObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DoorkeeperFeedStoryPublisher' => 'Phobject', 'DoorkeeperFeedWorker' => 'FeedPushWorker', 'DoorkeeperImportEngine' => 'Phobject', 'DoorkeeperJIRAFeedWorker' => 'DoorkeeperFeedWorker', 'DoorkeeperJIRARemarkupRule' => 'DoorkeeperRemarkupRule', 'DoorkeeperMissingLinkException' => 'Exception', 'DoorkeeperObjectRef' => 'Phobject', 'DoorkeeperRemarkupRule' => 'PhutilRemarkupRule', 'DoorkeeperSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'DoorkeeperTagView' => 'AphrontView', 'DoorkeeperTagsController' => 'PhabricatorController', 'DrydockAcquiredBrokenResourceException' => 'Exception', 'DrydockAlmanacServiceHostBlueprintImplementation' => 'DrydockBlueprintImplementation', 'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface', 'DrydockAuthorization' => array( 'DrydockDAO', 'PhabricatorPolicyInterface', 'PhabricatorConduitResultInterface', ), 'DrydockAuthorizationAuthorizeController' => 'DrydockController', 'DrydockAuthorizationListController' => 'DrydockController', 'DrydockAuthorizationListView' => 'AphrontView', 'DrydockAuthorizationPHIDType' => 'PhabricatorPHIDType', 'DrydockAuthorizationQuery' => 'DrydockQuery', 'DrydockAuthorizationSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'DrydockAuthorizationSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockAuthorizationViewController' => 'DrydockController', 'DrydockBlueprint' => array( 'DrydockDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorCustomFieldInterface', 'PhabricatorNgramsInterface', 'PhabricatorProjectInterface', 'PhabricatorConduitResultInterface', ), 'DrydockBlueprintController' => 'DrydockController', 'DrydockBlueprintCoreCustomField' => array( 'DrydockBlueprintCustomField', 'PhabricatorStandardCustomFieldInterface', ), 'DrydockBlueprintCustomField' => 'PhabricatorCustomField', 'DrydockBlueprintDatasource' => 'PhabricatorTypeaheadDatasource', 'DrydockBlueprintDisableController' => 'DrydockBlueprintController', 'DrydockBlueprintDisableTransaction' => 'DrydockBlueprintTransactionType', 'DrydockBlueprintEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'DrydockBlueprintEditController' => 'DrydockBlueprintController', 'DrydockBlueprintEditEngine' => 'PhabricatorEditEngine', 'DrydockBlueprintEditor' => 'PhabricatorApplicationTransactionEditor', 'DrydockBlueprintImplementation' => 'Phobject', 'DrydockBlueprintImplementationTestCase' => 'PhabricatorTestCase', 'DrydockBlueprintListController' => 'DrydockBlueprintController', 'DrydockBlueprintNameNgrams' => 'PhabricatorSearchNgrams', 'DrydockBlueprintNameTransaction' => 'DrydockBlueprintTransactionType', 'DrydockBlueprintPHIDType' => 'PhabricatorPHIDType', 'DrydockBlueprintQuery' => 'DrydockQuery', 'DrydockBlueprintSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'DrydockBlueprintSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockBlueprintTransaction' => 'PhabricatorModularTransaction', 'DrydockBlueprintTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'DrydockBlueprintTransactionType' => 'PhabricatorModularTransactionType', 'DrydockBlueprintTypeTransaction' => 'DrydockBlueprintTransactionType', 'DrydockBlueprintViewController' => 'DrydockBlueprintController', 'DrydockCommand' => array( 'DrydockDAO', 'PhabricatorPolicyInterface', ), 'DrydockCommandError' => 'Phobject', 'DrydockCommandInterface' => 'DrydockInterface', 'DrydockCommandQuery' => 'DrydockQuery', 'DrydockConsoleController' => 'DrydockController', 'DrydockController' => 'PhabricatorController', 'DrydockCreateBlueprintsCapability' => 'PhabricatorPolicyCapability', 'DrydockDAO' => 'PhabricatorLiskDAO', 'DrydockDefaultEditCapability' => 'PhabricatorPolicyCapability', 'DrydockDefaultViewCapability' => 'PhabricatorPolicyCapability', 'DrydockFilesystemInterface' => 'DrydockInterface', 'DrydockInterface' => 'Phobject', 'DrydockLandRepositoryOperation' => 'DrydockRepositoryOperationType', 'DrydockLease' => array( 'DrydockDAO', 'PhabricatorPolicyInterface', 'PhabricatorConduitResultInterface', ), 'DrydockLeaseAcquiredLogType' => 'DrydockLogType', 'DrydockLeaseActivatedLogType' => 'DrydockLogType', 'DrydockLeaseActivationFailureLogType' => 'DrydockLogType', 'DrydockLeaseActivationYieldLogType' => 'DrydockLogType', 'DrydockLeaseAllocationFailureLogType' => 'DrydockLogType', 'DrydockLeaseController' => 'DrydockController', 'DrydockLeaseDatasource' => 'PhabricatorTypeaheadDatasource', 'DrydockLeaseDestroyedLogType' => 'DrydockLogType', 'DrydockLeaseListController' => 'DrydockLeaseController', 'DrydockLeaseListView' => 'AphrontView', 'DrydockLeaseNoAuthorizationsLogType' => 'DrydockLogType', 'DrydockLeaseNoBlueprintsLogType' => 'DrydockLogType', 'DrydockLeasePHIDType' => 'PhabricatorPHIDType', 'DrydockLeaseQuery' => 'DrydockQuery', 'DrydockLeaseQueuedLogType' => 'DrydockLogType', 'DrydockLeaseReacquireLogType' => 'DrydockLogType', 'DrydockLeaseReclaimLogType' => 'DrydockLogType', 'DrydockLeaseReleaseController' => 'DrydockLeaseController', 'DrydockLeaseReleasedLogType' => 'DrydockLogType', 'DrydockLeaseSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'DrydockLeaseSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockLeaseStatus' => 'PhabricatorObjectStatus', 'DrydockLeaseUpdateWorker' => 'DrydockWorker', 'DrydockLeaseViewController' => 'DrydockLeaseController', 'DrydockLeaseWaitingForResourcesLogType' => 'DrydockLogType', 'DrydockLog' => array( 'DrydockDAO', 'PhabricatorPolicyInterface', ), 'DrydockLogController' => 'DrydockController', 'DrydockLogGarbageCollector' => 'PhabricatorGarbageCollector', 'DrydockLogListController' => 'DrydockLogController', 'DrydockLogListView' => 'AphrontView', 'DrydockLogQuery' => 'DrydockQuery', 'DrydockLogSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockLogType' => 'Phobject', 'DrydockManagementCommandWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementLeaseWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementReclaimWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementReleaseLeaseWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementReleaseResourceWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementUpdateLeaseWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementUpdateResourceWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementWorkflow' => 'PhabricatorManagementWorkflow', 'DrydockObjectAuthorizationView' => 'AphrontView', 'DrydockOperationWorkLogType' => 'DrydockLogType', 'DrydockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DrydockRepositoryOperation' => array( 'DrydockDAO', 'PhabricatorPolicyInterface', ), 'DrydockRepositoryOperationController' => 'DrydockController', 'DrydockRepositoryOperationDismissController' => 'DrydockRepositoryOperationController', 'DrydockRepositoryOperationListController' => 'DrydockRepositoryOperationController', 'DrydockRepositoryOperationPHIDType' => 'PhabricatorPHIDType', 'DrydockRepositoryOperationQuery' => 'DrydockQuery', 'DrydockRepositoryOperationSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockRepositoryOperationStatusController' => 'DrydockRepositoryOperationController', 'DrydockRepositoryOperationStatusView' => 'AphrontView', 'DrydockRepositoryOperationType' => 'Phobject', 'DrydockRepositoryOperationUpdateWorker' => 'DrydockWorker', 'DrydockRepositoryOperationViewController' => 'DrydockRepositoryOperationController', 'DrydockResource' => array( 'DrydockDAO', 'PhabricatorPolicyInterface', ), 'DrydockResourceActivationFailureLogType' => 'DrydockLogType', 'DrydockResourceActivationYieldLogType' => 'DrydockLogType', 'DrydockResourceAllocationFailureLogType' => 'DrydockLogType', 'DrydockResourceController' => 'DrydockController', 'DrydockResourceDatasource' => 'PhabricatorTypeaheadDatasource', 'DrydockResourceListController' => 'DrydockResourceController', 'DrydockResourceListView' => 'AphrontView', 'DrydockResourceLockException' => 'Exception', 'DrydockResourcePHIDType' => 'PhabricatorPHIDType', 'DrydockResourceQuery' => 'DrydockQuery', 'DrydockResourceReclaimLogType' => 'DrydockLogType', 'DrydockResourceReleaseController' => 'DrydockResourceController', 'DrydockResourceSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockResourceStatus' => 'PhabricatorObjectStatus', 'DrydockResourceUpdateWorker' => 'DrydockWorker', 'DrydockResourceViewController' => 'DrydockResourceController', 'DrydockSFTPFilesystemInterface' => 'DrydockFilesystemInterface', 'DrydockSSHCommandInterface' => 'DrydockCommandInterface', 'DrydockSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'DrydockSlotLock' => 'DrydockDAO', 'DrydockSlotLockException' => 'Exception', 'DrydockSlotLockFailureLogType' => 'DrydockLogType', 'DrydockTestRepositoryOperation' => 'DrydockRepositoryOperationType', 'DrydockTextLogType' => 'DrydockLogType', 'DrydockWebrootInterface' => 'DrydockInterface', 'DrydockWorker' => 'PhabricatorWorker', 'DrydockWorkingCopyBlueprintImplementation' => 'DrydockBlueprintImplementation', 'EdgeSearchConduitAPIMethod' => 'ConduitAPIMethod', 'FeedConduitAPIMethod' => 'ConduitAPIMethod', 'FeedPublishConduitAPIMethod' => 'FeedConduitAPIMethod', 'FeedPublisherHTTPWorker' => 'FeedPushWorker', 'FeedPublisherWorker' => 'FeedPushWorker', 'FeedPushWorker' => 'PhabricatorWorker', 'FeedQueryConduitAPIMethod' => 'FeedConduitAPIMethod', 'FeedStoryNotificationGarbageCollector' => 'PhabricatorGarbageCollector', 'FileAllocateConduitAPIMethod' => 'FileConduitAPIMethod', 'FileConduitAPIMethod' => 'ConduitAPIMethod', 'FileCreateMailReceiver' => 'PhabricatorMailReceiver', 'FileDeletionWorker' => 'PhabricatorWorker', 'FileDownloadConduitAPIMethod' => 'FileConduitAPIMethod', 'FileInfoConduitAPIMethod' => 'FileConduitAPIMethod', 'FileMailReceiver' => 'PhabricatorObjectMailReceiver', 'FileQueryChunksConduitAPIMethod' => 'FileConduitAPIMethod', 'FileReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'FileTypeIcon' => 'Phobject', 'FileUploadChunkConduitAPIMethod' => 'FileConduitAPIMethod', 'FileUploadConduitAPIMethod' => 'FileConduitAPIMethod', 'FileUploadHashConduitAPIMethod' => 'FileConduitAPIMethod', 'FilesDefaultViewCapability' => 'PhabricatorPolicyCapability', 'FlagConduitAPIMethod' => 'ConduitAPIMethod', 'FlagDeleteConduitAPIMethod' => 'FlagConduitAPIMethod', 'FlagEditConduitAPIMethod' => 'FlagConduitAPIMethod', 'FlagQueryConduitAPIMethod' => 'FlagConduitAPIMethod', 'FundBacker' => array( 'FundDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', ), 'FundBackerCart' => 'PhortuneCartImplementation', 'FundBackerEditor' => 'PhabricatorApplicationTransactionEditor', 'FundBackerListController' => 'FundController', 'FundBackerPHIDType' => 'PhabricatorPHIDType', 'FundBackerProduct' => 'PhortuneProductImplementation', 'FundBackerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'FundBackerRefundTransaction' => 'FundBackerTransactionType', 'FundBackerSearchEngine' => 'PhabricatorApplicationSearchEngine', 'FundBackerStatusTransaction' => 'FundBackerTransactionType', 'FundBackerTransaction' => 'PhabricatorModularTransaction', 'FundBackerTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'FundBackerTransactionType' => 'PhabricatorModularTransactionType', 'FundController' => 'PhabricatorController', 'FundCreateInitiativesCapability' => 'PhabricatorPolicyCapability', 'FundDAO' => 'PhabricatorLiskDAO', 'FundDefaultViewCapability' => 'PhabricatorPolicyCapability', 'FundInitiative' => array( 'FundDAO', 'PhabricatorPolicyInterface', 'PhabricatorProjectInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorSubscribableInterface', 'PhabricatorMentionableInterface', 'PhabricatorFlaggableInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorDestructibleInterface', 'PhabricatorFulltextInterface', 'PhabricatorFerretInterface', ), 'FundInitiativeBackController' => 'FundController', 'FundInitiativeBackerTransaction' => 'FundInitiativeTransactionType', 'FundInitiativeCloseController' => 'FundController', 'FundInitiativeDescriptionTransaction' => 'FundInitiativeTransactionType', 'FundInitiativeEditController' => 'FundController', 'FundInitiativeEditEngine' => 'PhabricatorEditEngine', 'FundInitiativeEditor' => 'PhabricatorApplicationTransactionEditor', 'FundInitiativeFerretEngine' => 'PhabricatorFerretEngine', 'FundInitiativeFulltextEngine' => 'PhabricatorFulltextEngine', 'FundInitiativeListController' => 'FundController', 'FundInitiativeMerchantTransaction' => 'FundInitiativeTransactionType', 'FundInitiativeNameTransaction' => 'FundInitiativeTransactionType', 'FundInitiativePHIDType' => 'PhabricatorPHIDType', 'FundInitiativeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'FundInitiativeRefundTransaction' => 'FundInitiativeTransactionType', 'FundInitiativeRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'FundInitiativeReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'FundInitiativeRisksTransaction' => 'FundInitiativeTransactionType', 'FundInitiativeSearchEngine' => 'PhabricatorApplicationSearchEngine', 'FundInitiativeStatusTransaction' => 'FundInitiativeTransactionType', 'FundInitiativeTransaction' => 'PhabricatorModularTransaction', 'FundInitiativeTransactionComment' => 'PhabricatorApplicationTransactionComment', 'FundInitiativeTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'FundInitiativeTransactionType' => 'PhabricatorModularTransactionType', 'FundInitiativeViewController' => 'FundController', 'FundSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'HarbormasterAbortOlderBuildsBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterArcLintBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterArcUnitBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterArtifact' => 'Phobject', 'HarbormasterAutotargetsTestCase' => 'PhabricatorTestCase', 'HarbormasterBuild' => array( 'HarbormasterDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorConduitResultInterface', 'PhabricatorDestructibleInterface', ), 'HarbormasterBuildAbortedException' => 'Exception', 'HarbormasterBuildActionController' => 'HarbormasterController', 'HarbormasterBuildArcanistAutoplan' => 'HarbormasterBuildAutoplan', 'HarbormasterBuildArtifact' => array( 'HarbormasterDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', ), 'HarbormasterBuildArtifactPHIDType' => 'PhabricatorPHIDType', 'HarbormasterBuildArtifactQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HarbormasterBuildAutoplan' => 'Phobject', 'HarbormasterBuildCommand' => 'HarbormasterDAO', 'HarbormasterBuildDependencyDatasource' => 'PhabricatorTypeaheadDatasource', 'HarbormasterBuildEngine' => 'Phobject', 'HarbormasterBuildFailureException' => 'Exception', 'HarbormasterBuildGraph' => 'AbstractDirectedGraph', 'HarbormasterBuildInitiatorDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'HarbormasterBuildLintMessage' => 'HarbormasterDAO', 'HarbormasterBuildListController' => 'HarbormasterController', 'HarbormasterBuildLog' => array( 'HarbormasterDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', 'PhabricatorConduitResultInterface', ), 'HarbormasterBuildLogChunk' => 'HarbormasterDAO', 'HarbormasterBuildLogChunkIterator' => 'PhutilBufferedIterator', 'HarbormasterBuildLogDownloadController' => 'HarbormasterController', 'HarbormasterBuildLogPHIDType' => 'PhabricatorPHIDType', 'HarbormasterBuildLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HarbormasterBuildLogRenderController' => 'HarbormasterController', 'HarbormasterBuildLogSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'HarbormasterBuildLogSearchEngine' => 'PhabricatorApplicationSearchEngine', 'HarbormasterBuildLogTestCase' => 'PhabricatorTestCase', 'HarbormasterBuildLogView' => 'AphrontView', 'HarbormasterBuildLogViewController' => 'HarbormasterController', 'HarbormasterBuildMessage' => array( 'HarbormasterDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', ), 'HarbormasterBuildMessageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HarbormasterBuildPHIDType' => 'PhabricatorPHIDType', 'HarbormasterBuildPlan' => array( 'HarbormasterDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorSubscribableInterface', 'PhabricatorNgramsInterface', 'PhabricatorConduitResultInterface', 'PhabricatorProjectInterface', ), 'HarbormasterBuildPlanDatasource' => 'PhabricatorTypeaheadDatasource', 'HarbormasterBuildPlanDefaultEditCapability' => 'PhabricatorPolicyCapability', 'HarbormasterBuildPlanDefaultViewCapability' => 'PhabricatorPolicyCapability', 'HarbormasterBuildPlanEditEngine' => 'PhabricatorEditEngine', 'HarbormasterBuildPlanEditor' => 'PhabricatorApplicationTransactionEditor', 'HarbormasterBuildPlanNameNgrams' => 'PhabricatorSearchNgrams', 'HarbormasterBuildPlanPHIDType' => 'PhabricatorPHIDType', 'HarbormasterBuildPlanQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HarbormasterBuildPlanSearchAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'HarbormasterBuildPlanSearchEngine' => 'PhabricatorApplicationSearchEngine', 'HarbormasterBuildPlanTransaction' => 'PhabricatorApplicationTransaction', 'HarbormasterBuildPlanTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'HarbormasterBuildQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HarbormasterBuildRequest' => 'Phobject', 'HarbormasterBuildSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'HarbormasterBuildSearchEngine' => 'PhabricatorApplicationSearchEngine', 'HarbormasterBuildStatus' => 'Phobject', 'HarbormasterBuildStatusDatasource' => 'PhabricatorTypeaheadDatasource', 'HarbormasterBuildStep' => array( 'HarbormasterDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorCustomFieldInterface', ), 'HarbormasterBuildStepCoreCustomField' => array( 'HarbormasterBuildStepCustomField', 'PhabricatorStandardCustomFieldInterface', ), 'HarbormasterBuildStepCustomField' => 'PhabricatorCustomField', 'HarbormasterBuildStepEditor' => 'PhabricatorApplicationTransactionEditor', 'HarbormasterBuildStepGroup' => 'Phobject', 'HarbormasterBuildStepImplementation' => 'Phobject', 'HarbormasterBuildStepImplementationTestCase' => 'PhabricatorTestCase', 'HarbormasterBuildStepPHIDType' => 'PhabricatorPHIDType', 'HarbormasterBuildStepQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HarbormasterBuildStepTransaction' => 'PhabricatorApplicationTransaction', 'HarbormasterBuildStepTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'HarbormasterBuildTarget' => array( 'HarbormasterDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', + 'PhabricatorConduitResultInterface', ), 'HarbormasterBuildTargetPHIDType' => 'PhabricatorPHIDType', 'HarbormasterBuildTargetQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'HarbormasterBuildTargetSearchEngine' => 'PhabricatorApplicationSearchEngine', 'HarbormasterBuildTransaction' => 'PhabricatorApplicationTransaction', 'HarbormasterBuildTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'HarbormasterBuildTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'HarbormasterBuildUnitMessage' => 'HarbormasterDAO', 'HarbormasterBuildViewController' => 'HarbormasterController', 'HarbormasterBuildWorker' => 'HarbormasterWorker', 'HarbormasterBuildable' => array( 'HarbormasterDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'HarbormasterBuildableInterface', + 'PhabricatorConduitResultInterface', 'PhabricatorDestructibleInterface', ), 'HarbormasterBuildableActionController' => 'HarbormasterController', 'HarbormasterBuildableEngine' => 'Phobject', 'HarbormasterBuildableListController' => 'HarbormasterController', 'HarbormasterBuildablePHIDType' => 'PhabricatorPHIDType', 'HarbormasterBuildableQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'HarbormasterBuildableSearchAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'HarbormasterBuildableSearchEngine' => 'PhabricatorApplicationSearchEngine', 'HarbormasterBuildableStatus' => 'Phobject', 'HarbormasterBuildableTransaction' => 'PhabricatorApplicationTransaction', 'HarbormasterBuildableTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'HarbormasterBuildableTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'HarbormasterBuildableViewController' => 'HarbormasterController', 'HarbormasterBuildkiteBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterBuildkiteHookController' => 'HarbormasterController', 'HarbormasterBuiltinBuildStepGroup' => 'HarbormasterBuildStepGroup', 'HarbormasterCircleCIBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterCircleCIHookController' => 'HarbormasterController', 'HarbormasterConduitAPIMethod' => 'ConduitAPIMethod', 'HarbormasterControlBuildStepGroup' => 'HarbormasterBuildStepGroup', 'HarbormasterController' => 'PhabricatorController', 'HarbormasterCreateArtifactConduitAPIMethod' => 'HarbormasterConduitAPIMethod', 'HarbormasterCreatePlansCapability' => 'PhabricatorPolicyCapability', 'HarbormasterDAO' => 'PhabricatorLiskDAO', 'HarbormasterDrydockBuildStepGroup' => 'HarbormasterBuildStepGroup', 'HarbormasterDrydockCommandBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterDrydockLeaseArtifact' => 'HarbormasterArtifact', 'HarbormasterExecFuture' => 'Future', 'HarbormasterExternalBuildStepGroup' => 'HarbormasterBuildStepGroup', 'HarbormasterFileArtifact' => 'HarbormasterArtifact', 'HarbormasterHTTPRequestBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterHostArtifact' => 'HarbormasterDrydockLeaseArtifact', 'HarbormasterLeaseWorkingCopyBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterLintMessagesController' => 'HarbormasterController', 'HarbormasterLintPropertyView' => 'AphrontView', 'HarbormasterLogWorker' => 'HarbormasterWorker', 'HarbormasterManagementArchiveLogsWorkflow' => 'HarbormasterManagementWorkflow', 'HarbormasterManagementBuildWorkflow' => 'HarbormasterManagementWorkflow', 'HarbormasterManagementPublishWorkflow' => 'HarbormasterManagementWorkflow', 'HarbormasterManagementRebuildLogWorkflow' => 'HarbormasterManagementWorkflow', 'HarbormasterManagementRestartWorkflow' => 'HarbormasterManagementWorkflow', 'HarbormasterManagementUpdateWorkflow' => 'HarbormasterManagementWorkflow', 'HarbormasterManagementWorkflow' => 'PhabricatorManagementWorkflow', 'HarbormasterManagementWriteLogWorkflow' => 'HarbormasterManagementWorkflow', 'HarbormasterMessageType' => 'Phobject', 'HarbormasterObject' => 'HarbormasterDAO', 'HarbormasterOtherBuildStepGroup' => 'HarbormasterBuildStepGroup', 'HarbormasterPlanController' => 'HarbormasterController', 'HarbormasterPlanDisableController' => 'HarbormasterPlanController', 'HarbormasterPlanEditController' => 'HarbormasterPlanController', 'HarbormasterPlanListController' => 'HarbormasterPlanController', 'HarbormasterPlanRunController' => 'HarbormasterPlanController', 'HarbormasterPlanViewController' => 'HarbormasterPlanController', 'HarbormasterPrototypeBuildStepGroup' => 'HarbormasterBuildStepGroup', 'HarbormasterPublishFragmentBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterQueryAutotargetsConduitAPIMethod' => 'HarbormasterConduitAPIMethod', 'HarbormasterQueryBuildablesConduitAPIMethod' => 'HarbormasterConduitAPIMethod', 'HarbormasterQueryBuildsConduitAPIMethod' => 'HarbormasterConduitAPIMethod', 'HarbormasterQueryBuildsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'HarbormasterRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'HarbormasterRunBuildPlansHeraldAction' => 'HeraldAction', 'HarbormasterSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'HarbormasterScratchTable' => 'HarbormasterDAO', 'HarbormasterSendMessageConduitAPIMethod' => 'HarbormasterConduitAPIMethod', 'HarbormasterSleepBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterStepAddController' => 'HarbormasterPlanController', 'HarbormasterStepDeleteController' => 'HarbormasterPlanController', 'HarbormasterStepEditController' => 'HarbormasterPlanController', 'HarbormasterStepViewController' => 'HarbormasterPlanController', 'HarbormasterTargetEngine' => 'Phobject', + 'HarbormasterTargetSearchAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'HarbormasterTargetWorker' => 'HarbormasterWorker', 'HarbormasterTestBuildStepGroup' => 'HarbormasterBuildStepGroup', 'HarbormasterThrowExceptionBuildStep' => 'HarbormasterBuildStepImplementation', 'HarbormasterUIEventListener' => 'PhabricatorEventListener', 'HarbormasterURIArtifact' => 'HarbormasterArtifact', 'HarbormasterUnitMessageListController' => 'HarbormasterController', 'HarbormasterUnitMessageViewController' => 'HarbormasterController', 'HarbormasterUnitPropertyView' => 'AphrontView', 'HarbormasterUnitStatus' => 'Phobject', 'HarbormasterUnitSummaryView' => 'AphrontView', 'HarbormasterUploadArtifactBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterWaitForPreviousBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterWorker' => 'PhabricatorWorker', 'HarbormasterWorkingCopyArtifact' => 'HarbormasterDrydockLeaseArtifact', 'HeraldActingUserField' => 'HeraldField', 'HeraldAction' => 'Phobject', 'HeraldActionGroup' => 'HeraldGroup', 'HeraldActionRecord' => 'HeraldDAO', 'HeraldAdapter' => 'Phobject', 'HeraldAdapterDatasource' => 'PhabricatorTypeaheadDatasource', 'HeraldAlwaysField' => 'HeraldField', 'HeraldAnotherRuleField' => 'HeraldField', 'HeraldApplicationActionGroup' => 'HeraldActionGroup', 'HeraldApplyTranscript' => 'Phobject', 'HeraldBasicFieldGroup' => 'HeraldFieldGroup', 'HeraldBuildableState' => 'HeraldState', 'HeraldCallWebhookAction' => 'HeraldAction', 'HeraldCommentAction' => 'HeraldAction', 'HeraldCommitAdapter' => array( 'HeraldAdapter', 'HarbormasterBuildableAdapterInterface', ), 'HeraldCondition' => 'HeraldDAO', 'HeraldConditionTranscript' => 'Phobject', 'HeraldContentSourceField' => 'HeraldField', 'HeraldController' => 'PhabricatorController', 'HeraldCoreStateReasons' => 'HeraldStateReasons', 'HeraldCreateWebhooksCapability' => 'PhabricatorPolicyCapability', 'HeraldDAO' => 'PhabricatorLiskDAO', 'HeraldDeprecatedFieldGroup' => 'HeraldFieldGroup', 'HeraldDifferentialAdapter' => 'HeraldAdapter', 'HeraldDifferentialDiffAdapter' => 'HeraldDifferentialAdapter', 'HeraldDifferentialRevisionAdapter' => array( 'HeraldDifferentialAdapter', 'HarbormasterBuildableAdapterInterface', ), 'HeraldDisableController' => 'HeraldController', 'HeraldDoNothingAction' => 'HeraldAction', 'HeraldEditFieldGroup' => 'HeraldFieldGroup', 'HeraldEffect' => 'Phobject', 'HeraldEmptyFieldValue' => 'HeraldFieldValue', 'HeraldEngine' => 'Phobject', 'HeraldExactProjectsField' => 'HeraldField', 'HeraldField' => 'Phobject', 'HeraldFieldGroup' => 'HeraldGroup', 'HeraldFieldTestCase' => 'PhutilTestCase', 'HeraldFieldValue' => 'Phobject', 'HeraldGroup' => 'Phobject', 'HeraldInvalidActionException' => 'Exception', 'HeraldInvalidConditionException' => 'Exception', 'HeraldMailableState' => 'HeraldState', 'HeraldManageGlobalRulesCapability' => 'PhabricatorPolicyCapability', + 'HeraldManagementWorkflow' => 'PhabricatorManagementWorkflow', 'HeraldManiphestTaskAdapter' => 'HeraldAdapter', 'HeraldNewController' => 'HeraldController', 'HeraldNewObjectField' => 'HeraldField', 'HeraldNotifyActionGroup' => 'HeraldActionGroup', 'HeraldObjectTranscript' => 'Phobject', 'HeraldPhameBlogAdapter' => 'HeraldAdapter', 'HeraldPhamePostAdapter' => 'HeraldAdapter', 'HeraldPholioMockAdapter' => 'HeraldAdapter', 'HeraldPonderQuestionAdapter' => 'HeraldAdapter', 'HeraldPreCommitAdapter' => 'HeraldAdapter', 'HeraldPreCommitContentAdapter' => 'HeraldPreCommitAdapter', 'HeraldPreCommitRefAdapter' => 'HeraldPreCommitAdapter', 'HeraldPreventActionGroup' => 'HeraldActionGroup', 'HeraldProjectsField' => 'HeraldField', 'HeraldRecursiveConditionsException' => 'Exception', 'HeraldRelatedFieldGroup' => 'HeraldFieldGroup', 'HeraldRemarkupFieldValue' => 'HeraldFieldValue', 'HeraldRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'HeraldRule' => array( 'HeraldDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorFlaggableInterface', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', 'PhabricatorSubscribableInterface', ), 'HeraldRuleAdapter' => 'HeraldAdapter', 'HeraldRuleAdapterField' => 'HeraldRuleField', 'HeraldRuleController' => 'HeraldController', 'HeraldRuleDatasource' => 'PhabricatorTypeaheadDatasource', 'HeraldRuleEditor' => 'PhabricatorApplicationTransactionEditor', 'HeraldRuleField' => 'HeraldField', 'HeraldRuleFieldGroup' => 'HeraldFieldGroup', 'HeraldRuleListController' => 'HeraldController', 'HeraldRulePHIDType' => 'PhabricatorPHIDType', 'HeraldRuleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HeraldRuleReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'HeraldRuleSearchEngine' => 'PhabricatorApplicationSearchEngine', 'HeraldRuleSerializer' => 'Phobject', 'HeraldRuleTestCase' => 'PhabricatorTestCase', 'HeraldRuleTransaction' => 'PhabricatorApplicationTransaction', 'HeraldRuleTransactionComment' => 'PhabricatorApplicationTransactionComment', 'HeraldRuleTranscript' => 'Phobject', 'HeraldRuleTypeConfig' => 'Phobject', 'HeraldRuleTypeDatasource' => 'PhabricatorTypeaheadDatasource', 'HeraldRuleTypeField' => 'HeraldRuleField', 'HeraldRuleViewController' => 'HeraldController', 'HeraldSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'HeraldSelectFieldValue' => 'HeraldFieldValue', 'HeraldSpaceField' => 'HeraldField', 'HeraldState' => 'Phobject', 'HeraldStateReasons' => 'Phobject', 'HeraldSubscribersField' => 'HeraldField', 'HeraldSupportActionGroup' => 'HeraldActionGroup', 'HeraldSupportFieldGroup' => 'HeraldFieldGroup', 'HeraldTestConsoleController' => 'HeraldController', + 'HeraldTestManagementWorkflow' => 'HeraldManagementWorkflow', 'HeraldTextFieldValue' => 'HeraldFieldValue', 'HeraldTokenizerFieldValue' => 'HeraldFieldValue', 'HeraldTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'HeraldTranscript' => array( 'HeraldDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', ), 'HeraldTranscriptController' => 'HeraldController', 'HeraldTranscriptDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', 'HeraldTranscriptGarbageCollector' => 'PhabricatorGarbageCollector', 'HeraldTranscriptListController' => 'HeraldController', 'HeraldTranscriptPHIDType' => 'PhabricatorPHIDType', 'HeraldTranscriptQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HeraldTranscriptSearchEngine' => 'PhabricatorApplicationSearchEngine', 'HeraldTranscriptTestCase' => 'PhabricatorTestCase', 'HeraldUtilityActionGroup' => 'HeraldActionGroup', 'HeraldWebhook' => array( 'HeraldDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorDestructibleInterface', 'PhabricatorProjectInterface', ), 'HeraldWebhookCallManagementWorkflow' => 'HeraldWebhookManagementWorkflow', 'HeraldWebhookController' => 'HeraldController', 'HeraldWebhookDatasource' => 'PhabricatorTypeaheadDatasource', 'HeraldWebhookEditController' => 'HeraldWebhookController', 'HeraldWebhookEditEngine' => 'PhabricatorEditEngine', 'HeraldWebhookEditor' => 'PhabricatorApplicationTransactionEditor', 'HeraldWebhookKeyController' => 'HeraldWebhookController', 'HeraldWebhookListController' => 'HeraldWebhookController', 'HeraldWebhookManagementWorkflow' => 'PhabricatorManagementWorkflow', 'HeraldWebhookNameTransaction' => 'HeraldWebhookTransactionType', 'HeraldWebhookPHIDType' => 'PhabricatorPHIDType', 'HeraldWebhookQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HeraldWebhookRequest' => array( 'HeraldDAO', 'PhabricatorPolicyInterface', 'PhabricatorExtendedPolicyInterface', ), 'HeraldWebhookRequestGarbageCollector' => 'PhabricatorGarbageCollector', 'HeraldWebhookRequestListView' => 'AphrontView', 'HeraldWebhookRequestPHIDType' => 'PhabricatorPHIDType', 'HeraldWebhookRequestQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HeraldWebhookSearchEngine' => 'PhabricatorApplicationSearchEngine', 'HeraldWebhookStatusTransaction' => 'HeraldWebhookTransactionType', 'HeraldWebhookTestController' => 'HeraldWebhookController', 'HeraldWebhookTransaction' => 'PhabricatorModularTransaction', 'HeraldWebhookTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'HeraldWebhookTransactionType' => 'PhabricatorModularTransactionType', 'HeraldWebhookURITransaction' => 'HeraldWebhookTransactionType', 'HeraldWebhookViewController' => 'HeraldWebhookController', 'HeraldWebhookWorker' => 'PhabricatorWorker', 'Javelin' => 'Phobject', 'LegalpadController' => 'PhabricatorController', 'LegalpadCreateDocumentsCapability' => 'PhabricatorPolicyCapability', 'LegalpadDAO' => 'PhabricatorLiskDAO', 'LegalpadDefaultEditCapability' => 'PhabricatorPolicyCapability', 'LegalpadDefaultViewCapability' => 'PhabricatorPolicyCapability', 'LegalpadDocument' => array( 'LegalpadDAO', 'PhabricatorPolicyInterface', 'PhabricatorSubscribableInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorDestructibleInterface', ), 'LegalpadDocumentBody' => array( 'LegalpadDAO', 'PhabricatorMarkupInterface', ), 'LegalpadDocumentDatasource' => 'PhabricatorTypeaheadDatasource', 'LegalpadDocumentDoneController' => 'LegalpadController', 'LegalpadDocumentEditController' => 'LegalpadController', 'LegalpadDocumentEditEngine' => 'PhabricatorEditEngine', 'LegalpadDocumentEditor' => 'PhabricatorApplicationTransactionEditor', 'LegalpadDocumentListController' => 'LegalpadController', 'LegalpadDocumentManageController' => 'LegalpadController', 'LegalpadDocumentPreambleTransaction' => 'LegalpadDocumentTransactionType', 'LegalpadDocumentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'LegalpadDocumentRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'LegalpadDocumentRequireSignatureTransaction' => 'LegalpadDocumentTransactionType', 'LegalpadDocumentSearchEngine' => 'PhabricatorApplicationSearchEngine', 'LegalpadDocumentSignController' => 'LegalpadController', 'LegalpadDocumentSignature' => array( 'LegalpadDAO', 'PhabricatorPolicyInterface', ), 'LegalpadDocumentSignatureAddController' => 'LegalpadController', 'LegalpadDocumentSignatureListController' => 'LegalpadController', 'LegalpadDocumentSignatureQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'LegalpadDocumentSignatureSearchEngine' => 'PhabricatorApplicationSearchEngine', 'LegalpadDocumentSignatureTypeTransaction' => 'LegalpadDocumentTransactionType', 'LegalpadDocumentSignatureVerificationController' => 'LegalpadController', 'LegalpadDocumentSignatureViewController' => 'LegalpadController', 'LegalpadDocumentTextTransaction' => 'LegalpadDocumentTransactionType', 'LegalpadDocumentTitleTransaction' => 'LegalpadDocumentTransactionType', 'LegalpadDocumentTransactionType' => 'PhabricatorModularTransactionType', 'LegalpadMailReceiver' => 'PhabricatorObjectMailReceiver', 'LegalpadObjectNeedsSignatureEdgeType' => 'PhabricatorEdgeType', 'LegalpadReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'LegalpadRequireSignatureHeraldAction' => 'HeraldAction', 'LegalpadSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'LegalpadSignatureNeededByObjectEdgeType' => 'PhabricatorEdgeType', 'LegalpadTransaction' => 'PhabricatorModularTransaction', 'LegalpadTransactionComment' => 'PhabricatorApplicationTransactionComment', 'LegalpadTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'LegalpadTransactionView' => 'PhabricatorApplicationTransactionView', 'LiskChunkTestCase' => 'PhabricatorTestCase', 'LiskDAO' => array( 'Phobject', 'AphrontDatabaseTableRefInterface', ), 'LiskDAOSet' => 'Phobject', 'LiskDAOTestCase' => 'PhabricatorTestCase', 'LiskEphemeralObjectException' => 'Exception', 'LiskFixtureTestCase' => 'PhabricatorTestCase', 'LiskIsolationTestCase' => 'PhabricatorTestCase', 'LiskIsolationTestDAO' => 'LiskDAO', 'LiskIsolationTestDAOException' => 'Exception', 'LiskMigrationIterator' => 'PhutilBufferedIterator', 'LiskRawMigrationIterator' => 'PhutilBufferedIterator', 'MacroConduitAPIMethod' => 'ConduitAPIMethod', 'MacroCreateMemeConduitAPIMethod' => 'MacroConduitAPIMethod', 'MacroEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'MacroEmojiExample' => 'PhabricatorUIExample', 'MacroQueryConduitAPIMethod' => 'MacroConduitAPIMethod', 'ManiphestAssignEmailCommand' => 'ManiphestEmailCommand', 'ManiphestAssigneeDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'ManiphestBulkEditCapability' => 'PhabricatorPolicyCapability', 'ManiphestBulkEditController' => 'ManiphestController', 'ManiphestClaimEmailCommand' => 'ManiphestEmailCommand', 'ManiphestCloseEmailCommand' => 'ManiphestEmailCommand', 'ManiphestConduitAPIMethod' => 'ConduitAPIMethod', 'ManiphestConfiguredCustomField' => array( 'ManiphestCustomField', 'PhabricatorStandardCustomFieldInterface', ), 'ManiphestConstants' => 'Phobject', 'ManiphestController' => 'PhabricatorController', 'ManiphestCreateMailReceiver' => 'PhabricatorMailReceiver', 'ManiphestCreateTaskConduitAPIMethod' => 'ManiphestConduitAPIMethod', 'ManiphestCustomField' => 'PhabricatorCustomField', 'ManiphestCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage', 'ManiphestCustomFieldStatusParser' => 'PhabricatorCustomFieldMonogramParser', 'ManiphestCustomFieldStatusParserTestCase' => 'PhabricatorTestCase', 'ManiphestCustomFieldStorage' => 'PhabricatorCustomFieldStorage', 'ManiphestCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage', 'ManiphestDAO' => 'PhabricatorLiskDAO', 'ManiphestDefaultEditCapability' => 'PhabricatorPolicyCapability', 'ManiphestDefaultViewCapability' => 'PhabricatorPolicyCapability', 'ManiphestEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'ManiphestEditEngine' => 'PhabricatorEditEngine', 'ManiphestEmailCommand' => 'MetaMTAEmailTransactionCommand', 'ManiphestGetTaskTransactionsConduitAPIMethod' => 'ManiphestConduitAPIMethod', 'ManiphestHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension', 'ManiphestInfoConduitAPIMethod' => 'ManiphestConduitAPIMethod', 'ManiphestMailEngineExtension' => 'PhabricatorMailEngineExtension', 'ManiphestNameIndex' => 'ManiphestDAO', 'ManiphestPointsConfigType' => 'PhabricatorJSONConfigType', 'ManiphestPrioritiesConfigType' => 'PhabricatorJSONConfigType', 'ManiphestPriorityEmailCommand' => 'ManiphestEmailCommand', 'ManiphestPrioritySearchConduitAPIMethod' => 'ManiphestConduitAPIMethod', 'ManiphestProjectNameFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension', 'ManiphestQueryConduitAPIMethod' => 'ManiphestConduitAPIMethod', 'ManiphestQueryStatusesConduitAPIMethod' => 'ManiphestConduitAPIMethod', 'ManiphestRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'ManiphestReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'ManiphestReportController' => 'ManiphestController', 'ManiphestSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'ManiphestSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'ManiphestStatusEmailCommand' => 'ManiphestEmailCommand', 'ManiphestStatusSearchConduitAPIMethod' => 'ManiphestConduitAPIMethod', 'ManiphestStatusesConfigType' => 'PhabricatorJSONConfigType', 'ManiphestSubpriorityController' => 'ManiphestController', 'ManiphestSubtypesConfigType' => 'PhabricatorJSONConfigType', 'ManiphestTask' => array( 'ManiphestDAO', 'PhabricatorSubscribableInterface', 'PhabricatorMarkupInterface', 'PhabricatorPolicyInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorFlaggableInterface', 'PhabricatorMentionableInterface', 'PhrequentTrackableInterface', 'PhabricatorCustomFieldInterface', 'PhabricatorDestructibleInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorProjectInterface', 'PhabricatorSpacesInterface', 'PhabricatorConduitResultInterface', 'PhabricatorFulltextInterface', 'PhabricatorFerretInterface', 'DoorkeeperBridgedObjectInterface', 'PhabricatorEditEngineSubtypeInterface', 'PhabricatorEditEngineLockableInterface', ), 'ManiphestTaskAssignHeraldAction' => 'HeraldAction', 'ManiphestTaskAssignOtherHeraldAction' => 'ManiphestTaskAssignHeraldAction', 'ManiphestTaskAssignSelfHeraldAction' => 'ManiphestTaskAssignHeraldAction', 'ManiphestTaskAssigneeHeraldField' => 'ManiphestTaskHeraldField', 'ManiphestTaskAttachTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskAuthorHeraldField' => 'ManiphestTaskHeraldField', 'ManiphestTaskAuthorPolicyRule' => 'PhabricatorPolicyRule', 'ManiphestTaskBulkEngine' => 'PhabricatorBulkEngine', 'ManiphestTaskCloseAsDuplicateRelationship' => 'ManiphestTaskRelationship', 'ManiphestTaskClosedStatusDatasource' => 'PhabricatorTypeaheadDatasource', 'ManiphestTaskCoverImageTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskDependedOnByTaskEdgeType' => 'PhabricatorEdgeType', 'ManiphestTaskDependsOnTaskEdgeType' => 'PhabricatorEdgeType', 'ManiphestTaskDescriptionHeraldField' => 'ManiphestTaskHeraldField', 'ManiphestTaskDescriptionTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskDetailController' => 'ManiphestController', 'ManiphestTaskEdgeTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskEditController' => 'ManiphestController', 'ManiphestTaskEditEngineLock' => 'PhabricatorEditEngineLock', 'ManiphestTaskFerretEngine' => 'PhabricatorFerretEngine', 'ManiphestTaskFulltextEngine' => 'PhabricatorFulltextEngine', 'ManiphestTaskGraph' => 'PhabricatorObjectGraph', 'ManiphestTaskHasCommitEdgeType' => 'PhabricatorEdgeType', 'ManiphestTaskHasCommitRelationship' => 'ManiphestTaskRelationship', 'ManiphestTaskHasDuplicateTaskEdgeType' => 'PhabricatorEdgeType', 'ManiphestTaskHasMockEdgeType' => 'PhabricatorEdgeType', 'ManiphestTaskHasMockRelationship' => 'ManiphestTaskRelationship', 'ManiphestTaskHasParentRelationship' => 'ManiphestTaskRelationship', 'ManiphestTaskHasRevisionEdgeType' => 'PhabricatorEdgeType', 'ManiphestTaskHasRevisionRelationship' => 'ManiphestTaskRelationship', 'ManiphestTaskHasSubtaskRelationship' => 'ManiphestTaskRelationship', 'ManiphestTaskHeraldField' => 'HeraldField', 'ManiphestTaskHeraldFieldGroup' => 'HeraldFieldGroup', 'ManiphestTaskIsDuplicateOfTaskEdgeType' => 'PhabricatorEdgeType', 'ManiphestTaskListController' => 'ManiphestController', 'ManiphestTaskListHTTPParameterType' => 'AphrontListHTTPParameterType', 'ManiphestTaskListView' => 'ManiphestView', 'ManiphestTaskMailReceiver' => 'PhabricatorObjectMailReceiver', 'ManiphestTaskMergeInRelationship' => 'ManiphestTaskRelationship', 'ManiphestTaskMergedFromTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskMergedIntoTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskOpenStatusDatasource' => 'PhabricatorTypeaheadDatasource', 'ManiphestTaskOwnerTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskPHIDResolver' => 'PhabricatorPHIDResolver', 'ManiphestTaskPHIDType' => 'PhabricatorPHIDType', 'ManiphestTaskParentTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskPoints' => 'Phobject', 'ManiphestTaskPointsTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskPriority' => 'ManiphestConstants', 'ManiphestTaskPriorityDatasource' => 'PhabricatorTypeaheadDatasource', 'ManiphestTaskPriorityHeraldAction' => 'HeraldAction', 'ManiphestTaskPriorityHeraldField' => 'ManiphestTaskHeraldField', 'ManiphestTaskPriorityTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'ManiphestTaskRelationship' => 'PhabricatorObjectRelationship', 'ManiphestTaskRelationshipSource' => 'PhabricatorObjectRelationshipSource', 'ManiphestTaskResultListView' => 'ManiphestView', 'ManiphestTaskSearchEngine' => 'PhabricatorApplicationSearchEngine', 'ManiphestTaskStatus' => 'ManiphestConstants', 'ManiphestTaskStatusDatasource' => 'PhabricatorTypeaheadDatasource', 'ManiphestTaskStatusFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'ManiphestTaskStatusHeraldAction' => 'HeraldAction', 'ManiphestTaskStatusHeraldField' => 'ManiphestTaskHeraldField', 'ManiphestTaskStatusTestCase' => 'PhabricatorTestCase', 'ManiphestTaskStatusTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskSubpriorityTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskSubtypeDatasource' => 'PhabricatorTypeaheadDatasource', 'ManiphestTaskTestCase' => 'PhabricatorTestCase', 'ManiphestTaskTitleHeraldField' => 'ManiphestTaskHeraldField', 'ManiphestTaskTitleTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskTransactionType' => 'PhabricatorModularTransactionType', 'ManiphestTaskUnblockTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTransaction' => 'PhabricatorModularTransaction', 'ManiphestTransactionComment' => 'PhabricatorApplicationTransactionComment', 'ManiphestTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'ManiphestTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'ManiphestUpdateConduitAPIMethod' => 'ManiphestConduitAPIMethod', 'ManiphestView' => 'AphrontView', 'MetaMTAEmailTransactionCommand' => 'Phobject', 'MetaMTAEmailTransactionCommandTestCase' => 'PhabricatorTestCase', 'MetaMTAMailReceivedGarbageCollector' => 'PhabricatorGarbageCollector', 'MetaMTAMailSentGarbageCollector' => 'PhabricatorGarbageCollector', 'MetaMTAReceivedMailStatus' => 'Phobject', 'MultimeterContext' => 'MultimeterDimension', 'MultimeterControl' => 'Phobject', 'MultimeterController' => 'PhabricatorController', 'MultimeterDAO' => 'PhabricatorLiskDAO', 'MultimeterDimension' => 'MultimeterDAO', 'MultimeterEvent' => 'MultimeterDAO', 'MultimeterEventGarbageCollector' => 'PhabricatorGarbageCollector', 'MultimeterHost' => 'MultimeterDimension', 'MultimeterLabel' => 'MultimeterDimension', 'MultimeterSampleController' => 'MultimeterController', 'MultimeterViewer' => 'MultimeterDimension', 'NuanceCommandImplementation' => 'Phobject', 'NuanceConduitAPIMethod' => 'ConduitAPIMethod', 'NuanceConsoleController' => 'NuanceController', 'NuanceContentSource' => 'PhabricatorContentSource', 'NuanceController' => 'PhabricatorController', 'NuanceDAO' => 'PhabricatorLiskDAO', 'NuanceFormItemType' => 'NuanceItemType', 'NuanceGitHubEventItemType' => 'NuanceItemType', 'NuanceGitHubImportCursor' => 'NuanceImportCursor', 'NuanceGitHubIssuesImportCursor' => 'NuanceGitHubImportCursor', 'NuanceGitHubRawEvent' => 'Phobject', 'NuanceGitHubRawEventTestCase' => 'PhabricatorTestCase', 'NuanceGitHubRepositoryImportCursor' => 'NuanceGitHubImportCursor', 'NuanceGitHubRepositorySourceDefinition' => 'NuanceSourceDefinition', 'NuanceImportCursor' => 'Phobject', 'NuanceImportCursorData' => array( 'NuanceDAO', 'PhabricatorPolicyInterface', ), 'NuanceImportCursorDataQuery' => 'NuanceQuery', 'NuanceImportCursorPHIDType' => 'PhabricatorPHIDType', 'NuanceItem' => array( 'NuanceDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', ), 'NuanceItemActionController' => 'NuanceController', 'NuanceItemCommand' => array( 'NuanceDAO', 'PhabricatorPolicyInterface', ), 'NuanceItemCommandQuery' => 'NuanceQuery', 'NuanceItemCommandSpec' => 'Phobject', 'NuanceItemCommandTransaction' => 'NuanceItemTransactionType', 'NuanceItemController' => 'NuanceController', 'NuanceItemEditor' => 'PhabricatorApplicationTransactionEditor', 'NuanceItemListController' => 'NuanceItemController', 'NuanceItemManageController' => 'NuanceController', 'NuanceItemOwnerTransaction' => 'NuanceItemTransactionType', 'NuanceItemPHIDType' => 'PhabricatorPHIDType', 'NuanceItemPropertyTransaction' => 'NuanceItemTransactionType', 'NuanceItemQuery' => 'NuanceQuery', 'NuanceItemQueueTransaction' => 'NuanceItemTransactionType', 'NuanceItemRequestorTransaction' => 'NuanceItemTransactionType', 'NuanceItemSearchEngine' => 'PhabricatorApplicationSearchEngine', 'NuanceItemSourceTransaction' => 'NuanceItemTransactionType', 'NuanceItemStatusTransaction' => 'NuanceItemTransactionType', 'NuanceItemTransaction' => 'NuanceTransaction', 'NuanceItemTransactionComment' => 'PhabricatorApplicationTransactionComment', 'NuanceItemTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'NuanceItemTransactionType' => 'PhabricatorModularTransactionType', 'NuanceItemType' => 'Phobject', 'NuanceItemUpdateWorker' => 'NuanceWorker', 'NuanceItemViewController' => 'NuanceController', 'NuanceManagementImportWorkflow' => 'NuanceManagementWorkflow', 'NuanceManagementUpdateWorkflow' => 'NuanceManagementWorkflow', 'NuanceManagementWorkflow' => 'PhabricatorManagementWorkflow', 'NuancePhabricatorFormSourceDefinition' => 'NuanceSourceDefinition', 'NuanceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'NuanceQueue' => array( 'NuanceDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', ), 'NuanceQueueController' => 'NuanceController', 'NuanceQueueDatasource' => 'PhabricatorTypeaheadDatasource', 'NuanceQueueEditController' => 'NuanceQueueController', 'NuanceQueueEditEngine' => 'PhabricatorEditEngine', 'NuanceQueueEditor' => 'PhabricatorApplicationTransactionEditor', 'NuanceQueueListController' => 'NuanceQueueController', 'NuanceQueueNameTransaction' => 'NuanceQueueTransactionType', 'NuanceQueuePHIDType' => 'PhabricatorPHIDType', 'NuanceQueueQuery' => 'NuanceQuery', 'NuanceQueueSearchEngine' => 'PhabricatorApplicationSearchEngine', 'NuanceQueueTransaction' => 'NuanceTransaction', 'NuanceQueueTransactionComment' => 'PhabricatorApplicationTransactionComment', 'NuanceQueueTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'NuanceQueueTransactionType' => 'PhabricatorModularTransactionType', 'NuanceQueueViewController' => 'NuanceQueueController', 'NuanceQueueWorkController' => 'NuanceQueueController', 'NuanceSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'NuanceSource' => array( 'NuanceDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorNgramsInterface', ), 'NuanceSourceActionController' => 'NuanceController', 'NuanceSourceController' => 'NuanceController', 'NuanceSourceDefaultEditCapability' => 'PhabricatorPolicyCapability', 'NuanceSourceDefaultQueueTransaction' => 'NuanceSourceTransactionType', 'NuanceSourceDefaultViewCapability' => 'PhabricatorPolicyCapability', 'NuanceSourceDefinition' => 'Phobject', 'NuanceSourceDefinitionTestCase' => 'PhabricatorTestCase', 'NuanceSourceEditController' => 'NuanceSourceController', 'NuanceSourceEditEngine' => 'PhabricatorEditEngine', 'NuanceSourceEditor' => 'PhabricatorApplicationTransactionEditor', 'NuanceSourceListController' => 'NuanceSourceController', 'NuanceSourceManageCapability' => 'PhabricatorPolicyCapability', 'NuanceSourceNameNgrams' => 'PhabricatorSearchNgrams', 'NuanceSourceNameTransaction' => 'NuanceSourceTransactionType', 'NuanceSourcePHIDType' => 'PhabricatorPHIDType', 'NuanceSourceQuery' => 'NuanceQuery', 'NuanceSourceSearchEngine' => 'PhabricatorApplicationSearchEngine', 'NuanceSourceTransaction' => 'NuanceTransaction', 'NuanceSourceTransactionComment' => 'PhabricatorApplicationTransactionComment', 'NuanceSourceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'NuanceSourceTransactionType' => 'PhabricatorModularTransactionType', 'NuanceSourceViewController' => 'NuanceSourceController', 'NuanceTransaction' => 'PhabricatorModularTransaction', 'NuanceTrashCommand' => 'NuanceCommandImplementation', 'NuanceWorker' => 'PhabricatorWorker', 'OwnersConduitAPIMethod' => 'ConduitAPIMethod', 'OwnersEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'OwnersPackageReplyHandler' => 'PhabricatorMailReplyHandler', 'OwnersQueryConduitAPIMethod' => 'OwnersConduitAPIMethod', 'OwnersSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'PHIDConduitAPIMethod' => 'ConduitAPIMethod', 'PHIDInfoConduitAPIMethod' => 'PHIDConduitAPIMethod', 'PHIDLookupConduitAPIMethod' => 'PHIDConduitAPIMethod', 'PHIDQueryConduitAPIMethod' => 'PHIDConduitAPIMethod', 'PHUI' => 'Phobject', 'PHUIActionPanelExample' => 'PhabricatorUIExample', 'PHUIActionPanelView' => 'AphrontTagView', 'PHUIApplicationMenuView' => 'Phobject', 'PHUIBadgeBoxView' => 'AphrontTagView', 'PHUIBadgeExample' => 'PhabricatorUIExample', 'PHUIBadgeMiniView' => 'AphrontTagView', 'PHUIBadgeView' => 'AphrontTagView', 'PHUIBigInfoExample' => 'PhabricatorUIExample', 'PHUIBigInfoView' => 'AphrontTagView', 'PHUIBoxExample' => 'PhabricatorUIExample', 'PHUIBoxView' => 'AphrontTagView', 'PHUIButtonBarExample' => 'PhabricatorUIExample', 'PHUIButtonBarView' => 'AphrontTagView', 'PHUIButtonExample' => 'PhabricatorUIExample', 'PHUIButtonView' => 'AphrontTagView', 'PHUICMSView' => 'AphrontTagView', 'PHUICalendarDayView' => 'AphrontView', 'PHUICalendarListView' => 'AphrontTagView', 'PHUICalendarMonthView' => 'AphrontView', 'PHUICalendarWeekView' => 'AphrontView', 'PHUICalendarWidgetView' => 'AphrontTagView', 'PHUIColorPalletteExample' => 'PhabricatorUIExample', 'PHUICrumbView' => 'AphrontView', 'PHUICrumbsView' => 'AphrontView', 'PHUICurtainExtension' => 'Phobject', 'PHUICurtainPanelView' => 'AphrontTagView', 'PHUICurtainView' => 'AphrontTagView', 'PHUIDiffGraphView' => 'Phobject', 'PHUIDiffGraphViewTestCase' => 'PhabricatorTestCase', 'PHUIDiffInlineCommentDetailView' => 'PHUIDiffInlineCommentView', 'PHUIDiffInlineCommentEditView' => 'PHUIDiffInlineCommentView', 'PHUIDiffInlineCommentPreviewListView' => 'AphrontView', 'PHUIDiffInlineCommentRowScaffold' => 'AphrontView', 'PHUIDiffInlineCommentTableScaffold' => 'AphrontView', 'PHUIDiffInlineCommentUndoView' => 'PHUIDiffInlineCommentView', 'PHUIDiffInlineCommentView' => 'AphrontView', 'PHUIDiffInlineThreader' => 'Phobject', 'PHUIDiffOneUpInlineCommentRowScaffold' => 'PHUIDiffInlineCommentRowScaffold', 'PHUIDiffRevealIconView' => 'AphrontView', 'PHUIDiffTableOfContentsItemView' => 'AphrontView', 'PHUIDiffTableOfContentsListView' => 'AphrontView', 'PHUIDiffTwoUpInlineCommentRowScaffold' => 'PHUIDiffInlineCommentRowScaffold', 'PHUIDocumentSummaryView' => 'AphrontTagView', 'PHUIDocumentView' => 'AphrontTagView', 'PHUIFeedStoryExample' => 'PhabricatorUIExample', 'PHUIFeedStoryView' => 'AphrontView', 'PHUIFormDividerControl' => 'AphrontFormControl', 'PHUIFormFileControl' => 'AphrontFormControl', 'PHUIFormFreeformDateControl' => 'AphrontFormControl', 'PHUIFormIconSetControl' => 'AphrontFormControl', 'PHUIFormInsetView' => 'AphrontView', 'PHUIFormLayoutView' => 'AphrontView', 'PHUIFormNumberControl' => 'AphrontFormControl', 'PHUIHandleListView' => 'AphrontTagView', 'PHUIHandleTagListView' => 'AphrontTagView', 'PHUIHandleView' => 'AphrontView', 'PHUIHeadThingView' => 'AphrontTagView', 'PHUIHeaderView' => 'AphrontTagView', 'PHUIHomeView' => 'AphrontTagView', 'PHUIHovercardUIExample' => 'PhabricatorUIExample', 'PHUIHovercardView' => 'AphrontTagView', 'PHUIIconCircleView' => 'AphrontTagView', 'PHUIIconExample' => 'PhabricatorUIExample', 'PHUIIconView' => 'AphrontTagView', 'PHUIImageMaskExample' => 'PhabricatorUIExample', 'PHUIImageMaskView' => 'AphrontTagView', 'PHUIInfoExample' => 'PhabricatorUIExample', 'PHUIInfoView' => 'AphrontTagView', 'PHUIInvisibleCharacterTestCase' => 'PhabricatorTestCase', 'PHUIInvisibleCharacterView' => 'AphrontView', 'PHUILeftRightExample' => 'PhabricatorUIExample', 'PHUILeftRightView' => 'AphrontTagView', 'PHUIListExample' => 'PhabricatorUIExample', 'PHUIListItemView' => 'AphrontTagView', 'PHUIListView' => 'AphrontTagView', 'PHUIListViewTestCase' => 'PhabricatorTestCase', 'PHUIObjectBoxView' => 'AphrontTagView', 'PHUIObjectItemListExample' => 'PhabricatorUIExample', 'PHUIObjectItemListView' => 'AphrontTagView', 'PHUIObjectItemView' => 'AphrontTagView', 'PHUIPagerView' => 'AphrontView', 'PHUIPinboardItemView' => 'AphrontView', 'PHUIPinboardView' => 'AphrontView', 'PHUIPolicySectionView' => 'AphrontTagView', 'PHUIPropertyGroupView' => 'AphrontTagView', 'PHUIPropertyListExample' => 'PhabricatorUIExample', 'PHUIPropertyListView' => 'AphrontView', 'PHUIRemarkupImageView' => 'AphrontView', 'PHUIRemarkupPreviewPanel' => 'AphrontTagView', 'PHUIRemarkupView' => 'AphrontView', 'PHUISegmentBarSegmentView' => 'AphrontTagView', 'PHUISegmentBarView' => 'AphrontTagView', 'PHUISpacesNamespaceContextView' => 'AphrontView', 'PHUIStatusItemView' => 'AphrontTagView', 'PHUIStatusListView' => 'AphrontTagView', 'PHUITabGroupView' => 'AphrontTagView', 'PHUITabView' => 'AphrontTagView', 'PHUITagExample' => 'PhabricatorUIExample', 'PHUITagView' => 'AphrontTagView', 'PHUITimelineEventView' => 'AphrontView', 'PHUITimelineExample' => 'PhabricatorUIExample', 'PHUITimelineView' => 'AphrontView', 'PHUITwoColumnView' => 'AphrontTagView', 'PHUITypeaheadExample' => 'PhabricatorUIExample', 'PHUIUserAvailabilityView' => 'AphrontTagView', 'PHUIWorkboardView' => 'AphrontTagView', 'PHUIWorkpanelView' => 'AphrontTagView', 'PHUIXComponentsExample' => 'PhabricatorUIExample', 'PassphraseAbstractKey' => 'Phobject', 'PassphraseConduitAPIMethod' => 'ConduitAPIMethod', 'PassphraseController' => 'PhabricatorController', 'PassphraseCredential' => array( 'PassphraseDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorFlaggableInterface', 'PhabricatorSubscribableInterface', 'PhabricatorDestructibleInterface', 'PhabricatorSpacesInterface', 'PhabricatorFulltextInterface', 'PhabricatorFerretInterface', ), 'PassphraseCredentialAuthorPolicyRule' => 'PhabricatorPolicyRule', 'PassphraseCredentialConduitController' => 'PassphraseController', 'PassphraseCredentialConduitTransaction' => 'PassphraseCredentialTransactionType', 'PassphraseCredentialControl' => 'AphrontFormControl', 'PassphraseCredentialCreateController' => 'PassphraseController', 'PassphraseCredentialDescriptionTransaction' => 'PassphraseCredentialTransactionType', 'PassphraseCredentialDestroyController' => 'PassphraseController', 'PassphraseCredentialDestroyTransaction' => 'PassphraseCredentialTransactionType', 'PassphraseCredentialEditController' => 'PassphraseController', 'PassphraseCredentialFerretEngine' => 'PhabricatorFerretEngine', 'PassphraseCredentialFulltextEngine' => 'PhabricatorFulltextEngine', 'PassphraseCredentialListController' => 'PassphraseController', 'PassphraseCredentialLockController' => 'PassphraseController', 'PassphraseCredentialLockTransaction' => 'PassphraseCredentialTransactionType', 'PassphraseCredentialLookedAtTransaction' => 'PassphraseCredentialTransactionType', 'PassphraseCredentialNameTransaction' => 'PassphraseCredentialTransactionType', 'PassphraseCredentialPHIDType' => 'PhabricatorPHIDType', 'PassphraseCredentialPublicController' => 'PassphraseController', 'PassphraseCredentialQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PassphraseCredentialRevealController' => 'PassphraseController', 'PassphraseCredentialSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PassphraseCredentialSecretIDTransaction' => 'PassphraseCredentialTransactionType', 'PassphraseCredentialTransaction' => 'PhabricatorModularTransaction', 'PassphraseCredentialTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'PassphraseCredentialTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PassphraseCredentialTransactionType' => 'PhabricatorModularTransactionType', 'PassphraseCredentialType' => 'Phobject', 'PassphraseCredentialTypeTestCase' => 'PhabricatorTestCase', 'PassphraseCredentialUsernameTransaction' => 'PassphraseCredentialTransactionType', 'PassphraseCredentialViewController' => 'PassphraseController', 'PassphraseDAO' => 'PhabricatorLiskDAO', 'PassphraseDefaultEditCapability' => 'PhabricatorPolicyCapability', 'PassphraseDefaultViewCapability' => 'PhabricatorPolicyCapability', 'PassphraseNoteCredentialType' => 'PassphraseCredentialType', 'PassphrasePasswordCredentialType' => 'PassphraseCredentialType', 'PassphrasePasswordKey' => 'PassphraseAbstractKey', 'PassphraseQueryConduitAPIMethod' => 'PassphraseConduitAPIMethod', 'PassphraseRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PassphraseSSHGeneratedKeyCredentialType' => 'PassphraseSSHPrivateKeyCredentialType', 'PassphraseSSHKey' => 'PassphraseAbstractKey', 'PassphraseSSHPrivateKeyCredentialType' => 'PassphraseCredentialType', 'PassphraseSSHPrivateKeyFileCredentialType' => 'PassphraseSSHPrivateKeyCredentialType', 'PassphraseSSHPrivateKeyTextCredentialType' => 'PassphraseSSHPrivateKeyCredentialType', 'PassphraseSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PassphraseSecret' => 'PassphraseDAO', 'PassphraseTokenCredentialType' => 'PassphraseCredentialType', 'PasteConduitAPIMethod' => 'ConduitAPIMethod', 'PasteCreateConduitAPIMethod' => 'PasteConduitAPIMethod', 'PasteCreateMailReceiver' => 'PhabricatorMailReceiver', 'PasteDefaultEditCapability' => 'PhabricatorPolicyCapability', 'PasteDefaultViewCapability' => 'PhabricatorPolicyCapability', 'PasteEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'PasteEmbedView' => 'AphrontView', 'PasteInfoConduitAPIMethod' => 'PasteConduitAPIMethod', 'PasteLanguageSelectDatasource' => 'PhabricatorTypeaheadDatasource', 'PasteMailReceiver' => 'PhabricatorObjectMailReceiver', 'PasteQueryConduitAPIMethod' => 'PasteConduitAPIMethod', 'PasteReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PasteSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'PeopleBrowseUserDirectoryCapability' => 'PhabricatorPolicyCapability', 'PeopleCreateUsersCapability' => 'PhabricatorPolicyCapability', 'PeopleDisableUsersCapability' => 'PhabricatorPolicyCapability', 'PeopleHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension', 'PeopleMainMenuBarExtension' => 'PhabricatorMainMenuBarExtension', 'PeopleUserLogGarbageCollector' => 'PhabricatorGarbageCollector', 'Phabricator404Controller' => 'PhabricatorController', 'PhabricatorAWSConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorAccessControlTestCase' => 'PhabricatorTestCase', 'PhabricatorAccessLog' => 'Phobject', 'PhabricatorAccessLogConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorAccessibilitySetting' => 'PhabricatorSelectSetting', 'PhabricatorAccountSettingsPanel' => 'PhabricatorEditEngineSettingsPanel', 'PhabricatorActionListView' => 'AphrontTagView', 'PhabricatorActionView' => 'AphrontView', 'PhabricatorActivitySettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorAdministratorsPolicyRule' => 'PhabricatorPolicyRule', 'PhabricatorAjaxRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler', 'PhabricatorAlmanacApplication' => 'PhabricatorApplication', 'PhabricatorAmazonAuthProvider' => 'PhabricatorOAuth2AuthProvider', 'PhabricatorAnchorView' => 'AphrontView', 'PhabricatorAphlictManagementDebugWorkflow' => 'PhabricatorAphlictManagementWorkflow', 'PhabricatorAphlictManagementRestartWorkflow' => 'PhabricatorAphlictManagementWorkflow', 'PhabricatorAphlictManagementStartWorkflow' => 'PhabricatorAphlictManagementWorkflow', 'PhabricatorAphlictManagementStatusWorkflow' => 'PhabricatorAphlictManagementWorkflow', 'PhabricatorAphlictManagementStopWorkflow' => 'PhabricatorAphlictManagementWorkflow', 'PhabricatorAphlictManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorAphlictSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorAphrontBarUIExample' => 'PhabricatorUIExample', 'PhabricatorAphrontViewTestCase' => 'PhabricatorTestCase', 'PhabricatorAppSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorApplication' => array( 'PhabricatorLiskDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', ), 'PhabricatorApplicationApplicationPHIDType' => 'PhabricatorPHIDType', 'PhabricatorApplicationApplicationTransaction' => 'PhabricatorModularTransaction', 'PhabricatorApplicationApplicationTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorApplicationConfigOptions' => 'Phobject', 'PhabricatorApplicationConfigurationPanel' => 'Phobject', 'PhabricatorApplicationConfigurationPanelTestCase' => 'PhabricatorTestCase', 'PhabricatorApplicationDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorApplicationDetailViewController' => 'PhabricatorApplicationsController', 'PhabricatorApplicationEditController' => 'PhabricatorApplicationsController', 'PhabricatorApplicationEditEngine' => 'PhabricatorEditEngine', 'PhabricatorApplicationEditHTTPParameterHelpView' => 'AphrontView', 'PhabricatorApplicationEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorApplicationEmailCommandsController' => 'PhabricatorApplicationsController', 'PhabricatorApplicationObjectMailEngineExtension' => 'PhabricatorMailEngineExtension', 'PhabricatorApplicationPanelController' => 'PhabricatorApplicationsController', 'PhabricatorApplicationPolicyChangeTransaction' => 'PhabricatorApplicationTransactionType', 'PhabricatorApplicationProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorApplicationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorApplicationSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorApplicationSearchController' => 'PhabricatorSearchBaseController', 'PhabricatorApplicationSearchEngine' => 'Phobject', 'PhabricatorApplicationSearchEngineTestCase' => 'PhabricatorTestCase', 'PhabricatorApplicationSearchResultView' => 'Phobject', 'PhabricatorApplicationTestCase' => 'PhabricatorTestCase', 'PhabricatorApplicationTransaction' => array( 'PhabricatorLiskDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', ), 'PhabricatorApplicationTransactionComment' => array( 'PhabricatorLiskDAO', 'PhabricatorMarkupInterface', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', ), 'PhabricatorApplicationTransactionCommentEditController' => 'PhabricatorApplicationTransactionController', 'PhabricatorApplicationTransactionCommentEditor' => 'PhabricatorEditor', 'PhabricatorApplicationTransactionCommentHistoryController' => 'PhabricatorApplicationTransactionController', 'PhabricatorApplicationTransactionCommentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorApplicationTransactionCommentQuoteController' => 'PhabricatorApplicationTransactionController', 'PhabricatorApplicationTransactionCommentRawController' => 'PhabricatorApplicationTransactionController', 'PhabricatorApplicationTransactionCommentRemoveController' => 'PhabricatorApplicationTransactionController', 'PhabricatorApplicationTransactionCommentView' => 'AphrontView', 'PhabricatorApplicationTransactionController' => 'PhabricatorController', 'PhabricatorApplicationTransactionDetailController' => 'PhabricatorApplicationTransactionController', 'PhabricatorApplicationTransactionEditor' => 'PhabricatorEditor', 'PhabricatorApplicationTransactionFeedStory' => 'PhabricatorFeedStory', 'PhabricatorApplicationTransactionNoEffectException' => 'Exception', 'PhabricatorApplicationTransactionNoEffectResponse' => 'AphrontProxyResponse', 'PhabricatorApplicationTransactionPublishWorker' => 'PhabricatorWorker', 'PhabricatorApplicationTransactionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorApplicationTransactionRemarkupPreviewController' => 'PhabricatorApplicationTransactionController', 'PhabricatorApplicationTransactionReplyHandler' => 'PhabricatorMailReplyHandler', 'PhabricatorApplicationTransactionResponse' => 'AphrontProxyResponse', 'PhabricatorApplicationTransactionShowOlderController' => 'PhabricatorApplicationTransactionController', 'PhabricatorApplicationTransactionStructureException' => 'Exception', 'PhabricatorApplicationTransactionTemplatedCommentQuery' => 'PhabricatorApplicationTransactionCommentQuery', 'PhabricatorApplicationTransactionTextDiffDetailView' => 'AphrontView', 'PhabricatorApplicationTransactionTransactionPHIDType' => 'PhabricatorPHIDType', 'PhabricatorApplicationTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorApplicationTransactionValidationError' => 'Phobject', 'PhabricatorApplicationTransactionValidationException' => 'Exception', 'PhabricatorApplicationTransactionValidationResponse' => 'AphrontProxyResponse', 'PhabricatorApplicationTransactionValueController' => 'PhabricatorApplicationTransactionController', 'PhabricatorApplicationTransactionView' => 'AphrontView', 'PhabricatorApplicationTransactionWarningException' => 'Exception', 'PhabricatorApplicationTransactionWarningResponse' => 'AphrontProxyResponse', 'PhabricatorApplicationUninstallController' => 'PhabricatorApplicationsController', 'PhabricatorApplicationUninstallTransaction' => 'PhabricatorApplicationTransactionType', 'PhabricatorApplicationsApplication' => 'PhabricatorApplication', 'PhabricatorApplicationsController' => 'PhabricatorController', 'PhabricatorApplicationsListController' => 'PhabricatorApplicationsController', 'PhabricatorApplyEditField' => 'PhabricatorEditField', 'PhabricatorAsanaAuthProvider' => 'PhabricatorOAuth2AuthProvider', 'PhabricatorAsanaConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorAsanaSubtaskHasObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorAsanaTaskHasObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorAudioDocumentEngine' => 'PhabricatorDocumentEngine', 'PhabricatorAuditActionConstants' => 'Phobject', 'PhabricatorAuditApplication' => 'PhabricatorApplication', 'PhabricatorAuditCommentEditor' => 'PhabricatorEditor', 'PhabricatorAuditController' => 'PhabricatorController', 'PhabricatorAuditEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorAuditInlineComment' => array( 'Phobject', 'PhabricatorInlineCommentInterface', ), 'PhabricatorAuditListView' => 'AphrontView', 'PhabricatorAuditMailReceiver' => 'PhabricatorObjectMailReceiver', 'PhabricatorAuditManagementDeleteWorkflow' => 'PhabricatorAuditManagementWorkflow', 'PhabricatorAuditManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorAuditReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PhabricatorAuditStatusConstants' => 'Phobject', 'PhabricatorAuditSynchronizeManagementWorkflow' => 'PhabricatorAuditManagementWorkflow', 'PhabricatorAuditTransaction' => 'PhabricatorModularTransaction', 'PhabricatorAuditTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PhabricatorAuditTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorAuditTransactionView' => 'PhabricatorApplicationTransactionView', 'PhabricatorAuditUpdateOwnersManagementWorkflow' => 'PhabricatorAuditManagementWorkflow', 'PhabricatorAuthAccountView' => 'AphrontView', 'PhabricatorAuthApplication' => 'PhabricatorApplication', 'PhabricatorAuthAuthFactorPHIDType' => 'PhabricatorPHIDType', 'PhabricatorAuthAuthProviderPHIDType' => 'PhabricatorPHIDType', 'PhabricatorAuthChangePasswordAction' => 'PhabricatorSystemAction', 'PhabricatorAuthConduitAPIMethod' => 'ConduitAPIMethod', 'PhabricatorAuthConduitTokenRevoker' => 'PhabricatorAuthRevoker', 'PhabricatorAuthConfirmLinkController' => 'PhabricatorAuthController', 'PhabricatorAuthController' => 'PhabricatorController', 'PhabricatorAuthDAO' => 'PhabricatorLiskDAO', 'PhabricatorAuthDisableController' => 'PhabricatorAuthProviderConfigController', 'PhabricatorAuthDowngradeSessionController' => 'PhabricatorAuthController', 'PhabricatorAuthEditController' => 'PhabricatorAuthProviderConfigController', 'PhabricatorAuthFactor' => 'Phobject', 'PhabricatorAuthFactorConfig' => 'PhabricatorAuthDAO', 'PhabricatorAuthFactorTestCase' => 'PhabricatorTestCase', 'PhabricatorAuthFinishController' => 'PhabricatorAuthController', 'PhabricatorAuthHMACKey' => 'PhabricatorAuthDAO', 'PhabricatorAuthHighSecurityRequiredException' => 'Exception', 'PhabricatorAuthHighSecurityToken' => 'Phobject', 'PhabricatorAuthInvite' => array( 'PhabricatorUserDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorAuthInviteAccountException' => 'PhabricatorAuthInviteDialogException', 'PhabricatorAuthInviteAction' => 'Phobject', 'PhabricatorAuthInviteActionTableView' => 'AphrontView', 'PhabricatorAuthInviteController' => 'PhabricatorAuthController', 'PhabricatorAuthInviteDialogException' => 'PhabricatorAuthInviteException', 'PhabricatorAuthInviteEngine' => 'Phobject', 'PhabricatorAuthInviteException' => 'Exception', 'PhabricatorAuthInviteInvalidException' => 'PhabricatorAuthInviteDialogException', 'PhabricatorAuthInviteLoginException' => 'PhabricatorAuthInviteDialogException', 'PhabricatorAuthInvitePHIDType' => 'PhabricatorPHIDType', 'PhabricatorAuthInviteQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthInviteRegisteredException' => 'PhabricatorAuthInviteException', 'PhabricatorAuthInviteSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorAuthInviteTestCase' => 'PhabricatorTestCase', 'PhabricatorAuthInviteVerifyException' => 'PhabricatorAuthInviteDialogException', 'PhabricatorAuthInviteWorker' => 'PhabricatorWorker', 'PhabricatorAuthLinkController' => 'PhabricatorAuthController', 'PhabricatorAuthListController' => 'PhabricatorAuthProviderConfigController', 'PhabricatorAuthLoginController' => 'PhabricatorAuthController', 'PhabricatorAuthLoginHandler' => 'Phobject', 'PhabricatorAuthLogoutConduitAPIMethod' => 'PhabricatorAuthConduitAPIMethod', 'PhabricatorAuthMainMenuBarExtension' => 'PhabricatorMainMenuBarExtension', 'PhabricatorAuthManagementCachePKCS8Workflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementLDAPWorkflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementListFactorsWorkflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementRecoverWorkflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementRefreshWorkflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementRevokeWorkflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementStripWorkflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementTrustOAuthClientWorkflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementUnlimitWorkflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementUntrustOAuthClientWorkflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementVerifyWorkflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorAuthNeedsApprovalController' => 'PhabricatorAuthController', 'PhabricatorAuthNeedsMultiFactorController' => 'PhabricatorAuthController', 'PhabricatorAuthNewController' => 'PhabricatorAuthProviderConfigController', 'PhabricatorAuthOldOAuthRedirectController' => 'PhabricatorAuthController', 'PhabricatorAuthOneTimeLoginController' => 'PhabricatorAuthController', 'PhabricatorAuthOneTimeLoginTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType', 'PhabricatorAuthPassword' => array( 'PhabricatorAuthDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', 'PhabricatorApplicationTransactionInterface', ), 'PhabricatorAuthPasswordEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorAuthPasswordEngine' => 'Phobject', 'PhabricatorAuthPasswordException' => 'Exception', 'PhabricatorAuthPasswordPHIDType' => 'PhabricatorPHIDType', 'PhabricatorAuthPasswordQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthPasswordResetTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType', 'PhabricatorAuthPasswordRevokeTransaction' => 'PhabricatorAuthPasswordTransactionType', 'PhabricatorAuthPasswordRevoker' => 'PhabricatorAuthRevoker', 'PhabricatorAuthPasswordTestCase' => 'PhabricatorTestCase', 'PhabricatorAuthPasswordTransaction' => 'PhabricatorModularTransaction', 'PhabricatorAuthPasswordTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorAuthPasswordTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorAuthPasswordUpgradeTransaction' => 'PhabricatorAuthPasswordTransactionType', 'PhabricatorAuthProvider' => 'Phobject', 'PhabricatorAuthProviderConfig' => array( 'PhabricatorAuthDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', ), 'PhabricatorAuthProviderConfigController' => 'PhabricatorAuthController', 'PhabricatorAuthProviderConfigEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorAuthProviderConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthProviderConfigTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorAuthProviderConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorAuthProvidersGuidanceContext' => 'PhabricatorGuidanceContext', 'PhabricatorAuthProvidersGuidanceEngineExtension' => 'PhabricatorGuidanceEngineExtension', 'PhabricatorAuthQueryPublicKeysConduitAPIMethod' => 'PhabricatorAuthConduitAPIMethod', 'PhabricatorAuthRegisterController' => 'PhabricatorAuthController', 'PhabricatorAuthRevokeTokenController' => 'PhabricatorAuthController', 'PhabricatorAuthRevoker' => 'Phobject', 'PhabricatorAuthSSHKey' => array( 'PhabricatorAuthDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', 'PhabricatorApplicationTransactionInterface', ), 'PhabricatorAuthSSHKeyController' => 'PhabricatorAuthController', 'PhabricatorAuthSSHKeyEditController' => 'PhabricatorAuthSSHKeyController', 'PhabricatorAuthSSHKeyEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorAuthSSHKeyGenerateController' => 'PhabricatorAuthSSHKeyController', 'PhabricatorAuthSSHKeyListController' => 'PhabricatorAuthSSHKeyController', 'PhabricatorAuthSSHKeyPHIDType' => 'PhabricatorPHIDType', 'PhabricatorAuthSSHKeyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthSSHKeyReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PhabricatorAuthSSHKeyRevokeController' => 'PhabricatorAuthSSHKeyController', 'PhabricatorAuthSSHKeySearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorAuthSSHKeyTableView' => 'AphrontView', 'PhabricatorAuthSSHKeyTestCase' => 'PhabricatorTestCase', 'PhabricatorAuthSSHKeyTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorAuthSSHKeyTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorAuthSSHKeyViewController' => 'PhabricatorAuthSSHKeyController', 'PhabricatorAuthSSHPublicKey' => 'Phobject', 'PhabricatorAuthSSHRevoker' => 'PhabricatorAuthRevoker', 'PhabricatorAuthSession' => array( 'PhabricatorAuthDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorAuthSessionEngine' => 'Phobject', 'PhabricatorAuthSessionEngineExtension' => 'Phobject', 'PhabricatorAuthSessionEngineExtensionModule' => 'PhabricatorConfigModule', 'PhabricatorAuthSessionGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorAuthSessionInfo' => 'Phobject', 'PhabricatorAuthSessionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthSessionRevoker' => 'PhabricatorAuthRevoker', 'PhabricatorAuthSetPasswordController' => 'PhabricatorAuthController', 'PhabricatorAuthSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorAuthStartController' => 'PhabricatorAuthController', 'PhabricatorAuthTOTPKeyTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType', 'PhabricatorAuthTemporaryToken' => array( 'PhabricatorAuthDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorAuthTemporaryTokenGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorAuthTemporaryTokenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthTemporaryTokenRevoker' => 'PhabricatorAuthRevoker', 'PhabricatorAuthTemporaryTokenType' => 'Phobject', 'PhabricatorAuthTemporaryTokenTypeModule' => 'PhabricatorConfigModule', 'PhabricatorAuthTerminateSessionController' => 'PhabricatorAuthController', 'PhabricatorAuthTryFactorAction' => 'PhabricatorSystemAction', 'PhabricatorAuthUnlinkController' => 'PhabricatorAuthController', 'PhabricatorAuthValidateController' => 'PhabricatorAuthController', 'PhabricatorAuthenticationConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorAutoEventListener' => 'PhabricatorEventListener', 'PhabricatorBadgesApplication' => 'PhabricatorApplication', 'PhabricatorBadgesArchiveController' => 'PhabricatorBadgesController', 'PhabricatorBadgesAward' => array( 'PhabricatorBadgesDAO', 'PhabricatorDestructibleInterface', 'PhabricatorPolicyInterface', ), 'PhabricatorBadgesAwardController' => 'PhabricatorBadgesController', 'PhabricatorBadgesAwardQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorBadgesAwardTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorBadgesBadge' => array( 'PhabricatorBadgesDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorSubscribableInterface', 'PhabricatorFlaggableInterface', 'PhabricatorDestructibleInterface', 'PhabricatorConduitResultInterface', 'PhabricatorNgramsInterface', ), 'PhabricatorBadgesBadgeAwardTransaction' => 'PhabricatorBadgesBadgeTransactionType', 'PhabricatorBadgesBadgeDescriptionTransaction' => 'PhabricatorBadgesBadgeTransactionType', 'PhabricatorBadgesBadgeFlavorTransaction' => 'PhabricatorBadgesBadgeTransactionType', 'PhabricatorBadgesBadgeIconTransaction' => 'PhabricatorBadgesBadgeTransactionType', 'PhabricatorBadgesBadgeNameNgrams' => 'PhabricatorSearchNgrams', 'PhabricatorBadgesBadgeNameTransaction' => 'PhabricatorBadgesBadgeTransactionType', 'PhabricatorBadgesBadgeQualityTransaction' => 'PhabricatorBadgesBadgeTransactionType', 'PhabricatorBadgesBadgeRevokeTransaction' => 'PhabricatorBadgesBadgeTransactionType', 'PhabricatorBadgesBadgeStatusTransaction' => 'PhabricatorBadgesBadgeTransactionType', 'PhabricatorBadgesBadgeTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorBadgesBadgeTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorBadgesCommentController' => 'PhabricatorBadgesController', 'PhabricatorBadgesController' => 'PhabricatorController', 'PhabricatorBadgesCreateCapability' => 'PhabricatorPolicyCapability', 'PhabricatorBadgesDAO' => 'PhabricatorLiskDAO', 'PhabricatorBadgesDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorBadgesDefaultEditCapability' => 'PhabricatorPolicyCapability', 'PhabricatorBadgesEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'PhabricatorBadgesEditController' => 'PhabricatorBadgesController', 'PhabricatorBadgesEditEngine' => 'PhabricatorEditEngine', 'PhabricatorBadgesEditRecipientsController' => 'PhabricatorBadgesController', 'PhabricatorBadgesEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorBadgesIconSet' => 'PhabricatorIconSet', 'PhabricatorBadgesListController' => 'PhabricatorBadgesController', 'PhabricatorBadgesLootContextFreeGrammar' => 'PhutilContextFreeGrammar', 'PhabricatorBadgesMailReceiver' => 'PhabricatorObjectMailReceiver', 'PhabricatorBadgesPHIDType' => 'PhabricatorPHIDType', 'PhabricatorBadgesProfileController' => 'PhabricatorController', 'PhabricatorBadgesQuality' => 'Phobject', 'PhabricatorBadgesQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorBadgesRecipientsController' => 'PhabricatorBadgesProfileController', 'PhabricatorBadgesRecipientsListView' => 'AphrontView', 'PhabricatorBadgesRemoveRecipientsController' => 'PhabricatorBadgesController', 'PhabricatorBadgesReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PhabricatorBadgesSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorBadgesSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'PhabricatorBadgesSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorBadgesTransaction' => 'PhabricatorModularTransaction', 'PhabricatorBadgesTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PhabricatorBadgesTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorBadgesViewController' => 'PhabricatorBadgesProfileController', 'PhabricatorBarePageView' => 'AphrontPageView', 'PhabricatorBaseURISetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorBcryptPasswordHasher' => 'PhabricatorPasswordHasher', 'PhabricatorBinariesSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorBitbucketAuthProvider' => 'PhabricatorOAuth1AuthProvider', 'PhabricatorBoardColumnsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'PhabricatorBoardLayoutEngine' => 'Phobject', 'PhabricatorBoardRenderingEngine' => 'Phobject', 'PhabricatorBoardResponseEngine' => 'Phobject', 'PhabricatorBoolConfigType' => 'PhabricatorTextConfigType', 'PhabricatorBoolEditField' => 'PhabricatorEditField', 'PhabricatorBoolMailStamp' => 'PhabricatorMailStamp', 'PhabricatorBritishEnglishTranslation' => 'PhutilTranslation', 'PhabricatorBuiltinDraftEngine' => 'PhabricatorDraftEngine', 'PhabricatorBuiltinFileCachePurger' => 'PhabricatorCachePurger', 'PhabricatorBuiltinPatchList' => 'PhabricatorSQLPatchList', 'PhabricatorBulkContentSource' => 'PhabricatorContentSource', 'PhabricatorBulkEditGroup' => 'Phobject', 'PhabricatorBulkEngine' => 'Phobject', 'PhabricatorBulkManagementExportWorkflow' => 'PhabricatorBulkManagementWorkflow', 'PhabricatorBulkManagementMakeSilentWorkflow' => 'PhabricatorBulkManagementWorkflow', 'PhabricatorBulkManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorCSVExportFormat' => 'PhabricatorExportFormat', 'PhabricatorCacheDAO' => 'PhabricatorLiskDAO', 'PhabricatorCacheEngine' => 'Phobject', 'PhabricatorCacheEngineExtension' => 'Phobject', 'PhabricatorCacheGeneralGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorCacheManagementPurgeWorkflow' => 'PhabricatorCacheManagementWorkflow', 'PhabricatorCacheManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorCacheMarkupGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorCachePurger' => 'Phobject', 'PhabricatorCacheSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorCacheSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorCacheSpec' => 'Phobject', 'PhabricatorCacheTTLGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorCachedClassMapQuery' => 'Phobject', 'PhabricatorCaches' => 'Phobject', 'PhabricatorCachesTestCase' => 'PhabricatorTestCase', 'PhabricatorCalendarApplication' => 'PhabricatorApplication', 'PhabricatorCalendarController' => 'PhabricatorController', 'PhabricatorCalendarDAO' => 'PhabricatorLiskDAO', 'PhabricatorCalendarEvent' => array( 'PhabricatorCalendarDAO', 'PhabricatorPolicyInterface', 'PhabricatorExtendedPolicyInterface', 'PhabricatorPolicyCodexInterface', 'PhabricatorProjectInterface', 'PhabricatorMarkupInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorSubscribableInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorDestructibleInterface', 'PhabricatorMentionableInterface', 'PhabricatorFlaggableInterface', 'PhabricatorSpacesInterface', 'PhabricatorFulltextInterface', 'PhabricatorFerretInterface', 'PhabricatorConduitResultInterface', ), 'PhabricatorCalendarEventAcceptTransaction' => 'PhabricatorCalendarEventReplyTransaction', 'PhabricatorCalendarEventAllDayTransaction' => 'PhabricatorCalendarEventTransactionType', 'PhabricatorCalendarEventAvailabilityController' => 'PhabricatorCalendarController', 'PhabricatorCalendarEventCancelController' => 'PhabricatorCalendarController', 'PhabricatorCalendarEventCancelTransaction' => 'PhabricatorCalendarEventTransactionType', 'PhabricatorCalendarEventDateTransaction' => 'PhabricatorCalendarEventTransactionType', 'PhabricatorCalendarEventDeclineTransaction' => 'PhabricatorCalendarEventReplyTransaction', 'PhabricatorCalendarEventDefaultEditCapability' => 'PhabricatorPolicyCapability', 'PhabricatorCalendarEventDefaultViewCapability' => 'PhabricatorPolicyCapability', 'PhabricatorCalendarEventDescriptionTransaction' => 'PhabricatorCalendarEventTransactionType', 'PhabricatorCalendarEventDragController' => 'PhabricatorCalendarController', 'PhabricatorCalendarEventEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'PhabricatorCalendarEventEditController' => 'PhabricatorCalendarController', 'PhabricatorCalendarEventEditEngine' => 'PhabricatorEditEngine', 'PhabricatorCalendarEventEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorCalendarEventEmailCommand' => 'MetaMTAEmailTransactionCommand', 'PhabricatorCalendarEventEndDateTransaction' => 'PhabricatorCalendarEventDateTransaction', 'PhabricatorCalendarEventExportController' => 'PhabricatorCalendarController', 'PhabricatorCalendarEventFerretEngine' => 'PhabricatorFerretEngine', 'PhabricatorCalendarEventForkTransaction' => 'PhabricatorCalendarEventTransactionType', 'PhabricatorCalendarEventFrequencyTransaction' => 'PhabricatorCalendarEventTransactionType', 'PhabricatorCalendarEventFulltextEngine' => 'PhabricatorFulltextEngine', 'PhabricatorCalendarEventHeraldAdapter' => 'HeraldAdapter', 'PhabricatorCalendarEventHeraldField' => 'HeraldField', 'PhabricatorCalendarEventHeraldFieldGroup' => 'HeraldFieldGroup', 'PhabricatorCalendarEventHostPolicyRule' => 'PhabricatorPolicyRule', 'PhabricatorCalendarEventHostTransaction' => 'PhabricatorCalendarEventTransactionType', 'PhabricatorCalendarEventIconTransaction' => 'PhabricatorCalendarEventTransactionType', 'PhabricatorCalendarEventInviteTransaction' => 'PhabricatorCalendarEventTransactionType', 'PhabricatorCalendarEventInvitee' => array( 'PhabricatorCalendarDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorCalendarEventInviteeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorCalendarEventInviteesPolicyRule' => 'PhabricatorPolicyRule', 'PhabricatorCalendarEventJoinController' => 'PhabricatorCalendarController', 'PhabricatorCalendarEventListController' => 'PhabricatorCalendarController', 'PhabricatorCalendarEventMailReceiver' => 'PhabricatorObjectMailReceiver', 'PhabricatorCalendarEventNameHeraldField' => 'PhabricatorCalendarEventHeraldField', 'PhabricatorCalendarEventNameTransaction' => 'PhabricatorCalendarEventTransactionType', 'PhabricatorCalendarEventNotificationView' => 'Phobject', 'PhabricatorCalendarEventPHIDType' => 'PhabricatorPHIDType', 'PhabricatorCalendarEventPolicyCodex' => 'PhabricatorPolicyCodex', 'PhabricatorCalendarEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorCalendarEventRSVPEmailCommand' => 'PhabricatorCalendarEventEmailCommand', 'PhabricatorCalendarEventRecurringTransaction' => 'PhabricatorCalendarEventTransactionType', 'PhabricatorCalendarEventReplyTransaction' => 'PhabricatorCalendarEventTransactionType', 'PhabricatorCalendarEventSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'PhabricatorCalendarEventSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorCalendarEventStartDateTransaction' => 'PhabricatorCalendarEventDateTransaction', 'PhabricatorCalendarEventTransaction' => 'PhabricatorModularTransaction', 'PhabricatorCalendarEventTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PhabricatorCalendarEventTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorCalendarEventTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorCalendarEventUntilDateTransaction' => 'PhabricatorCalendarEventDateTransaction', 'PhabricatorCalendarEventViewController' => 'PhabricatorCalendarController', 'PhabricatorCalendarExport' => array( 'PhabricatorCalendarDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorDestructibleInterface', ), 'PhabricatorCalendarExportDisableController' => 'PhabricatorCalendarController', 'PhabricatorCalendarExportDisableTransaction' => 'PhabricatorCalendarExportTransactionType', 'PhabricatorCalendarExportEditController' => 'PhabricatorCalendarController', 'PhabricatorCalendarExportEditEngine' => 'PhabricatorEditEngine', 'PhabricatorCalendarExportEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorCalendarExportICSController' => 'PhabricatorCalendarController', 'PhabricatorCalendarExportListController' => 'PhabricatorCalendarController', 'PhabricatorCalendarExportModeTransaction' => 'PhabricatorCalendarExportTransactionType', 'PhabricatorCalendarExportNameTransaction' => 'PhabricatorCalendarExportTransactionType', 'PhabricatorCalendarExportPHIDType' => 'PhabricatorPHIDType', 'PhabricatorCalendarExportQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorCalendarExportQueryKeyTransaction' => 'PhabricatorCalendarExportTransactionType', 'PhabricatorCalendarExportSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorCalendarExportTransaction' => 'PhabricatorModularTransaction', 'PhabricatorCalendarExportTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorCalendarExportTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorCalendarExportViewController' => 'PhabricatorCalendarController', 'PhabricatorCalendarExternalInvitee' => array( 'PhabricatorCalendarDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorCalendarExternalInviteePHIDType' => 'PhabricatorPHIDType', 'PhabricatorCalendarExternalInviteeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorCalendarICSFileImportEngine' => 'PhabricatorCalendarICSImportEngine', 'PhabricatorCalendarICSImportEngine' => 'PhabricatorCalendarImportEngine', 'PhabricatorCalendarICSURIImportEngine' => 'PhabricatorCalendarICSImportEngine', 'PhabricatorCalendarICSWriter' => 'Phobject', 'PhabricatorCalendarIconSet' => 'PhabricatorIconSet', 'PhabricatorCalendarImport' => array( 'PhabricatorCalendarDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorDestructibleInterface', ), 'PhabricatorCalendarImportDefaultLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportDeleteController' => 'PhabricatorCalendarController', 'PhabricatorCalendarImportDeleteLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportDeleteTransaction' => 'PhabricatorCalendarImportTransactionType', 'PhabricatorCalendarImportDisableController' => 'PhabricatorCalendarController', 'PhabricatorCalendarImportDisableTransaction' => 'PhabricatorCalendarImportTransactionType', 'PhabricatorCalendarImportDropController' => 'PhabricatorCalendarController', 'PhabricatorCalendarImportDuplicateLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportEditController' => 'PhabricatorCalendarController', 'PhabricatorCalendarImportEditEngine' => 'PhabricatorEditEngine', 'PhabricatorCalendarImportEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorCalendarImportEmptyLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportEngine' => 'Phobject', 'PhabricatorCalendarImportEpochLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportFetchLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportFrequencyLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportFrequencyTransaction' => 'PhabricatorCalendarImportTransactionType', 'PhabricatorCalendarImportICSFileTransaction' => 'PhabricatorCalendarImportTransactionType', 'PhabricatorCalendarImportICSLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportICSURITransaction' => 'PhabricatorCalendarImportTransactionType', 'PhabricatorCalendarImportICSWarningLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportIgnoredNodeLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportListController' => 'PhabricatorCalendarController', 'PhabricatorCalendarImportLog' => array( 'PhabricatorCalendarDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', ), 'PhabricatorCalendarImportLogListController' => 'PhabricatorCalendarController', 'PhabricatorCalendarImportLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorCalendarImportLogSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorCalendarImportLogType' => 'Phobject', 'PhabricatorCalendarImportLogView' => 'AphrontView', 'PhabricatorCalendarImportNameTransaction' => 'PhabricatorCalendarImportTransactionType', 'PhabricatorCalendarImportOriginalLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportOrphanLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportPHIDType' => 'PhabricatorPHIDType', 'PhabricatorCalendarImportQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorCalendarImportQueueLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportReloadController' => 'PhabricatorCalendarController', 'PhabricatorCalendarImportReloadTransaction' => 'PhabricatorCalendarImportTransactionType', 'PhabricatorCalendarImportReloadWorker' => 'PhabricatorWorker', 'PhabricatorCalendarImportSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorCalendarImportTransaction' => 'PhabricatorModularTransaction', 'PhabricatorCalendarImportTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorCalendarImportTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorCalendarImportTriggerLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportUpdateLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportViewController' => 'PhabricatorCalendarController', 'PhabricatorCalendarInviteeDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorCalendarInviteeUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorCalendarInviteeViewerFunctionDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorCalendarManagementNotifyWorkflow' => 'PhabricatorCalendarManagementWorkflow', 'PhabricatorCalendarManagementReloadWorkflow' => 'PhabricatorCalendarManagementWorkflow', 'PhabricatorCalendarManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorCalendarNotification' => 'PhabricatorCalendarDAO', 'PhabricatorCalendarNotificationEngine' => 'Phobject', 'PhabricatorCalendarRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PhabricatorCalendarReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PhabricatorCalendarSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorCelerityApplication' => 'PhabricatorApplication', 'PhabricatorCelerityTestCase' => 'PhabricatorTestCase', 'PhabricatorChangeParserTestCase' => 'PhabricatorWorkingCopyTestCase', 'PhabricatorChangesetCachePurger' => 'PhabricatorCachePurger', 'PhabricatorChangesetResponse' => 'AphrontProxyResponse', 'PhabricatorChatLogApplication' => 'PhabricatorApplication', 'PhabricatorChatLogChannel' => array( 'PhabricatorChatLogDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorChatLogChannelListController' => 'PhabricatorChatLogController', 'PhabricatorChatLogChannelLogController' => 'PhabricatorChatLogController', 'PhabricatorChatLogChannelQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorChatLogController' => 'PhabricatorController', 'PhabricatorChatLogDAO' => 'PhabricatorLiskDAO', 'PhabricatorChatLogEvent' => array( 'PhabricatorChatLogDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorChatLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorCheckboxesEditField' => 'PhabricatorEditField', 'PhabricatorChunkedFileStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorClassConfigType' => 'PhabricatorTextConfigType', 'PhabricatorClusterConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorClusterDatabasesConfigType' => 'PhabricatorJSONConfigType', 'PhabricatorClusterException' => 'Exception', 'PhabricatorClusterExceptionHandler' => 'PhabricatorRequestExceptionHandler', 'PhabricatorClusterImpossibleWriteException' => 'PhabricatorClusterException', 'PhabricatorClusterImproperWriteException' => 'PhabricatorClusterException', 'PhabricatorClusterMailersConfigType' => 'PhabricatorJSONConfigType', 'PhabricatorClusterNoHostForRoleException' => 'Exception', 'PhabricatorClusterSearchConfigType' => 'PhabricatorJSONConfigType', 'PhabricatorClusterServiceHealthRecord' => 'Phobject', 'PhabricatorClusterStrandedException' => 'PhabricatorClusterException', 'PhabricatorColumnsEditField' => 'PhabricatorPHIDListEditField', 'PhabricatorCommentEditEngineExtension' => 'PhabricatorEditEngineExtension', 'PhabricatorCommentEditField' => 'PhabricatorEditField', 'PhabricatorCommentEditType' => 'PhabricatorEditType', 'PhabricatorCommitBranchesField' => 'PhabricatorCommitCustomField', 'PhabricatorCommitCustomField' => 'PhabricatorCustomField', 'PhabricatorCommitMergedCommitsField' => 'PhabricatorCommitCustomField', 'PhabricatorCommitRepositoryField' => 'PhabricatorCommitCustomField', 'PhabricatorCommitSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorCommitTagsField' => 'PhabricatorCommitCustomField', 'PhabricatorCommonPasswords' => 'Phobject', 'PhabricatorConduitAPIController' => 'PhabricatorConduitController', 'PhabricatorConduitApplication' => 'PhabricatorApplication', 'PhabricatorConduitCallManagementWorkflow' => 'PhabricatorConduitManagementWorkflow', 'PhabricatorConduitCertificateToken' => 'PhabricatorConduitDAO', 'PhabricatorConduitConsoleController' => 'PhabricatorConduitController', 'PhabricatorConduitContentSource' => 'PhabricatorContentSource', 'PhabricatorConduitController' => 'PhabricatorController', 'PhabricatorConduitDAO' => 'PhabricatorLiskDAO', 'PhabricatorConduitEditField' => 'PhabricatorEditField', 'PhabricatorConduitListController' => 'PhabricatorConduitController', 'PhabricatorConduitLogController' => 'PhabricatorConduitController', 'PhabricatorConduitLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorConduitLogSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorConduitManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorConduitMethodCallLog' => array( 'PhabricatorConduitDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorConduitMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorConduitRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler', 'PhabricatorConduitResultInterface' => 'PhabricatorPHIDInterface', 'PhabricatorConduitSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorConduitSearchFieldSpecification' => 'Phobject', 'PhabricatorConduitTestCase' => 'PhabricatorTestCase', 'PhabricatorConduitToken' => array( 'PhabricatorConduitDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorConduitTokenController' => 'PhabricatorConduitController', 'PhabricatorConduitTokenEditController' => 'PhabricatorConduitController', 'PhabricatorConduitTokenHandshakeController' => 'PhabricatorConduitController', 'PhabricatorConduitTokenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorConduitTokenTerminateController' => 'PhabricatorConduitController', 'PhabricatorConduitTokensSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorConfigAllController' => 'PhabricatorConfigController', 'PhabricatorConfigApplication' => 'PhabricatorApplication', 'PhabricatorConfigApplicationController' => 'PhabricatorConfigController', 'PhabricatorConfigCacheController' => 'PhabricatorConfigController', 'PhabricatorConfigClusterDatabasesController' => 'PhabricatorConfigController', 'PhabricatorConfigClusterNotificationsController' => 'PhabricatorConfigController', 'PhabricatorConfigClusterRepositoriesController' => 'PhabricatorConfigController', 'PhabricatorConfigClusterSearchController' => 'PhabricatorConfigController', 'PhabricatorConfigCollectorsModule' => 'PhabricatorConfigModule', 'PhabricatorConfigColumnSchema' => 'PhabricatorConfigStorageSchema', 'PhabricatorConfigConfigPHIDType' => 'PhabricatorPHIDType', 'PhabricatorConfigConstants' => 'Phobject', 'PhabricatorConfigController' => 'PhabricatorController', 'PhabricatorConfigCoreSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorConfigDatabaseController' => 'PhabricatorConfigController', 'PhabricatorConfigDatabaseIssueController' => 'PhabricatorConfigDatabaseController', 'PhabricatorConfigDatabaseSchema' => 'PhabricatorConfigStorageSchema', 'PhabricatorConfigDatabaseSource' => 'PhabricatorConfigProxySource', 'PhabricatorConfigDatabaseStatusController' => 'PhabricatorConfigDatabaseController', 'PhabricatorConfigDefaultSource' => 'PhabricatorConfigProxySource', 'PhabricatorConfigDictionarySource' => 'PhabricatorConfigSource', 'PhabricatorConfigEdgeModule' => 'PhabricatorConfigModule', 'PhabricatorConfigEditController' => 'PhabricatorConfigController', 'PhabricatorConfigEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorConfigEntry' => array( 'PhabricatorConfigEntryDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', ), 'PhabricatorConfigEntryDAO' => 'PhabricatorLiskDAO', 'PhabricatorConfigEntryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorConfigFileSource' => 'PhabricatorConfigProxySource', 'PhabricatorConfigGroupConstants' => 'PhabricatorConfigConstants', 'PhabricatorConfigGroupController' => 'PhabricatorConfigController', 'PhabricatorConfigHTTPParameterTypesModule' => 'PhabricatorConfigModule', 'PhabricatorConfigHistoryController' => 'PhabricatorConfigController', 'PhabricatorConfigIgnoreController' => 'PhabricatorConfigController', 'PhabricatorConfigIssueListController' => 'PhabricatorConfigController', 'PhabricatorConfigIssuePanelController' => 'PhabricatorConfigController', 'PhabricatorConfigIssueViewController' => 'PhabricatorConfigController', 'PhabricatorConfigJSON' => 'Phobject', 'PhabricatorConfigJSONOptionType' => 'PhabricatorConfigOptionType', 'PhabricatorConfigKeySchema' => 'PhabricatorConfigStorageSchema', 'PhabricatorConfigListController' => 'PhabricatorConfigController', 'PhabricatorConfigLocalSource' => 'PhabricatorConfigProxySource', 'PhabricatorConfigManagementDeleteWorkflow' => 'PhabricatorConfigManagementWorkflow', 'PhabricatorConfigManagementDoneWorkflow' => 'PhabricatorConfigManagementWorkflow', 'PhabricatorConfigManagementGetWorkflow' => 'PhabricatorConfigManagementWorkflow', 'PhabricatorConfigManagementListWorkflow' => 'PhabricatorConfigManagementWorkflow', 'PhabricatorConfigManagementMigrateWorkflow' => 'PhabricatorConfigManagementWorkflow', 'PhabricatorConfigManagementSetWorkflow' => 'PhabricatorConfigManagementWorkflow', 'PhabricatorConfigManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorConfigManualActivity' => 'PhabricatorConfigEntryDAO', 'PhabricatorConfigModule' => 'Phobject', 'PhabricatorConfigModuleController' => 'PhabricatorConfigController', 'PhabricatorConfigOption' => 'Phobject', 'PhabricatorConfigOptionType' => 'Phobject', 'PhabricatorConfigPHIDModule' => 'PhabricatorConfigModule', 'PhabricatorConfigProxySource' => 'PhabricatorConfigSource', 'PhabricatorConfigPurgeCacheController' => 'PhabricatorConfigController', 'PhabricatorConfigRegexOptionType' => 'PhabricatorConfigJSONOptionType', 'PhabricatorConfigRequestExceptionHandlerModule' => 'PhabricatorConfigModule', 'PhabricatorConfigResponse' => 'AphrontStandaloneHTMLResponse', 'PhabricatorConfigSchemaQuery' => 'Phobject', 'PhabricatorConfigSchemaSpec' => 'Phobject', 'PhabricatorConfigServerSchema' => 'PhabricatorConfigStorageSchema', 'PhabricatorConfigSetupCheckModule' => 'PhabricatorConfigModule', 'PhabricatorConfigSiteModule' => 'PhabricatorConfigModule', 'PhabricatorConfigSiteSource' => 'PhabricatorConfigProxySource', 'PhabricatorConfigSource' => 'Phobject', 'PhabricatorConfigStackSource' => 'PhabricatorConfigSource', 'PhabricatorConfigStorageSchema' => 'Phobject', 'PhabricatorConfigTableSchema' => 'PhabricatorConfigStorageSchema', 'PhabricatorConfigTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorConfigType' => 'Phobject', 'PhabricatorConfigValidationException' => 'Exception', 'PhabricatorConfigVersionController' => 'PhabricatorConfigController', 'PhabricatorConpherenceApplication' => 'PhabricatorApplication', 'PhabricatorConpherenceColumnMinimizeSetting' => 'PhabricatorInternalSetting', 'PhabricatorConpherenceColumnVisibleSetting' => 'PhabricatorInternalSetting', 'PhabricatorConpherenceNotificationsSetting' => 'PhabricatorSelectSetting', 'PhabricatorConpherencePreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel', 'PhabricatorConpherenceProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorConpherenceRoomContextFreeGrammar' => 'PhutilContextFreeGrammar', 'PhabricatorConpherenceRoomTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorConpherenceSoundSetting' => 'PhabricatorSelectSetting', 'PhabricatorConpherenceThreadPHIDType' => 'PhabricatorPHIDType', 'PhabricatorConpherenceWidgetVisibleSetting' => 'PhabricatorInternalSetting', 'PhabricatorConsoleApplication' => 'PhabricatorApplication', 'PhabricatorConsoleContentSource' => 'PhabricatorContentSource', 'PhabricatorContentSource' => 'Phobject', 'PhabricatorContentSourceModule' => 'PhabricatorConfigModule', 'PhabricatorContentSourceView' => 'AphrontView', 'PhabricatorContributedToObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorController' => 'AphrontController', 'PhabricatorCookies' => 'Phobject', 'PhabricatorCoreConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorCoreCreateTransaction' => 'PhabricatorCoreTransactionType', 'PhabricatorCoreTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorCoreVoidTransaction' => 'PhabricatorModularTransactionType', 'PhabricatorCountFact' => 'PhabricatorFact', 'PhabricatorCountdown' => array( 'PhabricatorCountdownDAO', 'PhabricatorPolicyInterface', 'PhabricatorFlaggableInterface', 'PhabricatorSubscribableInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorSpacesInterface', 'PhabricatorProjectInterface', 'PhabricatorDestructibleInterface', 'PhabricatorConduitResultInterface', ), 'PhabricatorCountdownApplication' => 'PhabricatorApplication', 'PhabricatorCountdownController' => 'PhabricatorController', 'PhabricatorCountdownCountdownPHIDType' => 'PhabricatorPHIDType', 'PhabricatorCountdownDAO' => 'PhabricatorLiskDAO', 'PhabricatorCountdownDefaultEditCapability' => 'PhabricatorPolicyCapability', 'PhabricatorCountdownDefaultViewCapability' => 'PhabricatorPolicyCapability', 'PhabricatorCountdownDescriptionTransaction' => 'PhabricatorCountdownTransactionType', 'PhabricatorCountdownEditController' => 'PhabricatorCountdownController', 'PhabricatorCountdownEditEngine' => 'PhabricatorEditEngine', 'PhabricatorCountdownEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorCountdownEpochTransaction' => 'PhabricatorCountdownTransactionType', 'PhabricatorCountdownListController' => 'PhabricatorCountdownController', 'PhabricatorCountdownMailReceiver' => 'PhabricatorObjectMailReceiver', 'PhabricatorCountdownQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorCountdownRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PhabricatorCountdownReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PhabricatorCountdownSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorCountdownSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorCountdownTitleTransaction' => 'PhabricatorCountdownTransactionType', 'PhabricatorCountdownTransaction' => 'PhabricatorModularTransaction', 'PhabricatorCountdownTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PhabricatorCountdownTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorCountdownTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorCountdownView' => 'AphrontView', 'PhabricatorCountdownViewController' => 'PhabricatorCountdownController', 'PhabricatorCursorPagedPolicyAwareQuery' => 'PhabricatorPolicyAwareQuery', 'PhabricatorCustomField' => 'Phobject', 'PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorCustomFieldApplicationSearchDatasource' => 'PhabricatorTypeaheadProxyDatasource', 'PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorCustomFieldAttachment' => 'Phobject', 'PhabricatorCustomFieldConfigOptionType' => 'PhabricatorConfigOptionType', 'PhabricatorCustomFieldDataNotAvailableException' => 'Exception', 'PhabricatorCustomFieldEditEngineExtension' => 'PhabricatorEditEngineExtension', 'PhabricatorCustomFieldEditField' => 'PhabricatorEditField', 'PhabricatorCustomFieldEditType' => 'PhabricatorEditType', 'PhabricatorCustomFieldExportEngineExtension' => 'PhabricatorExportEngineExtension', 'PhabricatorCustomFieldFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension', 'PhabricatorCustomFieldHeraldAction' => 'HeraldAction', 'PhabricatorCustomFieldHeraldActionGroup' => 'HeraldActionGroup', 'PhabricatorCustomFieldHeraldField' => 'HeraldField', 'PhabricatorCustomFieldHeraldFieldGroup' => 'HeraldFieldGroup', 'PhabricatorCustomFieldImplementationIncompleteException' => 'Exception', 'PhabricatorCustomFieldIndexStorage' => 'PhabricatorLiskDAO', 'PhabricatorCustomFieldList' => 'Phobject', 'PhabricatorCustomFieldMonogramParser' => 'Phobject', 'PhabricatorCustomFieldNotAttachedException' => 'Exception', 'PhabricatorCustomFieldNotProxyException' => 'Exception', 'PhabricatorCustomFieldNumericIndexStorage' => 'PhabricatorCustomFieldIndexStorage', 'PhabricatorCustomFieldSearchEngineExtension' => 'PhabricatorSearchEngineExtension', 'PhabricatorCustomFieldStorage' => 'PhabricatorLiskDAO', 'PhabricatorCustomFieldStorageQuery' => 'Phobject', 'PhabricatorCustomFieldStringIndexStorage' => 'PhabricatorCustomFieldIndexStorage', 'PhabricatorCustomLogoConfigType' => 'PhabricatorConfigOptionType', 'PhabricatorCustomUIFooterConfigType' => 'PhabricatorConfigJSONOptionType', 'PhabricatorDaemon' => 'PhutilDaemon', 'PhabricatorDaemonBulkJobController' => 'PhabricatorDaemonController', 'PhabricatorDaemonBulkJobListController' => 'PhabricatorDaemonBulkJobController', 'PhabricatorDaemonBulkJobMonitorController' => 'PhabricatorDaemonBulkJobController', 'PhabricatorDaemonBulkJobViewController' => 'PhabricatorDaemonBulkJobController', 'PhabricatorDaemonConsoleController' => 'PhabricatorDaemonController', 'PhabricatorDaemonContentSource' => 'PhabricatorContentSource', 'PhabricatorDaemonController' => 'PhabricatorController', 'PhabricatorDaemonDAO' => 'PhabricatorLiskDAO', 'PhabricatorDaemonEventListener' => 'PhabricatorEventListener', 'PhabricatorDaemonLockLog' => 'PhabricatorDaemonDAO', 'PhabricatorDaemonLockLogGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorDaemonLog' => array( 'PhabricatorDaemonDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorDaemonLogEvent' => 'PhabricatorDaemonDAO', 'PhabricatorDaemonLogEventGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorDaemonLogEventViewController' => 'PhabricatorDaemonController', 'PhabricatorDaemonLogEventsView' => 'AphrontView', 'PhabricatorDaemonLogGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorDaemonLogListController' => 'PhabricatorDaemonController', 'PhabricatorDaemonLogListView' => 'AphrontView', 'PhabricatorDaemonLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorDaemonLogViewController' => 'PhabricatorDaemonController', 'PhabricatorDaemonManagementDebugWorkflow' => 'PhabricatorDaemonManagementWorkflow', 'PhabricatorDaemonManagementLaunchWorkflow' => 'PhabricatorDaemonManagementWorkflow', 'PhabricatorDaemonManagementListWorkflow' => 'PhabricatorDaemonManagementWorkflow', 'PhabricatorDaemonManagementLogWorkflow' => 'PhabricatorDaemonManagementWorkflow', 'PhabricatorDaemonManagementReloadWorkflow' => 'PhabricatorDaemonManagementWorkflow', 'PhabricatorDaemonManagementRestartWorkflow' => 'PhabricatorDaemonManagementWorkflow', 'PhabricatorDaemonManagementStartWorkflow' => 'PhabricatorDaemonManagementWorkflow', 'PhabricatorDaemonManagementStatusWorkflow' => 'PhabricatorDaemonManagementWorkflow', 'PhabricatorDaemonManagementStopWorkflow' => 'PhabricatorDaemonManagementWorkflow', 'PhabricatorDaemonManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorDaemonOverseerModule' => 'PhutilDaemonOverseerModule', 'PhabricatorDaemonReference' => 'Phobject', 'PhabricatorDaemonTaskGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorDaemonTasksTableView' => 'AphrontView', 'PhabricatorDaemonsApplication' => 'PhabricatorApplication', 'PhabricatorDaemonsSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorDailyRoutineTriggerClock' => 'PhabricatorTriggerClock', 'PhabricatorDarkConsoleSetting' => 'PhabricatorSelectSetting', 'PhabricatorDarkConsoleTabSetting' => 'PhabricatorInternalSetting', 'PhabricatorDarkConsoleVisibleSetting' => 'PhabricatorInternalSetting', 'PhabricatorDashboard' => array( 'PhabricatorDashboardDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorFlaggableInterface', 'PhabricatorDestructibleInterface', 'PhabricatorProjectInterface', 'PhabricatorNgramsInterface', ), 'PhabricatorDashboardAddPanelController' => 'PhabricatorDashboardController', 'PhabricatorDashboardApplication' => 'PhabricatorApplication', 'PhabricatorDashboardArchiveController' => 'PhabricatorDashboardController', 'PhabricatorDashboardArrangeController' => 'PhabricatorDashboardProfileController', 'PhabricatorDashboardController' => 'PhabricatorController', 'PhabricatorDashboardDAO' => 'PhabricatorLiskDAO', 'PhabricatorDashboardDashboardHasPanelEdgeType' => 'PhabricatorEdgeType', 'PhabricatorDashboardDashboardPHIDType' => 'PhabricatorPHIDType', 'PhabricatorDashboardDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorDashboardEditController' => 'PhabricatorDashboardController', 'PhabricatorDashboardIconSet' => 'PhabricatorIconSet', 'PhabricatorDashboardInstall' => 'PhabricatorDashboardDAO', 'PhabricatorDashboardInstallController' => 'PhabricatorDashboardController', 'PhabricatorDashboardLayoutConfig' => 'Phobject', 'PhabricatorDashboardListController' => 'PhabricatorDashboardController', 'PhabricatorDashboardManageController' => 'PhabricatorDashboardProfileController', 'PhabricatorDashboardMovePanelController' => 'PhabricatorDashboardController', 'PhabricatorDashboardNgrams' => 'PhabricatorSearchNgrams', 'PhabricatorDashboardPanel' => array( 'PhabricatorDashboardDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorCustomFieldInterface', 'PhabricatorFlaggableInterface', 'PhabricatorDestructibleInterface', 'PhabricatorNgramsInterface', ), 'PhabricatorDashboardPanelArchiveController' => 'PhabricatorDashboardController', 'PhabricatorDashboardPanelCoreCustomField' => array( 'PhabricatorDashboardPanelCustomField', 'PhabricatorStandardCustomFieldInterface', ), 'PhabricatorDashboardPanelCustomField' => 'PhabricatorCustomField', 'PhabricatorDashboardPanelDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorDashboardPanelEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'PhabricatorDashboardPanelEditController' => 'PhabricatorDashboardController', 'PhabricatorDashboardPanelEditEngine' => 'PhabricatorEditEngine', 'PhabricatorDashboardPanelEditproController' => 'PhabricatorDashboardController', 'PhabricatorDashboardPanelHasDashboardEdgeType' => 'PhabricatorEdgeType', 'PhabricatorDashboardPanelListController' => 'PhabricatorDashboardController', 'PhabricatorDashboardPanelNgrams' => 'PhabricatorSearchNgrams', 'PhabricatorDashboardPanelPHIDType' => 'PhabricatorPHIDType', 'PhabricatorDashboardPanelQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorDashboardPanelRenderController' => 'PhabricatorDashboardController', 'PhabricatorDashboardPanelRenderingEngine' => 'Phobject', 'PhabricatorDashboardPanelSearchApplicationCustomField' => 'PhabricatorStandardCustomField', 'PhabricatorDashboardPanelSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorDashboardPanelSearchQueryCustomField' => 'PhabricatorStandardCustomField', 'PhabricatorDashboardPanelTabsCustomField' => 'PhabricatorStandardCustomField', 'PhabricatorDashboardPanelTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorDashboardPanelTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorDashboardPanelTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorDashboardPanelType' => 'Phobject', 'PhabricatorDashboardPanelViewController' => 'PhabricatorDashboardController', 'PhabricatorDashboardProfileController' => 'PhabricatorController', 'PhabricatorDashboardProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorDashboardQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorDashboardQueryPanelInstallController' => 'PhabricatorDashboardController', 'PhabricatorDashboardQueryPanelType' => 'PhabricatorDashboardPanelType', 'PhabricatorDashboardRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PhabricatorDashboardRemovePanelController' => 'PhabricatorDashboardController', 'PhabricatorDashboardRenderingEngine' => 'Phobject', 'PhabricatorDashboardSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorDashboardSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorDashboardTabsPanelType' => 'PhabricatorDashboardPanelType', 'PhabricatorDashboardTextPanelType' => 'PhabricatorDashboardPanelType', 'PhabricatorDashboardTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorDashboardTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorDashboardTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorDashboardViewController' => 'PhabricatorDashboardProfileController', 'PhabricatorDataCacheSpec' => 'PhabricatorCacheSpec', 'PhabricatorDataNotAttachedException' => 'Exception', 'PhabricatorDatabaseRef' => 'Phobject', 'PhabricatorDatabaseRefParser' => 'Phobject', 'PhabricatorDatabaseSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorDatasourceApplicationEngineExtension' => 'PhabricatorDatasourceEngineExtension', 'PhabricatorDatasourceEditField' => 'PhabricatorTokenizerEditField', 'PhabricatorDatasourceEditType' => 'PhabricatorPHIDListEditType', 'PhabricatorDatasourceEngine' => 'Phobject', 'PhabricatorDatasourceEngineExtension' => 'Phobject', 'PhabricatorDateFormatSetting' => 'PhabricatorSelectSetting', 'PhabricatorDateTimeSettingsPanel' => 'PhabricatorEditEngineSettingsPanel', 'PhabricatorDebugController' => 'PhabricatorController', 'PhabricatorDefaultRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler', 'PhabricatorDefaultSyntaxStyle' => 'PhabricatorSyntaxStyle', 'PhabricatorDestructibleCodex' => 'Phobject', 'PhabricatorDestructionEngine' => 'Phobject', 'PhabricatorDestructionEngineExtension' => 'Phobject', 'PhabricatorDestructionEngineExtensionModule' => 'PhabricatorConfigModule', 'PhabricatorDeveloperConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorDeveloperPreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel', 'PhabricatorDiffInlineCommentQuery' => 'PhabricatorApplicationTransactionCommentQuery', 'PhabricatorDiffPreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel', 'PhabricatorDifferenceEngine' => 'Phobject', 'PhabricatorDifferentialApplication' => 'PhabricatorApplication', 'PhabricatorDifferentialAttachCommitWorkflow' => 'PhabricatorDifferentialManagementWorkflow', 'PhabricatorDifferentialConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorDifferentialExtractWorkflow' => 'PhabricatorDifferentialManagementWorkflow', 'PhabricatorDifferentialManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorDifferentialMigrateHunkWorkflow' => 'PhabricatorDifferentialManagementWorkflow', 'PhabricatorDifferentialRebuildChangesetsWorkflow' => 'PhabricatorDifferentialManagementWorkflow', 'PhabricatorDifferentialRevisionTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorDiffusionApplication' => 'PhabricatorApplication', 'PhabricatorDiffusionBlameSetting' => 'PhabricatorInternalSetting', 'PhabricatorDiffusionConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorDisabledUserController' => 'PhabricatorAuthController', 'PhabricatorDisplayPreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel', 'PhabricatorDisqusAuthProvider' => 'PhabricatorOAuth2AuthProvider', 'PhabricatorDividerEditField' => 'PhabricatorEditField', 'PhabricatorDividerProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorDivinerApplication' => 'PhabricatorApplication', 'PhabricatorDocumentEngine' => 'Phobject', 'PhabricatorDocumentRef' => 'Phobject', 'PhabricatorDocumentRenderingEngine' => 'Phobject', 'PhabricatorDoorkeeperApplication' => 'PhabricatorApplication', 'PhabricatorDoubleExportField' => 'PhabricatorExportField', 'PhabricatorDraft' => 'PhabricatorDraftDAO', 'PhabricatorDraftDAO' => 'PhabricatorLiskDAO', 'PhabricatorDraftEngine' => 'Phobject', 'PhabricatorDrydockApplication' => 'PhabricatorApplication', 'PhabricatorEdgeChangeRecord' => 'Phobject', 'PhabricatorEdgeChangeRecordTestCase' => 'PhabricatorTestCase', 'PhabricatorEdgeConfig' => 'PhabricatorEdgeConstants', 'PhabricatorEdgeConstants' => 'Phobject', 'PhabricatorEdgeCycleException' => 'Exception', 'PhabricatorEdgeEditType' => 'PhabricatorPHIDListEditType', 'PhabricatorEdgeEditor' => 'Phobject', 'PhabricatorEdgeGraph' => 'AbstractDirectedGraph', 'PhabricatorEdgeObject' => array( 'Phobject', 'PhabricatorPolicyInterface', ), 'PhabricatorEdgeObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorEdgeQuery' => 'PhabricatorQuery', 'PhabricatorEdgeTestCase' => 'PhabricatorTestCase', 'PhabricatorEdgeType' => 'Phobject', 'PhabricatorEdgeTypeTestCase' => 'PhabricatorTestCase', 'PhabricatorEdgesDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', 'PhabricatorEditEngine' => array( 'Phobject', 'PhabricatorPolicyInterface', ), 'PhabricatorEditEngineAPIMethod' => 'ConduitAPIMethod', 'PhabricatorEditEngineBulkJobType' => 'PhabricatorWorkerBulkJobType', 'PhabricatorEditEngineCheckboxesCommentAction' => 'PhabricatorEditEngineCommentAction', 'PhabricatorEditEngineColumnsCommentAction' => 'PhabricatorEditEngineCommentAction', 'PhabricatorEditEngineCommentAction' => 'Phobject', 'PhabricatorEditEngineCommentActionGroup' => 'Phobject', 'PhabricatorEditEngineConfiguration' => array( 'PhabricatorSearchDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', ), 'PhabricatorEditEngineConfigurationDefaultCreateController' => 'PhabricatorEditEngineController', 'PhabricatorEditEngineConfigurationDefaultsController' => 'PhabricatorEditEngineController', 'PhabricatorEditEngineConfigurationDisableController' => 'PhabricatorEditEngineController', 'PhabricatorEditEngineConfigurationEditController' => 'PhabricatorEditEngineController', 'PhabricatorEditEngineConfigurationEditEngine' => 'PhabricatorEditEngine', 'PhabricatorEditEngineConfigurationEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorEditEngineConfigurationIsEditController' => 'PhabricatorEditEngineController', 'PhabricatorEditEngineConfigurationListController' => 'PhabricatorEditEngineController', 'PhabricatorEditEngineConfigurationLockController' => 'PhabricatorEditEngineController', 'PhabricatorEditEngineConfigurationPHIDType' => 'PhabricatorPHIDType', 'PhabricatorEditEngineConfigurationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorEditEngineConfigurationReorderController' => 'PhabricatorEditEngineController', 'PhabricatorEditEngineConfigurationSaveController' => 'PhabricatorEditEngineController', 'PhabricatorEditEngineConfigurationSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorEditEngineConfigurationSortController' => 'PhabricatorEditEngineController', 'PhabricatorEditEngineConfigurationSubtypeController' => 'PhabricatorEditEngineController', 'PhabricatorEditEngineConfigurationTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorEditEngineConfigurationTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorEditEngineConfigurationViewController' => 'PhabricatorEditEngineController', 'PhabricatorEditEngineController' => 'PhabricatorApplicationTransactionController', 'PhabricatorEditEngineDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorEditEngineDefaultLock' => 'PhabricatorEditEngineLock', 'PhabricatorEditEngineExtension' => 'Phobject', 'PhabricatorEditEngineExtensionModule' => 'PhabricatorConfigModule', 'PhabricatorEditEngineListController' => 'PhabricatorEditEngineController', 'PhabricatorEditEngineLock' => 'Phobject', 'PhabricatorEditEnginePointsCommentAction' => 'PhabricatorEditEngineCommentAction', 'PhabricatorEditEngineProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorEditEngineQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorEditEngineSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorEditEngineSelectCommentAction' => 'PhabricatorEditEngineCommentAction', 'PhabricatorEditEngineSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorEditEngineStaticCommentAction' => 'PhabricatorEditEngineCommentAction', 'PhabricatorEditEngineSubtype' => 'Phobject', 'PhabricatorEditEngineSubtypeTestCase' => 'PhabricatorTestCase', 'PhabricatorEditEngineTokenizerCommentAction' => 'PhabricatorEditEngineCommentAction', 'PhabricatorEditField' => 'Phobject', 'PhabricatorEditPage' => 'Phobject', 'PhabricatorEditType' => 'Phobject', 'PhabricatorEditor' => 'Phobject', 'PhabricatorEditorMailEngineExtension' => 'PhabricatorMailEngineExtension', 'PhabricatorEditorMultipleSetting' => 'PhabricatorSelectSetting', 'PhabricatorEditorSetting' => 'PhabricatorStringSetting', 'PhabricatorElasticFulltextStorageEngine' => 'PhabricatorFulltextStorageEngine', 'PhabricatorElasticsearchHost' => 'PhabricatorSearchHost', 'PhabricatorElasticsearchSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorEmailAddressesSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorEmailContentSource' => 'PhabricatorContentSource', 'PhabricatorEmailDeliverySettingsPanel' => 'PhabricatorEditEngineSettingsPanel', 'PhabricatorEmailFormatSetting' => 'PhabricatorSelectSetting', 'PhabricatorEmailFormatSettingsPanel' => 'PhabricatorEditEngineSettingsPanel', 'PhabricatorEmailLoginController' => 'PhabricatorAuthController', 'PhabricatorEmailNotificationsSetting' => 'PhabricatorSelectSetting', 'PhabricatorEmailPreferencesSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorEmailRePrefixSetting' => 'PhabricatorSelectSetting', 'PhabricatorEmailSelfActionsSetting' => 'PhabricatorSelectSetting', 'PhabricatorEmailStampsSetting' => 'PhabricatorSelectSetting', 'PhabricatorEmailTagsSetting' => 'PhabricatorInternalSetting', 'PhabricatorEmailVarySubjectsSetting' => 'PhabricatorSelectSetting', 'PhabricatorEmailVerificationController' => 'PhabricatorAuthController', 'PhabricatorEmbedFileRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PhabricatorEmojiDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorEmojiRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorEmojiTranslation' => 'PhutilTranslation', 'PhabricatorEmptyQueryException' => 'Exception', 'PhabricatorEnumConfigType' => 'PhabricatorTextConfigType', 'PhabricatorEnv' => 'Phobject', 'PhabricatorEnvTestCase' => 'PhabricatorTestCase', 'PhabricatorEpochEditField' => 'PhabricatorEditField', 'PhabricatorEpochExportField' => 'PhabricatorExportField', 'PhabricatorEvent' => 'PhutilEvent', 'PhabricatorEventEngine' => 'Phobject', 'PhabricatorEventListener' => 'PhutilEventListener', 'PhabricatorEventType' => 'PhutilEventType', 'PhabricatorExampleEventListener' => 'PhabricatorEventListener', 'PhabricatorExcelExportFormat' => 'PhabricatorExportFormat', 'PhabricatorExecFutureFileUploadSource' => 'PhabricatorFileUploadSource', 'PhabricatorExportEngine' => 'Phobject', 'PhabricatorExportEngineBulkJobType' => 'PhabricatorWorkerSingleBulkJobType', 'PhabricatorExportEngineExtension' => 'Phobject', 'PhabricatorExportField' => 'Phobject', 'PhabricatorExportFormat' => 'Phobject', 'PhabricatorExportFormatSetting' => 'PhabricatorInternalSetting', 'PhabricatorExtendingPhabricatorConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorExtensionsSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorExternalAccount' => array( 'PhabricatorUserDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorExternalAccountQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorExternalAccountsSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorExtraConfigSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorFacebookAuthProvider' => 'PhabricatorOAuth2AuthProvider', 'PhabricatorFact' => 'Phobject', 'PhabricatorFactAggregate' => 'PhabricatorFactDAO', 'PhabricatorFactApplication' => 'PhabricatorApplication', 'PhabricatorFactChartController' => 'PhabricatorFactController', 'PhabricatorFactController' => 'PhabricatorController', 'PhabricatorFactCursor' => 'PhabricatorFactDAO', 'PhabricatorFactDAO' => 'PhabricatorLiskDAO', 'PhabricatorFactDaemon' => 'PhabricatorDaemon', 'PhabricatorFactDatapointQuery' => 'Phobject', 'PhabricatorFactDimension' => 'PhabricatorFactDAO', 'PhabricatorFactEngine' => 'Phobject', 'PhabricatorFactEngineTestCase' => 'PhabricatorTestCase', 'PhabricatorFactHomeController' => 'PhabricatorFactController', 'PhabricatorFactIntDatapoint' => 'PhabricatorFactDAO', 'PhabricatorFactKeyDimension' => 'PhabricatorFactDimension', 'PhabricatorFactManagementAnalyzeWorkflow' => 'PhabricatorFactManagementWorkflow', 'PhabricatorFactManagementCursorsWorkflow' => 'PhabricatorFactManagementWorkflow', 'PhabricatorFactManagementDestroyWorkflow' => 'PhabricatorFactManagementWorkflow', 'PhabricatorFactManagementListWorkflow' => 'PhabricatorFactManagementWorkflow', 'PhabricatorFactManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorFactObjectController' => 'PhabricatorFactController', 'PhabricatorFactObjectDimension' => 'PhabricatorFactDimension', 'PhabricatorFactRaw' => 'PhabricatorFactDAO', 'PhabricatorFactUpdateIterator' => 'PhutilBufferedIterator', 'PhabricatorFaviconRef' => 'Phobject', 'PhabricatorFaviconRefQuery' => 'Phobject', 'PhabricatorFavoritesApplication' => 'PhabricatorApplication', 'PhabricatorFavoritesController' => 'PhabricatorController', 'PhabricatorFavoritesMainMenuBarExtension' => 'PhabricatorMainMenuBarExtension', 'PhabricatorFavoritesMenuItemController' => 'PhabricatorFavoritesController', 'PhabricatorFavoritesProfileMenuEngine' => 'PhabricatorProfileMenuEngine', 'PhabricatorFaxContentSource' => 'PhabricatorContentSource', 'PhabricatorFeedApplication' => 'PhabricatorApplication', 'PhabricatorFeedBuilder' => 'Phobject', 'PhabricatorFeedConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorFeedController' => 'PhabricatorController', 'PhabricatorFeedDAO' => 'PhabricatorLiskDAO', 'PhabricatorFeedDetailController' => 'PhabricatorFeedController', 'PhabricatorFeedListController' => 'PhabricatorFeedController', 'PhabricatorFeedManagementRepublishWorkflow' => 'PhabricatorFeedManagementWorkflow', 'PhabricatorFeedManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorFeedQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorFeedSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorFeedStory' => array( 'Phobject', 'PhabricatorPolicyInterface', 'PhabricatorMarkupInterface', ), 'PhabricatorFeedStoryData' => array( 'PhabricatorFeedDAO', 'PhabricatorDestructibleInterface', ), 'PhabricatorFeedStoryNotification' => 'PhabricatorFeedDAO', 'PhabricatorFeedStoryPublisher' => 'Phobject', 'PhabricatorFeedStoryReference' => 'PhabricatorFeedDAO', 'PhabricatorFerretEngine' => 'Phobject', 'PhabricatorFerretEngineTestCase' => 'PhabricatorTestCase', 'PhabricatorFerretFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension', 'PhabricatorFerretFulltextStorageEngine' => 'PhabricatorFulltextStorageEngine', 'PhabricatorFerretMetadata' => 'Phobject', 'PhabricatorFerretSearchEngineExtension' => 'PhabricatorSearchEngineExtension', 'PhabricatorFile' => array( 'PhabricatorFileDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorSubscribableInterface', 'PhabricatorFlaggableInterface', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', 'PhabricatorConduitResultInterface', 'PhabricatorIndexableInterface', 'PhabricatorNgramsInterface', ), 'PhabricatorFileAES256StorageFormat' => 'PhabricatorFileStorageFormat', 'PhabricatorFileBundleLoader' => 'Phobject', 'PhabricatorFileChunk' => array( 'PhabricatorFileDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', ), 'PhabricatorFileChunkIterator' => array( 'Phobject', 'Iterator', ), 'PhabricatorFileChunkQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorFileComposeController' => 'PhabricatorFileController', 'PhabricatorFileController' => 'PhabricatorController', 'PhabricatorFileDAO' => 'PhabricatorLiskDAO', 'PhabricatorFileDataController' => 'PhabricatorFileController', 'PhabricatorFileDeleteController' => 'PhabricatorFileController', 'PhabricatorFileDeleteTransaction' => 'PhabricatorFileTransactionType', 'PhabricatorFileDocumentController' => 'PhabricatorFileController', 'PhabricatorFileDocumentRenderingEngine' => 'PhabricatorDocumentRenderingEngine', 'PhabricatorFileDropUploadController' => 'PhabricatorFileController', 'PhabricatorFileEditController' => 'PhabricatorFileController', 'PhabricatorFileEditEngine' => 'PhabricatorEditEngine', 'PhabricatorFileEditField' => 'PhabricatorEditField', 'PhabricatorFileEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorFileExternalRequest' => array( 'PhabricatorFileDAO', 'PhabricatorDestructibleInterface', ), 'PhabricatorFileExternalRequestGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorFileFilePHIDType' => 'PhabricatorPHIDType', 'PhabricatorFileHasObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorFileIconSetSelectController' => 'PhabricatorFileController', 'PhabricatorFileImageMacro' => array( 'PhabricatorFileDAO', 'PhabricatorSubscribableInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorFlaggableInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorPolicyInterface', ), 'PhabricatorFileImageProxyController' => 'PhabricatorFileController', 'PhabricatorFileImageTransform' => 'PhabricatorFileTransform', 'PhabricatorFileIntegrityException' => 'Exception', 'PhabricatorFileLightboxController' => 'PhabricatorFileController', 'PhabricatorFileLinkView' => 'AphrontTagView', 'PhabricatorFileListController' => 'PhabricatorFileController', 'PhabricatorFileNameNgrams' => 'PhabricatorSearchNgrams', 'PhabricatorFileNameTransaction' => 'PhabricatorFileTransactionType', 'PhabricatorFileQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorFileROT13StorageFormat' => 'PhabricatorFileStorageFormat', 'PhabricatorFileRawStorageFormat' => 'PhabricatorFileStorageFormat', 'PhabricatorFileSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorFileSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'PhabricatorFileSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorFileStorageBlob' => 'PhabricatorFileDAO', 'PhabricatorFileStorageConfigurationException' => 'Exception', 'PhabricatorFileStorageEngine' => 'Phobject', 'PhabricatorFileStorageEngineTestCase' => 'PhabricatorTestCase', 'PhabricatorFileStorageFormat' => 'Phobject', 'PhabricatorFileStorageFormatTestCase' => 'PhabricatorTestCase', 'PhabricatorFileTemporaryGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorFileTestCase' => 'PhabricatorTestCase', 'PhabricatorFileTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorFileThumbnailTransform' => 'PhabricatorFileImageTransform', 'PhabricatorFileTransaction' => 'PhabricatorModularTransaction', 'PhabricatorFileTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PhabricatorFileTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorFileTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorFileTransform' => 'Phobject', 'PhabricatorFileTransformController' => 'PhabricatorFileController', 'PhabricatorFileTransformListController' => 'PhabricatorFileController', 'PhabricatorFileTransformTestCase' => 'PhabricatorTestCase', 'PhabricatorFileUploadController' => 'PhabricatorFileController', 'PhabricatorFileUploadDialogController' => 'PhabricatorFileController', 'PhabricatorFileUploadException' => 'Exception', 'PhabricatorFileUploadSource' => 'Phobject', 'PhabricatorFileUploadSourceByteLimitException' => 'Exception', 'PhabricatorFileViewController' => 'PhabricatorFileController', 'PhabricatorFileinfoSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorFilesApplication' => 'PhabricatorApplication', 'PhabricatorFilesApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel', 'PhabricatorFilesBuiltinFile' => 'Phobject', 'PhabricatorFilesComposeAvatarBuiltinFile' => 'PhabricatorFilesBuiltinFile', 'PhabricatorFilesComposeIconBuiltinFile' => 'PhabricatorFilesBuiltinFile', 'PhabricatorFilesConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorFilesManagementCatWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementCompactWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementCycleWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementEncodeWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementEnginesWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementGenerateKeyWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementIntegrityWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementMigrateWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementRebuildWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorFilesOnDiskBuiltinFile' => 'PhabricatorFilesBuiltinFile', 'PhabricatorFilesOutboundRequestAction' => 'PhabricatorSystemAction', 'PhabricatorFiletreeVisibleSetting' => 'PhabricatorInternalSetting', 'PhabricatorFiletreeWidthSetting' => 'PhabricatorInternalSetting', 'PhabricatorFlag' => array( 'PhabricatorFlagDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorFlagAddFlagHeraldAction' => 'HeraldAction', 'PhabricatorFlagColor' => 'PhabricatorFlagConstants', 'PhabricatorFlagConstants' => 'Phobject', 'PhabricatorFlagController' => 'PhabricatorController', 'PhabricatorFlagDAO' => 'PhabricatorLiskDAO', 'PhabricatorFlagDeleteController' => 'PhabricatorFlagController', 'PhabricatorFlagDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', 'PhabricatorFlagEditController' => 'PhabricatorFlagController', 'PhabricatorFlagListController' => 'PhabricatorFlagController', 'PhabricatorFlagQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorFlagSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorFlagSelectControl' => 'AphrontFormControl', 'PhabricatorFlaggableInterface' => 'PhabricatorPHIDInterface', 'PhabricatorFlagsApplication' => 'PhabricatorApplication', 'PhabricatorFlagsUIEventListener' => 'PhabricatorEventListener', 'PhabricatorFulltextEngine' => 'Phobject', 'PhabricatorFulltextEngineExtension' => 'Phobject', 'PhabricatorFulltextEngineExtensionModule' => 'PhabricatorConfigModule', 'PhabricatorFulltextIndexEngineExtension' => 'PhabricatorIndexEngineExtension', 'PhabricatorFulltextInterface' => 'PhabricatorIndexableInterface', 'PhabricatorFulltextResultSet' => 'Phobject', 'PhabricatorFulltextStorageEngine' => 'Phobject', 'PhabricatorFulltextToken' => 'Phobject', 'PhabricatorFundApplication' => 'PhabricatorApplication', 'PhabricatorGDSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorGarbageCollector' => 'Phobject', 'PhabricatorGarbageCollectorManagementCollectWorkflow' => 'PhabricatorGarbageCollectorManagementWorkflow', 'PhabricatorGarbageCollectorManagementCompactEdgesWorkflow' => 'PhabricatorGarbageCollectorManagementWorkflow', 'PhabricatorGarbageCollectorManagementSetPolicyWorkflow' => 'PhabricatorGarbageCollectorManagementWorkflow', 'PhabricatorGarbageCollectorManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorGeneralCachePurger' => 'PhabricatorCachePurger', 'PhabricatorGestureUIExample' => 'PhabricatorUIExample', 'PhabricatorGitGraphStream' => 'PhabricatorRepositoryGraphStream', 'PhabricatorGitHubAuthProvider' => 'PhabricatorOAuth2AuthProvider', 'PhabricatorGlobalLock' => 'PhutilLock', 'PhabricatorGlobalUploadTargetView' => 'AphrontView', 'PhabricatorGoogleAuthProvider' => 'PhabricatorOAuth2AuthProvider', 'PhabricatorGuidanceContext' => 'Phobject', 'PhabricatorGuidanceEngine' => 'Phobject', 'PhabricatorGuidanceEngineExtension' => 'Phobject', 'PhabricatorGuidanceMessage' => 'Phobject', 'PhabricatorGuideApplication' => 'PhabricatorApplication', 'PhabricatorGuideController' => 'PhabricatorController', 'PhabricatorGuideInstallModule' => 'PhabricatorGuideModule', 'PhabricatorGuideItemView' => 'Phobject', 'PhabricatorGuideListView' => 'AphrontView', 'PhabricatorGuideModule' => 'Phobject', 'PhabricatorGuideModuleController' => 'PhabricatorGuideController', 'PhabricatorGuideQuickStartModule' => 'PhabricatorGuideModule', 'PhabricatorHMACTestCase' => 'PhabricatorTestCase', 'PhabricatorHTTPParameterTypeTableView' => 'AphrontView', 'PhabricatorHandleList' => array( 'Phobject', 'Iterator', 'ArrayAccess', 'Countable', ), 'PhabricatorHandleObjectSelectorDataView' => 'Phobject', 'PhabricatorHandlePool' => 'Phobject', 'PhabricatorHandlePoolTestCase' => 'PhabricatorTestCase', 'PhabricatorHandleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorHandleRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorHandlesEditField' => 'PhabricatorPHIDListEditField', 'PhabricatorHarbormasterApplication' => 'PhabricatorApplication', 'PhabricatorHash' => 'Phobject', 'PhabricatorHashTestCase' => 'PhabricatorTestCase', 'PhabricatorHelpApplication' => 'PhabricatorApplication', 'PhabricatorHelpController' => 'PhabricatorController', 'PhabricatorHelpDocumentationController' => 'PhabricatorHelpController', 'PhabricatorHelpEditorProtocolController' => 'PhabricatorHelpController', 'PhabricatorHelpKeyboardShortcutController' => 'PhabricatorHelpController', 'PhabricatorHeraldApplication' => 'PhabricatorApplication', 'PhabricatorHeraldContentSource' => 'PhabricatorContentSource', 'PhabricatorHexdumpDocumentEngine' => 'PhabricatorDocumentEngine', 'PhabricatorHighSecurityRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler', 'PhabricatorHomeApplication' => 'PhabricatorApplication', 'PhabricatorHomeConstants' => 'PhabricatorHomeController', 'PhabricatorHomeController' => 'PhabricatorController', 'PhabricatorHomeLauncherProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorHomeMenuItemController' => 'PhabricatorHomeController', 'PhabricatorHomeProfileMenuEngine' => 'PhabricatorProfileMenuEngine', 'PhabricatorHomeProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorHovercardEngineExtension' => 'Phobject', 'PhabricatorHovercardEngineExtensionModule' => 'PhabricatorConfigModule', 'PhabricatorIDExportField' => 'PhabricatorExportField', 'PhabricatorIDsSearchEngineExtension' => 'PhabricatorSearchEngineExtension', 'PhabricatorIDsSearchField' => 'PhabricatorSearchField', 'PhabricatorIconDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorIconRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorIconSet' => 'Phobject', 'PhabricatorIconSetEditField' => 'PhabricatorEditField', 'PhabricatorIconSetIcon' => 'Phobject', 'PhabricatorImageDocumentEngine' => 'PhabricatorDocumentEngine', 'PhabricatorImageMacroRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorImageRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorImageTransformer' => 'Phobject', 'PhabricatorImagemagickSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorInFlightErrorView' => 'AphrontView', 'PhabricatorIndexEngine' => 'Phobject', 'PhabricatorIndexEngineExtension' => 'Phobject', 'PhabricatorIndexEngineExtensionModule' => 'PhabricatorConfigModule', 'PhabricatorInfrastructureTestCase' => 'PhabricatorTestCase', 'PhabricatorInlineCommentController' => 'PhabricatorController', 'PhabricatorInlineCommentInterface' => 'PhabricatorMarkupInterface', 'PhabricatorInlineCommentPreviewController' => 'PhabricatorController', 'PhabricatorInlineSummaryView' => 'AphrontView', 'PhabricatorInstructionsEditField' => 'PhabricatorEditField', 'PhabricatorIntConfigType' => 'PhabricatorTextConfigType', 'PhabricatorIntEditField' => 'PhabricatorEditField', 'PhabricatorIntExportField' => 'PhabricatorExportField', 'PhabricatorInternalSetting' => 'PhabricatorSetting', 'PhabricatorInternationalizationManagementExtractWorkflow' => 'PhabricatorInternationalizationManagementWorkflow', 'PhabricatorInternationalizationManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorInvalidConfigSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorIteratedMD5PasswordHasher' => 'PhabricatorPasswordHasher', 'PhabricatorIteratedMD5PasswordHasherTestCase' => 'PhabricatorTestCase', 'PhabricatorIteratorFileUploadSource' => 'PhabricatorFileUploadSource', 'PhabricatorJIRAAuthProvider' => 'PhabricatorOAuth1AuthProvider', 'PhabricatorJSONConfigType' => 'PhabricatorTextConfigType', 'PhabricatorJSONDocumentEngine' => 'PhabricatorTextDocumentEngine', 'PhabricatorJSONExportFormat' => 'PhabricatorExportFormat', 'PhabricatorJavelinLinter' => 'ArcanistLinter', 'PhabricatorJiraIssueHasObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorJupyterDocumentEngine' => 'PhabricatorDocumentEngine', 'PhabricatorKeyValueDatabaseCache' => 'PhutilKeyValueCache', 'PhabricatorKeyValueSerializingCacheProxy' => 'PhutilKeyValueCacheProxy', 'PhabricatorKeyboardRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorKeyring' => 'Phobject', 'PhabricatorKeyringConfigOptionType' => 'PhabricatorConfigJSONOptionType', 'PhabricatorLDAPAuthProvider' => 'PhabricatorAuthProvider', 'PhabricatorLabelProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorLegalpadApplication' => 'PhabricatorApplication', 'PhabricatorLegalpadConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorLegalpadDocumentPHIDType' => 'PhabricatorPHIDType', 'PhabricatorLegalpadSignaturePolicyRule' => 'PhabricatorPolicyRule', 'PhabricatorLibraryTestCase' => 'PhutilLibraryTestCase', 'PhabricatorLinkProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorLipsumArtist' => 'Phobject', 'PhabricatorLipsumContentSource' => 'PhabricatorContentSource', 'PhabricatorLipsumGenerateWorkflow' => 'PhabricatorLipsumManagementWorkflow', 'PhabricatorLipsumManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorLipsumMondrianArtist' => 'PhabricatorLipsumArtist', 'PhabricatorLiskDAO' => 'LiskDAO', 'PhabricatorLiskExportEngineExtension' => 'PhabricatorExportEngineExtension', 'PhabricatorLiskFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension', 'PhabricatorLiskSearchEngineExtension' => 'PhabricatorSearchEngineExtension', 'PhabricatorLiskSerializer' => 'Phobject', 'PhabricatorListExportField' => 'PhabricatorExportField', 'PhabricatorLocalDiskFileStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorLocalTimeTestCase' => 'PhabricatorTestCase', 'PhabricatorLocaleScopeGuard' => 'Phobject', 'PhabricatorLocaleScopeGuardTestCase' => 'PhabricatorTestCase', 'PhabricatorLockLogManagementWorkflow' => 'PhabricatorLockManagementWorkflow', 'PhabricatorLockManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorLogTriggerAction' => 'PhabricatorTriggerAction', 'PhabricatorLogoutController' => 'PhabricatorAuthController', 'PhabricatorLunarPhasePolicyRule' => 'PhabricatorPolicyRule', 'PhabricatorMacroApplication' => 'PhabricatorApplication', 'PhabricatorMacroAudioBehaviorTransaction' => 'PhabricatorMacroTransactionType', 'PhabricatorMacroAudioController' => 'PhabricatorMacroController', 'PhabricatorMacroAudioTransaction' => 'PhabricatorMacroTransactionType', 'PhabricatorMacroConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorMacroController' => 'PhabricatorController', 'PhabricatorMacroDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorMacroDisableController' => 'PhabricatorMacroController', 'PhabricatorMacroDisabledTransaction' => 'PhabricatorMacroTransactionType', 'PhabricatorMacroEditController' => 'PhameBlogController', 'PhabricatorMacroEditEngine' => 'PhabricatorEditEngine', 'PhabricatorMacroEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorMacroFileTransaction' => 'PhabricatorMacroTransactionType', 'PhabricatorMacroListController' => 'PhabricatorMacroController', 'PhabricatorMacroMacroPHIDType' => 'PhabricatorPHIDType', 'PhabricatorMacroMailReceiver' => 'PhabricatorObjectMailReceiver', 'PhabricatorMacroManageCapability' => 'PhabricatorPolicyCapability', 'PhabricatorMacroMemeController' => 'PhabricatorMacroController', 'PhabricatorMacroMemeDialogController' => 'PhabricatorMacroController', 'PhabricatorMacroNameTransaction' => 'PhabricatorMacroTransactionType', 'PhabricatorMacroQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorMacroReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PhabricatorMacroSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorMacroTestCase' => 'PhabricatorTestCase', 'PhabricatorMacroTransaction' => 'PhabricatorModularTransaction', 'PhabricatorMacroTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PhabricatorMacroTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorMacroTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorMacroViewController' => 'PhabricatorMacroController', 'PhabricatorMailConfigTestCase' => 'PhabricatorTestCase', 'PhabricatorMailEmailHeraldField' => 'HeraldField', 'PhabricatorMailEmailHeraldFieldGroup' => 'HeraldFieldGroup', 'PhabricatorMailEmailSubjectHeraldField' => 'PhabricatorMailEmailHeraldField', 'PhabricatorMailEngineExtension' => 'Phobject', 'PhabricatorMailImplementationAdapter' => 'Phobject', 'PhabricatorMailImplementationAmazonSESAdapter' => 'PhabricatorMailImplementationPHPMailerLiteAdapter', 'PhabricatorMailImplementationMailgunAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailImplementationPHPMailerAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailImplementationPostmarkAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailImplementationSendGridAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailImplementationTestAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailManagementListInboundWorkflow' => 'PhabricatorMailManagementWorkflow', 'PhabricatorMailManagementListOutboundWorkflow' => 'PhabricatorMailManagementWorkflow', 'PhabricatorMailManagementReceiveTestWorkflow' => 'PhabricatorMailManagementWorkflow', 'PhabricatorMailManagementResendWorkflow' => 'PhabricatorMailManagementWorkflow', 'PhabricatorMailManagementSendTestWorkflow' => 'PhabricatorMailManagementWorkflow', 'PhabricatorMailManagementShowInboundWorkflow' => 'PhabricatorMailManagementWorkflow', 'PhabricatorMailManagementShowOutboundWorkflow' => 'PhabricatorMailManagementWorkflow', 'PhabricatorMailManagementUnverifyWorkflow' => 'PhabricatorMailManagementWorkflow', 'PhabricatorMailManagementVolumeWorkflow' => 'PhabricatorMailManagementWorkflow', 'PhabricatorMailManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorMailMustEncryptHeraldAction' => 'HeraldAction', 'PhabricatorMailOutboundMailHeraldAdapter' => 'HeraldAdapter', 'PhabricatorMailOutboundRoutingHeraldAction' => 'HeraldAction', 'PhabricatorMailOutboundRoutingSelfEmailHeraldAction' => 'PhabricatorMailOutboundRoutingHeraldAction', 'PhabricatorMailOutboundRoutingSelfNotificationHeraldAction' => 'PhabricatorMailOutboundRoutingHeraldAction', 'PhabricatorMailOutboundStatus' => 'Phobject', 'PhabricatorMailPropertiesDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', 'PhabricatorMailReceiver' => 'Phobject', 'PhabricatorMailReceiverTestCase' => 'PhabricatorTestCase', 'PhabricatorMailReplyHandler' => 'Phobject', 'PhabricatorMailRoutingRule' => 'Phobject', 'PhabricatorMailSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorMailStamp' => 'Phobject', 'PhabricatorMailTarget' => 'Phobject', 'PhabricatorMailgunConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorMainMenuBarExtension' => 'Phobject', 'PhabricatorMainMenuSearchView' => 'AphrontView', 'PhabricatorMainMenuView' => 'AphrontView', 'PhabricatorManageProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorManagementWorkflow' => 'PhutilArgumentWorkflow', 'PhabricatorManiphestApplication' => 'PhabricatorApplication', 'PhabricatorManiphestConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorManiphestTaskFactEngine' => 'PhabricatorTransactionFactEngine', 'PhabricatorManiphestTaskTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorManualActivitySetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorMarkupCache' => 'PhabricatorCacheDAO', 'PhabricatorMarkupEngine' => 'Phobject', 'PhabricatorMarkupEngineTestCase' => 'PhabricatorTestCase', 'PhabricatorMarkupOneOff' => array( 'Phobject', 'PhabricatorMarkupInterface', ), 'PhabricatorMarkupPreviewController' => 'PhabricatorController', 'PhabricatorMemeEngine' => 'Phobject', 'PhabricatorMemeRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorMentionRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorMercurialGraphStream' => 'PhabricatorRepositoryGraphStream', 'PhabricatorMetaMTAActor' => 'Phobject', 'PhabricatorMetaMTAActorQuery' => 'PhabricatorQuery', 'PhabricatorMetaMTAApplication' => 'PhabricatorApplication', 'PhabricatorMetaMTAApplicationEmail' => array( 'PhabricatorMetaMTADAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorDestructibleInterface', 'PhabricatorSpacesInterface', ), 'PhabricatorMetaMTAApplicationEmailDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorMetaMTAApplicationEmailEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorMetaMTAApplicationEmailHeraldField' => 'HeraldField', 'PhabricatorMetaMTAApplicationEmailPHIDType' => 'PhabricatorPHIDType', 'PhabricatorMetaMTAApplicationEmailPanel' => 'PhabricatorApplicationConfigurationPanel', 'PhabricatorMetaMTAApplicationEmailQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorMetaMTAApplicationEmailTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorMetaMTAApplicationEmailTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorMetaMTAAttachment' => 'Phobject', 'PhabricatorMetaMTAConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorMetaMTAController' => 'PhabricatorController', 'PhabricatorMetaMTADAO' => 'PhabricatorLiskDAO', 'PhabricatorMetaMTAEmailBodyParser' => 'Phobject', 'PhabricatorMetaMTAEmailBodyParserTestCase' => 'PhabricatorTestCase', 'PhabricatorMetaMTAEmailHeraldAction' => 'HeraldAction', 'PhabricatorMetaMTAEmailOthersHeraldAction' => 'PhabricatorMetaMTAEmailHeraldAction', 'PhabricatorMetaMTAEmailSelfHeraldAction' => 'PhabricatorMetaMTAEmailHeraldAction', 'PhabricatorMetaMTAErrorMailAction' => 'PhabricatorSystemAction', 'PhabricatorMetaMTAMail' => array( 'PhabricatorMetaMTADAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', ), 'PhabricatorMetaMTAMailBody' => 'Phobject', 'PhabricatorMetaMTAMailBodyTestCase' => 'PhabricatorTestCase', 'PhabricatorMetaMTAMailHasRecipientEdgeType' => 'PhabricatorEdgeType', 'PhabricatorMetaMTAMailListController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAMailPHIDType' => 'PhabricatorPHIDType', 'PhabricatorMetaMTAMailProperties' => array( 'PhabricatorMetaMTADAO', 'PhabricatorPolicyInterface', ), 'PhabricatorMetaMTAMailPropertiesQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorMetaMTAMailQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorMetaMTAMailSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorMetaMTAMailSection' => 'Phobject', 'PhabricatorMetaMTAMailTestCase' => 'PhabricatorTestCase', 'PhabricatorMetaMTAMailViewController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAMailableDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorMetaMTAMailableFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorMetaMTAMailgunReceiveController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAMemberQuery' => 'PhabricatorQuery', 'PhabricatorMetaMTAPermanentFailureException' => 'Exception', 'PhabricatorMetaMTAPostmarkReceiveController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAReceivedMail' => 'PhabricatorMetaMTADAO', 'PhabricatorMetaMTAReceivedMailProcessingException' => 'Exception', 'PhabricatorMetaMTAReceivedMailTestCase' => 'PhabricatorTestCase', 'PhabricatorMetaMTASchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorMetaMTASendGridReceiveController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAWorker' => 'PhabricatorWorker', 'PhabricatorMetronomicTriggerClock' => 'PhabricatorTriggerClock', 'PhabricatorModularTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorModularTransactionType' => 'Phobject', 'PhabricatorMonogramDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension', 'PhabricatorMonospacedFontSetting' => 'PhabricatorStringSetting', 'PhabricatorMonospacedTextareasSetting' => 'PhabricatorSelectSetting', 'PhabricatorMotivatorProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorMultiColumnUIExample' => 'PhabricatorUIExample', 'PhabricatorMultiFactorSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorMultimeterApplication' => 'PhabricatorApplication', 'PhabricatorMustVerifyEmailController' => 'PhabricatorAuthController', 'PhabricatorMutedByEdgeType' => 'PhabricatorEdgeType', 'PhabricatorMutedEdgeType' => 'PhabricatorEdgeType', 'PhabricatorMySQLConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorMySQLFileStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorMySQLSearchHost' => 'PhabricatorSearchHost', 'PhabricatorMySQLSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorNamedQuery' => array( 'PhabricatorSearchDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorNamedQueryConfig' => array( 'PhabricatorSearchDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorNamedQueryConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorNamedQueryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorNavigationRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorNeverTriggerClock' => 'PhabricatorTriggerClock', 'PhabricatorNgramsIndexEngineExtension' => 'PhabricatorIndexEngineExtension', 'PhabricatorNgramsInterface' => 'PhabricatorIndexableInterface', 'PhabricatorNotificationBuilder' => 'Phobject', 'PhabricatorNotificationClearController' => 'PhabricatorNotificationController', 'PhabricatorNotificationClient' => 'Phobject', 'PhabricatorNotificationConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorNotificationController' => 'PhabricatorController', 'PhabricatorNotificationDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', 'PhabricatorNotificationIndividualController' => 'PhabricatorNotificationController', 'PhabricatorNotificationListController' => 'PhabricatorNotificationController', 'PhabricatorNotificationPanelController' => 'PhabricatorNotificationController', 'PhabricatorNotificationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorNotificationSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorNotificationServerRef' => 'Phobject', 'PhabricatorNotificationServersConfigType' => 'PhabricatorJSONConfigType', 'PhabricatorNotificationStatusView' => 'AphrontTagView', 'PhabricatorNotificationTestController' => 'PhabricatorNotificationController', 'PhabricatorNotificationTestFeedStory' => 'PhabricatorFeedStory', 'PhabricatorNotificationUIExample' => 'PhabricatorUIExample', 'PhabricatorNotificationsApplication' => 'PhabricatorApplication', 'PhabricatorNotificationsSetting' => 'PhabricatorInternalSetting', 'PhabricatorNotificationsSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorNuanceApplication' => 'PhabricatorApplication', 'PhabricatorOAuth1AuthProvider' => 'PhabricatorOAuthAuthProvider', 'PhabricatorOAuth1SecretTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType', 'PhabricatorOAuth2AuthProvider' => 'PhabricatorOAuthAuthProvider', 'PhabricatorOAuthAuthProvider' => 'PhabricatorAuthProvider', 'PhabricatorOAuthClientAuthorization' => array( 'PhabricatorOAuthServerDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorOAuthClientAuthorizationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorOAuthClientController' => 'PhabricatorOAuthServerController', 'PhabricatorOAuthClientDisableController' => 'PhabricatorOAuthClientController', 'PhabricatorOAuthClientEditController' => 'PhabricatorOAuthClientController', 'PhabricatorOAuthClientListController' => 'PhabricatorOAuthClientController', 'PhabricatorOAuthClientSecretController' => 'PhabricatorOAuthClientController', 'PhabricatorOAuthClientTestController' => 'PhabricatorOAuthClientController', 'PhabricatorOAuthClientViewController' => 'PhabricatorOAuthClientController', 'PhabricatorOAuthResponse' => 'AphrontResponse', 'PhabricatorOAuthServer' => 'Phobject', 'PhabricatorOAuthServerAccessToken' => 'PhabricatorOAuthServerDAO', 'PhabricatorOAuthServerApplication' => 'PhabricatorApplication', 'PhabricatorOAuthServerAuthController' => 'PhabricatorOAuthServerController', 'PhabricatorOAuthServerAuthorizationCode' => 'PhabricatorOAuthServerDAO', 'PhabricatorOAuthServerAuthorizationsSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorOAuthServerClient' => array( 'PhabricatorOAuthServerDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorDestructibleInterface', ), 'PhabricatorOAuthServerClientAuthorizationPHIDType' => 'PhabricatorPHIDType', 'PhabricatorOAuthServerClientPHIDType' => 'PhabricatorPHIDType', 'PhabricatorOAuthServerClientQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorOAuthServerClientSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorOAuthServerController' => 'PhabricatorController', 'PhabricatorOAuthServerCreateClientsCapability' => 'PhabricatorPolicyCapability', 'PhabricatorOAuthServerDAO' => 'PhabricatorLiskDAO', 'PhabricatorOAuthServerEditEngine' => 'PhabricatorEditEngine', 'PhabricatorOAuthServerEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorOAuthServerSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorOAuthServerScope' => 'Phobject', 'PhabricatorOAuthServerTestCase' => 'PhabricatorTestCase', 'PhabricatorOAuthServerTokenController' => 'PhabricatorOAuthServerController', 'PhabricatorOAuthServerTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorOAuthServerTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorObjectGraph' => 'AbstractDirectedGraph', 'PhabricatorObjectHandle' => array( 'Phobject', 'PhabricatorPolicyInterface', ), 'PhabricatorObjectHasAsanaSubtaskEdgeType' => 'PhabricatorEdgeType', 'PhabricatorObjectHasAsanaTaskEdgeType' => 'PhabricatorEdgeType', 'PhabricatorObjectHasContributorEdgeType' => 'PhabricatorEdgeType', 'PhabricatorObjectHasDraftEdgeType' => 'PhabricatorEdgeType', 'PhabricatorObjectHasFileEdgeType' => 'PhabricatorEdgeType', 'PhabricatorObjectHasJiraIssueEdgeType' => 'PhabricatorEdgeType', 'PhabricatorObjectHasSubscriberEdgeType' => 'PhabricatorEdgeType', 'PhabricatorObjectHasUnsubscriberEdgeType' => 'PhabricatorEdgeType', 'PhabricatorObjectHasWatcherEdgeType' => 'PhabricatorEdgeType', 'PhabricatorObjectListQuery' => 'Phobject', 'PhabricatorObjectListQueryTestCase' => 'PhabricatorTestCase', 'PhabricatorObjectMailReceiver' => 'PhabricatorMailReceiver', 'PhabricatorObjectMailReceiverTestCase' => 'PhabricatorTestCase', 'PhabricatorObjectMentionedByObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorObjectMentionsObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorObjectRelationship' => 'Phobject', 'PhabricatorObjectRelationshipList' => 'Phobject', 'PhabricatorObjectRelationshipSource' => 'Phobject', 'PhabricatorObjectRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorObjectSelectorDialog' => 'Phobject', 'PhabricatorObjectStatus' => 'Phobject', 'PhabricatorOffsetPagedQuery' => 'PhabricatorQuery', 'PhabricatorOldWorldContentSource' => 'PhabricatorContentSource', 'PhabricatorOlderInlinesSetting' => 'PhabricatorSelectSetting', 'PhabricatorOneTimeTriggerClock' => 'PhabricatorTriggerClock', 'PhabricatorOpcodeCacheSpec' => 'PhabricatorCacheSpec', 'PhabricatorOptionGroupSetting' => 'PhabricatorSetting', 'PhabricatorOwnerPathQuery' => 'Phobject', 'PhabricatorOwnersApplication' => 'PhabricatorApplication', 'PhabricatorOwnersArchiveController' => 'PhabricatorOwnersController', 'PhabricatorOwnersConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorOwnersConfiguredCustomField' => array( 'PhabricatorOwnersCustomField', 'PhabricatorStandardCustomFieldInterface', ), 'PhabricatorOwnersController' => 'PhabricatorController', 'PhabricatorOwnersCustomField' => 'PhabricatorCustomField', 'PhabricatorOwnersCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage', 'PhabricatorOwnersCustomFieldStorage' => 'PhabricatorCustomFieldStorage', 'PhabricatorOwnersCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage', 'PhabricatorOwnersDAO' => 'PhabricatorLiskDAO', 'PhabricatorOwnersDefaultEditCapability' => 'PhabricatorPolicyCapability', 'PhabricatorOwnersDefaultViewCapability' => 'PhabricatorPolicyCapability', 'PhabricatorOwnersDetailController' => 'PhabricatorOwnersController', 'PhabricatorOwnersEditController' => 'PhabricatorOwnersController', 'PhabricatorOwnersHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension', 'PhabricatorOwnersListController' => 'PhabricatorOwnersController', 'PhabricatorOwnersOwner' => 'PhabricatorOwnersDAO', 'PhabricatorOwnersPackage' => array( 'PhabricatorOwnersDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorCustomFieldInterface', 'PhabricatorDestructibleInterface', 'PhabricatorConduitResultInterface', 'PhabricatorFulltextInterface', 'PhabricatorFerretInterface', 'PhabricatorNgramsInterface', ), 'PhabricatorOwnersPackageAuditingTransaction' => 'PhabricatorOwnersPackageTransactionType', 'PhabricatorOwnersPackageAutoreviewTransaction' => 'PhabricatorOwnersPackageTransactionType', 'PhabricatorOwnersPackageContextFreeGrammar' => 'PhutilContextFreeGrammar', 'PhabricatorOwnersPackageDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorOwnersPackageDescriptionTransaction' => 'PhabricatorOwnersPackageTransactionType', 'PhabricatorOwnersPackageDominionTransaction' => 'PhabricatorOwnersPackageTransactionType', 'PhabricatorOwnersPackageEditEngine' => 'PhabricatorEditEngine', 'PhabricatorOwnersPackageFerretEngine' => 'PhabricatorFerretEngine', 'PhabricatorOwnersPackageFulltextEngine' => 'PhabricatorFulltextEngine', 'PhabricatorOwnersPackageFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorOwnersPackageIgnoredTransaction' => 'PhabricatorOwnersPackageTransactionType', 'PhabricatorOwnersPackageNameNgrams' => 'PhabricatorSearchNgrams', 'PhabricatorOwnersPackageNameTransaction' => 'PhabricatorOwnersPackageTransactionType', 'PhabricatorOwnersPackageOwnerDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorOwnersPackageOwnersTransaction' => 'PhabricatorOwnersPackageTransactionType', 'PhabricatorOwnersPackagePHIDType' => 'PhabricatorPHIDType', 'PhabricatorOwnersPackagePathsTransaction' => 'PhabricatorOwnersPackageTransactionType', 'PhabricatorOwnersPackagePrimaryTransaction' => 'PhabricatorOwnersPackageTransactionType', 'PhabricatorOwnersPackageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorOwnersPackageRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PhabricatorOwnersPackageSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorOwnersPackageStatusTransaction' => 'PhabricatorOwnersPackageTransactionType', 'PhabricatorOwnersPackageTestCase' => 'PhabricatorTestCase', 'PhabricatorOwnersPackageTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorOwnersPackageTransaction' => 'PhabricatorModularTransaction', 'PhabricatorOwnersPackageTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorOwnersPackageTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorOwnersPackageTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorOwnersPath' => 'PhabricatorOwnersDAO', 'PhabricatorOwnersPathContextFreeGrammar' => 'PhutilContextFreeGrammar', 'PhabricatorOwnersPathsController' => 'PhabricatorOwnersController', 'PhabricatorOwnersPathsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'PhabricatorOwnersSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorOwnersSearchField' => 'PhabricatorSearchTokenizerField', 'PhabricatorPDFDocumentEngine' => 'PhabricatorDocumentEngine', 'PhabricatorPHDConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPHID' => 'Phobject', 'PhabricatorPHIDConstants' => 'Phobject', 'PhabricatorPHIDExportField' => 'PhabricatorExportField', 'PhabricatorPHIDListEditField' => 'PhabricatorEditField', 'PhabricatorPHIDListEditType' => 'PhabricatorEditType', 'PhabricatorPHIDListExportField' => 'PhabricatorListExportField', 'PhabricatorPHIDMailStamp' => 'PhabricatorMailStamp', 'PhabricatorPHIDResolver' => 'Phobject', 'PhabricatorPHIDType' => 'Phobject', 'PhabricatorPHIDTypeTestCase' => 'PhutilTestCase', 'PhabricatorPHIDsSearchField' => 'PhabricatorSearchField', 'PhabricatorPHPASTApplication' => 'PhabricatorApplication', 'PhabricatorPHPConfigSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorPHPMailerConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPHPPreflightSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorPackagesApplication' => 'PhabricatorApplication', 'PhabricatorPackagesController' => 'PhabricatorController', 'PhabricatorPackagesCreatePublisherCapability' => 'PhabricatorPolicyCapability', 'PhabricatorPackagesDAO' => 'PhabricatorLiskDAO', 'PhabricatorPackagesEditEngine' => 'PhabricatorEditEngine', 'PhabricatorPackagesEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorPackagesNgrams' => 'PhabricatorSearchNgrams', 'PhabricatorPackagesPackage' => array( 'PhabricatorPackagesDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorDestructibleInterface', 'PhabricatorSubscribableInterface', 'PhabricatorProjectInterface', 'PhabricatorConduitResultInterface', 'PhabricatorNgramsInterface', ), 'PhabricatorPackagesPackageController' => 'PhabricatorPackagesController', 'PhabricatorPackagesPackageDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorPackagesPackageDefaultEditCapability' => 'PhabricatorPolicyCapability', 'PhabricatorPackagesPackageDefaultViewCapability' => 'PhabricatorPolicyCapability', 'PhabricatorPackagesPackageEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'PhabricatorPackagesPackageEditController' => 'PhabricatorPackagesPackageController', 'PhabricatorPackagesPackageEditEngine' => 'PhabricatorPackagesEditEngine', 'PhabricatorPackagesPackageEditor' => 'PhabricatorPackagesEditor', 'PhabricatorPackagesPackageKeyTransaction' => 'PhabricatorPackagesPackageTransactionType', 'PhabricatorPackagesPackageListController' => 'PhabricatorPackagesPackageController', 'PhabricatorPackagesPackageListView' => 'PhabricatorPackagesView', 'PhabricatorPackagesPackageNameNgrams' => 'PhabricatorPackagesNgrams', 'PhabricatorPackagesPackageNameTransaction' => 'PhabricatorPackagesPackageTransactionType', 'PhabricatorPackagesPackagePHIDType' => 'PhabricatorPHIDType', 'PhabricatorPackagesPackagePublisherTransaction' => 'PhabricatorPackagesPackageTransactionType', 'PhabricatorPackagesPackageQuery' => 'PhabricatorPackagesQuery', 'PhabricatorPackagesPackageSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'PhabricatorPackagesPackageSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorPackagesPackageTransaction' => 'PhabricatorModularTransaction', 'PhabricatorPackagesPackageTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorPackagesPackageTransactionType' => 'PhabricatorPackagesTransactionType', 'PhabricatorPackagesPackageViewController' => 'PhabricatorPackagesPackageController', 'PhabricatorPackagesPublisher' => array( 'PhabricatorPackagesDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorDestructibleInterface', 'PhabricatorSubscribableInterface', 'PhabricatorProjectInterface', 'PhabricatorConduitResultInterface', 'PhabricatorNgramsInterface', ), 'PhabricatorPackagesPublisherController' => 'PhabricatorPackagesController', 'PhabricatorPackagesPublisherDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorPackagesPublisherDefaultEditCapability' => 'PhabricatorPolicyCapability', 'PhabricatorPackagesPublisherEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'PhabricatorPackagesPublisherEditController' => 'PhabricatorPackagesPublisherController', 'PhabricatorPackagesPublisherEditEngine' => 'PhabricatorPackagesEditEngine', 'PhabricatorPackagesPublisherEditor' => 'PhabricatorPackagesEditor', 'PhabricatorPackagesPublisherKeyTransaction' => 'PhabricatorPackagesPublisherTransactionType', 'PhabricatorPackagesPublisherListController' => 'PhabricatorPackagesPublisherController', 'PhabricatorPackagesPublisherListView' => 'PhabricatorPackagesView', 'PhabricatorPackagesPublisherNameNgrams' => 'PhabricatorPackagesNgrams', 'PhabricatorPackagesPublisherNameTransaction' => 'PhabricatorPackagesPublisherTransactionType', 'PhabricatorPackagesPublisherPHIDType' => 'PhabricatorPHIDType', 'PhabricatorPackagesPublisherQuery' => 'PhabricatorPackagesQuery', 'PhabricatorPackagesPublisherSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'PhabricatorPackagesPublisherSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorPackagesPublisherTransaction' => 'PhabricatorModularTransaction', 'PhabricatorPackagesPublisherTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorPackagesPublisherTransactionType' => 'PhabricatorPackagesTransactionType', 'PhabricatorPackagesPublisherViewController' => 'PhabricatorPackagesPublisherController', 'PhabricatorPackagesQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorPackagesSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorPackagesTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorPackagesVersion' => array( 'PhabricatorPackagesDAO', 'PhabricatorPolicyInterface', 'PhabricatorExtendedPolicyInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorDestructibleInterface', 'PhabricatorSubscribableInterface', 'PhabricatorProjectInterface', 'PhabricatorConduitResultInterface', 'PhabricatorNgramsInterface', ), 'PhabricatorPackagesVersionController' => 'PhabricatorPackagesController', 'PhabricatorPackagesVersionEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'PhabricatorPackagesVersionEditController' => 'PhabricatorPackagesVersionController', 'PhabricatorPackagesVersionEditEngine' => 'PhabricatorPackagesEditEngine', 'PhabricatorPackagesVersionEditor' => 'PhabricatorPackagesEditor', 'PhabricatorPackagesVersionListController' => 'PhabricatorPackagesVersionController', 'PhabricatorPackagesVersionListView' => 'PhabricatorPackagesView', 'PhabricatorPackagesVersionNameNgrams' => 'PhabricatorPackagesNgrams', 'PhabricatorPackagesVersionNameTransaction' => 'PhabricatorPackagesVersionTransactionType', 'PhabricatorPackagesVersionPHIDType' => 'PhabricatorPHIDType', 'PhabricatorPackagesVersionPackageTransaction' => 'PhabricatorPackagesVersionTransactionType', 'PhabricatorPackagesVersionQuery' => 'PhabricatorPackagesQuery', 'PhabricatorPackagesVersionSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'PhabricatorPackagesVersionSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorPackagesVersionTransaction' => 'PhabricatorModularTransaction', 'PhabricatorPackagesVersionTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorPackagesVersionTransactionType' => 'PhabricatorPackagesTransactionType', 'PhabricatorPackagesVersionViewController' => 'PhabricatorPackagesVersionController', 'PhabricatorPackagesView' => 'AphrontView', 'PhabricatorPagerUIExample' => 'PhabricatorUIExample', 'PhabricatorPassphraseApplication' => 'PhabricatorApplication', 'PhabricatorPasswordAuthProvider' => 'PhabricatorAuthProvider', 'PhabricatorPasswordDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', 'PhabricatorPasswordHasher' => 'Phobject', 'PhabricatorPasswordHasherTestCase' => 'PhabricatorTestCase', 'PhabricatorPasswordHasherUnavailableException' => 'Exception', 'PhabricatorPasswordSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorPaste' => array( 'PhabricatorPasteDAO', 'PhabricatorSubscribableInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorFlaggableInterface', 'PhabricatorMentionableInterface', 'PhabricatorPolicyInterface', 'PhabricatorProjectInterface', 'PhabricatorDestructibleInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorSpacesInterface', 'PhabricatorConduitResultInterface', ), 'PhabricatorPasteApplication' => 'PhabricatorApplication', 'PhabricatorPasteArchiveController' => 'PhabricatorPasteController', 'PhabricatorPasteConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPasteContentSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'PhabricatorPasteContentTransaction' => 'PhabricatorPasteTransactionType', 'PhabricatorPasteController' => 'PhabricatorController', 'PhabricatorPasteDAO' => 'PhabricatorLiskDAO', 'PhabricatorPasteEditController' => 'PhabricatorPasteController', 'PhabricatorPasteEditEngine' => 'PhabricatorEditEngine', 'PhabricatorPasteEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorPasteFilenameContextFreeGrammar' => 'PhutilContextFreeGrammar', 'PhabricatorPasteLanguageTransaction' => 'PhabricatorPasteTransactionType', 'PhabricatorPasteListController' => 'PhabricatorPasteController', 'PhabricatorPastePastePHIDType' => 'PhabricatorPHIDType', 'PhabricatorPasteQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorPasteRawController' => 'PhabricatorPasteController', 'PhabricatorPasteRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PhabricatorPasteSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorPasteSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorPasteSnippet' => 'Phobject', 'PhabricatorPasteStatusTransaction' => 'PhabricatorPasteTransactionType', 'PhabricatorPasteTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorPasteTitleTransaction' => 'PhabricatorPasteTransactionType', 'PhabricatorPasteTransaction' => 'PhabricatorModularTransaction', 'PhabricatorPasteTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PhabricatorPasteTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorPasteTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorPasteViewController' => 'PhabricatorPasteController', 'PhabricatorPathSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorPeopleAnyOwnerDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorPeopleApplication' => 'PhabricatorApplication', 'PhabricatorPeopleApproveController' => 'PhabricatorPeopleController', 'PhabricatorPeopleBadgesProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorPeopleCommitsProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorPeopleController' => 'PhabricatorController', 'PhabricatorPeopleCreateController' => 'PhabricatorPeopleController', 'PhabricatorPeopleCreateGuidanceContext' => 'PhabricatorGuidanceContext', 'PhabricatorPeopleDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorPeopleDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension', 'PhabricatorPeopleDeleteController' => 'PhabricatorPeopleController', 'PhabricatorPeopleDetailsProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorPeopleDisableController' => 'PhabricatorPeopleController', 'PhabricatorPeopleEmpowerController' => 'PhabricatorPeopleController', 'PhabricatorPeopleExternalPHIDType' => 'PhabricatorPHIDType', 'PhabricatorPeopleIconSet' => 'PhabricatorIconSet', 'PhabricatorPeopleInviteController' => 'PhabricatorPeopleController', 'PhabricatorPeopleInviteListController' => 'PhabricatorPeopleInviteController', 'PhabricatorPeopleInviteSendController' => 'PhabricatorPeopleInviteController', 'PhabricatorPeopleLdapController' => 'PhabricatorPeopleController', 'PhabricatorPeopleListController' => 'PhabricatorPeopleController', 'PhabricatorPeopleLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorPeopleLogSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorPeopleLogsController' => 'PhabricatorPeopleController', 'PhabricatorPeopleManageProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorPeopleManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorPeopleNewController' => 'PhabricatorPeopleController', 'PhabricatorPeopleNoOwnerDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorPeopleOwnerDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorPeoplePictureProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorPeopleProfileBadgesController' => 'PhabricatorPeopleProfileController', 'PhabricatorPeopleProfileCommitsController' => 'PhabricatorPeopleProfileController', 'PhabricatorPeopleProfileController' => 'PhabricatorPeopleController', 'PhabricatorPeopleProfileEditController' => 'PhabricatorPeopleProfileController', 'PhabricatorPeopleProfileImageWorkflow' => 'PhabricatorPeopleManagementWorkflow', 'PhabricatorPeopleProfileManageController' => 'PhabricatorPeopleProfileController', 'PhabricatorPeopleProfileMenuEngine' => 'PhabricatorProfileMenuEngine', 'PhabricatorPeopleProfilePictureController' => 'PhabricatorPeopleProfileController', 'PhabricatorPeopleProfileRevisionsController' => 'PhabricatorPeopleProfileController', 'PhabricatorPeopleProfileTasksController' => 'PhabricatorPeopleProfileController', 'PhabricatorPeopleProfileViewController' => 'PhabricatorPeopleProfileController', 'PhabricatorPeopleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorPeopleRenameController' => 'PhabricatorPeopleController', 'PhabricatorPeopleRevisionsProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorPeopleSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorPeopleTasksProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorPeopleTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorPeopleTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorPeopleUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorPeopleUserPHIDType' => 'PhabricatorPHIDType', 'PhabricatorPeopleWelcomeController' => 'PhabricatorPeopleController', 'PhabricatorPhabricatorAuthProvider' => 'PhabricatorOAuth2AuthProvider', 'PhabricatorPhameApplication' => 'PhabricatorApplication', 'PhabricatorPhameBlogPHIDType' => 'PhabricatorPHIDType', 'PhabricatorPhamePostPHIDType' => 'PhabricatorPHIDType', 'PhabricatorPhluxApplication' => 'PhabricatorApplication', 'PhabricatorPholioApplication' => 'PhabricatorApplication', 'PhabricatorPholioConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPholioMockTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorPhortuneApplication' => 'PhabricatorApplication', 'PhabricatorPhortuneContentSource' => 'PhabricatorContentSource', 'PhabricatorPhortuneManagementInvoiceWorkflow' => 'PhabricatorPhortuneManagementWorkflow', 'PhabricatorPhortuneManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorPhortuneTestCase' => 'PhabricatorTestCase', 'PhabricatorPhragmentApplication' => 'PhabricatorApplication', 'PhabricatorPhrequentApplication' => 'PhabricatorApplication', 'PhabricatorPhrictionApplication' => 'PhabricatorApplication', 'PhabricatorPhrictionConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPhurlApplication' => 'PhabricatorApplication', 'PhabricatorPhurlConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPhurlController' => 'PhabricatorController', 'PhabricatorPhurlDAO' => 'PhabricatorLiskDAO', 'PhabricatorPhurlLinkRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorPhurlRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PhabricatorPhurlSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorPhurlShortURLController' => 'PhabricatorPhurlController', 'PhabricatorPhurlShortURLDefaultController' => 'PhabricatorPhurlController', 'PhabricatorPhurlURL' => array( 'PhabricatorPhurlDAO', 'PhabricatorPolicyInterface', 'PhabricatorProjectInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorSubscribableInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorDestructibleInterface', 'PhabricatorMentionableInterface', 'PhabricatorFlaggableInterface', 'PhabricatorSpacesInterface', 'PhabricatorConduitResultInterface', 'PhabricatorNgramsInterface', ), 'PhabricatorPhurlURLAccessController' => 'PhabricatorPhurlController', 'PhabricatorPhurlURLAliasTransaction' => 'PhabricatorPhurlURLTransactionType', 'PhabricatorPhurlURLCreateCapability' => 'PhabricatorPolicyCapability', 'PhabricatorPhurlURLDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorPhurlURLDescriptionTransaction' => 'PhabricatorPhurlURLTransactionType', 'PhabricatorPhurlURLEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'PhabricatorPhurlURLEditController' => 'PhabricatorPhurlController', 'PhabricatorPhurlURLEditEngine' => 'PhabricatorEditEngine', 'PhabricatorPhurlURLEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorPhurlURLListController' => 'PhabricatorPhurlController', 'PhabricatorPhurlURLLongURLTransaction' => 'PhabricatorPhurlURLTransactionType', 'PhabricatorPhurlURLMailReceiver' => 'PhabricatorObjectMailReceiver', 'PhabricatorPhurlURLNameNgrams' => 'PhabricatorSearchNgrams', 'PhabricatorPhurlURLNameTransaction' => 'PhabricatorPhurlURLTransactionType', 'PhabricatorPhurlURLPHIDType' => 'PhabricatorPHIDType', 'PhabricatorPhurlURLQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorPhurlURLReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PhabricatorPhurlURLSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'PhabricatorPhurlURLSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorPhurlURLTransaction' => 'PhabricatorModularTransaction', 'PhabricatorPhurlURLTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PhabricatorPhurlURLTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorPhurlURLTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorPhurlURLViewController' => 'PhabricatorPhurlController', 'PhabricatorPinnedApplicationsSetting' => 'PhabricatorInternalSetting', 'PhabricatorPirateEnglishTranslation' => 'PhutilTranslation', 'PhabricatorPlatformSite' => 'PhabricatorSite', 'PhabricatorPointsEditField' => 'PhabricatorEditField', 'PhabricatorPointsFact' => 'PhabricatorFact', 'PhabricatorPolicies' => 'PhabricatorPolicyConstants', 'PhabricatorPolicy' => array( 'PhabricatorPolicyDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', ), 'PhabricatorPolicyApplication' => 'PhabricatorApplication', 'PhabricatorPolicyAwareQuery' => 'PhabricatorOffsetPagedQuery', 'PhabricatorPolicyAwareTestQuery' => 'PhabricatorPolicyAwareQuery', 'PhabricatorPolicyCanEditCapability' => 'PhabricatorPolicyCapability', 'PhabricatorPolicyCanInteractCapability' => 'PhabricatorPolicyCapability', 'PhabricatorPolicyCanJoinCapability' => 'PhabricatorPolicyCapability', 'PhabricatorPolicyCanViewCapability' => 'PhabricatorPolicyCapability', 'PhabricatorPolicyCapability' => 'Phobject', 'PhabricatorPolicyCapabilityTestCase' => 'PhabricatorTestCase', 'PhabricatorPolicyCodex' => 'Phobject', 'PhabricatorPolicyCodexRuleDescription' => 'Phobject', 'PhabricatorPolicyConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPolicyConstants' => 'Phobject', 'PhabricatorPolicyController' => 'PhabricatorController', 'PhabricatorPolicyDAO' => 'PhabricatorLiskDAO', 'PhabricatorPolicyDataTestCase' => 'PhabricatorTestCase', 'PhabricatorPolicyEditController' => 'PhabricatorPolicyController', 'PhabricatorPolicyEditEngineExtension' => 'PhabricatorEditEngineExtension', 'PhabricatorPolicyEditField' => 'PhabricatorEditField', 'PhabricatorPolicyException' => 'Exception', 'PhabricatorPolicyExplainController' => 'PhabricatorPolicyController', 'PhabricatorPolicyFavoritesSetting' => 'PhabricatorInternalSetting', 'PhabricatorPolicyFilter' => 'Phobject', 'PhabricatorPolicyInterface' => 'PhabricatorPHIDInterface', 'PhabricatorPolicyManagementShowWorkflow' => 'PhabricatorPolicyManagementWorkflow', 'PhabricatorPolicyManagementUnlockWorkflow' => 'PhabricatorPolicyManagementWorkflow', 'PhabricatorPolicyManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorPolicyPHIDTypePolicy' => 'PhabricatorPHIDType', 'PhabricatorPolicyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorPolicyRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler', 'PhabricatorPolicyRule' => 'Phobject', 'PhabricatorPolicySearchEngineExtension' => 'PhabricatorSearchEngineExtension', 'PhabricatorPolicyStrengthConstants' => 'PhabricatorPolicyConstants', 'PhabricatorPolicyTestCase' => 'PhabricatorTestCase', 'PhabricatorPolicyTestObject' => array( 'Phobject', 'PhabricatorPolicyInterface', 'PhabricatorExtendedPolicyInterface', ), 'PhabricatorPolicyType' => 'PhabricatorPolicyConstants', 'PhabricatorPonderApplication' => 'PhabricatorApplication', 'PhabricatorProfileMenuEditEngine' => 'PhabricatorEditEngine', 'PhabricatorProfileMenuEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorProfileMenuEngine' => 'Phobject', 'PhabricatorProfileMenuItem' => 'Phobject', 'PhabricatorProfileMenuItemConfiguration' => array( 'PhabricatorSearchDAO', 'PhabricatorPolicyInterface', 'PhabricatorExtendedPolicyInterface', 'PhabricatorApplicationTransactionInterface', ), 'PhabricatorProfileMenuItemConfigurationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorProfileMenuItemConfigurationTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorProfileMenuItemConfigurationTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorProfileMenuItemIconSet' => 'PhabricatorIconSet', 'PhabricatorProfileMenuItemPHIDType' => 'PhabricatorPHIDType', 'PhabricatorProject' => array( 'PhabricatorProjectDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorFlaggableInterface', 'PhabricatorPolicyInterface', 'PhabricatorExtendedPolicyInterface', 'PhabricatorCustomFieldInterface', 'PhabricatorDestructibleInterface', 'PhabricatorFulltextInterface', 'PhabricatorFerretInterface', 'PhabricatorConduitResultInterface', 'PhabricatorColumnProxyInterface', 'PhabricatorSpacesInterface', ), 'PhabricatorProjectAddHeraldAction' => 'PhabricatorProjectHeraldAction', 'PhabricatorProjectApplication' => 'PhabricatorApplication', 'PhabricatorProjectArchiveController' => 'PhabricatorProjectController', 'PhabricatorProjectBoardBackgroundController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardController' => 'PhabricatorProjectController', 'PhabricatorProjectBoardDisableController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardImportController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardManageController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardReorderController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardViewController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBuiltinsExample' => 'PhabricatorUIExample', 'PhabricatorProjectCardView' => 'AphrontTagView', 'PhabricatorProjectColorTransaction' => 'PhabricatorProjectTransactionType', 'PhabricatorProjectColorsConfigType' => 'PhabricatorJSONConfigType', 'PhabricatorProjectColumn' => array( 'PhabricatorProjectDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', 'PhabricatorExtendedPolicyInterface', 'PhabricatorConduitResultInterface', ), 'PhabricatorProjectColumnDetailController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectColumnEditController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectColumnHideController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectColumnPHIDType' => 'PhabricatorPHIDType', 'PhabricatorProjectColumnPosition' => array( 'PhabricatorProjectDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorProjectColumnPositionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorProjectColumnQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorProjectColumnSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorProjectColumnTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorProjectColumnTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorProjectColumnTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorProjectConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorProjectConfiguredCustomField' => array( 'PhabricatorProjectStandardCustomField', 'PhabricatorStandardCustomFieldInterface', ), 'PhabricatorProjectController' => 'PhabricatorController', 'PhabricatorProjectCoreTestCase' => 'PhabricatorTestCase', 'PhabricatorProjectCoverController' => 'PhabricatorProjectController', 'PhabricatorProjectCustomField' => 'PhabricatorCustomField', 'PhabricatorProjectCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage', 'PhabricatorProjectCustomFieldStorage' => 'PhabricatorCustomFieldStorage', 'PhabricatorProjectCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage', 'PhabricatorProjectDAO' => 'PhabricatorLiskDAO', 'PhabricatorProjectDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorProjectDefaultController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectDescriptionField' => 'PhabricatorProjectStandardCustomField', 'PhabricatorProjectDetailsProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorProjectEditController' => 'PhabricatorProjectController', 'PhabricatorProjectEditEngine' => 'PhabricatorEditEngine', 'PhabricatorProjectEditPictureController' => 'PhabricatorProjectController', 'PhabricatorProjectFerretEngine' => 'PhabricatorFerretEngine', 'PhabricatorProjectFilterTransaction' => 'PhabricatorProjectTransactionType', 'PhabricatorProjectFulltextEngine' => 'PhabricatorFulltextEngine', 'PhabricatorProjectHeraldAction' => 'HeraldAction', 'PhabricatorProjectHeraldAdapter' => 'HeraldAdapter', 'PhabricatorProjectHeraldFieldGroup' => 'HeraldFieldGroup', 'PhabricatorProjectHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension', 'PhabricatorProjectIconSet' => 'PhabricatorIconSet', 'PhabricatorProjectIconTransaction' => 'PhabricatorProjectTransactionType', 'PhabricatorProjectIconsConfigType' => 'PhabricatorJSONConfigType', 'PhabricatorProjectImageTransaction' => 'PhabricatorProjectTransactionType', 'PhabricatorProjectListController' => 'PhabricatorProjectController', 'PhabricatorProjectListView' => 'AphrontView', 'PhabricatorProjectLockController' => 'PhabricatorProjectController', 'PhabricatorProjectLockTransaction' => 'PhabricatorProjectTransactionType', 'PhabricatorProjectLogicalAncestorDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectLogicalDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectLogicalOnlyDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorProjectLogicalOrNotDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectLogicalUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectLogicalViewerDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorProjectManageController' => 'PhabricatorProjectController', 'PhabricatorProjectManageProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorProjectMaterializedMemberEdgeType' => 'PhabricatorEdgeType', 'PhabricatorProjectMemberListView' => 'PhabricatorProjectUserListView', 'PhabricatorProjectMemberOfProjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorProjectMembersAddController' => 'PhabricatorProjectController', 'PhabricatorProjectMembersDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectMembersPolicyRule' => 'PhabricatorPolicyRule', 'PhabricatorProjectMembersProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorProjectMembersRemoveController' => 'PhabricatorProjectController', 'PhabricatorProjectMembersViewController' => 'PhabricatorProjectController', 'PhabricatorProjectMenuItemController' => 'PhabricatorProjectController', 'PhabricatorProjectMilestoneTransaction' => 'PhabricatorProjectTypeTransaction', 'PhabricatorProjectMoveController' => 'PhabricatorProjectController', 'PhabricatorProjectNameContextFreeGrammar' => 'PhutilContextFreeGrammar', 'PhabricatorProjectNameTransaction' => 'PhabricatorProjectTransactionType', 'PhabricatorProjectNoProjectsDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorProjectObjectHasProjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorProjectOrUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectOrUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectPHIDResolver' => 'PhabricatorPHIDResolver', 'PhabricatorProjectParentTransaction' => 'PhabricatorProjectTypeTransaction', 'PhabricatorProjectPictureProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorProjectPointsProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorProjectProfileController' => 'PhabricatorProjectController', 'PhabricatorProjectProfileMenuEngine' => 'PhabricatorProfileMenuEngine', 'PhabricatorProjectProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorProjectProjectHasMemberEdgeType' => 'PhabricatorEdgeType', 'PhabricatorProjectProjectHasObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorProjectProjectPHIDType' => 'PhabricatorPHIDType', 'PhabricatorProjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorProjectRemoveHeraldAction' => 'PhabricatorProjectHeraldAction', 'PhabricatorProjectSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorProjectSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorProjectSearchField' => 'PhabricatorSearchTokenizerField', 'PhabricatorProjectSilenceController' => 'PhabricatorProjectController', 'PhabricatorProjectSilencedEdgeType' => 'PhabricatorEdgeType', 'PhabricatorProjectSlug' => 'PhabricatorProjectDAO', 'PhabricatorProjectSlugsTransaction' => 'PhabricatorProjectTransactionType', 'PhabricatorProjectSortTransaction' => 'PhabricatorProjectTransactionType', 'PhabricatorProjectStandardCustomField' => array( 'PhabricatorProjectCustomField', 'PhabricatorStandardCustomFieldInterface', ), 'PhabricatorProjectStatus' => 'Phobject', 'PhabricatorProjectStatusTransaction' => 'PhabricatorProjectTransactionType', 'PhabricatorProjectSubprojectWarningController' => 'PhabricatorProjectController', 'PhabricatorProjectSubprojectsController' => 'PhabricatorProjectController', 'PhabricatorProjectSubprojectsProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorProjectTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorProjectTransaction' => 'PhabricatorModularTransaction', 'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorProjectTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorProjectTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorProjectTypeTransaction' => 'PhabricatorProjectTransactionType', 'PhabricatorProjectUIEventListener' => 'PhabricatorEventListener', 'PhabricatorProjectUpdateController' => 'PhabricatorProjectController', 'PhabricatorProjectUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectUserListView' => 'AphrontView', 'PhabricatorProjectViewController' => 'PhabricatorProjectController', 'PhabricatorProjectWatchController' => 'PhabricatorProjectController', 'PhabricatorProjectWatcherListView' => 'PhabricatorProjectUserListView', 'PhabricatorProjectWorkboardBackgroundColor' => 'Phobject', 'PhabricatorProjectWorkboardBackgroundTransaction' => 'PhabricatorProjectTransactionType', 'PhabricatorProjectWorkboardProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorProjectWorkboardTransaction' => 'PhabricatorProjectTransactionType', 'PhabricatorProjectsAllPolicyRule' => 'PhabricatorProjectsBasePolicyRule', 'PhabricatorProjectsAncestorsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'PhabricatorProjectsBasePolicyRule' => 'PhabricatorPolicyRule', 'PhabricatorProjectsCurtainExtension' => 'PHUICurtainExtension', 'PhabricatorProjectsEditEngineExtension' => 'PhabricatorEditEngineExtension', 'PhabricatorProjectsEditField' => 'PhabricatorTokenizerEditField', 'PhabricatorProjectsExportEngineExtension' => 'PhabricatorExportEngineExtension', 'PhabricatorProjectsFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension', 'PhabricatorProjectsMailEngineExtension' => 'PhabricatorMailEngineExtension', 'PhabricatorProjectsMembersSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'PhabricatorProjectsMembershipIndexEngineExtension' => 'PhabricatorIndexEngineExtension', 'PhabricatorProjectsPolicyRule' => 'PhabricatorProjectsBasePolicyRule', 'PhabricatorProjectsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'PhabricatorProjectsSearchEngineExtension' => 'PhabricatorSearchEngineExtension', 'PhabricatorProjectsWatchersSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'PhabricatorPronounSetting' => 'PhabricatorSelectSetting', 'PhabricatorPygmentSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorQuery' => 'Phobject', 'PhabricatorQueryConstraint' => 'Phobject', 'PhabricatorQueryIterator' => 'PhutilBufferedIterator', 'PhabricatorQueryOrderItem' => 'Phobject', 'PhabricatorQueryOrderTestCase' => 'PhabricatorTestCase', 'PhabricatorQueryOrderVector' => array( 'Phobject', 'Iterator', ), 'PhabricatorQuickSearchEngineExtension' => 'PhabricatorDatasourceEngineExtension', 'PhabricatorRateLimitRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler', 'PhabricatorRecaptchaConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorRedirectController' => 'PhabricatorController', 'PhabricatorRefreshCSRFController' => 'PhabricatorAuthController', 'PhabricatorRegexListConfigType' => 'PhabricatorTextListConfigType', 'PhabricatorRegistrationProfile' => 'Phobject', 'PhabricatorReleephApplication' => 'PhabricatorApplication', 'PhabricatorReleephApplicationConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorRemarkupCachePurger' => 'PhabricatorCachePurger', 'PhabricatorRemarkupControl' => 'AphrontFormTextAreaControl', 'PhabricatorRemarkupCowsayBlockInterpreter' => 'PhutilRemarkupBlockInterpreter', 'PhabricatorRemarkupCustomBlockRule' => 'PhutilRemarkupBlockRule', 'PhabricatorRemarkupCustomInlineRule' => 'PhutilRemarkupRule', 'PhabricatorRemarkupDocumentEngine' => 'PhabricatorDocumentEngine', 'PhabricatorRemarkupEditField' => 'PhabricatorEditField', 'PhabricatorRemarkupFigletBlockInterpreter' => 'PhutilRemarkupBlockInterpreter', 'PhabricatorRemarkupUIExample' => 'PhabricatorUIExample', 'PhabricatorRepositoriesSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorRepository' => array( 'PhabricatorRepositoryDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorFlaggableInterface', 'PhabricatorMarkupInterface', 'PhabricatorDestructibleInterface', 'PhabricatorDestructibleCodexInterface', 'PhabricatorProjectInterface', 'PhabricatorSpacesInterface', 'PhabricatorConduitResultInterface', 'PhabricatorFulltextInterface', 'PhabricatorFerretInterface', ), + 'PhabricatorRepositoryActivateTransaction' => 'PhabricatorRepositoryTransactionType', 'PhabricatorRepositoryAuditRequest' => array( 'PhabricatorRepositoryDAO', 'PhabricatorPolicyInterface', ), + 'PhabricatorRepositoryAutocloseOnlyTransaction' => 'PhabricatorRepositoryTransactionType', + 'PhabricatorRepositoryAutocloseTransaction' => 'PhabricatorRepositoryTransactionType', + 'PhabricatorRepositoryBlueprintsTransaction' => 'PhabricatorRepositoryTransactionType', 'PhabricatorRepositoryBranch' => 'PhabricatorRepositoryDAO', + 'PhabricatorRepositoryCallsignTransaction' => 'PhabricatorRepositoryTransactionType', 'PhabricatorRepositoryCommit' => array( 'PhabricatorRepositoryDAO', 'PhabricatorPolicyInterface', 'PhabricatorFlaggableInterface', 'PhabricatorProjectInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorSubscribableInterface', 'PhabricatorMentionableInterface', 'HarbormasterBuildableInterface', 'HarbormasterCircleCIBuildableInterface', 'HarbormasterBuildkiteBuildableInterface', 'PhabricatorCustomFieldInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorFulltextInterface', 'PhabricatorFerretInterface', 'PhabricatorConduitResultInterface', 'PhabricatorDraftInterface', ), 'PhabricatorRepositoryCommitChangeParserWorker' => 'PhabricatorRepositoryCommitParserWorker', 'PhabricatorRepositoryCommitData' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositoryCommitHeraldWorker' => 'PhabricatorRepositoryCommitParserWorker', 'PhabricatorRepositoryCommitHint' => array( 'PhabricatorRepositoryDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorRepositoryCommitMessageParserWorker' => 'PhabricatorRepositoryCommitParserWorker', 'PhabricatorRepositoryCommitOwnersWorker' => 'PhabricatorRepositoryCommitParserWorker', 'PhabricatorRepositoryCommitPHIDType' => 'PhabricatorPHIDType', 'PhabricatorRepositoryCommitParserWorker' => 'PhabricatorWorker', 'PhabricatorRepositoryCommitRef' => 'Phobject', 'PhabricatorRepositoryCommitTestCase' => 'PhabricatorTestCase', 'PhabricatorRepositoryConfigOptions' => 'PhabricatorApplicationConfigOptions', + 'PhabricatorRepositoryCopyTimeLimitTransaction' => 'PhabricatorRepositoryTransactionType', 'PhabricatorRepositoryDAO' => 'PhabricatorLiskDAO', + 'PhabricatorRepositoryDangerousTransaction' => 'PhabricatorRepositoryTransactionType', + 'PhabricatorRepositoryDefaultBranchTransaction' => 'PhabricatorRepositoryTransactionType', + 'PhabricatorRepositoryDescriptionTransaction' => 'PhabricatorRepositoryTransactionType', 'PhabricatorRepositoryDestructibleCodex' => 'PhabricatorDestructibleCodex', 'PhabricatorRepositoryDiscoveryEngine' => 'PhabricatorRepositoryEngine', 'PhabricatorRepositoryEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhabricatorRepositoryEncodingTransaction' => 'PhabricatorRepositoryTransactionType', 'PhabricatorRepositoryEngine' => 'Phobject', + 'PhabricatorRepositoryEnormousTransaction' => 'PhabricatorRepositoryTransactionType', 'PhabricatorRepositoryFerretEngine' => 'PhabricatorFerretEngine', + 'PhabricatorRepositoryFilesizeLimitTransaction' => 'PhabricatorRepositoryTransactionType', 'PhabricatorRepositoryFulltextEngine' => 'PhabricatorFulltextEngine', 'PhabricatorRepositoryGitCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker', 'PhabricatorRepositoryGitCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker', 'PhabricatorRepositoryGitLFSRef' => array( 'PhabricatorRepositoryDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', ), 'PhabricatorRepositoryGitLFSRefQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorRepositoryGraphCache' => 'Phobject', 'PhabricatorRepositoryGraphStream' => 'Phobject', 'PhabricatorRepositoryIdentity' => array( 'PhabricatorRepositoryDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', ), 'PhabricatorRepositoryIdentityAssignTransaction' => 'PhabricatorRepositoryIdentityTransactionType', 'PhabricatorRepositoryIdentityChangeWorker' => 'PhabricatorWorker', 'PhabricatorRepositoryIdentityEditEngine' => 'PhabricatorEditEngine', 'PhabricatorRepositoryIdentityFerretEngine' => 'PhabricatorFerretEngine', 'PhabricatorRepositoryIdentityPHIDType' => 'PhabricatorPHIDType', 'PhabricatorRepositoryIdentityQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorRepositoryIdentityTransaction' => 'PhabricatorModularTransaction', 'PhabricatorRepositoryIdentityTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorRepositoryIdentityTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorRepositoryManagementCacheWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementClusterizeWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementDiscoverWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementHintWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementImportingWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementListPathsWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementListWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementLookupUsersWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementMarkImportedWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementMarkReachableWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementMirrorWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementMovePathsWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementParentsWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementPullWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementRebuildIdentitiesWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementRefsWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementReparseWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementThawWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementUnpublishWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementUpdateWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorRepositoryMercurialCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker', 'PhabricatorRepositoryMercurialCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker', 'PhabricatorRepositoryMirror' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositoryMirrorEngine' => 'PhabricatorRepositoryEngine', + 'PhabricatorRepositoryNameTransaction' => 'PhabricatorRepositoryTransactionType', + 'PhabricatorRepositoryNotifyTransaction' => 'PhabricatorRepositoryTransactionType', 'PhabricatorRepositoryOldRef' => array( 'PhabricatorRepositoryDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorRepositoryParsedChange' => 'Phobject', 'PhabricatorRepositoryPullEngine' => 'PhabricatorRepositoryEngine', 'PhabricatorRepositoryPullEvent' => array( 'PhabricatorRepositoryDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorRepositoryPullEventPHIDType' => 'PhabricatorPHIDType', 'PhabricatorRepositoryPullEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorRepositoryPullLocalDaemon' => 'PhabricatorDaemon', 'PhabricatorRepositoryPullLocalDaemonModule' => 'PhutilDaemonOverseerModule', 'PhabricatorRepositoryPushEvent' => array( 'PhabricatorRepositoryDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorRepositoryPushEventPHIDType' => 'PhabricatorPHIDType', 'PhabricatorRepositoryPushEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorRepositoryPushLog' => array( 'PhabricatorRepositoryDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorRepositoryPushLogPHIDType' => 'PhabricatorPHIDType', 'PhabricatorRepositoryPushLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorRepositoryPushLogSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorRepositoryPushMailWorker' => 'PhabricatorWorker', + 'PhabricatorRepositoryPushPolicyTransaction' => 'PhabricatorRepositoryTransactionType', 'PhabricatorRepositoryPushReplyHandler' => 'PhabricatorMailReplyHandler', 'PhabricatorRepositoryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorRepositoryRefCursor' => array( 'PhabricatorRepositoryDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorRepositoryRefCursorPHIDType' => 'PhabricatorPHIDType', 'PhabricatorRepositoryRefCursorQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorRepositoryRefEngine' => 'PhabricatorRepositoryEngine', 'PhabricatorRepositoryRefPosition' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositoryRepositoryPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorRepositorySVNSubpathTransaction' => 'PhabricatorRepositoryTransactionType', 'PhabricatorRepositorySchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorRepositorySearchEngine' => 'PhabricatorApplicationSearchEngine', + 'PhabricatorRepositoryServiceTransaction' => 'PhabricatorRepositoryTransactionType', + 'PhabricatorRepositorySlugTransaction' => 'PhabricatorRepositoryTransactionType', + 'PhabricatorRepositoryStagingURITransaction' => 'PhabricatorRepositoryTransactionType', 'PhabricatorRepositoryStatusMessage' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositorySvnCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker', 'PhabricatorRepositorySvnCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker', 'PhabricatorRepositorySymbol' => 'PhabricatorRepositoryDAO', + 'PhabricatorRepositorySymbolLanguagesTransaction' => 'PhabricatorRepositoryTransactionType', + 'PhabricatorRepositorySymbolSourcesTransaction' => 'PhabricatorRepositoryTransactionType', 'PhabricatorRepositorySyncEvent' => array( 'PhabricatorRepositoryDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorRepositorySyncEventPHIDType' => 'PhabricatorPHIDType', 'PhabricatorRepositorySyncEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorRepositoryTestCase' => 'PhabricatorTestCase', - 'PhabricatorRepositoryTransaction' => 'PhabricatorApplicationTransaction', + 'PhabricatorRepositoryTouchLimitTransaction' => 'PhabricatorRepositoryTransactionType', + 'PhabricatorRepositoryTrackOnlyTransaction' => 'PhabricatorRepositoryTransactionType', + 'PhabricatorRepositoryTransaction' => 'PhabricatorModularTransaction', 'PhabricatorRepositoryTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorRepositoryTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorRepositoryType' => 'Phobject', 'PhabricatorRepositoryURI' => array( 'PhabricatorRepositoryDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorExtendedPolicyInterface', 'PhabricatorConduitResultInterface', ), 'PhabricatorRepositoryURIIndex' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositoryURINormalizer' => 'Phobject', 'PhabricatorRepositoryURINormalizerTestCase' => 'PhabricatorTestCase', 'PhabricatorRepositoryURIPHIDType' => 'PhabricatorPHIDType', 'PhabricatorRepositoryURIQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorRepositoryURITestCase' => 'PhabricatorTestCase', 'PhabricatorRepositoryURITransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorRepositoryURITransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorRepositoryVCSTransaction' => 'PhabricatorRepositoryTransactionType', 'PhabricatorRepositoryWorkingCopyVersion' => 'PhabricatorRepositoryDAO', 'PhabricatorRequestExceptionHandler' => 'AphrontRequestExceptionHandler', 'PhabricatorResourceSite' => 'PhabricatorSite', 'PhabricatorRobotsController' => 'PhabricatorController', 'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorSMS' => 'PhabricatorSMSDAO', 'PhabricatorSMSConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorSMSDAO' => 'PhabricatorLiskDAO', 'PhabricatorSMSDemultiplexWorker' => 'PhabricatorSMSWorker', 'PhabricatorSMSImplementationAdapter' => 'Phobject', 'PhabricatorSMSImplementationTestBlackholeAdapter' => 'PhabricatorSMSImplementationAdapter', 'PhabricatorSMSImplementationTwilioAdapter' => 'PhabricatorSMSImplementationAdapter', 'PhabricatorSMSManagementListOutboundWorkflow' => 'PhabricatorSMSManagementWorkflow', 'PhabricatorSMSManagementSendTestWorkflow' => 'PhabricatorSMSManagementWorkflow', 'PhabricatorSMSManagementShowOutboundWorkflow' => 'PhabricatorSMSManagementWorkflow', 'PhabricatorSMSManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorSMSSendWorker' => 'PhabricatorSMSWorker', 'PhabricatorSMSWorker' => 'PhabricatorWorker', 'PhabricatorSQLPatchList' => 'Phobject', 'PhabricatorSSHKeyGenerator' => 'Phobject', 'PhabricatorSSHKeysSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorSSHLog' => 'Phobject', 'PhabricatorSSHPassthruCommand' => 'Phobject', 'PhabricatorSSHWorkflow' => 'PhutilArgumentWorkflow', 'PhabricatorSavedQuery' => array( 'PhabricatorSearchDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorSavedQueryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorScheduleTaskTriggerAction' => 'PhabricatorTriggerAction', 'PhabricatorScopedEnv' => 'Phobject', 'PhabricatorSearchAbstractDocument' => 'Phobject', 'PhabricatorSearchApplication' => 'PhabricatorApplication', 'PhabricatorSearchApplicationSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorSearchApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel', 'PhabricatorSearchBaseController' => 'PhabricatorController', 'PhabricatorSearchCheckboxesField' => 'PhabricatorSearchField', 'PhabricatorSearchConstraintException' => 'Exception', 'PhabricatorSearchController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchCustomFieldProxyField' => 'PhabricatorSearchField', 'PhabricatorSearchDAO' => 'PhabricatorLiskDAO', 'PhabricatorSearchDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorSearchDatasourceField' => 'PhabricatorSearchTokenizerField', 'PhabricatorSearchDateControlField' => 'PhabricatorSearchField', 'PhabricatorSearchDateField' => 'PhabricatorSearchField', 'PhabricatorSearchDefaultController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchDeleteController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchDocument' => 'PhabricatorSearchDAO', 'PhabricatorSearchDocumentField' => 'PhabricatorSearchDAO', 'PhabricatorSearchDocumentFieldType' => 'Phobject', 'PhabricatorSearchDocumentQuery' => 'PhabricatorPolicyAwareQuery', 'PhabricatorSearchDocumentRelationship' => 'PhabricatorSearchDAO', 'PhabricatorSearchDocumentTypeDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorSearchEditController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchEngineAPIMethod' => 'ConduitAPIMethod', 'PhabricatorSearchEngineAttachment' => 'Phobject', 'PhabricatorSearchEngineExtension' => 'Phobject', 'PhabricatorSearchEngineExtensionModule' => 'PhabricatorConfigModule', 'PhabricatorSearchFerretNgramGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorSearchField' => 'Phobject', 'PhabricatorSearchHost' => 'Phobject', 'PhabricatorSearchHovercardController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchIndexVersion' => 'PhabricatorSearchDAO', 'PhabricatorSearchIndexVersionDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', 'PhabricatorSearchManagementIndexWorkflow' => 'PhabricatorSearchManagementWorkflow', 'PhabricatorSearchManagementInitWorkflow' => 'PhabricatorSearchManagementWorkflow', 'PhabricatorSearchManagementNgramsWorkflow' => 'PhabricatorSearchManagementWorkflow', 'PhabricatorSearchManagementQueryWorkflow' => 'PhabricatorSearchManagementWorkflow', 'PhabricatorSearchManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorSearchNgrams' => 'PhabricatorSearchDAO', 'PhabricatorSearchNgramsDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', 'PhabricatorSearchOrderController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchOrderField' => 'PhabricatorSearchField', 'PhabricatorSearchRelationship' => 'Phobject', 'PhabricatorSearchRelationshipController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchRelationshipSourceController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchResultBucket' => 'Phobject', 'PhabricatorSearchResultBucketGroup' => 'Phobject', 'PhabricatorSearchResultView' => 'AphrontView', 'PhabricatorSearchSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorSearchScopeSetting' => 'PhabricatorInternalSetting', 'PhabricatorSearchSelectField' => 'PhabricatorSearchField', 'PhabricatorSearchService' => 'Phobject', 'PhabricatorSearchStringListField' => 'PhabricatorSearchField', 'PhabricatorSearchSubscribersField' => 'PhabricatorSearchTokenizerField', 'PhabricatorSearchTextField' => 'PhabricatorSearchField', 'PhabricatorSearchThreeStateField' => 'PhabricatorSearchField', 'PhabricatorSearchTokenizerField' => 'PhabricatorSearchField', 'PhabricatorSearchWorker' => 'PhabricatorWorker', 'PhabricatorSecurityConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorSecuritySetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorSelectEditField' => 'PhabricatorEditField', 'PhabricatorSelectSetting' => 'PhabricatorSetting', 'PhabricatorSendGridConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorSessionsSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorSetConfigType' => 'PhabricatorTextConfigType', 'PhabricatorSetting' => 'Phobject', 'PhabricatorSettingsAccountPanelGroup' => 'PhabricatorSettingsPanelGroup', 'PhabricatorSettingsAddEmailAction' => 'PhabricatorSystemAction', 'PhabricatorSettingsAdjustController' => 'PhabricatorController', 'PhabricatorSettingsApplication' => 'PhabricatorApplication', 'PhabricatorSettingsApplicationsPanelGroup' => 'PhabricatorSettingsPanelGroup', 'PhabricatorSettingsAuthenticationPanelGroup' => 'PhabricatorSettingsPanelGroup', 'PhabricatorSettingsDeveloperPanelGroup' => 'PhabricatorSettingsPanelGroup', 'PhabricatorSettingsEditEngine' => 'PhabricatorEditEngine', 'PhabricatorSettingsEmailPanelGroup' => 'PhabricatorSettingsPanelGroup', 'PhabricatorSettingsIssueController' => 'PhabricatorController', 'PhabricatorSettingsListController' => 'PhabricatorController', 'PhabricatorSettingsLogsPanelGroup' => 'PhabricatorSettingsPanelGroup', 'PhabricatorSettingsMainController' => 'PhabricatorController', 'PhabricatorSettingsPanel' => 'Phobject', 'PhabricatorSettingsPanelGroup' => 'Phobject', 'PhabricatorSettingsTimezoneController' => 'PhabricatorController', 'PhabricatorSetupCheck' => 'Phobject', 'PhabricatorSetupCheckTestCase' => 'PhabricatorTestCase', 'PhabricatorSetupEngine' => 'Phobject', 'PhabricatorSetupIssue' => 'Phobject', 'PhabricatorSetupIssueUIExample' => 'PhabricatorUIExample', 'PhabricatorSetupIssueView' => 'AphrontView', 'PhabricatorShortSite' => 'PhabricatorSite', 'PhabricatorShowFiletreeSetting' => 'PhabricatorSelectSetting', 'PhabricatorSimpleEditType' => 'PhabricatorEditType', 'PhabricatorSite' => 'AphrontSite', 'PhabricatorSlackAuthProvider' => 'PhabricatorOAuth2AuthProvider', 'PhabricatorSlowvoteApplication' => 'PhabricatorApplication', 'PhabricatorSlowvoteChoice' => 'PhabricatorSlowvoteDAO', 'PhabricatorSlowvoteCloseController' => 'PhabricatorSlowvoteController', 'PhabricatorSlowvoteCloseTransaction' => 'PhabricatorSlowvoteTransactionType', 'PhabricatorSlowvoteCommentController' => 'PhabricatorSlowvoteController', 'PhabricatorSlowvoteController' => 'PhabricatorController', 'PhabricatorSlowvoteDAO' => 'PhabricatorLiskDAO', 'PhabricatorSlowvoteDefaultViewCapability' => 'PhabricatorPolicyCapability', 'PhabricatorSlowvoteDescriptionTransaction' => 'PhabricatorSlowvoteTransactionType', 'PhabricatorSlowvoteEditController' => 'PhabricatorSlowvoteController', 'PhabricatorSlowvoteEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorSlowvoteListController' => 'PhabricatorSlowvoteController', 'PhabricatorSlowvoteMailReceiver' => 'PhabricatorObjectMailReceiver', 'PhabricatorSlowvoteOption' => 'PhabricatorSlowvoteDAO', 'PhabricatorSlowvotePoll' => array( 'PhabricatorSlowvoteDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorSubscribableInterface', 'PhabricatorFlaggableInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorProjectInterface', 'PhabricatorDestructibleInterface', 'PhabricatorSpacesInterface', ), 'PhabricatorSlowvotePollController' => 'PhabricatorSlowvoteController', 'PhabricatorSlowvotePollPHIDType' => 'PhabricatorPHIDType', 'PhabricatorSlowvoteQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorSlowvoteQuestionTransaction' => 'PhabricatorSlowvoteTransactionType', 'PhabricatorSlowvoteReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PhabricatorSlowvoteResponsesTransaction' => 'PhabricatorSlowvoteTransactionType', 'PhabricatorSlowvoteSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorSlowvoteSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorSlowvoteShuffleTransaction' => 'PhabricatorSlowvoteTransactionType', 'PhabricatorSlowvoteTransaction' => 'PhabricatorModularTransaction', 'PhabricatorSlowvoteTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PhabricatorSlowvoteTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorSlowvoteTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorSlowvoteVoteController' => 'PhabricatorSlowvoteController', 'PhabricatorSlug' => 'Phobject', 'PhabricatorSlugTestCase' => 'PhabricatorTestCase', 'PhabricatorSourceCodeView' => 'AphrontView', 'PhabricatorSourceDocumentEngine' => 'PhabricatorTextDocumentEngine', 'PhabricatorSpaceEditField' => 'PhabricatorEditField', 'PhabricatorSpacesApplication' => 'PhabricatorApplication', 'PhabricatorSpacesArchiveController' => 'PhabricatorSpacesController', 'PhabricatorSpacesCapabilityCreateSpaces' => 'PhabricatorPolicyCapability', 'PhabricatorSpacesCapabilityDefaultEdit' => 'PhabricatorPolicyCapability', 'PhabricatorSpacesCapabilityDefaultView' => 'PhabricatorPolicyCapability', 'PhabricatorSpacesController' => 'PhabricatorController', 'PhabricatorSpacesDAO' => 'PhabricatorLiskDAO', 'PhabricatorSpacesEditController' => 'PhabricatorSpacesController', 'PhabricatorSpacesExportEngineExtension' => 'PhabricatorExportEngineExtension', 'PhabricatorSpacesInterface' => 'PhabricatorPHIDInterface', 'PhabricatorSpacesListController' => 'PhabricatorSpacesController', 'PhabricatorSpacesMailEngineExtension' => 'PhabricatorMailEngineExtension', 'PhabricatorSpacesNamespace' => array( 'PhabricatorSpacesDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorDestructibleInterface', ), 'PhabricatorSpacesNamespaceArchiveTransaction' => 'PhabricatorSpacesNamespaceTransactionType', 'PhabricatorSpacesNamespaceDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorSpacesNamespaceDefaultTransaction' => 'PhabricatorSpacesNamespaceTransactionType', 'PhabricatorSpacesNamespaceDescriptionTransaction' => 'PhabricatorSpacesNamespaceTransactionType', 'PhabricatorSpacesNamespaceEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorSpacesNamespaceNameTransaction' => 'PhabricatorSpacesNamespaceTransactionType', 'PhabricatorSpacesNamespacePHIDType' => 'PhabricatorPHIDType', 'PhabricatorSpacesNamespaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorSpacesNamespaceSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorSpacesNamespaceTransaction' => 'PhabricatorModularTransaction', 'PhabricatorSpacesNamespaceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorSpacesNamespaceTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorSpacesNoAccessController' => 'PhabricatorSpacesController', 'PhabricatorSpacesRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PhabricatorSpacesSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorSpacesSearchEngineExtension' => 'PhabricatorSearchEngineExtension', 'PhabricatorSpacesSearchField' => 'PhabricatorSearchTokenizerField', 'PhabricatorSpacesTestCase' => 'PhabricatorTestCase', 'PhabricatorSpacesViewController' => 'PhabricatorSpacesController', 'PhabricatorStandardCustomField' => 'PhabricatorCustomField', 'PhabricatorStandardCustomFieldBlueprints' => 'PhabricatorStandardCustomFieldTokenizer', 'PhabricatorStandardCustomFieldBool' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldCredential' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldDatasource' => 'PhabricatorStandardCustomFieldTokenizer', 'PhabricatorStandardCustomFieldDate' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldHeader' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldInt' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldLink' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldPHIDs' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldRemarkup' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldSelect' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldText' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldTokenizer' => 'PhabricatorStandardCustomFieldPHIDs', 'PhabricatorStandardCustomFieldUsers' => 'PhabricatorStandardCustomFieldTokenizer', 'PhabricatorStandardPageView' => array( 'PhabricatorBarePageView', 'AphrontResponseProducerInterface', ), 'PhabricatorStandardSelectCustomFieldDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorStaticEditField' => 'PhabricatorEditField', 'PhabricatorStatusController' => 'PhabricatorController', 'PhabricatorStatusUIExample' => 'PhabricatorUIExample', 'PhabricatorStorageFixtureScopeGuard' => 'Phobject', 'PhabricatorStorageManagementAPI' => 'Phobject', 'PhabricatorStorageManagementAdjustWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementAnalyzeWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementDatabasesWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementDestroyWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementDumpWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementOptimizeWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementPartitionWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementProbeWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementQuickstartWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementRenamespaceWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementShellWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementStatusWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementUpgradeWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorStoragePatch' => 'Phobject', 'PhabricatorStorageSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorStorageSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorStringConfigType' => 'PhabricatorTextConfigType', 'PhabricatorStringExportField' => 'PhabricatorExportField', 'PhabricatorStringListConfigType' => 'PhabricatorTextListConfigType', 'PhabricatorStringListEditField' => 'PhabricatorEditField', 'PhabricatorStringListExportField' => 'PhabricatorListExportField', 'PhabricatorStringMailStamp' => 'PhabricatorMailStamp', 'PhabricatorStringSetting' => 'PhabricatorSetting', 'PhabricatorSubmitEditField' => 'PhabricatorEditField', 'PhabricatorSubscribedToObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorSubscribersEditField' => 'PhabricatorTokenizerEditField', 'PhabricatorSubscribersQuery' => 'PhabricatorQuery', 'PhabricatorSubscriptionTriggerClock' => 'PhabricatorTriggerClock', 'PhabricatorSubscriptionsAddSelfHeraldAction' => 'PhabricatorSubscriptionsHeraldAction', 'PhabricatorSubscriptionsAddSubscribersHeraldAction' => 'PhabricatorSubscriptionsHeraldAction', 'PhabricatorSubscriptionsApplication' => 'PhabricatorApplication', 'PhabricatorSubscriptionsCurtainExtension' => 'PHUICurtainExtension', 'PhabricatorSubscriptionsEditController' => 'PhabricatorController', 'PhabricatorSubscriptionsEditEngineExtension' => 'PhabricatorEditEngineExtension', 'PhabricatorSubscriptionsEditor' => 'PhabricatorEditor', 'PhabricatorSubscriptionsExportEngineExtension' => 'PhabricatorExportEngineExtension', 'PhabricatorSubscriptionsFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension', 'PhabricatorSubscriptionsHeraldAction' => 'HeraldAction', 'PhabricatorSubscriptionsListController' => 'PhabricatorController', 'PhabricatorSubscriptionsMailEngineExtension' => 'PhabricatorMailEngineExtension', 'PhabricatorSubscriptionsMuteController' => 'PhabricatorController', 'PhabricatorSubscriptionsRemoveSelfHeraldAction' => 'PhabricatorSubscriptionsHeraldAction', 'PhabricatorSubscriptionsRemoveSubscribersHeraldAction' => 'PhabricatorSubscriptionsHeraldAction', 'PhabricatorSubscriptionsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'PhabricatorSubscriptionsSearchEngineExtension' => 'PhabricatorSearchEngineExtension', 'PhabricatorSubscriptionsSubscribeEmailCommand' => 'MetaMTAEmailTransactionCommand', 'PhabricatorSubscriptionsSubscribersPolicyRule' => 'PhabricatorPolicyRule', 'PhabricatorSubscriptionsTransactionController' => 'PhabricatorController', 'PhabricatorSubscriptionsUIEventListener' => 'PhabricatorEventListener', 'PhabricatorSubscriptionsUnsubscribeEmailCommand' => 'MetaMTAEmailTransactionCommand', 'PhabricatorSubtypeEditEngineExtension' => 'PhabricatorEditEngineExtension', 'PhabricatorSupportApplication' => 'PhabricatorApplication', 'PhabricatorSyntaxHighlighter' => 'Phobject', 'PhabricatorSyntaxHighlightingConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorSyntaxStyle' => 'Phobject', 'PhabricatorSystemAction' => 'Phobject', 'PhabricatorSystemActionEngine' => 'Phobject', 'PhabricatorSystemActionGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorSystemActionLog' => 'PhabricatorSystemDAO', 'PhabricatorSystemActionRateLimitException' => 'Exception', 'PhabricatorSystemApplication' => 'PhabricatorApplication', 'PhabricatorSystemDAO' => 'PhabricatorLiskDAO', 'PhabricatorSystemDestructionGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorSystemDestructionLog' => 'PhabricatorSystemDAO', 'PhabricatorSystemObjectController' => 'PhabricatorController', 'PhabricatorSystemReadOnlyController' => 'PhabricatorController', 'PhabricatorSystemRemoveDestroyWorkflow' => 'PhabricatorSystemRemoveWorkflow', 'PhabricatorSystemRemoveLogWorkflow' => 'PhabricatorSystemRemoveWorkflow', 'PhabricatorSystemRemoveWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorSystemSelectEncodingController' => 'PhabricatorController', 'PhabricatorSystemSelectHighlightController' => 'PhabricatorController', 'PhabricatorTOTPAuthFactor' => 'PhabricatorAuthFactor', 'PhabricatorTOTPAuthFactorTestCase' => 'PhabricatorTestCase', 'PhabricatorTaskmasterDaemon' => 'PhabricatorDaemon', 'PhabricatorTaskmasterDaemonModule' => 'PhutilDaemonOverseerModule', 'PhabricatorTestApplication' => 'PhabricatorApplication', 'PhabricatorTestCase' => 'PhutilTestCase', 'PhabricatorTestController' => 'PhabricatorController', 'PhabricatorTestDataGenerator' => 'Phobject', 'PhabricatorTestNoCycleEdgeType' => 'PhabricatorEdgeType', 'PhabricatorTestStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorTestWorker' => 'PhabricatorWorker', 'PhabricatorTextAreaEditField' => 'PhabricatorEditField', 'PhabricatorTextConfigType' => 'PhabricatorConfigType', 'PhabricatorTextDocumentEngine' => 'PhabricatorDocumentEngine', 'PhabricatorTextEditField' => 'PhabricatorEditField', 'PhabricatorTextExportFormat' => 'PhabricatorExportFormat', 'PhabricatorTextListConfigType' => 'PhabricatorTextConfigType', 'PhabricatorTime' => 'Phobject', 'PhabricatorTimeFormatSetting' => 'PhabricatorSelectSetting', 'PhabricatorTimeGuard' => 'Phobject', 'PhabricatorTimeTestCase' => 'PhabricatorTestCase', 'PhabricatorTimezoneIgnoreOffsetSetting' => 'PhabricatorInternalSetting', 'PhabricatorTimezoneSetting' => 'PhabricatorOptionGroupSetting', 'PhabricatorTimezoneSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorTitleGlyphsSetting' => 'PhabricatorSelectSetting', 'PhabricatorToken' => array( 'PhabricatorTokenDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorTokenController' => 'PhabricatorController', 'PhabricatorTokenCount' => 'PhabricatorTokenDAO', 'PhabricatorTokenCountQuery' => 'PhabricatorOffsetPagedQuery', 'PhabricatorTokenDAO' => 'PhabricatorLiskDAO', 'PhabricatorTokenDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', 'PhabricatorTokenGiveController' => 'PhabricatorTokenController', 'PhabricatorTokenGiven' => array( 'PhabricatorTokenDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorTokenGivenController' => 'PhabricatorTokenController', 'PhabricatorTokenGivenEditor' => 'PhabricatorEditor', 'PhabricatorTokenGivenFeedStory' => 'PhabricatorFeedStory', 'PhabricatorTokenGivenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorTokenLeaderController' => 'PhabricatorTokenController', 'PhabricatorTokenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorTokenReceiverQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorTokenTokenPHIDType' => 'PhabricatorPHIDType', 'PhabricatorTokenUIEventListener' => 'PhabricatorEventListener', 'PhabricatorTokenizerEditField' => 'PhabricatorPHIDListEditField', 'PhabricatorTokensApplication' => 'PhabricatorApplication', 'PhabricatorTokensCurtainExtension' => 'PHUICurtainExtension', 'PhabricatorTokensSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorTokensToken' => array( 'PhabricatorTokenDAO', 'PhabricatorDestructibleInterface', 'PhabricatorSubscribableInterface', 'PhabricatorFlaggableInterface', 'PhabricatorConduitResultInterface', ), 'PhabricatorTransactionChange' => 'Phobject', 'PhabricatorTransactionFactEngine' => 'PhabricatorFactEngine', 'PhabricatorTransactionRemarkupChange' => 'PhabricatorTransactionChange', 'PhabricatorTransactions' => 'Phobject', 'PhabricatorTransactionsApplication' => 'PhabricatorApplication', 'PhabricatorTransactionsDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', 'PhabricatorTransactionsFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension', 'PhabricatorTransformedFile' => 'PhabricatorFileDAO', 'PhabricatorTranslationSetting' => 'PhabricatorOptionGroupSetting', 'PhabricatorTranslationsConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorTriggerAction' => 'Phobject', 'PhabricatorTriggerClock' => 'Phobject', 'PhabricatorTriggerClockTestCase' => 'PhabricatorTestCase', 'PhabricatorTriggerDaemon' => 'PhabricatorDaemon', 'PhabricatorTrivialTestCase' => 'PhabricatorTestCase', 'PhabricatorTwitchAuthProvider' => 'PhabricatorOAuth2AuthProvider', 'PhabricatorTwitterAuthProvider' => 'PhabricatorOAuth1AuthProvider', 'PhabricatorTypeaheadApplication' => 'PhabricatorApplication', 'PhabricatorTypeaheadCompositeDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorTypeaheadDatasource' => 'Phobject', 'PhabricatorTypeaheadDatasourceController' => 'PhabricatorController', 'PhabricatorTypeaheadDatasourceTestCase' => 'PhabricatorTestCase', 'PhabricatorTypeaheadFunctionHelpController' => 'PhabricatorTypeaheadDatasourceController', 'PhabricatorTypeaheadInvalidTokenException' => 'Exception', 'PhabricatorTypeaheadModularDatasourceController' => 'PhabricatorTypeaheadDatasourceController', 'PhabricatorTypeaheadMonogramDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorTypeaheadProxyDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorTypeaheadResult' => 'Phobject', 'PhabricatorTypeaheadRuntimeCompositeDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorTypeaheadTestNumbersDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorTypeaheadTokenView' => 'AphrontTagView', 'PhabricatorUIConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorUIExample' => 'Phobject', 'PhabricatorUIExampleRenderController' => 'PhabricatorController', 'PhabricatorUIExamplesApplication' => 'PhabricatorApplication', 'PhabricatorURIExportField' => 'PhabricatorExportField', 'PhabricatorUSEnglishTranslation' => 'PhutilTranslation', 'PhabricatorUnifiedDiffsSetting' => 'PhabricatorSelectSetting', 'PhabricatorUnitTestContentSource' => 'PhabricatorContentSource', 'PhabricatorUnitsTestCase' => 'PhabricatorTestCase', 'PhabricatorUnknownContentSource' => 'PhabricatorContentSource', 'PhabricatorUnsubscribedFromObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorUser' => array( 'PhabricatorUserDAO', 'PhutilPerson', 'PhabricatorPolicyInterface', 'PhabricatorCustomFieldInterface', 'PhabricatorDestructibleInterface', 'PhabricatorSSHPublicKeyInterface', 'PhabricatorFlaggableInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorFulltextInterface', 'PhabricatorFerretInterface', 'PhabricatorConduitResultInterface', 'PhabricatorAuthPasswordHashInterface', ), 'PhabricatorUserBadgesCacheType' => 'PhabricatorUserCacheType', 'PhabricatorUserBlurbField' => 'PhabricatorUserCustomField', 'PhabricatorUserCache' => 'PhabricatorUserDAO', 'PhabricatorUserCachePurger' => 'PhabricatorCachePurger', 'PhabricatorUserCacheType' => 'Phobject', 'PhabricatorUserCardView' => 'AphrontTagView', 'PhabricatorUserConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorUserConfiguredCustomField' => array( 'PhabricatorUserCustomField', 'PhabricatorStandardCustomFieldInterface', ), 'PhabricatorUserConfiguredCustomFieldStorage' => 'PhabricatorCustomFieldStorage', 'PhabricatorUserCustomField' => 'PhabricatorCustomField', 'PhabricatorUserCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage', 'PhabricatorUserCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage', 'PhabricatorUserDAO' => 'PhabricatorLiskDAO', 'PhabricatorUserDisableTransaction' => 'PhabricatorUserTransactionType', 'PhabricatorUserEditEngine' => 'PhabricatorEditEngine', 'PhabricatorUserEditor' => 'PhabricatorEditor', 'PhabricatorUserEditorTestCase' => 'PhabricatorTestCase', 'PhabricatorUserEmail' => 'PhabricatorUserDAO', 'PhabricatorUserEmailTestCase' => 'PhabricatorTestCase', 'PhabricatorUserFerretEngine' => 'PhabricatorFerretEngine', 'PhabricatorUserFulltextEngine' => 'PhabricatorFulltextEngine', 'PhabricatorUserIconField' => 'PhabricatorUserCustomField', 'PhabricatorUserLog' => array( 'PhabricatorUserDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorUserLogView' => 'AphrontView', 'PhabricatorUserMessageCountCacheType' => 'PhabricatorUserCacheType', 'PhabricatorUserNotificationCountCacheType' => 'PhabricatorUserCacheType', 'PhabricatorUserPHIDResolver' => 'PhabricatorPHIDResolver', 'PhabricatorUserPreferences' => array( 'PhabricatorUserDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', 'PhabricatorApplicationTransactionInterface', ), 'PhabricatorUserPreferencesCacheType' => 'PhabricatorUserCacheType', 'PhabricatorUserPreferencesEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorUserPreferencesPHIDType' => 'PhabricatorPHIDType', 'PhabricatorUserPreferencesQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorUserPreferencesSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorUserPreferencesTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorUserPreferencesTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorUserProfile' => 'PhabricatorUserDAO', 'PhabricatorUserProfileImageCacheType' => 'PhabricatorUserCacheType', 'PhabricatorUserRealNameField' => 'PhabricatorUserCustomField', 'PhabricatorUserRolesField' => 'PhabricatorUserCustomField', 'PhabricatorUserSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorUserSinceField' => 'PhabricatorUserCustomField', 'PhabricatorUserStatusField' => 'PhabricatorUserCustomField', 'PhabricatorUserTestCase' => 'PhabricatorTestCase', 'PhabricatorUserTitleField' => 'PhabricatorUserCustomField', 'PhabricatorUserTransaction' => 'PhabricatorModularTransaction', 'PhabricatorUserTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorUserTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorUsersEditField' => 'PhabricatorTokenizerEditField', 'PhabricatorUsersPolicyRule' => 'PhabricatorPolicyRule', 'PhabricatorUsersSearchField' => 'PhabricatorSearchTokenizerField', 'PhabricatorVCSResponse' => 'AphrontResponse', 'PhabricatorVersionedDraft' => 'PhabricatorDraftDAO', 'PhabricatorVeryWowEnglishTranslation' => 'PhutilTranslation', 'PhabricatorVideoDocumentEngine' => 'PhabricatorDocumentEngine', 'PhabricatorViewerDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorVoidDocumentEngine' => 'PhabricatorDocumentEngine', 'PhabricatorWatcherHasObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorWebContentSource' => 'PhabricatorContentSource', 'PhabricatorWebServerSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorWeekStartDaySetting' => 'PhabricatorSelectSetting', 'PhabricatorWildConfigType' => 'PhabricatorJSONConfigType', 'PhabricatorWordPressAuthProvider' => 'PhabricatorOAuth2AuthProvider', 'PhabricatorWorker' => 'Phobject', 'PhabricatorWorkerActiveTask' => 'PhabricatorWorkerTask', 'PhabricatorWorkerActiveTaskQuery' => 'PhabricatorWorkerTaskQuery', 'PhabricatorWorkerArchiveTask' => 'PhabricatorWorkerTask', 'PhabricatorWorkerArchiveTaskQuery' => 'PhabricatorWorkerTaskQuery', 'PhabricatorWorkerBulkJob' => array( 'PhabricatorWorkerDAO', 'PhabricatorPolicyInterface', 'PhabricatorSubscribableInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorDestructibleInterface', ), 'PhabricatorWorkerBulkJobCreateWorker' => 'PhabricatorWorkerBulkJobWorker', 'PhabricatorWorkerBulkJobEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorWorkerBulkJobPHIDType' => 'PhabricatorPHIDType', 'PhabricatorWorkerBulkJobQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorWorkerBulkJobSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorWorkerBulkJobTaskWorker' => 'PhabricatorWorkerBulkJobWorker', 'PhabricatorWorkerBulkJobTestCase' => 'PhabricatorTestCase', 'PhabricatorWorkerBulkJobTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorWorkerBulkJobTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorWorkerBulkJobType' => 'Phobject', 'PhabricatorWorkerBulkJobWorker' => 'PhabricatorWorker', 'PhabricatorWorkerBulkTask' => 'PhabricatorWorkerDAO', 'PhabricatorWorkerDAO' => 'PhabricatorLiskDAO', 'PhabricatorWorkerDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', 'PhabricatorWorkerLeaseQuery' => 'PhabricatorQuery', 'PhabricatorWorkerManagementCancelWorkflow' => 'PhabricatorWorkerManagementWorkflow', 'PhabricatorWorkerManagementExecuteWorkflow' => 'PhabricatorWorkerManagementWorkflow', 'PhabricatorWorkerManagementFloodWorkflow' => 'PhabricatorWorkerManagementWorkflow', 'PhabricatorWorkerManagementFreeWorkflow' => 'PhabricatorWorkerManagementWorkflow', 'PhabricatorWorkerManagementRetryWorkflow' => 'PhabricatorWorkerManagementWorkflow', 'PhabricatorWorkerManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorWorkerPermanentFailureException' => 'Exception', 'PhabricatorWorkerSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorWorkerSingleBulkJobType' => 'PhabricatorWorkerBulkJobType', 'PhabricatorWorkerTask' => 'PhabricatorWorkerDAO', 'PhabricatorWorkerTaskData' => 'PhabricatorWorkerDAO', 'PhabricatorWorkerTaskDetailController' => 'PhabricatorDaemonController', 'PhabricatorWorkerTaskQuery' => 'PhabricatorQuery', 'PhabricatorWorkerTestCase' => 'PhabricatorTestCase', 'PhabricatorWorkerTrigger' => array( 'PhabricatorWorkerDAO', 'PhabricatorDestructibleInterface', 'PhabricatorPolicyInterface', ), 'PhabricatorWorkerTriggerEvent' => 'PhabricatorWorkerDAO', 'PhabricatorWorkerTriggerManagementFireWorkflow' => 'PhabricatorWorkerTriggerManagementWorkflow', 'PhabricatorWorkerTriggerManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorWorkerTriggerPHIDType' => 'PhabricatorPHIDType', 'PhabricatorWorkerTriggerQuery' => 'PhabricatorPolicyAwareQuery', 'PhabricatorWorkerYieldException' => 'Exception', 'PhabricatorWorkingCopyDiscoveryTestCase' => 'PhabricatorWorkingCopyTestCase', 'PhabricatorWorkingCopyPullTestCase' => 'PhabricatorWorkingCopyTestCase', 'PhabricatorWorkingCopyTestCase' => 'PhabricatorTestCase', 'PhabricatorXHPASTDAO' => 'PhabricatorLiskDAO', 'PhabricatorXHPASTParseTree' => 'PhabricatorXHPASTDAO', 'PhabricatorXHPASTViewController' => 'PhabricatorController', 'PhabricatorXHPASTViewFrameController' => 'PhabricatorXHPASTViewController', 'PhabricatorXHPASTViewFramesetController' => 'PhabricatorXHPASTViewController', 'PhabricatorXHPASTViewInputController' => 'PhabricatorXHPASTViewPanelController', 'PhabricatorXHPASTViewPanelController' => 'PhabricatorXHPASTViewController', 'PhabricatorXHPASTViewRunController' => 'PhabricatorXHPASTViewController', 'PhabricatorXHPASTViewStreamController' => 'PhabricatorXHPASTViewPanelController', 'PhabricatorXHPASTViewTreeController' => 'PhabricatorXHPASTViewPanelController', 'PhabricatorXHProfApplication' => 'PhabricatorApplication', 'PhabricatorXHProfController' => 'PhabricatorController', 'PhabricatorXHProfDAO' => 'PhabricatorLiskDAO', 'PhabricatorXHProfDropController' => 'PhabricatorXHProfController', 'PhabricatorXHProfProfileController' => 'PhabricatorXHProfController', 'PhabricatorXHProfProfileSymbolView' => 'PhabricatorXHProfProfileView', 'PhabricatorXHProfProfileTopLevelView' => 'PhabricatorXHProfProfileView', 'PhabricatorXHProfProfileView' => 'AphrontView', 'PhabricatorXHProfSample' => array( 'PhabricatorXHProfDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorXHProfSampleListController' => 'PhabricatorXHProfController', 'PhabricatorXHProfSampleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorXHProfSampleSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorYoutubeRemarkupRule' => 'PhutilRemarkupRule', 'Phame404Response' => 'AphrontHTMLResponse', 'PhameBlog' => array( 'PhameDAO', 'PhabricatorPolicyInterface', 'PhabricatorMarkupInterface', 'PhabricatorSubscribableInterface', 'PhabricatorFlaggableInterface', 'PhabricatorProjectInterface', 'PhabricatorDestructibleInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorConduitResultInterface', 'PhabricatorFulltextInterface', 'PhabricatorFerretInterface', ), 'PhameBlog404Controller' => 'PhameLiveController', 'PhameBlogArchiveController' => 'PhameBlogController', 'PhameBlogController' => 'PhameController', 'PhameBlogCreateCapability' => 'PhabricatorPolicyCapability', 'PhameBlogDatasource' => 'PhabricatorTypeaheadDatasource', 'PhameBlogDescriptionTransaction' => 'PhameBlogTransactionType', 'PhameBlogEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'PhameBlogEditController' => 'PhameBlogController', 'PhameBlogEditEngine' => 'PhabricatorEditEngine', 'PhameBlogEditor' => 'PhabricatorApplicationTransactionEditor', 'PhameBlogFeedController' => 'PhameBlogController', 'PhameBlogFerretEngine' => 'PhabricatorFerretEngine', 'PhameBlogFullDomainTransaction' => 'PhameBlogTransactionType', 'PhameBlogFulltextEngine' => 'PhabricatorFulltextEngine', 'PhameBlogHeaderImageTransaction' => 'PhameBlogTransactionType', 'PhameBlogHeaderPictureController' => 'PhameBlogController', 'PhameBlogListController' => 'PhameBlogController', 'PhameBlogListView' => 'AphrontTagView', 'PhameBlogManageController' => 'PhameBlogController', 'PhameBlogNameTransaction' => 'PhameBlogTransactionType', 'PhameBlogParentDomainTransaction' => 'PhameBlogTransactionType', 'PhameBlogParentSiteTransaction' => 'PhameBlogTransactionType', 'PhameBlogProfileImageTransaction' => 'PhameBlogTransactionType', 'PhameBlogProfilePictureController' => 'PhameBlogController', 'PhameBlogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhameBlogReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PhameBlogSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'PhameBlogSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhameBlogSite' => 'PhameSite', 'PhameBlogStatusTransaction' => 'PhameBlogTransactionType', 'PhameBlogSubtitleTransaction' => 'PhameBlogTransactionType', 'PhameBlogTransaction' => 'PhabricatorModularTransaction', 'PhameBlogTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhameBlogTransactionType' => 'PhabricatorModularTransactionType', 'PhameBlogViewController' => 'PhameLiveController', 'PhameConstants' => 'Phobject', 'PhameController' => 'PhabricatorController', 'PhameDAO' => 'PhabricatorLiskDAO', 'PhameDescriptionView' => 'AphrontTagView', 'PhameDraftListView' => 'AphrontTagView', 'PhameHomeController' => 'PhamePostController', 'PhameLiveController' => 'PhameController', 'PhameNextPostView' => 'AphrontTagView', 'PhamePost' => array( 'PhameDAO', 'PhabricatorPolicyInterface', 'PhabricatorMarkupInterface', 'PhabricatorFlaggableInterface', 'PhabricatorProjectInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorSubscribableInterface', 'PhabricatorDestructibleInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorConduitResultInterface', 'PhabricatorFulltextInterface', 'PhabricatorFerretInterface', ), 'PhamePostArchiveController' => 'PhamePostController', 'PhamePostBlogTransaction' => 'PhamePostTransactionType', 'PhamePostBodyTransaction' => 'PhamePostTransactionType', 'PhamePostController' => 'PhameController', 'PhamePostEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'PhamePostEditController' => 'PhamePostController', 'PhamePostEditEngine' => 'PhabricatorEditEngine', 'PhamePostEditor' => 'PhabricatorApplicationTransactionEditor', 'PhamePostFerretEngine' => 'PhabricatorFerretEngine', 'PhamePostFulltextEngine' => 'PhabricatorFulltextEngine', 'PhamePostHeaderImageTransaction' => 'PhamePostTransactionType', 'PhamePostHeaderPictureController' => 'PhamePostController', 'PhamePostHistoryController' => 'PhamePostController', 'PhamePostListController' => 'PhamePostController', 'PhamePostListView' => 'AphrontTagView', 'PhamePostMailReceiver' => 'PhabricatorObjectMailReceiver', 'PhamePostMoveController' => 'PhamePostController', 'PhamePostPublishController' => 'PhamePostController', 'PhamePostQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhamePostRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PhamePostReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PhamePostSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'PhamePostSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhamePostSubtitleTransaction' => 'PhamePostTransactionType', 'PhamePostTitleTransaction' => 'PhamePostTransactionType', 'PhamePostTransaction' => 'PhabricatorModularTransaction', 'PhamePostTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PhamePostTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhamePostTransactionType' => 'PhabricatorModularTransactionType', 'PhamePostViewController' => 'PhameLiveController', 'PhamePostVisibilityTransaction' => 'PhamePostTransactionType', 'PhameSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhameSite' => 'PhabricatorSite', 'PhluxController' => 'PhabricatorController', 'PhluxDAO' => 'PhabricatorLiskDAO', 'PhluxEditController' => 'PhluxController', 'PhluxListController' => 'PhluxController', 'PhluxSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhluxTransaction' => 'PhabricatorApplicationTransaction', 'PhluxTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhluxVariable' => array( 'PhluxDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorFlaggableInterface', 'PhabricatorPolicyInterface', ), 'PhluxVariableEditor' => 'PhabricatorApplicationTransactionEditor', 'PhluxVariablePHIDType' => 'PhabricatorPHIDType', 'PhluxVariableQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhluxViewController' => 'PhluxController', 'PholioController' => 'PhabricatorController', 'PholioDAO' => 'PhabricatorLiskDAO', 'PholioDefaultEditCapability' => 'PhabricatorPolicyCapability', 'PholioDefaultViewCapability' => 'PhabricatorPolicyCapability', 'PholioImage' => array( 'PholioDAO', 'PhabricatorMarkupInterface', 'PhabricatorPolicyInterface', ), 'PholioImageDescriptionTransaction' => 'PholioImageTransactionType', 'PholioImageFileTransaction' => 'PholioImageTransactionType', 'PholioImageNameTransaction' => 'PholioImageTransactionType', 'PholioImagePHIDType' => 'PhabricatorPHIDType', 'PholioImageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PholioImageReplaceTransaction' => 'PholioImageTransactionType', 'PholioImageSequenceTransaction' => 'PholioImageTransactionType', 'PholioImageTransactionType' => 'PholioTransactionType', 'PholioImageUploadController' => 'PholioController', 'PholioInlineController' => 'PholioController', 'PholioInlineListController' => 'PholioController', 'PholioMock' => array( 'PholioDAO', 'PhabricatorMarkupInterface', 'PhabricatorPolicyInterface', 'PhabricatorSubscribableInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorFlaggableInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorProjectInterface', 'PhabricatorDestructibleInterface', 'PhabricatorSpacesInterface', 'PhabricatorMentionableInterface', 'PhabricatorFulltextInterface', 'PhabricatorFerretInterface', ), 'PholioMockArchiveController' => 'PholioController', 'PholioMockAuthorHeraldField' => 'PholioMockHeraldField', 'PholioMockCommentController' => 'PholioController', 'PholioMockDescriptionHeraldField' => 'PholioMockHeraldField', 'PholioMockDescriptionTransaction' => 'PholioMockTransactionType', 'PholioMockEditController' => 'PholioController', 'PholioMockEditor' => 'PhabricatorApplicationTransactionEditor', 'PholioMockEmbedView' => 'AphrontView', 'PholioMockFerretEngine' => 'PhabricatorFerretEngine', 'PholioMockFulltextEngine' => 'PhabricatorFulltextEngine', 'PholioMockHasTaskEdgeType' => 'PhabricatorEdgeType', 'PholioMockHasTaskRelationship' => 'PholioMockRelationship', 'PholioMockHeraldField' => 'HeraldField', 'PholioMockHeraldFieldGroup' => 'HeraldFieldGroup', 'PholioMockImagesView' => 'AphrontView', 'PholioMockInlineTransaction' => 'PholioMockTransactionType', 'PholioMockListController' => 'PholioController', 'PholioMockMailReceiver' => 'PhabricatorObjectMailReceiver', 'PholioMockNameHeraldField' => 'PholioMockHeraldField', 'PholioMockNameTransaction' => 'PholioMockTransactionType', 'PholioMockPHIDType' => 'PhabricatorPHIDType', 'PholioMockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PholioMockRelationship' => 'PhabricatorObjectRelationship', 'PholioMockRelationshipSource' => 'PhabricatorObjectRelationshipSource', 'PholioMockSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PholioMockStatusTransaction' => 'PholioMockTransactionType', 'PholioMockThumbGridView' => 'AphrontView', 'PholioMockTransactionType' => 'PholioTransactionType', 'PholioMockViewController' => 'PholioController', 'PholioRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PholioReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PholioSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PholioTransaction' => 'PhabricatorModularTransaction', 'PholioTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PholioTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PholioTransactionType' => 'PhabricatorModularTransactionType', 'PholioTransactionView' => 'PhabricatorApplicationTransactionView', 'PholioUploadedImageView' => 'AphrontView', 'PhortuneAccount' => array( 'PhortuneDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', ), 'PhortuneAccountAddManagerController' => 'PhortuneController', 'PhortuneAccountBillingController' => 'PhortuneAccountProfileController', 'PhortuneAccountChargeListController' => 'PhortuneController', 'PhortuneAccountController' => 'PhortuneController', 'PhortuneAccountEditController' => 'PhortuneController', 'PhortuneAccountEditEngine' => 'PhabricatorEditEngine', 'PhortuneAccountEditor' => 'PhabricatorApplicationTransactionEditor', 'PhortuneAccountHasMemberEdgeType' => 'PhabricatorEdgeType', 'PhortuneAccountListController' => 'PhortuneController', 'PhortuneAccountManagerController' => 'PhortuneAccountProfileController', 'PhortuneAccountNameTransaction' => 'PhortuneAccountTransactionType', 'PhortuneAccountPHIDType' => 'PhabricatorPHIDType', 'PhortuneAccountProfileController' => 'PhortuneAccountController', 'PhortuneAccountQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneAccountSubscriptionController' => 'PhortuneAccountProfileController', 'PhortuneAccountTransaction' => 'PhabricatorModularTransaction', 'PhortuneAccountTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhortuneAccountTransactionType' => 'PhabricatorModularTransactionType', 'PhortuneAccountViewController' => 'PhortuneAccountProfileController', 'PhortuneAdHocCart' => 'PhortuneCartImplementation', 'PhortuneAdHocProduct' => 'PhortuneProductImplementation', 'PhortuneCart' => array( 'PhortuneDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', ), 'PhortuneCartAcceptController' => 'PhortuneCartController', 'PhortuneCartCancelController' => 'PhortuneCartController', 'PhortuneCartCheckoutController' => 'PhortuneCartController', 'PhortuneCartController' => 'PhortuneController', 'PhortuneCartEditor' => 'PhabricatorApplicationTransactionEditor', 'PhortuneCartImplementation' => 'Phobject', 'PhortuneCartListController' => 'PhortuneController', 'PhortuneCartPHIDType' => 'PhabricatorPHIDType', 'PhortuneCartQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneCartReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PhortuneCartSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhortuneCartTransaction' => 'PhabricatorApplicationTransaction', 'PhortuneCartTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhortuneCartUpdateController' => 'PhortuneCartController', 'PhortuneCartViewController' => 'PhortuneCartController', 'PhortuneCharge' => array( 'PhortuneDAO', 'PhabricatorPolicyInterface', ), 'PhortuneChargePHIDType' => 'PhabricatorPHIDType', 'PhortuneChargeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneChargeSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhortuneChargeTableView' => 'AphrontView', 'PhortuneConstants' => 'Phobject', 'PhortuneController' => 'PhabricatorController', 'PhortuneCreditCardForm' => 'Phobject', 'PhortuneCurrency' => 'Phobject', 'PhortuneCurrencySerializer' => 'PhabricatorLiskSerializer', 'PhortuneCurrencyTestCase' => 'PhabricatorTestCase', 'PhortuneDAO' => 'PhabricatorLiskDAO', 'PhortuneErrCode' => 'PhortuneConstants', 'PhortuneInvoiceView' => 'AphrontTagView', 'PhortuneLandingController' => 'PhortuneController', 'PhortuneMemberHasAccountEdgeType' => 'PhabricatorEdgeType', 'PhortuneMemberHasMerchantEdgeType' => 'PhabricatorEdgeType', 'PhortuneMerchant' => array( 'PhortuneDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', ), 'PhortuneMerchantAddManagerController' => 'PhortuneController', 'PhortuneMerchantCapability' => 'PhabricatorPolicyCapability', 'PhortuneMerchantContactInfoTransaction' => 'PhortuneMerchantTransactionType', 'PhortuneMerchantController' => 'PhortuneController', 'PhortuneMerchantDescriptionTransaction' => 'PhortuneMerchantTransactionType', 'PhortuneMerchantEditController' => 'PhortuneMerchantController', 'PhortuneMerchantEditEngine' => 'PhabricatorEditEngine', 'PhortuneMerchantEditor' => 'PhabricatorApplicationTransactionEditor', 'PhortuneMerchantHasMemberEdgeType' => 'PhabricatorEdgeType', 'PhortuneMerchantInvoiceCreateController' => 'PhortuneMerchantProfileController', 'PhortuneMerchantInvoiceEmailTransaction' => 'PhortuneMerchantTransactionType', 'PhortuneMerchantInvoiceFooterTransaction' => 'PhortuneMerchantTransactionType', 'PhortuneMerchantListController' => 'PhortuneMerchantController', 'PhortuneMerchantManagerController' => 'PhortuneMerchantProfileController', 'PhortuneMerchantNameTransaction' => 'PhortuneMerchantTransactionType', 'PhortuneMerchantPHIDType' => 'PhabricatorPHIDType', 'PhortuneMerchantPictureController' => 'PhortuneMerchantProfileController', 'PhortuneMerchantPictureTransaction' => 'PhortuneMerchantTransactionType', 'PhortuneMerchantProfileController' => 'PhortuneController', 'PhortuneMerchantQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneMerchantSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhortuneMerchantTransaction' => 'PhabricatorModularTransaction', 'PhortuneMerchantTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhortuneMerchantTransactionType' => 'PhabricatorModularTransactionType', 'PhortuneMerchantViewController' => 'PhortuneMerchantProfileController', 'PhortuneMonthYearExpiryControl' => 'AphrontFormControl', 'PhortuneOrderTableView' => 'AphrontView', 'PhortunePayPalPaymentProvider' => 'PhortunePaymentProvider', 'PhortunePaymentMethod' => array( 'PhortuneDAO', 'PhabricatorPolicyInterface', ), 'PhortunePaymentMethodCreateController' => 'PhortuneController', 'PhortunePaymentMethodDisableController' => 'PhortuneController', 'PhortunePaymentMethodEditController' => 'PhortuneController', 'PhortunePaymentMethodPHIDType' => 'PhabricatorPHIDType', 'PhortunePaymentMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortunePaymentProvider' => 'Phobject', 'PhortunePaymentProviderConfig' => array( 'PhortuneDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', ), 'PhortunePaymentProviderConfigEditor' => 'PhabricatorApplicationTransactionEditor', 'PhortunePaymentProviderConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortunePaymentProviderConfigTransaction' => 'PhabricatorApplicationTransaction', 'PhortunePaymentProviderConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhortunePaymentProviderPHIDType' => 'PhabricatorPHIDType', 'PhortunePaymentProviderTestCase' => 'PhabricatorTestCase', 'PhortuneProduct' => array( 'PhortuneDAO', 'PhabricatorPolicyInterface', ), 'PhortuneProductImplementation' => 'Phobject', 'PhortuneProductListController' => 'PhabricatorController', 'PhortuneProductPHIDType' => 'PhabricatorPHIDType', 'PhortuneProductQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneProductViewController' => 'PhortuneController', 'PhortuneProviderActionController' => 'PhortuneController', 'PhortuneProviderDisableController' => 'PhortuneMerchantController', 'PhortuneProviderEditController' => 'PhortuneMerchantController', 'PhortunePurchase' => array( 'PhortuneDAO', 'PhabricatorPolicyInterface', ), 'PhortunePurchasePHIDType' => 'PhabricatorPHIDType', 'PhortunePurchaseQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhortuneStripePaymentProvider' => 'PhortunePaymentProvider', 'PhortuneSubscription' => array( 'PhortuneDAO', 'PhabricatorPolicyInterface', ), 'PhortuneSubscriptionCart' => 'PhortuneCartImplementation', 'PhortuneSubscriptionEditController' => 'PhortuneController', 'PhortuneSubscriptionImplementation' => 'Phobject', 'PhortuneSubscriptionListController' => 'PhortuneController', 'PhortuneSubscriptionPHIDType' => 'PhabricatorPHIDType', 'PhortuneSubscriptionProduct' => 'PhortuneProductImplementation', 'PhortuneSubscriptionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneSubscriptionSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhortuneSubscriptionTableView' => 'AphrontView', 'PhortuneSubscriptionViewController' => 'PhortuneController', 'PhortuneSubscriptionWorker' => 'PhabricatorWorker', 'PhortuneTestPaymentProvider' => 'PhortunePaymentProvider', 'PhortuneWePayPaymentProvider' => 'PhortunePaymentProvider', 'PhragmentBrowseController' => 'PhragmentController', 'PhragmentCanCreateCapability' => 'PhabricatorPolicyCapability', 'PhragmentConduitAPIMethod' => 'ConduitAPIMethod', 'PhragmentController' => 'PhabricatorController', 'PhragmentCreateController' => 'PhragmentController', 'PhragmentDAO' => 'PhabricatorLiskDAO', 'PhragmentFragment' => array( 'PhragmentDAO', 'PhabricatorPolicyInterface', ), 'PhragmentFragmentPHIDType' => 'PhabricatorPHIDType', 'PhragmentFragmentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhragmentFragmentVersion' => array( 'PhragmentDAO', 'PhabricatorPolicyInterface', ), 'PhragmentFragmentVersionPHIDType' => 'PhabricatorPHIDType', 'PhragmentFragmentVersionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhragmentGetPatchConduitAPIMethod' => 'PhragmentConduitAPIMethod', 'PhragmentHistoryController' => 'PhragmentController', 'PhragmentPatchController' => 'PhragmentController', 'PhragmentPatchUtil' => 'Phobject', 'PhragmentPolicyController' => 'PhragmentController', 'PhragmentQueryFragmentsConduitAPIMethod' => 'PhragmentConduitAPIMethod', 'PhragmentRevertController' => 'PhragmentController', 'PhragmentSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhragmentSnapshot' => array( 'PhragmentDAO', 'PhabricatorPolicyInterface', ), 'PhragmentSnapshotChild' => array( 'PhragmentDAO', 'PhabricatorPolicyInterface', ), 'PhragmentSnapshotChildQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhragmentSnapshotCreateController' => 'PhragmentController', 'PhragmentSnapshotDeleteController' => 'PhragmentController', 'PhragmentSnapshotPHIDType' => 'PhabricatorPHIDType', 'PhragmentSnapshotPromoteController' => 'PhragmentController', 'PhragmentSnapshotQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhragmentSnapshotViewController' => 'PhragmentController', 'PhragmentUpdateController' => 'PhragmentController', 'PhragmentVersionController' => 'PhragmentController', 'PhragmentZIPController' => 'PhragmentController', 'PhrequentConduitAPIMethod' => 'ConduitAPIMethod', 'PhrequentController' => 'PhabricatorController', 'PhrequentCurtainExtension' => 'PHUICurtainExtension', 'PhrequentDAO' => 'PhabricatorLiskDAO', 'PhrequentListController' => 'PhrequentController', 'PhrequentPopConduitAPIMethod' => 'PhrequentConduitAPIMethod', 'PhrequentPushConduitAPIMethod' => 'PhrequentConduitAPIMethod', 'PhrequentSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhrequentTimeBlock' => 'Phobject', 'PhrequentTimeBlockTestCase' => 'PhabricatorTestCase', 'PhrequentTimeSlices' => 'Phobject', 'PhrequentTrackController' => 'PhrequentController', 'PhrequentTrackingConduitAPIMethod' => 'PhrequentConduitAPIMethod', 'PhrequentTrackingEditor' => 'PhabricatorEditor', 'PhrequentUIEventListener' => 'PhabricatorEventListener', 'PhrequentUserTime' => array( 'PhrequentDAO', 'PhabricatorPolicyInterface', ), 'PhrequentUserTimeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhrictionChangeType' => 'PhrictionConstants', 'PhrictionConduitAPIMethod' => 'ConduitAPIMethod', 'PhrictionConstants' => 'Phobject', 'PhrictionContent' => array( 'PhrictionDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', 'PhabricatorConduitResultInterface', ), 'PhrictionContentPHIDType' => 'PhabricatorPHIDType', 'PhrictionContentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhrictionContentSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'PhrictionContentSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhrictionContentSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'PhrictionController' => 'PhabricatorController', 'PhrictionCreateConduitAPIMethod' => 'PhrictionConduitAPIMethod', 'PhrictionDAO' => 'PhabricatorLiskDAO', 'PhrictionDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension', 'PhrictionDeleteController' => 'PhrictionController', 'PhrictionDiffController' => 'PhrictionController', 'PhrictionDocument' => array( 'PhrictionDAO', 'PhabricatorPolicyInterface', 'PhabricatorSubscribableInterface', 'PhabricatorFlaggableInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorDestructibleInterface', 'PhabricatorFulltextInterface', 'PhabricatorFerretInterface', 'PhabricatorProjectInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorConduitResultInterface', 'PhabricatorPolicyCodexInterface', 'PhabricatorSpacesInterface', ), 'PhrictionDocumentAuthorHeraldField' => 'PhrictionDocumentHeraldField', 'PhrictionDocumentContentHeraldField' => 'PhrictionDocumentHeraldField', 'PhrictionDocumentContentTransaction' => 'PhrictionDocumentEditTransaction', 'PhrictionDocumentController' => 'PhrictionController', 'PhrictionDocumentDatasource' => 'PhabricatorTypeaheadDatasource', 'PhrictionDocumentDeleteTransaction' => 'PhrictionDocumentVersionTransaction', 'PhrictionDocumentDraftTransaction' => 'PhrictionDocumentEditTransaction', 'PhrictionDocumentEditEngine' => 'PhabricatorEditEngine', 'PhrictionDocumentEditTransaction' => 'PhrictionDocumentVersionTransaction', 'PhrictionDocumentFerretEngine' => 'PhabricatorFerretEngine', 'PhrictionDocumentFulltextEngine' => 'PhabricatorFulltextEngine', 'PhrictionDocumentHeraldAdapter' => 'HeraldAdapter', 'PhrictionDocumentHeraldField' => 'HeraldField', 'PhrictionDocumentHeraldFieldGroup' => 'HeraldFieldGroup', 'PhrictionDocumentMoveAwayTransaction' => 'PhrictionDocumentVersionTransaction', 'PhrictionDocumentMoveToTransaction' => 'PhrictionDocumentVersionTransaction', 'PhrictionDocumentPHIDType' => 'PhabricatorPHIDType', 'PhrictionDocumentPathHeraldField' => 'PhrictionDocumentHeraldField', 'PhrictionDocumentPolicyCodex' => 'PhabricatorPolicyCodex', 'PhrictionDocumentPublishTransaction' => 'PhrictionDocumentTransactionType', 'PhrictionDocumentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhrictionDocumentSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'PhrictionDocumentSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhrictionDocumentStatus' => 'PhabricatorObjectStatus', 'PhrictionDocumentTitleHeraldField' => 'PhrictionDocumentHeraldField', 'PhrictionDocumentTitleTransaction' => 'PhrictionDocumentVersionTransaction', 'PhrictionDocumentTransactionType' => 'PhabricatorModularTransactionType', 'PhrictionDocumentVersionTransaction' => 'PhrictionDocumentTransactionType', 'PhrictionEditConduitAPIMethod' => 'PhrictionConduitAPIMethod', 'PhrictionEditController' => 'PhrictionController', 'PhrictionEditEngineController' => 'PhrictionController', 'PhrictionHistoryConduitAPIMethod' => 'PhrictionConduitAPIMethod', 'PhrictionHistoryController' => 'PhrictionController', 'PhrictionInfoConduitAPIMethod' => 'PhrictionConduitAPIMethod', 'PhrictionListController' => 'PhrictionController', 'PhrictionMarkupPreviewController' => 'PhabricatorController', 'PhrictionMoveController' => 'PhrictionController', 'PhrictionNewController' => 'PhrictionController', 'PhrictionPublishController' => 'PhrictionController', 'PhrictionRemarkupRule' => 'PhutilRemarkupRule', 'PhrictionReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PhrictionSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhrictionTransaction' => 'PhabricatorModularTransaction', 'PhrictionTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PhrictionTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'PhrictionTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PolicyLockOptionType' => 'PhabricatorConfigJSONOptionType', 'PonderAddAnswerView' => 'AphrontView', 'PonderAnswer' => array( 'PonderDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorMarkupInterface', 'PhabricatorPolicyInterface', 'PhabricatorFlaggableInterface', 'PhabricatorSubscribableInterface', 'PhabricatorDestructibleInterface', ), 'PonderAnswerCommentController' => 'PonderController', 'PonderAnswerContentTransaction' => 'PonderAnswerTransactionType', 'PonderAnswerEditController' => 'PonderController', 'PonderAnswerEditor' => 'PonderEditor', 'PonderAnswerHistoryController' => 'PonderController', 'PonderAnswerMailReceiver' => 'PhabricatorObjectMailReceiver', 'PonderAnswerPHIDType' => 'PhabricatorPHIDType', 'PonderAnswerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PonderAnswerQuestionIDTransaction' => 'PonderAnswerTransactionType', 'PonderAnswerReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PonderAnswerSaveController' => 'PonderController', 'PonderAnswerStatus' => 'PonderConstants', 'PonderAnswerStatusTransaction' => 'PonderAnswerTransactionType', 'PonderAnswerTransaction' => 'PhabricatorModularTransaction', 'PonderAnswerTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PonderAnswerTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PonderAnswerTransactionType' => 'PhabricatorModularTransactionType', 'PonderAnswerView' => 'AphrontTagView', 'PonderConstants' => 'Phobject', 'PonderController' => 'PhabricatorController', 'PonderDAO' => 'PhabricatorLiskDAO', 'PonderDefaultViewCapability' => 'PhabricatorPolicyCapability', 'PonderEditor' => 'PhabricatorApplicationTransactionEditor', 'PonderFooterView' => 'AphrontTagView', 'PonderModerateCapability' => 'PhabricatorPolicyCapability', 'PonderQuestion' => array( 'PonderDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorMarkupInterface', 'PhabricatorSubscribableInterface', 'PhabricatorFlaggableInterface', 'PhabricatorPolicyInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorProjectInterface', 'PhabricatorDestructibleInterface', 'PhabricatorSpacesInterface', 'PhabricatorFulltextInterface', 'PhabricatorFerretInterface', ), 'PonderQuestionAnswerTransaction' => 'PonderQuestionTransactionType', 'PonderQuestionAnswerWikiTransaction' => 'PonderQuestionTransactionType', 'PonderQuestionCommentController' => 'PonderController', 'PonderQuestionContentTransaction' => 'PonderQuestionTransactionType', 'PonderQuestionCreateMailReceiver' => 'PhabricatorMailReceiver', 'PonderQuestionEditController' => 'PonderController', 'PonderQuestionEditEngine' => 'PhabricatorEditEngine', 'PonderQuestionEditor' => 'PonderEditor', 'PonderQuestionFerretEngine' => 'PhabricatorFerretEngine', 'PonderQuestionFulltextEngine' => 'PhabricatorFulltextEngine', 'PonderQuestionHistoryController' => 'PonderController', 'PonderQuestionListController' => 'PonderController', 'PonderQuestionMailReceiver' => 'PhabricatorObjectMailReceiver', 'PonderQuestionPHIDType' => 'PhabricatorPHIDType', 'PonderQuestionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PonderQuestionReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PonderQuestionSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PonderQuestionStatus' => 'PonderConstants', 'PonderQuestionStatusController' => 'PonderController', 'PonderQuestionStatusTransaction' => 'PonderQuestionTransactionType', 'PonderQuestionTitleTransaction' => 'PonderQuestionTransactionType', 'PonderQuestionTransaction' => 'PhabricatorModularTransaction', 'PonderQuestionTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PonderQuestionTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PonderQuestionTransactionType' => 'PhabricatorModularTransactionType', 'PonderQuestionViewController' => 'PonderController', 'PonderRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PonderSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'ProjectAddProjectsEmailCommand' => 'MetaMTAEmailTransactionCommand', 'ProjectBoardTaskCard' => 'Phobject', 'ProjectCanLockProjectsCapability' => 'PhabricatorPolicyCapability', 'ProjectColumnSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'ProjectConduitAPIMethod' => 'ConduitAPIMethod', 'ProjectCreateConduitAPIMethod' => 'ProjectConduitAPIMethod', 'ProjectCreateProjectsCapability' => 'PhabricatorPolicyCapability', 'ProjectDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension', 'ProjectDefaultEditCapability' => 'PhabricatorPolicyCapability', 'ProjectDefaultJoinCapability' => 'PhabricatorPolicyCapability', 'ProjectDefaultViewCapability' => 'PhabricatorPolicyCapability', 'ProjectEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'ProjectQueryConduitAPIMethod' => 'ProjectConduitAPIMethod', 'ProjectRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'ProjectRemarkupRuleTestCase' => 'PhabricatorTestCase', 'ProjectReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'ProjectSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'QueryFormattingTestCase' => 'PhabricatorTestCase', 'ReleephAuthorFieldSpecification' => 'ReleephFieldSpecification', 'ReleephBranch' => array( 'ReleephDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', ), 'ReleephBranchAccessController' => 'ReleephBranchController', 'ReleephBranchCommitFieldSpecification' => 'ReleephFieldSpecification', 'ReleephBranchController' => 'ReleephController', 'ReleephBranchCreateController' => 'ReleephProductController', 'ReleephBranchEditController' => 'ReleephBranchController', 'ReleephBranchEditor' => 'PhabricatorEditor', 'ReleephBranchHistoryController' => 'ReleephBranchController', 'ReleephBranchNamePreviewController' => 'ReleephController', 'ReleephBranchPHIDType' => 'PhabricatorPHIDType', 'ReleephBranchPreviewView' => 'AphrontFormControl', 'ReleephBranchQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'ReleephBranchSearchEngine' => 'PhabricatorApplicationSearchEngine', 'ReleephBranchTemplate' => 'Phobject', 'ReleephBranchTransaction' => 'PhabricatorApplicationTransaction', 'ReleephBranchTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'ReleephBranchViewController' => 'ReleephBranchController', 'ReleephCommitFinder' => 'Phobject', 'ReleephCommitFinderException' => 'Exception', 'ReleephCommitMessageFieldSpecification' => 'ReleephFieldSpecification', 'ReleephConduitAPIMethod' => 'ConduitAPIMethod', 'ReleephController' => 'PhabricatorController', 'ReleephDAO' => 'PhabricatorLiskDAO', 'ReleephDefaultFieldSelector' => 'ReleephFieldSelector', 'ReleephDependsOnFieldSpecification' => 'ReleephFieldSpecification', 'ReleephDiffChurnFieldSpecification' => 'ReleephFieldSpecification', 'ReleephDiffMessageFieldSpecification' => 'ReleephFieldSpecification', 'ReleephDiffSizeFieldSpecification' => 'ReleephFieldSpecification', 'ReleephFieldParseException' => 'Exception', 'ReleephFieldSelector' => 'Phobject', 'ReleephFieldSpecification' => array( 'PhabricatorCustomField', 'PhabricatorMarkupInterface', ), 'ReleephGetBranchesConduitAPIMethod' => 'ReleephConduitAPIMethod', 'ReleephIntentFieldSpecification' => 'ReleephFieldSpecification', 'ReleephLevelFieldSpecification' => 'ReleephFieldSpecification', 'ReleephOriginalCommitFieldSpecification' => 'ReleephFieldSpecification', 'ReleephProductActionController' => 'ReleephProductController', 'ReleephProductController' => 'ReleephController', 'ReleephProductCreateController' => 'ReleephProductController', 'ReleephProductEditController' => 'ReleephProductController', 'ReleephProductEditor' => 'PhabricatorApplicationTransactionEditor', 'ReleephProductHistoryController' => 'ReleephProductController', 'ReleephProductListController' => 'ReleephController', 'ReleephProductPHIDType' => 'PhabricatorPHIDType', 'ReleephProductQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'ReleephProductSearchEngine' => 'PhabricatorApplicationSearchEngine', 'ReleephProductTransaction' => 'PhabricatorApplicationTransaction', 'ReleephProductTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'ReleephProductViewController' => 'ReleephProductController', 'ReleephProject' => array( 'ReleephDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', ), 'ReleephQueryBranchesConduitAPIMethod' => 'ReleephConduitAPIMethod', 'ReleephQueryProductsConduitAPIMethod' => 'ReleephConduitAPIMethod', 'ReleephQueryRequestsConduitAPIMethod' => 'ReleephConduitAPIMethod', 'ReleephReasonFieldSpecification' => 'ReleephFieldSpecification', 'ReleephRequest' => array( 'ReleephDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorCustomFieldInterface', ), 'ReleephRequestActionController' => 'ReleephRequestController', 'ReleephRequestCommentController' => 'ReleephRequestController', 'ReleephRequestConduitAPIMethod' => 'ReleephConduitAPIMethod', 'ReleephRequestController' => 'ReleephController', 'ReleephRequestDifferentialCreateController' => 'ReleephController', 'ReleephRequestEditController' => 'ReleephBranchController', 'ReleephRequestMailReceiver' => 'PhabricatorObjectMailReceiver', 'ReleephRequestPHIDType' => 'PhabricatorPHIDType', 'ReleephRequestQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'ReleephRequestReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'ReleephRequestSearchEngine' => 'PhabricatorApplicationSearchEngine', 'ReleephRequestStatus' => 'Phobject', 'ReleephRequestTransaction' => 'PhabricatorApplicationTransaction', 'ReleephRequestTransactionComment' => 'PhabricatorApplicationTransactionComment', 'ReleephRequestTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'ReleephRequestTransactionalEditor' => 'PhabricatorApplicationTransactionEditor', 'ReleephRequestTypeaheadControl' => 'AphrontFormControl', 'ReleephRequestTypeaheadController' => 'PhabricatorTypeaheadDatasourceController', 'ReleephRequestView' => 'AphrontView', 'ReleephRequestViewController' => 'ReleephBranchController', 'ReleephRequestorFieldSpecification' => 'ReleephFieldSpecification', 'ReleephRevisionFieldSpecification' => 'ReleephFieldSpecification', 'ReleephSeverityFieldSpecification' => 'ReleephLevelFieldSpecification', 'ReleephSummaryFieldSpecification' => 'ReleephFieldSpecification', 'ReleephWorkCanPushConduitAPIMethod' => 'ReleephConduitAPIMethod', 'ReleephWorkGetAuthorInfoConduitAPIMethod' => 'ReleephConduitAPIMethod', 'ReleephWorkGetBranchCommitMessageConduitAPIMethod' => 'ReleephConduitAPIMethod', 'ReleephWorkGetBranchConduitAPIMethod' => 'ReleephConduitAPIMethod', 'ReleephWorkGetCommitMessageConduitAPIMethod' => 'ReleephConduitAPIMethod', 'ReleephWorkNextRequestConduitAPIMethod' => 'ReleephConduitAPIMethod', 'ReleephWorkRecordConduitAPIMethod' => 'ReleephConduitAPIMethod', 'ReleephWorkRecordPickStatusConduitAPIMethod' => 'ReleephConduitAPIMethod', 'RemarkupProcessConduitAPIMethod' => 'ConduitAPIMethod', 'RepositoryConduitAPIMethod' => 'ConduitAPIMethod', 'RepositoryQueryConduitAPIMethod' => 'RepositoryConduitAPIMethod', 'ShellLogView' => 'AphrontView', 'SlowvoteConduitAPIMethod' => 'ConduitAPIMethod', 'SlowvoteEmbedView' => 'AphrontView', 'SlowvoteInfoConduitAPIMethod' => 'SlowvoteConduitAPIMethod', 'SlowvoteRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'SubscriptionListDialogBuilder' => 'Phobject', 'SubscriptionListStringBuilder' => 'Phobject', 'TokenConduitAPIMethod' => 'ConduitAPIMethod', 'TokenGiveConduitAPIMethod' => 'TokenConduitAPIMethod', 'TokenGivenConduitAPIMethod' => 'TokenConduitAPIMethod', 'TokenQueryConduitAPIMethod' => 'TokenConduitAPIMethod', 'TransactionSearchConduitAPIMethod' => 'ConduitAPIMethod', 'UserConduitAPIMethod' => 'ConduitAPIMethod', 'UserDisableConduitAPIMethod' => 'UserConduitAPIMethod', 'UserEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'UserEnableConduitAPIMethod' => 'UserConduitAPIMethod', 'UserFindConduitAPIMethod' => 'UserConduitAPIMethod', 'UserQueryConduitAPIMethod' => 'UserConduitAPIMethod', 'UserSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'UserWhoAmIConduitAPIMethod' => 'UserConduitAPIMethod', ), )); diff --git a/src/aphront/handler/PhabricatorPolicyRequestExceptionHandler.php b/src/aphront/handler/PhabricatorPolicyRequestExceptionHandler.php index cb1dea3533..442cb08f66 100644 --- a/src/aphront/handler/PhabricatorPolicyRequestExceptionHandler.php +++ b/src/aphront/handler/PhabricatorPolicyRequestExceptionHandler.php @@ -1,95 +1,107 @@ isPhabricatorSite($request)) { return false; } return ($throwable instanceof PhabricatorPolicyException); } public function handleRequestThrowable( AphrontRequest $request, $throwable) { $viewer = $this->getViewer($request); if (!$viewer->isLoggedIn()) { // If the user isn't logged in, just give them a login form. This is // probably a generally more useful response than a policy dialog that // they have to click through to get a login form. // // Possibly we should add a header here like "you need to login to see // the thing you are trying to look at". $auth_app_class = 'PhabricatorAuthApplication'; $auth_app = PhabricatorApplication::getByClass($auth_app_class); return id(new PhabricatorAuthStartController()) ->setRequest($request) ->setCurrentApplication($auth_app) ->handleRequest($request); } $content = array( phutil_tag( 'div', array( 'class' => 'aphront-policy-rejection', ), $throwable->getRejection()), ); $list = null; if ($throwable->getCapabilityName()) { $list = $throwable->getMoreInfo(); foreach ($list as $key => $item) { $list[$key] = $item; } $content[] = phutil_tag( 'div', array( 'class' => 'aphront-capability-details', ), pht( 'Users with the "%s" capability:', $throwable->getCapabilityName())); } $dialog = id(new AphrontDialogView()) ->setTitle($throwable->getTitle()) ->setClass('aphront-access-dialog') ->setUser($viewer) ->appendChild($content); if ($list) { $dialog->appendList($list); } + // If the install is in developer mode, include a stack trace for the + // exception. When debugging things, it isn't always obvious where a + // policy exception came from and this can make it easier to hunt down + // bugs or improve ambiguous/confusing messaging. + + $is_developer = PhabricatorEnv::getEnvConfig('phabricator.developer-mode'); + if ($is_developer) { + $dialog->appendChild( + id(new AphrontStackTraceView()) + ->setTrace($throwable->getTrace())); + } + if ($request->isAjax()) { $dialog->addCancelButton('/', pht('Close')); } else { $dialog->addCancelButton('/', pht('OK')); } return $dialog; } } diff --git a/src/applications/almanac/editor/AlmanacBindingEditEngine.php b/src/applications/almanac/editor/AlmanacBindingEditEngine.php index 5146578fff..66db7fcbab 100644 --- a/src/applications/almanac/editor/AlmanacBindingEditEngine.php +++ b/src/applications/almanac/editor/AlmanacBindingEditEngine.php @@ -1,172 +1,172 @@ service = $service; return $this; } public function getService() { if (!$this->service) { throw new PhutilInvalidStateException('setService'); } return $this->service; } public function isEngineConfigurable() { return false; } public function getEngineName() { return pht('Almanac Bindings'); } public function getSummaryHeader() { return pht('Edit Almanac Binding Configurations'); } public function getSummaryText() { return pht('This engine is used to edit Almanac bindings.'); } public function getEngineApplicationClass() { return 'PhabricatorAlmanacApplication'; } protected function newEditableObject() { $service = $this->getService(); return AlmanacBinding::initializeNewBinding($service); } protected function newEditableObjectForDocumentation() { $service_type = AlmanacCustomServiceType::SERVICETYPE; $service = AlmanacService::initializeNewService($service_type); $this->setService($service); return $this->newEditableObject(); } protected function newEditableObjectFromConduit(array $raw_xactions) { $service_phid = null; foreach ($raw_xactions as $raw_xaction) { if ($raw_xaction['type'] !== 'service') { continue; } $service_phid = $raw_xaction['value']; } if ($service_phid === null) { throw new Exception( pht( 'When creating a new Almanac binding via the Conduit API, you '. 'must provide a "service" transaction to select a service to bind.')); } $service = id(new AlmanacServiceQuery()) ->setViewer($this->getViewer()) ->withPHIDs(array($service_phid)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$service) { throw new Exception( pht( 'Service "%s" is unrecognized, restricted, or you do not have '. 'permission to edit it.', $service_phid)); } $this->setService($service); return $this->newEditableObject(); } protected function newObjectQuery() { return id(new AlmanacBindingQuery()) ->needProperties(true); } protected function getObjectCreateTitleText($object) { return pht('Create Binding'); } protected function getObjectCreateButtonText($object) { return pht('Create Binding'); } protected function getObjectEditTitleText($object) { return pht('Edit Binding'); } protected function getObjectEditShortText($object) { return pht('Edit Binding'); } protected function getObjectCreateShortText() { return pht('Create Binding'); } protected function getObjectName() { return pht('Binding'); } protected function getEditorURI() { return '/almanac/binding/edit/'; } protected function getObjectCreateCancelURI($object) { return '/almanac/binding/'; } protected function getObjectViewURI($object) { return $object->getURI(); } protected function buildCustomEditFields($object) { return array( id(new PhabricatorTextEditField()) ->setKey('service') ->setLabel(pht('Service')) - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setTransactionType( AlmanacBindingServiceTransaction::TRANSACTIONTYPE) ->setDescription(pht('Service to create a binding for.')) ->setConduitDescription(pht('Select the service to bind.')) ->setConduitTypeDescription(pht('Service PHID.')) ->setValue($object->getServicePHID()), id(new PhabricatorTextEditField()) ->setKey('interface') ->setLabel(pht('Interface')) - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setTransactionType( AlmanacBindingInterfaceTransaction::TRANSACTIONTYPE) ->setDescription(pht('Interface to bind the service to.')) ->setConduitDescription(pht('Set the interface to bind.')) ->setConduitTypeDescription(pht('Interface PHID.')) ->setValue($object->getInterfacePHID()), id(new PhabricatorBoolEditField()) ->setKey('disabled') ->setLabel(pht('Disabled')) - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setTransactionType( AlmanacBindingDisableTransaction::TRANSACTIONTYPE) ->setDescription(pht('Disable or enable the binding.')) ->setConduitDescription(pht('Disable or enable the binding.')) ->setConduitTypeDescription(pht('True to disable the binding.')) ->setValue($object->getIsDisabled()) ->setOptions( pht('Enable Binding'), pht('Disable Binding')), ); } } diff --git a/src/applications/almanac/editor/AlmanacInterfaceEditEngine.php b/src/applications/almanac/editor/AlmanacInterfaceEditEngine.php index 30c371d6f9..ca57113bf2 100644 --- a/src/applications/almanac/editor/AlmanacInterfaceEditEngine.php +++ b/src/applications/almanac/editor/AlmanacInterfaceEditEngine.php @@ -1,186 +1,186 @@ device = $device; return $this; } public function getDevice() { if (!$this->device) { throw new PhutilInvalidStateException('setDevice'); } return $this->device; } public function isEngineConfigurable() { return false; } public function getEngineName() { return pht('Almanac Interfaces'); } public function getSummaryHeader() { return pht('Edit Almanac Interface Configurations'); } public function getSummaryText() { return pht('This engine is used to edit Almanac interfaces.'); } public function getEngineApplicationClass() { return 'PhabricatorAlmanacApplication'; } protected function newEditableObject() { $interface = AlmanacInterface::initializeNewInterface(); $device = $this->getDevice(); $interface ->setDevicePHID($device->getPHID()) ->attachDevice($device); return $interface; } protected function newEditableObjectForDocumentation() { $this->setDevice(new AlmanacDevice()); return $this->newEditableObject(); } protected function newEditableObjectFromConduit(array $raw_xactions) { $device_phid = null; foreach ($raw_xactions as $raw_xaction) { if ($raw_xaction['type'] !== 'device') { continue; } $device_phid = $raw_xaction['value']; } if ($device_phid === null) { throw new Exception( pht( 'When creating a new Almanac interface via the Conduit API, you '. 'must provide a "device" transaction to select a device.')); } $device = id(new AlmanacDeviceQuery()) ->setViewer($this->getViewer()) ->withPHIDs(array($device_phid)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$device) { throw new Exception( pht( 'Device "%s" is unrecognized, restricted, or you do not have '. 'permission to edit it.', $device_phid)); } $this->setDevice($device); return $this->newEditableObject(); } protected function newObjectQuery() { return new AlmanacInterfaceQuery(); } protected function getObjectCreateTitleText($object) { return pht('Create Interface'); } protected function getObjectCreateButtonText($object) { return pht('Create Interface'); } protected function getObjectEditTitleText($object) { return pht('Edit Interface'); } protected function getObjectEditShortText($object) { return pht('Edit Interface'); } protected function getObjectCreateShortText() { return pht('Create Interface'); } protected function getObjectName() { return pht('Interface'); } protected function getEditorURI() { return '/almanac/interface/edit/'; } protected function getObjectCreateCancelURI($object) { if ($this->getDevice()) { return $this->getDevice()->getURI(); } return '/almanac/interface/'; } protected function getObjectViewURI($object) { return $object->getDevice()->getURI(); } protected function buildCustomEditFields($object) { $viewer = $this->getViewer(); // TODO: Some day, this should be a datasource. $networks = id(new AlmanacNetworkQuery()) ->setViewer($viewer) ->execute(); $network_map = mpull($networks, 'getName', 'getPHID'); return array( id(new PhabricatorTextEditField()) ->setKey('device') ->setLabel(pht('Device')) - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setTransactionType( AlmanacInterfaceDeviceTransaction::TRANSACTIONTYPE) ->setDescription(pht('When creating an interface, set the device.')) ->setConduitDescription(pht('Set the device.')) ->setConduitTypeDescription(pht('Device PHID.')) ->setValue($object->getDevicePHID()), id(new PhabricatorSelectEditField()) ->setKey('network') ->setLabel(pht('Network')) ->setDescription(pht('Network for the interface.')) ->setTransactionType( AlmanacInterfaceNetworkTransaction::TRANSACTIONTYPE) ->setValue($object->getNetworkPHID()) ->setOptions($network_map), id(new PhabricatorTextEditField()) ->setKey('address') ->setLabel(pht('Address')) ->setDescription(pht('Address of the service.')) ->setTransactionType( AlmanacInterfaceAddressTransaction::TRANSACTIONTYPE) ->setIsRequired(true) ->setValue($object->getAddress()), id(new PhabricatorIntEditField()) ->setKey('port') ->setLabel(pht('Port')) ->setDescription(pht('Port of the service.')) ->setTransactionType(AlmanacInterfacePortTransaction::TRANSACTIONTYPE) ->setIsRequired(true) ->setValue($object->getPort()), ); } } diff --git a/src/applications/almanac/editor/AlmanacServiceEditEngine.php b/src/applications/almanac/editor/AlmanacServiceEditEngine.php index 00e54962b3..b63075543f 100644 --- a/src/applications/almanac/editor/AlmanacServiceEditEngine.php +++ b/src/applications/almanac/editor/AlmanacServiceEditEngine.php @@ -1,149 +1,149 @@ serviceType = $service_type; return $this; } public function getServiceType() { return $this->serviceType; } public function isEngineConfigurable() { return false; } public function getEngineName() { return pht('Almanac Services'); } public function getSummaryHeader() { return pht('Edit Almanac Service Configurations'); } public function getSummaryText() { return pht('This engine is used to edit Almanac services.'); } public function getEngineApplicationClass() { return 'PhabricatorAlmanacApplication'; } protected function newEditableObject() { $service_type = $this->getServiceType(); return AlmanacService::initializeNewService($service_type); } protected function newEditableObjectFromConduit(array $raw_xactions) { $type = null; foreach ($raw_xactions as $raw_xaction) { if ($raw_xaction['type'] !== 'type') { continue; } $type = $raw_xaction['value']; } if ($type === null) { throw new Exception( pht( 'When creating a new Almanac service via the Conduit API, you '. 'must provide a "type" transaction to select a type.')); } $map = AlmanacServiceType::getAllServiceTypes(); if (!isset($map[$type])) { throw new Exception( pht( 'Service type "%s" is unrecognized. Valid types are: %s.', $type, implode(', ', array_keys($map)))); } $this->setServiceType($type); return $this->newEditableObject(); } protected function newEditableObjectForDocumentation() { $service_type = new AlmanacCustomServiceType(); $this->setServiceType($service_type->getServiceTypeConstant()); return $this->newEditableObject(); } protected function newObjectQuery() { return id(new AlmanacServiceQuery()) ->needProperties(true); } protected function getObjectCreateTitleText($object) { return pht('Create Service'); } protected function getObjectCreateButtonText($object) { return pht('Create Service'); } protected function getObjectEditTitleText($object) { return pht('Edit Service: %s', $object->getName()); } protected function getObjectEditShortText($object) { return pht('Edit Service'); } protected function getObjectCreateShortText() { return pht('Create Service'); } protected function getObjectName() { return pht('Service'); } protected function getEditorURI() { return '/almanac/service/edit/'; } protected function getObjectCreateCancelURI($object) { return '/almanac/service/'; } protected function getObjectViewURI($object) { return $object->getURI(); } protected function getCreateNewObjectPolicy() { return $this->getApplication()->getPolicy( AlmanacCreateServicesCapability::CAPABILITY); } protected function buildCustomEditFields($object) { return array( id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) ->setDescription(pht('Name of the service.')) ->setTransactionType(AlmanacServiceNameTransaction::TRANSACTIONTYPE) ->setIsRequired(true) ->setValue($object->getName()), id(new PhabricatorTextEditField()) ->setKey('type') ->setLabel(pht('Type')) - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setTransactionType( AlmanacServiceTypeTransaction::TRANSACTIONTYPE) ->setDescription(pht('When creating a service, set the type.')) ->setConduitDescription(pht('Set the service type.')) ->setConduitTypeDescription(pht('Service type.')) ->setValue($object->getServiceType()), ); } } diff --git a/src/applications/almanac/engineextension/AlmanacPropertiesEditEngineExtension.php b/src/applications/almanac/engineextension/AlmanacPropertiesEditEngineExtension.php index 965c193f40..425ee04b3c 100644 --- a/src/applications/almanac/engineextension/AlmanacPropertiesEditEngineExtension.php +++ b/src/applications/almanac/engineextension/AlmanacPropertiesEditEngineExtension.php @@ -1,44 +1,44 @@ setKey('property.set') ->setTransactionType($object->getAlmanacPropertySetTransactionType()) ->setConduitDescription( pht('Pass a map of values to set one or more properties.')) ->setConduitTypeDescription(pht('Map of property names to values.')) - ->setIsConduitOnly(true), + ->setIsFormField(false), id(new AlmanacDeletePropertyEditField()) ->setKey('property.delete') ->setTransactionType($object->getAlmanacPropertyDeleteTransactionType()) ->setConduitDescription( pht('Pass a list of property names to delete properties.')) ->setConduitTypeDescription(pht('List of property names.')) - ->setIsConduitOnly(true), + ->setIsFormField(false), ); } } diff --git a/src/applications/almanac/query/AlmanacInterfaceQuery.php b/src/applications/almanac/query/AlmanacInterfaceQuery.php index bb6fc2f9d9..d5886761c1 100644 --- a/src/applications/almanac/query/AlmanacInterfaceQuery.php +++ b/src/applications/almanac/query/AlmanacInterfaceQuery.php @@ -1,200 +1,200 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withNetworkPHIDs(array $phids) { $this->networkPHIDs = $phids; return $this; } public function withDevicePHIDs(array $phids) { $this->devicePHIDs = $phids; return $this; } public function withAddresses(array $addresses) { $this->addresses = $addresses; return $this; } public function newResultObject() { return new AlmanacInterface(); } protected function loadPage() { return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $interfaces) { $network_phids = mpull($interfaces, 'getNetworkPHID'); $device_phids = mpull($interfaces, 'getDevicePHID'); $networks = id(new AlmanacNetworkQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($network_phids) ->needProperties($this->getNeedProperties()) ->execute(); $networks = mpull($networks, null, 'getPHID'); $devices = id(new AlmanacDeviceQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($device_phids) ->needProperties($this->getNeedProperties()) ->execute(); $devices = mpull($devices, null, 'getPHID'); foreach ($interfaces as $key => $interface) { $network = idx($networks, $interface->getNetworkPHID()); $device = idx($devices, $interface->getDevicePHID()); if (!$network || !$device) { $this->didRejectResult($interface); unset($interfaces[$key]); continue; } $interface->attachNetwork($network); $interface->attachDevice($device); } return $interfaces; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'interface.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'interface.phid IN (%Ls)', $this->phids); } if ($this->networkPHIDs !== null) { $where[] = qsprintf( $conn, 'interface.networkPHID IN (%Ls)', $this->networkPHIDs); } if ($this->devicePHIDs !== null) { $where[] = qsprintf( $conn, 'interface.devicePHID IN (%Ls)', $this->devicePHIDs); } if ($this->addresses !== null) { $parts = array(); foreach ($this->addresses as $address) { $parts[] = qsprintf( $conn, '(interface.networkPHID = %s '. 'AND interface.address = %s '. 'AND interface.port = %d)', $address->getNetworkPHID(), $address->getAddress(), $address->getPort()); } - $where[] = implode(' OR ', $parts); + $where[] = qsprintf($conn, '%LO', $parts); } return $where; } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { $joins = parent::buildJoinClauseParts($conn); if ($this->shouldJoinDeviceTable()) { $joins[] = qsprintf( $conn, 'JOIN %T device ON device.phid = interface.devicePHID', id(new AlmanacDevice())->getTableName()); } return $joins; } protected function shouldGroupQueryResultRows() { if ($this->shouldJoinDeviceTable()) { return true; } return parent::shouldGroupQueryResultRows(); } private function shouldJoinDeviceTable() { $vector = $this->getOrderVector(); if ($vector->containsKey('name')) { return true; } return false; } protected function getPrimaryTableAlias() { return 'interface'; } public function getQueryApplicationClass() { return 'PhabricatorAlmanacApplication'; } public function getBuiltinOrders() { return array( 'name' => array( 'vector' => array('name', 'id'), 'name' => pht('Device Name'), ), ) + parent::getBuiltinOrders(); } public function getOrderableColumns() { return parent::getOrderableColumns() + array( 'name' => array( 'table' => 'device', 'column' => 'name', 'type' => 'string', 'reverse' => true, ), ); } protected function getPagingValueMap($cursor, array $keys) { $interface = $this->loadCursorObject($cursor); $map = array( 'id' => $interface->getID(), 'name' => $interface->getDevice()->getName(), ); return $map; } } diff --git a/src/applications/audit/query/PhabricatorCommitSearchEngine.php b/src/applications/audit/query/PhabricatorCommitSearchEngine.php index ee5105f193..e286573e93 100644 --- a/src/applications/audit/query/PhabricatorCommitSearchEngine.php +++ b/src/applications/audit/query/PhabricatorCommitSearchEngine.php @@ -1,255 +1,268 @@ needAuditRequests(true) ->needCommitData(true) ->needIdentities(true) ->needDrafts(true); } protected function newResultBuckets() { return DiffusionCommitResultBucket::getAllResultBuckets(); } protected function buildQueryFromParameters(array $map) { $query = $this->newQuery(); if ($map['responsiblePHIDs']) { $query->withResponsiblePHIDs($map['responsiblePHIDs']); } if ($map['auditorPHIDs']) { $query->withAuditorPHIDs($map['auditorPHIDs']); } if ($map['authorPHIDs']) { $query->withAuthorPHIDs($map['authorPHIDs']); } if ($map['statuses']) { $query->withStatuses($map['statuses']); } if ($map['repositoryPHIDs']) { $query->withRepositoryPHIDs($map['repositoryPHIDs']); } if ($map['packagePHIDs']) { $query->withPackagePHIDs($map['packagePHIDs']); } if ($map['unreachable'] !== null) { $query->withUnreachable($map['unreachable']); } if ($map['ancestorsOf']) { $query->withAncestorsOf($map['ancestorsOf']); } + if ($map['identifiers']) { + $query->withIdentifiers($map['identifiers']); + } + return $query; } protected function buildCustomSearchFields() { return array( id(new PhabricatorSearchDatasourceField()) ->setLabel(pht('Responsible Users')) ->setKey('responsiblePHIDs') ->setConduitKey('responsible') ->setAliases(array('responsible', 'responsibles', 'responsiblePHID')) ->setDatasource(new DifferentialResponsibleDatasource()) ->setDescription( pht( 'Find commits where given users, projects, or packages are '. 'responsible for the next steps in the audit workflow.')), id(new PhabricatorUsersSearchField()) ->setLabel(pht('Authors')) ->setKey('authorPHIDs') ->setConduitKey('authors') ->setAliases(array('author', 'authors', 'authorPHID')) ->setDescription(pht('Find commits authored by particular users.')), id(new PhabricatorSearchDatasourceField()) ->setLabel(pht('Auditors')) ->setKey('auditorPHIDs') ->setConduitKey('auditors') ->setAliases(array('auditor', 'auditors', 'auditorPHID')) ->setDatasource(new DiffusionAuditorFunctionDatasource()) ->setDescription( pht( 'Find commits where given users, projects, or packages are '. 'auditors.')), id(new PhabricatorSearchCheckboxesField()) ->setLabel(pht('Audit Status')) ->setKey('statuses') ->setAliases(array('status')) ->setOptions(DiffusionCommitAuditStatus::newOptions()) ->setDeprecatedOptions( DiffusionCommitAuditStatus::newDeprecatedOptions()) ->setDescription(pht('Find commits with given audit statuses.')), id(new PhabricatorSearchDatasourceField()) ->setLabel(pht('Repositories')) ->setKey('repositoryPHIDs') ->setConduitKey('repositories') ->setAliases(array('repository', 'repositories', 'repositoryPHID')) ->setDatasource(new DiffusionRepositoryFunctionDatasource()) ->setDescription(pht('Find commits in particular repositories.')), id(new PhabricatorSearchDatasourceField()) ->setLabel(pht('Packages')) ->setKey('packagePHIDs') ->setConduitKey('packages') ->setAliases(array('package', 'packages', 'packagePHID')) ->setDatasource(new PhabricatorOwnersPackageDatasource()) ->setDescription( pht('Find commits which affect given packages.')), id(new PhabricatorSearchThreeStateField()) ->setLabel(pht('Unreachable')) ->setKey('unreachable') ->setOptions( pht('(Show All)'), pht('Show Only Unreachable Commits'), pht('Hide Unreachable Commits')) ->setDescription( pht( 'Find or exclude unreachable commits which are not ancestors of '. 'any branch, tag, or ref.')), id(new PhabricatorSearchStringListField()) ->setLabel(pht('Ancestors Of')) ->setKey('ancestorsOf') ->setDescription( pht( 'Find commits which are ancestors of a particular ref, '. 'like "master".')), + id(new PhabricatorSearchStringListField()) + ->setLabel(pht('Identifiers')) + ->setKey('identifiers') + ->setDescription( + pht( + 'Find commits with particular identifiers (usually, hashes). '. + 'Supports full or partial identifiers (like "abcd12340987..." or '. + '"abcd1234") and qualified or unqualified identifiers (like '. + '"rXabcd1234" or "abcd1234").')), ); } protected function getURI($path) { return '/diffusion/commit/'.$path; } protected function getBuiltinQueryNames() { $names = array(); if ($this->requireViewer()->isLoggedIn()) { $names['active'] = pht('Active Audits'); $names['authored'] = pht('Authored'); $names['audited'] = pht('Audited'); } $names['all'] = pht('All Commits'); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); $viewer = $this->requireViewer(); $viewer_phid = $viewer->getPHID(); switch ($query_key) { case 'all': return $query; case 'active': $bucket_key = DiffusionCommitRequiredActionResultBucket::BUCKETKEY; $open = DiffusionCommitAuditStatus::getOpenStatusConstants(); $query ->setParameter('responsiblePHIDs', array($viewer_phid)) ->setParameter('statuses', $open) ->setParameter('bucket', $bucket_key) ->setParameter('unreachable', false); return $query; case 'authored': $query ->setParameter('authorPHIDs', array($viewer_phid)); return $query; case 'audited': $query ->setParameter('auditorPHIDs', array($viewer_phid)); return $query; } return parent::buildSavedQueryFromBuiltin($query_key); } protected function renderResultList( array $commits, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($commits, 'PhabricatorRepositoryCommit'); $viewer = $this->requireViewer(); $bucket = $this->getResultBucket($query); $template = id(new PhabricatorAuditListView()) ->setViewer($viewer) ->setShowDrafts(true); $views = array(); if ($bucket) { $bucket->setViewer($viewer); try { $groups = $bucket->newResultGroups($query, $commits); foreach ($groups as $group) { // 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()); } } else { $views[] = id(clone $template) ->setCommits($commits) ->setNoDataString(pht('No commits found.')); } if (!$views) { $views[] = id(new PhabricatorAuditListView()) ->setViewer($viewer) ->setNoDataString(pht('No commits found.')); } if (count($views) == 1) { $list = head($views)->buildList(); } else { $list = $views; } $result = new PhabricatorApplicationSearchResultView(); $result->setContent($list); return $result; } protected function getNewUserBody() { $view = id(new PHUIBigInfoView()) ->setIcon('fa-check-circle-o') ->setTitle(pht('Welcome to Audit')) ->setDescription( pht('Post-commit code review and auditing. Audits you are assigned '. 'to will appear here.')); return $view; } } diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php index 6e40cdde98..4ce86e8f97 100644 --- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php @@ -1,856 +1,912 @@ establishConnection('r'); $session_key = PhabricatorHash::weakDigest($session_token); $cache_parts = $this->getUserCacheQueryParts($conn_r); list($cache_selects, $cache_joins, $cache_map, $types_map) = $cache_parts; $info = queryfx_one( $conn_r, 'SELECT s.id AS s_id, s.sessionExpires AS s_sessionExpires, s.sessionStart AS s_sessionStart, s.highSecurityUntil AS s_highSecurityUntil, s.isPartial AS s_isPartial, s.signedLegalpadDocuments as s_signedLegalpadDocuments, u.* %Q FROM %T u JOIN %T s ON u.phid = s.userPHID - AND s.type = %s AND s.sessionKey = %s %Q', + AND s.type = %s AND s.sessionKey = %P %Q', $cache_selects, $user_table->getTableName(), $session_table->getTableName(), $session_type, - $session_key, + new PhutilOpaqueEnvelope($session_key), $cache_joins); if (!$info) { return null; } $session_dict = array( 'userPHID' => $info['phid'], 'sessionKey' => $session_key, 'type' => $session_type, ); $cache_raw = array_fill_keys($cache_map, null); foreach ($info as $key => $value) { if (strncmp($key, 's_', 2) === 0) { unset($info[$key]); $session_dict[substr($key, 2)] = $value; continue; } if (isset($cache_map[$key])) { unset($info[$key]); $cache_raw[$cache_map[$key]] = $value; continue; } } $user = $user_table->loadFromArray($info); $cache_raw = $this->filterRawCacheData($user, $types_map, $cache_raw); $user->attachRawCacheData($cache_raw); switch ($session_type) { case PhabricatorAuthSession::TYPE_WEB: // Explicitly prevent bots and mailing lists from establishing web // sessions. It's normally impossible to attach authentication to these // accounts, and likewise impossible to generate sessions, but it's // technically possible that a session could exist in the database. If // one does somehow, refuse to load it. if (!$user->canEstablishWebSessions()) { return null; } break; } $session = id(new PhabricatorAuthSession())->loadFromArray($session_dict); $ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type); // If more than 20% of the time on this session has been used, refresh the // TTL back up to the full duration. The idea here is that sessions are // good forever if used regularly, but get GC'd when they fall out of use. // NOTE: If we begin rotating session keys when extending sessions, the // CSRF code needs to be updated so CSRF tokens survive session rotation. if (time() + (0.80 * $ttl) > $session->getSessionExpires()) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $conn_w = $session_table->establishConnection('w'); queryfx( $conn_w, 'UPDATE %T SET sessionExpires = UNIX_TIMESTAMP() + %d WHERE id = %d', $session->getTableName(), $ttl, $session->getID()); unset($unguarded); } $user->attachSession($session); return $user; } /** * Issue a new session key for a given identity. Phabricator supports * different types of sessions (like "web" and "conduit") and each session * type may have multiple concurrent sessions (this allows a user to be * logged in on multiple browsers at the same time, for instance). * * Note that this method is transport-agnostic and does not set cookies or * issue other types of tokens, it ONLY generates a new session key. * * You can configure the maximum number of concurrent sessions for various * session types in the Phabricator configuration. * * @param const Session type constant (see * @{class:PhabricatorAuthSession}). * @param phid|null Identity to establish a session for, usually a user * PHID. With `null`, generates an anonymous session. * @param bool True to issue a partial session. * @return string Newly generated session key. */ public function establishSession($session_type, $identity_phid, $partial) { // Consume entropy to generate a new session key, forestalling the eventual // heat death of the universe. $session_key = Filesystem::readRandomCharacters(40); if ($identity_phid === null) { return self::KIND_ANONYMOUS.'/'.$session_key; } $session_table = new PhabricatorAuthSession(); $conn_w = $session_table->establishConnection('w'); // This has a side effect of validating the session type. $session_ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type); $digest_key = PhabricatorHash::weakDigest($session_key); // Logging-in users don't have CSRF stuff yet, so we have to unguard this // write. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); id(new PhabricatorAuthSession()) ->setUserPHID($identity_phid) ->setType($session_type) ->setSessionKey($digest_key) ->setSessionStart(time()) ->setSessionExpires(time() + $session_ttl) ->setIsPartial($partial ? 1 : 0) ->setSignedLegalpadDocuments(0) ->save(); $log = PhabricatorUserLog::initializeNewLog( null, $identity_phid, ($partial ? PhabricatorUserLog::ACTION_LOGIN_PARTIAL : PhabricatorUserLog::ACTION_LOGIN)); $log->setDetails( array( 'session_type' => $session_type, )); $log->setSession($digest_key); $log->save(); unset($unguarded); $info = id(new PhabricatorAuthSessionInfo()) ->setSessionType($session_type) ->setIdentityPHID($identity_phid) ->setIsPartial($partial); $extensions = PhabricatorAuthSessionEngineExtension::getAllExtensions(); foreach ($extensions as $extension) { $extension->didEstablishSession($info); } return $session_key; } /** * Terminate all of a user's login sessions. * * This is used when users change passwords, linked accounts, or add * multifactor authentication. * * @param PhabricatorUser User whose sessions should be terminated. * @param string|null Optionally, one session to keep. Normally, the current * login session. * * @return void */ public function terminateLoginSessions( PhabricatorUser $user, $except_session = null) { $sessions = id(new PhabricatorAuthSessionQuery()) ->setViewer($user) ->withIdentityPHIDs(array($user->getPHID())) ->execute(); if ($except_session !== null) { $except_session = PhabricatorHash::weakDigest($except_session); } foreach ($sessions as $key => $session) { if ($except_session !== null) { $is_except = phutil_hashes_are_identical( $session->getSessionKey(), $except_session); if ($is_except) { continue; } } $session->delete(); } } public function logoutSession( PhabricatorUser $user, PhabricatorAuthSession $session) { $log = PhabricatorUserLog::initializeNewLog( $user, $user->getPHID(), PhabricatorUserLog::ACTION_LOGOUT); $log->save(); $extensions = PhabricatorAuthSessionEngineExtension::getAllExtensions(); foreach ($extensions as $extension) { $extension->didLogout($user, array($session)); } $session->delete(); } /* -( High Security )------------------------------------------------------ */ + /** + * Require the user respond to a high security (MFA) check. + * + * This method differs from @{method:requireHighSecuritySession} in that it + * does not upgrade the user's session as a side effect. This method is + * appropriate for one-time checks. + * + * @param PhabricatorUser User whose session needs to be in high security. + * @param AphrontReqeust Current request. + * @param string URI to return the user to if they cancel. + * @return PhabricatorAuthHighSecurityToken Security token. + * @task hisec + */ + public function requireHighSecurityToken( + PhabricatorUser $viewer, + AphrontRequest $request, + $cancel_uri) { + + return $this->newHighSecurityToken( + $viewer, + $request, + $cancel_uri, + false, + false); + } + + /** * Require high security, or prompt the user to enter high security. * * If the user's session is in high security, this method will return a * token. Otherwise, it will throw an exception which will eventually * be converted into a multi-factor authentication workflow. * + * This method upgrades the user's session to high security for a short + * period of time, and is appropriate if you anticipate they may need to + * take multiple high security actions. To perform a one-time check instead, + * use @{method:requireHighSecurityToken}. + * * @param PhabricatorUser User whose session needs to be in high security. * @param AphrontReqeust Current request. * @param string URI to return the user to if they cancel. * @param bool True to jump partial sessions directly into high * security instead of just upgrading them to full * sessions. * @return PhabricatorAuthHighSecurityToken Security token. * @task hisec */ public function requireHighSecuritySession( PhabricatorUser $viewer, AphrontRequest $request, $cancel_uri, $jump_into_hisec = false) { + return $this->newHighSecurityToken( + $viewer, + $request, + $cancel_uri, + false, + true); + } + + private function newHighSecurityToken( + PhabricatorUser $viewer, + AphrontRequest $request, + $cancel_uri, + $jump_into_hisec, + $upgrade_session) { + if (!$viewer->hasSession()) { throw new Exception( pht('Requiring a high-security session from a user with no session!')); } + // TODO: If a user answers a "requireHighSecurityToken()" prompt and hits + // a "requireHighSecuritySession()" prompt a short time later, the one-shot + // token should be good enough to upgrade the session. + $session = $viewer->getSession(); // Check if the session is already in high security mode. $token = $this->issueHighSecurityToken($session); if ($token) { return $token; } // Load the multi-factor auth sources attached to this account. $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere( 'userPHID = %s', $viewer->getPHID()); // If the account has no associated multi-factor auth, just issue a token // without putting the session into high security mode. This is generally // easier for users. A minor but desirable side effect is that when a user // adds an auth factor, existing sessions won't get a free pass into hisec, // since they never actually got marked as hisec. if (!$factors) { return $this->issueHighSecurityToken($session, true); } // Check for a rate limit without awarding points, so the user doesn't // get partway through the workflow only to get blocked. PhabricatorSystemActionEngine::willTakeAction( array($viewer->getPHID()), new PhabricatorAuthTryFactorAction(), 0); $validation_results = array(); if ($request->isHTTPPost()) { $request->validateCSRF(); if ($request->getExists(AphrontRequest::TYPE_HISEC)) { // Limit factor verification rates to prevent brute force attacks. PhabricatorSystemActionEngine::willTakeAction( array($viewer->getPHID()), new PhabricatorAuthTryFactorAction(), 1); $ok = true; foreach ($factors as $factor) { $id = $factor->getID(); $impl = $factor->requireImplementation(); $validation_results[$id] = $impl->processValidateFactorForm( $factor, $viewer, $request); if (!$impl->isFactorValid($factor, $validation_results[$id])) { $ok = false; } } if ($ok) { // Give the user a credit back for a successful factor verification. PhabricatorSystemActionEngine::willTakeAction( array($viewer->getPHID()), new PhabricatorAuthTryFactorAction(), -1); if ($session->getIsPartial() && !$jump_into_hisec) { // If we have a partial session and are not jumping directly into // hisec, just issue a token without putting it in high security // mode. return $this->issueHighSecurityToken($session, true); } + // If we aren't upgrading the session itself, just issue a token. + if (!$upgrade_session) { + return $this->issueHighSecurityToken($session, true); + } + $until = time() + phutil_units('15 minutes in seconds'); $session->setHighSecurityUntil($until); queryfx( $session->establishConnection('w'), 'UPDATE %T SET highSecurityUntil = %d WHERE id = %d', $session->getTableName(), $until, $session->getID()); $log = PhabricatorUserLog::initializeNewLog( $viewer, $viewer->getPHID(), PhabricatorUserLog::ACTION_ENTER_HISEC); $log->save(); } else { $log = PhabricatorUserLog::initializeNewLog( $viewer, $viewer->getPHID(), PhabricatorUserLog::ACTION_FAIL_HISEC); $log->save(); } } } $token = $this->issueHighSecurityToken($session); if ($token) { return $token; } throw id(new PhabricatorAuthHighSecurityRequiredException()) ->setCancelURI($cancel_uri) ->setFactors($factors) ->setFactorValidationResults($validation_results); } /** * Issue a high security token for a session, if authorized. * * @param PhabricatorAuthSession Session to issue a token for. * @param bool Force token issue. * @return PhabricatorAuthHighSecurityToken|null Token, if authorized. * @task hisec */ private function issueHighSecurityToken( PhabricatorAuthSession $session, $force = false) { if ($session->isHighSecuritySession() || $force) { return new PhabricatorAuthHighSecurityToken(); } return null; } /** * Render a form for providing relevant multi-factor credentials. * * @param PhabricatorUser Viewing user. * @param AphrontRequest Current request. * @return AphrontFormView Renderable form. * @task hisec */ public function renderHighSecurityForm( array $factors, array $validation_results, PhabricatorUser $viewer, AphrontRequest $request) { $form = id(new AphrontFormView()) ->setUser($viewer) ->appendRemarkupInstructions(''); foreach ($factors as $factor) { $factor->requireImplementation()->renderValidateFactorForm( $factor, $form, $viewer, idx($validation_results, $factor->getID())); } $form->appendRemarkupInstructions(''); return $form; } /** * Strip the high security flag from a session. * * Kicks a session out of high security and logs the exit. * * @param PhabricatorUser Acting user. * @param PhabricatorAuthSession Session to return to normal security. * @return void * @task hisec */ public function exitHighSecurity( PhabricatorUser $viewer, PhabricatorAuthSession $session) { if (!$session->getHighSecurityUntil()) { return; } queryfx( $session->establishConnection('w'), 'UPDATE %T SET highSecurityUntil = NULL WHERE id = %d', $session->getTableName(), $session->getID()); $log = PhabricatorUserLog::initializeNewLog( $viewer, $viewer->getPHID(), PhabricatorUserLog::ACTION_EXIT_HISEC); $log->save(); } /* -( Partial Sessions )--------------------------------------------------- */ /** * Upgrade a partial session to a full session. * * @param PhabricatorAuthSession Session to upgrade. * @return void * @task partial */ public function upgradePartialSession(PhabricatorUser $viewer) { if (!$viewer->hasSession()) { throw new Exception( pht('Upgrading partial session of user with no session!')); } $session = $viewer->getSession(); if (!$session->getIsPartial()) { throw new Exception(pht('Session is not partial!')); } $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $session->setIsPartial(0); queryfx( $session->establishConnection('w'), 'UPDATE %T SET isPartial = %d WHERE id = %d', $session->getTableName(), 0, $session->getID()); $log = PhabricatorUserLog::initializeNewLog( $viewer, $viewer->getPHID(), PhabricatorUserLog::ACTION_LOGIN_FULL); $log->save(); unset($unguarded); } /* -( Legalpad Documents )-------------------------------------------------- */ /** * Upgrade a session to have all legalpad documents signed. * * @param PhabricatorUser User whose session should upgrade. * @param array LegalpadDocument objects * @return void * @task partial */ public function signLegalpadDocuments(PhabricatorUser $viewer, array $docs) { if (!$viewer->hasSession()) { throw new Exception( pht('Signing session legalpad documents of user with no session!')); } $session = $viewer->getSession(); if ($session->getSignedLegalpadDocuments()) { throw new Exception(pht( 'Session has already signed required legalpad documents!')); } $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $session->setSignedLegalpadDocuments(1); queryfx( $session->establishConnection('w'), 'UPDATE %T SET signedLegalpadDocuments = %d WHERE id = %d', $session->getTableName(), 1, $session->getID()); if (!empty($docs)) { $log = PhabricatorUserLog::initializeNewLog( $viewer, $viewer->getPHID(), PhabricatorUserLog::ACTION_LOGIN_LEGALPAD); $log->save(); } unset($unguarded); } /* -( One Time Login URIs )------------------------------------------------ */ /** * Retrieve a temporary, one-time URI which can log in to an account. * * These URIs are used for password recovery and to regain access to accounts * which users have been locked out of. * * @param PhabricatorUser User to generate a URI for. * @param PhabricatorUserEmail Optionally, email to verify when * link is used. * @param string Optional context string for the URI. This is purely cosmetic * and used only to customize workflow and error messages. * @return string Login URI. * @task onetime */ public function getOneTimeLoginURI( PhabricatorUser $user, PhabricatorUserEmail $email = null, $type = self::ONETIME_RESET) { $key = Filesystem::readRandomCharacters(32); $key_hash = $this->getOneTimeLoginKeyHash($user, $email, $key); $onetime_type = PhabricatorAuthOneTimeLoginTemporaryTokenType::TOKENTYPE; $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); id(new PhabricatorAuthTemporaryToken()) ->setTokenResource($user->getPHID()) ->setTokenType($onetime_type) ->setTokenExpires(time() + phutil_units('1 day in seconds')) ->setTokenCode($key_hash) ->save(); unset($unguarded); $uri = '/login/once/'.$type.'/'.$user->getID().'/'.$key.'/'; if ($email) { $uri = $uri.$email->getID().'/'; } try { $uri = PhabricatorEnv::getProductionURI($uri); } catch (Exception $ex) { // If a user runs `bin/auth recover` before configuring the base URI, // just show the path. We don't have any way to figure out the domain. // See T4132. } return $uri; } /** * Load the temporary token associated with a given one-time login key. * * @param PhabricatorUser User to load the token for. * @param PhabricatorUserEmail Optionally, email to verify when * link is used. * @param string Key user is presenting as a valid one-time login key. * @return PhabricatorAuthTemporaryToken|null Token, if one exists. * @task onetime */ public function loadOneTimeLoginKey( PhabricatorUser $user, PhabricatorUserEmail $email = null, $key = null) { $key_hash = $this->getOneTimeLoginKeyHash($user, $email, $key); $onetime_type = PhabricatorAuthOneTimeLoginTemporaryTokenType::TOKENTYPE; return id(new PhabricatorAuthTemporaryTokenQuery()) ->setViewer($user) ->withTokenResources(array($user->getPHID())) ->withTokenTypes(array($onetime_type)) ->withTokenCodes(array($key_hash)) ->withExpired(false) ->executeOne(); } /** * Hash a one-time login key for storage as a temporary token. * * @param PhabricatorUser User this key is for. * @param PhabricatorUserEmail Optionally, email to verify when * link is used. * @param string The one time login key. * @return string Hash of the key. * task onetime */ private function getOneTimeLoginKeyHash( PhabricatorUser $user, PhabricatorUserEmail $email = null, $key = null) { $parts = array( $key, $user->getAccountSecret(), ); if ($email) { $parts[] = $email->getVerificationCode(); } return PhabricatorHash::weakDigest(implode(':', $parts)); } /* -( User Cache )--------------------------------------------------------- */ /** * @task cache */ private function getUserCacheQueryParts(AphrontDatabaseConnection $conn) { $cache_selects = array(); $cache_joins = array(); $cache_map = array(); $keys = array(); $types_map = array(); $cache_types = PhabricatorUserCacheType::getAllCacheTypes(); foreach ($cache_types as $cache_type) { foreach ($cache_type->getAutoloadKeys() as $autoload_key) { $keys[] = $autoload_key; $types_map[$autoload_key] = $cache_type; } } $cache_table = id(new PhabricatorUserCache())->getTableName(); $cache_idx = 1; foreach ($keys as $key) { $join_as = 'ucache_'.$cache_idx; $select_as = 'ucache_'.$cache_idx.'_v'; $cache_selects[] = qsprintf( $conn, '%T.cacheData %T', $join_as, $select_as); $cache_joins[] = qsprintf( $conn, 'LEFT JOIN %T AS %T ON u.phid = %T.userPHID AND %T.cacheIndex = %s', $cache_table, $join_as, $join_as, $join_as, PhabricatorHash::digestForIndex($key)); $cache_map[$select_as] = $key; $cache_idx++; } if ($cache_selects) { - $cache_selects = ', '.implode(', ', $cache_selects); + $cache_selects = qsprintf($conn, ', %LQ', $cache_selects); } else { - $cache_selects = ''; + $cache_selects = qsprintf($conn, ''); } if ($cache_joins) { - $cache_joins = implode(' ', $cache_joins); + $cache_joins = qsprintf($conn, '%LJ', $cache_joins); } else { - $cache_joins = ''; + $cache_joins = qsprintf($conn, ''); } return array($cache_selects, $cache_joins, $cache_map, $types_map); } private function filterRawCacheData( PhabricatorUser $user, array $types_map, array $cache_raw) { foreach ($cache_raw as $cache_key => $cache_data) { $type = $types_map[$cache_key]; if ($type->shouldValidateRawCacheData()) { if (!$type->isRawCacheDataValid($user, $cache_key, $cache_data)) { unset($cache_raw[$cache_key]); } } } return $cache_raw; } public function willServeRequestForUser(PhabricatorUser $user) { // We allow the login user to generate any missing cache data inline. $user->setAllowInlineCacheGeneration(true); // Switch to the user's translation. PhabricatorEnv::setLocaleCode($user->getTranslation()); $extensions = PhabricatorAuthSessionEngineExtension::getAllExtensions(); foreach ($extensions as $extension) { $extension->willServeRequestForUser($user); } } } diff --git a/src/applications/auth/query/PhabricatorAuthInviteQuery.php b/src/applications/auth/query/PhabricatorAuthInviteQuery.php index 1ae617db65..55b325d603 100644 --- a/src/applications/auth/query/PhabricatorAuthInviteQuery.php +++ b/src/applications/auth/query/PhabricatorAuthInviteQuery.php @@ -1,116 +1,116 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withEmailAddresses(array $addresses) { $this->emailAddresses = $addresses; return $this; } public function withVerificationCodes(array $codes) { $this->verificationCodes = $codes; return $this; } public function withAuthorPHIDs(array $phids) { $this->authorPHIDs = $phids; return $this; } protected function loadPage() { $table = new PhabricatorAuthInvite(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); $invites = $table->loadAllFromArray($data); // If the objects were loaded via verification code, set a flag to make // sure the viewer can see them. if ($this->verificationCodes !== null) { foreach ($invites as $invite) { $invite->setViewerHasVerificationCode(true); } } return $invites; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } if ($this->emailAddresses !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'emailAddress IN (%Ls)', $this->emailAddresses); } if ($this->verificationCodes !== null) { $hashes = array(); foreach ($this->verificationCodes as $code) { $hashes[] = PhabricatorHash::digestForIndex($code); } $where[] = qsprintf( - $conn_r, + $conn, 'verificationHash IN (%Ls)', $hashes); } if ($this->authorPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'authorPHID IN (%Ls)', $this->authorPHIDs); } - $where[] = $this->buildPagingClause($conn_r); + $where[] = $this->buildPagingClause($conn); - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } public function getQueryApplicationClass() { // NOTE: This query is issued by logged-out users, who often will not be // able to see applications. They still need to be able to see invites. return null; } } diff --git a/src/applications/auth/query/PhabricatorAuthProviderConfigQuery.php b/src/applications/auth/query/PhabricatorAuthProviderConfigQuery.php index 44e5913290..626c80348f 100644 --- a/src/applications/auth/query/PhabricatorAuthProviderConfigQuery.php +++ b/src/applications/auth/query/PhabricatorAuthProviderConfigQuery.php @@ -1,103 +1,103 @@ phids = $phids; return $this; } public function withIDs(array $ids) { $this->ids = $ids; return $this; } public function withStatus($status) { $this->status = $status; return $this; } public function withProviderClasses(array $classes) { $this->providerClasses = $classes; return $this; } public static function getStatusOptions() { return array( self::STATUS_ALL => pht('All Providers'), self::STATUS_ENABLED => pht('Enabled Providers'), ); } protected function loadPage() { $table = new PhabricatorAuthProviderConfig(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); - if ($this->ids) { + if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } - if ($this->phids) { + if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } - if ($this->providerClasses) { + if ($this->providerClasses !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'providerClass IN (%Ls)', $this->providerClasses); } $status = $this->status; switch ($status) { case self::STATUS_ALL: break; case self::STATUS_ENABLED: $where[] = qsprintf( - $conn_r, + $conn, 'isEnabled = 1'); break; default: throw new Exception(pht("Unknown status '%s'!", $status)); } - $where[] = $this->buildPagingClause($conn_r); + $where[] = $this->buildPagingClause($conn); - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } public function getQueryApplicationClass() { return 'PhabricatorAuthApplication'; } } diff --git a/src/applications/auth/query/PhabricatorAuthSSHKeyQuery.php b/src/applications/auth/query/PhabricatorAuthSSHKeyQuery.php index d8474085b2..3a310ed173 100644 --- a/src/applications/auth/query/PhabricatorAuthSSHKeyQuery.php +++ b/src/applications/auth/query/PhabricatorAuthSSHKeyQuery.php @@ -1,138 +1,138 @@ deleteKey($authfile_key); } public function withIDs(array $ids) { $this->ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withObjectPHIDs(array $object_phids) { $this->objectPHIDs = $object_phids; return $this; } public function withKeys(array $keys) { assert_instances_of($keys, 'PhabricatorAuthSSHPublicKey'); $this->keys = $keys; return $this; } public function withIsActive($active) { $this->isActive = $active; return $this; } public function newResultObject() { return new PhabricatorAuthSSHKey(); } protected function loadPage() { return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $keys) { $object_phids = mpull($keys, 'getObjectPHID'); $objects = id(new PhabricatorObjectQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withPHIDs($object_phids) ->execute(); $objects = mpull($objects, null, 'getPHID'); foreach ($keys as $key => $ssh_key) { $object = idx($objects, $ssh_key->getObjectPHID()); // We must have an object, and that object must be a valid object for // SSH keys. if (!$object || !($object instanceof PhabricatorSSHPublicKeyInterface)) { $this->didRejectResult($ssh_key); unset($keys[$key]); continue; } $ssh_key->attachObject($object); } return $keys; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'phid IN (%Ls)', $this->phids); } if ($this->objectPHIDs !== null) { $where[] = qsprintf( $conn, 'objectPHID IN (%Ls)', $this->objectPHIDs); } if ($this->keys !== null) { $sql = array(); foreach ($this->keys as $key) { $sql[] = qsprintf( $conn, '(keyType = %s AND keyIndex = %s)', $key->getType(), $key->getHash()); } - $where[] = implode(' OR ', $sql); + $where[] = qsprintf($conn, '%LO', $sql); } if ($this->isActive !== null) { if ($this->isActive) { $where[] = qsprintf( $conn, 'isActive = %d', 1); } else { $where[] = qsprintf( $conn, 'isActive IS NULL'); } } return $where; } public function getQueryApplicationClass() { return 'PhabricatorAuthApplication'; } } diff --git a/src/applications/auth/query/PhabricatorAuthSessionQuery.php b/src/applications/auth/query/PhabricatorAuthSessionQuery.php index dea95dd450..25928e72c1 100644 --- a/src/applications/auth/query/PhabricatorAuthSessionQuery.php +++ b/src/applications/auth/query/PhabricatorAuthSessionQuery.php @@ -1,112 +1,112 @@ identityPHIDs = $identity_phids; return $this; } public function withSessionKeys(array $keys) { $this->sessionKeys = $keys; return $this; } public function withSessionTypes(array $types) { $this->sessionTypes = $types; return $this; } public function withIDs(array $ids) { $this->ids = $ids; return $this; } protected function loadPage() { $table = new PhabricatorAuthSession(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } protected function willFilterPage(array $sessions) { $identity_phids = mpull($sessions, 'getUserPHID'); $identity_objects = id(new PhabricatorObjectQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withPHIDs($identity_phids) ->execute(); $identity_objects = mpull($identity_objects, null, 'getPHID'); foreach ($sessions as $key => $session) { $identity_object = idx($identity_objects, $session->getUserPHID()); if (!$identity_object) { unset($sessions[$key]); } else { $session->attachIdentityObject($identity_object); } } return $sessions; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); - if ($this->ids) { + if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } - if ($this->identityPHIDs) { + if ($this->identityPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'userPHID IN (%Ls)', $this->identityPHIDs); } - if ($this->sessionKeys) { + if ($this->sessionKeys !== null) { $hashes = array(); foreach ($this->sessionKeys as $session_key) { $hashes[] = PhabricatorHash::weakDigest($session_key); } $where[] = qsprintf( - $conn_r, + $conn, 'sessionKey IN (%Ls)', $hashes); } - if ($this->sessionTypes) { + if ($this->sessionTypes !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'type IN (%Ls)', $this->sessionTypes); } - $where[] = $this->buildPagingClause($conn_r); + $where[] = $this->buildPagingClause($conn); - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } public function getQueryApplicationClass() { return 'PhabricatorAuthApplication'; } } diff --git a/src/applications/badges/editor/PhabricatorBadgesEditEngine.php b/src/applications/badges/editor/PhabricatorBadgesEditEngine.php index e7a441343c..721184852c 100644 --- a/src/applications/badges/editor/PhabricatorBadgesEditEngine.php +++ b/src/applications/badges/editor/PhabricatorBadgesEditEngine.php @@ -1,147 +1,147 @@ getViewer()); } protected function newObjectQuery() { return new PhabricatorBadgesQuery(); } protected function getObjectCreateTitleText($object) { return pht('Create New Badge'); } protected function getObjectEditTitleText($object) { return pht('Edit Badge: %s', $object->getName()); } protected function getObjectEditShortText($object) { return $object->getName(); } protected function getObjectCreateShortText() { return pht('Create Badge'); } protected function getObjectName() { return pht('Badge'); } protected function getObjectCreateCancelURI($object) { return $this->getApplication()->getApplicationURI('/'); } protected function getEditorURI() { return $this->getApplication()->getApplicationURI('edit/'); } protected function getCommentViewHeaderText($object) { return pht('Render Honors'); } protected function getCommentViewButtonText($object) { return pht('Salute'); } protected function getObjectViewURI($object) { return $object->getViewURI(); } protected function getCreateNewObjectPolicy() { return $this->getApplication()->getPolicy( PhabricatorBadgesCreateCapability::CAPABILITY); } protected function buildCustomEditFields($object) { return array( id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) ->setDescription(pht('Badge name.')) ->setConduitTypeDescription(pht('New badge name.')) ->setTransactionType( PhabricatorBadgesBadgeNameTransaction::TRANSACTIONTYPE) ->setValue($object->getName()) ->setIsRequired(true), id(new PhabricatorTextEditField()) ->setKey('flavor') ->setLabel(pht('Flavor Text')) ->setDescription(pht('Short description of the badge.')) ->setConduitTypeDescription(pht('New badge flavor.')) ->setValue($object->getFlavor()) ->setTransactionType( PhabricatorBadgesBadgeFlavorTransaction::TRANSACTIONTYPE), id(new PhabricatorIconSetEditField()) ->setKey('icon') ->setLabel(pht('Icon')) ->setIconSet(new PhabricatorBadgesIconSet()) ->setTransactionType( PhabricatorBadgesBadgeIconTransaction::TRANSACTIONTYPE) ->setConduitDescription(pht('Change the badge icon.')) ->setConduitTypeDescription(pht('New badge icon.')) ->setValue($object->getIcon()), id(new PhabricatorSelectEditField()) ->setKey('quality') ->setLabel(pht('Quality')) ->setDescription(pht('Color and rarity of the badge.')) ->setConduitTypeDescription(pht('New badge quality.')) ->setValue($object->getQuality()) ->setTransactionType( PhabricatorBadgesBadgeQualityTransaction::TRANSACTIONTYPE) ->setOptions(PhabricatorBadgesQuality::getDropdownQualityMap()), id(new PhabricatorRemarkupEditField()) ->setKey('description') ->setLabel(pht('Description')) ->setDescription(pht('Badge long description.')) ->setConduitTypeDescription(pht('New badge description.')) ->setTransactionType( PhabricatorBadgesBadgeDescriptionTransaction::TRANSACTIONTYPE) ->setValue($object->getDescription()), id(new PhabricatorUsersEditField()) ->setKey('award') - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setDescription(pht('New badge award recipients.')) ->setConduitTypeDescription(pht('New badge award recipients.')) ->setTransactionType( PhabricatorBadgesBadgeAwardTransaction::TRANSACTIONTYPE) ->setLabel(pht('Award Recipients')), id(new PhabricatorUsersEditField()) ->setKey('revoke') - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setDescription(pht('Revoke badge award recipients.')) ->setConduitTypeDescription(pht('Revoke badge award recipients.')) ->setTransactionType( PhabricatorBadgesBadgeRevokeTransaction::TRANSACTIONTYPE) ->setLabel(pht('Revoke Recipients')), ); } } diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php index e276e035e4..1cabeb0709 100644 --- a/src/applications/base/PhabricatorApplication.php +++ b/src/applications/base/PhabricatorApplication.php @@ -1,666 +1,666 @@ pht('Core Applications'), self::GROUP_UTILITIES => pht('Utilities'), self::GROUP_ADMIN => pht('Administration'), self::GROUP_DEVELOPER => pht('Developer Tools'), ); } final public function getApplicationName() { return 'application'; } final public function getTableName() { return 'application_application'; } final protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, ) + parent::getConfiguration(); } final public function generatePHID() { return $this->getPHID(); } final public function save() { // When "save()" is called on applications, we just return without // actually writing anything to the database. return $this; } /* -( Application Information )-------------------------------------------- */ abstract public function getName(); public function getShortDescription() { return pht('%s Application', $this->getName()); } final public function isInstalled() { if (!$this->canUninstall()) { return true; } $prototypes = PhabricatorEnv::getEnvConfig('phabricator.show-prototypes'); if (!$prototypes && $this->isPrototype()) { return false; } $uninstalled = PhabricatorEnv::getEnvConfig( 'phabricator.uninstalled-applications'); return empty($uninstalled[get_class($this)]); } public function isPrototype() { return false; } /** * Return `true` if this application should never appear in application lists * in the UI. Primarily intended for unit test applications or other * pseudo-applications. * * Few applications should be unlisted. For most applications, use * @{method:isLaunchable} to hide them from main launch views instead. * * @return bool True to remove application from UI lists. */ public function isUnlisted() { return false; } /** * Return `true` if this application is a normal application with a base * URI and a web interface. * * Launchable applications can be pinned to the home page, and show up in the * "Launcher" view of the Applications application. Making an application * unlaunchable prevents pinning and hides it from this view. * * Usually, an application should be marked unlaunchable if: * * - it is available on every page anyway (like search); or * - it does not have a web interface (like subscriptions); or * - it is still pre-release and being intentionally buried. * * To hide applications more completely, use @{method:isUnlisted}. * * @return bool True if the application is launchable. */ public function isLaunchable() { return true; } /** * Return `true` if this application should be pinned by default. * * Users who have not yet set preferences see a default list of applications. * * @param PhabricatorUser User viewing the pinned application list. * @return bool True if this application should be pinned by default. */ public function isPinnedByDefault(PhabricatorUser $viewer) { return false; } /** * Returns true if an application is first-party (developed by Phacility) * and false otherwise. * * @return bool True if this application is developed by Phacility. */ final public function isFirstParty() { $where = id(new ReflectionClass($this))->getFileName(); $root = phutil_get_library_root('phabricator'); if (!Filesystem::isDescendant($where, $root)) { return false; } if (Filesystem::isDescendant($where, $root.'/extensions')) { return false; } return true; } public function canUninstall() { return true; } final public function getPHID() { return 'PHID-APPS-'.get_class($this); } public function getTypeaheadURI() { return $this->isLaunchable() ? $this->getBaseURI() : null; } public function getBaseURI() { return null; } final public function getApplicationURI($path = '') { return $this->getBaseURI().ltrim($path, '/'); } public function getIcon() { return 'fa-puzzle-piece'; } public function getApplicationOrder() { return PHP_INT_MAX; } public function getApplicationGroup() { return self::GROUP_CORE; } public function getTitleGlyph() { return null; } final public function getHelpMenuItems(PhabricatorUser $viewer) { $items = array(); $articles = $this->getHelpDocumentationArticles($viewer); if ($articles) { foreach ($articles as $article) { $item = id(new PhabricatorActionView()) ->setName($article['name']) ->setHref($article['href']) ->addSigil('help-item') ->setOpenInNewWindow(true); $items[] = $item; } } $command_specs = $this->getMailCommandObjects(); if ($command_specs) { foreach ($command_specs as $key => $spec) { $object = $spec['object']; $class = get_class($this); $href = '/applications/mailcommands/'.$class.'/'.$key.'/'; $item = id(new PhabricatorActionView()) ->setName($spec['name']) ->setHref($href) ->addSigil('help-item') ->setOpenInNewWindow(true); $items[] = $item; } } if ($items) { $divider = id(new PhabricatorActionView()) ->addSigil('help-item') ->setType(PhabricatorActionView::TYPE_DIVIDER); array_unshift($items, $divider); } return array_values($items); } public function getHelpDocumentationArticles(PhabricatorUser $viewer) { return array(); } public function getOverview() { return null; } public function getEventListeners() { return array(); } public function getRemarkupRules() { return array(); } public function getQuicksandURIPatternBlacklist() { return array(); } public function getMailCommandObjects() { return array(); } /* -( URI Routing )-------------------------------------------------------- */ public function getRoutes() { return array(); } public function getResourceRoutes() { return array(); } /* -( Email Integration )-------------------------------------------------- */ public function supportsEmailIntegration() { return false; } final protected function getInboundEmailSupportLink() { return PhabricatorEnv::getDoclink('Configuring Inbound Email'); } public function getAppEmailBlurb() { throw new PhutilMethodNotImplementedException(); } /* -( Fact Integration )--------------------------------------------------- */ public function getFactObjectsForAnalysis() { return array(); } /* -( UI Integration )----------------------------------------------------- */ /** * You can provide an optional piece of flavor text for the application. This * is currently rendered in application launch views if the application has no * status elements. * * @return string|null Flavor text. * @task ui */ public function getFlavorText() { return null; } /** * Build items for the main menu. * * @param PhabricatorUser The viewing user. * @param AphrontController The current controller. May be null for special * pages like 404, exception handlers, etc. * @return list List of menu items. * @task ui */ public function buildMainMenuItems( PhabricatorUser $user, PhabricatorController $controller = null) { return array(); } /* -( Application Management )--------------------------------------------- */ final public static function getByClass($class_name) { $selected = null; $applications = self::getAllApplications(); foreach ($applications as $application) { if (get_class($application) == $class_name) { $selected = $application; break; } } if (!$selected) { throw new Exception(pht("No application '%s'!", $class_name)); } return $selected; } final public static function getAllApplications() { static $applications; if ($applications === null) { $apps = id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setSortMethod('getApplicationOrder') ->execute(); // Reorder the applications into "application order". Notably, this // ensures their event handlers register in application order. $apps = mgroup($apps, 'getApplicationGroup'); $group_order = array_keys(self::getApplicationGroups()); $apps = array_select_keys($apps, $group_order) + $apps; $apps = array_mergev($apps); $applications = $apps; } return $applications; } final public static function getAllInstalledApplications() { $all_applications = self::getAllApplications(); $apps = array(); foreach ($all_applications as $app) { if (!$app->isInstalled()) { continue; } $apps[] = $app; } return $apps; } /** * Determine if an application is installed, by application class name. * * To check if an application is installed //and// available to a particular * viewer, user @{method:isClassInstalledForViewer}. * * @param string Application class name. * @return bool True if the class is installed. * @task meta */ final public static function isClassInstalled($class) { return self::getByClass($class)->isInstalled(); } /** * Determine if an application is installed and available to a viewer, by * application class name. * * To check if an application is installed at all, use * @{method:isClassInstalled}. * * @param string Application class name. * @param PhabricatorUser Viewing user. * @return bool True if the class is installed for the viewer. * @task meta */ final public static function isClassInstalledForViewer( $class, PhabricatorUser $viewer) { if ($viewer->isOmnipotent()) { return true; } $cache = PhabricatorCaches::getRequestCache(); $viewer_fragment = $viewer->getCacheFragment(); $key = 'app.'.$class.'.installed.'.$viewer_fragment; $result = $cache->getKey($key); if ($result === null) { if (!self::isClassInstalled($class)) { $result = false; } else { $application = self::getByClass($class); if (!$application->canUninstall()) { // If the application can not be uninstalled, always allow viewers // to see it. In particular, this allows logged-out viewers to see // Settings and load global default settings even if the install // does not allow public viewers. $result = true; } else { $result = PhabricatorPolicyFilter::hasCapability( $viewer, self::getByClass($class), PhabricatorPolicyCapability::CAN_VIEW); } } $cache->setKey($key, $result); } return $result; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array_merge( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ), array_keys($this->getCustomCapabilities())); } public function getPolicy($capability) { $default = $this->getCustomPolicySetting($capability); if ($default) { return $default; } switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicies::getMostOpenPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_ADMIN; default: $spec = $this->getCustomCapabilitySpecification($capability); return idx($spec, 'default', PhabricatorPolicies::POLICY_USER); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } /* -( Policies )----------------------------------------------------------- */ protected function getCustomCapabilities() { return array(); } final private function getCustomPolicySetting($capability) { if (!$this->isCapabilityEditable($capability)) { return null; } $policy_locked = PhabricatorEnv::getEnvConfig('policy.locked'); if (isset($policy_locked[$capability])) { return $policy_locked[$capability]; } $config = PhabricatorEnv::getEnvConfig('phabricator.application-settings'); $app = idx($config, $this->getPHID()); if (!$app) { return null; } $policy = idx($app, 'policy'); if (!$policy) { return null; } return idx($policy, $capability); } final private function getCustomCapabilitySpecification($capability) { $custom = $this->getCustomCapabilities(); if (!isset($custom[$capability])) { throw new Exception(pht("Unknown capability '%s'!", $capability)); } return $custom[$capability]; } final public function getCapabilityLabel($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return pht('Can Use Application'); case PhabricatorPolicyCapability::CAN_EDIT: return pht('Can Configure Application'); } $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability); if ($capobj) { return $capobj->getCapabilityName(); } return null; } final public function isCapabilityEditable($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->canUninstall(); case PhabricatorPolicyCapability::CAN_EDIT: - return false; + return true; default: $spec = $this->getCustomCapabilitySpecification($capability); return idx($spec, 'edit', true); } } final public function getCapabilityCaption($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if (!$this->canUninstall()) { return pht( 'This application is required for Phabricator to operate, so all '. 'users must have access to it.'); } else { return null; } case PhabricatorPolicyCapability::CAN_EDIT: return null; default: $spec = $this->getCustomCapabilitySpecification($capability); return idx($spec, 'caption'); } } final public function getCapabilityTemplatePHIDType($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: case PhabricatorPolicyCapability::CAN_EDIT: return null; } $spec = $this->getCustomCapabilitySpecification($capability); return idx($spec, 'template'); } final public function getDefaultObjectTypePolicyMap() { $map = array(); foreach ($this->getCustomCapabilities() as $capability => $spec) { if (empty($spec['template'])) { continue; } if (empty($spec['capability'])) { continue; } $default = $this->getPolicy($capability); $map[$spec['template']][$spec['capability']] = $default; } return $map; } public function getApplicationSearchDocumentTypes() { return array(); } protected function getEditRoutePattern($base = null) { return $base.'(?:'. '(?P[0-9]\d*)/)?'. '(?:'. '(?:'. '(?Pparameters|nodefault|nocreate|nomanage|comment)/'. '|'. '(?:form/(?P[^/]+)/)?(?:page/(?P[^/]+)/)?'. ')'. ')?'; } protected function getBulkRoutePattern($base = null) { return $base.'(?:query/(?P[^/]+)/)?'; } protected function getQueryRoutePattern($base = null) { return $base.'(?:query/(?P[^/]+)/(?:(?P[^/]+)/)?)?'; } protected function getProfileMenuRouting($controller) { $edit_route = $this->getEditRoutePattern(); $mode_route = '(?Pglobal|custom)/'; return array( '(?Pview)/(?P[^/]+)/' => $controller, '(?Phide)/(?P[^/]+)/' => $controller, '(?Pdefault)/(?P[^/]+)/' => $controller, '(?Pconfigure)/' => $controller, '(?Pconfigure)/'.$mode_route => $controller, '(?Preorder)/'.$mode_route => $controller, '(?Pedit)/'.$edit_route => $controller, '(?Pnew)/'.$mode_route.'(?[^/]+)/'.$edit_route => $controller, '(?Pbuiltin)/(?[^/]+)/'.$edit_route => $controller, ); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorApplicationEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorApplicationApplicationTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } } diff --git a/src/applications/cache/PhabricatorKeyValueDatabaseCache.php b/src/applications/cache/PhabricatorKeyValueDatabaseCache.php index c6a52024fe..0b4609074a 100644 --- a/src/applications/cache/PhabricatorKeyValueDatabaseCache.php +++ b/src/applications/cache/PhabricatorKeyValueDatabaseCache.php @@ -1,174 +1,174 @@ digestKeys(array_keys($keys)); $conn_w = $this->establishConnection('w'); $sql = array(); foreach ($map as $key => $hash) { $value = $keys[$key]; list($format, $storage_value) = $this->willWriteValue($key, $value); $sql[] = qsprintf( $conn_w, '(%s, %s, %s, %B, %d, %nd)', $hash, $key, $format, $storage_value, time(), $ttl ? (time() + $ttl) : null); } $guard = AphrontWriteGuard::beginScopedUnguardedWrites(); foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) { queryfx( $conn_w, 'INSERT INTO %T (cacheKeyHash, cacheKey, cacheFormat, cacheData, - cacheCreated, cacheExpires) VALUES %Q + cacheCreated, cacheExpires) VALUES %LQ ON DUPLICATE KEY UPDATE cacheKey = VALUES(cacheKey), cacheFormat = VALUES(cacheFormat), cacheData = VALUES(cacheData), cacheCreated = VALUES(cacheCreated), cacheExpires = VALUES(cacheExpires)', $this->getTableName(), $chunk); } unset($guard); } return $this; } public function getKeys(array $keys) { $results = array(); if ($keys) { $map = $this->digestKeys($keys); $rows = queryfx_all( $this->establishConnection('r'), 'SELECT * FROM %T WHERE cacheKeyHash IN (%Ls)', $this->getTableName(), $map); $rows = ipull($rows, null, 'cacheKey'); foreach ($keys as $key) { if (empty($rows[$key])) { continue; } $row = $rows[$key]; if ($row['cacheExpires'] && ($row['cacheExpires'] < time())) { continue; } try { $results[$key] = $this->didReadValue( $row['cacheFormat'], $row['cacheData']); } catch (Exception $ex) { // Treat this as a cache miss. phlog($ex); } } } return $results; } public function deleteKeys(array $keys) { if ($keys) { $map = $this->digestKeys($keys); queryfx( $this->establishConnection('w'), 'DELETE FROM %T WHERE cacheKeyHash IN (%Ls)', $this->getTableName(), $map); } return $this; } public function destroyCache() { queryfx( $this->establishConnection('w'), 'DELETE FROM %T', $this->getTableName()); return $this; } /* -( Raw Cache Access )--------------------------------------------------- */ public function establishConnection($mode) { // TODO: This is the only concrete table we have on the database right // now. return id(new PhabricatorMarkupCache())->establishConnection($mode); } public function getTableName() { return 'cache_general'; } /* -( Implementation )----------------------------------------------------- */ private function digestKeys(array $keys) { $map = array(); foreach ($keys as $key) { $map[$key] = PhabricatorHash::digestForIndex($key); } return $map; } private function willWriteValue($key, $value) { if (!is_string($value)) { throw new Exception(pht('Only strings may be written to the DB cache!')); } static $can_deflate; if ($can_deflate === null) { $can_deflate = function_exists('gzdeflate') && PhabricatorEnv::getEnvConfig('cache.enable-deflate'); } if ($can_deflate) { $deflated = PhabricatorCaches::maybeDeflateData($value); if ($deflated !== null) { return array(self::CACHE_FORMAT_DEFLATE, $deflated); } } return array(self::CACHE_FORMAT_RAW, $value); } private function didReadValue($format, $value) { switch ($format) { case self::CACHE_FORMAT_RAW: return $value; case self::CACHE_FORMAT_DEFLATE: return PhabricatorCaches::inflateData($value); default: throw new Exception(pht('Unknown cache format.')); } } } diff --git a/src/applications/calendar/editor/PhabricatorCalendarEventEditEngine.php b/src/applications/calendar/editor/PhabricatorCalendarEventEditEngine.php index a9eab9376a..1b23b13fbf 100644 --- a/src/applications/calendar/editor/PhabricatorCalendarEventEditEngine.php +++ b/src/applications/calendar/editor/PhabricatorCalendarEventEditEngine.php @@ -1,419 +1,419 @@ seriesEditMode = $series_edit_mode; return $this; } public function getSeriesEditMode() { return $this->seriesEditMode; } public function getEngineName() { return pht('Calendar Events'); } public function getSummaryHeader() { return pht('Configure Calendar Event Forms'); } public function getSummaryText() { return pht('Configure how users create and edit events.'); } public function getEngineApplicationClass() { return 'PhabricatorCalendarApplication'; } protected function newEditableObject() { return PhabricatorCalendarEvent::initializeNewCalendarEvent( $this->getViewer()); } protected function newObjectQuery() { return new PhabricatorCalendarEventQuery(); } protected function getObjectCreateTitleText($object) { return pht('Create New Event'); } protected function getObjectEditTitleText($object) { return pht('Edit Event: %s', $object->getName()); } protected function getObjectEditShortText($object) { return $object->getMonogram(); } protected function getObjectCreateShortText() { return pht('Create Event'); } protected function getObjectName() { return pht('Event'); } protected function getObjectViewURI($object) { return $object->getURI(); } protected function getEditorURI() { return $this->getApplication()->getApplicationURI('event/edit/'); } protected function buildCustomEditFields($object) { $viewer = $this->getViewer(); if ($this->getIsCreate()) { $invitee_phids = array($viewer->getPHID()); } else { $invitee_phids = $object->getInviteePHIDsForEdit(); } $frequency_map = PhabricatorCalendarEvent::getFrequencyMap(); $frequency_options = ipull($frequency_map, 'label'); $rrule = $object->newRecurrenceRule(); if ($rrule) { $frequency = $rrule->getFrequency(); } else { $frequency = null; } // At least for now, just hide "Invitees" when editing all future events. // This may eventually deserve a more nuanced approach. $is_future = ($this->getSeriesEditMode() == self::MODE_FUTURE); $fields = array( id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) ->setDescription(pht('Name of the event.')) ->setIsRequired(true) ->setTransactionType( PhabricatorCalendarEventNameTransaction::TRANSACTIONTYPE) ->setConduitDescription(pht('Rename the event.')) ->setConduitTypeDescription(pht('New event name.')) ->setValue($object->getName()), id(new PhabricatorBoolEditField()) ->setIsLockable(false) ->setIsDefaultable(false) ->setKey('isAllDay') ->setOptions(pht('Normal Event'), pht('All Day Event')) ->setAsCheckbox(true) ->setTransactionType( PhabricatorCalendarEventAllDayTransaction::TRANSACTIONTYPE) ->setDescription(pht('Marks this as an all day event.')) ->setConduitDescription(pht('Make the event an all day event.')) ->setConduitTypeDescription(pht('Mark the event as an all day event.')) ->setValue($object->getIsAllDay()), id(new PhabricatorEpochEditField()) ->setKey('start') ->setLabel(pht('Start')) ->setIsLockable(false) ->setIsDefaultable(false) ->setTransactionType( PhabricatorCalendarEventStartDateTransaction::TRANSACTIONTYPE) ->setDescription(pht('Start time of the event.')) ->setConduitDescription(pht('Change the start time of the event.')) ->setConduitTypeDescription(pht('New event start time.')) ->setValue($object->getStartDateTimeEpoch()), id(new PhabricatorEpochEditField()) ->setKey('end') ->setLabel(pht('End')) ->setIsLockable(false) ->setIsDefaultable(false) ->setTransactionType( PhabricatorCalendarEventEndDateTransaction::TRANSACTIONTYPE) ->setDescription(pht('End time of the event.')) ->setConduitDescription(pht('Change the end time of the event.')) ->setConduitTypeDescription(pht('New event end time.')) ->setValue($object->newEndDateTimeForEdit()->getEpoch()), id(new PhabricatorBoolEditField()) ->setKey('cancelled') ->setOptions(pht('Active'), pht('Cancelled')) ->setLabel(pht('Cancelled')) ->setDescription(pht('Cancel the event.')) ->setTransactionType( PhabricatorCalendarEventCancelTransaction::TRANSACTIONTYPE) - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setConduitDescription(pht('Cancel or restore the event.')) ->setConduitTypeDescription(pht('True to cancel the event.')) ->setValue($object->getIsCancelled()), id(new PhabricatorUsersEditField()) ->setIsLockable(false) ->setIsDefaultable(false) ->setKey('hostPHID') ->setAliases(array('host')) ->setLabel(pht('Host')) ->setDescription(pht('Host of the event.')) ->setTransactionType( PhabricatorCalendarEventHostTransaction::TRANSACTIONTYPE) - ->setIsConduitOnly($this->getIsCreate()) + ->setIsFormField(!$this->getIsCreate()) ->setConduitDescription(pht('Change the host of the event.')) ->setConduitTypeDescription(pht('New event host.')) ->setSingleValue($object->getHostPHID()), id(new PhabricatorDatasourceEditField()) ->setIsLockable(false) ->setIsDefaultable(false) ->setIsHidden($is_future) ->setKey('inviteePHIDs') ->setAliases(array('invite', 'invitee', 'invitees', 'inviteePHID')) ->setLabel(pht('Invitees')) ->setDatasource(new PhabricatorMetaMTAMailableDatasource()) ->setTransactionType( PhabricatorCalendarEventInviteTransaction::TRANSACTIONTYPE) ->setDescription(pht('Users invited to the event.')) ->setConduitDescription(pht('Change invited users.')) ->setConduitTypeDescription(pht('New event invitees.')) ->setValue($invitee_phids) ->setCommentActionLabel(pht('Change Invitees')), id(new PhabricatorRemarkupEditField()) ->setKey('description') ->setLabel(pht('Description')) ->setDescription(pht('Description of the event.')) ->setTransactionType( PhabricatorCalendarEventDescriptionTransaction::TRANSACTIONTYPE) ->setConduitDescription(pht('Update the event description.')) ->setConduitTypeDescription(pht('New event description.')) ->setValue($object->getDescription()), id(new PhabricatorIconSetEditField()) ->setKey('icon') ->setLabel(pht('Icon')) ->setIconSet(new PhabricatorCalendarIconSet()) ->setTransactionType( PhabricatorCalendarEventIconTransaction::TRANSACTIONTYPE) ->setDescription(pht('Event icon.')) ->setConduitDescription(pht('Change the event icon.')) ->setConduitTypeDescription(pht('New event icon.')) ->setValue($object->getIcon()), // NOTE: We're being a little sneaky here. This field is hidden and // always has the value "true", so it makes the event recurring when you // submit a form which contains the field. Then we put the the field on // the "recurring" page in the "Make Recurring" dialog to simplify the // workflow. This is still normal, explicit field from the perspective // of the API. id(new PhabricatorBoolEditField()) ->setIsHidden(true) ->setIsLockable(false) ->setIsDefaultable(false) ->setKey('isRecurring') ->setLabel(pht('Recurring')) ->setOptions(pht('One-Time Event'), pht('Recurring Event')) ->setTransactionType( PhabricatorCalendarEventRecurringTransaction::TRANSACTIONTYPE) ->setDescription(pht('One time or recurring event.')) ->setConduitDescription(pht('Make the event recurring.')) ->setConduitTypeDescription(pht('Mark the event as a recurring event.')) ->setValue(true), id(new PhabricatorSelectEditField()) ->setIsLockable(false) ->setIsDefaultable(false) ->setKey('frequency') ->setLabel(pht('Frequency')) ->setOptions($frequency_options) ->setTransactionType( PhabricatorCalendarEventFrequencyTransaction::TRANSACTIONTYPE) ->setDescription(pht('Recurring event frequency.')) ->setConduitDescription(pht('Change the event frequency.')) ->setConduitTypeDescription(pht('New event frequency.')) ->setValue($frequency), id(new PhabricatorEpochEditField()) ->setIsLockable(false) ->setIsDefaultable(false) ->setAllowNull(true) ->setHideTime($object->getIsAllDay()) ->setKey('until') ->setLabel(pht('Repeat Until')) ->setTransactionType( PhabricatorCalendarEventUntilDateTransaction::TRANSACTIONTYPE) ->setDescription(pht('Last instance of the event.')) ->setConduitDescription(pht('Change when the event repeats until.')) ->setConduitTypeDescription(pht('New final event time.')) ->setValue($object->getUntilDateTimeEpoch()), ); return $fields; } protected function willBuildEditForm($object, array $fields) { $all_day_field = idx($fields, 'isAllDay'); $start_field = idx($fields, 'start'); $end_field = idx($fields, 'end'); if ($all_day_field) { $is_all_day = $all_day_field->getValueForTransaction(); $control_ids = array(); if ($start_field) { $control_ids[] = $start_field->getControlID(); } if ($end_field) { $control_ids[] = $end_field->getControlID(); } Javelin::initBehavior( 'event-all-day', array( 'allDayID' => $all_day_field->getControlID(), 'controlIDs' => $control_ids, )); } else { $is_all_day = $object->getIsAllDay(); } if ($is_all_day) { if ($start_field) { $start_field->setHideTime(true); } if ($end_field) { $end_field->setHideTime(true); } } return $fields; } protected function newPages($object) { // Controls for event recurrence behavior go on a separate page which we // put in a dialog. This simplifies event creation in the common case. return array( id(new PhabricatorEditPage()) ->setKey('core') ->setLabel(pht('Core')) ->setIsDefault(true), id(new PhabricatorEditPage()) ->setKey('recurring') ->setLabel(pht('Recurrence')) ->setFieldKeys( array( 'isRecurring', 'frequency', 'until', )), ); } protected function willApplyTransactions($object, array $xactions) { $viewer = $this->getViewer(); $is_parent = $object->isParentEvent(); $is_child = $object->isChildEvent(); $is_future = ($this->getSeriesEditMode() === self::MODE_FUTURE); // Figure out which transactions we can apply to the whole series of events. // Some transactions (like comments) can never be bulk applied. $inherited_xactions = array(); foreach ($xactions as $xaction) { $modular_type = $xaction->getModularType(); if (!($modular_type instanceof PhabricatorCalendarEventTransactionType)) { continue; } $inherited_edit = $modular_type->isInheritedEdit(); if ($inherited_edit) { $inherited_xactions[] = $xaction; } } $this->rawTransactions = $this->cloneTransactions($inherited_xactions); $must_fork = ($is_child && $is_future) || ($is_parent && !$is_future); // We don't need to fork when editing a parent event if none of the edits // can transfer to child events. For example, commenting on a parent is // fine. if ($is_parent && !$is_future) { if (!$inherited_xactions) { $must_fork = false; } } if ($must_fork) { $fork_target = $object->loadForkTarget($viewer); if ($fork_target) { $fork_xaction = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventForkTransaction::TRANSACTIONTYPE) ->setNewValue(true); if ($fork_target->getPHID() == $object->getPHID()) { // We're forking the object itself, so just slip it into the // transactions we're going to apply. array_unshift($xactions, $fork_xaction); } else { // Otherwise, we're forking a different object, so we have to // apply that separately. $this->applyTransactions($fork_target, array($fork_xaction)); } } } return $xactions; } protected function didApplyTransactions($object, array $xactions) { $viewer = $this->getViewer(); if ($this->getSeriesEditMode() !== self::MODE_FUTURE) { return; } $targets = $object->loadFutureEvents($viewer); if (!$targets) { return; } foreach ($targets as $target) { $apply = $this->cloneTransactions($this->rawTransactions); $this->applyTransactions($target, $apply); } } private function applyTransactions($target, array $xactions) { $viewer = $this->getViewer(); // TODO: This isn't the most accurate source we could use, but this mode // is web-only for now. $content_source = PhabricatorContentSource::newForSource( PhabricatorWebContentSource::SOURCECONST); $editor = id(new PhabricatorCalendarEventEditor()) ->setActor($viewer) ->setContentSource($content_source) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); try { $editor->applyTransactions($target, $xactions); } catch (PhabricatorApplicationTransactionValidationException $ex) { // Just ignore any issues we run into. } } private function cloneTransactions(array $xactions) { $result = array(); foreach ($xactions as $xaction) { $result[] = clone $xaction; } return $result; } } diff --git a/src/applications/calendar/editor/PhabricatorCalendarExportEditEngine.php b/src/applications/calendar/editor/PhabricatorCalendarExportEditEngine.php index 8cd35e7323..bc8fc360c6 100644 --- a/src/applications/calendar/editor/PhabricatorCalendarExportEditEngine.php +++ b/src/applications/calendar/editor/PhabricatorCalendarExportEditEngine.php @@ -1,133 +1,133 @@ getViewer()); } protected function newObjectQuery() { return new PhabricatorCalendarExportQuery(); } protected function getObjectCreateTitleText($object) { return pht('Create New Export'); } protected function getObjectEditTitleText($object) { return pht('Edit Export: %s', $object->getName()); } protected function getObjectEditShortText($object) { return pht('Export %d', $object->getID()); } protected function getObjectCreateShortText() { return pht('Create Export'); } protected function getObjectName() { return pht('Export'); } protected function getObjectViewURI($object) { return $object->getURI(); } protected function getEditorURI() { return $this->getApplication()->getApplicationURI('export/edit/'); } protected function buildCustomEditFields($object) { $viewer = $this->getViewer(); $export_modes = PhabricatorCalendarExport::getAvailablePolicyModes(); $export_modes = array_fuse($export_modes); $current_mode = $object->getPolicyMode(); if (empty($export_modes[$current_mode])) { array_unshift($export_modes, $current_mode); } $mode_options = array(); foreach ($export_modes as $export_mode) { $mode_name = PhabricatorCalendarExport::getPolicyModeName($export_mode); $mode_summary = PhabricatorCalendarExport::getPolicyModeSummary( $export_mode); $mode_options[$export_mode] = pht('%s: %s', $mode_name, $mode_summary); } $fields = array( id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) ->setDescription(pht('Name of the export.')) ->setIsRequired(true) ->setTransactionType( PhabricatorCalendarExportNameTransaction::TRANSACTIONTYPE) ->setConduitDescription(pht('Rename the export.')) ->setConduitTypeDescription(pht('New export name.')) ->setValue($object->getName()), id(new PhabricatorBoolEditField()) ->setKey('disabled') ->setOptions(pht('Active'), pht('Disabled')) ->setLabel(pht('Disabled')) ->setDescription(pht('Disable the export.')) ->setTransactionType( PhabricatorCalendarExportDisableTransaction::TRANSACTIONTYPE) - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setConduitDescription(pht('Disable or restore the export.')) ->setConduitTypeDescription(pht('True to cancel the export.')) ->setValue($object->getIsDisabled()), id(new PhabricatorTextEditField()) ->setKey('queryKey') ->setLabel(pht('Query Key')) ->setDescription(pht('Query to execute.')) ->setIsRequired(true) ->setTransactionType( PhabricatorCalendarExportQueryKeyTransaction::TRANSACTIONTYPE) ->setConduitDescription(pht('Change the export query key.')) ->setConduitTypeDescription(pht('New export query key.')) ->setValue($object->getQueryKey()), id(new PhabricatorSelectEditField()) ->setKey('mode') ->setLabel(pht('Mode')) ->setTransactionType( PhabricatorCalendarExportModeTransaction::TRANSACTIONTYPE) ->setOptions($mode_options) ->setDescription(pht('Change the policy mode for the export.')) ->setConduitDescription(pht('Adjust export mode.')) ->setConduitTypeDescription(pht('New export mode.')) ->setValue($current_mode), ); return $fields; } } diff --git a/src/applications/calendar/editor/PhabricatorCalendarImportEditEngine.php b/src/applications/calendar/editor/PhabricatorCalendarImportEditEngine.php index 90b4962ca9..7be3969671 100644 --- a/src/applications/calendar/editor/PhabricatorCalendarImportEditEngine.php +++ b/src/applications/calendar/editor/PhabricatorCalendarImportEditEngine.php @@ -1,155 +1,155 @@ importEngine = $engine; return $this; } public function getImportEngine() { return $this->importEngine; } public function getEngineName() { return pht('Calendar Imports'); } public function isEngineConfigurable() { return false; } public function getSummaryHeader() { return pht('Configure Calendar Import Forms'); } public function getSummaryText() { return pht('Configure how users create and edit imports.'); } public function getEngineApplicationClass() { return 'PhabricatorCalendarApplication'; } protected function newEditableObject() { $viewer = $this->getViewer(); $engine = $this->getImportEngine(); return PhabricatorCalendarImport::initializeNewCalendarImport( $viewer, $engine); } protected function newObjectQuery() { return new PhabricatorCalendarImportQuery(); } protected function getObjectCreateTitleText($object) { return pht('Create New Import'); } protected function getObjectEditTitleText($object) { return pht('Edit Import: %s', $object->getDisplayName()); } protected function getObjectEditShortText($object) { return pht('Import %d', $object->getID()); } protected function getObjectCreateShortText() { return pht('Create Import'); } protected function getObjectName() { return pht('Import'); } protected function getObjectViewURI($object) { return $object->getURI(); } protected function getEditorURI() { return $this->getApplication()->getApplicationURI('import/edit/'); } protected function buildCustomEditFields($object) { $viewer = $this->getViewer(); $engine = $object->getEngine(); $can_trigger = $engine->supportsTriggers($object); $fields = array( id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) ->setDescription(pht('Name of the import.')) ->setTransactionType( PhabricatorCalendarImportNameTransaction::TRANSACTIONTYPE) ->setConduitDescription(pht('Rename the import.')) ->setConduitTypeDescription(pht('New import name.')) ->setPlaceholder($object->getDisplayName()) ->setValue($object->getName()), id(new PhabricatorBoolEditField()) ->setKey('disabled') ->setOptions(pht('Active'), pht('Disabled')) ->setLabel(pht('Disabled')) ->setDescription(pht('Disable the import.')) ->setTransactionType( PhabricatorCalendarImportDisableTransaction::TRANSACTIONTYPE) - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setConduitDescription(pht('Disable or restore the import.')) ->setConduitTypeDescription(pht('True to cancel the import.')) ->setValue($object->getIsDisabled()), id(new PhabricatorBoolEditField()) ->setKey('delete') ->setLabel(pht('Delete Imported Events')) ->setDescription(pht('Delete all events from this source.')) ->setTransactionType( PhabricatorCalendarImportDisableTransaction::TRANSACTIONTYPE) - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setConduitDescription(pht('Disable or restore the import.')) ->setConduitTypeDescription(pht('True to delete imported events.')) ->setValue(false), id(new PhabricatorBoolEditField()) ->setKey('reload') ->setLabel(pht('Reload Import')) ->setDescription(pht('Reload events imported from this source.')) ->setTransactionType( PhabricatorCalendarImportDisableTransaction::TRANSACTIONTYPE) - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setConduitDescription(pht('Disable or restore the import.')) ->setConduitTypeDescription(pht('True to reload the import.')) ->setValue(false), ); if ($can_trigger) { $frequency_map = PhabricatorCalendarImport::getTriggerFrequencyMap(); $frequency_options = ipull($frequency_map, 'name'); $fields[] = id(new PhabricatorSelectEditField()) ->setKey('frequency') ->setLabel(pht('Update Automatically')) ->setDescription(pht('Configure an automatic update frequency.')) ->setTransactionType( PhabricatorCalendarImportFrequencyTransaction::TRANSACTIONTYPE) ->setConduitDescription(pht('Set the automatic update frequency.')) ->setConduitTypeDescription(pht('Update frequency constant.')) ->setValue($object->getTriggerFrequency()) ->setOptions($frequency_options); } $import_engine = $object->getEngine(); foreach ($import_engine->newEditEngineFields($this, $object) as $field) { $fields[] = $field; } return $fields; } } diff --git a/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php b/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php index 59d8476c87..cf7fc30554 100644 --- a/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php +++ b/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php @@ -1,305 +1,305 @@ cursor) { $now = PhabricatorTime::getNow(); $this->cursor = $now - phutil_units('10 minutes in seconds'); } return $this->cursor; } public function setCursor($cursor) { $this->cursor = $cursor; return $this; } public function setNotifyWindow($notify_window) { $this->notifyWindow = $notify_window; return $this; } public function getNotifyWindow() { if (!$this->notifyWindow) { return phutil_units('15 minutes in seconds'); } return $this->notifyWindow; } public function publishNotifications() { $cursor = $this->getCursor(); $now = PhabricatorTime::getNow(); if ($cursor > $now) { return; } $calendar_class = 'PhabricatorCalendarApplication'; if (!PhabricatorApplication::isClassInstalled($calendar_class)) { return; } try { $lock = PhabricatorGlobalLock::newLock('calendar.notify') ->lock(5); } catch (PhutilLockException $ex) { return; } $caught = null; try { $this->sendNotifications(); } catch (Exception $ex) { $caught = $ex; } $lock->unlock(); // Wait a little while before checking for new notifications to send. $this->setCursor($cursor + phutil_units('1 minute in seconds')); if ($caught) { throw $caught; } } private function sendNotifications() { $cursor = $this->getCursor(); $window_min = $cursor - phutil_units('16 hours in seconds'); $window_max = $cursor + phutil_units('16 hours in seconds'); $viewer = PhabricatorUser::getOmnipotentUser(); $events = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) ->withDateRange($window_min, $window_max) ->withIsCancelled(false) ->withIsImported(false) ->setGenerateGhosts(true) ->execute(); if (!$events) { // No events are starting soon in any timezone, so there is nothing // left to be done. return; } $attendee_map = array(); foreach ($events as $key => $event) { $notifiable_phids = array(); foreach ($event->getInvitees() as $invitee) { if (!$invitee->isAttending()) { continue; } $notifiable_phids[] = $invitee->getInviteePHID(); } if ($notifiable_phids) { $attendee_map[$key] = array_fuse($notifiable_phids); } else { unset($events[$key]); } } if (!$attendee_map) { // None of the events have any notifiable attendees, so there is no // one to notify of anything. return; } $all_attendees = array(); foreach ($attendee_map as $key => $attendee_phids) { foreach ($attendee_phids as $attendee_phid) { $all_attendees[$attendee_phid] = $attendee_phid; } } $user_map = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withPHIDs($all_attendees) ->withIsDisabled(false) ->needUserSettings(true) ->execute(); $user_map = mpull($user_map, null, 'getPHID'); if (!$user_map) { // None of the attendees are valid users: they're all imported users // or projects or invalid or some other kind of unnotifiable entity. return; } $all_event_phids = array(); foreach ($events as $key => $event) { foreach ($event->getNotificationPHIDs() as $phid) { $all_event_phids[$phid] = $phid; } } $table = new PhabricatorCalendarNotification(); $conn = $table->establishConnection('w'); $rows = queryfx_all( $conn, 'SELECT * FROM %T WHERE eventPHID IN (%Ls) AND targetPHID IN (%Ls)', $table->getTableName(), $all_event_phids, $all_attendees); $sent_map = array(); foreach ($rows as $row) { $event_phid = $row['eventPHID']; $target_phid = $row['targetPHID']; $initial_epoch = $row['utcInitialEpoch']; $sent_map[$event_phid][$target_phid][$initial_epoch] = $row; } $now = PhabricatorTime::getNow(); $notify_min = $now; $notify_max = $now + $this->getNotifyWindow(); $notify_map = array(); foreach ($events as $key => $event) { $initial_epoch = $event->getUTCInitialEpoch(); $event_phids = $event->getNotificationPHIDs(); // Select attendees who actually exist, and who we have not sent any // notifications to yet. $attendee_phids = $attendee_map[$key]; $users = array_select_keys($user_map, $attendee_phids); foreach ($users as $user_phid => $user) { foreach ($event_phids as $event_phid) { if (isset($sent_map[$event_phid][$user_phid][$initial_epoch])) { unset($users[$user_phid]); continue 2; } } } if (!$users) { continue; } // Discard attendees for whom the event start time isn't soon. Events // may start at different times for different users, so we need to // check every user's start time. foreach ($users as $user_phid => $user) { $user_datetime = $event->newStartDateTime() ->setViewerTimezone($user->getTimezoneIdentifier()); $user_epoch = $user_datetime->getEpoch(); if ($user_epoch < $notify_min || $user_epoch > $notify_max) { unset($users[$user_phid]); continue; } $view = id(new PhabricatorCalendarEventNotificationView()) ->setViewer($user) ->setEvent($event) ->setDateTime($user_datetime) ->setEpoch($user_epoch); $notify_map[$user_phid][] = $view; } } $mail_list = array(); $mark_list = array(); $now = PhabricatorTime::getNow(); foreach ($notify_map as $user_phid => $events) { $user = $user_map[$user_phid]; $locale = PhabricatorEnv::beginScopedLocale($user->getTranslation()); $caught = null; try { $mail_list[] = $this->newMailMessage($user, $events); } catch (Exception $ex) { $caught = $ex; } unset($locale); if ($caught) { throw $ex; } foreach ($events as $view) { $event = $view->getEvent(); foreach ($event->getNotificationPHIDs() as $phid) { $mark_list[] = qsprintf( $conn, '(%s, %s, %d, %d)', $phid, $user_phid, $event->getUTCInitialEpoch(), $now); } } } // Mark all the notifications we're about to send as delivered so we // do not double-notify. foreach (PhabricatorLiskDAO::chunkSQL($mark_list) as $chunk) { queryfx( $conn, 'INSERT IGNORE INTO %T (eventPHID, targetPHID, utcInitialEpoch, didNotifyEpoch) - VALUES %Q', + VALUES %LQ', $table->getTableName(), $chunk); } foreach ($mail_list as $mail) { $mail->saveAndSend(); } } private function newMailMessage(PhabricatorUser $viewer, array $events) { $events = msort($events, 'getEpoch'); $next_event = head($events); $body = new PhabricatorMetaMTAMailBody(); foreach ($events as $event) { $body->addTextSection( null, pht( '%s is starting in %s minute(s), at %s.', $event->getEvent()->getName(), $event->getDisplayMinutes(), $event->getDisplayTimeWithTimezone())); $body->addLinkSection( pht('EVENT DETAIL'), PhabricatorEnv::getProductionURI($event->getEvent()->getURI())); } $next_event = head($events)->getEvent(); $subject = $next_event->getName(); if (count($events) > 1) { $more = pht( '(+%s more...)', new PhutilNumber(count($events) - 1)); $subject = "{$subject} {$more}"; } $calendar_phid = id(new PhabricatorCalendarApplication()) ->getPHID(); return id(new PhabricatorMetaMTAMail()) ->setSubject($subject) ->addTos(array($viewer->getPHID())) ->setSensitiveContent(false) ->setFrom($calendar_phid) ->setIsBulk(true) ->setSubjectPrefix(pht('[Calendar]')) ->setVarySubjectPrefix(pht('[Reminder]')) ->setThreadID($next_event->getPHID(), false) ->setRelatedPHID($next_event->getPHID()) ->setBody($body->render()) ->setHTMLBody($body->renderHTML()); } } diff --git a/src/applications/calendar/query/PhabricatorCalendarEventInviteeQuery.php b/src/applications/calendar/query/PhabricatorCalendarEventInviteeQuery.php index 5ffa2a8aab..683d6cd918 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventInviteeQuery.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventInviteeQuery.php @@ -1,99 +1,99 @@ ids = $ids; return $this; } public function withEventPHIDs(array $phids) { $this->eventPHIDs = $phids; return $this; } public function withInviteePHIDs(array $phids) { $this->inviteePHIDs = $phids; return $this; } public function withInviterPHIDs(array $phids) { $this->inviterPHIDs = $phids; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } protected function loadPage() { $table = new PhabricatorCalendarEventInvitee(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->eventPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'eventPHID IN (%Ls)', $this->eventPHIDs); } if ($this->inviteePHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'inviteePHID IN (%Ls)', $this->inviteePHIDs); } if ($this->inviterPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'inviterPHID IN (%Ls)', $this->inviterPHIDs); } if ($this->statuses !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'status = %d', $this->statuses); } - $where[] = $this->buildPagingClause($conn_r); + $where[] = $this->buildPagingClause($conn); - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } public function getQueryApplicationClass() { return 'PhabricatorCalendarApplication'; } } diff --git a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php index 9b7189cfdf..fc1399fdb3 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php @@ -1,753 +1,748 @@ generateGhosts = $generate_ghosts; return $this; } public function withIDs(array $ids) { $this->ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withDateRange($begin, $end) { $this->rangeBegin = $begin; $this->rangeEnd = $end; return $this; } public function withUTCInitialEpochBetween($min, $max) { $this->utcInitialEpochMin = $min; $this->utcInitialEpochMax = $max; return $this; } public function withInvitedPHIDs(array $phids) { $this->inviteePHIDs = $phids; return $this; } public function withHostPHIDs(array $phids) { $this->hostPHIDs = $phids; return $this; } public function withIsCancelled($is_cancelled) { $this->isCancelled = $is_cancelled; return $this; } public function withIsStub($is_stub) { $this->isStub = $is_stub; return $this; } public function withEventsWithNoParent($events_with_no_parent) { $this->eventsWithNoParent = $events_with_no_parent; return $this; } public function withInstanceSequencePairs(array $pairs) { $this->instanceSequencePairs = $pairs; return $this; } public function withParentEventPHIDs(array $parent_phids) { $this->parentEventPHIDs = $parent_phids; return $this; } public function withImportSourcePHIDs(array $import_phids) { $this->importSourcePHIDs = $import_phids; return $this; } public function withImportAuthorPHIDs(array $author_phids) { $this->importAuthorPHIDs = $author_phids; return $this; } public function withImportUIDs(array $uids) { $this->importUIDs = $uids; return $this; } public function withIsImported($is_imported) { $this->isImported = $is_imported; return $this; } public function needRSVPs(array $phids) { $this->needRSVPs = $phids; return $this; } protected function getDefaultOrderVector() { return array('start', 'id'); } public function getBuiltinOrders() { return array( 'start' => array( 'vector' => array('start', 'id'), 'name' => pht('Event Start'), ), ) + parent::getBuiltinOrders(); } public function getOrderableColumns() { return array( 'start' => array( 'table' => $this->getPrimaryTableAlias(), 'column' => 'utcInitialEpoch', 'reverse' => true, 'type' => 'int', 'unique' => false, ), ) + parent::getOrderableColumns(); } protected function getPagingValueMap($cursor, array $keys) { $event = $this->loadCursorObject($cursor); return array( 'start' => $event->getStartDateTimeEpoch(), 'id' => $event->getID(), ); } protected function shouldLimitResults() { // When generating ghosts, we can't rely on database ordering because // MySQL can't predict the ghost start times. We'll just load all matching // events, then generate results from there. if ($this->generateGhosts) { return false; } return true; } protected function loadPage() { $events = $this->loadStandardPage($this->newResultObject()); $viewer = $this->getViewer(); foreach ($events as $event) { $event->applyViewerTimezone($viewer); } if (!$this->generateGhosts) { return $events; } $raw_limit = $this->getRawResultLimit(); if (!$raw_limit && !$this->rangeEnd) { throw new Exception( pht( 'Event queries which generate ghost events must include either a '. 'result limit or an end date, because they may otherwise generate '. 'an infinite number of results. This query has neither.')); } foreach ($events as $key => $event) { $sequence_start = 0; $sequence_end = null; $end = null; $instance_of = $event->getInstanceOfEventPHID(); if ($instance_of == null && $this->isCancelled !== null) { if ($event->getIsCancelled() != $this->isCancelled) { unset($events[$key]); continue; } } } // Pull out all of the parents first. We may discard them as we begin // generating ghost events, but we still want to process all of them. $parents = array(); foreach ($events as $key => $event) { if ($event->isParentEvent()) { $parents[$key] = $event; } } // Now that we've picked out all the parent events, we can immediately // discard anything outside of the time window. $events = $this->getEventsInRange($events); $generate_from = $this->rangeBegin; $generate_until = $this->rangeEnd; foreach ($parents as $key => $event) { $duration = $event->getDuration(); $start_date = $this->getRecurrenceWindowStart( $event, $generate_from - $duration); $end_date = $this->getRecurrenceWindowEnd( $event, $generate_until); $limit = $this->getRecurrenceLimit($event, $raw_limit); $set = $event->newRecurrenceSet(); $recurrences = $set->getEventsBetween( $start_date, $end_date, $limit + 1); // We're generating events from the beginning and then filtering them // here (instead of only generating events starting at the start date) // because we need to know the proper sequence indexes to generate ghost // events. This may change after RDATE support. if ($start_date) { $start_epoch = $start_date->getEpoch(); } else { $start_epoch = null; } foreach ($recurrences as $sequence_index => $sequence_datetime) { if (!$sequence_index) { // This is the parent event, which we already have. continue; } if ($start_epoch) { if ($sequence_datetime->getEpoch() < $start_epoch) { continue; } } $events[] = $event->newGhost( $viewer, $sequence_index, $sequence_datetime); } // NOTE: We're slicing results every time because this makes it cheaper // to generate future ghosts. If we already have 100 events that occur // before July 1, we know we never need to generate ghosts after that // because they couldn't possibly ever appear in the result set. if ($raw_limit) { if (count($events) > $raw_limit) { $events = msort($events, 'getStartDateTimeEpoch'); $events = array_slice($events, 0, $raw_limit, true); $generate_until = last($events)->getEndDateTimeEpoch(); } } } // Now that we're done generating ghost events, we're going to remove any // ghosts that we have concrete events for (or which we can load the // concrete events for). These concrete events are generated when users // edit a ghost, and replace the ghost events. // First, generate a map of all concrete events we // already loaded. We don't need to load these again. $have_pairs = array(); foreach ($events as $event) { if ($event->getIsGhostEvent()) { continue; } $parent_phid = $event->getInstanceOfEventPHID(); $sequence = $event->getSequenceIndex(); $have_pairs[$parent_phid][$sequence] = true; } // Now, generate a map of all events we generated // ghosts for. We need to try to load these if we don't already have them. $map = array(); $parent_pairs = array(); foreach ($events as $key => $event) { if (!$event->getIsGhostEvent()) { continue; } $parent_phid = $event->getInstanceOfEventPHID(); $sequence = $event->getSequenceIndex(); // We already loaded the concrete version of this event, so we can just // throw out the ghost and move on. if (isset($have_pairs[$parent_phid][$sequence])) { unset($events[$key]); continue; } // We didn't load the concrete version of this event, so we need to // try to load it if it exists. $parent_pairs[] = array($parent_phid, $sequence); $map[$parent_phid][$sequence] = $key; } if ($parent_pairs) { $instances = id(new self()) ->setViewer($viewer) ->setParentQuery($this) ->withInstanceSequencePairs($parent_pairs) ->execute(); foreach ($instances as $instance) { $parent_phid = $instance->getInstanceOfEventPHID(); $sequence = $instance->getSequenceIndex(); $indexes = idx($map, $parent_phid); $key = idx($indexes, $sequence); // Replace the ghost with the corresponding concrete event. $events[$key] = $instance; } } $events = msort($events, 'getStartDateTimeEpoch'); return $events; } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn_r) { $parts = parent::buildJoinClauseParts($conn_r); if ($this->inviteePHIDs !== null) { $parts[] = qsprintf( $conn_r, 'JOIN %T invitee ON invitee.eventPHID = event.phid AND invitee.status != %s', id(new PhabricatorCalendarEventInvitee())->getTableName(), PhabricatorCalendarEventInvitee::STATUS_UNINVITED); } return $parts; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'event.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'event.phid IN (%Ls)', $this->phids); } // NOTE: The date ranges we query for are larger than the requested ranges // because we need to catch all-day events. We'll refine this range later // after adjusting the visible range of events we load. if ($this->rangeBegin) { $where[] = qsprintf( $conn, '(event.utcUntilEpoch >= %d) OR (event.utcUntilEpoch IS NULL)', $this->rangeBegin - phutil_units('16 hours in seconds')); } if ($this->rangeEnd) { $where[] = qsprintf( $conn, 'event.utcInitialEpoch <= %d', $this->rangeEnd + phutil_units('16 hours in seconds')); } if ($this->utcInitialEpochMin !== null) { $where[] = qsprintf( $conn, 'event.utcInitialEpoch >= %d', $this->utcInitialEpochMin); } if ($this->utcInitialEpochMax !== null) { $where[] = qsprintf( $conn, 'event.utcInitialEpoch <= %d', $this->utcInitialEpochMax); } if ($this->inviteePHIDs !== null) { $where[] = qsprintf( $conn, 'invitee.inviteePHID IN (%Ls)', $this->inviteePHIDs); } if ($this->hostPHIDs !== null) { $where[] = qsprintf( $conn, 'event.hostPHID IN (%Ls)', $this->hostPHIDs); } if ($this->isCancelled !== null) { $where[] = qsprintf( $conn, 'event.isCancelled = %d', (int)$this->isCancelled); } if ($this->eventsWithNoParent == true) { $where[] = qsprintf( $conn, 'event.instanceOfEventPHID IS NULL'); } if ($this->instanceSequencePairs !== null) { $sql = array(); foreach ($this->instanceSequencePairs as $pair) { $sql[] = qsprintf( $conn, '(event.instanceOfEventPHID = %s AND event.sequenceIndex = %d)', $pair[0], $pair[1]); } $where[] = qsprintf( $conn, - '%Q', - implode(' OR ', $sql)); + '%LO', + $sql); } if ($this->isStub !== null) { $where[] = qsprintf( $conn, 'event.isStub = %d', (int)$this->isStub); } if ($this->parentEventPHIDs !== null) { $where[] = qsprintf( $conn, 'event.instanceOfEventPHID IN (%Ls)', $this->parentEventPHIDs); } if ($this->importSourcePHIDs !== null) { $where[] = qsprintf( $conn, 'event.importSourcePHID IN (%Ls)', $this->importSourcePHIDs); } if ($this->importAuthorPHIDs !== null) { $where[] = qsprintf( $conn, 'event.importAuthorPHID IN (%Ls)', $this->importAuthorPHIDs); } if ($this->importUIDs !== null) { $where[] = qsprintf( $conn, 'event.importUID IN (%Ls)', $this->importUIDs); } if ($this->isImported !== null) { if ($this->isImported) { $where[] = qsprintf( $conn, 'event.importSourcePHID IS NOT NULL'); } else { $where[] = qsprintf( $conn, 'event.importSourcePHID IS NULL'); } } return $where; } protected function getPrimaryTableAlias() { return 'event'; } protected function shouldGroupQueryResultRows() { if ($this->inviteePHIDs !== null) { return true; } return parent::shouldGroupQueryResultRows(); } - protected function getApplicationSearchObjectPHIDColumn() { - return 'event.phid'; - } - public function getQueryApplicationClass() { return 'PhabricatorCalendarApplication'; } - protected function willFilterPage(array $events) { $instance_of_event_phids = array(); $recurring_events = array(); $viewer = $this->getViewer(); $events = $this->getEventsInRange($events); $import_phids = array(); foreach ($events as $event) { $import_phid = $event->getImportSourcePHID(); if ($import_phid !== null) { $import_phids[$import_phid] = $import_phid; } } if ($import_phids) { $imports = id(new PhabricatorCalendarImportQuery()) ->setParentQuery($this) ->setViewer($viewer) ->withPHIDs($import_phids) ->execute(); $imports = mpull($imports, null, 'getPHID'); } else { $imports = array(); } foreach ($events as $key => $event) { $import_phid = $event->getImportSourcePHID(); if ($import_phid === null) { $event->attachImportSource(null); continue; } $import = idx($imports, $import_phid); if (!$import) { unset($events[$key]); $this->didRejectResult($event); continue; } $event->attachImportSource($import); } $phids = array(); foreach ($events as $event) { $phids[] = $event->getPHID(); $instance_of = $event->getInstanceOfEventPHID(); if ($instance_of) { $instance_of_event_phids[] = $instance_of; } } if (count($instance_of_event_phids) > 0) { $recurring_events = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) ->withPHIDs($instance_of_event_phids) ->withEventsWithNoParent(true) ->execute(); $recurring_events = mpull($recurring_events, null, 'getPHID'); } if ($events) { $invitees = id(new PhabricatorCalendarEventInviteeQuery()) ->setViewer($viewer) ->withEventPHIDs($phids) ->execute(); $invitees = mgroup($invitees, 'getEventPHID'); } else { $invitees = array(); } foreach ($events as $key => $event) { $event_invitees = idx($invitees, $event->getPHID(), array()); $event->attachInvitees($event_invitees); $instance_of = $event->getInstanceOfEventPHID(); if (!$instance_of) { continue; } $parent = idx($recurring_events, $instance_of); // should never get here if (!$parent) { unset($events[$key]); continue; } $event->attachParentEvent($parent); if ($this->isCancelled !== null) { if ($event->getIsCancelled() != $this->isCancelled) { unset($events[$key]); continue; } } } $events = msort($events, 'getStartDateTimeEpoch'); if ($this->needRSVPs) { $rsvp_phids = $this->needRSVPs; $project_type = PhabricatorProjectProjectPHIDType::TYPECONST; $project_phids = array(); foreach ($events as $event) { foreach ($event->getInvitees() as $invitee) { $invitee_phid = $invitee->getInviteePHID(); if (phid_get_type($invitee_phid) == $project_type) { $project_phids[] = $invitee_phid; } } } if ($project_phids) { $member_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST; $query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($project_phids) ->withEdgeTypes(array($member_type)) ->withDestinationPHIDs($rsvp_phids); $edges = $query->execute(); $project_map = array(); foreach ($edges as $src => $types) { foreach ($types as $type => $dsts) { foreach ($dsts as $dst => $edge) { $project_map[$dst][] = $src; } } } } else { $project_map = array(); } $membership_map = array(); foreach ($rsvp_phids as $rsvp_phid) { $membership_map[$rsvp_phid] = array(); $membership_map[$rsvp_phid][] = $rsvp_phid; $project_phids = idx($project_map, $rsvp_phid); if ($project_phids) { foreach ($project_phids as $project_phid) { $membership_map[$rsvp_phid][] = $project_phid; } } } foreach ($events as $event) { $invitees = $event->getInvitees(); $invitees = mpull($invitees, null, 'getInviteePHID'); $rsvp_map = array(); foreach ($rsvp_phids as $rsvp_phid) { $membership_phids = $membership_map[$rsvp_phid]; $rsvps = array_select_keys($invitees, $membership_phids); $rsvp_map[$rsvp_phid] = $rsvps; } $event->attachRSVPs($rsvp_map); } } return $events; } private function getEventsInRange(array $events) { $range_start = $this->rangeBegin; $range_end = $this->rangeEnd; foreach ($events as $key => $event) { $event_start = $event->getStartDateTimeEpoch(); $event_end = $event->getEndDateTimeEpoch(); if ($range_start && $event_end < $range_start) { unset($events[$key]); } if ($range_end && $event_start > $range_end) { unset($events[$key]); } } return $events; } private function getRecurrenceWindowStart( PhabricatorCalendarEvent $event, $generate_from) { if (!$generate_from) { return null; } return PhutilCalendarAbsoluteDateTime::newFromEpoch($generate_from); } private function getRecurrenceWindowEnd( PhabricatorCalendarEvent $event, $generate_until) { $end_epochs = array(); if ($generate_until) { $end_epochs[] = $generate_until; } $until_epoch = $event->getUntilDateTimeEpoch(); if ($until_epoch) { $end_epochs[] = $until_epoch; } if (!$end_epochs) { return null; } return PhutilCalendarAbsoluteDateTime::newFromEpoch(min($end_epochs)); } private function getRecurrenceLimit( PhabricatorCalendarEvent $event, $raw_limit) { $count = $event->getRecurrenceCount(); if ($count && ($count <= $raw_limit)) { return ($count - 1); } return $raw_limit; } } diff --git a/src/applications/chatlog/query/PhabricatorChatLogChannelQuery.php b/src/applications/chatlog/query/PhabricatorChatLogChannelQuery.php index 2aded5c11b..a13514eec7 100644 --- a/src/applications/chatlog/query/PhabricatorChatLogChannelQuery.php +++ b/src/applications/chatlog/query/PhabricatorChatLogChannelQuery.php @@ -1,63 +1,63 @@ channels = $channels; return $this; } public function withIDs(array $channel_ids) { $this->channelIDs = $channel_ids; return $this; } protected function loadPage() { $table = new PhabricatorChatLogChannel(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T c %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); $logs = $table->loadAllFromArray($data); return $logs; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); - $where[] = $this->buildPagingClause($conn_r); + $where[] = $this->buildPagingClause($conn); if ($this->channelIDs) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->channelIDs); } if ($this->channels) { $where[] = qsprintf( - $conn_r, + $conn, 'channelName IN (%Ls)', $this->channels); } - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } public function getQueryApplicationClass() { return 'PhabricatorChatLogApplication'; } } diff --git a/src/applications/chatlog/query/PhabricatorChatLogQuery.php b/src/applications/chatlog/query/PhabricatorChatLogQuery.php index d174fdac90..88cf6da7e3 100644 --- a/src/applications/chatlog/query/PhabricatorChatLogQuery.php +++ b/src/applications/chatlog/query/PhabricatorChatLogQuery.php @@ -1,84 +1,84 @@ channelIDs = $channel_ids; return $this; } public function withMaximumEpoch($epoch) { $this->maximumEpoch = $epoch; return $this; } protected function loadPage() { $table = new PhabricatorChatLogEvent(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T e %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); $logs = $table->loadAllFromArray($data); return $logs; } protected function willFilterPage(array $events) { $channel_ids = mpull($events, 'getChannelID', 'getChannelID'); $channels = id(new PhabricatorChatLogChannelQuery()) ->setViewer($this->getViewer()) ->withIDs($channel_ids) ->execute(); $channels = mpull($channels, null, 'getID'); foreach ($events as $key => $event) { $channel = idx($channels, $event->getChannelID()); if (!$channel) { unset($events[$key]); continue; } $event->attachChannel($channel); } return $events; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); - $where[] = $this->buildPagingClause($conn_r); + $where[] = $this->buildPagingClause($conn); - if ($this->maximumEpoch) { + if ($this->maximumEpoch !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'epoch <= %d', $this->maximumEpoch); } - if ($this->channelIDs) { + if ($this->channelIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'channelID IN (%Ld)', $this->channelIDs); } - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } public function getQueryApplicationClass() { return 'PhabricatorChatLogApplication'; } } diff --git a/src/applications/config/editor/PhabricatorConfigEditor.php b/src/applications/config/editor/PhabricatorConfigEditor.php index f776c3ec0c..deccf1ef5c 100644 --- a/src/applications/config/editor/PhabricatorConfigEditor.php +++ b/src/applications/config/editor/PhabricatorConfigEditor.php @@ -1,160 +1,165 @@ getTransactionType()) { case PhabricatorConfigTransaction::TYPE_EDIT: return array( 'deleted' => (int)$object->getIsDeleted(), 'value' => $object->getValue(), ); } } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorConfigTransaction::TYPE_EDIT: return $xaction->getNewValue(); } } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorConfigTransaction::TYPE_EDIT: $v = $xaction->getNewValue(); // If this is a defined configuration option (vs a straggler from an // old version of Phabricator or a configuration file misspelling) // submit it to the validation gauntlet. $key = $object->getConfigKey(); $all_options = PhabricatorApplicationConfigOptions::loadAllOptions(); $option = idx($all_options, $key); if ($option) { $option->getGroup()->validateOption( $option, $v['value']); } $object->setIsDeleted((int)$v['deleted']); $object->setValue($v['value']); break; } } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { return; } protected function mergeTransactions( PhabricatorApplicationTransaction $u, PhabricatorApplicationTransaction $v) { $type = $u->getTransactionType(); switch ($type) { case PhabricatorConfigTransaction::TYPE_EDIT: return $v; } return parent::mergeTransactions($u, $v); } protected function transactionHasEffect( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $type = $xaction->getTransactionType(); switch ($type) { case PhabricatorConfigTransaction::TYPE_EDIT: // If an edit deletes an already-deleted entry, no-op it. if (idx($old, 'deleted') && idx($new, 'deleted')) { return false; } break; } return parent::transactionHasEffect($object, $xaction); } protected function didApplyTransactions($object, array $xactions) { // Force all the setup checks to run on the next page load. PhabricatorSetupCheck::deleteSetupCheckCache(); return $xactions; } public static function storeNewValue( PhabricatorUser $user, PhabricatorConfigEntry $config_entry, $value, - PhabricatorContentSource $source) { + PhabricatorContentSource $source, + $acting_as_phid = null) { $xaction = id(new PhabricatorConfigTransaction()) ->setTransactionType(PhabricatorConfigTransaction::TYPE_EDIT) ->setNewValue( array( 'deleted' => false, 'value' => $value, )); $editor = id(new PhabricatorConfigEditor()) ->setActor($user) ->setContinueOnNoEffect(true) ->setContentSource($source); + if ($acting_as_phid) { + $editor->setActingAsPHID($acting_as_phid); + } + $editor->applyTransactions($config_entry, array($xaction)); } public static function deleteConfig( PhabricatorUser $user, PhabricatorConfigEntry $config_entry, PhabricatorContentSource $source) { $xaction = id(new PhabricatorConfigTransaction()) ->setTransactionType(PhabricatorConfigTransaction::TYPE_EDIT) ->setNewValue( array( 'deleted' => true, 'value' => null, )); $editor = id(new PhabricatorConfigEditor()) ->setActor($user) ->setContinueOnNoEffect(true) ->setContentSource($source); $editor->applyTransactions($config_entry, array($xaction)); } } diff --git a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php index 8a236a883b..ad8a62d184 100644 --- a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php +++ b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php @@ -1,337 +1,321 @@ deformat(pht(<<deformat(pht(<<deformat(pht(<<deformat(pht(<<deformat(pht(<<deformat(pht(<<deformat(pht(<<deformat(pht(<<deformat(pht(<<deformat(pht(<<deformat(pht(<<deformat(pht(<<deformat(pht(<<deformat(pht(<<' - `real`: 'George Washington ' - `full`: 'gwashington (George Washington) ' The default is `full`. EODOC )); $mailers_description = $this->deformat(pht(<<newOption('cluster.mailers', 'cluster.mailers', null) ->setHidden(true) ->setDescription($mailers_description), $this->newOption( 'metamta.default-address', 'string', 'noreply@phabricator.example.com') ->setDescription(pht('Default "From" address.')), $this->newOption( 'metamta.domain', 'string', 'phabricator.example.com') ->setDescription(pht('Domain used to generate Message-IDs.')), $this->newOption( 'metamta.mail-adapter', 'class', 'PhabricatorMailImplementationPHPMailerLiteAdapter') ->setBaseClass('PhabricatorMailImplementationAdapter') ->setSummary(pht('Control how mail is sent.')) ->setDescription($adapter_description), $this->newOption( 'metamta.one-mail-per-recipient', 'bool', true) ->setLocked(true) ->setBoolOptions( array( pht('Send Mail To Each Recipient'), pht('Send Mail To All Recipients'), )) ->setSummary( pht( 'Controls whether Phabricator sends one email with multiple '. 'recipients in the "To:" line, or multiple emails, each with a '. 'single recipient in the "To:" line.')) ->setDescription($one_mail_per_recipient_desc), $this->newOption('metamta.can-send-as-user', 'bool', false) ->setBoolOptions( array( pht('Send as User Taking Action'), pht('Send as Phabricator'), )) ->setSummary( pht( 'Controls whether Phabricator sends email "From" users.')) ->setDescription($send_as_user_desc), $this->newOption( 'metamta.reply-handler-domain', 'string', null) ->setLocked(true) ->setDescription(pht('Domain used for reply email addresses.')) ->addExample('phabricator.example.com', ''), - $this->newOption('metamta.herald.show-hints', 'bool', true) - ->setBoolOptions( - array( - pht('Show Herald Hints'), - pht('No Herald Hints'), - )) - ->setSummary(pht('Show hints about Herald rules in email.')) - ->setDescription($herald_hints_description), $this->newOption('metamta.recipients.show-hints', 'bool', true) ->setBoolOptions( array( pht('Show Recipient Hints'), pht('No Recipient Hints'), )) ->setSummary(pht('Show "To:" and "Cc:" footer hints in email.')) ->setDescription($recipient_hints_description), $this->newOption('metamta.email-preferences', 'bool', true) ->setBoolOptions( array( pht('Show Email Preferences Link'), pht('No Email Preferences Link'), )) ->setSummary(pht('Show email preferences link in email.')) ->setDescription($email_preferences_description), $this->newOption('metamta.insecure-auth-with-reply-to', 'bool', false) ->setBoolOptions( array( pht('Allow Insecure Reply-To Auth'), pht('Disallow Reply-To Auth'), )) ->setSummary(pht('Trust "Reply-To" headers for authentication.')) ->setDescription($reply_to_description), $this->newOption('metamta.placeholder-to-recipient', 'string', null) ->setSummary(pht('Placeholder for mail with only CCs.')) ->setDescription($placeholder_description), $this->newOption('metamta.public-replies', 'bool', false) ->setBoolOptions( array( pht('Use Public Replies (Less Secure)'), pht('Use Private Replies (More Secure)'), )) ->setSummary( pht( 'Phabricator can use less-secure but mailing list friendly public '. 'reply addresses.')) ->setDescription($public_replies_description), $this->newOption('metamta.single-reply-handler-prefix', 'string', null) ->setSummary( pht('Allow Phabricator to use a single mailbox for all replies.')) ->setDescription($single_description), $this->newOption('metamta.user-address-format', 'enum', 'full') ->setEnumOptions( array( 'short' => pht('Short'), 'real' => pht('Real'), 'full' => pht('Full'), )) ->setSummary(pht('Control how Phabricator renders user names in mail.')) ->setDescription($address_description) ->addExample('gwashington ', 'short') ->addExample('George Washington ', 'real') ->addExample( 'gwashington (George Washington) ', 'full'), $this->newOption('metamta.email-body-limit', 'int', 524288) ->setDescription( pht( 'You can set a limit for the maximum byte size of outbound mail. '. 'Mail which is larger than this limit will be truncated before '. 'being sent. This can be useful if your MTA rejects mail which '. 'exceeds some limit (this is reasonably common). Specify a value '. 'in bytes.')) ->setSummary(pht('Global cap for size of generated emails (bytes).')) ->addExample(524288, pht('Truncate at 512KB')) ->addExample(1048576, pht('Truncate at 1MB')), ); } } diff --git a/src/applications/config/query/PhabricatorConfigEntryQuery.php b/src/applications/config/query/PhabricatorConfigEntryQuery.php index 56bf15a267..f46fdb7d1e 100644 --- a/src/applications/config/query/PhabricatorConfigEntryQuery.php +++ b/src/applications/config/query/PhabricatorConfigEntryQuery.php @@ -1,60 +1,60 @@ ids = $ids; return $this; } public function withPHIDs($phids) { $this->phids = $phids; return $this; } protected function loadPage() { $table = new PhabricatorConfigEntry(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); - if ($this->ids) { + if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } - if ($this->phids) { + if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } - $where[] = $this->buildPagingClause($conn_r); + $where[] = $this->buildPagingClause($conn); - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } public function getQueryApplicationClass() { return 'PhabricatorConfigApplication'; } } diff --git a/src/applications/config/schema/PhabricatorConfigSchemaQuery.php b/src/applications/config/schema/PhabricatorConfigSchemaQuery.php index 2cb7763110..4a93396671 100644 --- a/src/applications/config/schema/PhabricatorConfigSchemaQuery.php +++ b/src/applications/config/schema/PhabricatorConfigSchemaQuery.php @@ -1,380 +1,380 @@ refs = $refs; return $this; } public function getRefs() { if (!$this->refs) { return PhabricatorDatabaseRef::getMasterDatabaseRefs(); } return $this->refs; } public function setAPIs(array $apis) { $map = array(); foreach ($apis as $api) { $map[$api->getRef()->getRefKey()] = $api; } $this->apis = $map; return $this; } private function getDatabaseNames(PhabricatorDatabaseRef $ref) { $api = $this->getAPI($ref); $patches = PhabricatorSQLPatchList::buildAllPatches(); return $api->getDatabaseList( $patches, $only_living = true); } private function getAPI(PhabricatorDatabaseRef $ref) { $key = $ref->getRefKey(); if (isset($this->apis[$key])) { return $this->apis[$key]; } return id(new PhabricatorStorageManagementAPI()) ->setUser($ref->getUser()) ->setHost($ref->getHost()) ->setPort($ref->getPort()) ->setNamespace(PhabricatorLiskDAO::getDefaultStorageNamespace()) ->setPassword($ref->getPass()); } public function loadActualSchemata() { $refs = $this->getRefs(); $schemata = array(); foreach ($refs as $ref) { $schema = $this->loadActualSchemaForServer($ref); $schemata[$schema->getRef()->getRefKey()] = $schema; } return $schemata; } private function loadActualSchemaForServer(PhabricatorDatabaseRef $ref) { $databases = $this->getDatabaseNames($ref); $conn = $ref->newManagementConnection(); $tables = queryfx_all( $conn, 'SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_COLLATION, ENGINE FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA IN (%Ls)', $databases); $database_info = queryfx_all( $conn, 'SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME IN (%Ls)', $databases); $database_info = ipull($database_info, null, 'SCHEMA_NAME'); // Find databases which exist, but which the user does not have permission // to see. $invisible_databases = array(); foreach ($databases as $database_name) { if (isset($database_info[$database_name])) { continue; } try { queryfx($conn, 'SHOW TABLES IN %T', $database_name); } catch (AphrontAccessDeniedQueryException $ex) { // This database exists, the user just doesn't have permission to // see it. $invisible_databases[] = $database_name; } catch (AphrontSchemaQueryException $ex) { // This database is legitimately missing. } } $sql = array(); foreach ($tables as $table) { $sql[] = qsprintf( $conn, '(TABLE_SCHEMA = %s AND TABLE_NAME = %s)', $table['TABLE_SCHEMA'], $table['TABLE_NAME']); } if ($sql) { $column_info = queryfx_all( $conn, 'SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, CHARACTER_SET_NAME, COLLATION_NAME, COLUMN_TYPE, IS_NULLABLE, EXTRA FROM INFORMATION_SCHEMA.COLUMNS - WHERE (%Q)', - '('.implode(') OR (', $sql).')'); + WHERE %LO', + $sql); $column_info = igroup($column_info, 'TABLE_SCHEMA'); } else { $column_info = array(); } // NOTE: Tables like KEY_COLUMN_USAGE and TABLE_CONSTRAINTS only contain // primary, unique, and foreign keys, so we can't use them here. We pull // indexes later on using SHOW INDEXES. $server_schema = id(new PhabricatorConfigServerSchema()) ->setRef($ref); $tables = igroup($tables, 'TABLE_SCHEMA'); foreach ($tables as $database_name => $database_tables) { $info = $database_info[$database_name]; $database_schema = id(new PhabricatorConfigDatabaseSchema()) ->setName($database_name) ->setCharacterSet($info['DEFAULT_CHARACTER_SET_NAME']) ->setCollation($info['DEFAULT_COLLATION_NAME']); $database_column_info = idx($column_info, $database_name, array()); $database_column_info = igroup($database_column_info, 'TABLE_NAME'); foreach ($database_tables as $table) { $table_name = $table['TABLE_NAME']; $table_schema = id(new PhabricatorConfigTableSchema()) ->setName($table_name) ->setCollation($table['TABLE_COLLATION']) ->setEngine($table['ENGINE']); $columns = idx($database_column_info, $table_name, array()); foreach ($columns as $column) { if (strpos($column['EXTRA'], 'auto_increment') === false) { $auto_increment = false; } else { $auto_increment = true; } $column_schema = id(new PhabricatorConfigColumnSchema()) ->setName($column['COLUMN_NAME']) ->setCharacterSet($column['CHARACTER_SET_NAME']) ->setCollation($column['COLLATION_NAME']) ->setColumnType($column['COLUMN_TYPE']) ->setNullable($column['IS_NULLABLE'] == 'YES') ->setAutoIncrement($auto_increment); $table_schema->addColumn($column_schema); } $key_parts = queryfx_all( $conn, 'SHOW INDEXES FROM %T.%T', $database_name, $table_name); $keys = igroup($key_parts, 'Key_name'); foreach ($keys as $key_name => $key_pieces) { $key_pieces = isort($key_pieces, 'Seq_in_index'); $head = head($key_pieces); // This handles string indexes which index only a prefix of a field. $column_names = array(); foreach ($key_pieces as $piece) { $name = $piece['Column_name']; if ($piece['Sub_part']) { $name = $name.'('.$piece['Sub_part'].')'; } $column_names[] = $name; } $key_schema = id(new PhabricatorConfigKeySchema()) ->setName($key_name) ->setColumnNames($column_names) ->setUnique(!$head['Non_unique']) ->setIndexType($head['Index_type']); $table_schema->addKey($key_schema); } $database_schema->addTable($table_schema); } $server_schema->addDatabase($database_schema); } foreach ($invisible_databases as $database_name) { $server_schema->addDatabase( id(new PhabricatorConfigDatabaseSchema()) ->setName($database_name) ->setAccessDenied(true)); } return $server_schema; } public function loadExpectedSchemata() { $refs = $this->getRefs(); $schemata = array(); foreach ($refs as $ref) { $schema = $this->loadExpectedSchemaForServer($ref); $schemata[$schema->getRef()->getRefKey()] = $schema; } return $schemata; } public function loadExpectedSchemaForServer(PhabricatorDatabaseRef $ref) { $databases = $this->getDatabaseNames($ref); $info = $this->getAPI($ref)->getCharsetInfo(); $specs = id(new PhutilClassMapQuery()) ->setAncestorClass('PhabricatorConfigSchemaSpec') ->execute(); $server_schema = id(new PhabricatorConfigServerSchema()) ->setRef($ref); foreach ($specs as $spec) { $spec ->setUTF8Charset( $info[PhabricatorStorageManagementAPI::CHARSET_DEFAULT]) ->setUTF8BinaryCollation( $info[PhabricatorStorageManagementAPI::COLLATE_TEXT]) ->setUTF8SortingCollation( $info[PhabricatorStorageManagementAPI::COLLATE_SORT]) ->setServer($server_schema) ->buildSchemata($server_schema); } return $server_schema; } public function buildComparisonSchemata( array $expect_servers, array $actual_servers) { $schemata = array(); foreach ($actual_servers as $key => $actual_server) { $schemata[$key] = $this->buildComparisonSchemaForServer( $expect_servers[$key], $actual_server); } return $schemata; } private function buildComparisonSchemaForServer( PhabricatorConfigServerSchema $expect, PhabricatorConfigServerSchema $actual) { $comp_server = $actual->newEmptyClone(); $all_databases = $actual->getDatabases() + $expect->getDatabases(); foreach ($all_databases as $database_name => $database_template) { $actual_database = $actual->getDatabase($database_name); $expect_database = $expect->getDatabase($database_name); $issues = $this->compareSchemata($expect_database, $actual_database); $comp_database = $database_template->newEmptyClone() ->setIssues($issues); if (!$actual_database) { $actual_database = $expect_database->newEmptyClone(); } if (!$expect_database) { $expect_database = $actual_database->newEmptyClone(); } $all_tables = $actual_database->getTables() + $expect_database->getTables(); foreach ($all_tables as $table_name => $table_template) { $actual_table = $actual_database->getTable($table_name); $expect_table = $expect_database->getTable($table_name); $issues = $this->compareSchemata($expect_table, $actual_table); $comp_table = $table_template->newEmptyClone() ->setIssues($issues); if (!$actual_table) { $actual_table = $expect_table->newEmptyClone(); } if (!$expect_table) { $expect_table = $actual_table->newEmptyClone(); } $all_columns = $actual_table->getColumns() + $expect_table->getColumns(); foreach ($all_columns as $column_name => $column_template) { $actual_column = $actual_table->getColumn($column_name); $expect_column = $expect_table->getColumn($column_name); $issues = $this->compareSchemata($expect_column, $actual_column); $comp_column = $column_template->newEmptyClone() ->setIssues($issues); $comp_table->addColumn($comp_column); } $all_keys = $actual_table->getKeys() + $expect_table->getKeys(); foreach ($all_keys as $key_name => $key_template) { $actual_key = $actual_table->getKey($key_name); $expect_key = $expect_table->getKey($key_name); $issues = $this->compareSchemata($expect_key, $actual_key); $comp_key = $key_template->newEmptyClone() ->setIssues($issues); $comp_table->addKey($comp_key); } $comp_table->setPersistenceType($expect_table->getPersistenceType()); $comp_database->addTable($comp_table); } $comp_server->addDatabase($comp_database); } return $comp_server; } private function compareSchemata( PhabricatorConfigStorageSchema $expect = null, PhabricatorConfigStorageSchema $actual = null) { $expect_is_key = ($expect instanceof PhabricatorConfigKeySchema); $actual_is_key = ($actual instanceof PhabricatorConfigKeySchema); if ($expect_is_key || $actual_is_key) { $missing_issue = PhabricatorConfigStorageSchema::ISSUE_MISSINGKEY; $surplus_issue = PhabricatorConfigStorageSchema::ISSUE_SURPLUSKEY; } else { $missing_issue = PhabricatorConfigStorageSchema::ISSUE_MISSING; $surplus_issue = PhabricatorConfigStorageSchema::ISSUE_SURPLUS; } if (!$expect && !$actual) { throw new Exception(pht('Can not compare two missing schemata!')); } else if ($expect && !$actual) { $issues = array($missing_issue); } else if ($actual && !$expect) { $issues = array($surplus_issue); } else { $issues = $actual->compareTo($expect); } return $issues; } } diff --git a/src/applications/conpherence/editor/ConpherenceEditEngine.php b/src/applications/conpherence/editor/ConpherenceEditEngine.php index 91cd8fd081..f5f850e637 100644 --- a/src/applications/conpherence/editor/ConpherenceEditEngine.php +++ b/src/applications/conpherence/editor/ConpherenceEditEngine.php @@ -1,118 +1,118 @@ getViewer()); } protected function newObjectQuery() { return new ConpherenceThreadQuery(); } protected function getObjectCreateTitleText($object) { return pht('Create New Room'); } protected function getObjectEditTitleText($object) { return pht('Edit Room: %s', $object->getTitle()); } protected function getObjectEditShortText($object) { return $object->getTitle(); } protected function getObjectCreateShortText() { return pht('Create Room'); } protected function getObjectName() { return pht('Room'); } protected function getObjectCreateCancelURI($object) { return $this->getApplication()->getApplicationURI('/'); } protected function getEditorURI() { return $this->getApplication()->getApplicationURI('edit/'); } protected function getObjectViewURI($object) { return $object->getURI(); } public function isEngineConfigurable() { return false; } protected function buildCustomEditFields($object) { $viewer = $this->getViewer(); if ($this->getIsCreate()) { $participant_phids = array($viewer->getPHID()); $initial_phids = array(); } else { $participant_phids = $object->getParticipantPHIDs(); $initial_phids = $participant_phids; } - // Only show participants on create or conduit, not edit - $conduit_only = !$this->getIsCreate(); + // Only show participants on create or conduit, not edit. + $show_participants = (bool)$this->getIsCreate(); return array( id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) ->setDescription(pht('Room name.')) ->setConduitTypeDescription(pht('New Room name.')) ->setIsRequired(true) ->setTransactionType( ConpherenceThreadTitleTransaction::TRANSACTIONTYPE) ->setValue($object->getTitle()), id(new PhabricatorTextEditField()) ->setKey('topic') ->setLabel(pht('Topic')) ->setDescription(pht('Room topic.')) ->setConduitTypeDescription(pht('New Room topic.')) ->setTransactionType( ConpherenceThreadTopicTransaction::TRANSACTIONTYPE) ->setValue($object->getTopic()), id(new PhabricatorUsersEditField()) ->setKey('participants') ->setValue($participant_phids) ->setInitialValue($initial_phids) - ->setIsConduitOnly($conduit_only) + ->setIsFormField($show_participants) ->setAliases(array('users', 'members', 'participants', 'userPHID')) ->setDescription(pht('Room participants.')) ->setUseEdgeTransactions(true) ->setConduitTypeDescription(pht('New Room participants.')) ->setTransactionType( ConpherenceThreadParticipantsTransaction::TRANSACTIONTYPE) ->setLabel(pht('Initial Participants')), ); } } diff --git a/src/applications/conpherence/query/ConpherenceFulltextQuery.php b/src/applications/conpherence/query/ConpherenceFulltextQuery.php index ba734049f8..99ee4f559d 100644 --- a/src/applications/conpherence/query/ConpherenceFulltextQuery.php +++ b/src/applications/conpherence/query/ConpherenceFulltextQuery.php @@ -1,85 +1,85 @@ threadPHIDs = $phids; return $this; } public function withPreviousTransactionPHIDs(array $phids) { $this->previousTransactionPHIDs = $phids; return $this; } public function withFulltext($fulltext) { $this->fulltext = $fulltext; return $this; } public function execute() { $table = new ConpherenceIndex(); $conn_r = $table->establishConnection('r'); $rows = queryfx_all( $conn_r, 'SELECT threadPHID, transactionPHID, previousTransactionPHID FROM %T i %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderByClause($conn_r), $this->buildLimitClause($conn_r)); return $rows; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->threadPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'i.threadPHID IN (%Ls)', $this->threadPHIDs); } if ($this->previousTransactionPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'i.previousTransactionPHID IN (%Ls)', $this->previousTransactionPHIDs); } if (strlen($this->fulltext)) { $compiler = PhabricatorSearchDocument::newQueryCompiler(); $tokens = $compiler->newTokens($this->fulltext); $compiled_query = $compiler->compileQuery($tokens); $where[] = qsprintf( - $conn_r, + $conn, 'MATCH(i.corpus) AGAINST (%s IN BOOLEAN MODE)', $compiled_query); } - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } private function buildOrderByClause(AphrontDatabaseConnection $conn_r) { if (strlen($this->fulltext)) { return qsprintf( $conn_r, 'ORDER BY MATCH(i.corpus) AGAINST (%s IN BOOLEAN MODE) DESC', $this->fulltext); } else { return qsprintf( $conn_r, 'ORDER BY id DESC'); } } } diff --git a/src/applications/conpherence/query/ConpherenceParticipantCountQuery.php b/src/applications/conpherence/query/ConpherenceParticipantCountQuery.php index 268af0ccf1..c9ac9f76c1 100644 --- a/src/applications/conpherence/query/ConpherenceParticipantCountQuery.php +++ b/src/applications/conpherence/query/ConpherenceParticipantCountQuery.php @@ -1,69 +1,69 @@ participantPHIDs = $phids; return $this; } public function withUnread($unread) { $this->unread = $unread; return $this; } public function execute() { $thread = new ConpherenceThread(); $table = new ConpherenceParticipant(); $conn = $table->establishConnection('r'); $rows = queryfx_all( $conn, 'SELECT COUNT(*) as count, participantPHID FROM %T participant JOIN %T thread ON participant.conpherencePHID = thread.phid %Q %Q %Q', $table->getTableName(), $thread->getTableName(), $this->buildWhereClause($conn), $this->buildGroupByClause($conn), $this->buildLimitClause($conn)); return ipull($rows, 'count', 'participantPHID'); } protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->participantPHIDs !== null) { $where[] = qsprintf( $conn, 'participant.participantPHID IN (%Ls)', $this->participantPHIDs); } if ($this->unread !== null) { if ($this->unread) { $where[] = qsprintf( $conn, 'participant.seenMessageCount < thread.messageCount'); } else { $where[] = qsprintf( $conn, 'participant.seenMessageCount >= thread.messageCount'); } } - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } private function buildGroupByClause(AphrontDatabaseConnection $conn) { return qsprintf( $conn, 'GROUP BY participantPHID'); } } diff --git a/src/applications/conpherence/query/ConpherenceParticipantQuery.php b/src/applications/conpherence/query/ConpherenceParticipantQuery.php index fb4c3eff0f..0316d5e2c2 100644 --- a/src/applications/conpherence/query/ConpherenceParticipantQuery.php +++ b/src/applications/conpherence/query/ConpherenceParticipantQuery.php @@ -1,50 +1,50 @@ participantPHIDs = $phids; return $this; } public function execute() { $table = new ConpherenceParticipant(); $thread = new ConpherenceThread(); $conn = $table->establishConnection('r'); $data = queryfx_all( $conn, 'SELECT * FROM %T participant JOIN %T thread ON participant.conpherencePHID = thread.phid %Q %Q %Q', $table->getTableName(), $thread->getTableName(), $this->buildWhereClause($conn), $this->buildOrderClause($conn), $this->buildLimitClause($conn)); return $table->loadAllFromArray($data); } protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->participantPHIDs !== null) { $where[] = qsprintf( $conn, 'participantPHID IN (%Ls)', $this->participantPHIDs); } - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } private function buildOrderClause(AphrontDatabaseConnection $conn) { return qsprintf( $conn, 'ORDER BY thread.dateModified DESC, thread.id DESC, participant.id DESC'); } } diff --git a/src/applications/daemon/management/PhabricatorLockLogManagementWorkflow.php b/src/applications/daemon/management/PhabricatorLockLogManagementWorkflow.php index 5602bda309..8bf9be9423 100644 --- a/src/applications/daemon/management/PhabricatorLockLogManagementWorkflow.php +++ b/src/applications/daemon/management/PhabricatorLockLogManagementWorkflow.php @@ -1,222 +1,222 @@ setName('log') ->setSynopsis(pht('Enable, disable, or show the lock log.')) ->setArguments( array( array( 'name' => 'enable', 'help' => pht('Enable the lock log.'), ), array( 'name' => 'disable', 'help' => pht('Disable the lock log.'), ), array( 'name' => 'name', 'param' => 'name', 'help' => pht('Review logs for a specific lock.'), ), )); } public function execute(PhutilArgumentParser $args) { $is_enable = $args->getArg('enable'); $is_disable = $args->getArg('disable'); if ($is_enable && $is_disable) { throw new PhutilArgumentUsageException( pht( 'You can not both "--enable" and "--disable" the lock log.')); } $with_name = $args->getArg('name'); if ($is_enable || $is_disable) { if (strlen($with_name)) { throw new PhutilArgumentUsageException( pht( 'You can not both "--enable" or "--disable" with search '. 'parameters like "--name".')); } $gc = new PhabricatorDaemonLockLogGarbageCollector(); $is_enabled = (bool)$gc->getRetentionPolicy(); $config_key = 'phd.garbage-collection'; $const = $gc->getCollectorConstant(); $value = PhabricatorEnv::getEnvConfig($config_key); if ($is_disable) { if (!$is_enabled) { echo tsprintf( "%s\n", pht('Lock log is already disabled.')); return 0; } echo tsprintf( "%s\n", pht('Disabling the lock log.')); unset($value[$const]); } else { if ($is_enabled) { echo tsprintf( "%s\n", pht('Lock log is already enabled.')); return 0; } echo tsprintf( "%s\n", pht('Enabling the lock log.')); $value[$const] = phutil_units('24 hours in seconds'); } id(new PhabricatorConfigLocalSource()) ->setKeys( array( $config_key => $value, )); echo tsprintf( "%s\n", pht('Done.')); echo tsprintf( "%s\n", pht('Restart daemons to apply changes.')); return 0; } $table = new PhabricatorDaemonLockLog(); $conn = $table->establishConnection('r'); $parts = array(); if (strlen($with_name)) { $parts[] = qsprintf( $conn, 'lockName = %s', $with_name); } if (!$parts) { - $constraint = '1 = 1'; + $constraint = qsprintf($conn, '1 = 1'); } else { - $constraint = '('.implode(') AND (', $parts).')'; + $constraint = qsprintf($conn, '%LA', $parts); } $logs = $table->loadAllWhere( '%Q ORDER BY id DESC LIMIT 100', $constraint); $logs = array_reverse($logs); if (!$logs) { echo tsprintf( "%s\n", pht('No matching lock logs.')); return 0; } $table = id(new PhutilConsoleTable()) ->setBorders(true) ->addColumn( 'id', array( 'title' => pht('Lock'), )) ->addColumn( 'name', array( 'title' => pht('Name'), )) ->addColumn( 'acquired', array( 'title' => pht('Acquired'), )) ->addColumn( 'released', array( 'title' => pht('Released'), )) ->addColumn( 'held', array( 'title' => pht('Held'), )) ->addColumn( 'parameters', array( 'title' => pht('Parameters'), )) ->addColumn( 'context', array( 'title' => pht('Context'), )); $viewer = $this->getViewer(); foreach ($logs as $log) { $created = $log->getDateCreated(); $released = $log->getLockReleased(); if ($released) { $held = '+'.($released - $created); } else { $held = null; } $created = phabricator_datetime($created, $viewer); $released = phabricator_datetime($released, $viewer); $parameters = $log->getLockParameters(); $context = $log->getLockContext(); $table->addRow( array( 'id' => $log->getID(), 'name' => $log->getLockName(), 'acquired' => $created, 'released' => $released, 'held' => $held, 'parameters' => $this->flattenParameters($parameters), 'context' => $this->flattenParameters($context), )); } $table->draw(); return 0; } private function flattenParameters(array $params, $keys = true) { $flat = array(); foreach ($params as $key => $value) { if (is_array($value)) { $value = $this->flattenParameters($value, false); } if ($keys) { $flat[] = "{$key}={$value}"; } else { $flat[] = "{$value}"; } } if ($keys) { $flat = implode(', ', $flat); } else { $flat = implode(' ', $flat); } return $flat; } } diff --git a/src/applications/daemon/query/PhabricatorDaemonLogQuery.php b/src/applications/daemon/query/PhabricatorDaemonLogQuery.php index 961c1cfc61..2c5b6baa3b 100644 --- a/src/applications/daemon/query/PhabricatorDaemonLogQuery.php +++ b/src/applications/daemon/query/PhabricatorDaemonLogQuery.php @@ -1,194 +1,195 @@ ids = $ids; return $this; } public function withoutIDs(array $ids) { $this->notIDs = $ids; return $this; } public function withStatus($status) { $this->status = $status; return $this; } public function withDaemonClasses(array $classes) { $this->daemonClasses = $classes; return $this; } public function setAllowStatusWrites($allow) { $this->allowStatusWrites = $allow; return $this; } public function withDaemonIDs(array $daemon_ids) { $this->daemonIDs = $daemon_ids; return $this; } protected function loadPage() { $table = new PhabricatorDaemonLog(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } protected function willFilterPage(array $daemons) { $unknown_delay = self::getTimeUntilUnknown(); $dead_delay = self::getTimeUntilDead(); $status_running = PhabricatorDaemonLog::STATUS_RUNNING; $status_unknown = PhabricatorDaemonLog::STATUS_UNKNOWN; $status_wait = PhabricatorDaemonLog::STATUS_WAIT; $status_exiting = PhabricatorDaemonLog::STATUS_EXITING; $status_exited = PhabricatorDaemonLog::STATUS_EXITED; $status_dead = PhabricatorDaemonLog::STATUS_DEAD; $filter = array_fuse($this->getStatusConstants()); foreach ($daemons as $key => $daemon) { $status = $daemon->getStatus(); $seen = $daemon->getDateModified(); $is_running = ($status == $status_running) || ($status == $status_wait) || ($status == $status_exiting); // If we haven't seen the daemon recently, downgrade its status to // unknown. $unknown_time = ($seen + $unknown_delay); if ($is_running && ($unknown_time < time())) { $status = $status_unknown; } // If the daemon hasn't been seen in quite a while, assume it is dead. $dead_time = ($seen + $dead_delay); if (($status == $status_unknown) && ($dead_time < time())) { $status = $status_dead; } // If we changed the daemon's status, adjust it. if ($status != $daemon->getStatus()) { $daemon->setStatus($status); // ...and write it, if we're in a context where that's reasonable. if ($this->allowStatusWrites) { $guard = AphrontWriteGuard::beginScopedUnguardedWrites(); $daemon->save(); unset($guard); } } // If the daemon no longer matches the filter, get rid of it. if ($filter) { if (empty($filter[$daemon->getStatus()])) { unset($daemons[$key]); } } } return $daemons; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->notIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id NOT IN (%Ld)', $this->notIDs); } if ($this->getStatusConstants()) { $where[] = qsprintf( - $conn_r, + $conn, 'status IN (%Ls)', $this->getStatusConstants()); } if ($this->daemonClasses !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'daemon IN (%Ls)', $this->daemonClasses); } if ($this->daemonIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'daemonID IN (%Ls)', $this->daemonIDs); } - $where[] = $this->buildPagingClause($conn_r); - return $this->formatWhereClause($where); + $where[] = $this->buildPagingClause($conn); + + return $this->formatWhereClause($conn, $where); } private function getStatusConstants() { $status = $this->status; switch ($status) { case self::STATUS_ALL: return array(); case self::STATUS_RUNNING: return array( PhabricatorDaemonLog::STATUS_RUNNING, ); case self::STATUS_ALIVE: return array( PhabricatorDaemonLog::STATUS_UNKNOWN, PhabricatorDaemonLog::STATUS_RUNNING, PhabricatorDaemonLog::STATUS_WAIT, PhabricatorDaemonLog::STATUS_EXITING, ); default: throw new Exception(pht('Unknown status "%s"!', $status)); } } public function getQueryApplicationClass() { return 'PhabricatorDaemonsApplication'; } } diff --git a/src/applications/differential/customfield/DifferentialReviewersField.php b/src/applications/differential/customfield/DifferentialReviewersField.php index f835854e2f..31b175b09d 100644 --- a/src/applications/differential/customfield/DifferentialReviewersField.php +++ b/src/applications/differential/customfield/DifferentialReviewersField.php @@ -1,94 +1,109 @@ getReviewers(); } public function shouldAppearInPropertyView() { return true; } public function renderPropertyViewLabel() { return $this->getFieldName(); } public function getRequiredHandlePHIDsForPropertyView() { return mpull($this->getUserReviewers(), 'getReviewerPHID'); } public function renderPropertyViewValue(array $handles) { $reviewers = $this->getUserReviewers(); if (!$reviewers) { return phutil_tag('em', array(), pht('None')); } $view = id(new DifferentialReviewersView()) ->setUser($this->getViewer()) ->setReviewers($reviewers) ->setHandles($handles); $diff = $this->getActiveDiff(); if ($diff) { $view->setActiveDiff($diff); } return $view; } private function getUserReviewers() { $reviewers = array(); foreach ($this->getObject()->getReviewers() as $reviewer) { if ($reviewer->isUser()) { $reviewers[] = $reviewer; } } return $reviewers; } public function getRequiredHandlePHIDsForRevisionHeaderWarnings() { return mpull($this->getValue(), 'getReviewerPHID'); } public function getWarningsForRevisionHeader(array $handles) { $revision = $this->getObject(); if (!$revision->isNeedsReview()) { return array(); } + $all_resigned = true; + $all_disabled = true; + $any_reviewers = false; + foreach ($this->getValue() as $reviewer) { - if (!$handles[$reviewer->getReviewerPHID()]->isDisabled()) { - return array(); + $reviewer_phid = $reviewer->getReviewerPHID(); + + $any_reviewers = true; + + if (!$handles[$reviewer_phid]->isDisabled()) { + $all_disabled = false; + } + + if (!$reviewer->isResigned()) { + $all_resigned = false; } } $warnings = array(); - if ($this->getValue()) { + if (!$any_reviewers) { + $warnings[] = pht( + 'This revision needs review, but there are no reviewers specified.'); + } else if ($all_disabled) { $warnings[] = pht( 'This revision needs review, but all specified reviewers are '. 'disabled or inactive.'); - } else { + } else if ($all_resigned) { $warnings[] = pht( - 'This revision needs review, but there are no reviewers specified.'); + 'This revision needs review, but all reviewers have resigned.'); } return $warnings; } } diff --git a/src/applications/differential/editor/DifferentialRevisionEditEngine.php b/src/applications/differential/editor/DifferentialRevisionEditEngine.php index 0404bd6201..ffae7fab16 100644 --- a/src/applications/differential/editor/DifferentialRevisionEditEngine.php +++ b/src/applications/differential/editor/DifferentialRevisionEditEngine.php @@ -1,346 +1,346 @@ getViewer(); return DifferentialRevision::initializeNewRevision($viewer); } protected function newObjectQuery() { return id(new DifferentialRevisionQuery()) ->needActiveDiffs(true) ->needReviewers(true) ->needReviewerAuthority(true); } protected function getObjectCreateTitleText($object) { return pht('Create New Revision'); } protected function getObjectEditTitleText($object) { $monogram = $object->getMonogram(); $title = $object->getTitle(); $diff = $this->getDiff(); if ($diff) { return pht('Update Revision %s: %s', $monogram, $title); } else { return pht('Edit Revision %s: %s', $monogram, $title); } } protected function getObjectEditShortText($object) { return $object->getMonogram(); } protected function getObjectCreateShortText() { return pht('Create Revision'); } protected function getObjectName() { return pht('Revision'); } protected function getCommentViewButtonText($object) { if ($object->isDraft()) { return pht('Submit Quietly'); } return parent::getCommentViewButtonText($object); } protected function getObjectViewURI($object) { return $object->getURI(); } protected function getEditorURI() { return $this->getApplication()->getApplicationURI('revision/edit/'); } public function setDiff(DifferentialDiff $diff) { $this->diff = $diff; return $this; } public function getDiff() { return $this->diff; } protected function newCommentActionGroups() { return array( id(new PhabricatorEditEngineCommentActionGroup()) ->setKey(self::ACTIONGROUP_REVIEW) ->setLabel(pht('Review Actions')), id(new PhabricatorEditEngineCommentActionGroup()) ->setKey(self::ACTIONGROUP_REVISION) ->setLabel(pht('Revision Actions')), ); } protected function buildCustomEditFields($object) { $viewer = $this->getViewer(); $plan_required = PhabricatorEnv::getEnvConfig( 'differential.require-test-plan-field'); $plan_enabled = $this->isCustomFieldEnabled( $object, 'differential:test-plan'); $diff = $this->getDiff(); if ($diff) { $diff_phid = $diff->getPHID(); } else { $diff_phid = null; } $is_create = $this->getIsCreate(); $is_update = ($diff && !$is_create); $fields = array(); $fields[] = id(new PhabricatorHandlesEditField()) ->setKey(DifferentialRevisionUpdateTransaction::EDITKEY) ->setLabel(pht('Update Diff')) ->setDescription(pht('New diff to create or update the revision with.')) ->setConduitDescription(pht('Create or update a revision with a diff.')) ->setConduitTypeDescription(pht('PHID of the diff.')) ->setTransactionType( DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE) ->setHandleParameterType(new AphrontPHIDListHTTPParameterType()) ->setSingleValue($diff_phid) - ->setIsConduitOnly(!$diff) + ->setIsFormField((bool)$diff) ->setIsReorderable(false) ->setIsDefaultable(false) ->setIsInvisible(true) ->setIsLockable(false); if ($is_update) { $fields[] = id(new PhabricatorInstructionsEditField()) ->setKey('update.help') ->setValue(pht('Describe the updates you have made to the diff.')); $fields[] = id(new PhabricatorCommentEditField()) ->setKey('update.comment') ->setLabel(pht('Comment')) ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->setIsWebOnly(true) ->setDescription(pht('Comments providing context for the update.')); $fields[] = id(new PhabricatorSubmitEditField()) ->setKey('update.submit') ->setValue($this->getObjectEditButtonText($object)); $fields[] = id(new PhabricatorDividerEditField()) ->setKey('update.note'); } $fields[] = id(new PhabricatorTextEditField()) ->setKey(DifferentialRevisionTitleTransaction::EDITKEY) ->setLabel(pht('Title')) ->setIsRequired(true) ->setTransactionType( DifferentialRevisionTitleTransaction::TRANSACTIONTYPE) ->setDescription(pht('The title of the revision.')) ->setConduitDescription(pht('Retitle the revision.')) ->setConduitTypeDescription(pht('New revision title.')) ->setValue($object->getTitle()); $fields[] = id(new PhabricatorRemarkupEditField()) ->setKey(DifferentialRevisionSummaryTransaction::EDITKEY) ->setLabel(pht('Summary')) ->setTransactionType( DifferentialRevisionSummaryTransaction::TRANSACTIONTYPE) ->setDescription(pht('The summary of the revision.')) ->setConduitDescription(pht('Change the revision summary.')) ->setConduitTypeDescription(pht('New revision summary.')) ->setValue($object->getSummary()); if ($plan_enabled) { $fields[] = id(new PhabricatorRemarkupEditField()) ->setKey(DifferentialRevisionTestPlanTransaction::EDITKEY) ->setLabel(pht('Test Plan')) ->setIsRequired($plan_required) ->setTransactionType( DifferentialRevisionTestPlanTransaction::TRANSACTIONTYPE) ->setDescription( pht('Actions performed to verify the behavior of the change.')) ->setConduitDescription(pht('Update the revision test plan.')) ->setConduitTypeDescription(pht('New test plan.')) ->setValue($object->getTestPlan()); } $fields[] = id(new PhabricatorDatasourceEditField()) ->setKey(DifferentialRevisionReviewersTransaction::EDITKEY) ->setLabel(pht('Reviewers')) ->setDatasource(new DifferentialReviewerDatasource()) ->setUseEdgeTransactions(true) ->setTransactionType( DifferentialRevisionReviewersTransaction::TRANSACTIONTYPE) ->setCommentActionLabel(pht('Change Reviewers')) ->setDescription(pht('Reviewers for this revision.')) ->setConduitDescription(pht('Change the reviewers for this revision.')) ->setConduitTypeDescription(pht('New reviewers.')) ->setValue($object->getReviewerPHIDsForEdit()); $fields[] = id(new PhabricatorDatasourceEditField()) ->setKey('repositoryPHID') ->setLabel(pht('Repository')) ->setDatasource(new DiffusionRepositoryDatasource()) ->setTransactionType( DifferentialRevisionRepositoryTransaction::TRANSACTIONTYPE) ->setDescription(pht('The repository the revision belongs to.')) ->setConduitDescription(pht('Change the repository for this revision.')) ->setConduitTypeDescription(pht('New repository.')) ->setSingleValue($object->getRepositoryPHID()); // This is a little flimsy, but allows "Maniphest Tasks: ..." to continue // working properly in commit messages until we fully sort out T5873. $fields[] = id(new PhabricatorHandlesEditField()) ->setKey('tasks') ->setUseEdgeTransactions(true) - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue( 'edge:type', DifferentialRevisionHasTaskEdgeType::EDGECONST) ->setDescription(pht('Tasks associated with this revision.')) ->setConduitDescription(pht('Change associated tasks.')) ->setConduitTypeDescription(pht('List of tasks.')) ->setValue(array()); $actions = DifferentialRevisionActionTransaction::loadAllActions(); $actions = msortv($actions, 'getRevisionActionOrderVector'); foreach ($actions as $key => $action) { $fields[] = $action->newEditField($object, $viewer); } $fields[] = id(new PhabricatorBoolEditField()) ->setKey('draft') ->setLabel(pht('Hold as Draft')) - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setOptions( pht('Autosubmit Once Builds Finish'), pht('Hold as Draft')) ->setTransactionType( DifferentialRevisionHoldDraftTransaction::TRANSACTIONTYPE) ->setDescription(pht('Hold revision as as draft.')) ->setConduitDescription( pht( 'Change autosubmission from draft state after builds finish.')) ->setConduitTypeDescription(pht('New "Hold as Draft" setting.')) ->setValue($object->getHoldAsDraft()); return $fields; } private function isCustomFieldEnabled(DifferentialRevision $revision, $key) { $field_list = PhabricatorCustomField::getObjectFields( $revision, PhabricatorCustomField::ROLE_VIEW); $fields = $field_list->getFields(); return isset($fields[$key]); } protected function newAutomaticCommentTransactions($object) { $viewer = $this->getViewer(); $xactions = array(); $inlines = DifferentialTransactionQuery::loadUnsubmittedInlineComments( $viewer, $object); $inlines = msort($inlines, 'getID'); foreach ($inlines as $inline) { $xactions[] = id(new DifferentialTransaction()) ->setTransactionType(DifferentialTransaction::TYPE_INLINE) ->attachComment($inline); } $viewer_phid = $viewer->getPHID(); $viewer_is_author = ($object->getAuthorPHID() == $viewer_phid); if ($viewer_is_author) { $state_map = PhabricatorTransactions::getInlineStateMap(); $inlines = id(new DifferentialDiffInlineCommentQuery()) ->setViewer($viewer) ->withRevisionPHIDs(array($object->getPHID())) ->withFixedStates(array_keys($state_map)) ->execute(); if ($inlines) { $old_value = mpull($inlines, 'getFixedState', 'getPHID'); $new_value = array(); foreach ($old_value as $key => $state) { $new_value[$key] = $state_map[$state]; } $xactions[] = id(new DifferentialTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE) ->setIgnoreOnNoEffect(true) ->setOldValue($old_value) ->setNewValue($new_value); } } return $xactions; } protected function newCommentPreviewContent($object, array $xactions) { $viewer = $this->getViewer(); $type_inline = DifferentialTransaction::TYPE_INLINE; $inlines = array(); foreach ($xactions as $xaction) { if ($xaction->getTransactionType() === $type_inline) { $inlines[] = $xaction->getComment(); } } $content = array(); if ($inlines) { $inline_preview = id(new PHUIDiffInlineCommentPreviewListView()) ->setViewer($viewer) ->setInlineComments($inlines); $content[] = phutil_tag( 'div', array( 'id' => 'inline-comment-preview', ), $inline_preview); } return $content; } } diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index 5bc33dabe1..7fa4cd61e2 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -1,1762 +1,1767 @@ firstBroadcast; } public function getDiffUpdateTransaction(array $xactions) { $type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE; foreach ($xactions as $xaction) { if ($xaction->getTransactionType() == $type_update) { return $xaction; } } return null; } public function setIsCloseByCommit($is_close_by_commit) { $this->isCloseByCommit = $is_close_by_commit; return $this; } public function getIsCloseByCommit() { return $this->isCloseByCommit; } public function setChangedPriorToCommitURI($uri) { $this->changedPriorToCommitURI = $uri; return $this; } public function getChangedPriorToCommitURI() { return $this->changedPriorToCommitURI; } public function setRepositoryPHIDOverride($phid_or_null) { $this->repositoryPHIDOverride = $phid_or_null; return $this; } public function getTransactionTypes() { $types = parent::getTransactionTypes(); $types[] = PhabricatorTransactions::TYPE_COMMENT; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; $types[] = PhabricatorTransactions::TYPE_INLINESTATE; $types[] = DifferentialTransaction::TYPE_INLINE; return $types; } protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case DifferentialTransaction::TYPE_INLINE: return null; } return parent::getCustomTransactionOldValue($object, $xaction); } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case DifferentialTransaction::TYPE_INLINE: return null; } return parent::getCustomTransactionNewValue($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case DifferentialTransaction::TYPE_INLINE: return; } return parent::applyCustomInternalTransaction($object, $xaction); } protected function expandTransactions( PhabricatorLiskDAO $object, array $xactions) { foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_INLINESTATE: // If we have an "Inline State" transaction already, the caller // built it for us so we don't need to expand it again. $this->didExpandInlineState = true; break; case DifferentialRevisionPlanChangesTransaction::TRANSACTIONTYPE: if ($xaction->getMetadataValue('draft.demote')) { $this->isDraftDemotion = true; } break; } } $this->wasBroadcasting = $object->getShouldBroadcast(); return parent::expandTransactions($object, $xactions); } protected function expandTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $results = parent::expandTransaction($object, $xaction); $actor = $this->getActor(); $actor_phid = $this->getActingAsPHID(); $type_edge = PhabricatorTransactions::TYPE_EDGE; $edge_ref_task = DifferentialRevisionHasTaskEdgeType::EDGECONST; $want_downgrade = array(); $must_downgrade = array(); if ($this->getIsCloseByCommit()) { // Never downgrade reviewers when we're closing a revision after a // commit. } else { switch ($xaction->getTransactionType()) { case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE: $want_downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED; $want_downgrade[] = DifferentialReviewerStatus::STATUS_REJECTED; break; case DifferentialRevisionRequestReviewTransaction::TRANSACTIONTYPE: if (!$object->isChangePlanned()) { // If the old state isn't "Changes Planned", downgrade the accepts // even if they're sticky. // We don't downgrade for "Changes Planned" to allow an author to // undo a "Plan Changes" by immediately following it up with a // "Request Review". $want_downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED; $must_downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED; } $want_downgrade[] = DifferentialReviewerStatus::STATUS_REJECTED; break; } } if ($want_downgrade) { $void_type = DifferentialRevisionVoidTransaction::TRANSACTIONTYPE; $results[] = id(new DifferentialTransaction()) ->setTransactionType($void_type) ->setIgnoreOnNoEffect(true) ->setMetadataValue('void.force', $must_downgrade) ->setNewValue($want_downgrade); } $is_commandeer = false; switch ($xaction->getTransactionType()) { case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE: if ($this->getIsCloseByCommit()) { // Don't bother with any of this if this update is a side effect of // commit detection. break; } // When a revision is updated and the diff comes from a branch named // "T123" or similar, automatically associate the commit with the // task that the branch names. $maniphest = 'PhabricatorManiphestApplication'; if (PhabricatorApplication::isClassInstalled($maniphest)) { $diff = $this->requireDiff($xaction->getNewValue()); $branch = $diff->getBranch(); // No "$", to allow for branches like T123_demo. $match = null; if (preg_match('/^T(\d+)/i', $branch, $match)) { $task_id = $match[1]; $tasks = id(new ManiphestTaskQuery()) ->setViewer($this->getActor()) ->withIDs(array($task_id)) ->execute(); if ($tasks) { $task = head($tasks); $task_phid = $task->getPHID(); $results[] = id(new DifferentialTransaction()) ->setTransactionType($type_edge) ->setMetadataValue('edge:type', $edge_ref_task) ->setIgnoreOnNoEffect(true) ->setNewValue(array('+' => array($task_phid => $task_phid))); } } } break; case DifferentialRevisionCommandeerTransaction::TRANSACTIONTYPE: $is_commandeer = true; break; } if ($is_commandeer) { $results[] = $this->newCommandeerReviewerTransaction($object); } if (!$this->didExpandInlineState) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE: case DifferentialTransaction::TYPE_INLINE: $this->didExpandInlineState = true; $actor_phid = $this->getActingAsPHID(); $author_phid = $object->getAuthorPHID(); $actor_is_author = ($actor_phid == $author_phid); $state_map = PhabricatorTransactions::getInlineStateMap(); $query = id(new DifferentialDiffInlineCommentQuery()) ->setViewer($this->getActor()) ->withRevisionPHIDs(array($object->getPHID())) ->withFixedStates(array_keys($state_map)); $inlines = array(); // We're going to undraft any "done" marks on your own inlines. $inlines[] = id(clone $query) ->withAuthorPHIDs(array($actor_phid)) ->withHasTransaction(false) ->execute(); // If you're the author, we also undraft any "done" marks on other // inlines. if ($actor_is_author) { $inlines[] = id(clone $query) ->withHasTransaction(true) ->execute(); } $inlines = array_mergev($inlines); if (!$inlines) { break; } $old_value = mpull($inlines, 'getFixedState', 'getPHID'); $new_value = array(); foreach ($old_value as $key => $state) { $new_value[$key] = $state_map[$state]; } $results[] = id(new DifferentialTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE) ->setIgnoreOnNoEffect(true) ->setOldValue($old_value) ->setNewValue($new_value); break; } } return $results; } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case DifferentialTransaction::TYPE_INLINE: $reply = $xaction->getComment()->getReplyToComment(); if ($reply && !$reply->getHasReplies()) { $reply->setHasReplies(1)->save(); } return; } return parent::applyCustomExternalTransaction($object, $xaction); } protected function applyBuiltinExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_INLINESTATE: $table = new DifferentialTransactionComment(); $conn_w = $table->establishConnection('w'); foreach ($xaction->getNewValue() as $phid => $state) { queryfx( $conn_w, 'UPDATE %T SET fixedState = %s WHERE phid = %s', $table->getTableName(), $state, $phid); } break; } return parent::applyBuiltinExternalTransaction($object, $xaction); } protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { // Load the most up-to-date version of the revision and its reviewers, // so we don't need to try to deduce the state of reviewers by examining // all the changes made by the transactions. Then, update the reviewers // on the object to make sure we're acting on the current reviewer set // (and, for example, sending mail to the right people). $new_revision = id(new DifferentialRevisionQuery()) ->setViewer($this->getActor()) ->needReviewers(true) ->needActiveDiffs(true) ->withIDs(array($object->getID())) ->executeOne(); if (!$new_revision) { throw new Exception( pht('Failed to load revision from transaction finalization.')); } $object->attachReviewers($new_revision->getReviewers()); $object->attachActiveDiff($new_revision->getActiveDiff()); $object->attachRepository($new_revision->getRepository()); foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE: $diff = $this->requireDiff($xaction->getNewValue(), true); // Update these denormalized index tables when we attach a new // diff to a revision. $this->updateRevisionHashTable($object, $diff); $this->updateAffectedPathTable($object, $diff); break; } } $xactions = $this->updateReviewStatus($object, $xactions); $this->markReviewerComments($object, $xactions); return $xactions; } private function updateReviewStatus( DifferentialRevision $revision, array $xactions) { $was_accepted = $revision->isAccepted(); $was_revision = $revision->isNeedsRevision(); $was_review = $revision->isNeedsReview(); if (!$was_accepted && !$was_revision && !$was_review) { // Revisions can't transition out of other statuses (like closed or // abandoned) as a side effect of reviewer status changes. return $xactions; } // Try to move a revision to "accepted". We look for: // // - at least one accepting reviewer who is a user; and // - no rejects; and // - no rejects of older diffs; and // - no blocking reviewers. $has_accepting_user = false; $has_rejecting_reviewer = false; $has_rejecting_older_reviewer = false; $has_blocking_reviewer = false; $active_diff = $revision->getActiveDiff(); foreach ($revision->getReviewers() as $reviewer) { $reviewer_status = $reviewer->getReviewerStatus(); switch ($reviewer_status) { case DifferentialReviewerStatus::STATUS_REJECTED: $active_phid = $active_diff->getPHID(); if ($reviewer->isRejected($active_phid)) { $has_rejecting_reviewer = true; } else { $has_rejecting_older_reviewer = true; } break; case DifferentialReviewerStatus::STATUS_REJECTED_OLDER: $has_rejecting_older_reviewer = true; break; case DifferentialReviewerStatus::STATUS_BLOCKING: $has_blocking_reviewer = true; break; case DifferentialReviewerStatus::STATUS_ACCEPTED: if ($reviewer->isUser()) { $active_phid = $active_diff->getPHID(); if ($reviewer->isAccepted($active_phid)) { $has_accepting_user = true; } } break; } } $new_status = null; if ($has_accepting_user && !$has_rejecting_reviewer && !$has_rejecting_older_reviewer && !$has_blocking_reviewer) { $new_status = DifferentialRevisionStatus::ACCEPTED; } else if ($has_rejecting_reviewer) { // This isn't accepted, and there's at least one rejecting reviewer, // so the revision needs changes. This usually happens after a // "reject". $new_status = DifferentialRevisionStatus::NEEDS_REVISION; } else if ($was_accepted) { // This revision was accepted, but it no longer satisfies the // conditions for acceptance. This usually happens after an accepting // reviewer resigns or is removed. $new_status = DifferentialRevisionStatus::NEEDS_REVIEW; + } else if ($was_revision) { + // This revision was "Needs Revision", but no longer has any rejecting + // reviewers. This usually happens after the last rejecting reviewer + // resigns or is removed. Put the revision back in "Needs Review". + $new_status = DifferentialRevisionStatus::NEEDS_REVIEW; } if ($new_status === null) { return $xactions; } $old_status = $revision->getModernRevisionStatus(); if ($new_status == $old_status) { return $xactions; } $xaction = id(new DifferentialTransaction()) ->setTransactionType( DifferentialRevisionStatusTransaction::TRANSACTIONTYPE) ->setOldValue($old_status) ->setNewValue($new_status); $xaction = $this->populateTransaction($revision, $xaction) ->save(); $xactions[] = $xaction; // Save the status adjustment we made earlier. $revision ->setModernRevisionStatus($new_status) ->save(); return $xactions; } protected function sortTransactions(array $xactions) { $xactions = parent::sortTransactions($xactions); $head = array(); $tail = array(); foreach ($xactions as $xaction) { $type = $xaction->getTransactionType(); if ($type == DifferentialTransaction::TYPE_INLINE) { $tail[] = $xaction; } else { $head[] = $xaction; } } return array_values(array_merge($head, $tail)); } protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { if (!$object->getShouldBroadcast()) { return false; } return true; } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function getMailTo(PhabricatorLiskDAO $object) { if ($object->getShouldBroadcast()) { $this->requireReviewers($object); $phids = array(); $phids[] = $object->getAuthorPHID(); foreach ($object->getReviewers() as $reviewer) { if ($reviewer->isResigned()) { continue; } $phids[] = $reviewer->getReviewerPHID(); } return $phids; } // If we're demoting a draft after a build failure, just notify the author. if ($this->isDraftDemotion) { $author_phid = $object->getAuthorPHID(); return array( $author_phid, ); } return array(); } protected function getMailCC(PhabricatorLiskDAO $object) { if (!$object->getShouldBroadcast()) { return array(); } return parent::getMailCC($object); } protected function newMailUnexpandablePHIDs(PhabricatorLiskDAO $object) { $this->requireReviewers($object); $phids = array(); foreach ($object->getReviewers() as $reviewer) { if ($reviewer->isResigned()) { $phids[] = $reviewer->getReviewerPHID(); } } return $phids; } protected function getMailAction( PhabricatorLiskDAO $object, array $xactions) { $show_lines = false; if ($this->isFirstBroadcast()) { $action = pht('Request'); $show_lines = true; } else { $action = parent::getMailAction($object, $xactions); $strongest = $this->getStrongestAction($object, $xactions); $type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE; if ($strongest->getTransactionType() == $type_update) { $show_lines = true; } } if ($show_lines) { $count = new PhutilNumber($object->getLineCount()); $action = pht('%s] [%s', $action, $object->getRevisionScaleGlyphs()); } return $action; } protected function getMailSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.differential.subject-prefix'); } protected function getMailThreadID(PhabricatorLiskDAO $object) { // This is nonstandard, but retains threading with older messages. $phid = $object->getPHID(); return "differential-rev-{$phid}-req"; } protected function buildReplyHandler(PhabricatorLiskDAO $object) { return id(new DifferentialReplyHandler()) ->setMailReceiver($object); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $monogram = $object->getMonogram(); $title = $object->getTitle(); return id(new PhabricatorMetaMTAMail()) ->setSubject(pht('%s: %s', $monogram, $title)) ->setMustEncryptSubject(pht('%s: Revision Updated', $monogram)) ->setMustEncryptURI($object->getURI()); } protected function getTransactionsForMail( PhabricatorLiskDAO $object, array $xactions) { // If this is the first time we're sending mail about this revision, we // generate mail for all prior transactions, not just whatever is being // applied now. This gets the "added reviewers" lines and other relevant // information into the mail. if ($this->isFirstBroadcast()) { return $this->loadUnbroadcastTransactions($object); } return $xactions; } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $viewer = $this->requireActor(); $body = id(new PhabricatorMetaMTAMailBody()) ->setViewer($viewer); $revision_uri = $object->getURI(); $revision_uri = PhabricatorEnv::getProductionURI($revision_uri); $new_uri = $revision_uri.'/new/'; $this->addHeadersAndCommentsToMailBody( $body, $xactions, pht('View Revision'), $revision_uri); $type_inline = DifferentialTransaction::TYPE_INLINE; $inlines = array(); foreach ($xactions as $xaction) { if ($xaction->getTransactionType() == $type_inline) { $inlines[] = $xaction; } } if ($inlines) { $this->appendInlineCommentsForMail($object, $inlines, $body); } $update_xaction = null; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE: $update_xaction = $xaction; break; } } if ($update_xaction) { $diff = $this->requireDiff($update_xaction->getNewValue(), true); } else { $diff = null; } $changed_uri = $this->getChangedPriorToCommitURI(); if ($changed_uri) { $body->addLinkSection( pht('CHANGED PRIOR TO COMMIT'), $changed_uri); } $this->addCustomFieldsToMailBody($body, $object, $xactions); if (!$this->isFirstBroadcast()) { $body->addLinkSection(pht('CHANGES SINCE LAST ACTION'), $new_uri); } $body->addLinkSection( pht('REVISION DETAIL'), $revision_uri); if ($update_xaction) { $body->addTextSection( pht('AFFECTED FILES'), $this->renderAffectedFilesForMail($diff)); $config_key_inline = 'metamta.differential.inline-patches'; $config_inline = PhabricatorEnv::getEnvConfig($config_key_inline); $config_key_attach = 'metamta.differential.attach-patches'; $config_attach = PhabricatorEnv::getEnvConfig($config_key_attach); if ($config_inline || $config_attach) { $body_limit = PhabricatorEnv::getEnvConfig('metamta.email-body-limit'); try { $patch = $this->buildPatchForMail($diff, $body_limit); } catch (ArcanistDiffByteSizeException $ex) { $patch = null; } if (($patch !== null) && $config_inline) { $lines = substr_count($patch, "\n"); $bytes = strlen($patch); // Limit the patch size to the smaller of 256 bytes per line or // the mail body limit. This prevents degenerate behavior for patches // with one line that is 10MB long. See T11748. $byte_limits = array(); $byte_limits[] = (256 * $config_inline); $byte_limits[] = $body_limit; $byte_limit = min($byte_limits); $lines_ok = ($lines <= $config_inline); $bytes_ok = ($bytes <= $byte_limit); if ($lines_ok && $bytes_ok) { $this->appendChangeDetailsForMail($object, $diff, $patch, $body); } else { // TODO: Provide a helpful message about the patch being too // large or lengthy here. } } if (($patch !== null) && $config_attach) { // See T12033, T11767, and PHI55. This is a crude fix to stop the // major concrete problems that lackluster email size limits cause. if (strlen($patch) < $body_limit) { $name = pht('D%s.%s.patch', $object->getID(), $diff->getID()); $mime_type = 'text/x-patch; charset=utf-8'; $body->addAttachment( new PhabricatorMetaMTAAttachment($patch, $name, $mime_type)); } } } } return $body; } public function getMailTagsMap() { return array( DifferentialTransaction::MAILTAG_REVIEW_REQUEST => pht('A revision is created.'), DifferentialTransaction::MAILTAG_UPDATED => pht('A revision is updated.'), DifferentialTransaction::MAILTAG_COMMENT => pht('Someone comments on a revision.'), DifferentialTransaction::MAILTAG_CLOSED => pht('A revision is closed.'), DifferentialTransaction::MAILTAG_REVIEWERS => pht("A revision's reviewers change."), DifferentialTransaction::MAILTAG_CC => pht("A revision's CCs change."), DifferentialTransaction::MAILTAG_OTHER => pht('Other revision activity not listed above occurs.'), ); } protected function supportsSearch() { return true; } protected function expandCustomRemarkupBlockTransactions( PhabricatorLiskDAO $object, array $xactions, array $changes, PhutilMarkupEngine $engine) { // For "Fixes ..." and "Depends on ...", we're only going to look at // content blocks which are part of the revision itself (like "Summary" // and "Test Plan"), not comments. $content_parts = array(); foreach ($changes as $change) { if ($change->getTransaction()->isCommentTransaction()) { continue; } $content_parts[] = $change->getNewValue(); } if (!$content_parts) { return array(); } $content_block = implode("\n\n", $content_parts); $task_map = array(); $task_refs = id(new ManiphestCustomFieldStatusParser()) ->parseCorpus($content_block); foreach ($task_refs as $match) { foreach ($match['monograms'] as $monogram) { $task_id = (int)trim($monogram, 'tT'); $task_map[$task_id] = true; } } $rev_map = array(); $rev_refs = id(new DifferentialCustomFieldDependsOnParser()) ->parseCorpus($content_block); foreach ($rev_refs as $match) { foreach ($match['monograms'] as $monogram) { $rev_id = (int)trim($monogram, 'dD'); $rev_map[$rev_id] = true; } } $edges = array(); $task_phids = array(); $rev_phids = array(); if ($task_map) { $tasks = id(new ManiphestTaskQuery()) ->setViewer($this->getActor()) ->withIDs(array_keys($task_map)) ->execute(); if ($tasks) { $task_phids = mpull($tasks, 'getPHID', 'getPHID'); $edge_related = DifferentialRevisionHasTaskEdgeType::EDGECONST; $edges[$edge_related] = $task_phids; } } if ($rev_map) { $revs = id(new DifferentialRevisionQuery()) ->setViewer($this->getActor()) ->withIDs(array_keys($rev_map)) ->execute(); $rev_phids = mpull($revs, 'getPHID', 'getPHID'); // NOTE: Skip any write attempts if a user cleverly implies a revision // depends upon itself. unset($rev_phids[$object->getPHID()]); if ($revs) { $depends = DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST; $edges[$depends] = $rev_phids; } } $revert_refs = id(new DifferentialCustomFieldRevertsParser()) ->parseCorpus($content_block); $revert_monograms = array(); foreach ($revert_refs as $match) { foreach ($match['monograms'] as $monogram) { $revert_monograms[] = $monogram; } } if ($revert_monograms) { $revert_objects = id(new PhabricatorObjectQuery()) ->setViewer($this->getActor()) ->withNames($revert_monograms) ->withTypes( array( DifferentialRevisionPHIDType::TYPECONST, PhabricatorRepositoryCommitPHIDType::TYPECONST, )) ->execute(); $revert_phids = mpull($revert_objects, 'getPHID', 'getPHID'); // Don't let an object revert itself, although other silly stuff like // cycles of objects reverting each other is not prevented. unset($revert_phids[$object->getPHID()]); $revert_type = DiffusionCommitRevertsCommitEdgeType::EDGECONST; $edges[$revert_type] = $revert_phids; } else { $revert_phids = array(); } // See PHI574. Respect any unmentionable PHIDs which were set on the // Editor by the caller. $unmentionable_map = $this->getUnmentionablePHIDMap(); $unmentionable_map += $task_phids; $unmentionable_map += $rev_phids; $unmentionable_map += $revert_phids; $this->setUnmentionablePHIDMap($unmentionable_map); $result = array(); foreach ($edges as $type => $specs) { $result[] = id(new DifferentialTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $type) ->setNewValue(array('+' => $specs)); } return $result; } private function appendInlineCommentsForMail( PhabricatorLiskDAO $object, array $inlines, PhabricatorMetaMTAMailBody $body) { $limit = 100; $limit_note = null; if (count($inlines) > $limit) { $limit_note = pht( '(Showing first %s of %s inline comments.)', new PhutilNumber($limit), phutil_count($inlines)); $inlines = array_slice($inlines, 0, $limit, true); } $section = id(new DifferentialInlineCommentMailView()) ->setViewer($this->getActor()) ->setInlines($inlines) ->buildMailSection(); $header = pht('INLINE COMMENTS'); $section_text = "\n".$section->getPlaintext(); if ($limit_note) { $section_text = $limit_note."\n".$section_text; } $style = array( 'margin: 6px 0 12px 0;', ); $section_html = phutil_tag( 'div', array( 'style' => implode(' ', $style), ), $section->getHTML()); if ($limit_note) { $section_html = array( phutil_tag( 'em', array(), $limit_note), $section_html, ); } $body->addPlaintextSection($header, $section_text, false); $body->addHTMLSection($header, $section_html); } private function appendChangeDetailsForMail( PhabricatorLiskDAO $object, DifferentialDiff $diff, $patch, PhabricatorMetaMTAMailBody $body) { $section = id(new DifferentialChangeDetailMailView()) ->setViewer($this->getActor()) ->setDiff($diff) ->setPatch($patch) ->buildMailSection(); $header = pht('CHANGE DETAILS'); $section_text = "\n".$section->getPlaintext(); $style = array( 'margin: 6px 0 12px 0;', ); $section_html = phutil_tag( 'div', array( 'style' => implode(' ', $style), ), $section->getHTML()); $body->addPlaintextSection($header, $section_text, false); $body->addHTMLSection($header, $section_html); } private function loadDiff($phid, $need_changesets = false) { $query = id(new DifferentialDiffQuery()) ->withPHIDs(array($phid)) ->setViewer($this->getActor()); if ($need_changesets) { $query->needChangesets(true); } return $query->executeOne(); } public function requireDiff($phid, $need_changesets = false) { $diff = $this->loadDiff($phid, $need_changesets); if (!$diff) { throw new Exception(pht('Diff "%s" does not exist!', $phid)); } return $diff; } /* -( Herald Integration )------------------------------------------------- */ protected function shouldApplyHeraldRules( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function didApplyHeraldRules( PhabricatorLiskDAO $object, HeraldAdapter $adapter, HeraldTranscript $transcript) { $repository = $object->getRepository(); if (!$repository) { return array(); } $diff = $this->ownersDiff; $changesets = $this->ownersChangesets; $this->ownersDiff = null; $this->ownersChangesets = null; if (!$changesets) { return array(); } $packages = PhabricatorOwnersPackage::loadAffectedPackagesForChangesets( $repository, $diff, $changesets); if (!$packages) { return array(); } // Identify the packages with "Non-Owner Author" review rules and remove // them if the author has authority over the package. $autoreview_map = PhabricatorOwnersPackage::getAutoreviewOptionsMap(); $need_authority = array(); foreach ($packages as $package) { $autoreview_setting = $package->getAutoReview(); $spec = idx($autoreview_map, $autoreview_setting); if (!$spec) { continue; } if (idx($spec, 'authority')) { $need_authority[$package->getPHID()] = $package->getPHID(); } } if ($need_authority) { $authority = id(new PhabricatorOwnersPackageQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs($need_authority) ->withAuthorityPHIDs(array($object->getAuthorPHID())) ->execute(); $authority = mpull($authority, null, 'getPHID'); foreach ($packages as $key => $package) { $package_phid = $package->getPHID(); if (isset($authority[$package_phid])) { unset($packages[$key]); continue; } } if (!$packages) { return array(); } } $auto_subscribe = array(); $auto_review = array(); $auto_block = array(); foreach ($packages as $package) { switch ($package->getAutoReview()) { case PhabricatorOwnersPackage::AUTOREVIEW_REVIEW: case PhabricatorOwnersPackage::AUTOREVIEW_REVIEW_ALWAYS: $auto_review[] = $package; break; case PhabricatorOwnersPackage::AUTOREVIEW_BLOCK: case PhabricatorOwnersPackage::AUTOREVIEW_BLOCK_ALWAYS: $auto_block[] = $package; break; case PhabricatorOwnersPackage::AUTOREVIEW_SUBSCRIBE: case PhabricatorOwnersPackage::AUTOREVIEW_SUBSCRIBE_ALWAYS: $auto_subscribe[] = $package; break; case PhabricatorOwnersPackage::AUTOREVIEW_NONE: default: break; } } $owners_phid = id(new PhabricatorOwnersApplication()) ->getPHID(); $xactions = array(); if ($auto_subscribe) { $xactions[] = $object->getApplicationTransactionTemplate() ->setAuthorPHID($owners_phid) ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) ->setNewValue( array( '+' => mpull($auto_subscribe, 'getPHID'), )); } $specs = array( array($auto_review, false), array($auto_block, true), ); foreach ($specs as $spec) { list($reviewers, $blocking) = $spec; if (!$reviewers) { continue; } $phids = mpull($reviewers, 'getPHID'); $xaction = $this->newAutoReviewTransaction($object, $phids, $blocking); if ($xaction) { $xactions[] = $xaction; } } return $xactions; } private function newAutoReviewTransaction( PhabricatorLiskDAO $object, array $phids, $is_blocking) { // TODO: This is substantially similar to DifferentialReviewersHeraldAction // and both are needlessly complex. This logic should live in the normal // transaction application pipeline. See T10967. $reviewers = $object->getReviewers(); $reviewers = mpull($reviewers, null, 'getReviewerPHID'); if ($is_blocking) { $new_status = DifferentialReviewerStatus::STATUS_BLOCKING; } else { $new_status = DifferentialReviewerStatus::STATUS_ADDED; } $new_strength = DifferentialReviewerStatus::getStatusStrength( $new_status); $current = array(); foreach ($phids as $phid) { if (!isset($reviewers[$phid])) { continue; } // 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]->getReviewerStatus()); if ($old_strength <= $new_strength) { continue; } $current[] = $phid; } $phids = array_diff($phids, $current); if (!$phids) { return null; } $phids = array_fuse($phids); $value = array(); foreach ($phids as $phid) { if ($is_blocking) { $value[] = 'blocking('.$phid.')'; } else { $value[] = $phid; } } $owners_phid = id(new PhabricatorOwnersApplication()) ->getPHID(); $reviewers_type = DifferentialRevisionReviewersTransaction::TRANSACTIONTYPE; return $object->getApplicationTransactionTemplate() ->setAuthorPHID($owners_phid) ->setTransactionType($reviewers_type) ->setNewValue( array( '+' => $value, )); } protected function buildHeraldAdapter( PhabricatorLiskDAO $object, array $xactions) { $revision = id(new DifferentialRevisionQuery()) ->setViewer($this->getActor()) ->withPHIDs(array($object->getPHID())) ->needActiveDiffs(true) ->needReviewers(true) ->executeOne(); if (!$revision) { throw new Exception( pht('Failed to load revision for Herald adapter construction!')); } $adapter = HeraldDifferentialRevisionAdapter::newLegacyAdapter( $revision, $revision->getActiveDiff()); // If the object is still a draft, prevent "Send me an email" and other // similar rules from acting yet. if (!$object->getShouldBroadcast()) { $adapter->setForbiddenAction( HeraldMailableState::STATECONST, DifferentialHeraldStateReasons::REASON_DRAFT); } // If this edit didn't actually change the diff (for example, a user // edited the title or changed subscribers), prevent "Run build plan" // and other similar rules from acting yet, since the build results will // not (or, at least, should not) change unless the actual source changes. // We also don't run Differential builds if the update was caused by // discovering a commit, as the expectation is that Diffusion builds take // over once things land. $has_update = false; $has_commit = false; $type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE; foreach ($xactions as $xaction) { if ($xaction->getTransactionType() != $type_update) { continue; } if ($xaction->getMetadataValue('isCommitUpdate')) { $has_commit = true; } else { $has_update = true; } break; } if ($has_commit) { $adapter->setForbiddenAction( HeraldBuildableState::STATECONST, DifferentialHeraldStateReasons::REASON_LANDED); } else if (!$has_update) { $adapter->setForbiddenAction( HeraldBuildableState::STATECONST, DifferentialHeraldStateReasons::REASON_UNCHANGED); } return $adapter; } /** * Update the table which links Differential revisions to paths they affect, * so Diffusion can efficiently find pending revisions for a given file. */ private function updateAffectedPathTable( DifferentialRevision $revision, DifferentialDiff $diff) { $repository = $revision->getRepository(); if (!$repository) { // The repository where the code lives is untracked. return; } $path_prefix = null; $local_root = $diff->getSourceControlPath(); if ($local_root) { // We're in a working copy which supports subdirectory checkouts (e.g., // SVN) so we need to figure out what prefix we should add to each path // (e.g., trunk/projects/example/) to get the absolute path from the // root of the repository. DVCS systems like Git and Mercurial are not // affected. // Normalize both paths and check if the repository root is a prefix of // the local root. If so, throw it away. Note that this correctly handles // the case where the remote path is "/". $local_root = id(new PhutilURI($local_root))->getPath(); $local_root = rtrim($local_root, '/'); $repo_root = id(new PhutilURI($repository->getRemoteURI()))->getPath(); $repo_root = rtrim($repo_root, '/'); if (!strncmp($repo_root, $local_root, strlen($repo_root))) { $path_prefix = substr($local_root, strlen($repo_root)); } } $changesets = $diff->getChangesets(); $paths = array(); foreach ($changesets as $changeset) { $paths[] = $path_prefix.'/'.$changeset->getFilename(); } // If this change affected paths, save the changesets so we can apply // Owners rules to them later. if ($paths) { $this->ownersDiff = $diff; $this->ownersChangesets = $changesets; } // Mark this as also touching all parent paths, so you can see all pending // changes to any file within a directory. $all_paths = array(); foreach ($paths as $local) { foreach (DiffusionPathIDQuery::expandPathToRoot($local) as $path) { $all_paths[$path] = true; } } $all_paths = array_keys($all_paths); $path_ids = PhabricatorRepositoryCommitChangeParserWorker::lookupOrCreatePaths( $all_paths); $table = new DifferentialAffectedPath(); $conn_w = $table->establishConnection('w'); $sql = array(); foreach ($path_ids as $path_id) { $sql[] = qsprintf( $conn_w, '(%d, %d, %d, %d)', $repository->getID(), $path_id, time(), $revision->getID()); } queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d', $table->getTableName(), $revision->getID()); foreach (array_chunk($sql, 256) as $chunk) { queryfx( $conn_w, 'INSERT INTO %T (repositoryID, pathID, epoch, revisionID) VALUES %Q', $table->getTableName(), implode(', ', $chunk)); } } /** * Update the table connecting revisions to DVCS local hashes, so we can * identify revisions by commit/tree hashes. */ private function updateRevisionHashTable( DifferentialRevision $revision, DifferentialDiff $diff) { $vcs = $diff->getSourceControlSystem(); if ($vcs == DifferentialRevisionControlSystem::SVN) { // Subversion has no local commit or tree hash information, so we don't // have to do anything. return; } $property = id(new DifferentialDiffProperty())->loadOneWhere( 'diffID = %d AND name = %s', $diff->getID(), 'local:commits'); if (!$property) { return; } $hashes = array(); $data = $property->getData(); switch ($vcs) { case DifferentialRevisionControlSystem::GIT: foreach ($data as $commit) { $hashes[] = array( ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT, $commit['commit'], ); $hashes[] = array( ArcanistDifferentialRevisionHash::HASH_GIT_TREE, $commit['tree'], ); } break; case DifferentialRevisionControlSystem::MERCURIAL: foreach ($data as $commit) { $hashes[] = array( ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT, $commit['rev'], ); } break; } $conn_w = $revision->establishConnection('w'); $sql = array(); foreach ($hashes as $info) { list($type, $hash) = $info; $sql[] = qsprintf( $conn_w, '(%d, %s, %s)', $revision->getID(), $type, $hash); } queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d', ArcanistDifferentialRevisionHash::TABLE_NAME, $revision->getID()); if ($sql) { queryfx( $conn_w, 'INSERT INTO %T (revisionID, type, hash) VALUES %Q', ArcanistDifferentialRevisionHash::TABLE_NAME, implode(', ', $sql)); } } private function renderAffectedFilesForMail(DifferentialDiff $diff) { $changesets = $diff->getChangesets(); $filenames = mpull($changesets, 'getDisplayFilename'); sort($filenames); $count = count($filenames); $max = 250; if ($count > $max) { $filenames = array_slice($filenames, 0, $max); $filenames[] = pht('(%d more files...)', ($count - $max)); } return implode("\n", $filenames); } private function renderPatchHTMLForMail($patch) { return phutil_tag('pre', array('style' => 'font-family: monospace;'), $patch); } private function buildPatchForMail(DifferentialDiff $diff, $byte_limit) { $format = PhabricatorEnv::getEnvConfig('metamta.differential.patch-format'); return id(new DifferentialRawDiffRenderer()) ->setViewer($this->getActor()) ->setFormat($format) ->setChangesets($diff->getChangesets()) ->setByteLimit($byte_limit) ->buildPatch(); } protected function willPublish(PhabricatorLiskDAO $object, array $xactions) { // Reload to pick up the active diff and reviewer status. return id(new DifferentialRevisionQuery()) ->setViewer($this->getActor()) ->needReviewers(true) ->needActiveDiffs(true) ->withIDs(array($object->getID())) ->executeOne(); } protected function getCustomWorkerState() { return array( 'changedPriorToCommitURI' => $this->changedPriorToCommitURI, 'firstBroadcast' => $this->firstBroadcast, 'isDraftDemotion' => $this->isDraftDemotion, ); } protected function loadCustomWorkerState(array $state) { $this->changedPriorToCommitURI = idx($state, 'changedPriorToCommitURI'); $this->firstBroadcast = idx($state, 'firstBroadcast'); $this->isDraftDemotion = idx($state, 'isDraftDemotion'); return $this; } private function newCommandeerReviewerTransaction( DifferentialRevision $revision) { $actor_phid = $this->getActingAsPHID(); $owner_phid = $revision->getAuthorPHID(); // If the user is commandeering, add the previous owner as a // reviewer and remove the actor. $edits = array( '-' => array( $actor_phid, ), '+' => array( $owner_phid, ), ); // NOTE: We're setting setIsCommandeerSideEffect() on this because normally // you can't add a revision's author as a reviewer, but this action swaps // them after validation executes. $xaction_type = DifferentialRevisionReviewersTransaction::TRANSACTIONTYPE; return id(new DifferentialTransaction()) ->setTransactionType($xaction_type) ->setIgnoreOnNoEffect(true) ->setIsCommandeerSideEffect(true) ->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); } private function loadUnbroadcastTransactions($object) { $viewer = $this->requireActor(); $xactions = id(new DifferentialTransactionQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($object->getPHID())) ->execute(); return array_reverse($xactions); } protected function didApplyTransactions($object, array $xactions) { // In a moment, we're going to try to publish draft revisions which have // completed all their builds. However, we only want to do that if the // actor is either the revision author or an omnipotent user (generally, // the Harbormaster application). // If we let any actor publish the revision as a side effect of other // changes then an unlucky third party who innocently comments on the draft // can end up racing Harbormaster and promoting the revision. At best, this // is confusing. It can also run into validation problems with the "Request // Review" transaction. See PHI309 for some discussion. $author_phid = $object->getAuthorPHID(); $viewer = $this->requireActor(); $can_undraft = ($this->getActingAsPHID() === $author_phid) || ($viewer->isOmnipotent()); // If a draft revision has no outstanding builds and we're automatically // making drafts public after builds finish, make the revision public. if ($can_undraft) { $auto_undraft = !$object->getHoldAsDraft(); } else { $auto_undraft = false; } $can_promote = false; $can_demote = false; // "Draft" revisions can promote to "Review Requested" after builds pass, // or demote to "Changes Planned" after builds fail. if ($object->isDraft()) { $can_promote = true; $can_demote = true; } // See PHI584. "Changes Planned" revisions which are not yet broadcasting // can promote to "Review Requested" if builds pass. // This pass is presumably the result of someone restarting the builds and // having them work this time, perhaps because the builds are not perfectly // reliable or perhaps because someone fixed some issue with build hardware // or some other dependency. // Currently, there's no legitimate way to end up in this state except // through automatic demotion, so this behavior should not generate an // undue level of confusion or ambiguity. Also note that these changes can // not demote again since they've already been demoted once. if ($object->isChangePlanned()) { if (!$object->getShouldBroadcast()) { $can_promote = true; } } if (($can_promote || $can_demote) && $auto_undraft) { $status = $this->loadCompletedBuildableStatus($object); $is_passed = ($status === HarbormasterBuildableStatus::STATUS_PASSED); $is_failed = ($status === HarbormasterBuildableStatus::STATUS_FAILED); if ($is_passed && $can_promote) { // When Harbormaster moves a revision out of the draft state, we // attribute the action to the revision author since this is more // natural and more useful. // Additionally, we change the acting PHID for the transaction set // to the author if it isn't already a user so that mail comes from // the natural author. $acting_phid = $this->getActingAsPHID(); $user_type = PhabricatorPeopleUserPHIDType::TYPECONST; if (phid_get_type($acting_phid) != $user_type) { $this->setActingAsPHID($author_phid); } $xaction = $object->getApplicationTransactionTemplate() ->setAuthorPHID($author_phid) ->setTransactionType( DifferentialRevisionRequestReviewTransaction::TRANSACTIONTYPE) ->setNewValue(true); // If we're creating this revision and immediately moving it out of // the draft state, mark this as a create transaction so it gets // hidden in the timeline and mail, since it isn't interesting: it // is as though the draft phase never happened. if ($this->getIsNewObject()) { $xaction->setIsCreateTransaction(true); } // Queue this transaction and apply it separately after the current // batch of transactions finishes so that Herald can fire on the new // revision state. See T13027 for discussion. $this->queueTransaction($xaction); } else if ($is_failed && $can_demote) { // When demoting a revision, we act as "Harbormaster" instead of // the author since this feels a little more natural. $harbormaster_phid = id(new PhabricatorHarbormasterApplication()) ->getPHID(); $xaction = $object->getApplicationTransactionTemplate() ->setAuthorPHID($harbormaster_phid) ->setMetadataValue('draft.demote', true) ->setTransactionType( DifferentialRevisionPlanChangesTransaction::TRANSACTIONTYPE) ->setNewValue(true); $this->queueTransaction($xaction); } } // If the revision is new or was a draft, and is no longer a draft, we // might be sending the first email about it. // This might mean it was created directly into a non-draft state, or // it just automatically undrafted after builds finished, or a user // explicitly promoted it out of the draft state with an action like // "Request Review". // If we haven't sent any email about it yet, mark this email as the first // email so the mail gets enriched with "SUMMARY" and "TEST PLAN". $is_new = $this->getIsNewObject(); $was_broadcasting = $this->wasBroadcasting; if ($object->getShouldBroadcast()) { if (!$was_broadcasting || $is_new) { // Mark this as the first broadcast we're sending about the revision // so mail can generate specially. $this->firstBroadcast = true; } } return $xactions; } private function loadCompletedBuildableStatus( DifferentialRevision $revision) { $viewer = $this->requireActor(); $builds = $revision->loadImpactfulBuilds($viewer); return $revision->newBuildableStatusForBuilds($builds); } private function requireReviewers(DifferentialRevision $revision) { if ($revision->hasAttachedReviewers()) { return; } $with_reviewers = id(new DifferentialRevisionQuery()) ->setViewer($this->getActor()) ->needReviewers(true) ->withPHIDs(array($revision->getPHID())) ->executeOne(); if (!$with_reviewers) { throw new Exception( pht( 'Failed to reload revision ("%s").', $revision->getPHID())); } $revision->attachReviewers($with_reviewers->getReviewers()); } } diff --git a/src/applications/differential/mail/DifferentialInlineCommentMailView.php b/src/applications/differential/mail/DifferentialInlineCommentMailView.php index 0bc914914b..c1430320e3 100644 --- a/src/applications/differential/mail/DifferentialInlineCommentMailView.php +++ b/src/applications/differential/mail/DifferentialInlineCommentMailView.php @@ -1,494 +1,494 @@ viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setInlines($inlines) { $this->inlines = $inlines; return $this; } public function getInlines() { return $this->inlines; } public function buildMailSection() { $inlines = $this->getInlines(); $comments = mpull($inlines, 'getComment'); $comments = mpull($comments, null, 'getPHID'); $parents = $this->loadParents($comments); $all_comments = $comments + $parents; $this->changesets = $this->loadChangesets($all_comments); $this->authors = $this->loadAuthors($all_comments); $groups = $this->groupInlines($inlines); $hunk_parser = new DifferentialHunkParser(); $spacer_text = null; $spacer_html = phutil_tag('br'); $section = new PhabricatorMetaMTAMailSection(); $last_group_key = last_key($groups); foreach ($groups as $changeset_id => $group) { $changeset = $this->getChangeset($changeset_id); if (!$changeset) { continue; } $is_last_group = ($changeset_id == $last_group_key); $last_inline_key = last_key($group); foreach ($group as $inline_key => $inline) { $comment = $inline->getComment(); $parent_phid = $comment->getReplyToCommentPHID(); $is_last_inline = ($inline_key == $last_inline_key); $context_text = null; $context_html = null; if ($parent_phid) { $parent = idx($parents, $parent_phid); if ($parent) { $context_text = $this->renderInline($parent, false, true); $context_html = $this->renderInline($parent, true, true); } } else { $patch_text = $this->getPatch($hunk_parser, $comment, false); $context_text = $this->renderPatch($comment, $patch_text, false); $patch_html = $this->getPatch($hunk_parser, $comment, true); $context_html = $this->renderPatch($comment, $patch_html, true); } $render_text = $this->renderInline($comment, false, false); $render_html = $this->renderInline($comment, true, false); $section->addPlaintextFragment($context_text); $section->addPlaintextFragment($spacer_text); $section->addPlaintextFragment($render_text); $html_fragment = $this->renderContentBox( array( $context_html, $render_html, )); $section->addHTMLFragment($html_fragment); if (!$is_last_group || !$is_last_inline) { $section->addPlaintextFragment($spacer_text); $section->addHTMLFragment($spacer_html); } } } return $section; } private function loadChangesets(array $comments) { if (!$comments) { return array(); } $ids = array(); foreach ($comments as $comment) { $ids[] = $comment->getChangesetID(); } $changesets = id(new DifferentialChangesetQuery()) ->setViewer($this->getViewer()) ->withIDs($ids) ->needHunks(true) ->execute(); return mpull($changesets, null, 'getID'); } private function loadParents(array $comments) { $viewer = $this->getViewer(); $phids = array(); foreach ($comments as $comment) { $parent_phid = $comment->getReplyToCommentPHID(); if (!$parent_phid) { continue; } $phids[] = $parent_phid; } if (!$phids) { return array(); } $parents = id(new DifferentialDiffInlineCommentQuery()) ->setViewer($viewer) ->withPHIDs($phids) ->execute(); return mpull($parents, null, 'getPHID'); } private function loadAuthors(array $comments) { $viewer = $this->getViewer(); $phids = array(); foreach ($comments as $comment) { $author_phid = $comment->getAuthorPHID(); if (!$author_phid) { continue; } $phids[] = $author_phid; } if (!$phids) { return array(); } return $viewer->loadHandles($phids); } private function groupInlines(array $inlines) { return DifferentialTransactionComment::sortAndGroupInlines( $inlines, $this->changesets); } private function renderInline( DifferentialTransactionComment $comment, $is_html, $is_quote) { $changeset = $this->getChangeset($comment->getChangesetID()); if (!$changeset) { return null; } $content = $comment->getContent(); $content = $this->renderRemarkupContent($content, $is_html); if ($is_quote) { $header = $this->renderHeader($comment, $is_html, true); } else { $header = null; } if ($is_html) { $style = array( 'margin: 8px 0;', 'padding: 0 12px;', ); if ($is_quote) { $style[] = 'color: #74777D;'; } $content = phutil_tag( 'div', array( 'style' => implode(' ', $style), ), $content); } $parts = array( $header, "\n", $content, ); if (!$is_html) { $parts = implode('', $parts); $parts = trim($parts); } if ($is_quote) { if ($is_html) { $parts = $this->quoteHTML($parts); } else { $parts = $this->quoteText($parts); } } return $parts; } private function renderRemarkupContent($content, $is_html) { $viewer = $this->getViewer(); $production_uri = PhabricatorEnv::getProductionURI('/'); if ($is_html) { $mode = PhutilRemarkupEngine::MODE_HTML_MAIL; } else { $mode = PhutilRemarkupEngine::MODE_TEXT; } $attributes = array( 'style' => 'padding: 0; margin: 8px;', ); $engine = PhabricatorMarkupEngine::newMarkupEngine(array()) ->setConfig('viewer', $viewer) ->setConfig('uri.base', $production_uri) ->setConfig('default.p.attributes', $attributes) ->setMode($mode); try { return $engine->markupText($content); } catch (Exception $ex) { return $content; } } private function getChangeset($id) { return idx($this->changesets, $id); } private function getAuthor($phid) { if (isset($this->authors[$phid])) { return $this->authors[$phid]; } return null; } private function quoteText($block) { $block = phutil_split_lines($block); foreach ($block as $key => $line) { $block[$key] = '> '.$line; } return implode('', $block); } private function quoteHTML($block) { $styles = array( 'padding: 0;', 'background: #F7F7F7;', 'border-color: #e3e4e8;', 'border-style: solid;', 'border-width: 0 0 1px 0;', 'margin: 0;', ); $styles = implode(' ', $styles); return phutil_tag( 'div', array( 'style' => $styles, ), $block); } private function getPatch( DifferentialHunkParser $parser, DifferentialTransactionComment $comment, $is_html) { $changeset = $this->getChangeset($comment->getChangesetID()); $is_new = $comment->getIsNewFile(); $start = $comment->getLineNumber(); $length = $comment->getLineLength(); // By default, show one line of context around the target inline. $context = 1; // If the inline is at least 3 lines long, don't show any extra context. if ($length >= 2) { $context = 0; } // If the inline is more than 7 lines long, only show the first 7 lines. if ($length >= 6) { $length = 6; } if (!$is_html) { $hunks = $changeset->getHunks(); $patch = $parser->makeContextDiff( $hunks, $is_new, $start, $length, $context); $patch = phutil_split_lines($patch); // Remove the "@@ -x,y +u,v @@" line. array_shift($patch); return implode('', $patch); } $viewer = $this->getViewer(); $engine = new PhabricatorMarkupEngine(); if ($is_new) { $offset_mode = 'new'; } else { $offset_mode = 'old'; } // See PHI894. Use the parse cache since we can end up with a large // rendering cost otherwise when users or bots leave hundreds of inline // comments on diffs with long recipient lists. $cache_key = $changeset->getID(); $parser = id(new DifferentialChangesetParser()) ->setRenderCacheKey($cache_key) ->setUser($viewer) ->setChangeset($changeset) ->setOffsetMode($offset_mode) ->setMarkupEngine($engine); $parser->setRenderer(new DifferentialChangesetOneUpMailRenderer()); return $parser->render( $start - $context, - $length + 1 + (2 * $context), + $length + (2 * $context), array()); } private function renderPatch( DifferentialTransactionComment $comment, $patch, $is_html) { if ($is_html) { $patch = $this->renderCodeBlock($patch); } $header = $this->renderHeader($comment, $is_html, false); $patch = array( $header, "\n", $patch, ); if (!$is_html) { $patch = implode('', $patch); $patch = $this->quoteText($patch); } else { $patch = $this->quoteHTML($patch); } return $patch; } private function renderHeader( DifferentialTransactionComment $comment, $is_html, $with_author) { $changeset = $this->getChangeset($comment->getChangesetID()); $path = $changeset->getFilename(); // Only show the filename. $path = basename($path); $start = $comment->getLineNumber(); $length = $comment->getLineLength(); if ($length) { $range = pht('%s-%s', $start, $start + $length); } else { $range = $start; } $header = "{$path}:{$range}"; if ($is_html) { $header = $this->renderHeaderBold($header); } if ($with_author) { $author = $this->getAuthor($comment->getAuthorPHID()); } else { $author = null; } if ($author) { $byline = $author->getName(); if ($is_html) { $byline = $this->renderHeaderBold($byline); } $header = pht('%s wrote in %s', $byline, $header); } if ($is_html) { $link_href = $this->getInlineURI($comment); if ($link_href) { $link_style = array( 'float: right;', 'text-decoration: none;', ); $link = phutil_tag( 'a', array( 'style' => implode(' ', $link_style), 'href' => $link_href, ), array( pht('View Inline'), // See PHI920. Add a space after the link so we render this into // the document: // // View Inline filename.txt // // Otherwise, we render "Inlinefilename.txt" and double-clicking // the file name selects the word "Inline" as well. ' ', )); } else { $link = null; } $header = $this->renderHeaderBlock(array($link, $header)); } return $header; } private function getInlineURI(DifferentialTransactionComment $comment) { $changeset = $this->getChangeset($comment->getChangesetID()); if (!$changeset) { return null; } $diff = $changeset->getDiff(); if (!$diff) { return null; } $revision = $diff->getRevision(); if (!$revision) { return null; } $link_href = '/'.$revision->getMonogram().'#inline-'.$comment->getID(); $link_href = PhabricatorEnv::getProductionURI($link_href); return $link_href; } } diff --git a/src/applications/differential/parser/DifferentialChangesetParser.php b/src/applications/differential/parser/DifferentialChangesetParser.php index 596ea0e426..e214aa16a4 100644 --- a/src/applications/differential/parser/DifferentialChangesetParser.php +++ b/src/applications/differential/parser/DifferentialChangesetParser.php @@ -1,1483 +1,1490 @@ rangeStart = $start; $this->rangeEnd = $end; return $this; } public function setMask(array $mask) { $this->mask = $mask; return $this; } public function renderChangeset() { return $this->render($this->rangeStart, $this->rangeEnd, $this->mask); } public function setShowEditAndReplyLinks($bool) { $this->showEditAndReplyLinks = $bool; return $this; } public function getShowEditAndReplyLinks() { return $this->showEditAndReplyLinks; } public function setHighlightAs($highlight_as) { $this->highlightAs = $highlight_as; return $this; } public function getHighlightAs() { return $this->highlightAs; } public function setCharacterEncoding($character_encoding) { $this->characterEncoding = $character_encoding; return $this; } public function getCharacterEncoding() { return $this->characterEncoding; } public function setRenderer(DifferentialChangesetRenderer $renderer) { $this->renderer = $renderer; return $this; } public function getRenderer() { if (!$this->renderer) { return new DifferentialChangesetTwoUpRenderer(); } return $this->renderer; } public function setDisableCache($disable_cache) { $this->disableCache = $disable_cache; return $this; } public function getDisableCache() { return $this->disableCache; } public function setCanMarkDone($can_mark_done) { $this->canMarkDone = $can_mark_done; return $this; } public function getCanMarkDone() { return $this->canMarkDone; } public function setObjectOwnerPHID($phid) { $this->objectOwnerPHID = $phid; return $this; } public function getObjectOwnerPHID() { return $this->objectOwnerPHID; } public function setOffsetMode($offset_mode) { $this->offsetMode = $offset_mode; return $this; } public function getOffsetMode() { return $this->offsetMode; } public static function getDefaultRendererForViewer(PhabricatorUser $viewer) { $is_unified = $viewer->compareUserSetting( PhabricatorUnifiedDiffsSetting::SETTINGKEY, PhabricatorUnifiedDiffsSetting::VALUE_ALWAYS_UNIFIED); if ($is_unified) { return '1up'; } return null; } public function readParametersFromRequest(AphrontRequest $request) { $this->setWhitespaceMode($request->getStr('whitespace')); $this->setCharacterEncoding($request->getStr('encoding')); $this->setHighlightAs($request->getStr('highlight')); $renderer = null; // If the viewer prefers unified diffs, always set the renderer to unified. // Otherwise, we leave it unspecified and the client will choose a // renderer based on the screen size. if ($request->getStr('renderer')) { $renderer = $request->getStr('renderer'); } else { $renderer = self::getDefaultRendererForViewer($request->getViewer()); } switch ($renderer) { case '1up': $this->setRenderer(new DifferentialChangesetOneUpRenderer()); break; default: $this->setRenderer(new DifferentialChangesetTwoUpRenderer()); break; } return $this; } const CACHE_VERSION = 11; const CACHE_MAX_SIZE = 8e6; const ATTR_GENERATED = 'attr:generated'; const ATTR_DELETED = 'attr:deleted'; const ATTR_UNCHANGED = 'attr:unchanged'; const ATTR_WHITELINES = 'attr:white'; const ATTR_MOVEAWAY = 'attr:moveaway'; const WHITESPACE_SHOW_ALL = 'show-all'; const WHITESPACE_IGNORE_TRAILING = 'ignore-trailing'; const WHITESPACE_IGNORE_MOST = 'ignore-most'; const WHITESPACE_IGNORE_ALL = 'ignore-all'; public function setOldLines(array $lines) { $this->old = $lines; return $this; } public function setNewLines(array $lines) { $this->new = $lines; return $this; } public function setSpecialAttributes(array $attributes) { $this->specialAttributes = $attributes; return $this; } public function setIntraLineDiffs(array $diffs) { $this->intra = $diffs; return $this; } public function setVisibileLinesMask(array $mask) { $this->visible = $mask; return $this; } public function setLinesOfContext($lines_of_context) { $this->linesOfContext = $lines_of_context; return $this; } public function getLinesOfContext() { return $this->linesOfContext; } /** * Configure which Changeset comments added to the right side of the visible * diff will be attached to. The ID must be the ID of a real Differential * Changeset. * * The complexity here is that we may show an arbitrary side of an arbitrary * changeset as either the left or right part of a diff. This method allows * the left and right halves of the displayed diff to be correctly mapped to * storage changesets. * * @param id The Differential Changeset ID that comments added to the right * side of the visible diff should be attached to. * @param bool If true, attach new comments to the right side of the storage * changeset. Note that this may be false, if the left side of * some storage changeset is being shown as the right side of * a display diff. * @return this */ public function setRightSideCommentMapping($id, $is_new) { $this->rightSideChangesetID = $id; $this->rightSideAttachesToNewFile = $is_new; return $this; } /** * See setRightSideCommentMapping(), but this sets information for the left * side of the display diff. */ public function setLeftSideCommentMapping($id, $is_new) { $this->leftSideChangesetID = $id; $this->leftSideAttachesToNewFile = $is_new; return $this; } public function setOriginals( DifferentialChangeset $left, DifferentialChangeset $right) { $this->originalLeft = $left; $this->originalRight = $right; return $this; } public function diffOriginals() { $engine = new PhabricatorDifferenceEngine(); $changeset = $engine->generateChangesetFromFileContent( implode('', mpull($this->originalLeft->getHunks(), 'getChanges')), implode('', mpull($this->originalRight->getHunks(), 'getChanges'))); $parser = new DifferentialHunkParser(); return $parser->parseHunksForHighlightMasks( $changeset->getHunks(), $this->originalLeft->getHunks(), $this->originalRight->getHunks()); } /** * Set a key for identifying this changeset in the render cache. If set, the * parser will attempt to use the changeset render cache, which can improve * performance for frequently-viewed changesets. * * By default, there is no render cache key and parsers do not use the cache. * This is appropriate for rarely-viewed changesets. * * NOTE: Currently, this key must be a valid Differential Changeset ID. * * @param string Key for identifying this changeset in the render cache. * @return this */ public function setRenderCacheKey($key) { $this->renderCacheKey = $key; return $this; } private function getRenderCacheKey() { return $this->renderCacheKey; } public function setChangeset(DifferentialChangeset $changeset) { $this->changeset = $changeset; $this->setFilename($changeset->getFilename()); return $this; } public function setWhitespaceMode($whitespace_mode) { $this->whitespaceMode = $whitespace_mode; return $this; } public function setRenderingReference($ref) { $this->renderingReference = $ref; return $this; } private function getRenderingReference() { return $this->renderingReference; } public function getChangeset() { return $this->changeset; } public function setFilename($filename) { $this->filename = $filename; return $this; } public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } public function setMarkupEngine(PhabricatorMarkupEngine $engine) { $this->markupEngine = $engine; return $this; } public function setUser(PhabricatorUser $user) { $this->user = $user; return $this; } public function getUser() { return $this->user; } public function setCoverage($coverage) { $this->coverage = $coverage; return $this; } private function getCoverage() { return $this->coverage; } public function parseInlineComment( PhabricatorInlineCommentInterface $comment) { // Parse only comments which are actually visible. if ($this->isCommentVisibleOnRenderedDiff($comment)) { $this->comments[] = $comment; } return $this; } private function loadCache() { $render_cache_key = $this->getRenderCacheKey(); if (!$render_cache_key) { return false; } $data = null; $changeset = new DifferentialChangeset(); $conn_r = $changeset->establishConnection('r'); $data = queryfx_one( $conn_r, 'SELECT * FROM %T WHERE id = %d', $changeset->getTableName().'_parse_cache', $render_cache_key); if (!$data) { return false; } if ($data['cache'][0] == '{') { // This is likely an old-style JSON cache which we will not be able to // deserialize. return false; } $data = unserialize($data['cache']); if (!is_array($data) || !$data) { return false; } foreach (self::getCacheableProperties() as $cache_key) { if (!array_key_exists($cache_key, $data)) { // If we're missing a cache key, assume we're looking at an old cache // and ignore it. return false; } } if ($data['cacheVersion'] !== self::CACHE_VERSION) { return false; } // Someone displays contents of a partially cached shielded file. if (!isset($data['newRender']) && (!$this->isTopLevel || $this->comments)) { return false; } unset($data['cacheVersion'], $data['cacheHost']); $cache_prop = array_select_keys($data, self::getCacheableProperties()); foreach ($cache_prop as $cache_key => $v) { $this->$cache_key = $v; } return true; } protected static function getCacheableProperties() { return array( 'visible', 'new', 'old', 'intra', 'newRender', 'oldRender', 'specialAttributes', 'hunkStartLines', 'cacheVersion', 'cacheHost', 'highlightingDisabled', ); } public function saveCache() { if (PhabricatorEnv::isReadOnly()) { return false; } if ($this->highlightErrors) { return false; } $render_cache_key = $this->getRenderCacheKey(); if (!$render_cache_key) { return false; } $cache = array(); foreach (self::getCacheableProperties() as $cache_key) { switch ($cache_key) { case 'cacheVersion': $cache[$cache_key] = self::CACHE_VERSION; break; case 'cacheHost': $cache[$cache_key] = php_uname('n'); break; default: $cache[$cache_key] = $this->$cache_key; break; } } $cache = serialize($cache); // We don't want to waste too much space by a single changeset. if (strlen($cache) > self::CACHE_MAX_SIZE) { return; } $changeset = new DifferentialChangeset(); $conn_w = $changeset->establishConnection('w'); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); try { queryfx( $conn_w, 'INSERT INTO %T (id, cache, dateCreated) VALUES (%d, %B, %d) ON DUPLICATE KEY UPDATE cache = VALUES(cache)', DifferentialChangeset::TABLE_CACHE, $render_cache_key, $cache, time()); } catch (AphrontQueryException $ex) { // Ignore these exceptions. A common cause is that the cache is // larger than 'max_allowed_packet', in which case we're better off // not writing it. // TODO: It would be nice to tailor this more narrowly. } unset($unguarded); } private function markGenerated($new_corpus_block = '') { $generated_guess = (strpos($new_corpus_block, '@'.'generated') !== false); if (!$generated_guess) { $generated_path_regexps = PhabricatorEnv::getEnvConfig( 'differential.generated-paths'); foreach ($generated_path_regexps as $regexp) { if (preg_match($regexp, $this->changeset->getFilename())) { $generated_guess = true; break; } } } $event = new PhabricatorEvent( PhabricatorEventType::TYPE_DIFFERENTIAL_WILLMARKGENERATED, array( 'corpus' => $new_corpus_block, 'is_generated' => $generated_guess, ) ); PhutilEventEngine::dispatchEvent($event); $generated = $event->getValue('is_generated'); $attribute = $this->changeset->isGeneratedChangeset(); if ($attribute) { $generated = true; } $this->specialAttributes[self::ATTR_GENERATED] = $generated; } public function isGenerated() { return idx($this->specialAttributes, self::ATTR_GENERATED, false); } public function isDeleted() { return idx($this->specialAttributes, self::ATTR_DELETED, false); } public function isUnchanged() { return idx($this->specialAttributes, self::ATTR_UNCHANGED, false); } public function isWhitespaceOnly() { return idx($this->specialAttributes, self::ATTR_WHITELINES, false); } public function isMoveAway() { return idx($this->specialAttributes, self::ATTR_MOVEAWAY, false); } private function applyIntraline(&$render, $intra, $corpus) { foreach ($render as $key => $text) { if (isset($intra[$key])) { $render[$key] = ArcanistDiffUtils::applyIntralineDiff( $text, $intra[$key]); } } } private function getHighlightFuture($corpus) { $language = $this->highlightAs; if (!$language) { $language = $this->highlightEngine->getLanguageFromFilename( $this->filename); if (($language != 'txt') && (strlen($corpus) > self::HIGHLIGHT_BYTE_LIMIT)) { $this->highlightingDisabled = true; $language = 'txt'; } } return $this->highlightEngine->getHighlightFuture( $language, $corpus); } protected function processHighlightedSource($data, $result) { $result_lines = phutil_split_lines($result); foreach ($data as $key => $info) { if (!$info) { unset($result_lines[$key]); } } return $result_lines; } private function tryCacheStuff() { $whitespace_mode = $this->whitespaceMode; switch ($whitespace_mode) { case self::WHITESPACE_SHOW_ALL: case self::WHITESPACE_IGNORE_TRAILING: case self::WHITESPACE_IGNORE_ALL: break; default: $whitespace_mode = self::WHITESPACE_IGNORE_MOST; break; } $skip_cache = ($whitespace_mode != self::WHITESPACE_IGNORE_MOST); if ($this->disableCache) { $skip_cache = true; } if ($this->characterEncoding) { $skip_cache = true; } if ($this->highlightAs) { $skip_cache = true; } $this->whitespaceMode = $whitespace_mode; $changeset = $this->changeset; if ($changeset->getFileType() != DifferentialChangeType::FILE_TEXT && $changeset->getFileType() != DifferentialChangeType::FILE_SYMLINK) { $this->markGenerated(); } else { if ($skip_cache || !$this->loadCache()) { $this->process(); if (!$skip_cache) { $this->saveCache(); } } } } private function process() { $whitespace_mode = $this->whitespaceMode; $changeset = $this->changeset; $ignore_all = (($whitespace_mode == self::WHITESPACE_IGNORE_MOST) || ($whitespace_mode == self::WHITESPACE_IGNORE_ALL)); $force_ignore = ($whitespace_mode == self::WHITESPACE_IGNORE_ALL); if (!$force_ignore) { if ($ignore_all && $changeset->getWhitespaceMatters()) { $ignore_all = false; } } // The "ignore all whitespace" algorithm depends on rediffing the // files, and we currently need complete representations of both // files to do anything reasonable. If we only have parts of the files, // don't use the "ignore all" algorithm. if ($ignore_all) { $hunks = $changeset->getHunks(); if (count($hunks) !== 1) { $ignore_all = false; } else { $first_hunk = reset($hunks); if ($first_hunk->getOldOffset() != 1 || $first_hunk->getNewOffset() != 1) { $ignore_all = false; } } } if ($ignore_all) { $old_file = $changeset->makeOldFile(); $new_file = $changeset->makeNewFile(); if ($old_file == $new_file) { // If the old and new files are exactly identical, the synthetic // diff below will give us nonsense and whitespace modes are // irrelevant anyway. This occurs when you, e.g., copy a file onto // itself in Subversion (see T271). $ignore_all = false; } } $hunk_parser = new DifferentialHunkParser(); $hunk_parser->setWhitespaceMode($whitespace_mode); $hunk_parser->parseHunksForLineData($changeset->getHunks()); // Depending on the whitespace mode, we may need to compute a different // set of changes than the set of changes in the hunk data (specifically, // we might want to consider changed lines which have only whitespace // changes as unchanged). if ($ignore_all) { $engine = new PhabricatorDifferenceEngine(); $engine->setIgnoreWhitespace(true); $no_whitespace_changeset = $engine->generateChangesetFromFileContent( $old_file, $new_file); $type_parser = new DifferentialHunkParser(); $type_parser->parseHunksForLineData($no_whitespace_changeset->getHunks()); $hunk_parser->setOldLineTypeMap($type_parser->getOldLineTypeMap()); $hunk_parser->setNewLineTypeMap($type_parser->getNewLineTypeMap()); } $hunk_parser->reparseHunksForSpecialAttributes(); $unchanged = false; if (!$hunk_parser->getHasAnyChanges()) { $filetype = $this->changeset->getFileType(); if ($filetype == DifferentialChangeType::FILE_TEXT || $filetype == DifferentialChangeType::FILE_SYMLINK) { $unchanged = true; } } $moveaway = false; $changetype = $this->changeset->getChangeType(); if ($changetype == DifferentialChangeType::TYPE_MOVE_AWAY) { $moveaway = true; } $this->setSpecialAttributes(array( self::ATTR_UNCHANGED => $unchanged, self::ATTR_DELETED => $hunk_parser->getIsDeleted(), self::ATTR_WHITELINES => !$hunk_parser->getHasTextChanges(), self::ATTR_MOVEAWAY => $moveaway, )); $lines_context = $this->getLinesOfContext(); $hunk_parser->generateIntraLineDiffs(); $hunk_parser->generateVisibileLinesMask($lines_context); $this->setOldLines($hunk_parser->getOldLines()); $this->setNewLines($hunk_parser->getNewLines()); $this->setIntraLineDiffs($hunk_parser->getIntraLineDiffs()); $this->setVisibileLinesMask($hunk_parser->getVisibleLinesMask()); $this->hunkStartLines = $hunk_parser->getHunkStartLines( $changeset->getHunks()); $new_corpus = $hunk_parser->getNewCorpus(); $new_corpus_block = implode('', $new_corpus); $this->markGenerated($new_corpus_block); if ($this->isTopLevel && !$this->comments && ($this->isGenerated() || $this->isUnchanged() || $this->isDeleted())) { return; } $old_corpus = $hunk_parser->getOldCorpus(); $old_corpus_block = implode('', $old_corpus); $old_future = $this->getHighlightFuture($old_corpus_block); $new_future = $this->getHighlightFuture($new_corpus_block); $futures = array( 'old' => $old_future, 'new' => $new_future, ); $corpus_blocks = array( 'old' => $old_corpus_block, 'new' => $new_corpus_block, ); $this->highlightErrors = false; foreach (new FutureIterator($futures) as $key => $future) { try { try { $highlighted = $future->resolve(); } catch (PhutilSyntaxHighlighterException $ex) { $this->highlightErrors = true; $highlighted = id(new PhutilDefaultSyntaxHighlighter()) ->getHighlightFuture($corpus_blocks[$key]) ->resolve(); } switch ($key) { case 'old': $this->oldRender = $this->processHighlightedSource( $this->old, $highlighted); break; case 'new': $this->newRender = $this->processHighlightedSource( $this->new, $highlighted); break; } } catch (Exception $ex) { phlog($ex); throw $ex; } } $this->applyIntraline( $this->oldRender, ipull($this->intra, 0), $old_corpus); $this->applyIntraline( $this->newRender, ipull($this->intra, 1), $new_corpus); } private function shouldRenderPropertyChangeHeader($changeset) { if (!$this->isTopLevel) { // We render properties only at top level; otherwise we get multiple // copies of them when a user clicks "Show More". return false; } return true; } public function render( $range_start = null, $range_len = null, $mask_force = array()) { // "Top level" renders are initial requests for the whole file, versus // requests for a specific range generated by clicking "show more". We // generate property changes and "shield" UI elements only for toplevel // requests. $this->isTopLevel = (($range_start === null) && ($range_len === null)); $this->highlightEngine = PhabricatorSyntaxHighlighter::newEngine(); $encoding = null; if ($this->characterEncoding) { // We are forcing this changeset to be interpreted with a specific // character encoding, so force all the hunks into that encoding and // propagate it to the renderer. $encoding = $this->characterEncoding; foreach ($this->changeset->getHunks() as $hunk) { $hunk->forceEncoding($this->characterEncoding); } } else { // We're just using the default, so tell the renderer what that is // (by reading the encoding from the first hunk). foreach ($this->changeset->getHunks() as $hunk) { $encoding = $hunk->getDataEncoding(); break; } } $this->tryCacheStuff(); // If we're rendering in an offset mode, treat the range numbers as line // numbers instead of rendering offsets. $offset_mode = $this->getOffsetMode(); if ($offset_mode) { if ($offset_mode == 'new') { $offset_map = $this->new; } else { $offset_map = $this->old; } + // NOTE: Inline comments use zero-based lengths. For example, a comment + // that starts and ends on line 123 has length 0. Rendering considers + // this range to have length 1. Probably both should agree, but that + // ship likely sailed long ago. Tweak things here to get the two systems + // to agree. See PHI985, where this affected mail rendering of inline + // comments left on the final line of a file. + $range_end = $this->getOffset($offset_map, $range_start + $range_len); $range_start = $this->getOffset($offset_map, $range_start); - $range_len = ($range_end - $range_start); + $range_len = ($range_end - $range_start) + 1; } $render_pch = $this->shouldRenderPropertyChangeHeader($this->changeset); $rows = max( count($this->old), count($this->new)); $renderer = $this->getRenderer() ->setUser($this->getUser()) ->setChangeset($this->changeset) ->setRenderPropertyChangeHeader($render_pch) ->setIsTopLevel($this->isTopLevel) ->setOldRender($this->oldRender) ->setNewRender($this->newRender) ->setHunkStartLines($this->hunkStartLines) ->setOldChangesetID($this->leftSideChangesetID) ->setNewChangesetID($this->rightSideChangesetID) ->setOldAttachesToNewFile($this->leftSideAttachesToNewFile) ->setNewAttachesToNewFile($this->rightSideAttachesToNewFile) ->setCodeCoverage($this->getCoverage()) ->setRenderingReference($this->getRenderingReference()) ->setMarkupEngine($this->markupEngine) ->setHandles($this->handles) ->setOldLines($this->old) ->setNewLines($this->new) ->setOriginalCharacterEncoding($encoding) ->setShowEditAndReplyLinks($this->getShowEditAndReplyLinks()) ->setCanMarkDone($this->getCanMarkDone()) ->setObjectOwnerPHID($this->getObjectOwnerPHID()) ->setHighlightingDisabled($this->highlightingDisabled); $shield = null; if ($this->isTopLevel && !$this->comments) { if ($this->isGenerated()) { $shield = $renderer->renderShield( pht( 'This file contains generated code, which does not normally '. 'need to be reviewed.')); } else if ($this->isMoveAway()) { // We put an empty shield on these files. Normally, they do not have // any diff content anyway. However, if they come through `arc`, they // may have content. We don't want to show it (it's not useful) and // we bailed out of fully processing it earlier anyway. // We could show a message like "this file was moved", but we show // that as a change header anyway, so it would be redundant. Instead, // just render an empty shield to skip rendering the diff body. $shield = ''; } else if ($this->isUnchanged()) { $type = 'text'; if (!$rows) { // NOTE: Normally, diffs which don't change files do not include // file content (for example, if you "chmod +x" a file and then // run "git show", the file content is not available). Similarly, // if you move a file from A to B without changing it, diffs normally // do not show the file content. In some cases `arc` is able to // synthetically generate content for these diffs, but for raw diffs // we'll never have it so we need to be prepared to not render a link. $type = 'none'; } $type_add = DifferentialChangeType::TYPE_ADD; if ($this->changeset->getChangeType() == $type_add) { // Although the generic message is sort of accurate in a technical // sense, this more-tailored message is less confusing. $shield = $renderer->renderShield( pht('This is an empty file.'), $type); } else { $shield = $renderer->renderShield( pht('The contents of this file were not changed.'), $type); } } else if ($this->isWhitespaceOnly()) { $shield = $renderer->renderShield( pht('This file was changed only by adding or removing whitespace.'), 'whitespace'); } else if ($this->isDeleted()) { $shield = $renderer->renderShield( pht('This file was completely deleted.')); } else if ($this->changeset->getAffectedLineCount() > 2500) { $shield = $renderer->renderShield( pht( 'This file has a very large number of changes (%s lines).', new PhutilNumber($this->changeset->getAffectedLineCount()))); } } if ($shield !== null) { return $renderer->renderChangesetTable($shield); } // This request should render the "undershield" headers if it's a top-level // request which made it this far (indicating the changeset has no shield) // or it's a request with no mask information (indicating it's the request // that removes the rendering shield). Possibly, this second class of // request might need to be made more explicit. $is_undershield = (empty($mask_force) || $this->isTopLevel); $renderer->setIsUndershield($is_undershield); $old_comments = array(); $new_comments = array(); $old_mask = array(); $new_mask = array(); $feedback_mask = array(); $lines_context = $this->getLinesOfContext(); if ($this->comments) { // If there are any comments which appear in sections of the file which // we don't have, we're going to move them backwards to the closest // earlier line. Two cases where this may happen are: // // - Porting ghost comments forward into a file which was mostly // deleted. // - Porting ghost comments forward from a full-context diff to a // partial-context diff. list($old_backmap, $new_backmap) = $this->buildLineBackmaps(); foreach ($this->comments as $comment) { $new_side = $this->isCommentOnRightSideWhenDisplayed($comment); $line = $comment->getLineNumber(); if ($new_side) { $back_line = $new_backmap[$line]; } else { $back_line = $old_backmap[$line]; } if ($back_line != $line) { // TODO: This should probably be cleaner, but just be simple and // obvious for now. $ghost = $comment->getIsGhost(); if ($ghost) { $moved = pht( 'This comment originally appeared on line %s, but that line '. 'does not exist in this version of the diff. It has been '. 'moved backward to the nearest line.', new PhutilNumber($line)); $ghost['reason'] = $ghost['reason']."\n\n".$moved; $comment->setIsGhost($ghost); } $comment->setLineNumber($back_line); $comment->setLineLength(0); } $start = max($comment->getLineNumber() - $lines_context, 0); $end = $comment->getLineNumber() + $comment->getLineLength() + $lines_context; for ($ii = $start; $ii <= $end; $ii++) { if ($new_side) { $new_mask[$ii] = true; } else { $old_mask[$ii] = true; } } } foreach ($this->old as $ii => $old) { if (isset($old['line']) && isset($old_mask[$old['line']])) { $feedback_mask[$ii] = true; } } foreach ($this->new as $ii => $new) { if (isset($new['line']) && isset($new_mask[$new['line']])) { $feedback_mask[$ii] = true; } } $this->comments = id(new PHUIDiffInlineThreader()) ->reorderAndThreadCommments($this->comments); foreach ($this->comments as $comment) { $final = $comment->getLineNumber() + $comment->getLineLength(); $final = max(1, $final); if ($this->isCommentOnRightSideWhenDisplayed($comment)) { $new_comments[$final][] = $comment; } else { $old_comments[$final][] = $comment; } } } $renderer ->setOldComments($old_comments) ->setNewComments($new_comments); switch ($this->changeset->getFileType()) { case DifferentialChangeType::FILE_IMAGE: $old = null; $new = null; // TODO: Improve the architectural issue as discussed in D955 // https://secure.phabricator.com/D955 $reference = $this->getRenderingReference(); $parts = explode('/', $reference); if (count($parts) == 2) { list($id, $vs) = $parts; } else { $id = $parts[0]; $vs = 0; } $id = (int)$id; $vs = (int)$vs; if (!$vs) { $metadata = $this->changeset->getMetadata(); $data = idx($metadata, 'attachment-data'); $old_phid = idx($metadata, 'old:binary-phid'); $new_phid = idx($metadata, 'new:binary-phid'); } else { $vs_changeset = id(new DifferentialChangeset())->load($vs); $old_phid = null; $new_phid = null; // TODO: This is spooky, see D6851 if ($vs_changeset) { $vs_metadata = $vs_changeset->getMetadata(); $old_phid = idx($vs_metadata, 'new:binary-phid'); } $changeset = id(new DifferentialChangeset())->load($id); if ($changeset) { $metadata = $changeset->getMetadata(); $new_phid = idx($metadata, 'new:binary-phid'); } } if ($old_phid || $new_phid) { // grab the files, (micro) optimization for 1 query not 2 $file_phids = array(); if ($old_phid) { $file_phids[] = $old_phid; } if ($new_phid) { $file_phids[] = $new_phid; } $files = id(new PhabricatorFileQuery()) ->setViewer($this->getUser()) ->withPHIDs($file_phids) ->execute(); foreach ($files as $file) { if (empty($file)) { continue; } if ($file->getPHID() == $old_phid) { $old = $file; } else if ($file->getPHID() == $new_phid) { $new = $file; } } } $renderer->attachOldFile($old); $renderer->attachNewFile($new); return $renderer->renderFileChange($old, $new, $id, $vs); case DifferentialChangeType::FILE_DIRECTORY: case DifferentialChangeType::FILE_BINARY: $output = $renderer->renderChangesetTable(null); return $output; } if ($this->originalLeft && $this->originalRight) { list($highlight_old, $highlight_new) = $this->diffOriginals(); $highlight_old = array_flip($highlight_old); $highlight_new = array_flip($highlight_new); $renderer ->setHighlightOld($highlight_old) ->setHighlightNew($highlight_new); } $renderer ->setOriginalOld($this->originalLeft) ->setOriginalNew($this->originalRight); if ($range_start === null) { $range_start = 0; } if ($range_len === null) { $range_len = $rows; } $range_len = min($range_len, $rows - $range_start); list($gaps, $mask, $depths) = $this->calculateGapsMaskAndDepths( $mask_force, $feedback_mask, $range_start, $range_len); $renderer ->setGaps($gaps) ->setMask($mask) ->setDepths($depths); $html = $renderer->renderTextChange( $range_start, $range_len, $rows); return $renderer->renderChangesetTable($html); } /** * This function calculates a lot of stuff we need to know to display * the diff: * * Gaps - compute gaps in the visible display diff, where we will render * "Show more context" spacers. If a gap is smaller than the context size, * we just display it. Otherwise, we record it into $gaps and will render a * "show more context" element instead of diff text below. A given $gap * is a tuple of $gap_line_number_start and $gap_length. * * Mask - compute the actual lines that need to be shown (because they * are near changes lines, near inline comments, or the request has * explicitly asked for them, i.e. resulting from the user clicking * "show more"). The $mask returned is a sparsely populated dictionary * of $visible_line_number => true. * * Depths - compute how indented any given line is. The $depths returned * is a sparsely populated dictionary of $visible_line_number => $depth. * * This function also has the side effect of modifying member variable * new such that tabs are normalized to spaces for each line of the diff. * * @return array($gaps, $mask, $depths) */ private function calculateGapsMaskAndDepths( $mask_force, $feedback_mask, $range_start, $range_len) { $lines_context = $this->getLinesOfContext(); // Calculate gaps and mask first $gaps = array(); $gap_start = 0; $in_gap = false; $base_mask = $this->visible + $mask_force + $feedback_mask; $base_mask[$range_start + $range_len] = true; for ($ii = $range_start; $ii <= $range_start + $range_len; $ii++) { if (isset($base_mask[$ii])) { if ($in_gap) { $gap_length = $ii - $gap_start; if ($gap_length <= $lines_context) { for ($jj = $gap_start; $jj <= $gap_start + $gap_length; $jj++) { $base_mask[$jj] = true; } } else { $gaps[] = array($gap_start, $gap_length); } $in_gap = false; } } else { if (!$in_gap) { $gap_start = $ii; $in_gap = true; } } } $gaps = array_reverse($gaps); $mask = $base_mask; // Time to calculate depth. // We need to go backwards to properly indent whitespace in this code: // // 0: class C { // 1: // 1: function f() { // 2: // 2: return; // 1: // 1: } // 0: // 0: } // $depths = array(); $last_depth = 0; $range_end = $range_start + $range_len; if (!isset($this->new[$range_end])) { $range_end--; } for ($ii = $range_end; $ii >= $range_start; $ii--) { // We need to expand tabs to process mixed indenting and to round // correctly later. $line = str_replace("\t", ' ', $this->new[$ii]['text']); $trimmed = ltrim($line); if ($trimmed != '') { // We round down to flatten "/**" and " *". $last_depth = floor((strlen($line) - strlen($trimmed)) / 2); } $depths[$ii] = $last_depth; } return array($gaps, $mask, $depths); } /** * Determine if an inline comment will appear on the rendered diff, * taking into consideration which halves of which changesets will actually * be shown. * * @param PhabricatorInlineCommentInterface Comment to test for visibility. * @return bool True if the comment is visible on the rendered diff. */ private function isCommentVisibleOnRenderedDiff( PhabricatorInlineCommentInterface $comment) { $changeset_id = $comment->getChangesetID(); $is_new = $comment->getIsNewFile(); if ($changeset_id == $this->rightSideChangesetID && $is_new == $this->rightSideAttachesToNewFile) { return true; } if ($changeset_id == $this->leftSideChangesetID && $is_new == $this->leftSideAttachesToNewFile) { return true; } return false; } /** * Determine if a comment will appear on the right side of the display diff. * Note that the comment must appear somewhere on the rendered changeset, as * per isCommentVisibleOnRenderedDiff(). * * @param PhabricatorInlineCommentInterface Comment to test for display * location. * @return bool True for right, false for left. */ private function isCommentOnRightSideWhenDisplayed( PhabricatorInlineCommentInterface $comment) { if (!$this->isCommentVisibleOnRenderedDiff($comment)) { throw new Exception(pht('Comment is not visible on changeset!')); } $changeset_id = $comment->getChangesetID(); $is_new = $comment->getIsNewFile(); if ($changeset_id == $this->rightSideChangesetID && $is_new == $this->rightSideAttachesToNewFile) { return true; } return false; } /** * Parse the 'range' specification that this class and the client-side JS * emit to indicate that a user clicked "Show more..." on a diff. Generally, * use is something like this: * * $spec = $request->getStr('range'); * $parsed = DifferentialChangesetParser::parseRangeSpecification($spec); * list($start, $end, $mask) = $parsed; * $parser->render($start, $end, $mask); * * @param string Range specification, indicating the range of the diff that * should be rendered. * @return tuple List of suitable for passing to * @{method:render}. */ public static function parseRangeSpecification($spec) { $range_s = null; $range_e = null; $mask = array(); if ($spec) { $match = null; if (preg_match('@^(\d+)-(\d+)(?:/(\d+)-(\d+))?$@', $spec, $match)) { $range_s = (int)$match[1]; $range_e = (int)$match[2]; if (count($match) > 3) { $start = (int)$match[3]; $len = (int)$match[4]; for ($ii = $start; $ii < $start + $len; $ii++) { $mask[$ii] = true; } } } } return array($range_s, $range_e, $mask); } /** * Render "modified coverage" information; test coverage on modified lines. * This synthesizes diff information with unit test information into a useful * indicator of how well tested a change is. */ public function renderModifiedCoverage() { $na = phutil_tag('em', array(), '-'); $coverage = $this->getCoverage(); if (!$coverage) { return $na; } $covered = 0; $not_covered = 0; foreach ($this->new as $k => $new) { if (!$new['line']) { continue; } if (!$new['type']) { continue; } if (empty($coverage[$new['line'] - 1])) { continue; } switch ($coverage[$new['line'] - 1]) { case 'C': $covered++; break; case 'U': $not_covered++; break; } } if (!$covered && !$not_covered) { return $na; } return sprintf('%d%%', 100 * ($covered / ($covered + $not_covered))); } /** * Build maps from lines comments appear on to actual lines. */ private function buildLineBackmaps() { $old_back = array(); $new_back = array(); foreach ($this->old as $ii => $old) { $old_back[$old['line']] = $old['line']; } foreach ($this->new as $ii => $new) { $new_back[$new['line']] = $new['line']; } $max_old_line = 0; $max_new_line = 0; foreach ($this->comments as $comment) { if ($this->isCommentOnRightSideWhenDisplayed($comment)) { $max_new_line = max($max_new_line, $comment->getLineNumber()); } else { $max_old_line = max($max_old_line, $comment->getLineNumber()); } } $cursor = 1; for ($ii = 1; $ii <= $max_old_line; $ii++) { if (empty($old_back[$ii])) { $old_back[$ii] = $cursor; } else { $cursor = $old_back[$ii]; } } $cursor = 1; for ($ii = 1; $ii <= $max_new_line; $ii++) { if (empty($new_back[$ii])) { $new_back[$ii] = $cursor; } else { $cursor = $new_back[$ii]; } } return array($old_back, $new_back); } private function getOffset(array $map, $line) { if (!$map) { return null; } $line = (int)$line; foreach ($map as $key => $spec) { if ($spec && isset($spec['line'])) { if ((int)$spec['line'] >= $line) { return $key; } } } return $key; } } diff --git a/src/applications/differential/query/DifferentialInlineCommentQuery.php b/src/applications/differential/query/DifferentialInlineCommentQuery.php index 3f8ea62e14..38549cd933 100644 --- a/src/applications/differential/query/DifferentialInlineCommentQuery.php +++ b/src/applications/differential/query/DifferentialInlineCommentQuery.php @@ -1,483 +1,483 @@ viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function withIDs(array $ids) { $this->ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withDrafts($drafts) { $this->drafts = $drafts; return $this; } public function withAuthorPHIDs(array $author_phids) { $this->authorPHIDs = $author_phids; return $this; } public function withRevisionPHIDs(array $revision_phids) { $this->revisionPHIDs = $revision_phids; return $this; } public function withDeletedDrafts($deleted_drafts) { $this->deletedDrafts = $deleted_drafts; return $this; } public function needHidden($need) { $this->needHidden = $need; return $this; } public function execute() { $table = new DifferentialTransactionComment(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildLimitClause($conn_r)); $comments = $table->loadAllFromArray($data); if ($this->needHidden) { $viewer_phid = $this->getViewer()->getPHID(); if ($viewer_phid && $comments) { $hidden = queryfx_all( $conn_r, 'SELECT commentID FROM %T WHERE userPHID = %s AND commentID IN (%Ls)', id(new DifferentialHiddenComment())->getTableName(), $viewer_phid, mpull($comments, 'getID')); $hidden = array_fuse(ipull($hidden, 'commentID')); } else { $hidden = array(); } foreach ($comments as $inline) { $inline->attachIsHidden(isset($hidden[$inline->getID()])); } } foreach ($comments as $key => $value) { $comments[$key] = DifferentialInlineComment::newFromModernComment( $value); } return $comments; } public function executeOne() { // TODO: Remove when this query moves to PolicyAware. return head($this->execute()); } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); // Only find inline comments. $where[] = qsprintf( - $conn_r, + $conn, 'changesetID IS NOT NULL'); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } if ($this->revisionPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'revisionPHID IN (%Ls)', $this->revisionPHIDs); } if ($this->drafts === null) { if ($this->deletedDrafts) { $where[] = qsprintf( - $conn_r, + $conn, '(authorPHID = %s) OR (transactionPHID IS NOT NULL)', $this->getViewer()->getPHID()); } else { $where[] = qsprintf( - $conn_r, + $conn, '(authorPHID = %s AND isDeleted = 0) OR (transactionPHID IS NOT NULL)', $this->getViewer()->getPHID()); } } else if ($this->drafts) { $where[] = qsprintf( - $conn_r, + $conn, '(authorPHID = %s AND isDeleted = 0) AND (transactionPHID IS NULL)', $this->getViewer()->getPHID()); } else { $where[] = qsprintf( - $conn_r, + $conn, 'transactionPHID IS NOT NULL'); } - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } public function adjustInlinesForChangesets( array $inlines, array $old, array $new, DifferentialRevision $revision) { assert_instances_of($inlines, 'DifferentialInlineComment'); assert_instances_of($old, 'DifferentialChangeset'); assert_instances_of($new, 'DifferentialChangeset'); $viewer = $this->getViewer(); $no_ghosts = $viewer->compareUserSetting( PhabricatorOlderInlinesSetting::SETTINGKEY, PhabricatorOlderInlinesSetting::VALUE_GHOST_INLINES_DISABLED); if ($no_ghosts) { return $inlines; } $all = array_merge($old, $new); $changeset_ids = mpull($inlines, 'getChangesetID'); $changeset_ids = array_unique($changeset_ids); $all_map = mpull($all, null, 'getID'); // We already have at least some changesets, and we might not need to do // any more data fetching. Remove everything we already have so we can // tell if we need new stuff. foreach ($changeset_ids as $key => $id) { if (isset($all_map[$id])) { unset($changeset_ids[$key]); } } if ($changeset_ids) { $changesets = id(new DifferentialChangesetQuery()) ->setViewer($viewer) ->withIDs($changeset_ids) ->execute(); $changesets = mpull($changesets, null, 'getID'); } else { $changesets = array(); } $changesets += $all_map; $id_map = array(); foreach ($all as $changeset) { $id_map[$changeset->getID()] = $changeset->getID(); } // Generate filename maps for older and newer comments. If we're bringing // an older comment forward in a diff-of-diffs, we want to put it on the // left side of the screen, not the right side. Both sides are "new" files // with the same name, so they're both appropriate targets, but the left // is a better target conceptually for users because it's more consistent // with the rest of the UI, which shows old information on the left and // new information on the right. $move_here = DifferentialChangeType::TYPE_MOVE_HERE; $name_map_old = array(); $name_map_new = array(); $move_map = array(); foreach ($all as $changeset) { $changeset_id = $changeset->getID(); $filenames = array(); $filenames[] = $changeset->getFilename(); // If this is the target of a move, also map comments on the old filename // to this changeset. if ($changeset->getChangeType() == $move_here) { $old_file = $changeset->getOldFile(); $filenames[] = $old_file; $move_map[$changeset_id][$old_file] = true; } foreach ($filenames as $filename) { // We update the old map only if we don't already have an entry (oldest // changeset persists). if (empty($name_map_old[$filename])) { $name_map_old[$filename] = $changeset_id; } // We always update the new map (newest changeset overwrites). $name_map_new[$changeset->getFilename()] = $changeset_id; } } // Find the smallest "new" changeset ID. We'll consider everything // larger than this to be "newer", and everything smaller to be "older". $first_new_id = min(mpull($new, 'getID')); $results = array(); foreach ($inlines as $inline) { $changeset_id = $inline->getChangesetID(); if (isset($id_map[$changeset_id])) { // This inline is legitimately on one of the current changesets, so // we can include it in the result set unmodified. $results[] = $inline; continue; } $changeset = idx($changesets, $changeset_id); if (!$changeset) { // Just discard this inline, as it has bogus data. continue; } $target_id = null; if ($changeset_id >= $first_new_id) { $name_map = $name_map_new; $is_new = true; } else { $name_map = $name_map_old; $is_new = false; } $filename = $changeset->getFilename(); if (isset($name_map[$filename])) { // This changeset is on a file with the same name as the current // changeset, so we're going to port it forward or backward. $target_id = $name_map[$filename]; $is_move = isset($move_map[$target_id][$filename]); if ($is_new) { if ($is_move) { $reason = pht( 'This comment was made on a file with the same name as the '. 'file this file was moved from, but in a newer diff.'); } else { $reason = pht( 'This comment was made on a file with the same name, but '. 'in a newer diff.'); } } else { if ($is_move) { $reason = pht( 'This comment was made on a file with the same name as the '. 'file this file was moved from, but in an older diff.'); } else { $reason = pht( 'This comment was made on a file with the same name, but '. 'in an older diff.'); } } } // If we didn't find a target and this change is the target of a move, // look for a match against the old filename. if (!$target_id) { if ($changeset->getChangeType() == $move_here) { $filename = $changeset->getOldFile(); if (isset($name_map[$filename])) { $target_id = $name_map[$filename]; if ($is_new) { $reason = pht( 'This comment was made on a file which this file was moved '. 'to, but in a newer diff.'); } else { $reason = pht( 'This comment was made on a file which this file was moved '. 'to, but in an older diff.'); } } } } // If we found a changeset to port this comment to, bring it forward // or backward and mark it. if ($target_id) { $diff_id = $changeset->getDiffID(); $inline_id = $inline->getID(); $revision_id = $revision->getID(); $href = "/D{$revision_id}?id={$diff_id}#inline-{$inline_id}"; $inline ->makeEphemeral(true) ->setChangesetID($target_id) ->setIsGhost( array( 'new' => $is_new, 'reason' => $reason, 'href' => $href, 'originalID' => $changeset->getID(), )); $results[] = $inline; } } // Filter out the inlines we ported forward which won't be visible because // they appear on the wrong side of a file. $keep_map = array(); foreach ($old as $changeset) { $keep_map[$changeset->getID()][0] = true; } foreach ($new as $changeset) { $keep_map[$changeset->getID()][1] = true; } foreach ($results as $key => $inline) { $is_new = (int)$inline->getIsNewFile(); $changeset_id = $inline->getChangesetID(); if (!isset($keep_map[$changeset_id][$is_new])) { unset($results[$key]); continue; } } // Adjust inline line numbers to account for content changes across // updates and rebases. $plan = array(); $need = array(); foreach ($results as $inline) { $ghost = $inline->getIsGhost(); if (!$ghost) { // If this isn't a "ghost" inline, ignore it. continue; } $src_id = $ghost['originalID']; $dst_id = $inline->getChangesetID(); $xforms = array(); // If the comment is on the right, transform it through the inverse map // back to the left. if ($inline->getIsNewFile()) { $xforms[] = array($src_id, $src_id, true); } // Transform it across rebases. $xforms[] = array($src_id, $dst_id, false); // If the comment is on the right, transform it back onto the right. if ($inline->getIsNewFile()) { $xforms[] = array($dst_id, $dst_id, false); } $key = array(); foreach ($xforms as $xform) { list($u, $v, $inverse) = $xform; $short = $u.'/'.$v; $need[$short] = array($u, $v); $part = $u.($inverse ? '<' : '>').$v; $key[] = $part; } $key = implode(',', $key); if (empty($plan[$key])) { $plan[$key] = array( 'xforms' => $xforms, 'inlines' => array(), ); } $plan[$key]['inlines'][] = $inline; } if ($need) { $maps = DifferentialLineAdjustmentMap::loadMaps($need); } else { $maps = array(); } foreach ($plan as $step) { $xforms = $step['xforms']; $chain = null; foreach ($xforms as $xform) { list($u, $v, $inverse) = $xform; $map = idx(idx($maps, $u, array()), $v); if (!$map) { continue 2; } if ($inverse) { $map = DifferentialLineAdjustmentMap::newInverseMap($map); } else { $map = clone $map; } if ($chain) { $chain->addMapToChain($map); } else { $chain = $map; } } foreach ($step['inlines'] as $inline) { $head_line = $inline->getLineNumber(); $tail_line = ($head_line + $inline->getLineLength()); $head_info = $chain->mapLine($head_line, false); $tail_info = $chain->mapLine($tail_line, true); list($head_deleted, $head_offset, $head_line) = $head_info; list($tail_deleted, $tail_offset, $tail_line) = $tail_info; if ($head_offset !== false) { $inline->setLineNumber($head_line + 1 + $head_offset); } else { $inline->setLineNumber($head_line); $inline->setLineLength($tail_line - $head_line); } } } return $results; } } diff --git a/src/applications/differential/query/DifferentialRevisionQuery.php b/src/applications/differential/query/DifferentialRevisionQuery.php index bc93de2db2..fdd4904bee 100644 --- a/src/applications/differential/query/DifferentialRevisionQuery.php +++ b/src/applications/differential/query/DifferentialRevisionQuery.php @@ -1,1014 +1,1029 @@ pathIDs[] = array( 'repositoryID' => $repository_id, 'pathID' => $path_id, ); return $this; } /** * Filter results to revisions authored by one of the given PHIDs. Calling * this function will clear anything set by previous calls to * @{method:withAuthors}. * * @param array List of PHIDs of authors * @return this * @task config */ public function withAuthors(array $author_phids) { $this->authors = $author_phids; return $this; } /** * Filter results to revisions which CC one of the listed people. Calling this * function will clear anything set by previous calls to @{method:withCCs}. * * @param array List of PHIDs of subscribers. * @return this * @task config */ public function withCCs(array $cc_phids) { $this->ccs = $cc_phids; return $this; } /** * Filter results to revisions that have one of the provided PHIDs as * reviewers. Calling this function will clear anything set by previous calls * to @{method:withReviewers}. * * @param array List of PHIDs of reviewers * @return this * @task config */ public function withReviewers(array $reviewer_phids) { $this->reviewers = $reviewer_phids; return $this; } /** * Filter results to revisions that have one of the provided commit hashes. * Calling this function will clear anything set by previous calls to * @{method:withCommitHashes}. * * @param array List of pairs * @return this * @task config */ public function withCommitHashes(array $commit_hashes) { $this->commitHashes = $commit_hashes; return $this; } /** * Filter results to revisions that have one of the provided PHIDs as * commits. Calling this function will clear anything set by previous calls * to @{method:withCommitPHIDs}. * * @param array List of PHIDs of commits * @return this * @task config */ public function withCommitPHIDs(array $commit_phids) { $this->commitPHIDs = $commit_phids; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } public function withIsOpen($is_open) { $this->isOpen = $is_open; return $this; } /** * Filter results to revisions on given branches. * * @param list List of branch names. * @return this * @task config */ public function withBranches(array $branches) { $this->branches = $branches; return $this; } /** * Filter results to only return revisions whose ids are in the given set. * * @param array List of revision ids * @return this * @task config */ public function withIDs(array $ids) { $this->revIDs = $ids; return $this; } /** * Filter results to only return revisions whose PHIDs are in the given set. * * @param array List of revision PHIDs * @return this * @task config */ public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } /** * Given a set of users, filter results to return only revisions they are * responsible for (i.e., they are either authors or reviewers). * * @param array List of user PHIDs. * @return this * @task config */ public function withResponsibleUsers(array $responsible_phids) { $this->responsibles = $responsible_phids; return $this; } public function withRepositoryPHIDs(array $repository_phids) { $this->repositoryPHIDs = $repository_phids; return $this; } public function withUpdatedEpochBetween($min, $max) { $this->updatedEpochMin = $min; $this->updatedEpochMax = $max; return $this; } public function withCreatedEpochBetween($min, $max) { $this->createdEpochMin = $min; $this->createdEpochMax = $max; return $this; } /** * Set whether or not the query should load the active diff for each * revision. * * @param bool True to load and attach diffs. * @return this * @task config */ public function needActiveDiffs($need_active_diffs) { $this->needActiveDiffs = $need_active_diffs; return $this; } /** * Set whether or not the query should load the associated commit PHIDs for * each revision. * * @param bool True to load and attach diffs. * @return this * @task config */ public function needCommitPHIDs($need_commit_phids) { $this->needCommitPHIDs = $need_commit_phids; return $this; } /** * Set whether or not the query should load associated diff IDs for each * revision. * * @param bool True to load and attach diff IDs. * @return this * @task config */ public function needDiffIDs($need_diff_ids) { $this->needDiffIDs = $need_diff_ids; return $this; } /** * Set whether or not the query should load associated commit hashes for each * revision. * * @param bool True to load and attach commit hashes. * @return this * @task config */ public function needHashes($need_hashes) { $this->needHashes = $need_hashes; return $this; } /** * Set whether or not the query should load associated reviewers. * * @param bool True to load and attach reviewers. * @return this * @task config */ public function needReviewers($need_reviewers) { $this->needReviewers = $need_reviewers; return $this; } /** * Request information about the viewer's authority to act on behalf of each * reviewer. In particular, they have authority to act on behalf of projects * they are a member of. * * @param bool True to load and attach authority. * @return this * @task config */ public function needReviewerAuthority($need_reviewer_authority) { $this->needReviewerAuthority = $need_reviewer_authority; return $this; } public function needFlags($need_flags) { $this->needFlags = $need_flags; return $this; } public function needDrafts($need_drafts) { $this->needDrafts = $need_drafts; return $this; } /* -( Query Execution )---------------------------------------------------- */ public function newResultObject() { return new DifferentialRevision(); } /** * Execute the query as configured, returning matching * @{class:DifferentialRevision} objects. * * @return list List of matching DifferentialRevision objects. * @task exec */ protected function loadPage() { $data = $this->loadData(); $data = $this->didLoadRawRows($data); $table = $this->newResultObject(); return $table->loadAllFromArray($data); } protected function willFilterPage(array $revisions) { $viewer = $this->getViewer(); $repository_phids = mpull($revisions, 'getRepositoryPHID'); $repository_phids = array_filter($repository_phids); $repositories = array(); if ($repository_phids) { $repositories = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()) ->withPHIDs($repository_phids) ->execute(); $repositories = mpull($repositories, null, 'getPHID'); } // If a revision is associated with a repository: // // - the viewer must be able to see the repository; or // - the viewer must have an automatic view capability. // // In the latter case, we'll load the revision but not load the repository. $can_view = PhabricatorPolicyCapability::CAN_VIEW; foreach ($revisions as $key => $revision) { $repo_phid = $revision->getRepositoryPHID(); if (!$repo_phid) { // The revision has no associated repository. Attach `null` and move on. $revision->attachRepository(null); continue; } $repository = idx($repositories, $repo_phid); if ($repository) { // The revision has an associated repository, and the viewer can see // it. Attach it and move on. $revision->attachRepository($repository); continue; } if ($revision->hasAutomaticCapability($can_view, $viewer)) { // The revision has an associated repository which the viewer can not // see, but the viewer has an automatic capability on this revision. // Load the revision without attaching a repository. $revision->attachRepository(null); continue; } if ($this->getViewer()->isOmnipotent()) { // The viewer is omnipotent. Allow the revision to load even without // a repository. $revision->attachRepository(null); continue; } // The revision has an associated repository, and the viewer can't see // it, and the viewer has no special capabilities. Filter out this // revision. $this->didRejectResult($revision); unset($revisions[$key]); } if (!$revisions) { return array(); } $table = new DifferentialRevision(); $conn_r = $table->establishConnection('r'); if ($this->needCommitPHIDs) { $this->loadCommitPHIDs($conn_r, $revisions); } $need_active = $this->needActiveDiffs; $need_ids = $need_active || $this->needDiffIDs; if ($need_ids) { $this->loadDiffIDs($conn_r, $revisions); } if ($need_active) { $this->loadActiveDiffs($conn_r, $revisions); } if ($this->needHashes) { $this->loadHashes($conn_r, $revisions); } if ($this->needReviewers || $this->needReviewerAuthority) { $this->loadReviewers($conn_r, $revisions); } return $revisions; } protected function didFilterPage(array $revisions) { $viewer = $this->getViewer(); if ($this->needFlags) { $flags = id(new PhabricatorFlagQuery()) ->setViewer($viewer) ->withOwnerPHIDs(array($viewer->getPHID())) ->withObjectPHIDs(mpull($revisions, 'getPHID')) ->execute(); $flags = mpull($flags, null, 'getObjectPHID'); foreach ($revisions as $revision) { $revision->attachFlag( $viewer, idx($flags, $revision->getPHID())); } } if ($this->needDrafts) { PhabricatorDraftEngine::attachDrafts( $viewer, $revisions); } return $revisions; } private function loadData() { $table = $this->newResultObject(); - $conn_r = $table->establishConnection('r'); + $conn = $table->establishConnection('r'); $selects = array(); // NOTE: If the query includes "responsiblePHIDs", we execute it as a // UNION of revisions they own and revisions they're reviewing. This has // much better performance than doing it with JOIN/WHERE. if ($this->responsibles) { $basic_authors = $this->authors; $basic_reviewers = $this->reviewers; try { // Build the query where the responsible users are authors. $this->authors = array_merge($basic_authors, $this->responsibles); $this->reviewers = $basic_reviewers; - $selects[] = $this->buildSelectStatement($conn_r); + $selects[] = $this->buildSelectStatement($conn); // Build the query where the responsible users are reviewers, or // projects they are members of are reviewers. $this->authors = $basic_authors; $this->reviewers = array_merge($basic_reviewers, $this->responsibles); - $selects[] = $this->buildSelectStatement($conn_r); + $selects[] = $this->buildSelectStatement($conn); // Put everything back like it was. $this->authors = $basic_authors; $this->reviewers = $basic_reviewers; } catch (Exception $ex) { $this->authors = $basic_authors; $this->reviewers = $basic_reviewers; throw $ex; } } else { - $selects[] = $this->buildSelectStatement($conn_r); + $selects[] = $this->buildSelectStatement($conn); } if (count($selects) > 1) { + $unions = null; + foreach ($selects as $select) { + if (!$unions) { + $unions = $select; + continue; + } + + $unions = qsprintf( + $conn, + '%Q UNION DISTINCT %Q', + $unions, + $select); + } + $query = qsprintf( - $conn_r, + $conn, '%Q %Q %Q', - implode(' UNION DISTINCT ', $selects), - $this->buildOrderClause($conn_r, true), - $this->buildLimitClause($conn_r)); + $unions, + $this->buildOrderClause($conn, true), + $this->buildLimitClause($conn)); } else { $query = head($selects); } - return queryfx_all($conn_r, '%Q', $query); + return queryfx_all($conn, '%Q', $query); } private function buildSelectStatement(AphrontDatabaseConnection $conn_r) { $table = new DifferentialRevision(); $select = $this->buildSelectClause($conn_r); $from = qsprintf( $conn_r, 'FROM %T r', $table->getTableName()); $joins = $this->buildJoinsClause($conn_r); $where = $this->buildWhereClause($conn_r); $group_by = $this->buildGroupClause($conn_r); $having = $this->buildHavingClause($conn_r); $order_by = $this->buildOrderClause($conn_r); $limit = $this->buildLimitClause($conn_r); return qsprintf( $conn_r, '(%Q %Q %Q %Q %Q %Q %Q %Q)', $select, $from, $joins, $where, $group_by, $having, $order_by, $limit); } /* -( Internals )---------------------------------------------------------- */ /** * @task internal */ - private function buildJoinsClause($conn_r) { + private function buildJoinsClause(AphrontDatabaseConnection $conn) { $joins = array(); if ($this->pathIDs) { $path_table = new DifferentialAffectedPath(); $joins[] = qsprintf( - $conn_r, + $conn, 'JOIN %T p ON p.revisionID = r.id', $path_table->getTableName()); } if ($this->commitHashes) { $joins[] = qsprintf( - $conn_r, + $conn, 'JOIN %T hash_rel ON hash_rel.revisionID = r.id', ArcanistDifferentialRevisionHash::TABLE_NAME); } if ($this->ccs) { $joins[] = qsprintf( - $conn_r, + $conn, 'JOIN %T e_ccs ON e_ccs.src = r.phid '. 'AND e_ccs.type = %s '. 'AND e_ccs.dst in (%Ls)', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorObjectHasSubscriberEdgeType::EDGECONST, $this->ccs); } if ($this->reviewers) { $joins[] = qsprintf( - $conn_r, + $conn, '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); } if ($this->draftAuthors) { $joins[] = qsprintf( - $conn_r, + $conn, 'JOIN %T has_draft ON has_draft.srcPHID = r.phid AND has_draft.type = %s AND has_draft.dstPHID IN (%Ls)', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorObjectHasDraftEdgeType::EDGECONST, $this->draftAuthors); } if ($this->commitPHIDs) { $joins[] = qsprintf( - $conn_r, + $conn, 'JOIN %T commits ON commits.revisionID = r.id', DifferentialRevision::TABLE_COMMIT); } - $joins[] = $this->buildJoinClauseParts($conn_r); + $joins[] = $this->buildJoinClauseParts($conn); - return $this->formatJoinClause($joins); + return $this->formatJoinClause($conn, $joins); } /** * @task internal */ - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->pathIDs) { $path_clauses = array(); $repo_info = igroup($this->pathIDs, 'repositoryID'); foreach ($repo_info as $repository_id => $paths) { $path_clauses[] = qsprintf( - $conn_r, + $conn, '(p.repositoryID = %d AND p.pathID IN (%Ld))', $repository_id, ipull($paths, 'pathID')); } - $path_clauses = '('.implode(' OR ', $path_clauses).')'; + $path_clauses = qsprintf($conn, '%LO', $path_clauses); $where[] = $path_clauses; } if ($this->authors) { $where[] = qsprintf( - $conn_r, + $conn, 'r.authorPHID IN (%Ls)', $this->authors); } if ($this->revIDs) { $where[] = qsprintf( - $conn_r, + $conn, 'r.id IN (%Ld)', $this->revIDs); } if ($this->repositoryPHIDs) { $where[] = qsprintf( - $conn_r, + $conn, 'r.repositoryPHID IN (%Ls)', $this->repositoryPHIDs); } if ($this->commitHashes) { $hash_clauses = array(); foreach ($this->commitHashes as $info) { list($type, $hash) = $info; $hash_clauses[] = qsprintf( - $conn_r, + $conn, '(hash_rel.type = %s AND hash_rel.hash = %s)', $type, $hash); } - $hash_clauses = '('.implode(' OR ', $hash_clauses).')'; + $hash_clauses = qsprintf($conn, '%LO', $hash_clauses); $where[] = $hash_clauses; } if ($this->commitPHIDs) { $where[] = qsprintf( - $conn_r, + $conn, 'commits.commitPHID IN (%Ls)', $this->commitPHIDs); } if ($this->phids) { $where[] = qsprintf( - $conn_r, + $conn, 'r.phid IN (%Ls)', $this->phids); } if ($this->branches) { $where[] = qsprintf( - $conn_r, + $conn, 'r.branchName in (%Ls)', $this->branches); } if ($this->updatedEpochMin !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'r.dateModified >= %d', $this->updatedEpochMin); } if ($this->updatedEpochMax !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'r.dateModified <= %d', $this->updatedEpochMax); } if ($this->createdEpochMin !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'r.dateCreated >= %d', $this->createdEpochMin); } if ($this->createdEpochMax !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'r.dateCreated <= %d', $this->createdEpochMax); } if ($this->statuses !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'r.status in (%Ls)', $this->statuses); } if ($this->isOpen !== null) { if ($this->isOpen) { $statuses = DifferentialLegacyQuery::getModernValues( DifferentialLegacyQuery::STATUS_OPEN); } else { $statuses = DifferentialLegacyQuery::getModernValues( DifferentialLegacyQuery::STATUS_CLOSED); } $where[] = qsprintf( - $conn_r, + $conn, 'r.status in (%Ls)', $statuses); } - $where[] = $this->buildWhereClauseParts($conn_r); - return $this->formatWhereClause($where); + $where[] = $this->buildWhereClauseParts($conn); + + return $this->formatWhereClause($conn, $where); } /** * @task internal */ protected function shouldGroupQueryResultRows() { $join_triggers = array_merge( $this->pathIDs, $this->ccs, $this->reviewers); if (count($join_triggers) > 1) { return true; } return parent::shouldGroupQueryResultRows(); } public function getBuiltinOrders() { $orders = parent::getBuiltinOrders() + array( 'updated' => array( 'vector' => array('updated', 'id'), 'name' => pht('Date Updated (Latest First)'), 'aliases' => array(self::ORDER_MODIFIED), ), 'outdated' => array( 'vector' => array('-updated', '-id'), 'name' => pht('Date Updated (Oldest First)'), ), ); // Alias the "newest" builtin to the historical key for it. $orders['newest']['aliases'][] = self::ORDER_CREATED; return $orders; } protected function getDefaultOrderVector() { return array('updated', 'id'); } public function getOrderableColumns() { return array( 'updated' => array( 'table' => $this->getPrimaryTableAlias(), 'column' => 'dateModified', 'type' => 'int', ), ) + parent::getOrderableColumns(); } protected function getPagingValueMap($cursor, array $keys) { $revision = $this->loadCursorObject($cursor); return array( 'id' => $revision->getID(), 'updated' => $revision->getDateModified(), ); } private function loadCommitPHIDs($conn_r, array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $commit_phids = queryfx_all( $conn_r, 'SELECT * FROM %T WHERE revisionID IN (%Ld)', DifferentialRevision::TABLE_COMMIT, mpull($revisions, 'getID')); $commit_phids = igroup($commit_phids, 'revisionID'); foreach ($revisions as $revision) { $phids = idx($commit_phids, $revision->getID(), array()); $phids = ipull($phids, 'commitPHID'); $revision->attachCommitPHIDs($phids); } } private function loadDiffIDs($conn_r, array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $diff_table = new DifferentialDiff(); $diff_ids = queryfx_all( $conn_r, 'SELECT revisionID, id FROM %T WHERE revisionID IN (%Ld) ORDER BY id DESC', $diff_table->getTableName(), mpull($revisions, 'getID')); $diff_ids = igroup($diff_ids, 'revisionID'); foreach ($revisions as $revision) { $ids = idx($diff_ids, $revision->getID(), array()); $ids = ipull($ids, 'id'); $revision->attachDiffIDs($ids); } } private function loadActiveDiffs($conn_r, array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $diff_table = new DifferentialDiff(); $load_ids = array(); foreach ($revisions as $revision) { $diffs = $revision->getDiffIDs(); if ($diffs) { $load_ids[] = max($diffs); } } $active_diffs = array(); if ($load_ids) { $active_diffs = $diff_table->loadAllWhere( 'id IN (%Ld)', $load_ids); } $active_diffs = mpull($active_diffs, null, 'getRevisionID'); foreach ($revisions as $revision) { $revision->attachActiveDiff(idx($active_diffs, $revision->getID())); } } private function loadHashes( AphrontDatabaseConnection $conn_r, array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T WHERE revisionID IN (%Ld)', 'differential_revisionhash', mpull($revisions, 'getID')); $data = igroup($data, 'revisionID'); foreach ($revisions as $revision) { $hashes = idx($data, $revision->getID(), array()); $list = array(); foreach ($hashes as $hash) { $list[] = array($hash['type'], $hash['hash']); } $revision->attachHashes($list); } } private function loadReviewers( AphrontDatabaseConnection $conn, array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $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); // Figure out which of these reviewers the viewer has authority to act as. if ($this->needReviewerAuthority && $viewer_phid) { $authority = $this->loadReviewerAuthority( $revisions, $reviewer_map, $allow_self); } foreach ($revisions as $revision) { $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. $has_authority = false; } else if ((!$allow_self) && ($revision->getAuthorPHID() == $viewer_phid)) { // The author can never have authority unless we allow self-accept. $has_authority = false; } else { // Otherwise, look up whether the viewer has authority. $has_authority = isset($authority[$reviewer_phid]); } $reviewer->attachAuthority($viewer, $has_authority); } $reviewers[$reviewer_phid] = $reviewer; } $revision->attachReviewers($reviewers); } } private function loadReviewerAuthority( array $revisions, array $reviewers, $allow_self) { $revision_map = mpull($revisions, null, 'getPHID'); $viewer_phid = $this->getViewer()->getPHID(); // Find all the project/package reviewers which the user may have authority // over. $project_phids = array(); $package_phids = array(); $project_type = PhabricatorProjectProjectPHIDType::TYPECONST; $package_type = PhabricatorOwnersPackagePHIDType::TYPECONST; foreach ($reviewers as $revision_phid => $reviewer_list) { if (!$allow_self) { 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 // data about these reviewers. continue; } } foreach ($reviewer_list as $reviewer_phid => $reviewer) { $phid_type = phid_get_type($reviewer_phid); if ($phid_type == $project_type) { $project_phids[] = $reviewer_phid; } if ($phid_type == $package_type) { $package_phids[] = $reviewer_phid; } } } // The viewer has authority over themselves. $user_authority = array_fuse(array($viewer_phid)); // And over any projects they are a member of. $project_authority = array(); if ($project_phids) { $project_authority = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs($project_phids) ->withMemberPHIDs(array($viewer_phid)) ->execute(); $project_authority = mpull($project_authority, 'getPHID'); $project_authority = array_fuse($project_authority); } // And over any packages they own. $package_authority = array(); if ($package_phids) { $package_authority = id(new PhabricatorOwnersPackageQuery()) ->setViewer($this->getViewer()) ->withPHIDs($package_phids) ->withAuthorityPHIDs(array($viewer_phid)) ->execute(); $package_authority = mpull($package_authority, 'getPHID'); $package_authority = array_fuse($package_authority); } return $user_authority + $project_authority + $package_authority; } public function getQueryApplicationClass() { return 'PhabricatorDifferentialApplication'; } protected function getPrimaryTableAlias() { return 'r'; } } diff --git a/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php b/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php index cf2b557764..99dc8bebc6 100644 --- a/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php @@ -1,194 +1,202 @@ getPhobjectClassConstant('ACTIONKEY', 32); } public function isActionAvailable($object, PhabricatorUser $viewer) { try { $this->validateAction($object, $viewer); return true; } catch (Exception $ex) { return false; } } 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; } public function getCommandAliases() { return array(); } public function getCommandSummary() { return null; } protected function getRevisionActionOrder() { return 1000; } public function getActionStrength() { return 3; } public function getRevisionActionOrderVector() { return id(new PhutilSortVector()) ->addInt($this->getRevisionActionOrder()); } protected function getRevisionActionGroupKey() { return DifferentialRevisionEditEngine::ACTIONGROUP_REVISION; } protected function getRevisionActionDescription( DifferentialRevision $revision) { return null; } + protected function getRevisionActionSubmitButtonText( + DifferentialRevision $revision) { + return null; + } + public static function loadAllActions() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setUniqueMethod('getRevisionActionKey') ->execute(); } protected function isViewerRevisionAuthor( DifferentialRevision $revision, PhabricatorUser $viewer) { if (!$viewer->getPHID()) { return false; } return ($viewer->getPHID() === $revision->getAuthorPHID()); } protected function getActionOptions( PhabricatorUser $viewer, DifferentialRevision $revision) { return array( array(), array(), ); } public function newEditField( DifferentialRevision $revision, PhabricatorUser $viewer) { // Actions in the "review" group, like "Accept Revision", do not require // that the actor be able to edit the revision. $group_review = DifferentialRevisionEditEngine::ACTIONGROUP_REVIEW; $is_review = ($this->getRevisionActionGroupKey() == $group_review); $field = id(new PhabricatorApplyEditField()) ->setKey($this->getRevisionActionKey()) ->setTransactionType($this->getTransactionTypeConstant()) ->setCanApplyWithoutEditCapability($is_review) ->setValue(true); if ($this->isActionAvailable($revision, $viewer)) { $label = $this->getRevisionActionLabel(); if ($label !== null) { $field->setCommentActionLabel($label); $description = $this->getRevisionActionDescription($revision); $field->setActionDescription($description); $group_key = $this->getRevisionActionGroupKey(); $field->setCommentActionGroupKey($group_key); + $button_text = $this->getRevisionActionSubmitButtonText($revision); + $field->setActionSubmitButtonText($button_text); + // Currently, every revision action conflicts with every other // revision action: for example, you can not simultaneously Accept and // Reject a revision. // Under some configurations, some combinations of actions are sort of // technically permissible. For example, you could reasonably Reject // and Abandon a revision if "anyone can abandon anything" is enabled. // 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); // 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, // or can accept on behalf of a reviewer other than themselves (see // T12533). $can_multi = (count($options) > 1); $can_force = (count($value) < count($options)); $not_self = (head_key($options) != $viewer->getPHID()); if ($can_multi || $can_force || $not_self) { $field->setOptions($options); $field->setValue($value); } } } return $field; } public function validateTransactions($object, array $xactions) { $errors = array(); $actor = $this->getActor(); $action_exception = null; foreach ($xactions as $xaction) { // If this is a draft demotion action, let it skip all the normal // validation. This is a little hacky and should perhaps move down // into the actual action implementations, but currently we can not // apply this rule in validateAction() because it doesn't operate on // the actual transaction. if ($xaction->getMetadataValue('draft.demote')) { continue; } try { $this->validateAction($object, $actor); } catch (Exception $ex) { $action_exception = $ex; } break; } foreach ($xactions as $xaction) { if ($action_exception) { $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); } } return $errors; } } diff --git a/src/applications/differential/xaction/DifferentialRevisionCommandeerTransaction.php b/src/applications/differential/xaction/DifferentialRevisionCommandeerTransaction.php index 5aafa536f3..a296597bc7 100644 --- a/src/applications/differential/xaction/DifferentialRevisionCommandeerTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionCommandeerTransaction.php @@ -1,98 +1,108 @@ getAuthorPHID(); } public function generateNewValue($object, $value) { $actor = $this->getActor(); return $actor->getPHID(); } public function applyInternalEffects($object, $value) { $object->setAuthorPHID($value); } protected function validateAction($object, PhabricatorUser $viewer) { - if ($object->isClosed()) { + // If a revision has already landed, we generally want to discourage + // reopening and reusing it since this tends to create a big mess (users + // should create a new revision instead). Thus, we stop you from + // commandeering closed revisions. + + // See PHI985. If the revision was abandoned, there's no peril in allowing + // the commandeer since the change (likely) never actually landed. So + // it's okay to commandeer abandoned revisions. + + if ($object->isClosed() && !$object->isAbandoned()) { throw new Exception( pht( 'You can not commandeer this revision because it has already '. - 'been closed. You can only commandeer open revisions.')); + 'been closed. You can only commandeer open or abandoned '. + 'revisions.')); } if ($this->isViewerRevisionAuthor($object, $viewer)) { throw new Exception( pht( 'You can not commandeer this revision because you are already '. 'the author.')); } } public function getTitle() { return pht( '%s commandeered this revision.', $this->renderAuthor()); } public function getTitleForFeed() { return pht( '%s commandeered %s.', $this->renderAuthor(), $this->renderObject()); } public function getTransactionTypeForConduit($xaction) { return 'commandeer'; } public function getFieldValuesForConduit($object, $data) { return array(); } } diff --git a/src/applications/differential/xaction/DifferentialRevisionRequestReviewTransaction.php b/src/applications/differential/xaction/DifferentialRevisionRequestReviewTransaction.php index a3d2699c74..169e41dec5 100644 --- a/src/applications/differential/xaction/DifferentialRevisionRequestReviewTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionRequestReviewTransaction.php @@ -1,95 +1,108 @@ isDraft()) { return pht('This revision will be submitted to reviewers for feedback.'); } else { return pht('This revision will be returned to reviewers for feedback.'); } } + protected function getRevisionActionSubmitButtonText( + DifferentialRevision $revision) { + + // See PHI975. When the action stack will promote the revision out of + // draft, change the button text from "Submit Quietly". + if ($revision->isDraft()) { + return pht('Publish Revision'); + } + + return null; + } + + public function getColor() { return 'sky'; } protected function getRevisionActionOrder() { return 200; } public function getActionName() { return pht('Requested Review'); } public function generateOldValue($object) { return $object->isNeedsReview(); } public function applyInternalEffects($object, $value) { $status_review = DifferentialRevisionStatus::NEEDS_REVIEW; $object ->setModernRevisionStatus($status_review) ->setShouldBroadcast(true); } protected function validateAction($object, PhabricatorUser $viewer) { if ($object->isNeedsReview()) { throw new Exception( pht( 'You can not request review of this revision because this '. 'revision is already under review and the action would have '. 'no effect.')); } if ($object->isClosed()) { throw new Exception( pht( 'You can not request review of this revision because it has '. 'already been closed. You can only request review of open '. 'revisions.')); } // When revisions automatically promote out of "Draft" after builds finish, // the viewer may be acting as the Harbormaster application. if (!$viewer->isOmnipotent()) { if (!$this->isViewerRevisionAuthor($object, $viewer)) { throw new Exception( pht( 'You can not request review of this revision because you are not '. 'the author of the revision.')); } } } public function getTitle() { return pht( '%s requested review of this revision.', $this->renderAuthor()); } public function getTitleForFeed() { return pht( '%s requested review of %s.', $this->renderAuthor(), $this->renderObject()); } public function getTransactionTypeForConduit($xaction) { return 'request-review'; } public function getFieldValuesForConduit($object, $data) { return array(); } } diff --git a/src/applications/differential/xaction/DifferentialRevisionUpdateTransaction.php b/src/applications/differential/xaction/DifferentialRevisionUpdateTransaction.php index bf1e6de870..33bbaceb7e 100644 --- a/src/applications/differential/xaction/DifferentialRevisionUpdateTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionUpdateTransaction.php @@ -1,232 +1,238 @@ getActiveDiffPHID(); } public function applyInternalEffects($object, $value) { $should_review = $this->shouldRequestReviewAfterUpdate($object); if ($should_review) { // If we're updating a non-broadcasting revision, put it back in draft // rather than moving it directly to "Needs Review". if ($object->getShouldBroadcast()) { $new_status = DifferentialRevisionStatus::NEEDS_REVIEW; } else { $new_status = DifferentialRevisionStatus::DRAFT; } $object->setModernRevisionStatus($new_status); } $editor = $this->getEditor(); $diff = $editor->requireDiff($value); $this->updateRevisionLineCounts($object, $diff); $object->setRepositoryPHID($diff->getRepositoryPHID()); $object->setActiveDiffPHID($diff->getPHID()); $object->attachActiveDiff($diff); } private function shouldRequestReviewAfterUpdate($object) { if ($this->isCommitUpdate()) { return false; } $should_update = $object->isNeedsRevision() || $object->isChangePlanned() || $object->isAbandoned(); if ($should_update) { return true; } return false; } public function applyExternalEffects($object, $value) { $editor = $this->getEditor(); $diff = $editor->requireDiff($value); // TODO: This can race with diff updates, particularly those from // Harbormaster. See discussion in T8650. $diff->setRevisionID($object->getID()); $diff->save(); + } + + public function didCommitTransaction($object, $value) { + $editor = $this->getEditor(); + $diff = $editor->requireDiff($value); + $omnipotent = PhabricatorUser::getOmnipotentUser(); // If there are any outstanding buildables for this diff, tell // Harbormaster that their containers need to be updated. This is // common, because `arc` creates buildables so it can upload lint // and unit results. $buildables = id(new HarbormasterBuildableQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->setViewer($omnipotent) ->withManualBuildables(false) ->withBuildablePHIDs(array($diff->getPHID())) ->execute(); foreach ($buildables as $buildable) { $buildable->sendMessage( $this->getActor(), HarbormasterMessageType::BUILDABLE_CONTAINER, true); } } public function getColor() { return 'sky'; } public function getIcon() { return 'fa-refresh'; } public function getActionName() { if ($this->isCreateTransaction()) { return pht('Request'); } else { return pht('Updated'); } } public function getActionStrength() { return 2; } public function getTitle() { $old = $this->getOldValue(); $new = $this->getNewValue(); if ($this->isCommitUpdate()) { return pht( 'This revision was automatically updated to reflect the '. 'committed changes.'); } // NOTE: Very, very old update transactions did not have a new value or // did not use a diff PHID as a new value. This was changed years ago, // but wasn't migrated. We might consider migrating if this causes issues. return pht( '%s updated this revision to %s.', $this->renderAuthor(), $this->renderNewHandle()); } public function getTitleForFeed() { return pht( '%s updated the diff for %s.', $this->renderAuthor(), $this->renderObject()); } public function validateTransactions($object, array $xactions) { $errors = array(); $diff_phid = null; foreach ($xactions as $xaction) { $diff_phid = $xaction->getNewValue(); $diff = id(new DifferentialDiffQuery()) ->withPHIDs(array($diff_phid)) ->setViewer($this->getActor()) ->executeOne(); if (!$diff) { $errors[] = $this->newInvalidError( pht( 'Specified diff ("%s") does not exist.', $diff_phid), $xaction); continue; } $is_attached = ($diff->getRevisionID()) && ($diff->getRevisionID() == $object->getID()); if ($is_attached) { $is_active = ($diff_phid == $object->getActiveDiffPHID()); } else { $is_active = false; } if ($is_attached) { if ($is_active) { // This is a no-op: we're reattaching the current active diff to the // revision it is already attached to. This is valid and will just // be dropped later on in the process. } else { // At least for now, there's no support for "undoing" a diff and // reverting to an older proposed change without just creating a // new diff from whole cloth. $errors[] = $this->newInvalidError( pht( 'You can not update this revision with the specified diff '. '("%s") because this diff is already attached to the revision '. 'as an older version of the change.', $diff_phid), $xaction); continue; } } else if ($diff->getRevisionID()) { $errors[] = $this->newInvalidError( pht( 'You can not update this revision with the specified diff ("%s") '. 'because the diff is already attached to another revision.', $diff_phid), $xaction); continue; } } if (!$diff_phid && !$object->getActiveDiffPHID()) { $errors[] = $this->newInvalidError( pht( 'You must specify an initial diff when creating a revision.')); } return $errors; } public function isCommitUpdate() { return (bool)$this->getMetadataValue('isCommitUpdate'); } private function updateRevisionLineCounts( DifferentialRevision $revision, DifferentialDiff $diff) { $revision->setLineCount($diff->getLineCount()); $conn = $revision->establishConnection('r'); $row = queryfx_one( $conn, 'SELECT SUM(addLines) A, SUM(delLines) D FROM %T WHERE diffID = %d', id(new DifferentialChangeset())->getTableName(), $diff->getID()); if ($row) { $revision->setAddedLineCount((int)$row['A']); $revision->setRemovedLineCount((int)$row['D']); } } public function getTransactionTypeForConduit($xaction) { return 'update'; } public function getFieldValuesForConduit($object, $data) { $commit_phids = $object->getMetadataValue('commitPHIDs', array()); return array( 'old' => $object->getOldValue(), 'new' => $object->getNewValue(), 'commitPHIDs' => $commit_phids, ); } } diff --git a/src/applications/diffusion/DiffusionLintSaveRunner.php b/src/applications/diffusion/DiffusionLintSaveRunner.php index 980234f95d..a1a2fe7d32 100644 --- a/src/applications/diffusion/DiffusionLintSaveRunner.php +++ b/src/applications/diffusion/DiffusionLintSaveRunner.php @@ -1,308 +1,308 @@ arc = $path; return $this; } public function setSeverity($string) { $this->severity = $string; return $this; } public function setAll($bool) { $this->all = $bool; return $this; } public function setChunkSize($number) { $this->chunkSize = $number; return $this; } public function setNeedsBlame($boolean) { $this->needsBlame = $boolean; return $this; } public function run($dir) { $working_copy = ArcanistWorkingCopyIdentity::newFromPath($dir); $configuration_manager = new ArcanistConfigurationManager(); $configuration_manager->setWorkingCopyIdentity($working_copy); $api = ArcanistRepositoryAPI::newAPIFromConfigurationManager( $configuration_manager); $this->svnRoot = id(new PhutilURI($api->getSourceControlPath()))->getPath(); if ($api instanceof ArcanistGitAPI) { $svn_fetch = $api->getGitConfig('svn-remote.svn.fetch'); list($this->svnRoot) = explode(':', $svn_fetch); if ($this->svnRoot != '') { $this->svnRoot = '/'.$this->svnRoot; } } $callsign = $configuration_manager->getConfigFromAnySource( 'repository.callsign'); $uuid = $api->getRepositoryUUID(); $remote_uri = $api->getRemoteURI(); $repository_query = id(new PhabricatorRepositoryQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()); if ($callsign) { $repository_query->withCallsigns(array($callsign)); } else if ($uuid) { $repository_query->withUUIDs(array($uuid)); } else if ($remote_uri) { $repository_query->withURIs(array($remote_uri)); } $repository = $repository_query->executeOne(); $branch_name = $api->getBranchName(); if (!$repository) { throw new Exception(pht('No repository was found.')); } $this->branch = PhabricatorRepositoryBranch::loadOrCreateBranch( $repository->getID(), $branch_name); $this->conn = $this->branch->establishConnection('w'); $this->lintCommit = null; if (!$this->all) { $this->lintCommit = $this->branch->getLintCommit(); } if ($this->lintCommit) { try { $commit = $this->lintCommit; if ($this->svnRoot) { $commit = $api->getCanonicalRevisionName('@'.$commit); } $all_files = $api->getChangedFiles($commit); } catch (ArcanistCapabilityNotSupportedException $ex) { $this->lintCommit = null; } } if (!$this->lintCommit) { $where = ($this->svnRoot ? qsprintf($this->conn, 'AND path LIKE %>', $this->svnRoot.'/') : ''); queryfx( $this->conn, 'DELETE FROM %T WHERE branchID = %d %Q', PhabricatorRepository::TABLE_LINTMESSAGE, $this->branch->getID(), $where); $all_files = $api->getAllFiles(); } $count = 0; $files = array(); foreach ($all_files as $file => $val) { $count++; if (!$this->lintCommit) { $file = $val; } else { $this->deletes[] = $this->svnRoot.'/'.$file; if ($val & ArcanistRepositoryAPI::FLAG_DELETED) { continue; } } $files[$file] = $file; if (count($files) >= $this->chunkSize) { $this->runArcLint($files); $files = array(); } } $this->runArcLint($files); $this->saveLintMessages(); $this->lintCommit = $api->getUnderlyingWorkingCopyRevision(); $this->branch->setLintCommit($this->lintCommit); $this->branch->save(); if ($this->blame) { $this->blameAuthors(); $this->blame = array(); } return $count; } private function runArcLint(array $files) { if (!$files) { return; } echo '.'; try { $future = new ExecFuture( '%C lint --severity %s --output json %Ls', $this->arc, $this->severity, $files); foreach (new LinesOfALargeExecFuture($future) as $json) { $paths = null; try { $paths = phutil_json_decode($json); } catch (PhutilJSONParserException $ex) { fprintf(STDERR, pht('Invalid JSON: %s', $json)."\n"); continue; } foreach ($paths as $path => $messages) { if (!isset($files[$path])) { continue; } foreach ($messages as $message) { $line = idx($message, 'line', 0); $this->inserts[] = qsprintf( $this->conn, '(%d, %s, %d, %s, %s, %s, %s)', $this->branch->getID(), $this->svnRoot.'/'.$path, $line, idx($message, 'code', ''), idx($message, 'severity', ''), idx($message, 'name', ''), idx($message, 'description', '')); if ($line && $this->needsBlame) { $this->blame[$path][$line] = true; } } if (count($this->deletes) >= 1024 || count($this->inserts) >= 256) { $this->saveLintMessages(); } } } } catch (Exception $ex) { fprintf(STDERR, $ex->getMessage()."\n"); } } private function saveLintMessages() { $this->conn->openTransaction(); foreach (array_chunk($this->deletes, 1024) as $paths) { queryfx( $this->conn, 'DELETE FROM %T WHERE branchID = %d AND path IN (%Ls)', PhabricatorRepository::TABLE_LINTMESSAGE, $this->branch->getID(), $paths); } foreach (array_chunk($this->inserts, 256) as $values) { queryfx( $this->conn, 'INSERT INTO %T (branchID, path, line, code, severity, name, description) - VALUES %Q', + VALUES %LQ', PhabricatorRepository::TABLE_LINTMESSAGE, - implode(', ', $values)); + $values); } $this->conn->saveTransaction(); $this->deletes = array(); $this->inserts = array(); } private function blameAuthors() { $repository = id(new PhabricatorRepositoryQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withIDs(array($this->branch->getRepositoryID())) ->executeOne(); $queries = array(); $futures = array(); foreach ($this->blame as $path => $lines) { $drequest = DiffusionRequest::newFromDictionary(array( 'user' => PhabricatorUser::getOmnipotentUser(), 'repository' => $repository, 'branch' => $this->branch->getName(), 'path' => $path, 'commit' => $this->lintCommit, )); // TODO: Restore blame information / generally fix this workflow. $query = DiffusionFileContentQuery::newFromDiffusionRequest($drequest); $queries[$path] = $query; $futures[$path] = new ImmediateFuture($query->executeInline()); } $authors = array(); $futures = id(new FutureIterator($futures)) ->limit(8); foreach ($futures as $path => $future) { $queries[$path]->loadFileContentFromFuture($future); list(, $rev_list, $blame_dict) = $queries[$path]->getBlameData(); foreach (array_keys($this->blame[$path]) as $line) { $commit_identifier = $rev_list[$line - 1]; $author = idx($blame_dict[$commit_identifier], 'authorPHID'); if ($author) { $authors[$author][$path][] = $line; } } } if ($authors) { $this->conn->openTransaction(); foreach ($authors as $author => $paths) { $where = array(); foreach ($paths as $path => $lines) { $where[] = qsprintf( $this->conn, '(path = %s AND line IN (%Ld))', $this->svnRoot.'/'.$path, $lines); } queryfx( $this->conn, - 'UPDATE %T SET authorPHID = %s WHERE %Q', + 'UPDATE %T SET authorPHID = %s WHERE %LO', PhabricatorRepository::TABLE_LINTMESSAGE, $author, - implode(' OR ', $where)); + $where); } $this->conn->saveTransaction(); } } } diff --git a/src/applications/diffusion/conduit/DiffusionBranchQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionBranchQueryConduitAPIMethod.php index 62878c0291..ac2c223c1e 100644 --- a/src/applications/diffusion/conduit/DiffusionBranchQueryConduitAPIMethod.php +++ b/src/applications/diffusion/conduit/DiffusionBranchQueryConduitAPIMethod.php @@ -1,141 +1,144 @@ '; } protected function defineCustomParamTypes() { return array( 'closed' => 'optional bool', 'limit' => 'optional int', 'offset' => 'optional int', 'contains' => 'optional string', + 'patterns' => 'optional list', ); } protected function getGitResult(ConduitAPIRequest $request) { $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $contains = $request->getValue('contains'); if (strlen($contains)) { - // See PHI720. If the standard "branch" field is provided, use it - // as the "pattern" argument to "git branch ..." to let callers test - // for reachability from a particular branch head. - $pattern = $request->getValue('branch'); - if (strlen($pattern)) { - $pattern_argv = array($pattern); - } else { - $pattern_argv = array(); - } + // See PHI958 (and, earlier, PHI720). If "patterns" are provided, pass + // them to "git branch ..." to let callers test for reachability from + // particular branch heads. + $patterns_argv = $request->getValue('patterns', array()); + PhutilTypeSpec::checkMap( + array( + 'patterns' => $patterns_argv, + ), + array( + 'patterns' => 'list', + )); // NOTE: We can't use DiffusionLowLevelGitRefQuery here because // `git for-each-ref` does not support `--contains`. if ($repository->isWorkingCopyBare()) { list($stdout) = $repository->execxLocalCommand( 'branch --verbose --no-abbrev --contains %s -- %Ls', $contains, - $pattern_argv); + $patterns_argv); $ref_map = DiffusionGitBranch::parseLocalBranchOutput( $stdout); } else { list($stdout) = $repository->execxLocalCommand( 'branch -r --verbose --no-abbrev --contains %s -- %Ls', $contains, - $pattern_argv); + $patterns_argv); $ref_map = DiffusionGitBranch::parseRemoteBranchOutput( $stdout, DiffusionGitBranch::DEFAULT_GIT_REMOTE); } $refs = array(); foreach ($ref_map as $ref => $commit) { $refs[] = id(new DiffusionRepositoryRef()) ->setShortName($ref) ->setCommitIdentifier($commit); } } else { $refs = id(new DiffusionLowLevelGitRefQuery()) ->setRepository($repository) ->withRefTypes( array( PhabricatorRepositoryRefCursor::TYPE_BRANCH, )) ->execute(); } return $this->processBranchRefs($request, $refs); } protected function getMercurialResult(ConduitAPIRequest $request) { $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $query = id(new DiffusionLowLevelMercurialBranchesQuery()) ->setRepository($repository); $contains = $request->getValue('contains'); if (strlen($contains)) { $query->withContainsCommit($contains); } $refs = $query->execute(); return $this->processBranchRefs($request, $refs); } protected function getSVNResult(ConduitAPIRequest $request) { // Since SVN doesn't have meaningful branches, just return nothing for all // queries. return array(); } private function processBranchRefs(ConduitAPIRequest $request, array $refs) { $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $offset = $request->getValue('offset'); $limit = $request->getValue('limit'); foreach ($refs as $key => $ref) { if (!$repository->shouldTrackBranch($ref->getShortName())) { unset($refs[$key]); } } $with_closed = $request->getValue('closed'); if ($with_closed !== null) { foreach ($refs as $key => $ref) { $fields = $ref->getRawFields(); if (idx($fields, 'closed') != $with_closed) { unset($refs[$key]); } } } // NOTE: We can't apply the offset or limit until here, because we may have // filtered untrackable branches out of the result set. if ($offset) { $refs = array_slice($refs, $offset); } if ($limit) { $refs = array_slice($refs, 0, $limit); } return mpull($refs, 'toDictionary'); } } diff --git a/src/applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php index 2e272d69a7..fe99471b0c 100644 --- a/src/applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php +++ b/src/applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php @@ -1,546 +1,546 @@ 'optional string', 'commit' => 'optional string', 'needValidityOnly' => 'optional bool', 'limit' => 'optional int', 'offset' => 'optional int', ); } protected function getResult(ConduitAPIRequest $request) { $result = parent::getResult($request); return $result->toDictionary(); } protected function getGitResult(ConduitAPIRequest $request) { $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $path = $request->getValue('path'); $commit = $request->getValue('commit'); $offset = (int)$request->getValue('offset'); $limit = (int)$request->getValue('limit'); $result = $this->getEmptyResultSet(); if ($path == '') { // Fast path to improve the performance of the repository view; we know // the root is always a tree at any commit and always exists. $stdout = 'tree'; } else { try { list($stdout) = $repository->execxLocalCommand( 'cat-file -t %s:%s', $commit, $path); } catch (CommandException $e) { // The "cat-file" command may fail if the path legitimately does not // exist, but it may also fail if the path is a submodule. This can // produce either "Not a valid object name" or "could not get object // info". // To detect if we have a submodule, use `git ls-tree`. If the path // is a submodule, we'll get a "160000" mode mask with type "commit". list($sub_err, $sub_stdout) = $repository->execLocalCommand( 'ls-tree %s -- %s', $commit, $path); if (!$sub_err) { // If the path failed "cat-file" but "ls-tree" worked, we assume it // must be a submodule. If it is, the output will look something // like this: // // 160000 commit // // We make sure it has the 160000 mode mask to confirm that it's // definitely a submodule. $mode = (int)$sub_stdout; if ($mode & 160000) { $submodule_reason = DiffusionBrowseResultSet::REASON_IS_SUBMODULE; $result ->setReasonForEmptyResultSet($submodule_reason); return $result; } } $stderr = $e->getStderr(); if (preg_match('/^fatal: Not a valid object name/', $stderr)) { // Grab two logs, since the first one is when the object was deleted. list($stdout) = $repository->execxLocalCommand( 'log -n2 --format="%%H" %s -- %s', $commit, $path); $stdout = trim($stdout); if ($stdout) { $commits = explode("\n", $stdout); $result ->setReasonForEmptyResultSet( DiffusionBrowseResultSet::REASON_IS_DELETED) ->setDeletedAtCommit(idx($commits, 0)) ->setExistedAtCommit(idx($commits, 1)); return $result; } $result->setReasonForEmptyResultSet( DiffusionBrowseResultSet::REASON_IS_NONEXISTENT); return $result; } else { throw $e; } } } if (trim($stdout) == 'blob') { $result->setReasonForEmptyResultSet( DiffusionBrowseResultSet::REASON_IS_FILE); return $result; } $result->setIsValidResults(true); if ($this->shouldOnlyTestValidity($request)) { return $result; } list($stdout) = $repository->execxLocalCommand( 'ls-tree -z -l %s:%s', $commit, $path); $submodules = array(); if (strlen($path)) { $prefix = rtrim($path, '/').'/'; } else { $prefix = ''; } $count = 0; $results = array(); $lines = empty($stdout) ? array() : explode("\0", rtrim($stdout)); foreach ($lines as $line) { // NOTE: Limit to 5 components so we parse filenames with spaces in them // correctly. // NOTE: The output uses a mixture of tabs and one-or-more spaces to // delimit fields. $parts = preg_split('/\s+/', $line, 5); if (count($parts) < 5) { throw new Exception( pht( 'Expected " \t", for ls-tree of '. '"%s:%s", got: %s', $commit, $path, $line)); } list($mode, $type, $hash, $size, $name) = $parts; $path_result = new DiffusionRepositoryPath(); if ($type == 'tree') { $file_type = DifferentialChangeType::FILE_DIRECTORY; } else if ($type == 'commit') { $file_type = DifferentialChangeType::FILE_SUBMODULE; $submodules[] = $path_result; } else { $mode = intval($mode, 8); if (($mode & 0120000) == 0120000) { $file_type = DifferentialChangeType::FILE_SYMLINK; } else { $file_type = DifferentialChangeType::FILE_NORMAL; } } $path_result->setFullPath($prefix.$name); $path_result->setPath($name); $path_result->setHash($hash); $path_result->setFileType($file_type); $path_result->setFileSize($size); if ($count >= $offset) { $results[] = $path_result; } $count++; if ($limit && $count >= ($offset + $limit)) { break; } } // If we identified submodules, lookup the module info at this commit to // find their source URIs. if ($submodules) { // NOTE: We need to read the file out of git and write it to a temporary // location because "git config -f" doesn't accept a "commit:path"-style // argument. // NOTE: This file may not exist, e.g. because the commit author removed // it when they added the submodule. See T1448. If it's not present, just // show the submodule without enriching it. If ".gitmodules" was removed // it seems to partially break submodules, but the repository as a whole // continues to work fine and we've seen at least two cases of this in // the wild. list($err, $contents) = $repository->execLocalCommand( 'cat-file blob %s:.gitmodules', $commit); if (!$err) { $tmp = new TempFile(); Filesystem::writeFile($tmp, $contents); list($module_info) = $repository->execxLocalCommand( 'config -l -f %s', $tmp); $dict = array(); $lines = explode("\n", trim($module_info)); foreach ($lines as $line) { list($key, $value) = explode('=', $line, 2); $parts = explode('.', $key); $dict[$key] = $value; } foreach ($submodules as $path) { $full_path = $path->getFullPath(); $key = 'submodule.'.$full_path.'.url'; if (isset($dict[$key])) { $path->setExternalURI($dict[$key]); } } } } return $result->setPaths($results); } protected function getMercurialResult(ConduitAPIRequest $request) { $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $path = $request->getValue('path'); $commit = $request->getValue('commit'); $offset = (int)$request->getValue('offset'); $limit = (int)$request->getValue('limit'); $result = $this->getEmptyResultSet(); $entire_manifest = id(new DiffusionLowLevelMercurialPathsQuery()) ->setRepository($repository) ->withCommit($commit) ->withPath($path) ->execute(); $results = array(); $match_against = trim($path, '/'); $match_len = strlen($match_against); // For the root, don't trim. For other paths, trim the "/" after we match. // We need this because Mercurial's canonical paths have no leading "/", // but ours do. $trim_len = $match_len ? $match_len + 1 : 0; $count = 0; foreach ($entire_manifest as $path) { if (strncmp($path, $match_against, $match_len)) { continue; } if (!strlen($path)) { continue; } $remainder = substr($path, $trim_len); if (!strlen($remainder)) { // There is a file with this exact name in the manifest, so clearly // it's a file. $result->setReasonForEmptyResultSet( DiffusionBrowseResultSet::REASON_IS_FILE); return $result; } $parts = explode('/', $remainder); $name = reset($parts); // If we've already seen this path component, we're looking at a file // inside a directory we already processed. Just move on. if (isset($results[$name])) { continue; } if (count($parts) == 1) { $type = DifferentialChangeType::FILE_NORMAL; } else { $type = DifferentialChangeType::FILE_DIRECTORY; } if ($count >= $offset) { $results[$name] = $type; } $count++; if ($limit && ($count >= ($offset + $limit))) { break; } } foreach ($results as $key => $type) { $path_result = new DiffusionRepositoryPath(); $path_result->setPath($key); $path_result->setFileType($type); $path_result->setFullPath(ltrim($match_against.'/', '/').$key); $results[$key] = $path_result; } $valid_results = true; if (empty($results)) { // TODO: Detect "deleted" by issuing "hg log"? $result->setReasonForEmptyResultSet( DiffusionBrowseResultSet::REASON_IS_NONEXISTENT); $valid_results = false; } return $result ->setPaths($results) ->setIsValidResults($valid_results); } protected function getSVNResult(ConduitAPIRequest $request) { $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $path = $request->getValue('path'); $commit = $request->getValue('commit'); $offset = (int)$request->getValue('offset'); $limit = (int)$request->getValue('limit'); $result = $this->getEmptyResultSet(); $subpath = $repository->getDetail('svn-subpath'); if ($subpath && strncmp($subpath, $path, strlen($subpath))) { // If we have a subpath and the path isn't a child of it, it (almost // certainly) won't exist since we don't track commits which affect // it. (Even if it exists, return a consistent result.) $result->setReasonForEmptyResultSet( DiffusionBrowseResultSet::REASON_IS_UNTRACKED_PARENT); return $result; } $conn_r = $repository->establishConnection('r'); $parent_path = DiffusionPathIDQuery::getParentPath($path); $path_query = new DiffusionPathIDQuery( array( $path, $parent_path, )); $path_map = $path_query->loadPathIDs(); $path_id = $path_map[$path]; $parent_path_id = $path_map[$parent_path]; if (empty($path_id)) { $result->setReasonForEmptyResultSet( DiffusionBrowseResultSet::REASON_IS_NONEXISTENT); return $result; } if ($commit) { $slice_clause = 'AND svnCommit <= '.(int)$commit; } else { $slice_clause = ''; } $index = queryfx_all( $conn_r, 'SELECT pathID, max(svnCommit) maxCommit FROM %T WHERE repositoryID = %d AND parentID = %d %Q GROUP BY pathID', PhabricatorRepository::TABLE_FILESYSTEM, $repository->getID(), $path_id, $slice_clause); if (!$index) { if ($path == '/') { $result->setReasonForEmptyResultSet( DiffusionBrowseResultSet::REASON_IS_EMPTY); } else { // NOTE: The parent path ID is included so this query can take // advantage of the table's primary key; it is uniquely determined by // the pathID but if we don't do the lookup ourselves MySQL doesn't have // the information it needs to avoid a table scan. $reasons = queryfx_all( $conn_r, 'SELECT * FROM %T WHERE repositoryID = %d AND parentID = %d AND pathID = %d %Q ORDER BY svnCommit DESC LIMIT 2', PhabricatorRepository::TABLE_FILESYSTEM, $repository->getID(), $parent_path_id, $path_id, $slice_clause); $reason = reset($reasons); if (!$reason) { $result->setReasonForEmptyResultSet( DiffusionBrowseResultSet::REASON_IS_NONEXISTENT); } else { $file_type = $reason['fileType']; if (empty($reason['existed'])) { $result->setReasonForEmptyResultSet( DiffusionBrowseResultSet::REASON_IS_DELETED); $result->setDeletedAtCommit($reason['svnCommit']); if (!empty($reasons[1])) { $result->setExistedAtCommit($reasons[1]['svnCommit']); } } else if ($file_type == DifferentialChangeType::FILE_DIRECTORY) { $result->setReasonForEmptyResultSet( DiffusionBrowseResultSet::REASON_IS_EMPTY); } else { $result->setReasonForEmptyResultSet( DiffusionBrowseResultSet::REASON_IS_FILE); } } } return $result; } $result->setIsValidResults(true); if ($this->shouldOnlyTestValidity($request)) { return $result; } $sql = array(); foreach ($index as $row) { $sql[] = '(pathID = '.(int)$row['pathID'].' AND '. 'svnCommit = '.(int)$row['maxCommit'].')'; } $browse = queryfx_all( $conn_r, 'SELECT *, p.path pathName FROM %T f JOIN %T p ON f.pathID = p.id WHERE repositoryID = %d AND parentID = %d AND existed = 1 - AND (%Q) + AND (%LO) ORDER BY pathName', PhabricatorRepository::TABLE_FILESYSTEM, PhabricatorRepository::TABLE_PATH, $repository->getID(), $path_id, - implode(' OR ', $sql)); + $sql); $loadable_commits = array(); foreach ($browse as $key => $file) { // We need to strip out directories because we don't store last-modified // in the filesystem table. if ($file['fileType'] != DifferentialChangeType::FILE_DIRECTORY) { $loadable_commits[] = $file['svnCommit']; $browse[$key]['hasCommit'] = true; } } $commits = array(); $commit_data = array(); if ($loadable_commits) { // NOTE: Even though these are integers, use '%Ls' because MySQL doesn't // use the second part of the key otherwise! $commits = id(new PhabricatorRepositoryCommit())->loadAllWhere( 'repositoryID = %d AND commitIdentifier IN (%Ls)', $repository->getID(), $loadable_commits); $commits = mpull($commits, null, 'getCommitIdentifier'); if ($commits) { $commit_data = id(new PhabricatorRepositoryCommitData())->loadAllWhere( 'commitID in (%Ld)', mpull($commits, 'getID')); $commit_data = mpull($commit_data, null, 'getCommitID'); } else { $commit_data = array(); } } $path_normal = DiffusionPathIDQuery::normalizePath($path); $results = array(); $count = 0; foreach ($browse as $file) { $full_path = $file['pathName']; $file_path = ltrim(substr($full_path, strlen($path_normal)), '/'); $full_path = ltrim($full_path, '/'); $result_path = new DiffusionRepositoryPath(); $result_path->setPath($file_path); $result_path->setFullPath($full_path); $result_path->setFileType($file['fileType']); if (!empty($file['hasCommit'])) { $commit = idx($commits, $file['svnCommit']); if ($commit) { $data = idx($commit_data, $commit->getID()); $result_path->setLastModifiedCommit($commit); $result_path->setLastCommitData($data); } } if ($count >= $offset) { $results[] = $result_path; } $count++; if ($limit && ($count >= ($offset + $limit))) { break; } } if (empty($results)) { $result->setReasonForEmptyResultSet( DiffusionBrowseResultSet::REASON_IS_EMPTY); } return $result->setPaths($results); } private function getEmptyResultSet() { return id(new DiffusionBrowseResultSet()) ->setPaths(array()) ->setReasonForEmptyResultSet(null) ->setIsValidResults(false); } private function shouldOnlyTestValidity(ConduitAPIRequest $request) { return $request->getValue('needValidityOnly', false); } } diff --git a/src/applications/diffusion/conduit/DiffusionUpdateCoverageConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionUpdateCoverageConduitAPIMethod.php index be0d2c4faa..256d023345 100644 --- a/src/applications/diffusion/conduit/DiffusionUpdateCoverageConduitAPIMethod.php +++ b/src/applications/diffusion/conduit/DiffusionUpdateCoverageConduitAPIMethod.php @@ -1,117 +1,117 @@ 'required phid', 'branch' => 'required string', 'commit' => 'required string', 'coverage' => 'required map', 'mode' => 'optional '.$this->formatStringConstants($modes), ); } protected function execute(ConduitAPIRequest $request) { $viewer = $request->getUser(); $repository_phid = $request->getValue('repositoryPHID'); $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withPHIDs(array($repository_phid)) ->executeOne(); if (!$repository) { throw new Exception( pht('No repository exists with PHID "%s".', $repository_phid)); } $commit_name = $request->getValue('commit'); $commit = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withRepository($repository) ->withIdentifiers(array($commit_name)) ->executeOne(); if (!$commit) { throw new Exception( pht('No commit exists with identifier "%s".', $commit_name)); } $branch = PhabricatorRepositoryBranch::loadOrCreateBranch( $repository->getID(), $request->getValue('branch')); $coverage = $request->getValue('coverage'); $path_map = id(new DiffusionPathIDQuery(array_keys($coverage))) ->loadPathIDs(); $conn = $repository->establishConnection('w'); $sql = array(); foreach ($coverage as $path => $coverage_info) { $sql[] = qsprintf( $conn, '(%d, %d, %d, %s)', $branch->getID(), $path_map[$path], $commit->getID(), $coverage_info); } $table_name = 'repository_coverage'; $conn->openTransaction(); $mode = $request->getValue('mode'); switch ($mode) { case '': case 'overwrite': // sets the coverage for the whole branch, deleting all previous // coverage information queryfx( $conn, 'DELETE FROM %T WHERE branchID = %d', $table_name, $branch->getID()); break; case 'update': // sets the coverage for the provided files on the specified commit break; default: $conn->killTransaction(); throw new Exception(pht('Invalid mode "%s".', $mode)); } foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) { queryfx( $conn, - 'INSERT INTO %T (branchID, pathID, commitID, coverage) VALUES %Q'. - ' ON DUPLICATE KEY UPDATE coverage=VALUES(coverage)', + 'INSERT INTO %T (branchID, pathID, commitID, coverage) VALUES %LQ'. + ' ON DUPLICATE KEY UPDATE coverage = VALUES(coverage)', $table_name, $chunk); } $conn->saveTransaction(); } } diff --git a/src/applications/diffusion/controller/DiffusionLintController.php b/src/applications/diffusion/controller/DiffusionLintController.php index af3ed571ce..704435882d 100644 --- a/src/applications/diffusion/controller/DiffusionLintController.php +++ b/src/applications/diffusion/controller/DiffusionLintController.php @@ -1,536 +1,536 @@ getViewer(); if ($this->getRepositoryIdentifierFromRequest($request)) { $response = $this->loadDiffusionContext(); if ($response) { return $response; } $drequest = $this->getDiffusionRequest(); } else { $drequest = null; } $code = $request->getStr('lint'); if (strlen($code)) { return $this->buildDetailsResponse(); } $owners = array(); if (!$drequest) { if (!$request->getArr('owner')) { $owners = array($viewer->getPHID()); } else { $owners = array(head($request->getArr('owner'))); } } $codes = $this->loadLintCodes($drequest, $owners); if ($codes) { $branches = id(new PhabricatorRepositoryBranch())->loadAllWhere( 'id IN (%Ld)', array_unique(ipull($codes, 'branchID'))); $branches = mpull($branches, null, 'getID'); } else { $branches = array(); } if ($branches) { $repositories = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withIDs(mpull($branches, 'getRepositoryID')) ->execute(); $repositories = mpull($repositories, null, 'getID'); } else { $repositories = array(); } $rows = array(); $total = 0; foreach ($codes as $code) { $branch = idx($branches, $code['branchID']); if (!$branch) { continue; } $repository = idx($repositories, $branch->getRepositoryID()); if (!$repository) { continue; } $total += $code['n']; if ($drequest) { $href_lint = $drequest->generateURI( array( 'action' => 'lint', 'lint' => $code['code'], )); $href_browse = $drequest->generateURI( array( 'action' => 'browse', 'lint' => $code['code'], )); $href_repo = $drequest->generateURI( array( 'action' => 'lint', )); } else { $href_lint = $repository->generateURI( array( 'action' => 'lint', 'lint' => $code['code'], )); $href_browse = $repository->generateURI( array( 'action' => 'browse', 'lint' => $code['code'], )); $href_repo = $repository->generateURI( array( 'action' => 'lint', )); } $rows[] = array( phutil_tag('a', array('href' => $href_lint), $code['n']), phutil_tag('a', array('href' => $href_browse), $code['files']), phutil_tag( 'a', array( 'href' => $href_repo, ), $repository->getDisplayName()), ArcanistLintSeverity::getStringForSeverity($code['maxSeverity']), $code['code'], $code['maxName'], $code['maxDescription'], ); } $table = id(new AphrontTableView($rows)) ->setHeaders(array( pht('Problems'), pht('Files'), pht('Repository'), pht('Severity'), pht('Code'), pht('Name'), pht('Example'), )) ->setColumnVisibility(array(true, true, !$drequest)) ->setColumnClasses(array('n', 'n', '', '', 'pri', '', '')); $content = array(); if (!$drequest) { $form = id(new AphrontFormView()) ->setUser($viewer) ->setMethod('GET') ->appendControl( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorPeopleDatasource()) ->setLimit(1) ->setName('owner') ->setLabel(pht('Owner')) ->setValue($owners)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Filter'))); $content[] = id(new AphrontListFilterView())->appendChild($form); } $content[] = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Lint')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($table); $title = array('Lint'); $crumbs = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'lint', )); $crumbs->setBorder(true); if ($drequest) { $title[] = $drequest->getRepository()->getDisplayName(); } else { $crumbs->addTextCrumb(pht('All Lint')); } if ($drequest) { $branch = $drequest->loadBranch(); $header = id(new PHUIHeaderView()) ->setHeader(pht('Lint: %s', $this->renderPathLinks($drequest, 'lint'))) ->setUser($viewer) ->setHeaderIcon('fa-code'); $actions = $this->buildActionView($drequest); $properties = $this->buildPropertyView( $drequest, $branch, $total, $actions); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->addPropertyList($properties); } else { $object_box = null; $header = id(new PHUIHeaderView()) ->setHeader(pht('All Lint')) ->setHeaderIcon('fa-code'); } $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter(array( $object_box, $content, )); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild( array( $view, )); } private function loadLintCodes($drequest, array $owner_phids) { $conn = id(new PhabricatorRepository())->establishConnection('r'); $where = array('1 = 1'); if ($drequest) { $branch = $drequest->loadBranch(); if (!$branch) { return array(); } $where[] = qsprintf($conn, 'branchID = %d', $branch->getID()); if ($drequest->getPath() != '') { $path = '/'.$drequest->getPath(); $is_dir = (substr($path, -1) == '/'); $where[] = ($is_dir ? qsprintf($conn, 'path LIKE %>', $path) : qsprintf($conn, 'path = %s', $path)); } } if ($owner_phids) { $or = array(); $or[] = qsprintf($conn, 'authorPHID IN (%Ls)', $owner_phids); $paths = array(); $packages = id(new PhabricatorOwnersOwner()) ->loadAllWhere('userPHID IN (%Ls)', $owner_phids); if ($packages) { $paths = id(new PhabricatorOwnersPath())->loadAllWhere( 'packageID IN (%Ld)', mpull($packages, 'getPackageID')); } if ($paths) { $repositories = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getRequest()->getUser()) ->withPHIDs(mpull($paths, 'getRepositoryPHID')) ->execute(); $repositories = mpull($repositories, 'getID', 'getPHID'); $branches = id(new PhabricatorRepositoryBranch())->loadAllWhere( 'repositoryID IN (%Ld)', $repositories); $branches = mgroup($branches, 'getRepositoryID'); } foreach ($paths as $path) { $branch = idx( $branches, idx( $repositories, $path->getRepositoryPHID())); if ($branch) { $condition = qsprintf( $conn, '(branchID IN (%Ld) AND path LIKE %>)', array_keys($branch), $path->getPath()); if ($path->getExcluded()) { - $where[] = 'NOT '.$condition; + $where[] = qsprintf($conn, 'NOT %Q', $condition); } else { $or[] = $condition; } } } - $where[] = '('.implode(' OR ', $or).')'; + $where[] = qsprintf($conn, '%LO', $or); } return queryfx_all( $conn, 'SELECT branchID, code, MAX(severity) AS maxSeverity, MAX(name) AS maxName, MAX(description) AS maxDescription, COUNT(DISTINCT path) AS files, COUNT(*) AS n FROM %T - WHERE %Q + WHERE %LA GROUP BY branchID, code ORDER BY n DESC', PhabricatorRepository::TABLE_LINTMESSAGE, - implode(' AND ', $where)); + $where); } protected function buildActionView(DiffusionRequest $drequest) { $viewer = $this->getRequest()->getUser(); $view = id(new PhabricatorActionListView()) ->setUser($viewer); $list_uri = $drequest->generateURI( array( 'action' => 'lint', 'lint' => '', )); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('View As List')) ->setHref($list_uri) ->setIcon('fa-list')); $history_uri = $drequest->generateURI( array( 'action' => 'history', )); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('View History')) ->setHref($history_uri) ->setIcon('fa-clock-o')); $browse_uri = $drequest->generateURI( array( 'action' => 'browse', )); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Browse Content')) ->setHref($browse_uri) ->setIcon('fa-files-o')); return $view; } protected function buildPropertyView( DiffusionRequest $drequest, PhabricatorRepositoryBranch $branch, $total, PhabricatorActionListView $actions) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setActionList($actions); $lint_commit = $branch->getLintCommit(); $view->addProperty( pht('Lint Commit'), phutil_tag( 'a', array( 'href' => $drequest->generateURI( array( 'action' => 'commit', 'commit' => $lint_commit, )), ), $drequest->getRepository()->formatCommitName($lint_commit))); $view->addProperty( pht('Total Messages'), pht('%s', new PhutilNumber($total))); return $view; } private function buildDetailsResponse() { $request = $this->getRequest(); $limit = 500; $pager = id(new PHUIPagerView()) ->readFromRequest($request) ->setPageSize($limit); $offset = $pager->getOffset(); $drequest = $this->getDiffusionRequest(); $branch = $drequest->loadBranch(); $messages = $this->loadLintMessages($branch, $limit, $offset); $is_dir = (substr('/'.$drequest->getPath(), -1) == '/'); $pager->setHasMorePages(count($messages) >= $limit); $authors = $this->loadViewerHandles(ipull($messages, 'authorPHID')); $rows = array(); foreach ($messages as $message) { $path = phutil_tag( 'a', array( 'href' => $drequest->generateURI(array( 'action' => 'lint', 'path' => $message['path'], )), ), substr($message['path'], strlen($drequest->getPath()) + 1)); $line = phutil_tag( 'a', array( 'href' => $drequest->generateURI(array( 'action' => 'browse', 'path' => $message['path'], 'line' => $message['line'], 'commit' => $branch->getLintCommit(), )), ), $message['line']); $author = $message['authorPHID']; if ($author && $authors[$author]) { $author = $authors[$author]->renderLink(); } $rows[] = array( $path, $line, $author, ArcanistLintSeverity::getStringForSeverity($message['severity']), $message['name'], $message['description'], ); } $table = id(new AphrontTableView($rows)) ->setHeaders(array( pht('Path'), pht('Line'), pht('Author'), pht('Severity'), pht('Name'), pht('Description'), )) ->setColumnClasses(array('', 'n')) ->setColumnVisibility(array($is_dir)); $content = array(); $content[] = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Lint Details')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($table) ->setPager($pager); $crumbs = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'lint', )); $crumbs->setBorder(true); $header = id(new PHUIHeaderView()) ->setHeader(pht('Lint: %s', $drequest->getRepository()->getDisplayName())) ->setHeaderIcon('fa-code'); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter(array( $content, )); return $this->newPage() ->setTitle( array( pht('Lint'), $drequest->getRepository()->getDisplayName(), )) ->setCrumbs($crumbs) ->appendChild( array( $view, )); } private function loadLintMessages( PhabricatorRepositoryBranch $branch, $limit, $offset) { $drequest = $this->getDiffusionRequest(); if (!$branch) { return array(); } $conn = $branch->establishConnection('r'); $where = array( qsprintf($conn, 'branchID = %d', $branch->getID()), ); if ($drequest->getPath() != '') { $path = '/'.$drequest->getPath(); $is_dir = (substr($path, -1) == '/'); $where[] = ($is_dir ? qsprintf($conn, 'path LIKE %>', $path) : qsprintf($conn, 'path = %s', $path)); } if ($drequest->getLint() != '') { $where[] = qsprintf( $conn, 'code = %s', $drequest->getLint()); } return queryfx_all( $conn, 'SELECT * FROM %T - WHERE %Q + WHERE %LA ORDER BY path, code, line LIMIT %d OFFSET %d', PhabricatorRepository::TABLE_LINTMESSAGE, - implode(' AND ', $where), + $where, $limit, $offset); } } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditActivateController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditActivateController.php index b383333fbc..7e2d06982d 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditActivateController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditActivateController.php @@ -1,78 +1,79 @@ loadDiffusionContextForEdit(); if ($response) { return $response; } $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $panel_uri = id(new DiffusionRepositoryBasicsManagementPanel()) ->setRepository($repository) ->getPanelURI(); if ($request->isFormPost()) { if (!$repository->isTracked()) { $new_status = PhabricatorRepository::STATUS_ACTIVE; } else { $new_status = PhabricatorRepository::STATUS_INACTIVE; } $xaction = id(new PhabricatorRepositoryTransaction()) - ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_ACTIVATE) + ->setTransactionType( + PhabricatorRepositoryActivateTransaction::TRANSACTIONTYPE) ->setNewValue($new_status); $editor = id(new PhabricatorRepositoryEditor()) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->setContentSourceFromRequest($request) ->setActor($viewer) ->applyTransactions($repository, array($xaction)); return id(new AphrontReloadResponse())->setURI($panel_uri); } if ($repository->isTracked()) { $title = pht('Deactivate Repository'); $body = pht( 'If you deactivate this repository, it will no longer be updated. '. 'Observation and mirroring will cease, and pushing and pulling will '. 'be disabled. You can reactivate the repository later.'); $submit = pht('Deactivate Repository'); } else { $title = pht('Activate Repository'); $is_new = $repository->isNewlyInitialized(); if ($is_new) { if ($repository->isHosted()) { $body = pht( 'This repository will become a new hosted repository. '. 'It will begin serving read and write traffic.'); } else { $body = pht( 'This repository will observe an existing remote repository. '. 'It will begin fetching changes from the remote.'); } } else { $body = pht( 'This repository will resume updates, observation, mirroring, '. 'and serving any configured read and write traffic.'); } $submit = pht('Activate Repository'); } return $this->newDialog() ->setTitle($title) ->appendChild($body) ->addSubmitButton($submit) ->addCancelButton($panel_uri); } } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditDangerousController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditDangerousController.php index 1088733cfc..9503a9e386 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditDangerousController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditDangerousController.php @@ -1,87 +1,88 @@ loadDiffusionContextForEdit(); if ($response) { return $response; } $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $panel_uri = id(new DiffusionRepositoryBasicsManagementPanel()) ->setRepository($repository) ->getPanelURI(); if (!$repository->canAllowDangerousChanges()) { return $this->newDialog() ->setTitle(pht('Unprotectable Repository')) ->appendParagraph( pht( 'This repository can not be protected from dangerous changes '. 'because Phabricator does not control what users are allowed '. 'to push to it.')) ->addCancelButton($panel_uri); } if ($request->isFormPost()) { $xaction = id(new PhabricatorRepositoryTransaction()) - ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_DANGEROUS) + ->setTransactionType( + PhabricatorRepositoryDangerousTransaction::TRANSACTIONTYPE) ->setNewValue(!$repository->shouldAllowDangerousChanges()); $editor = id(new PhabricatorRepositoryEditor()) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) ->setActor($viewer) ->applyTransactions($repository, array($xaction)); return id(new AphrontReloadResponse())->setURI($panel_uri); } $force = phutil_tag('tt', array(), '--force'); if ($repository->shouldAllowDangerousChanges()) { $title = pht('Prevent Dangerous Changes'); if ($repository->isSVN()) { $body = pht( 'It will no longer be possible to edit revprops in this '. 'repository.'); } else { $body = pht( 'It will no longer be possible to delete branches from this '. 'repository, or %s push to this repository.', $force); } $submit = pht('Prevent Dangerous Changes'); } else { $title = pht('Allow Dangerous Changes'); if ($repository->isSVN()) { $body = pht( 'If you allow dangerous changes, it will be possible to edit '. 'reprops in this repository, including arbitrarily rewriting '. 'commit messages. These operations can alter a repository in a '. 'way that is difficult to recover from.'); } else { $body = pht( 'If you allow dangerous changes, it will be possible to delete '. 'branches and %s push this repository. These operations can '. 'alter a repository in a way that is difficult to recover from.', $force); } $submit = pht('Allow Dangerous Changes'); } return $this->newDialog() ->setTitle($title) ->appendParagraph($body) ->addSubmitButton($submit) ->addCancelButton($panel_uri); } } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditEnormousController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditEnormousController.php index d4eeb118d7..11a3ee3736 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditEnormousController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditEnormousController.php @@ -1,90 +1,91 @@ loadDiffusionContextForEdit(); if ($response) { return $response; } $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $panel_uri = id(new DiffusionRepositoryBasicsManagementPanel()) ->setRepository($repository) ->getPanelURI(); if (!$repository->canAllowEnormousChanges()) { return $this->newDialog() ->setTitle(pht('Unprotectable Repository')) ->appendParagraph( pht( 'This repository can not be protected from enormous changes '. 'because Phabricator does not control what users are allowed '. 'to push to it.')) ->addCancelButton($panel_uri); } if ($request->isFormPost()) { $xaction = id(new PhabricatorRepositoryTransaction()) - ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_ENORMOUS) + ->setTransactionType( + PhabricatorRepositoryEnormousTransaction::TRANSACTIONTYPE) ->setNewValue(!$repository->shouldAllowEnormousChanges()); $editor = id(new PhabricatorRepositoryEditor()) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) ->setActor($viewer) ->applyTransactions($repository, array($xaction)); return id(new AphrontReloadResponse())->setURI($panel_uri); } if ($repository->shouldAllowEnormousChanges()) { $title = pht('Prevent Enormous Changes'); $body = pht( 'It will no longer be possible to push enormous changes to this '. 'repository.'); $submit = pht('Prevent Enormous Changes'); } else { $title = pht('Allow Enormous Changes'); $body = array( pht( 'If you allow enormous changes, users can push commits which are '. 'too large for Herald to process content rules for. This can allow '. 'users to evade content rules implemented in Herald.'), pht( 'You can selectively configure Herald by adding rules to prevent a '. 'subset of enormous changes (for example, based on who is trying '. 'to push the change).'), ); $submit = pht('Allow Enormous Changes'); } $more_help = pht( 'Enormous changes are commits which are too large to process with '. 'content rules because: the diff text for the change is larger than '. '%s bytes; or the diff text takes more than %s seconds to extract.', new PhutilNumber(HeraldCommitAdapter::getEnormousByteLimit()), new PhutilNumber(HeraldCommitAdapter::getEnormousTimeLimit())); $response = $this->newDialog(); foreach ((array)$body as $paragraph) { $response->appendParagraph($paragraph); } return $response ->setTitle($title) ->appendParagraph($more_help) ->addSubmitButton($submit) ->addCancelButton($panel_uri); } } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryManagePanelsController.php b/src/applications/diffusion/controller/DiffusionRepositoryManagePanelsController.php index 7871c5192a..dfb84cc3f0 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryManagePanelsController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryManagePanelsController.php @@ -1,163 +1,192 @@ navigation) { return $this->navigation->getMenu(); } return parent::buildApplicationMenu(); } public function handleRequest(AphrontRequest $request) { $response = $this->loadDiffusionContext(); if ($response) { return $response; } $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $panels = DiffusionRepositoryManagementPanel::getAllPanels(); foreach ($panels as $key => $panel) { $panel ->setViewer($viewer) ->setRepository($repository) ->setController($this); if (!$panel->shouldEnableForRepository($repository)) { unset($panels[$key]); continue; } } $selected = $request->getURIData('panel'); if (!strlen($selected)) { $selected = head_key($panels); } if (empty($panels[$selected])) { return new Aphront404Response(); } $nav = $this->renderSideNav($repository, $panels, $selected); $this->navigation = $nav; $panel = $panels[$selected]; $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($panel->getManagementPanelLabel()); $crumbs->setBorder(true); $content = $panel->buildManagementPanelContent(); $title = array( $panel->getManagementPanelLabel(), $repository->getDisplayName(), ); $header = $this->buildHeaderView($repository->getDisplayName()); $view = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) ->setMainColumn($content); + $curtain = $panel->buildManagementPanelCurtain(); + if ($curtain) { + $view->setCurtain($curtain); + } + return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($view); } private function renderSideNav( PhabricatorRepository $repository, array $panels, $selected) { $base_uri = $repository->getPathURI('manage/'); $base_uri = new PhutilURI($base_uri); $nav = id(new AphrontSideNavFilterView()) ->setBaseURI($base_uri); - foreach ($panels as $panel) { - $key = $panel->getManagementPanelKey(); - $label = $panel->getManagementPanelLabel(); - $icon = $panel->getManagementPanelIcon(); - $href = $panel->getPanelNavigationURI(); + $groups = DiffusionRepositoryManagementPanelGroup::getAllPanelGroups(); + $panel_groups = mgroup($panels, 'getManagementPanelGroupKey'); + $other_key = DiffusionRepositoryManagementOtherPanelGroup::PANELGROUPKEY; + + foreach ($groups as $group_key => $group) { + // If this is the "Other" group, include everything else that isn't in + // some actual group. + if ($group_key === $other_key) { + $group_panels = array_mergev($panel_groups); + $panel_groups = array(); + } else { + $group_panels = idx($panel_groups, $group_key); + unset($panel_groups[$group_key]); + } + + if (!$group_panels) { + continue; + } + + $label = $group->getManagementPanelGroupLabel(); + if ($label) { + $nav->addLabel($label); + } + + foreach ($group_panels as $panel) { + $key = $panel->getManagementPanelKey(); + $label = $panel->getManagementPanelLabel(); + $icon = $panel->getManagementPanelIcon(); + $href = $panel->getPanelNavigationURI(); - $item = id(new PHUIListItemView()) - ->setKey($key) - ->setName($label) - ->setType(PHUIListItemView::TYPE_LINK) - ->setHref($href) - ->setIcon($icon); + $item = id(new PHUIListItemView()) + ->setKey($key) + ->setName($label) + ->setType(PHUIListItemView::TYPE_LINK) + ->setHref($href) + ->setIcon($icon); - $nav->addMenuItem($item); + $nav->addMenuItem($item); + } } $nav->selectFilter($selected); return $nav; } public function buildHeaderView($title) { $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setProfileHeader(true) ->setHref($repository->getURI()) ->setImage($repository->getProfileImageURI()); if ($repository->isTracked()) { $header->setStatus('fa-check', 'bluegrey', pht('Active')); } else { $header->setStatus('fa-ban', 'dark', pht('Inactive')); } $doc_href = PhabricatorEnv::getDoclink( 'Diffusion User Guide: Managing Repositories'); $header->addActionLink( id(new PHUIButtonView()) ->setTag('a') ->setText(pht('View Repository')) ->setHref($repository->getURI()) ->setIcon('fa-code') ->setColor(PHUIButtonView::GREY)); $header->addActionLink( id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-book') ->setHref($doc_href) ->setText(pht('Help')) ->setColor(PHUIButtonView::GREY)); return $header; } public function newTimeline(PhabricatorRepository $repository) { $timeline = $this->buildTransactionTimeline( $repository, new PhabricatorRepositoryTransactionQuery()); $timeline->setShouldTerminate(true); return $timeline; } } diff --git a/src/applications/diffusion/editor/DiffusionRepositoryEditEngine.php b/src/applications/diffusion/editor/DiffusionRepositoryEditEngine.php index dcb79a6c86..29dc588b45 100644 --- a/src/applications/diffusion/editor/DiffusionRepositoryEditEngine.php +++ b/src/applications/diffusion/editor/DiffusionRepositoryEditEngine.php @@ -1,468 +1,506 @@ versionControlSystem = $version_control_system; return $this; } public function getVersionControlSystem() { return $this->versionControlSystem; } public function isEngineConfigurable() { return false; } public function isDefaultQuickCreateEngine() { return true; } public function getQuickCreateOrderVector() { return id(new PhutilSortVector())->addInt(300); } public function getEngineName() { return pht('Repositories'); } public function getSummaryHeader() { return pht('Edit Repositories'); } public function getSummaryText() { return pht('Creates and edits repositories.'); } public function getEngineApplicationClass() { return 'PhabricatorDiffusionApplication'; } protected function newEditableObject() { $viewer = $this->getViewer(); $repository = PhabricatorRepository::initializeNewRepository($viewer); $repository->setDetail('newly-initialized', true); $vcs = $this->getVersionControlSystem(); if ($vcs) { $repository->setVersionControlSystem($vcs); } // Pick a random open service to allocate this repository on, if any exist. // If there are no services, we aren't in cluster mode and will allocate // locally. If there are services but none permit allocations, we fail. // Eventually we can make this more flexible, but this rule is a reasonable // starting point as we begin to deploy cluster services. $services = id(new AlmanacServiceQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withServiceTypes( array( AlmanacClusterRepositoryServiceType::SERVICETYPE, )) ->needProperties(true) ->execute(); if ($services) { // Filter out services which do not permit new allocations. foreach ($services as $key => $possible_service) { if ($possible_service->getAlmanacPropertyValue('closed')) { unset($services[$key]); } } if (!$services) { throw new Exception( pht( 'This install is configured in cluster mode, but all available '. 'repository cluster services are closed to new allocations. '. 'At least one service must be open to allow new allocations to '. 'take place.')); } shuffle($services); $service = head($services); $repository->setAlmanacServicePHID($service->getPHID()); } return $repository; } protected function newObjectQuery() { return new PhabricatorRepositoryQuery(); } protected function getObjectCreateTitleText($object) { return pht('Create Repository'); } protected function getObjectCreateButtonText($object) { return pht('Create Repository'); } protected function getObjectEditTitleText($object) { return pht('Edit Repository: %s', $object->getName()); } protected function getObjectEditShortText($object) { return $object->getDisplayName(); } protected function getObjectCreateShortText() { return pht('Create Repository'); } protected function getObjectName() { return pht('Repository'); } protected function getObjectViewURI($object) { return $object->getPathURI('manage/'); } protected function getCreateNewObjectPolicy() { return $this->getApplication()->getPolicy( DiffusionCreateRepositoriesCapability::CAPABILITY); } protected function newPages($object) { $panels = DiffusionRepositoryManagementPanel::getAllPanels(); $pages = array(); $uris = array(); foreach ($panels as $panel_key => $panel) { $panel->setRepository($object); $uris[$panel_key] = $panel->getPanelURI(); $page = $panel->newEditEnginePage(); if (!$page) { continue; } $pages[] = $page; } $basics_key = DiffusionRepositoryBasicsManagementPanel::PANELKEY; $basics_uri = $uris[$basics_key]; $more_pages = array( id(new PhabricatorEditPage()) ->setKey('encoding') ->setLabel(pht('Text Encoding')) ->setViewURI($basics_uri) ->setFieldKeys( array( 'encoding', )), id(new PhabricatorEditPage()) ->setKey('extensions') ->setLabel(pht('Extensions')) ->setIsDefault(true), ); foreach ($more_pages as $page) { $pages[] = $page; } return $pages; } protected function willConfigureFields($object, array $fields) { // Change the default field order so related fields are adjacent. $after = array( 'policy.edit' => array('policy.push'), ); $result = array(); foreach ($fields as $key => $value) { $result[$key] = $value; if (!isset($after[$key])) { continue; } foreach ($after[$key] as $next_key) { if (!isset($fields[$next_key])) { continue; } unset($result[$next_key]); $result[$next_key] = $fields[$next_key]; unset($fields[$next_key]); } } return $result; } protected function buildCustomEditFields($object) { $viewer = $this->getViewer(); $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($object) ->execute(); $track_value = $object->getDetail('branch-filter', array()); $track_value = array_keys($track_value); $autoclose_value = $object->getDetail('close-commits-filter', array()); $autoclose_value = array_keys($autoclose_value); $automation_instructions = pht( "Configure **Repository Automation** to allow Phabricator to ". "write to this repository.". "\n\n". "IMPORTANT: This feature is new, experimental, and not supported. ". "Use it at your own risk."); $staging_instructions = pht( "To make it easier to run integration tests and builds on code ". "under review, you can configure a **Staging Area**. When `arc` ". "creates a diff, it will push a copy of the changes to the ". "configured staging area with a corresponding tag.". "\n\n". "IMPORTANT: This feature is new, experimental, and not supported. ". "Use it at your own risk."); $subpath_instructions = pht( 'If you want to import only part of a repository, like `trunk/`, '. 'you can set a path in **Import Only**. Phabricator will ignore '. 'commits which do not affect this path.'); return array( id(new PhabricatorSelectEditField()) ->setKey('vcs') ->setLabel(pht('Version Control System')) - ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_VCS) - ->setIsConduitOnly(true) + ->setTransactionType( + PhabricatorRepositoryVCSTransaction::TRANSACTIONTYPE) + ->setIsFormField(false) ->setIsCopyable(true) ->setOptions(PhabricatorRepositoryType::getAllRepositoryTypes()) ->setDescription(pht('Underlying repository version control system.')) ->setConduitDescription( pht( 'Choose which version control system to use when creating a '. 'repository.')) ->setConduitTypeDescription(pht('Version control system selection.')) ->setValue($object->getVersionControlSystem()), id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) ->setIsRequired(true) - ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_NAME) + ->setTransactionType( + PhabricatorRepositoryNameTransaction::TRANSACTIONTYPE) ->setDescription(pht('The repository name.')) ->setConduitDescription(pht('Rename the repository.')) ->setConduitTypeDescription(pht('New repository name.')) ->setValue($object->getName()), id(new PhabricatorTextEditField()) ->setKey('callsign') ->setLabel(pht('Callsign')) - ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_CALLSIGN) + ->setTransactionType( + PhabricatorRepositoryCallsignTransaction::TRANSACTIONTYPE) ->setDescription(pht('The repository callsign.')) ->setConduitDescription(pht('Change the repository callsign.')) ->setConduitTypeDescription(pht('New repository callsign.')) ->setValue($object->getCallsign()), id(new PhabricatorTextEditField()) ->setKey('shortName') ->setLabel(pht('Short Name')) - ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_SLUG) + ->setTransactionType( + PhabricatorRepositorySlugTransaction::TRANSACTIONTYPE) ->setDescription(pht('Short, unique repository name.')) ->setConduitDescription(pht('Change the repository short name.')) ->setConduitTypeDescription(pht('New short name for the repository.')) ->setValue($object->getRepositorySlug()), id(new PhabricatorRemarkupEditField()) ->setKey('description') ->setLabel(pht('Description')) - ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_DESCRIPTION) + ->setTransactionType( + PhabricatorRepositoryDescriptionTransaction::TRANSACTIONTYPE) ->setDescription(pht('Repository description.')) ->setConduitDescription(pht('Change the repository description.')) ->setConduitTypeDescription(pht('New repository description.')) ->setValue($object->getDetail('description')), id(new PhabricatorTextEditField()) ->setKey('encoding') ->setLabel(pht('Text Encoding')) ->setIsCopyable(true) - ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_ENCODING) + ->setTransactionType( + PhabricatorRepositoryEncodingTransaction::TRANSACTIONTYPE) ->setDescription(pht('Default text encoding.')) ->setConduitDescription(pht('Change the default text encoding.')) ->setConduitTypeDescription(pht('New text encoding.')) ->setValue($object->getDetail('encoding')), id(new PhabricatorBoolEditField()) ->setKey('allowDangerousChanges') ->setLabel(pht('Allow Dangerous Changes')) ->setIsCopyable(true) - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setOptions( pht('Prevent Dangerous Changes'), pht('Allow Dangerous Changes')) - ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_DANGEROUS) + ->setTransactionType( + PhabricatorRepositoryDangerousTransaction::TRANSACTIONTYPE) ->setDescription(pht('Permit dangerous changes to be made.')) ->setConduitDescription(pht('Allow or prevent dangerous changes.')) ->setConduitTypeDescription(pht('New protection setting.')) ->setValue($object->shouldAllowDangerousChanges()), id(new PhabricatorBoolEditField()) ->setKey('allowEnormousChanges') ->setLabel(pht('Allow Enormous Changes')) ->setIsCopyable(true) - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setOptions( pht('Prevent Enormous Changes'), pht('Allow Enormous Changes')) - ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_ENORMOUS) + ->setTransactionType( + PhabricatorRepositoryEnormousTransaction::TRANSACTIONTYPE) ->setDescription(pht('Permit enormous changes to be made.')) ->setConduitDescription(pht('Allow or prevent enormous changes.')) ->setConduitTypeDescription(pht('New protection setting.')) ->setValue($object->shouldAllowEnormousChanges()), id(new PhabricatorSelectEditField()) ->setKey('status') ->setLabel(pht('Status')) - ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_ACTIVATE) - ->setIsConduitOnly(true) + ->setTransactionType( + PhabricatorRepositoryActivateTransaction::TRANSACTIONTYPE) + ->setIsFormField(false) ->setOptions(PhabricatorRepository::getStatusNameMap()) ->setDescription(pht('Active or inactive status.')) ->setConduitDescription(pht('Active or deactivate the repository.')) ->setConduitTypeDescription(pht('New repository status.')) ->setValue($object->getStatus()), id(new PhabricatorTextEditField()) ->setKey('defaultBranch') ->setLabel(pht('Default Branch')) ->setTransactionType( - PhabricatorRepositoryTransaction::TYPE_DEFAULT_BRANCH) + PhabricatorRepositoryDefaultBranchTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setDescription(pht('Default branch name.')) ->setConduitDescription(pht('Set the default branch name.')) ->setConduitTypeDescription(pht('New default branch name.')) ->setValue($object->getDetail('default-branch')), id(new PhabricatorTextAreaEditField()) ->setIsStringList(true) ->setKey('trackOnly') ->setLabel(pht('Track Only')) ->setTransactionType( - PhabricatorRepositoryTransaction::TYPE_TRACK_ONLY) + PhabricatorRepositoryTrackOnlyTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setDescription(pht('Track only these branches.')) ->setConduitDescription(pht('Set the tracked branches.')) ->setConduitTypeDescription(pht('New tracked branches.')) ->setValue($track_value), id(new PhabricatorTextAreaEditField()) ->setIsStringList(true) ->setKey('autocloseOnly') ->setLabel(pht('Autoclose Only')) ->setTransactionType( - PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE_ONLY) + PhabricatorRepositoryAutocloseOnlyTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setDescription(pht('Autoclose commits on only these branches.')) ->setConduitDescription(pht('Set the autoclose branches.')) ->setConduitTypeDescription(pht('New default tracked branches.')) ->setValue($autoclose_value), id(new PhabricatorTextEditField()) ->setKey('importOnly') ->setLabel(pht('Import Only')) ->setTransactionType( - PhabricatorRepositoryTransaction::TYPE_SVN_SUBPATH) + PhabricatorRepositorySVNSubpathTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setDescription(pht('Subpath to selectively import.')) ->setConduitDescription(pht('Set the subpath to import.')) ->setConduitTypeDescription(pht('New subpath to import.')) ->setValue($object->getDetail('svn-subpath')) ->setControlInstructions($subpath_instructions), id(new PhabricatorTextEditField()) ->setKey('stagingAreaURI') ->setLabel(pht('Staging Area URI')) ->setTransactionType( - PhabricatorRepositoryTransaction::TYPE_STAGING_URI) + PhabricatorRepositoryStagingURITransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setDescription(pht('Staging area URI.')) ->setConduitDescription(pht('Set the staging area URI.')) ->setConduitTypeDescription(pht('New staging area URI.')) ->setValue($object->getStagingURI()) ->setControlInstructions($staging_instructions), id(new PhabricatorDatasourceEditField()) ->setKey('automationBlueprintPHIDs') ->setLabel(pht('Use Blueprints')) ->setTransactionType( - PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS) + PhabricatorRepositoryBlueprintsTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setDatasource(new DrydockBlueprintDatasource()) ->setDescription(pht('Automation blueprints.')) ->setConduitDescription(pht('Change automation blueprints.')) ->setConduitTypeDescription(pht('New blueprint PHIDs.')) ->setValue($object->getAutomationBlueprintPHIDs()) ->setControlInstructions($automation_instructions), id(new PhabricatorStringListEditField()) ->setKey('symbolLanguages') ->setLabel(pht('Languages')) ->setTransactionType( - PhabricatorRepositoryTransaction::TYPE_SYMBOLS_LANGUAGE) + PhabricatorRepositorySymbolLanguagesTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setDescription( pht('Languages which define symbols in this repository.')) ->setConduitDescription( pht('Change symbol languages for this repository.')) ->setConduitTypeDescription( pht('New symbol languages.')) ->setValue($object->getSymbolLanguages()), id(new PhabricatorDatasourceEditField()) ->setKey('symbolRepositoryPHIDs') ->setLabel(pht('Uses Symbols From')) ->setTransactionType( - PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES) + PhabricatorRepositorySymbolSourcesTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setDatasource(new DiffusionRepositoryDatasource()) ->setDescription(pht('Repositories to link symbols from.')) ->setConduitDescription(pht('Change symbol source repositories.')) ->setConduitTypeDescription(pht('New symbol repositories.')) ->setValue($object->getSymbolSources()), id(new PhabricatorBoolEditField()) ->setKey('publish') ->setLabel(pht('Publish/Notify')) ->setTransactionType( - PhabricatorRepositoryTransaction::TYPE_NOTIFY) + PhabricatorRepositoryNotifyTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setOptions( pht('Disable Notifications, Feed, and Herald'), pht('Enable Notifications, Feed, and Herald')) ->setDescription(pht('Configure how changes are published.')) ->setConduitDescription(pht('Change publishing options.')) ->setConduitTypeDescription(pht('New notification setting.')) ->setValue(!$object->getDetail('herald-disabled')), id(new PhabricatorBoolEditField()) ->setKey('autoclose') ->setLabel(pht('Autoclose')) ->setTransactionType( - PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE) + PhabricatorRepositoryAutocloseTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setOptions( pht('Disable Autoclose'), pht('Enable Autoclose')) ->setDescription(pht('Stop or resume autoclosing in this repository.')) ->setConduitDescription(pht('Change autoclose setting.')) ->setConduitTypeDescription(pht('New autoclose setting.')) ->setValue(!$object->getDetail('disable-autoclose')), id(new PhabricatorPolicyEditField()) ->setKey('policy.push') ->setLabel(pht('Push Policy')) ->setAliases(array('push')) ->setIsCopyable(true) ->setCapability(DiffusionPushCapability::CAPABILITY) ->setPolicies($policies) - ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY) + ->setTransactionType( + PhabricatorRepositoryPushPolicyTransaction::TRANSACTIONTYPE) ->setDescription( pht('Controls who can push changes to the repository.')) ->setConduitDescription( pht('Change the push policy of the repository.')) ->setConduitTypeDescription(pht('New policy PHID or constant.')) ->setValue($object->getPolicy(DiffusionPushCapability::CAPABILITY)), + id(new PhabricatorTextEditField()) + ->setKey('filesizeLimit') + ->setLabel(pht('Filesize Limit')) + ->setTransactionType( + PhabricatorRepositoryFilesizeLimitTransaction::TRANSACTIONTYPE) + ->setDescription(pht('Maximum permitted file size.')) + ->setConduitDescription(pht('Change the filesize limit.')) + ->setConduitTypeDescription(pht('New repository filesize limit.')) + ->setValue($object->getFilesizeLimit()), + id(new PhabricatorTextEditField()) + ->setKey('copyTimeLimit') + ->setLabel(pht('Clone/Fetch Timeout')) + ->setTransactionType( + PhabricatorRepositoryCopyTimeLimitTransaction::TRANSACTIONTYPE) + ->setDescription( + pht('Maximum permitted duration of internal clone/fetch.')) + ->setConduitDescription(pht('Change the copy time limit.')) + ->setConduitTypeDescription(pht('New repository copy time limit.')) + ->setValue($object->getCopyTimeLimit()), + id(new PhabricatorTextEditField()) + ->setKey('touchLimit') + ->setLabel(pht('Touched Paths Limit')) + ->setTransactionType( + PhabricatorRepositoryTouchLimitTransaction::TRANSACTIONTYPE) + ->setDescription(pht('Maximum permitted paths touched per commit.')) + ->setConduitDescription(pht('Change the touch limit.')) + ->setConduitTypeDescription(pht('New repository touch limit.')) + ->setValue($object->getTouchLimit()), ); } } diff --git a/src/applications/diffusion/editor/DiffusionURIEditEngine.php b/src/applications/diffusion/editor/DiffusionURIEditEngine.php index fdc91cff5f..dbc1238153 100644 --- a/src/applications/diffusion/editor/DiffusionURIEditEngine.php +++ b/src/applications/diffusion/editor/DiffusionURIEditEngine.php @@ -1,219 +1,219 @@ repository = $repository; return $this; } public function getRepository() { return $this->repository; } public function isEngineConfigurable() { return false; } public function getEngineName() { return pht('Repository URIs'); } public function getSummaryHeader() { return pht('Edit Repository URI'); } public function getSummaryText() { return pht('Creates and edits repository URIs.'); } public function getEngineApplicationClass() { return 'PhabricatorDiffusionApplication'; } protected function newEditableObject() { $uri = PhabricatorRepositoryURI::initializeNewURI(); $repository = $this->getRepository(); if ($repository) { $uri->setRepositoryPHID($repository->getPHID()); $uri->attachRepository($repository); } return $uri; } protected function newObjectQuery() { return new PhabricatorRepositoryURIQuery(); } protected function getObjectCreateTitleText($object) { return pht('Create Repository URI'); } protected function getObjectCreateButtonText($object) { return pht('Create Repository URI'); } protected function getObjectEditTitleText($object) { return pht('Edit Repository URI %d', $object->getID()); } protected function getObjectEditShortText($object) { return pht('URI %d', $object->getID()); } protected function getObjectCreateShortText() { return pht('Create Repository URI'); } protected function getObjectName() { return pht('Repository URI'); } protected function getObjectViewURI($object) { return $object->getViewURI(); } protected function buildCustomEditFields($object) { $viewer = $this->getViewer(); $uri_instructions = null; if ($object->isBuiltin()) { $is_builtin = true; $uri_value = (string)$object->getDisplayURI(); switch ($object->getBuiltinProtocol()) { case PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH: $uri_instructions = pht( " - Configure [[ %s | %s ]] to change the SSH username.\n". " - Configure [[ %s | %s ]] to change the SSH host.\n". " - Configure [[ %s | %s ]] to change the SSH port.", '/config/edit/diffusion.ssh-user/', 'diffusion.ssh-user', '/config/edit/diffusion.ssh-host/', 'diffusion.ssh-host', '/config/edit/diffusion.ssh-port/', 'diffusion.ssh-port'); break; } } else { $is_builtin = false; $uri_value = $object->getURI(); if ($object->getRepositoryPHID()) { $repository = $object->getRepository(); if ($repository->isGit()) { $uri_instructions = pht( "Provide the URI of a Git repository. It should usually look ". "like one of these examples:\n". "\n". "| Example Git URIs\n". "| -----------------------\n". "| `git@github.com:example/example.git`\n". "| `ssh://user@host.com/git/example.git`\n". "| `https://example.com/repository.git`"); } else if ($repository->isHg()) { $uri_instructions = pht( "Provide the URI of a Mercurial repository. It should usually ". "look like one of these examples:\n". "\n". "| Example Mercurial URIs\n". "|-----------------------\n". "| `ssh://hg@bitbucket.org/example/repository`\n". "| `https://bitbucket.org/example/repository`"); } else if ($repository->isSVN()) { $uri_instructions = pht( "Provide the **Repository Root** of a Subversion repository. ". "You can identify this by running `svn info` in a working ". "copy. It should usually look like one of these examples:\n". "\n". "| Example Subversion URIs\n". "|-----------------------\n". "| `http://svn.example.org/svnroot/`\n". "| `svn+ssh://svn.example.com/svnroot/`\n". "| `svn://svn.example.net/svnroot/`\n\n". "You **MUST** specify the root of the repository, not a ". "subdirectory."); } } } return array( id(new PhabricatorHandlesEditField()) ->setKey('repository') ->setAliases(array('repositoryPHID')) ->setLabel(pht('Repository')) ->setIsRequired(true) - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setTransactionType( PhabricatorRepositoryURITransaction::TYPE_REPOSITORY) ->setDescription(pht('The repository this URI is associated with.')) ->setConduitDescription( pht( 'Create a URI in a given repository. This transaction type '. 'must be present when creating a new URI and must not be '. 'present when editing an existing URI.')) ->setConduitTypeDescription( pht('Repository PHID to create a new URI for.')) ->setSingleValue($object->getRepositoryPHID()), id(new PhabricatorTextEditField()) ->setKey('uri') ->setLabel(pht('URI')) ->setTransactionType(PhabricatorRepositoryURITransaction::TYPE_URI) ->setDescription(pht('The repository URI.')) ->setConduitDescription(pht('Change the repository URI.')) ->setConduitTypeDescription(pht('New repository URI.')) ->setIsRequired(!$is_builtin) ->setIsLocked($is_builtin) ->setValue($uri_value) ->setControlInstructions($uri_instructions), id(new PhabricatorSelectEditField()) ->setKey('io') ->setLabel(pht('I/O Type')) ->setTransactionType(PhabricatorRepositoryURITransaction::TYPE_IO) ->setDescription(pht('URI I/O behavior.')) ->setConduitDescription(pht('Adjust I/O behavior.')) ->setConduitTypeDescription(pht('New I/O behavior.')) ->setValue($object->getIOType()) ->setOptions($object->getAvailableIOTypeOptions()), id(new PhabricatorSelectEditField()) ->setKey('display') ->setLabel(pht('Display Type')) ->setTransactionType(PhabricatorRepositoryURITransaction::TYPE_DISPLAY) ->setDescription(pht('URI display behavior.')) ->setConduitDescription(pht('Change display behavior.')) ->setConduitTypeDescription(pht('New display behavior.')) ->setValue($object->getDisplayType()) ->setOptions($object->getAvailableDisplayTypeOptions()), id(new PhabricatorHandlesEditField()) ->setKey('credential') ->setAliases(array('credentialPHID')) ->setLabel(pht('Credential')) - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setTransactionType( PhabricatorRepositoryURITransaction::TYPE_CREDENTIAL) ->setDescription( pht('The credential to use when interacting with this URI.')) ->setConduitDescription(pht('Change the credential for this URI.')) ->setConduitTypeDescription(pht('New credential PHID, or null.')) ->setSingleValue($object->getCredentialPHID()), id(new PhabricatorBoolEditField()) ->setKey('disable') ->setLabel(pht('Disabled')) - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setTransactionType(PhabricatorRepositoryURITransaction::TYPE_DISABLE) ->setDescription(pht('Active status of the URI.')) ->setConduitDescription(pht('Disable or activate the URI.')) ->setConduitTypeDescription(pht('True to disable the URI.')) ->setOptions(pht('Enable'), pht('Disable')) ->setValue($object->getIsDisabled()), ); } } diff --git a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php index 236e289ec0..6856b8676d 100644 --- a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php +++ b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php @@ -1,1376 +1,1482 @@ remoteProtocol = $remote_protocol; return $this; } public function getRemoteProtocol() { return $this->remoteProtocol; } public function setRemoteAddress($remote_address) { $this->remoteAddress = $remote_address; return $this; } public function getRemoteAddress() { return $this->remoteAddress; } public function setRequestIdentifier($request_identifier) { $this->requestIdentifier = $request_identifier; return $this; } public function getRequestIdentifier() { return $this->requestIdentifier; } public function setStartTime($start_time) { $this->startTime = $start_time; return $this; } public function getStartTime() { return $this->startTime; } public function setSubversionTransactionInfo($transaction, $repository) { $this->subversionTransaction = $transaction; $this->subversionRepository = $repository; return $this; } public function setStdin($stdin) { $this->stdin = $stdin; return $this; } public function getStdin() { return $this->stdin; } public function setOriginalArgv(array $original_argv) { $this->originalArgv = $original_argv; return $this; } public function getOriginalArgv() { return $this->originalArgv; } public function setRepository(PhabricatorRepository $repository) { $this->repository = $repository; return $this; } public function getRepository() { return $this->repository; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setMercurialHook($mercurial_hook) { $this->mercurialHook = $mercurial_hook; return $this; } public function getMercurialHook() { return $this->mercurialHook; } /* -( Hook Execution )----------------------------------------------------- */ public function execute() { $ref_updates = $this->findRefUpdates(); $all_updates = $ref_updates; $caught = null; try { try { $this->rejectDangerousChanges($ref_updates); } catch (DiffusionCommitHookRejectException $ex) { // If we're rejecting dangerous changes, flag everything that we've // seen as rejected so it's clear that none of it was accepted. $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_DANGEROUS; throw $ex; } $content_updates = $this->findContentUpdates($ref_updates); $all_updates = array_merge($ref_updates, $content_updates); // If this is an "initial import" (a sizable push to a previously empty // repository) we'll allow enormous changes and disable Herald rules. // These rulesets can consume a large amount of time and memory and are // generally not relevant when importing repository history. $is_initial_import = $this->isInitialImport($all_updates); if (!$is_initial_import) { $this->applyHeraldRefRules($ref_updates); } + try { + if (!$is_initial_import) { + $this->rejectOversizedFiles($content_updates); + } + } catch (DiffusionCommitHookRejectException $ex) { + // If we're rejecting oversized files, flag everything. + $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_OVERSIZED; + throw $ex; + } + + try { + if (!$is_initial_import) { + $this->rejectCommitsAffectingTooManyPaths($content_updates); + } + } catch (DiffusionCommitHookRejectException $ex) { + $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_TOUCHES; + throw $ex; + } + try { if (!$is_initial_import) { $this->rejectEnormousChanges($content_updates); } } catch (DiffusionCommitHookRejectException $ex) { // If we're rejecting enormous changes, flag everything. $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_ENORMOUS; throw $ex; } if (!$is_initial_import) { $this->applyHeraldContentRules($content_updates); } // Run custom scripts in `hook.d/` directories. $this->applyCustomHooks($all_updates); // If we make it this far, we're accepting these changes. Mark all the // logs as accepted. $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_ACCEPT; } catch (Exception $ex) { // We'll throw this again in a minute, but we want to save all the logs // first. $caught = $ex; } // Save all the logs no matter what the outcome was. $event = $this->newPushEvent(); $event->setRejectCode($this->rejectCode); $event->setRejectDetails($this->rejectDetails); $event->openTransaction(); $event->save(); foreach ($all_updates as $update) { $update->setPushEventPHID($event->getPHID()); $update->save(); } $event->saveTransaction(); if ($caught) { throw $caught; } // If this went through cleanly and was an import, set the importing flag // on the repository. It will be cleared once we fully process everything. if ($is_initial_import) { $repository = $this->getRepository(); $repository->markImporting(); } if ($this->emailPHIDs) { // If Herald rules triggered email to users, queue a worker to send the // mail. We do this out-of-process so that we block pushes as briefly // as possible. // (We do need to pull some commit info here because the commit objects // may not exist yet when this worker runs, which could be immediately.) PhabricatorWorker::scheduleTask( 'PhabricatorRepositoryPushMailWorker', array( 'eventPHID' => $event->getPHID(), 'emailPHIDs' => array_values($this->emailPHIDs), 'info' => $this->loadCommitInfoForWorker($all_updates), ), array( 'priority' => PhabricatorWorker::PRIORITY_ALERTS, )); } return 0; } private function findRefUpdates() { $type = $this->getRepository()->getVersionControlSystem(); switch ($type) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: return $this->findGitRefUpdates(); case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: return $this->findMercurialRefUpdates(); case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: return $this->findSubversionRefUpdates(); default: throw new Exception(pht('Unsupported repository type "%s"!', $type)); } } private function rejectDangerousChanges(array $ref_updates) { assert_instances_of($ref_updates, 'PhabricatorRepositoryPushLog'); $repository = $this->getRepository(); if ($repository->shouldAllowDangerousChanges()) { return; } $flag_dangerous = PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS; foreach ($ref_updates as $ref_update) { if (!$ref_update->hasChangeFlags($flag_dangerous)) { // This is not a dangerous change. continue; } // We either have a branch deletion or a non fast-forward branch update. // Format a message and reject the push. $message = pht( "DANGEROUS CHANGE: %s\n". "Dangerous change protection is enabled for this repository.\n". "Edit the repository configuration before making dangerous changes.", $ref_update->getDangerousChangeDescription()); throw new DiffusionCommitHookRejectException($message); } } private function findContentUpdates(array $ref_updates) { assert_instances_of($ref_updates, 'PhabricatorRepositoryPushLog'); $type = $this->getRepository()->getVersionControlSystem(); switch ($type) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: return $this->findGitContentUpdates($ref_updates); case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: return $this->findMercurialContentUpdates($ref_updates); case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: return $this->findSubversionContentUpdates($ref_updates); default: throw new Exception(pht('Unsupported repository type "%s"!', $type)); } } /* -( Herald )------------------------------------------------------------- */ private function applyHeraldRefRules(array $ref_updates) { $this->applyHeraldRules( $ref_updates, new HeraldPreCommitRefAdapter()); } private function applyHeraldContentRules(array $content_updates) { $this->applyHeraldRules( $content_updates, new HeraldPreCommitContentAdapter()); } private function applyHeraldRules( array $updates, HeraldAdapter $adapter_template) { if (!$updates) { return; } $viewer = $this->getViewer(); $adapter_template ->setHookEngine($this) ->setActingAsPHID($viewer->getPHID()); $engine = new HeraldEngine(); $rules = null; $blocking_effect = null; $blocked_update = null; $blocking_xscript = null; foreach ($updates as $update) { $adapter = id(clone $adapter_template) ->setPushLog($update); if ($rules === null) { $rules = $engine->loadRulesForAdapter($adapter); } $effects = $engine->applyRules($rules, $adapter); $engine->applyEffects($effects, $adapter, $rules); $xscript = $engine->getTranscript(); // Store any PHIDs we want to send email to for later. foreach ($adapter->getEmailPHIDs() as $email_phid) { $this->emailPHIDs[$email_phid] = $email_phid; } $block_action = DiffusionBlockHeraldAction::ACTIONCONST; if ($blocking_effect === null) { foreach ($effects as $effect) { if ($effect->getAction() == $block_action) { $blocking_effect = $effect; $blocked_update = $update; $blocking_xscript = $xscript; break; } } } } if ($blocking_effect) { $rule = $blocking_effect->getRule(); $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_HERALD; $this->rejectDetails = $rule->getPHID(); $message = $blocking_effect->getTarget(); if (!strlen($message)) { $message = pht('(None.)'); } $blocked_ref_name = coalesce( $blocked_update->getRefName(), $blocked_update->getRefNewShort()); $blocked_name = $blocked_update->getRefType().'/'.$blocked_ref_name; throw new DiffusionCommitHookRejectException( pht( "This push was rejected by Herald push rule %s.\n". " Change: %s\n". " Rule: %s\n". " Reason: %s\n". "Transcript: %s", $rule->getMonogram(), $blocked_name, $rule->getName(), $message, PhabricatorEnv::getProductionURI( '/herald/transcript/'.$blocking_xscript->getID().'/'))); } } public function loadViewerProjectPHIDsForHerald() { // This just caches the viewer's projects so we don't need to load them // over and over again when applying Herald rules. if ($this->heraldViewerProjects === null) { $this->heraldViewerProjects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withMemberPHIDs(array($this->getViewer()->getPHID())) ->execute(); } return mpull($this->heraldViewerProjects, 'getPHID'); } /* -( Git )---------------------------------------------------------------- */ private function findGitRefUpdates() { $ref_updates = array(); // First, parse stdin, which lists all the ref changes. The input looks // like this: // // $stdin = $this->getStdin(); $lines = phutil_split_lines($stdin, $retain_endings = false); foreach ($lines as $line) { $parts = explode(' ', $line, 3); if (count($parts) != 3) { throw new Exception(pht('Expected "old new ref", got "%s".', $line)); } $ref_old = $parts[0]; $ref_new = $parts[1]; $ref_raw = $parts[2]; if (preg_match('(^refs/heads/)', $ref_raw)) { $ref_type = PhabricatorRepositoryPushLog::REFTYPE_BRANCH; $ref_raw = substr($ref_raw, strlen('refs/heads/')); } else if (preg_match('(^refs/tags/)', $ref_raw)) { $ref_type = PhabricatorRepositoryPushLog::REFTYPE_TAG; $ref_raw = substr($ref_raw, strlen('refs/tags/')); } else { throw new Exception( pht( "Unable to identify the reftype of '%s'. Rejecting push.", $ref_raw)); } $ref_update = $this->newPushLog() ->setRefType($ref_type) ->setRefName($ref_raw) ->setRefOld($ref_old) ->setRefNew($ref_new); $ref_updates[] = $ref_update; } $this->findGitMergeBases($ref_updates); $this->findGitChangeFlags($ref_updates); return $ref_updates; } private function findGitMergeBases(array $ref_updates) { assert_instances_of($ref_updates, 'PhabricatorRepositoryPushLog'); $futures = array(); foreach ($ref_updates as $key => $ref_update) { // If the old hash is "00000...", the ref is being created (either a new // branch, or a new tag). If the new hash is "00000...", the ref is being // deleted. If both are nonempty, the ref is being updated. For updates, // we'll figure out the `merge-base` of the old and new objects here. This // lets us reject non-FF changes cheaply; later, we'll figure out exactly // which commits are new. $ref_old = $ref_update->getRefOld(); $ref_new = $ref_update->getRefNew(); if (($ref_old === self::EMPTY_HASH) || ($ref_new === self::EMPTY_HASH)) { continue; } $futures[$key] = $this->getRepository()->getLocalCommandFuture( 'merge-base %s %s', $ref_old, $ref_new); } $futures = id(new FutureIterator($futures)) ->limit(8); foreach ($futures as $key => $future) { // If 'old' and 'new' have no common ancestors (for example, a force push // which completely rewrites a ref), `git merge-base` will exit with // an error and no output. It would be nice to find a positive test // for this instead, but I couldn't immediately come up with one. See // T4224. Assume this means there are no ancestors. list($err, $stdout) = $future->resolve(); if ($err) { $merge_base = null; } else { $merge_base = rtrim($stdout, "\n"); } $ref_update = $ref_updates[$key]; $ref_update->setMergeBase($merge_base); } return $ref_updates; } private function findGitChangeFlags(array $ref_updates) { assert_instances_of($ref_updates, 'PhabricatorRepositoryPushLog'); foreach ($ref_updates as $key => $ref_update) { $ref_old = $ref_update->getRefOld(); $ref_new = $ref_update->getRefNew(); $ref_type = $ref_update->getRefType(); $ref_flags = 0; $dangerous = null; if (($ref_old === self::EMPTY_HASH) && ($ref_new === self::EMPTY_HASH)) { // This happens if you try to delete a tag or branch which does not // exist by pushing directly to the ref. Git will warn about it but // allow it. Just call it a delete, without flagging it as dangerous. $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE; } else if ($ref_old === self::EMPTY_HASH) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD; } else if ($ref_new === self::EMPTY_HASH) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE; if ($ref_type == PhabricatorRepositoryPushLog::REFTYPE_BRANCH) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS; $dangerous = pht( "The change you're attempting to push deletes the branch '%s'.", $ref_update->getRefName()); } } else { $merge_base = $ref_update->getMergeBase(); if ($merge_base == $ref_old) { // This is a fast-forward update to an existing branch. // These are safe. $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND; } else { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE; // For now, we don't consider deleting or moving tags to be a // "dangerous" update. It's way harder to get wrong and should be easy // to recover from once we have better logging. Only add the dangerous // flag if this ref is a branch. if ($ref_type == PhabricatorRepositoryPushLog::REFTYPE_BRANCH) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS; $dangerous = pht( "The change you're attempting to push updates the branch '%s' ". "from '%s' to '%s', but this is not a fast-forward. Pushes ". "which rewrite published branch history are dangerous.", $ref_update->getRefName(), $ref_update->getRefOldShort(), $ref_update->getRefNewShort()); } } } $ref_update->setChangeFlags($ref_flags); if ($dangerous !== null) { $ref_update->attachDangerousChangeDescription($dangerous); } } return $ref_updates; } private function findGitContentUpdates(array $ref_updates) { $flag_delete = PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE; $futures = array(); foreach ($ref_updates as $key => $ref_update) { if ($ref_update->hasChangeFlags($flag_delete)) { // Deleting a branch or tag can never create any new commits. continue; } // NOTE: This piece of magic finds all new commits, by walking backward // from the new value to the value of *any* existing ref in the // repository. Particularly, this will cover the cases of a new branch, a // completely moved tag, etc. $futures[$key] = $this->getRepository()->getLocalCommandFuture( 'log --format=%s %s --not --all', '%H', $ref_update->getRefNew()); } $content_updates = array(); $futures = id(new FutureIterator($futures)) ->limit(8); foreach ($futures as $key => $future) { list($stdout) = $future->resolvex(); if (!strlen(trim($stdout))) { // This change doesn't have any new commits. One common case of this // is creating a new tag which points at an existing commit. continue; } $commits = phutil_split_lines($stdout, $retain_newlines = false); // If we're looking at a branch, mark all of the new commits as on that // branch. It's only possible for these commits to be on updated branches, // since any other branch heads are necessarily behind them. $branch_name = null; $ref_update = $ref_updates[$key]; $type_branch = PhabricatorRepositoryPushLog::REFTYPE_BRANCH; if ($ref_update->getRefType() == $type_branch) { $branch_name = $ref_update->getRefName(); } foreach ($commits as $commit) { if ($branch_name) { $this->gitCommits[$commit][] = $branch_name; } $content_updates[$commit] = $this->newPushLog() ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT) ->setRefNew($commit) ->setChangeFlags(PhabricatorRepositoryPushLog::CHANGEFLAG_ADD); } } return $content_updates; } /* -( Custom )------------------------------------------------------------- */ private function applyCustomHooks(array $updates) { $args = $this->getOriginalArgv(); $stdin = $this->getStdin(); $console = PhutilConsole::getConsole(); $env = array( self::ENV_REPOSITORY => $this->getRepository()->getPHID(), self::ENV_USER => $this->getViewer()->getUsername(), self::ENV_REQUEST => $this->getRequestIdentifier(), self::ENV_REMOTE_PROTOCOL => $this->getRemoteProtocol(), self::ENV_REMOTE_ADDRESS => $this->getRemoteAddress(), ); $repository = $this->getRepository(); $env += $repository->getPassthroughEnvironmentalVariables(); $directories = $repository->getHookDirectories(); foreach ($directories as $directory) { $hooks = $this->getExecutablesInDirectory($directory); sort($hooks); foreach ($hooks as $hook) { // NOTE: We're explicitly running the hooks in sequential order to // make this more predictable. $future = id(new ExecFuture('%s %Ls', $hook, $args)) ->setEnv($env, $wipe_process_env = false) ->write($stdin); list($err, $stdout, $stderr) = $future->resolve(); if (!$err) { // This hook ran OK, but echo its output in case there was something // informative. $console->writeOut('%s', $stdout); $console->writeErr('%s', $stderr); continue; } $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_EXTERNAL; $this->rejectDetails = basename($hook); throw new DiffusionCommitHookRejectException( pht( "This push was rejected by custom hook script '%s':\n\n%s%s", basename($hook), $stdout, $stderr)); } } } private function getExecutablesInDirectory($directory) { $executables = array(); if (!Filesystem::pathExists($directory)) { return $executables; } foreach (Filesystem::listDirectory($directory) as $path) { $full_path = $directory.DIRECTORY_SEPARATOR.$path; if (!is_executable($full_path)) { // Don't include non-executable files. continue; } if (basename($full_path) == 'README') { // Don't include README, even if it is marked as executable. It almost // certainly got caught in the crossfire of a sweeping `chmod`, since // users do this with some frequency. continue; } $executables[] = $full_path; } return $executables; } /* -( Mercurial )---------------------------------------------------------- */ private function findMercurialRefUpdates() { $hook = $this->getMercurialHook(); switch ($hook) { case 'pretxnchangegroup': return $this->findMercurialChangegroupRefUpdates(); case 'prepushkey': return $this->findMercurialPushKeyRefUpdates(); default: throw new Exception(pht('Unrecognized hook "%s"!', $hook)); } } private function findMercurialChangegroupRefUpdates() { $hg_node = getenv('HG_NODE'); if (!$hg_node) { throw new Exception( pht( 'Expected %s in environment!', 'HG_NODE')); } // NOTE: We need to make sure this is passed to subprocesses, or they won't // be able to see new commits. Mercurial uses this as a marker to determine // whether the pending changes are visible or not. $_ENV['HG_PENDING'] = getenv('HG_PENDING'); $repository = $this->getRepository(); $futures = array(); foreach (array('old', 'new') as $key) { $futures[$key] = $repository->getLocalCommandFuture( 'heads --template %s', '{node}\1{branch}\2'); } // Wipe HG_PENDING out of the old environment so we see the pre-commit // state of the repository. $futures['old']->updateEnv('HG_PENDING', null); $futures['commits'] = $repository->getLocalCommandFuture( 'log --rev %s --template %s', hgsprintf('%s:%s', $hg_node, 'tip'), '{node}\1{branch}\2'); // Resolve all of the futures now. We don't need the 'commits' future yet, // but it simplifies the logic to just get it out of the way. foreach (new FutureIterator($futures) as $future) { $future->resolve(); } list($commit_raw) = $futures['commits']->resolvex(); $commit_map = $this->parseMercurialCommits($commit_raw); $this->mercurialCommits = $commit_map; // NOTE: `hg heads` exits with an error code and no output if the repository // has no heads. Most commonly this happens on a new repository. We know // we can run `hg` successfully since the `hg log` above didn't error, so // just ignore the error code. list($err, $old_raw) = $futures['old']->resolve(); $old_refs = $this->parseMercurialHeads($old_raw); list($err, $new_raw) = $futures['new']->resolve(); $new_refs = $this->parseMercurialHeads($new_raw); $all_refs = array_keys($old_refs + $new_refs); $ref_updates = array(); foreach ($all_refs as $ref) { $old_heads = idx($old_refs, $ref, array()); $new_heads = idx($new_refs, $ref, array()); sort($old_heads); sort($new_heads); if (!$old_heads && !$new_heads) { // This should never be possible, as it makes no sense. Explode. throw new Exception( pht( 'Mercurial repository has no new or old heads for branch "%s" '. 'after push. This makes no sense; rejecting change.', $ref)); } if ($old_heads === $new_heads) { // No changes to this branch, so skip it. continue; } $stray_heads = array(); $head_map = array(); if ($old_heads && !$new_heads) { // This is a branch deletion with "--close-branch". foreach ($old_heads as $old_head) { $head_map[$old_head] = array(self::EMPTY_HASH); } } else if (count($old_heads) > 1) { // HORRIBLE: In Mercurial, branches can have multiple heads. If the // old branch had multiple heads, we need to figure out which new // heads descend from which old heads, so we can tell whether you're // actively creating new heads (dangerous) or just working in a // repository that's already full of garbage (strongly discouraged but // not as inherently dangerous). These cases should be very uncommon. // NOTE: We're only looking for heads on the same branch. The old // tip of the branch may be the branchpoint for other branches, but that // is OK. $dfutures = array(); foreach ($old_heads as $old_head) { $dfutures[$old_head] = $repository->getLocalCommandFuture( 'log --branch %s --rev %s --template %s', $ref, hgsprintf('(descendants(%s) and head())', $old_head), '{node}\1'); } foreach (new FutureIterator($dfutures) as $future_head => $dfuture) { list($stdout) = $dfuture->resolvex(); $descendant_heads = array_filter(explode("\1", $stdout)); if ($descendant_heads) { // This old head has at least one descendant in the push. $head_map[$future_head] = $descendant_heads; } else { // This old head has no descendants, so it is being deleted. $head_map[$future_head] = array(self::EMPTY_HASH); } } // Now, find all the new stray heads this push creates, if any. These // are new heads which do not descend from the old heads. $seen = array_fuse(array_mergev($head_map)); foreach ($new_heads as $new_head) { if ($new_head === self::EMPTY_HASH) { // If a branch head is being deleted, don't insert it as an add. continue; } if (empty($seen[$new_head])) { $head_map[self::EMPTY_HASH][] = $new_head; } } } else if ($old_heads) { $head_map[head($old_heads)] = $new_heads; } else { $head_map[self::EMPTY_HASH] = $new_heads; } foreach ($head_map as $old_head => $child_heads) { foreach ($child_heads as $new_head) { if ($new_head === $old_head) { continue; } $ref_flags = 0; $dangerous = null; if ($old_head == self::EMPTY_HASH) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD; } else { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND; } $deletes_existing_head = ($new_head == self::EMPTY_HASH); $splits_existing_head = (count($child_heads) > 1); $creates_duplicate_head = ($old_head == self::EMPTY_HASH) && (count($head_map) > 1); if ($splits_existing_head || $creates_duplicate_head) { $readable_child_heads = array(); foreach ($child_heads as $child_head) { $readable_child_heads[] = substr($child_head, 0, 12); } $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS; if ($splits_existing_head) { // We're splitting an existing head into two or more heads. // This is dangerous, and a super bad idea. Note that we're only // raising this if you're actively splitting a branch head. If a // head split in the past, we don't consider appends to it // to be dangerous. $dangerous = pht( "The change you're attempting to push splits the head of ". "branch '%s' into multiple heads: %s. This is inadvisable ". "and dangerous.", $ref, implode(', ', $readable_child_heads)); } else { // We're adding a second (or more) head to a branch. The new // head is not a descendant of any old head. $dangerous = pht( "The change you're attempting to push creates new, divergent ". "heads for the branch '%s': %s. This is inadvisable and ". "dangerous.", $ref, implode(', ', $readable_child_heads)); } } if ($deletes_existing_head) { // TODO: Somewhere in here we should be setting CHANGEFLAG_REWRITE // if we are also creating at least one other head to replace // this one. // NOTE: In Git, this is a dangerous change, but it is not dangerous // in Mercurial. Mercurial branches are version controlled, and // Mercurial does not prompt you for any special flags when pushing // a `--close-branch` commit by default. $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE; } $ref_update = $this->newPushLog() ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_BRANCH) ->setRefName($ref) ->setRefOld($old_head) ->setRefNew($new_head) ->setChangeFlags($ref_flags); if ($dangerous !== null) { $ref_update->attachDangerousChangeDescription($dangerous); } $ref_updates[] = $ref_update; } } } return $ref_updates; } private function findMercurialPushKeyRefUpdates() { $key_namespace = getenv('HG_NAMESPACE'); if ($key_namespace === 'phases') { // Mercurial changes commit phases as part of normal push operations. We // just ignore these, as they don't seem to represent anything // interesting. return array(); } $key_name = getenv('HG_KEY'); $key_old = getenv('HG_OLD'); if (!strlen($key_old)) { $key_old = null; } $key_new = getenv('HG_NEW'); if (!strlen($key_new)) { $key_new = null; } if ($key_namespace !== 'bookmarks') { throw new Exception( pht( "Unknown Mercurial key namespace '%s', with key '%s' (%s -> %s). ". "Rejecting push.", $key_namespace, $key_name, coalesce($key_old, pht('null')), coalesce($key_new, pht('null')))); } if ($key_old === $key_new) { // We get a callback when the bookmark doesn't change. Just ignore this, // as it's a no-op. return array(); } $ref_flags = 0; $merge_base = null; if ($key_old === null) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD; } else if ($key_new === null) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE; } else { list($merge_base_raw) = $this->getRepository()->execxLocalCommand( 'log --template %s --rev %s', '{node}', hgsprintf('ancestor(%s, %s)', $key_old, $key_new)); if (strlen(trim($merge_base_raw))) { $merge_base = trim($merge_base_raw); } if ($merge_base && ($merge_base === $key_old)) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND; } else { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE; } } $ref_update = $this->newPushLog() ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_BOOKMARK) ->setRefName($key_name) ->setRefOld(coalesce($key_old, self::EMPTY_HASH)) ->setRefNew(coalesce($key_new, self::EMPTY_HASH)) ->setChangeFlags($ref_flags); return array($ref_update); } private function findMercurialContentUpdates(array $ref_updates) { $content_updates = array(); foreach ($this->mercurialCommits as $commit => $branches) { $content_updates[$commit] = $this->newPushLog() ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT) ->setRefNew($commit) ->setChangeFlags(PhabricatorRepositoryPushLog::CHANGEFLAG_ADD); } return $content_updates; } private function parseMercurialCommits($raw) { $commits_lines = explode("\2", $raw); $commits_lines = array_filter($commits_lines); $commit_map = array(); foreach ($commits_lines as $commit_line) { list($node, $branch) = explode("\1", $commit_line); $commit_map[$node] = array($branch); } return $commit_map; } private function parseMercurialHeads($raw) { $heads_map = $this->parseMercurialCommits($raw); $heads = array(); foreach ($heads_map as $commit => $branches) { foreach ($branches as $branch) { $heads[$branch][] = $commit; } } return $heads; } /* -( Subversion )--------------------------------------------------------- */ private function findSubversionRefUpdates() { // Subversion doesn't have any kind of mutable ref metadata. return array(); } private function findSubversionContentUpdates(array $ref_updates) { list($youngest) = execx( 'svnlook youngest %s', $this->subversionRepository); $ref_new = (int)$youngest + 1; $ref_flags = 0; $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD; $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND; $ref_content = $this->newPushLog() ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT) ->setRefNew($ref_new) ->setChangeFlags($ref_flags); return array($ref_content); } /* -( Internals )---------------------------------------------------------- */ private function newPushLog() { // NOTE: We generate PHIDs up front so the Herald transcripts can pick them // up. $phid = id(new PhabricatorRepositoryPushLog())->generatePHID(); $device = AlmanacKeys::getLiveDevice(); if ($device) { $device_phid = $device->getPHID(); } else { $device_phid = null; } return PhabricatorRepositoryPushLog::initializeNewLog($this->getViewer()) ->setPHID($phid) ->setDevicePHID($device_phid) ->setRepositoryPHID($this->getRepository()->getPHID()) ->attachRepository($this->getRepository()) ->setEpoch(PhabricatorTime::getNow()); } private function newPushEvent() { $viewer = $this->getViewer(); $hook_start = $this->getStartTime(); $event = PhabricatorRepositoryPushEvent::initializeNewEvent($viewer) ->setRepositoryPHID($this->getRepository()->getPHID()) ->setRemoteAddress($this->getRemoteAddress()) ->setRemoteProtocol($this->getRemoteProtocol()) ->setEpoch(PhabricatorTime::getNow()) ->setHookWait(phutil_microseconds_since($hook_start)); $identifier = $this->getRequestIdentifier(); if (strlen($identifier)) { $event->setRequestIdentifier($identifier); } return $event; } private function rejectEnormousChanges(array $content_updates) { $repository = $this->getRepository(); if ($repository->shouldAllowEnormousChanges()) { return; } // See T13142. Don't cache more than 64MB of changesets. For normal small // pushes, caching everything here can let us hit the cache from Herald if // we need to run content rules, which speeds things up a bit. For large // pushes, we may not be able to hold everything in memory. $cache_limit = 1024 * 1024 * 64; foreach ($content_updates as $update) { $identifier = $update->getRefNew(); try { $info = $this->loadChangesetsForCommit($identifier); list($changesets, $size) = $info; if ($this->changesetsSize + $size <= $cache_limit) { $this->changesets[$identifier] = $changesets; $this->changesetsSize += $size; } } catch (Exception $ex) { $this->changesets[$identifier] = $ex; $message = pht( 'ENORMOUS CHANGE'. "\n". 'Enormous change protection is enabled for this repository, but '. 'you are pushing an enormous change ("%s"). Edit the repository '. 'configuration before making enormous changes.'. "\n\n". "Content Exception: %s", $identifier, $ex->getMessage()); throw new DiffusionCommitHookRejectException($message); } } } private function loadChangesetsForCommit($identifier) { $byte_limit = HeraldCommitAdapter::getEnormousByteLimit(); $time_limit = HeraldCommitAdapter::getEnormousTimeLimit(); $vcs = $this->getRepository()->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: // For git and hg, we can use normal commands. $drequest = DiffusionRequest::newFromDictionary( array( 'repository' => $this->getRepository(), 'user' => $this->getViewer(), 'commit' => $identifier, )); $raw_diff = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest) ->setTimeout($time_limit) ->setByteLimit($byte_limit) ->setLinesOfContext(0) ->executeInline(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: // TODO: This diff has 3 lines of context, which produces slightly // incorrect "added file content" and "removed file content" results. // This may also choke on binaries, but "svnlook diff" does not support // the "--diff-cmd" flag. // For subversion, we need to use `svnlook`. $future = new ExecFuture( 'svnlook diff -t %s %s', $this->subversionTransaction, $this->subversionRepository); $future->setTimeout($time_limit); $future->setStdoutSizeLimit($byte_limit); $future->setStderrSizeLimit($byte_limit); list($raw_diff) = $future->resolvex(); break; default: throw new Exception(pht("Unknown VCS '%s!'", $vcs)); } if (strlen($raw_diff) >= $byte_limit) { throw new Exception( pht( 'The raw text of this change ("%s") is enormous (larger than %s '. 'bytes).', $identifier, new PhutilNumber($byte_limit))); } if (!strlen($raw_diff)) { // If the commit is actually empty, just return no changesets. return array(array(), 0); } $parser = new ArcanistDiffParser(); $changes = $parser->parseDiff($raw_diff); $diff = DifferentialDiff::newEphemeralFromRawChanges( $changes); $changesets = $diff->getChangesets(); $size = strlen($raw_diff); return array($changesets, $size); } public function getChangesetsForCommit($identifier) { if (isset($this->changesets[$identifier])) { $cached = $this->changesets[$identifier]; if ($cached instanceof Exception) { throw $cached; } return $cached; } $info = $this->loadChangesetsForCommit($identifier); list($changesets, $size) = $info; return $changesets; } + private function rejectOversizedFiles(array $content_updates) { + $repository = $this->getRepository(); + + $limit = $repository->getFilesizeLimit(); + if (!$limit) { + return; + } + + foreach ($content_updates as $update) { + $identifier = $update->getRefNew(); + + $sizes = $this->getFileSizesForCommit($identifier); + + foreach ($sizes as $path => $size) { + if ($size <= $limit) { + continue; + } + + $message = pht( + 'OVERSIZED FILE'. + "\n". + 'This repository ("%s") is configured with a maximum individual '. + 'file size limit, but you are pushing a change ("%s") which causes '. + 'the size of a file ("%s") to exceed the limit. The commit makes '. + 'the file %s bytes long, but the limit for this repository is '. + '%s bytes.', + $repository->getDisplayName(), + $identifier, + $path, + new PhutilNumber($size), + new PhutilNumber($limit)); + + throw new DiffusionCommitHookRejectException($message); + } + } + } + + private function rejectCommitsAffectingTooManyPaths(array $content_updates) { + $repository = $this->getRepository(); + + $limit = $repository->getTouchLimit(); + if (!$limit) { + return; + } + + foreach ($content_updates as $update) { + $identifier = $update->getRefNew(); + + $sizes = $this->getFileSizesForCommit($identifier); + if (count($sizes) > $limit) { + $message = pht( + 'COMMIT AFFECTS TOO MANY PATHS'. + "\n". + 'This repository ("%s") is configured with a touched files limit '. + 'that caps the maximum number of paths any single commit may '. + 'affect. You are pushing a change ("%s") which exceeds this '. + 'limit: it affects %s paths, but the largest number of paths any '. + 'commit may affect is %s paths.', + $repository->getDisplayName(), + $identifier, + phutil_count($sizes), + new PhutilNumber($limit)); + + throw new DiffusionCommitHookRejectException($message); + } + } + } + + public function getFileSizesForCommit($identifier) { + if (!isset($this->filesizeCache[$identifier])) { + $file_sizes = $this->loadFileSizesForCommit($identifier); + $this->filesizeCache[$identifier] = $file_sizes; + } + + return $this->filesizeCache[$identifier]; + } + + private function loadFileSizesForCommit($identifier) { + $repository = $this->getRepository(); + + return id(new DiffusionLowLevelFilesizeQuery()) + ->setRepository($repository) + ->withIdentifier($identifier) + ->execute(); + } + public function loadCommitRefForCommit($identifier) { $repository = $this->getRepository(); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: return id(new DiffusionLowLevelCommitQuery()) ->setRepository($repository) ->withIdentifier($identifier) ->execute(); case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: // For subversion, we need to use `svnlook`. list($message) = execx( 'svnlook log -t %s %s', $this->subversionTransaction, $this->subversionRepository); return id(new DiffusionCommitRef()) ->setMessage($message); break; default: throw new Exception(pht("Unknown VCS '%s!'", $vcs)); } } public function loadBranches($identifier) { $repository = $this->getRepository(); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: return idx($this->gitCommits, $identifier, array()); case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: // NOTE: This will be "the branch the commit was made to", not // "a list of all branch heads which descend from the commit". // This is consistent with Mercurial, but possibly confusing. return idx($this->mercurialCommits, $identifier, array()); case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: // Subversion doesn't have branches. return array(); } } private function loadCommitInfoForWorker(array $all_updates) { $type_commit = PhabricatorRepositoryPushLog::REFTYPE_COMMIT; $map = array(); foreach ($all_updates as $update) { if ($update->getRefType() != $type_commit) { continue; } $map[$update->getRefNew()] = array(); } foreach ($map as $identifier => $info) { $ref = $this->loadCommitRefForCommit($identifier); $map[$identifier] += array( 'summary' => $ref->getSummary(), 'branches' => $this->loadBranches($identifier), ); } return $map; } private function isInitialImport(array $all_updates) { $repository = $this->getRepository(); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: // There is no meaningful way to import history into Subversion by // pushing. return false; default: break; } // Now, apply a heuristic to guess whether this is a normal commit or // an initial import. We guess something is an initial import if: // // - the repository is currently empty; and // - it pushes more than 7 commits at once. // // The number "7" is chosen arbitrarily as seeming reasonable. We could // also look at author data (do the commits come from multiple different // authors?) and commit date data (is the oldest commit more than 48 hours // old), but we don't have immediate access to those and this simple // heuristic might be good enough. $commit_count = 0; $type_commit = PhabricatorRepositoryPushLog::REFTYPE_COMMIT; foreach ($all_updates as $update) { if ($update->getRefType() != $type_commit) { continue; } $commit_count++; } if ($commit_count <= PhabricatorRepository::IMPORT_THRESHOLD) { // If this pushes a very small number of commits, assume it's an // initial commit or stack of a few initial commits. return false; } $any_commits = id(new DiffusionCommitQuery()) ->setViewer($this->getViewer()) ->withRepository($repository) ->setLimit(1) ->execute(); if ($any_commits) { // If the repository already has commits, this isn't an import. return false; } return true; } } diff --git a/src/applications/diffusion/management/DiffusionRepositoryActionsManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryActionsManagementPanel.php index a5805fd0cc..eb1b98dba7 100644 --- a/src/applications/diffusion/management/DiffusionRepositoryActionsManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositoryActionsManagementPanel.php @@ -1,64 +1,86 @@ getRepository(); + + $has_any = + $repository->getDetail('herald-disabled') || + $repository->getDetail('disable-autoclose'); + + // NOTE: Any value here really means something is disabled, so try to + // hint that a little bit with the icon. + + if ($has_any) { + return 'fa-flash'; + } else { + return 'fa-flash grey'; + } } protected function getEditEngineFieldKeys() { return array( 'publish', 'autoclose', ); } + public function buildManagementPanelCurtain() { + $repository = $this->getRepository(); + $viewer = $this->getViewer(); + $action_list = $this->newActionList(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $repository, + PhabricatorPolicyCapability::CAN_EDIT); + + $actions_uri = $this->getEditPageURI(); + + $action_list->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-pencil') + ->setName(pht('Edit Actions')) + ->setHref($actions_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + return $this->newCurtainView() + ->setActionList($action_list); + } + public function buildManagementPanelContent() { $repository = $this->getRepository(); $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setViewer($viewer); $notify = $repository->getDetail('herald-disabled') ? pht('Off') : pht('On'); $notify = phutil_tag('em', array(), $notify); $view->addProperty(pht('Publish/Notify'), $notify); $autoclose = $repository->getDetail('disable-autoclose') ? pht('Off') : pht('On'); $autoclose = phutil_tag('em', array(), $autoclose); $view->addProperty(pht('Autoclose'), $autoclose); - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $repository, - PhabricatorPolicyCapability::CAN_EDIT); - - $actions_uri = $this->getEditPageURI(); - - $button = id(new PHUIButtonView()) - ->setTag('a') - ->setIcon('fa-pencil') - ->setText(pht('Edit')) - ->setHref($actions_uri) - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit); - - return $this->newBox(pht('Actions'), $view, array($button)); + return $this->newBox(pht('Actions'), $view); } } diff --git a/src/applications/diffusion/management/DiffusionRepositoryAutomationManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryAutomationManagementPanel.php index 22afd5a53b..ac0c1251bb 100644 --- a/src/applications/diffusion/management/DiffusionRepositoryAutomationManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositoryAutomationManagementPanel.php @@ -1,90 +1,110 @@ isGit(); } protected function getEditEngineFieldKeys() { return array( 'automationBlueprintPHIDs', ); } public function getManagementPanelIcon() { $repository = $this->getRepository(); + if (!$repository->canPerformAutomation()) { + return 'fa-truck grey'; + } + $blueprint_phids = $repository->getAutomationBlueprintPHIDs(); + if (!$blueprint_phids) { + return 'fa-truck grey'; + } $is_authorized = DrydockAuthorizationQuery::isFullyAuthorized( $repository->getPHID(), $blueprint_phids); if (!$is_authorized) { - return 'fa-exclamation-triangle'; + return 'fa-exclamation-triangle yellow'; } return 'fa-truck'; } + public function buildManagementPanelCurtain() { + $repository = $this->getRepository(); + $viewer = $this->getViewer(); + $action_list = $this->newActionList(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $repository, + PhabricatorPolicyCapability::CAN_EDIT); + + $can_test = $can_edit && $repository->canPerformAutomation(); + + $automation_uri = $this->getEditPageURI(); + $test_uri = $repository->getPathURI('edit/testautomation/'); + + $action_list->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-pencil') + ->setName(pht('Edit Automation')) + ->setHref($automation_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + $action_list->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-gamepad') + ->setName(pht('Test Configuration')) + ->setWorkflow(true) + ->setDisabled(!$can_test) + ->setHref($test_uri)); + + return $this->newCurtainView() + ->setActionList($action_list); + } + public function buildManagementPanelContent() { $repository = $this->getRepository(); $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setViewer($viewer); $blueprint_phids = $repository->getAutomationBlueprintPHIDs(); if (!$blueprint_phids) { $blueprint_view = phutil_tag('em', array(), pht('Not Configured')); } else { $blueprint_view = id(new DrydockObjectAuthorizationView()) ->setUser($viewer) ->setObjectPHID($repository->getPHID()) ->setBlueprintPHIDs($blueprint_phids); } $view->addProperty(pht('Automation'), $blueprint_view); - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $repository, - PhabricatorPolicyCapability::CAN_EDIT); - - $can_test = $can_edit && $repository->canPerformAutomation(); - - $automation_uri = $this->getEditPageURI(); - $test_uri = $repository->getPathURI('edit/testautomation/'); - - $edit = id(new PHUIButtonView()) - ->setTag('a') - ->setIcon('fa-pencil') - ->setText(pht('Edit')) - ->setHref($automation_uri) - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit); - - $test = id(new PHUIButtonView()) - ->setTag('a') - ->setIcon('fa-gamepad') - ->setText(pht('Test Config')) - ->setWorkflow(true) - ->setDisabled(!$can_test) - ->setHref($test_uri); - - return $this->newBox(pht('Automation'), $view, array($edit, $test)); + return $this->newBox(pht('Automation'), $view); } } diff --git a/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php index af5e9b2881..bcf3ad6d74 100644 --- a/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php @@ -1,721 +1,721 @@ getRepository(); $viewer = $this->getViewer(); + $action_list = id(new PhabricatorActionListView()) ->setViewer($viewer); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $repository, PhabricatorPolicyCapability::CAN_EDIT); $edit_uri = $this->getEditPageURI(); $activate_uri = $repository->getPathURI('edit/activate/'); $delete_uri = $repository->getPathURI('edit/delete/'); $encoding_uri = $this->getEditPageURI('encoding'); $dangerous_uri = $repository->getPathURI('edit/dangerous/'); $enormous_uri = $repository->getPathURI('edit/enormous/'); + $update_uri = $repository->getPathURI('edit/update/'); if ($repository->isTracked()) { + $activate_icon = 'fa-ban'; $activate_label = pht('Deactivate Repository'); } else { + $activate_icon = 'fa-check'; $activate_label = pht('Activate Repository'); } $should_dangerous = $repository->shouldAllowDangerousChanges(); if ($should_dangerous) { + $dangerous_icon = 'fa-shield'; $dangerous_name = pht('Prevent Dangerous Changes'); $can_dangerous = $can_edit; } else { + $dangerous_icon = 'fa-exclamation-triangle'; $dangerous_name = pht('Allow Dangerous Changes'); $can_dangerous = ($can_edit && $repository->canAllowDangerousChanges()); } $should_enormous = $repository->shouldAllowEnormousChanges(); if ($should_enormous) { + $enormous_icon = 'fa-shield'; $enormous_name = pht('Prevent Enormous Changes'); $can_enormous = $can_edit; } else { + $enormous_icon = 'fa-exclamation-triangle'; $enormous_name = pht('Allow Enormous Changes'); $can_enormous = ($can_edit && $repository->canAllowEnormousChanges()); } $action_list->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Basic Information')) ->setHref($edit_uri) + ->setIcon('fa-pencil') ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $action_list->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Text Encoding')) + ->setIcon('fa-text-width') ->setHref($encoding_uri) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $action_list->addAction( id(new PhabricatorActionView()) ->setName($dangerous_name) ->setHref($dangerous_uri) + ->setIcon($dangerous_icon) ->setDisabled(!$can_dangerous) ->setWorkflow(true)); $action_list->addAction( id(new PhabricatorActionView()) ->setName($enormous_name) ->setHref($enormous_uri) + ->setIcon($enormous_icon) ->setDisabled(!$can_enormous) ->setWorkflow(true)); $action_list->addAction( id(new PhabricatorActionView()) - ->setHref($activate_uri) ->setName($activate_label) + ->setHref($activate_uri) + ->setIcon($activate_icon) ->setDisabled(!$can_edit) ->setWorkflow(true)); + $action_list->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Update Now')) + ->setHref($update_uri) + ->setIcon('fa-refresh') + ->setWorkflow(true) + ->setDisabled(!$can_edit)); + $action_list->addAction( id(new PhabricatorActionView()) ->setType(PhabricatorActionView::TYPE_DIVIDER)); $action_list->addAction( id(new PhabricatorActionView()) ->setName(pht('Delete Repository')) ->setHref($delete_uri) + ->setIcon('fa-times') ->setColor(PhabricatorActionView::RED) ->setDisabled(true) ->setWorkflow(true)); - return $action_list; + return $this->newCurtainView() + ->setActionList($action_list); } public function buildManagementPanelContent() { - - $button = id(new PHUIButtonView()) - ->setTag('a') - ->setText(pht('Actions')) - ->setHref('#') - ->setIcon('fa-bars') - ->addClass('phui-mobile-menu') - ->setDropdownMenu($this->buildActionMenu()); - $basics = $this->buildBasics(); - $basics = $this->newBox(pht('Properties'), $basics, array($button)); + $basics = $this->newBox(pht('Properties'), $basics); $repository = $this->getRepository(); $is_new = $repository->isNewlyInitialized(); $info_view = null; if ($is_new) { $messages = array(); $messages[] = pht( 'This newly created repository is not active yet. Configure policies, '. 'options, and URIs. When ready, %s the repository.', phutil_tag('strong', array(), pht('Activate'))); if ($repository->isHosted()) { $messages[] = pht( 'If activated now, this repository will become a new hosted '. 'repository. To observe an existing repository instead, configure '. 'it in the %s panel.', phutil_tag('strong', array(), pht('URIs'))); } else { $messages[] = pht( 'If activated now, this repository will observe an existing remote '. 'repository and begin importing changes.'); } $info_view = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->setErrors($messages); } $description = $this->buildDescription(); if ($description) { $description = $this->newBox(pht('Description'), $description); } $status = $this->buildStatus(); return array($info_view, $basics, $description, $status); } private function buildBasics() { $repository = $this->getRepository(); $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setViewer($viewer); $name = $repository->getName(); $view->addProperty(pht('Name'), $name); $type = PhabricatorRepositoryType::getNameForRepositoryType( $repository->getVersionControlSystem()); $view->addProperty(pht('Type'), $type); $callsign = $repository->getCallsign(); if (!strlen($callsign)) { $callsign = phutil_tag('em', array(), pht('No Callsign')); } $view->addProperty(pht('Callsign'), $callsign); $short_name = $repository->getRepositorySlug(); if ($short_name === null) { $short_name = phutil_tag('em', array(), pht('No Short Name')); } $view->addProperty(pht('Short Name'), $short_name); $encoding = $repository->getDetail('encoding'); if (!$encoding) { $encoding = phutil_tag('em', array(), pht('Use Default (UTF-8)')); } $view->addProperty(pht('Encoding'), $encoding); $can_dangerous = $repository->canAllowDangerousChanges(); if (!$can_dangerous) { $dangerous = phutil_tag('em', array(), pht('Not Preventable')); } else { $should_dangerous = $repository->shouldAllowDangerousChanges(); if ($should_dangerous) { $dangerous = pht('Allowed'); } else { $dangerous = pht('Not Allowed'); } } $view->addProperty(pht('Dangerous Changes'), $dangerous); $can_enormous = $repository->canAllowEnormousChanges(); if (!$can_enormous) { $enormous = phutil_tag('em', array(), pht('Not Preventable')); } else { $should_enormous = $repository->shouldAllowEnormousChanges(); if ($should_enormous) { $enormous = pht('Allowed'); } else { $enormous = pht('Not Allowed'); } } $view->addProperty(pht('Enormous Changes'), $enormous); return $view; } private function buildDescription() { $repository = $this->getRepository(); $viewer = $this->getViewer(); $description = $repository->getDetail('description'); $view = id(new PHUIPropertyListView()) ->setViewer($viewer); if (!strlen($description)) { return null; } else { $description = new PHUIRemarkupView($viewer, $description); } $view->addTextContent($description); return $view; } private function buildStatus() { $repository = $this->getRepository(); $viewer = $this->getViewer(); - $update_uri = $repository->getPathURI('edit/update/'); $view = id(new PHUIPropertyListView()) ->setViewer($viewer); $view->addProperty( pht('Update Frequency'), $this->buildRepositoryUpdateInterval($repository)); $messages = $this->loadStatusMessages($repository); $status = $this->buildRepositoryStatus($repository, $messages); $raw_error = $this->buildRepositoryRawError($repository, $messages); $view->addProperty(pht('Status'), $status); if ($raw_error) { $view->addSectionHeader(pht('Raw Error')); $view->addTextContent($raw_error); } - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $repository, - PhabricatorPolicyCapability::CAN_EDIT); - - $button = id(new PHUIButtonView()) - ->setTag('a') - ->setIcon('fa-refresh') - ->setText(pht('Update Now')) - ->setWorkflow(true) - ->setDisabled(!$can_edit) - ->setHref($update_uri); - - return $this->newBox(pht('Status'), $view, array($button)); + return $this->newBox(pht('Status'), $view); } private function buildRepositoryUpdateInterval( PhabricatorRepository $repository) { $smart_wait = $repository->loadUpdateInterval(); $doc_href = PhabricatorEnv::getDoclink( 'Diffusion User Guide: Repository Updates'); return array( phutil_format_relative_time_detailed($smart_wait), " \xC2\xB7 ", phutil_tag( 'a', array( 'href' => $doc_href, 'target' => '_blank', ), pht('Learn More')), ); } private function buildRepositoryStatus( PhabricatorRepository $repository, array $messages) { $viewer = $this->getViewer(); $is_cluster = $repository->getAlmanacServicePHID(); $view = new PHUIStatusListView(); if ($repository->isTracked()) { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') ->setTarget(pht('Repository Active'))); } else { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_WARNING, 'bluegrey') ->setTarget(pht('Repository Inactive')) ->setNote( pht('Activate this repository to begin or resume import.'))); return $view; } $binaries = array(); $svnlook_check = false; switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $binaries[] = 'git'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $binaries[] = 'svn'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $binaries[] = 'hg'; break; } if ($repository->isHosted()) { $proto_https = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTPS; $proto_http = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTP; $can_http = $repository->canServeProtocol($proto_http, false) || $repository->canServeProtocol($proto_https, false); if ($can_http) { switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $binaries[] = 'git-http-backend'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $binaries[] = 'svnserve'; $binaries[] = 'svnadmin'; $binaries[] = 'svnlook'; $svnlook_check = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $binaries[] = 'hg'; break; } } $proto_ssh = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH; $can_ssh = $repository->canServeProtocol($proto_ssh, false); if ($can_ssh) { switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $binaries[] = 'git-receive-pack'; $binaries[] = 'git-upload-pack'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $binaries[] = 'svnserve'; $binaries[] = 'svnadmin'; $binaries[] = 'svnlook'; $svnlook_check = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $binaries[] = 'hg'; break; } } } $binaries = array_unique($binaries); if (!$is_cluster) { // We're only checking for binaries if we aren't running with a cluster // configuration. In theory, we could check for binaries on the // repository host machine, but we'd need to make this more complicated // to do that. foreach ($binaries as $binary) { $where = Filesystem::resolveBinary($binary); if (!$where) { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_WARNING, 'red') ->setTarget( pht('Missing Binary %s', phutil_tag('tt', array(), $binary))) ->setNote(pht( "Unable to find this binary in the webserver's PATH. You may ". "need to configure %s.", $this->getEnvConfigLink()))); } else { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') ->setTarget( pht('Found Binary %s', phutil_tag('tt', array(), $binary))) ->setNote(phutil_tag('tt', array(), $where))); } } // This gets checked generically above. However, for svn commit hooks, we // need this to be in environment.append-paths because subversion strips // PATH. if ($svnlook_check) { $where = Filesystem::resolveBinary('svnlook'); if ($where) { $path = substr($where, 0, strlen($where) - strlen('svnlook')); $dirs = PhabricatorEnv::getEnvConfig('environment.append-paths'); $in_path = false; foreach ($dirs as $dir) { if (Filesystem::isDescendant($path, $dir)) { $in_path = true; break; } } if (!$in_path) { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_WARNING, 'red') ->setTarget( pht('Missing Binary %s', phutil_tag('tt', array(), $binary))) ->setNote(pht( 'Unable to find this binary in `%s`. '. 'You need to configure %s and include %s.', 'environment.append-paths', $this->getEnvConfigLink(), $path))); } } } } $doc_href = PhabricatorEnv::getDoclink('Managing Daemons with phd'); $daemon_instructions = pht( 'Use %s to start daemons. See %s.', phutil_tag('tt', array(), 'bin/phd start'), phutil_tag( 'a', array( 'href' => $doc_href, ), pht('Managing Daemons with phd'))); $pull_daemon = id(new PhabricatorDaemonLogQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE) ->withDaemonClasses(array('PhabricatorRepositoryPullLocalDaemon')) ->setLimit(1) ->execute(); if ($pull_daemon) { // TODO: In a cluster environment, we need a daemon on this repository's // host, specifically, and we aren't checking for that right now. This // is a reasonable proxy for things being more-or-less correctly set up, // though. $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') ->setTarget(pht('Pull Daemon Running'))); } else { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_WARNING, 'red') ->setTarget(pht('Pull Daemon Not Running')) ->setNote($daemon_instructions)); } $task_daemon = id(new PhabricatorDaemonLogQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE) ->withDaemonClasses(array('PhabricatorTaskmasterDaemon')) ->setLimit(1) ->execute(); if ($task_daemon) { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') ->setTarget(pht('Task Daemon Running'))); } else { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_WARNING, 'red') ->setTarget(pht('Task Daemon Not Running')) ->setNote($daemon_instructions)); } if ($is_cluster) { // Just omit this status check for now in cluster environments. We // could make a service call and pull it from the repository host // eventually. } else if ($repository->usesLocalWorkingCopy()) { $local_parent = dirname($repository->getLocalPath()); if (Filesystem::pathExists($local_parent)) { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') ->setTarget(pht('Storage Directory OK')) ->setNote(phutil_tag('tt', array(), $local_parent))); } else { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_WARNING, 'red') ->setTarget(pht('No Storage Directory')) ->setNote( pht( 'Storage directory %s does not exist, or is not readable by '. 'the webserver. Create this directory or make it readable.', phutil_tag('tt', array(), $local_parent)))); return $view; } $local_path = $repository->getLocalPath(); $message = idx($messages, PhabricatorRepositoryStatusMessage::TYPE_INIT); if ($message) { switch ($message->getStatusCode()) { case PhabricatorRepositoryStatusMessage::CODE_ERROR: $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_WARNING, 'red') ->setTarget(pht('Initialization Error')) ->setNote($message->getParameter('message'))); return $view; case PhabricatorRepositoryStatusMessage::CODE_OKAY: if (Filesystem::pathExists($local_path)) { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') ->setTarget(pht('Working Copy OK')) ->setNote(phutil_tag('tt', array(), $local_path))); } else { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_WARNING, 'red') ->setTarget(pht('Working Copy Error')) ->setNote( pht( 'Working copy %s has been deleted, or is not '. 'readable by the webserver. Make this directory '. 'readable. If it has been deleted, the daemons should '. 'restore it automatically.', phutil_tag('tt', array(), $local_path)))); return $view; } break; default: $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_CLOCK, 'green') ->setTarget(pht('Initializing Working Copy')) ->setNote(pht('Daemons are initializing the working copy.'))); return $view; } } else { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_CLOCK, 'orange') ->setTarget(pht('No Working Copy Yet')) ->setNote( pht('Waiting for daemons to build a working copy.'))); return $view; } } $message = idx($messages, PhabricatorRepositoryStatusMessage::TYPE_FETCH); if ($message) { switch ($message->getStatusCode()) { case PhabricatorRepositoryStatusMessage::CODE_ERROR: $message = $message->getParameter('message'); $suggestion = null; if (preg_match('/Permission denied \(publickey\)./', $message)) { $suggestion = pht( 'Public Key Error: This error usually indicates that the '. 'keypair you have configured does not have permission to '. 'access the repository.'); } $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_WARNING, 'red') ->setTarget(pht('Update Error')) ->setNote($suggestion)); return $view; case PhabricatorRepositoryStatusMessage::CODE_OKAY: $ago = (PhabricatorTime::getNow() - $message->getEpoch()); $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') ->setTarget(pht('Updates OK')) ->setNote( pht( 'Last updated %s (%s ago).', phabricator_datetime($message->getEpoch(), $viewer), phutil_format_relative_time_detailed($ago)))); break; } } else { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_CLOCK, 'orange') ->setTarget(pht('Waiting For Update')) ->setNote( pht('Waiting for daemons to read updates.'))); } if ($repository->isImporting()) { $ratio = $repository->loadImportProgress(); $percentage = sprintf('%.2f%%', 100 * $ratio); $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_CLOCK, 'green') ->setTarget(pht('Importing')) ->setNote( pht('%s Complete', $percentage))); } else { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') ->setTarget(pht('Fully Imported'))); } if (idx($messages, PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE)) { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_UP, 'indigo') ->setTarget(pht('Prioritized')) ->setNote(pht('This repository will be updated soon!'))); } return $view; } private function buildRepositoryRawError( PhabricatorRepository $repository, array $messages) { $viewer = $this->getViewer(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $repository, PhabricatorPolicyCapability::CAN_EDIT); $raw_error = null; $message = idx($messages, PhabricatorRepositoryStatusMessage::TYPE_FETCH); if ($message) { switch ($message->getStatusCode()) { case PhabricatorRepositoryStatusMessage::CODE_ERROR: $raw_error = $message->getParameter('message'); break; } } if ($raw_error !== null) { if (!$can_edit) { $raw_message = pht( 'You must be able to edit a repository to see raw error messages '. 'because they sometimes disclose sensitive information.'); $raw_message = phutil_tag('em', array(), $raw_message); } else { $raw_message = phutil_escape_html_newlines($raw_error); } } else { $raw_message = null; } return $raw_message; } private function loadStatusMessages(PhabricatorRepository $repository) { $messages = id(new PhabricatorRepositoryStatusMessage()) ->loadAllWhere('repositoryID = %d', $repository->getID()); $messages = mpull($messages, null, 'getStatusType'); return $messages; } private function getEnvConfigLink() { $config_href = '/config/edit/environment.append-paths/'; return phutil_tag( 'a', array( 'href' => $config_href, ), 'environment.append-paths'); } } diff --git a/src/applications/diffusion/management/DiffusionRepositoryBranchesManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryBranchesManagementPanel.php index 7c7f4bcf63..e3ace3a5a5 100644 --- a/src/applications/diffusion/management/DiffusionRepositoryBranchesManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositoryBranchesManagementPanel.php @@ -1,174 +1,194 @@ isGit() || $repository->isHg()); } public function getManagementPanelIcon() { - return 'fa-code-fork'; + $repository = $this->getRepository(); + + $has_any = + $repository->getDetail('default-branch') || + $repository->getDetail('branch-filter') || + $repository->getDetail('close-commits-filter'); + + if ($has_any) { + return 'fa-code-fork'; + } else { + return 'fa-code-fork grey'; + } } protected function getEditEngineFieldKeys() { return array( 'defaultBranch', 'trackOnly', 'autocloseOnly', ); } + public function buildManagementPanelCurtain() { + $repository = $this->getRepository(); + $viewer = $this->getViewer(); + $action_list = $this->newActionList(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $repository, + PhabricatorPolicyCapability::CAN_EDIT); + + $branches_uri = $this->getEditPageURI(); + + $action_list->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-pencil') + ->setName(pht('Edit Branches')) + ->setHref($branches_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + return $this->newCurtainView() + ->setActionList($action_list); + } + public function buildManagementPanelContent() { $repository = $this->getRepository(); $viewer = $this->getViewer(); $content = array(); $view = id(new PHUIPropertyListView()) ->setViewer($viewer); $default_branch = nonempty( $repository->getHumanReadableDetail('default-branch'), phutil_tag('em', array(), $repository->getDefaultBranch())); $view->addProperty(pht('Default Branch'), $default_branch); $track_only = nonempty( $repository->getHumanReadableDetail('branch-filter', array()), phutil_tag('em', array(), pht('Track All Branches'))); $view->addProperty(pht('Track Only'), $track_only); $autoclose_only = nonempty( $repository->getHumanReadableDetail('close-commits-filter', array()), phutil_tag('em', array(), pht('Autoclose On All Branches'))); $autoclose_disabled = false; if ($repository->getDetail('disable-autoclose')) { $autoclose_disabled = true; $autoclose_only = phutil_tag('em', array(), pht('Autoclose has been disabled')); } $view->addProperty(pht('Autoclose Only'), $autoclose_only); - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $repository, - PhabricatorPolicyCapability::CAN_EDIT); - - $branches_uri = $this->getEditPageURI(); - - $button = id(new PHUIButtonView()) - ->setTag('a') - ->setIcon('fa-pencil') - ->setText(pht('Edit')) - ->setHref($branches_uri) - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit); - - $content[] = $this->newBox(pht('Branches'), $view, array($button)); + $content[] = $this->newBox(pht('Branches'), $view); // Branch Autoclose Table if (!$repository->isImporting()) { $request = $this->getRequest(); $pager = id(new PHUIPagerView()) ->readFromRequest($request); $params = array( 'offset' => $pager->getOffset(), 'limit' => $pager->getPageSize() + 1, 'repository' => $repository->getID(), ); $branches = id(new ConduitCall('diffusion.branchquery', $params)) ->setUser($viewer) ->execute(); $branches = DiffusionRepositoryRef::loadAllFromDictionaries($branches); $branches = $pager->sliceResults($branches); $can_close_branches = ($repository->isHg()); $rows = array(); foreach ($branches as $branch) { $branch_name = $branch->getShortName(); $tracking = $repository->shouldTrackBranch($branch_name); $autoclosing = $repository->shouldAutocloseBranch($branch_name); $default = $repository->getDefaultBranch(); $icon = null; if ($default == $branch->getShortName()) { $icon = id(new PHUIIconView()) ->setIcon('fa-code-fork'); } $fields = $branch->getRawFields(); $closed = idx($fields, 'closed'); if ($closed) { $status = pht('Closed'); } else { $status = pht('Open'); } if ($autoclose_disabled) { $autoclose_status = pht('Disabled (Repository)'); } else { $autoclose_status = pht('Off'); } $rows[] = array( $icon, $branch_name, $status, $tracking ? pht('Tracking') : pht('Off'), $autoclosing ? pht('Autoclose On') : $autoclose_status, ); } $branch_table = new AphrontTableView($rows); $branch_table->setHeaders( array( '', pht('Branch'), pht('Status'), pht('Track'), pht('Autoclose'), )); $branch_table->setColumnClasses( array( '', 'pri', 'narrow', 'narrow', 'wide', )); $branch_table->setColumnVisibility( array( true, true, $can_close_branches, true, true, )); $box = $this->newBox(pht('Branch Status'), $branch_table); $box->setPager($pager); $content[] = $box; } else { $content[] = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->appendChild(pht('Branch status in unavailable while the repository '. 'is still importing.')); } return $content; } } diff --git a/src/applications/diffusion/management/DiffusionRepositoryLimitsManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryLimitsManagementPanel.php new file mode 100644 index 0000000000..b57ed5cf56 --- /dev/null +++ b/src/applications/diffusion/management/DiffusionRepositoryLimitsManagementPanel.php @@ -0,0 +1,112 @@ +isGit(); + } + + public function getManagementPanelIcon() { + $repository = $this->getRepository(); + + $any_limit = false; + + if ($repository->getFilesizeLimit()) { + $any_limit = true; + } + + if ($any_limit) { + return 'fa-signal'; + } else { + return 'fa-signal grey'; + } + } + + protected function getEditEngineFieldKeys() { + return array( + 'filesizeLimit', + 'copyTimeLimit', + 'touchLimit', + ); + } + + public function buildManagementPanelCurtain() { + $repository = $this->getRepository(); + $viewer = $this->getViewer(); + $action_list = $this->newActionList(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $repository, + PhabricatorPolicyCapability::CAN_EDIT); + + $limits_uri = $this->getEditPageURI(); + + $action_list->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-pencil') + ->setName(pht('Edit Limits')) + ->setHref($limits_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + return $this->newCurtainView() + ->setActionList($action_list); + } + + public function buildManagementPanelContent() { + $repository = $this->getRepository(); + $viewer = $this->getViewer(); + + $view = id(new PHUIPropertyListView()) + ->setViewer($viewer); + + $byte_limit = $repository->getFilesizeLimit(); + if ($byte_limit) { + $filesize_display = pht('%s Bytes', new PhutilNumber($byte_limit)); + } else { + $filesize_display = pht('Unlimited'); + $filesize_display = phutil_tag('em', array(), $filesize_display); + } + + $view->addProperty(pht('Filesize Limit'), $filesize_display); + + $copy_limit = $repository->getCopyTimeLimit(); + if ($copy_limit) { + $copy_display = pht('%s Seconds', new PhutilNumber($copy_limit)); + } else { + $copy_default = $repository->getDefaultCopyTimeLimit(); + $copy_display = pht( + 'Default (%s Seconds)', + new PhutilNumber($copy_default)); + $copy_display = phutil_tag('em', array(), $copy_display); + } + + $view->addProperty(pht('Clone/Fetch Timeout'), $copy_display); + + $touch_limit = $repository->getTouchLimit(); + if ($touch_limit) { + $touch_display = pht('%s Paths', new PhutilNumber($touch_limit)); + } else { + $touch_display = pht('Unlimited'); + $touch_display = phutil_tag('em', array(), $touch_display); + } + + $view->addProperty(pht('Touched Paths Limit'), $touch_display); + + return $this->newBox(pht('Limits'), $view); + } + +} diff --git a/src/applications/diffusion/management/DiffusionRepositoryManagementBuildsPanelGroup.php b/src/applications/diffusion/management/DiffusionRepositoryManagementBuildsPanelGroup.php new file mode 100644 index 0000000000..e80901b494 --- /dev/null +++ b/src/applications/diffusion/management/DiffusionRepositoryManagementBuildsPanelGroup.php @@ -0,0 +1,16 @@ +viewer = $viewer; return $this; } final public function getViewer() { return $this->viewer; } final public function setRepository(PhabricatorRepository $repository) { $this->repository = $repository; return $this; } final public function getRepository() { return $this->repository; } final public function getRequest() { return $this->controller->getRequest(); } final public function setController(PhabricatorController $controller) { $this->controller = $controller; return $this; } final public function getManagementPanelKey() { return $this->getPhobjectClassConstant('PANELKEY'); } abstract public function getManagementPanelLabel(); abstract public function getManagementPanelOrder(); abstract public function buildManagementPanelContent(); + public function buildManagementPanelCurtain() { return null; } public function getManagementPanelIcon() { return 'fa-pencil'; } - protected function buildManagementPanelActions() { - return array(); + public function getManagementPanelGroupKey() { + return DiffusionRepositoryManagementMainPanelGroup::PANELGROUPKEY; } public function shouldEnableForRepository( PhabricatorRepository $repository) { return true; } public static function getAllPanels() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setUniqueMethod('getManagementPanelKey') ->setSortMethod('getManagementPanelOrder') ->execute(); } - final protected function newBox($header_text, $body, $button = array()) { - $header = id(new PHUIHeaderView()) - ->setHeader($header_text); - - foreach ($button as $link) { - $header->addActionLink($link); - } - - $view = id(new PHUIObjectBoxView()) - ->setHeader($header) - ->setBackground(PHUIObjectBoxView::WHITE_CONFIG) - ->appendChild($body); - - return $view; - } - final protected function newTimeline() { return $this->controller->newTimeline($this->getRepository()); } final public function getPanelURI() { $repository = $this->getRepository(); $key = $this->getManagementPanelKey(); return $repository->getPathURI("manage/{$key}/"); } final public function newEditEnginePage() { $field_keys = $this->getEditEngineFieldKeys(); if (!$field_keys) { return null; } $key = $this->getManagementPanelKey(); $label = $this->getManagementPanelLabel(); $panel_uri = $this->getPanelURI(); return id(new PhabricatorEditPage()) ->setKey($key) ->setLabel($label) ->setViewURI($panel_uri) ->setFieldKeys($field_keys); } protected function getEditEngineFieldKeys() { return array(); } protected function getEditPageURI($page = null) { if ($page === null) { $page = $this->getManagementPanelKey(); } $repository = $this->getRepository(); $id = $repository->getID(); return "/diffusion/edit/{$id}/page/{$page}/"; } public function getPanelNavigationURI() { return $this->getPanelURI(); } + final protected function newActionList() { + $viewer = $this->getViewer(); + $action_id = celerity_generate_unique_node_id(); + + return id(new PhabricatorActionListView()) + ->setViewer($viewer) + ->setID($action_id); + } + + final protected function newCurtainView() { + $viewer = $this->getViewer(); + + return id(new PHUICurtainView()) + ->setViewer($viewer); + } + + final protected function newBox($header_text, $body) { + $viewer = $this->getViewer(); + + $header = id(new PHUIHeaderView()) + ->setViewer($viewer) + ->setHeader($header_text); + + $view = id(new PHUIObjectBoxView()) + ->setViewer($viewer) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($body); + + return $view; + } + } diff --git a/src/applications/diffusion/management/DiffusionRepositoryManagementPanelGroup.php b/src/applications/diffusion/management/DiffusionRepositoryManagementPanelGroup.php new file mode 100644 index 0000000000..61e3f19c3b --- /dev/null +++ b/src/applications/diffusion/management/DiffusionRepositoryManagementPanelGroup.php @@ -0,0 +1,21 @@ +getPhobjectClassConstant('PANELGROUPKEY'); + } + + abstract public function getManagementPanelGroupOrder(); + abstract public function getManagementPanelGroupLabel(); + + public static function getAllPanelGroups() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getManagementPanelGroupKey') + ->setSortMethod('getManagementPanelGroupOrder') + ->execute(); + } + +} diff --git a/src/applications/diffusion/management/DiffusionRepositoryPoliciesManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryPoliciesManagementPanel.php index 3da6ea80dc..7cada92fed 100644 --- a/src/applications/diffusion/management/DiffusionRepositoryPoliciesManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositoryPoliciesManagementPanel.php @@ -1,79 +1,117 @@ getViewer(); + $repository = $this->getRepository(); + + $can_view = PhabricatorPolicyCapability::CAN_VIEW; + $can_edit = PhabricatorPolicyCapability::CAN_EDIT; + $can_push = DiffusionPushCapability::CAPABILITY; + + $actual_values = array( + 'spacePHID' => $repository->getSpacePHID(), + 'view' => $repository->getPolicy($can_view), + 'edit' => $repository->getPolicy($can_edit), + 'push' => $repository->getPolicy($can_push), + ); + + $default = PhabricatorRepository::initializeNewRepository( + $viewer); + + $default_values = array( + 'spacePHID' => $default->getSpacePHID(), + 'view' => $default->getPolicy($can_view), + 'edit' => $default->getPolicy($can_edit), + 'push' => $default->getPolicy($can_push), + ); + + if ($actual_values === $default_values) { + return 'fa-lock grey'; + } else { + return 'fa-lock'; + } } protected function getEditEngineFieldKeys() { return array( 'policy.view', 'policy.edit', 'spacePHID', 'policy.push', ); } + public function buildManagementPanelCurtain() { + $repository = $this->getRepository(); + $viewer = $this->getViewer(); + $action_list = $this->newActionList(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $repository, + PhabricatorPolicyCapability::CAN_EDIT); + + $edit_uri = $this->getEditPageURI(); + + $action_list->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Policies')) + ->setHref($edit_uri) + ->setIcon('fa-pencil') + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + return $this->newCurtainView() + ->setActionList($action_list); + } + + public function buildManagementPanelContent() { $repository = $this->getRepository(); $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setViewer($viewer); $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( $viewer, $repository); $view_parts = array(); if (PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($viewer)) { $space_phid = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID( $repository); $view_parts[] = $viewer->renderHandle($space_phid); } $view_parts[] = $descriptions[PhabricatorPolicyCapability::CAN_VIEW]; $view->addProperty( pht('Visible To'), phutil_implode_html(" \xC2\xB7 ", $view_parts)); $view->addProperty( pht('Editable By'), $descriptions[PhabricatorPolicyCapability::CAN_EDIT]); $pushable = $repository->isHosted() ? $descriptions[DiffusionPushCapability::CAPABILITY] : phutil_tag('em', array(), pht('Not a Hosted Repository')); $view->addProperty(pht('Pushable By'), $pushable); - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $repository, - PhabricatorPolicyCapability::CAN_EDIT); - - $edit_uri = $this->getEditPageURI(); - - $button = id(new PHUIButtonView()) - ->setTag('a') - ->setIcon('fa-pencil') - ->setText(pht('Edit')) - ->setHref($edit_uri) - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit); - - return $this->newBox(pht('Policies'), $view, array($button)); + return $this->newBox(pht('Policies'), $view); } } diff --git a/src/applications/diffusion/management/DiffusionRepositoryStagingManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryStagingManagementPanel.php index ebd970f032..2d92e08dfa 100644 --- a/src/applications/diffusion/management/DiffusionRepositoryStagingManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositoryStagingManagementPanel.php @@ -1,64 +1,84 @@ isGit(); } - public function getManagementPanelIcon() { - return 'fa-upload'; + $repository = $this->getRepository(); + + $staging_uri = $repository->getStagingURI(); + + if ($staging_uri) { + return 'fa-upload'; + } else { + return 'fa-upload grey'; + } } protected function getEditEngineFieldKeys() { return array( 'stagingAreaURI', ); } + public function buildManagementPanelCurtain() { + $repository = $this->getRepository(); + $viewer = $this->getViewer(); + $action_list = $this->newActionList(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $repository, + PhabricatorPolicyCapability::CAN_EDIT); + + $staging_uri = $this->getEditPageURI(); + + $action_list->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-pencil') + ->setName(pht('Edit Staging')) + ->setHref($staging_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + return $this->newCurtainView() + ->setActionList($action_list); + } + public function buildManagementPanelContent() { $repository = $this->getRepository(); $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setViewer($viewer); $staging_uri = $repository->getStagingURI(); if (!$staging_uri) { $staging_uri = phutil_tag('em', array(), pht('No Staging Area')); } $view->addProperty(pht('Staging Area URI'), $staging_uri); - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $repository, - PhabricatorPolicyCapability::CAN_EDIT); - - $staging_uri = $this->getEditPageURI(); - - $button = id(new PHUIButtonView()) - ->setTag('a') - ->setIcon('fa-pencil') - ->setText(pht('Edit')) - ->setHref($staging_uri) - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit); - - return $this->newBox(pht('Staging Area'), $view, array($button)); + return $this->newBox(pht('Staging Area'), $view); } } diff --git a/src/applications/diffusion/management/DiffusionRepositoryStorageManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryStorageManagementPanel.php index a39dfaa632..0f09b3dc22 100644 --- a/src/applications/diffusion/management/DiffusionRepositoryStorageManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositoryStorageManagementPanel.php @@ -1,231 +1,265 @@ getRepository(); + + if ($repository->getAlmanacServicePHID()) { + return 'fa-sitemap'; + } else if ($repository->isHosted()) { + return 'fa-database'; + } else { + return 'fa-download'; + } + } + + public function buildManagementPanelCurtain() { + $repository = $this->getRepository(); + $viewer = $this->getViewer(); + $action_list = $this->newActionList(); + + $doc_href = PhabricatorEnv::getDoclink('Cluster: Repositories'); + + $action_list->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-book') + ->setHref($doc_href) + ->setName(pht('Cluster Documentation'))); + + return $this->newCurtainView() + ->setActionList($action_list); } public function buildManagementPanelContent() { return array( $this->buildStorageStatusPanel(), $this->buildClusterStatusPanel(), ); } private function buildStorageStatusPanel() { $repository = $this->getRepository(); $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setViewer($viewer); if ($repository->usesLocalWorkingCopy()) { $storage_path = $repository->getLocalPath(); } else { $storage_path = phutil_tag('em', array(), pht('No Local Working Copy')); } $service_phid = $repository->getAlmanacServicePHID(); if ($service_phid) { $storage_service = $viewer->renderHandle($service_phid); } else { $storage_service = phutil_tag('em', array(), pht('Local')); } $view->addProperty(pht('Storage Path'), $storage_path); $view->addProperty(pht('Storage Cluster'), $storage_service); - $doc_href = PhabricatorEnv::getDoclink('Cluster: Repositories'); - - $button = id(new PHUIButtonView()) - ->setTag('a') - ->setIcon('fa-book') - ->setHref($doc_href) - ->setText(pht('Help')); - - return $this->newBox(pht('Storage'), $view, array($button)); + return $this->newBox(pht('Storage'), $view); } private function buildClusterStatusPanel() { $repository = $this->getRepository(); $viewer = $this->getViewer(); $service_phid = $repository->getAlmanacServicePHID(); if ($service_phid) { $service = id(new AlmanacServiceQuery()) ->setViewer($viewer) ->withServiceTypes( array( AlmanacClusterRepositoryServiceType::SERVICETYPE, )) ->withPHIDs(array($service_phid)) ->needBindings(true) ->executeOne(); if (!$service) { // TODO: Viewer may not have permission to see the service, or it may // be invalid? Raise some more useful error here? throw new Exception(pht('Unable to load cluster service.')); } } else { $service = null; } Javelin::initBehavior('phabricator-tooltips'); $rows = array(); if ($service) { $bindings = $service->getBindings(); $bindings = mgroup($bindings, 'getDevicePHID'); // This is an unusual read which always comes from the master. if (PhabricatorEnv::isReadOnly()) { $versions = array(); } else { $versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions( $repository->getPHID()); } $versions = mpull($versions, null, 'getDevicePHID'); - foreach ($bindings as $binding_group) { - $all_disabled = true; - foreach ($binding_group as $binding) { - if (!$binding->getIsDisabled()) { - $all_disabled = false; - break; - } - } + // List enabled devices first, then sort devices in each group by name. + $sort = array(); + foreach ($bindings as $key => $binding_group) { + $all_disabled = $this->isDisabledGroup($binding_group); + $sort[$key] = id(new PhutilSortVector()) + ->addInt($all_disabled ? 1 : 0) + ->addString(head($binding_group)->getDevice()->getName()); + } + $sort = msortv($sort, 'getSelf'); + $bindings = array_select_keys($bindings, array_keys($sort)) + $bindings; + + foreach ($bindings as $binding_group) { + $all_disabled = $this->isDisabledGroup($binding_group); $any_binding = head($binding_group); if ($all_disabled) { $binding_icon = 'fa-times grey'; $binding_tip = pht('Disabled'); } else { $binding_icon = 'fa-folder-open green'; $binding_tip = pht('Active'); } $binding_icon = id(new PHUIIconView()) ->setIcon($binding_icon) ->addSigil('has-tooltip') ->setMetadata( array( 'tip' => $binding_tip, )); $device = $any_binding->getDevice(); $version = idx($versions, $device->getPHID()); if ($version) { $version_number = $version->getRepositoryVersion(); $href = null; if ($repository->isHosted()) { $href = "/diffusion/pushlog/view/{$version_number}/"; } else { $commit = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withIDs(array($version_number)) ->executeOne(); if ($commit) { $href = $commit->getURI(); } } if ($href) { $version_number = phutil_tag( 'a', array( 'href' => $href, ), $version_number); } } else { $version_number = '-'; } if ($version && $version->getIsWriting()) { $is_writing = id(new PHUIIconView()) ->setIcon('fa-pencil green'); } else { $is_writing = id(new PHUIIconView()) ->setIcon('fa-pencil grey'); } $write_properties = null; if ($version) { $write_properties = $version->getWriteProperties(); if ($write_properties) { try { $write_properties = phutil_json_decode($write_properties); } catch (Exception $ex) { $write_properties = null; } } } if ($write_properties) { $writer_phid = idx($write_properties, 'userPHID'); $last_writer = $viewer->renderHandle($writer_phid); $writer_epoch = idx($write_properties, 'epoch'); $writer_epoch = phabricator_datetime($writer_epoch, $viewer); } else { $last_writer = null; $writer_epoch = null; } $rows[] = array( $binding_icon, phutil_tag( 'a', array( 'href' => $device->getURI(), ), $device->getName()), $version_number, $is_writing, $last_writer, $writer_epoch, ); } } $table = id(new AphrontTableView($rows)) ->setNoDataString(pht('This is not a cluster repository.')) ->setHeaders( array( null, pht('Device'), pht('Version'), pht('Writing'), pht('Last Writer'), pht('Last Write At'), )) ->setColumnClasses( array( null, null, null, 'right wide', null, 'date', )); return $this->newBox(pht('Cluster Status'), $table); } + private function isDisabledGroup(array $binding_group) { + assert_instances_of($binding_group, 'AlmanacBinding'); + + foreach ($binding_group as $binding) { + if (!$binding->getIsDisabled()) { + return false; + } + } + + return true; + } + } diff --git a/src/applications/diffusion/management/DiffusionRepositorySubversionManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositorySubversionManagementPanel.php index cd6e2c5be0..f87a5d9b85 100644 --- a/src/applications/diffusion/management/DiffusionRepositorySubversionManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositorySubversionManagementPanel.php @@ -1,61 +1,78 @@ isSVN(); } public function getManagementPanelIcon() { - return 'fa-folder'; + $repository = $this->getRepository(); + + $has_any = (bool)$repository->getDetail('svn-subpath'); + + if ($has_any) { + return 'fa-folder'; + } else { + return 'fa-folder grey'; + } } protected function getEditEngineFieldKeys() { return array( 'importOnly', ); } + public function buildManagementPanelCurtain() { + $repository = $this->getRepository(); + $viewer = $this->getViewer(); + $action_list = $this->newActionList(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $repository, + PhabricatorPolicyCapability::CAN_EDIT); + + $subversion_uri = $this->getEditPageURI(); + + $action_list->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-pencil') + ->setName(pht('Edit Properties')) + ->setHref($subversion_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + return $this->newCurtainView($action_list) + ->setActionList($action_list); + } + public function buildManagementPanelContent() { $repository = $this->getRepository(); $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setViewer($viewer); $default_branch = nonempty( $repository->getHumanReadableDetail('svn-subpath'), phutil_tag('em', array(), pht('Import Entire Repository'))); $view->addProperty(pht('Import Only'), $default_branch); - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $repository, - PhabricatorPolicyCapability::CAN_EDIT); - - $subversion_uri = $this->getEditPageURI(); - - $button = id(new PHUIButtonView()) - ->setTag('a') - ->setIcon('fa-pencil') - ->setText(pht('Edit')) - ->setHref($subversion_uri) - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit); - - return $this->newBox(pht('Subversion'), $view, array($button)); + return $this->newBox(pht('Subversion'), $view); } } diff --git a/src/applications/diffusion/management/DiffusionRepositorySymbolsManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositorySymbolsManagementPanel.php index f64fbf8d6e..63a02dcf61 100644 --- a/src/applications/diffusion/management/DiffusionRepositorySymbolsManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositorySymbolsManagementPanel.php @@ -1,68 +1,91 @@ getRepository(); + + $has_any = + $repository->getSymbolLanguages() || + $repository->getSymbolSources(); + + if ($has_any) { + return 'fa-link'; + } else { + return 'fa-link grey'; + } } protected function getEditEngineFieldKeys() { return array( 'symbolLanguages', 'symbolRepositoryPHIDs', ); } + public function buildManagementPanelCurtain() { + $repository = $this->getRepository(); + $viewer = $this->getViewer(); + $action_list = $this->newActionList(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $repository, + PhabricatorPolicyCapability::CAN_EDIT); + + $symbols_uri = $this->getEditPageURI(); + + $action_list->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-pencil') + ->setName(pht('Edit Symbols')) + ->setHref($symbols_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + return $this->newCurtainView() + ->setActionList($action_list); + } + public function buildManagementPanelContent() { $repository = $this->getRepository(); $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setViewer($viewer); $languages = $repository->getSymbolLanguages(); if ($languages) { $languages = implode(', ', $languages); } else { $languages = phutil_tag('em', array(), pht('Any')); } $view->addProperty(pht('Languages'), $languages); $sources = $repository->getSymbolSources(); if ($sources) { $sources = $viewer->renderHandleList($sources); } else { $sources = phutil_tag('em', array(), pht('This Repository Only')); } $view->addProperty(pht('Uses Symbols From'), $sources); - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $repository, - PhabricatorPolicyCapability::CAN_EDIT); - - $symbols_uri = $this->getEditPageURI(); - - $button = id(new PHUIButtonView()) - ->setTag('a') - ->setIcon('fa-pencil') - ->setText(pht('Edit')) - ->setHref($symbols_uri) - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit); - - return $this->newBox(pht('Symbols'), $view, array($button)); + return $this->newBox(pht('Symbols'), $view); } } diff --git a/src/applications/diffusion/management/DiffusionRepositoryURIsManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryURIsManagementPanel.php index 220bf4e2c1..a795c955a7 100644 --- a/src/applications/diffusion/management/DiffusionRepositoryURIsManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositoryURIsManagementPanel.php @@ -1,151 +1,160 @@ getRepository(); + $viewer = $this->getViewer(); + $action_list = $this->newActionList(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $repository, + PhabricatorPolicyCapability::CAN_EDIT); + + $doc_href = PhabricatorEnv::getDoclink('Diffusion User Guide: URIs'); + $add_href = $repository->getPathURI('uri/edit/'); + + $action_list->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-plus') + ->setHref($add_href) + ->setDisabled(!$can_edit) + ->setName(pht('Add New URI'))); + + $action_list->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-book') + ->setHref($doc_href) + ->setName(pht('URI Documentation'))); + + return $this->newCurtainView() + ->setActionList($action_list); + } + public function buildManagementPanelContent() { $repository = $this->getRepository(); $viewer = $this->getViewer(); $uris = $repository->getURIs(); Javelin::initBehavior('phabricator-tooltips'); $rows = array(); foreach ($uris as $uri) { $uri_name = $uri->getDisplayURI(); $uri_name = phutil_tag( 'a', array( 'href' => $uri->getViewURI(), ), $uri_name); if ($uri->getIsDisabled()) { $status_icon = 'fa-times grey'; } else { $status_icon = 'fa-check green'; } $uri_status = id(new PHUIIconView())->setIcon($status_icon); $io_type = $uri->getEffectiveIOType(); $io_map = PhabricatorRepositoryURI::getIOTypeMap(); $io_spec = idx($io_map, $io_type, array()); $io_icon = idx($io_spec, 'icon'); $io_color = idx($io_spec, 'color'); $io_label = idx($io_spec, 'label', $io_type); $uri_io = array( id(new PHUIIconView())->setIcon("{$io_icon} {$io_color}"), ' ', $io_label, ); $display_type = $uri->getEffectiveDisplayType(); $display_map = PhabricatorRepositoryURI::getDisplayTypeMap(); $display_spec = idx($display_map, $display_type, array()); $display_icon = idx($display_spec, 'icon'); $display_color = idx($display_spec, 'color'); $display_label = idx($display_spec, 'label', $display_type); $uri_display = array( id(new PHUIIconView())->setIcon("{$display_icon} {$display_color}"), ' ', $display_label, ); $rows[] = array( $uri_status, $uri_name, $uri_io, $uri_display, ); } $table = id(new AphrontTableView($rows)) ->setNoDataString(pht('This repository has no URIs.')) ->setHeaders( array( null, pht('URI'), pht('I/O'), pht('Display'), )) ->setColumnClasses( array( null, 'pri wide', null, null, )); $is_new = $repository->isNewlyInitialized(); $messages = array(); if ($repository->isHosted()) { if ($is_new) { $host_message = pht('Phabricator will host this repository.'); } else { $host_message = pht('Phabricator is hosting this repository.'); } $messages[] = $host_message; } else { if ($is_new) { $observe_message = pht( 'Phabricator will observe a remote repository.'); } else { $observe_message = pht( 'This repository is hosted remotely. Phabricator is observing it.'); } $messages[] = $observe_message; } $info_view = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->setErrors($messages); - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $repository, - PhabricatorPolicyCapability::CAN_EDIT); - - $doc_href = PhabricatorEnv::getDoclink('Diffusion User Guide: URIs'); - $add_href = $repository->getPathURI('uri/edit/'); - - $add = id(new PHUIButtonView()) - ->setTag('a') - ->setIcon('fa-plus') - ->setHref($add_href) - ->setDisabled(!$can_edit) - ->setText(pht('New URI')); - - $help = id(new PHUIButtonView()) - ->setTag('a') - ->setIcon('fa-book') - ->setHref($doc_href) - ->setText(pht('Help')); - - $box = $this->newBox(pht('Repository URIs'), $table, array($add, $help)); + $box = $this->newBox(pht('Repository URIs'), $table); - return array($box, $info_view); + return array($info_view, $box); } } diff --git a/src/applications/diffusion/protocol/DiffusionCommandEngine.php b/src/applications/diffusion/protocol/DiffusionCommandEngine.php index d7da62b11d..a0fe568168 100644 --- a/src/applications/diffusion/protocol/DiffusionCommandEngine.php +++ b/src/applications/diffusion/protocol/DiffusionCommandEngine.php @@ -1,303 +1,304 @@ canBuildForRepository($repository)) { return id(clone $engine) ->setRepository($repository); } } throw new Exception( pht( 'No registered command engine can build commands for this '. 'repository ("%s").', $repository->getDisplayName())); } private static function newCommandEngines() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->execute(); } abstract protected function canBuildForRepository( PhabricatorRepository $repository); abstract protected function newFormattedCommand($pattern, array $argv); abstract protected function newCustomEnvironment(); public function setRepository(PhabricatorRepository $repository) { $this->repository = $repository; return $this; } public function getRepository() { return $this->repository; } public function setURI(PhutilURI $uri) { $this->uri = $uri; $this->setProtocol($uri->getProtocol()); return $this; } public function getURI() { return $this->uri; } public function setProtocol($protocol) { $this->protocol = $protocol; return $this; } public function getProtocol() { return $this->protocol; } public function getDisplayProtocol() { return $this->getProtocol().'://'; } public function setCredentialPHID($credential_phid) { $this->credentialPHID = $credential_phid; return $this; } public function getCredentialPHID() { return $this->credentialPHID; } public function setArgv(array $argv) { $this->argv = $argv; return $this; } public function getArgv() { return $this->argv; } public function setPassthru($passthru) { $this->passthru = $passthru; return $this; } public function getPassthru() { return $this->passthru; } public function setConnectAsDevice($connect_as_device) { $this->connectAsDevice = $connect_as_device; return $this; } public function getConnectAsDevice() { return $this->connectAsDevice; } public function setSudoAsDaemon($sudo_as_daemon) { $this->sudoAsDaemon = $sudo_as_daemon; return $this; } public function getSudoAsDaemon() { return $this->sudoAsDaemon; } public function newFuture() { $argv = $this->newCommandArgv(); $env = $this->newCommandEnvironment(); if ($this->getSudoAsDaemon()) { $command = call_user_func_array('csprintf', $argv); $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); $argv = array('%C', $command); } if ($this->getPassthru()) { $future = newv('PhutilExecPassthru', $argv); } else { $future = newv('ExecFuture', $argv); } $future->setEnv($env); // See T13108. By default, don't let any cluster command run indefinitely // to try to avoid cases where `git fetch` hangs for some reason and we're // left sitting with a held lock forever. - $future->setTimeout(phutil_units('15 minutes in seconds')); + $repository = $this->getRepository(); + $future->setTimeout($repository->getEffectiveCopyTimeLimit()); return $future; } private function newCommandArgv() { $argv = $this->argv; $pattern = $argv[0]; $argv = array_slice($argv, 1); list($pattern, $argv) = $this->newFormattedCommand($pattern, $argv); return array_merge(array($pattern), $argv); } private function newCommandEnvironment() { $env = $this->newCommonEnvironment() + $this->newCustomEnvironment(); foreach ($env as $key => $value) { if ($value === null) { unset($env[$key]); } } return $env; } private function newCommonEnvironment() { $repository = $this->getRepository(); $env = array(); // NOTE: Force the language to "en_US.UTF-8", which overrides locale // settings. This makes stuff print in English instead of, e.g., French, // so we can parse the output of some commands, error messages, etc. $env['LANG'] = 'en_US.UTF-8'; // Propagate PHABRICATOR_ENV explicitly. For discussion, see T4155. $env['PHABRICATOR_ENV'] = PhabricatorEnv::getSelectedEnvironmentName(); $as_device = $this->getConnectAsDevice(); $credential_phid = $this->getCredentialPHID(); if ($as_device) { $device = AlmanacKeys::getLiveDevice(); if (!$device) { throw new Exception( pht( 'Attempting to build a repository command (for repository "%s") '. 'as device, but this host ("%s") is not configured as a cluster '. 'device.', $repository->getDisplayName(), php_uname('n'))); } if ($credential_phid) { throw new Exception( pht( 'Attempting to build a repository command (for repository "%s"), '. 'but the CommandEngine is configured to connect as both the '. 'current cluster device ("%s") and with a specific credential '. '("%s"). These options are mutually exclusive. Connections must '. 'authenticate as one or the other, not both.', $repository->getDisplayName(), $device->getName(), $credential_phid)); } } if ($this->isAnySSHProtocol()) { if ($credential_phid) { $env['PHABRICATOR_CREDENTIAL'] = $credential_phid; } if ($as_device) { $env['PHABRICATOR_AS_DEVICE'] = 1; } } $env += $repository->getPassthroughEnvironmentalVariables(); return $env; } public function isSSHProtocol() { return ($this->getProtocol() == 'ssh'); } public function isSVNProtocol() { return ($this->getProtocol() == 'svn'); } public function isSVNSSHProtocol() { return ($this->getProtocol() == 'svn+ssh'); } public function isHTTPProtocol() { return ($this->getProtocol() == 'http'); } public function isHTTPSProtocol() { return ($this->getProtocol() == 'https'); } public function isAnyHTTPProtocol() { return ($this->isHTTPProtocol() || $this->isHTTPSProtocol()); } public function isAnySSHProtocol() { return ($this->isSSHProtocol() || $this->isSVNSSHProtocol()); } public function isCredentialSupported() { return ($this->getPassphraseProvidesCredentialType() !== null); } public function isCredentialOptional() { if ($this->isAnySSHProtocol()) { return false; } return true; } public function getPassphraseCredentialLabel() { if ($this->isAnySSHProtocol()) { return pht('SSH Key'); } if ($this->isAnyHTTPProtocol() || $this->isSVNProtocol()) { return pht('Password'); } return null; } public function getPassphraseDefaultCredentialType() { if ($this->isAnySSHProtocol()) { return PassphraseSSHPrivateKeyTextCredentialType::CREDENTIAL_TYPE; } if ($this->isAnyHTTPProtocol() || $this->isSVNProtocol()) { return PassphrasePasswordCredentialType::CREDENTIAL_TYPE; } return null; } public function getPassphraseProvidesCredentialType() { if ($this->isAnySSHProtocol()) { return PassphraseSSHPrivateKeyCredentialType::PROVIDES_TYPE; } if ($this->isAnyHTTPProtocol() || $this->isSVNProtocol()) { return PassphrasePasswordCredentialType::PROVIDES_TYPE; } return null; } protected function getSSHWrapper() { $root = dirname(phutil_get_library_root('phabricator')); return $root.'/bin/ssh-connect'; } } diff --git a/src/applications/diffusion/query/DiffusionCachedResolveRefsQuery.php b/src/applications/diffusion/query/DiffusionCachedResolveRefsQuery.php index 0217c97d58..1666056d63 100644 --- a/src/applications/diffusion/query/DiffusionCachedResolveRefsQuery.php +++ b/src/applications/diffusion/query/DiffusionCachedResolveRefsQuery.php @@ -1,187 +1,187 @@ refs = $refs; return $this; } public function withTypes(array $types) { $this->types = $types; return $this; } protected function executeQuery() { if (!$this->refs) { return array(); } switch ($this->getRepository()->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $result = $this->resolveGitAndMercurialRefs(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $result = $this->resolveSubversionRefs(); break; default: throw new Exception(pht('Unsupported repository type!')); } if ($this->types !== null) { $result = $this->filterRefsByType($result, $this->types); } return $result; } /** * Resolve refs in Git and Mercurial repositories. * * We can resolve commit hashes from the commits table, and branch and tag * names from the refcursor table. */ private function resolveGitAndMercurialRefs() { $repository = $this->getRepository(); $conn_r = $repository->establishConnection('r'); $results = array(); $prefixes = array(); foreach ($this->refs as $ref) { // We require refs to look like hashes and be at least 4 characters // long. This is similar to the behavior of git. if (preg_match('/^[a-f0-9]{4,}$/', $ref)) { $prefixes[] = qsprintf( $conn_r, '(commitIdentifier LIKE %>)', $ref); } } if ($prefixes) { $commits = queryfx_all( $conn_r, 'SELECT commitIdentifier FROM %T - WHERE repositoryID = %s AND %Q', + WHERE repositoryID = %s AND %LO', id(new PhabricatorRepositoryCommit())->getTableName(), $repository->getID(), - implode(' OR ', $prefixes)); + $prefixes); foreach ($commits as $commit) { $hash = $commit['commitIdentifier']; foreach ($this->refs as $ref) { if (!strncmp($hash, $ref, strlen($ref))) { $results[$ref][] = array( 'type' => 'commit', 'identifier' => $hash, ); } } } } $name_hashes = array(); foreach ($this->refs as $ref) { $name_hashes[PhabricatorHash::digestForIndex($ref)] = $ref; } $cursors = queryfx_all( $conn_r, 'SELECT c.refNameHash, c.refType, p.commitIdentifier, p.isClosed FROM %T c JOIN %T p ON p.cursorID = c.id WHERE c.repositoryPHID = %s AND c.refNameHash IN (%Ls)', id(new PhabricatorRepositoryRefCursor())->getTableName(), id(new PhabricatorRepositoryRefPosition())->getTableName(), $repository->getPHID(), array_keys($name_hashes)); foreach ($cursors as $cursor) { if (isset($name_hashes[$cursor['refNameHash']])) { $results[$name_hashes[$cursor['refNameHash']]][] = array( 'type' => $cursor['refType'], 'identifier' => $cursor['commitIdentifier'], 'closed' => (bool)$cursor['isClosed'], ); // TODO: In Git, we don't store (and thus don't return) the hash // of the tag itself. It would be vaguely nice to do this. } } return $results; } /** * Resolve refs in Subversion repositories. * * We can resolve all numeric identifiers and the keyword `HEAD`. */ private function resolveSubversionRefs() { $repository = $this->getRepository(); $max_commit = id(new PhabricatorRepositoryCommit()) ->loadOneWhere( 'repositoryID = %d ORDER BY epoch DESC, id DESC LIMIT 1', $repository->getID()); if (!$max_commit) { // This repository is empty or hasn't parsed yet, so none of the refs are // going to resolve. return array(); } $max_commit_id = (int)$max_commit->getCommitIdentifier(); $results = array(); foreach ($this->refs as $ref) { if ($ref == 'HEAD') { // Resolve "HEAD" to mean "the most recent commit". $results[$ref][] = array( 'type' => 'commit', 'identifier' => $max_commit_id, ); continue; } if (!preg_match('/^\d+$/', $ref)) { // This ref is non-numeric, so it doesn't resolve to anything. continue; } // Resolve other commits if we can deduce their existence. // TODO: When we import only part of a repository, we won't necessarily // have all of the smaller commits. Should we fail to resolve them here // for repositories with a subpath? It might let us simplify other things // elsewhere. if ((int)$ref <= $max_commit_id) { $results[$ref][] = array( 'type' => 'commit', 'identifier' => (int)$ref, ); } } return $results; } } diff --git a/src/applications/diffusion/query/DiffusionCommitQuery.php b/src/applications/diffusion/query/DiffusionCommitQuery.php index f2d0920b3a..cf8c25baf1 100644 --- a/src/applications/diffusion/query/DiffusionCommitQuery.php +++ b/src/applications/diffusion/query/DiffusionCommitQuery.php @@ -1,852 +1,852 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withAuthorPHIDs(array $phids) { $this->authorPHIDs = $phids; return $this; } /** * Load commits by partial or full identifiers, e.g. "rXab82393", "rX1234", * or "a9caf12". When an identifier matches multiple commits, they will all * be returned; callers should be prepared to deal with more results than * they queried for. */ public function withIdentifiers(array $identifiers) { // Some workflows (like blame lookups) can pass in large numbers of // duplicate identifiers. We only care about unique identifiers, so // get rid of duplicates immediately. $identifiers = array_fuse($identifiers); $this->identifiers = $identifiers; return $this; } /** * Look up commits in a specific repository. This is a shorthand for calling * @{method:withDefaultRepository} and @{method:withRepositoryIDs}. */ public function withRepository(PhabricatorRepository $repository) { $this->withDefaultRepository($repository); $this->withRepositoryIDs(array($repository->getID())); return $this; } /** * Look up commits in a specific repository. Prefer * @{method:withRepositoryIDs}; the underlying table is keyed by ID such * that this method requires a separate initial query to map PHID to ID. */ public function withRepositoryPHIDs(array $phids) { $this->repositoryPHIDs = $phids; return $this; } /** * If a default repository is provided, ambiguous commit identifiers will * be assumed to belong to the default repository. * * For example, "r123" appearing in a commit message in repository X is * likely to be unambiguously "rX123". Normally the reference would be * considered ambiguous, but if you provide a default repository it will * be correctly resolved. */ public function withDefaultRepository(PhabricatorRepository $repository) { $this->defaultRepository = $repository; return $this; } public function withRepositoryIDs(array $repository_ids) { $this->repositoryIDs = array_unique($repository_ids); return $this; } public function needCommitData($need) { $this->needCommitData = $need; return $this; } public function needDrafts($need) { $this->needDrafts = $need; return $this; } public function needIdentities($need) { $this->needIdentities = $need; return $this; } public function needAuditRequests($need) { $this->needAuditRequests = $need; return $this; } public function withAuditIDs(array $ids) { $this->auditIDs = $ids; return $this; } public function withAuditorPHIDs(array $auditor_phids) { $this->auditorPHIDs = $auditor_phids; return $this; } public function withResponsiblePHIDs(array $responsible_phids) { $this->responsiblePHIDs = $responsible_phids; return $this; } public function withPackagePHIDs(array $package_phids) { $this->packagePHIDs = $package_phids; return $this; } public function withUnreachable($unreachable) { $this->unreachable = $unreachable; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } public function withEpochRange($min, $max) { $this->epochMin = $min; $this->epochMax = $max; return $this; } public function withImporting($importing) { $this->importing = $importing; return $this; } public function withAncestorsOf(array $refs) { $this->ancestorsOf = $refs; return $this; } public function getIdentifierMap() { if ($this->identifierMap === null) { throw new Exception( pht( 'You must %s the query before accessing the identifier map.', 'execute()')); } return $this->identifierMap; } protected function getPrimaryTableAlias() { return 'commit'; } protected function willExecute() { if ($this->identifierMap === null) { $this->identifierMap = array(); } } public function newResultObject() { return new PhabricatorRepositoryCommit(); } protected function loadPage() { $table = $this->newResultObject(); $conn = $table->establishConnection('r'); $subqueries = array(); if ($this->responsiblePHIDs) { $base_authors = $this->authorPHIDs; $base_auditors = $this->auditorPHIDs; $responsible_phids = $this->responsiblePHIDs; if ($base_authors) { $all_authors = array_merge($base_authors, $responsible_phids); } else { $all_authors = $responsible_phids; } if ($base_auditors) { $all_auditors = array_merge($base_auditors, $responsible_phids); } else { $all_auditors = $responsible_phids; } $this->authorPHIDs = $all_authors; $this->auditorPHIDs = $base_auditors; $subqueries[] = $this->buildStandardPageQuery( $conn, $table->getTableName()); $this->authorPHIDs = $base_authors; $this->auditorPHIDs = $all_auditors; $subqueries[] = $this->buildStandardPageQuery( $conn, $table->getTableName()); } else { $subqueries[] = $this->buildStandardPageQuery( $conn, $table->getTableName()); } if (count($subqueries) > 1) { foreach ($subqueries as $key => $subquery) { $subqueries[$key] = '('.$subquery.')'; } $query = qsprintf( $conn, '%Q %Q %Q', implode(' UNION DISTINCT ', $subqueries), $this->buildOrderClause($conn, true), $this->buildLimitClause($conn)); } else { $query = head($subqueries); } $rows = queryfx_all($conn, '%Q', $query); $rows = $this->didLoadRawRows($rows); return $table->loadAllFromArray($rows); } protected function willFilterPage(array $commits) { $repository_ids = mpull($commits, 'getRepositoryID', 'getRepositoryID'); $repos = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()) ->withIDs($repository_ids) ->execute(); $min_qualified = PhabricatorRepository::MINIMUM_QUALIFIED_HASH; $result = array(); foreach ($commits as $key => $commit) { $repo = idx($repos, $commit->getRepositoryID()); if ($repo) { $commit->attachRepository($repo); } else { $this->didRejectResult($commit); unset($commits[$key]); continue; } // Build the identifierMap if ($this->identifiers !== null) { $ids = $this->identifiers; $prefixes = array( 'r'.$commit->getRepository()->getCallsign(), 'r'.$commit->getRepository()->getCallsign().':', 'R'.$commit->getRepository()->getID().':', '', // No prefix is valid too and will only match the commitIdentifier ); $suffix = $commit->getCommitIdentifier(); if ($commit->getRepository()->isSVN()) { foreach ($prefixes as $prefix) { if (isset($ids[$prefix.$suffix])) { $result[$prefix.$suffix][] = $commit; } } } else { // This awkward construction is so we can link the commits up in O(N) // time instead of O(N^2). for ($ii = $min_qualified; $ii <= strlen($suffix); $ii++) { $part = substr($suffix, 0, $ii); foreach ($prefixes as $prefix) { if (isset($ids[$prefix.$part])) { $result[$prefix.$part][] = $commit; } } } } } } if ($result) { foreach ($result as $identifier => $matching_commits) { if (count($matching_commits) == 1) { $result[$identifier] = head($matching_commits); } else { // This reference is ambiguous (it matches more than one commit) so // don't link it. unset($result[$identifier]); } } $this->identifierMap += $result; } return $commits; } protected function didFilterPage(array $commits) { $viewer = $this->getViewer(); if ($this->mustFilterRefs) { // If this flag is set, the query has an "Ancestors Of" constraint and // at least one of the constraining refs had too many ancestors for us // to apply the constraint with a big "commitIdentifier IN (%Ls)" clause. // We're going to filter each page and hope we get a full result set // before the query overheats. $ancestor_list = mpull($commits, 'getCommitIdentifier'); $ancestor_list = array_values($ancestor_list); foreach ($this->ancestorsOf as $ref) { try { $ancestor_list = DiffusionQuery::callConduitWithDiffusionRequest( $viewer, DiffusionRequest::newFromDictionary( array( 'repository' => $this->refRepository, 'user' => $viewer, )), 'diffusion.internal.ancestors', array( 'ref' => $ref, 'commits' => $ancestor_list, )); } catch (ConduitClientException $ex) { throw new PhabricatorSearchConstraintException( $ex->getMessage()); } if (!$ancestor_list) { break; } } $ancestor_list = array_fuse($ancestor_list); foreach ($commits as $key => $commit) { $identifier = $commit->getCommitIdentifier(); if (!isset($ancestor_list[$identifier])) { $this->didRejectResult($commit); unset($commits[$key]); } } if (!$commits) { return $commits; } } if ($this->needCommitData) { $data = id(new PhabricatorRepositoryCommitData())->loadAllWhere( 'commitID in (%Ld)', mpull($commits, 'getID')); $data = mpull($data, null, 'getCommitID'); foreach ($commits as $commit) { $commit_data = idx($data, $commit->getID()); if (!$commit_data) { $commit_data = new PhabricatorRepositoryCommitData(); } $commit->attachCommitData($commit_data); } } if ($this->needAuditRequests) { $requests = id(new PhabricatorRepositoryAuditRequest())->loadAllWhere( 'commitPHID IN (%Ls)', mpull($commits, 'getPHID')); $requests = mgroup($requests, 'getCommitPHID'); foreach ($commits as $commit) { $audit_requests = idx($requests, $commit->getPHID(), array()); $commit->attachAudits($audit_requests); foreach ($audit_requests as $audit_request) { $audit_request->attachCommit($commit); } } } if ($this->needIdentities) { $identity_phids = array_merge( mpull($commits, 'getAuthorIdentityPHID'), mpull($commits, 'getCommitterIdentityPHID')); $data = id(new PhabricatorRepositoryIdentityQuery()) ->withPHIDs($identity_phids) ->setViewer($this->getViewer()) ->execute(); $data = mpull($data, null, 'getPHID'); foreach ($commits as $commit) { $author_identity = idx($data, $commit->getAuthorIdentityPHID()); $committer_identity = idx($data, $commit->getCommitterIdentityPHID()); $commit->attachIdentities($author_identity, $committer_identity); } } if ($this->needDrafts) { PhabricatorDraftEngine::attachDrafts( $viewer, $commits); } return $commits; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->repositoryPHIDs !== null) { $map_repositories = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()) ->withPHIDs($this->repositoryPHIDs) ->execute(); if (!$map_repositories) { throw new PhabricatorEmptyQueryException(); } $repository_ids = mpull($map_repositories, 'getID'); if ($this->repositoryIDs !== null) { $repository_ids = array_merge($repository_ids, $this->repositoryIDs); } $this->withRepositoryIDs($repository_ids); } if ($this->ancestorsOf !== null) { if (count($this->repositoryIDs) !== 1) { throw new PhabricatorSearchConstraintException( pht( 'To search for commits which are ancestors of particular refs, '. 'you must constrain the search to exactly one repository.')); } $repository_id = head($this->repositoryIDs); $history_limit = $this->getRawResultLimit() * 32; $viewer = $this->getViewer(); $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withIDs(array($repository_id)) ->executeOne(); if (!$repository) { throw new PhabricatorEmptyQueryException(); } if ($repository->isSVN()) { throw new PhabricatorSearchConstraintException( pht( 'Subversion does not support searching for ancestors of '. 'a particular ref. This operation is not meaningful in '. 'Subversion.')); } if ($repository->isHg()) { throw new PhabricatorSearchConstraintException( pht( 'Mercurial does not currently support searching for ancestors of '. 'a particular ref.')); } $can_constrain = true; $history_identifiers = array(); foreach ($this->ancestorsOf as $key => $ref) { try { $raw_history = DiffusionQuery::callConduitWithDiffusionRequest( $viewer, DiffusionRequest::newFromDictionary( array( 'repository' => $repository, 'user' => $viewer, )), 'diffusion.historyquery', array( 'commit' => $ref, 'limit' => $history_limit, )); } catch (ConduitClientException $ex) { throw new PhabricatorSearchConstraintException( $ex->getMessage()); } $ref_identifiers = array(); foreach ($raw_history['pathChanges'] as $change) { $ref_identifiers[] = $change['commitIdentifier']; } // If this ref had fewer total commits than the limit, we're safe to // apply the constraint as a large `IN (...)` query for a list of // commit identifiers. This is efficient. if ($history_limit) { if (count($ref_identifiers) >= $history_limit) { $can_constrain = false; break; } } $history_identifiers += array_fuse($ref_identifiers); } // If all refs had a small number of ancestors, we can just put the // constraint into the query here and we're done. Otherwise, we need // to filter each page after it comes out of the MySQL layer. if ($can_constrain) { $where[] = qsprintf( $conn, 'commit.commitIdentifier IN (%Ls)', $history_identifiers); } else { $this->mustFilterRefs = true; $this->refRepository = $repository; } } if ($this->ids !== null) { $where[] = qsprintf( $conn, 'commit.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'commit.phid IN (%Ls)', $this->phids); } if ($this->repositoryIDs !== null) { $where[] = qsprintf( $conn, 'commit.repositoryID IN (%Ld)', $this->repositoryIDs); } if ($this->authorPHIDs !== null) { $where[] = qsprintf( $conn, 'commit.authorPHID IN (%Ls)', $this->authorPHIDs); } if ($this->epochMin !== null) { $where[] = qsprintf( $conn, 'commit.epoch >= %d', $this->epochMin); } if ($this->epochMax !== null) { $where[] = qsprintf( $conn, 'commit.epoch <= %d', $this->epochMax); } if ($this->importing !== null) { if ($this->importing) { $where[] = qsprintf( $conn, '(commit.importStatus & %d) != %d', PhabricatorRepositoryCommit::IMPORTED_ALL, PhabricatorRepositoryCommit::IMPORTED_ALL); } else { $where[] = qsprintf( $conn, '(commit.importStatus & %d) = %d', PhabricatorRepositoryCommit::IMPORTED_ALL, PhabricatorRepositoryCommit::IMPORTED_ALL); } } if ($this->identifiers !== null) { $min_unqualified = PhabricatorRepository::MINIMUM_UNQUALIFIED_HASH; $min_qualified = PhabricatorRepository::MINIMUM_QUALIFIED_HASH; $refs = array(); $bare = array(); foreach ($this->identifiers as $identifier) { $matches = null; preg_match('/^(?:[rR]([A-Z]+:?|[0-9]+:))?(.*)$/', $identifier, $matches); $repo = nonempty(rtrim($matches[1], ':'), null); $commit_identifier = nonempty($matches[2], null); if ($repo === null) { if ($this->defaultRepository) { $repo = $this->defaultRepository->getPHID(); } } if ($repo === null) { if (strlen($commit_identifier) < $min_unqualified) { continue; } $bare[] = $commit_identifier; } else { $refs[] = array( 'repository' => $repo, 'identifier' => $commit_identifier, ); } } $sql = array(); foreach ($bare as $identifier) { $sql[] = qsprintf( $conn, '(commit.commitIdentifier LIKE %> AND '. 'LENGTH(commit.commitIdentifier) = 40)', $identifier); } if ($refs) { $repositories = ipull($refs, 'repository'); $repos = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()) ->withIdentifiers($repositories); $repos->execute(); $repos = $repos->getIdentifierMap(); foreach ($refs as $key => $ref) { $repo = idx($repos, $ref['repository']); if (!$repo) { continue; } if ($repo->isSVN()) { if (!ctype_digit((string)$ref['identifier'])) { continue; } $sql[] = qsprintf( $conn, '(commit.repositoryID = %d AND commit.commitIdentifier = %s)', $repo->getID(), // NOTE: Because the 'commitIdentifier' column is a string, MySQL // ignores the index if we hand it an integer. Hand it a string. // See T3377. (int)$ref['identifier']); } else { if (strlen($ref['identifier']) < $min_qualified) { continue; } $identifier = $ref['identifier']; if (strlen($identifier) == 40) { // MySQL seems to do slightly better with this version if the // clause, so issue it if we have a full commit hash. $sql[] = qsprintf( $conn, '(commit.repositoryID = %d AND commit.commitIdentifier = %s)', $repo->getID(), $identifier); } else { $sql[] = qsprintf( $conn, '(commit.repositoryID = %d AND commit.commitIdentifier LIKE %>)', $repo->getID(), $identifier); } } } } if (!$sql) { // If we discarded all possible identifiers (e.g., they all referenced // bogus repositories or were all too short), make sure the query finds // nothing. throw new PhabricatorEmptyQueryException( pht('No commit identifiers.')); } - $where[] = '('.implode(' OR ', $sql).')'; + $where[] = qsprintf($conn, '%LO', $sql); } if ($this->auditIDs !== null) { $where[] = qsprintf( $conn, 'auditor.id IN (%Ld)', $this->auditIDs); } if ($this->auditorPHIDs !== null) { $where[] = qsprintf( $conn, 'auditor.auditorPHID IN (%Ls)', $this->auditorPHIDs); } if ($this->statuses !== null) { $statuses = DiffusionCommitAuditStatus::newModernKeys( $this->statuses); $where[] = qsprintf( $conn, 'commit.auditStatus IN (%Ls)', $statuses); } if ($this->packagePHIDs !== null) { $where[] = qsprintf( $conn, 'package.dst IN (%Ls)', $this->packagePHIDs); } if ($this->unreachable !== null) { if ($this->unreachable) { $where[] = qsprintf( $conn, '(commit.importStatus & %d) = %d', PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE, PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE); } else { $where[] = qsprintf( $conn, '(commit.importStatus & %d) = 0', PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE); } } return $where; } protected function didFilterResults(array $filtered) { if ($this->identifierMap) { foreach ($this->identifierMap as $name => $commit) { if (isset($filtered[$commit->getPHID()])) { unset($this->identifierMap[$name]); } } } } private function shouldJoinAuditor() { return ($this->auditIDs || $this->auditorPHIDs); } private function shouldJoinOwners() { return (bool)$this->packagePHIDs; } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { $join = parent::buildJoinClauseParts($conn); $audit_request = new PhabricatorRepositoryAuditRequest(); if ($this->shouldJoinAuditor()) { $join[] = qsprintf( $conn, 'JOIN %T auditor ON commit.phid = auditor.commitPHID', $audit_request->getTableName()); } if ($this->shouldJoinOwners()) { $join[] = qsprintf( $conn, 'JOIN %T package ON commit.phid = package.src AND package.type = %s', PhabricatorEdgeConfig::TABLE_NAME_EDGE, DiffusionCommitHasPackageEdgeType::EDGECONST); } return $join; } protected function shouldGroupQueryResultRows() { if ($this->shouldJoinAuditor()) { return true; } if ($this->shouldJoinOwners()) { return true; } return parent::shouldGroupQueryResultRows(); } public function getQueryApplicationClass() { return 'PhabricatorDiffusionApplication'; } public function getOrderableColumns() { return parent::getOrderableColumns() + array( 'epoch' => array( 'table' => $this->getPrimaryTableAlias(), 'column' => 'epoch', 'type' => 'int', 'reverse' => false, ), ); } protected function getPagingValueMap($cursor, array $keys) { $commit = $this->loadCursorObject($cursor); return array( 'id' => $commit->getID(), 'epoch' => $commit->getEpoch(), ); } public function getBuiltinOrders() { $parent = parent::getBuiltinOrders(); // Rename the default ID-based orders. $parent['importnew'] = array( 'name' => pht('Import Date (Newest First)'), ) + $parent['newest']; $parent['importold'] = array( 'name' => pht('Import Date (Oldest First)'), ) + $parent['oldest']; return array( 'newest' => array( 'vector' => array('epoch', 'id'), 'name' => pht('Commit Date (Newest First)'), ), 'oldest' => array( 'vector' => array('-epoch', '-id'), 'name' => pht('Commit Date (Oldest First)'), ), ) + $parent; } } diff --git a/src/applications/diffusion/query/DiffusionLintCountQuery.php b/src/applications/diffusion/query/DiffusionLintCountQuery.php index 4441ccf3ef..2c1a2a9867 100644 --- a/src/applications/diffusion/query/DiffusionLintCountQuery.php +++ b/src/applications/diffusion/query/DiffusionLintCountQuery.php @@ -1,123 +1,123 @@ branchIDs = $branch_ids; return $this; } public function withPaths(array $paths) { $this->paths = $paths; return $this; } public function withCodes(array $codes) { $this->codes = $codes; return $this; } public function execute() { if (!$this->paths) { throw new PhutilInvalidStateException('withPaths'); } if (!$this->branchIDs) { throw new PhutilInvalidStateException('withBranchIDs'); } $conn_r = id(new PhabricatorRepositoryCommit())->establishConnection('r'); $this->paths = array_unique($this->paths); list($dirs, $paths) = $this->processPaths(); $parts = array(); foreach ($dirs as $dir) { $parts[$dir] = qsprintf( $conn_r, 'path LIKE %>', $dir); } foreach ($paths as $path) { $parts[$path] = qsprintf( $conn_r, 'path = %s', $path); } $queries = array(); foreach ($parts as $key => $part) { $queries[] = qsprintf( $conn_r, 'SELECT %s path_prefix, COUNT(*) N FROM %T %Q', $key, PhabricatorRepository::TABLE_LINTMESSAGE, $this->buildCustomWhereClause($conn_r, $part)); } $huge_union_query = '('.implode(') UNION ALL (', $queries).')'; $data = queryfx_all( $conn_r, '%Q', $huge_union_query); return $this->processResults($data); } protected function buildCustomWhereClause( - AphrontDatabaseConnection $conn_r, + AphrontDatabaseConnection $conn, $part) { $where = array(); $where[] = $part; if ($this->codes !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'code IN (%Ls)', $this->codes); } if ($this->branchIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'branchID IN (%Ld)', $this->branchIDs); } - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } private function processPaths() { $dirs = array(); $paths = array(); foreach ($this->paths as $path) { $path = '/'.$path; if (substr($path, -1) == '/') { $dirs[] = $path; } else { $paths[] = $path; } } return array($dirs, $paths); } private function processResults(array $data) { $data = ipull($data, 'N', 'path_prefix'); // Strip the leading "/" back off each path. $output = array(); foreach ($data as $path => $count) { $output[substr($path, 1)] = $count; } return $output; } } diff --git a/src/applications/diffusion/query/DiffusionPathQuery.php b/src/applications/diffusion/query/DiffusionPathQuery.php index 45dc978ec1..0783643dc0 100644 --- a/src/applications/diffusion/query/DiffusionPathQuery.php +++ b/src/applications/diffusion/query/DiffusionPathQuery.php @@ -1,43 +1,43 @@ pathIDs = $path_ids; return $this; } public function execute() { - $conn_r = id(new PhabricatorRepository())->establishConnection('r'); + $conn = id(new PhabricatorRepository())->establishConnection('r'); - $where = $this->buildWhereClause($conn_r); + $where = $this->buildWhereClause($conn); $results = queryfx_all( - $conn_r, + $conn, 'SELECT * FROM %T %Q', PhabricatorRepository::TABLE_PATH, $where); return ipull($results, null, 'id'); } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->pathIDs) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->pathIDs); } if ($where) { - return 'WHERE ('.implode(') AND (', $where).')'; + return qsprintf($conn, 'WHERE %LA', $where); } else { - return ''; + return qsprintf($conn, ''); } } } diff --git a/src/applications/diffusion/query/DiffusionSymbolQuery.php b/src/applications/diffusion/query/DiffusionSymbolQuery.php index 0e08b7b32d..3415fbbefa 100644 --- a/src/applications/diffusion/query/DiffusionSymbolQuery.php +++ b/src/applications/diffusion/query/DiffusionSymbolQuery.php @@ -1,285 +1,285 @@ viewer = $viewer; return $this; } /** * @task config */ public function getViewer() { return $this->viewer; } /** * @task config */ public function setContext($context) { $this->context = $context; return $this; } /** * @task config */ public function setName($name) { $this->name = $name; return $this; } /** * @task config */ public function setNamePrefix($name_prefix) { $this->namePrefix = $name_prefix; return $this; } /** * @task config */ public function withRepositoryPHIDs(array $repository_phids) { $this->repositoryPHIDs = $repository_phids; return $this; } /** * @task config */ public function setLanguage($language) { $this->language = $language; return $this; } /** * @task config */ public function setType($type) { $this->type = $type; return $this; } /** * @task config */ public function needPaths($need_paths) { $this->needPaths = $need_paths; return $this; } /** * @task config */ public function needRepositories($need_repositories) { $this->needRepositories = $need_repositories; return $this; } /* -( Specialized Query )-------------------------------------------------- */ public function existsSymbolsInRepository($repository_phid) { $this ->withRepositoryPHIDs(array($repository_phid)) ->setLimit(1); $symbol = new PhabricatorRepositorySymbol(); $conn_r = $symbol->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q', $symbol->getTableName(), $this->buildWhereClause($conn_r), $this->buildLimitClause($conn_r)); return (!empty($data)); } /* -( Executing the Query )------------------------------------------------ */ /** * @task exec */ public function execute() { if ($this->name && $this->namePrefix) { throw new Exception( pht('You can not set both a name and a name prefix!')); } else if (!$this->name && !$this->namePrefix) { throw new Exception( pht('You must set a name or a name prefix!')); } $symbol = new PhabricatorRepositorySymbol(); $conn_r = $symbol->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $symbol->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); $symbols = $symbol->loadAllFromArray($data); if ($symbols) { if ($this->needPaths) { $this->loadPaths($symbols); } if ($this->needRepositories) { $symbols = $this->loadRepositories($symbols); } } return $symbols; } /* -( Internals )---------------------------------------------------------- */ /** * @task internal */ private function buildOrderClause($conn_r) { return qsprintf( $conn_r, 'ORDER BY symbolName ASC'); } /** * @task internal */ - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if (isset($this->context)) { $where[] = qsprintf( - $conn_r, + $conn, 'symbolContext = %s', $this->context); } if ($this->name) { $where[] = qsprintf( - $conn_r, + $conn, 'symbolName = %s', $this->name); } if ($this->namePrefix) { $where[] = qsprintf( - $conn_r, + $conn, 'symbolName LIKE %>', $this->namePrefix); } if ($this->repositoryPHIDs) { $where[] = qsprintf( - $conn_r, + $conn, 'repositoryPHID IN (%Ls)', $this->repositoryPHIDs); } if ($this->language) { $where[] = qsprintf( - $conn_r, + $conn, 'symbolLanguage = %s', $this->language); } if ($this->type) { $where[] = qsprintf( - $conn_r, + $conn, 'symbolType = %s', $this->type); } - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } /** * @task internal */ private function loadPaths(array $symbols) { assert_instances_of($symbols, 'PhabricatorRepositorySymbol'); $path_map = queryfx_all( id(new PhabricatorRepository())->establishConnection('r'), 'SELECT * FROM %T WHERE id IN (%Ld)', PhabricatorRepository::TABLE_PATH, mpull($symbols, 'getPathID')); $path_map = ipull($path_map, 'path', 'id'); foreach ($symbols as $symbol) { $symbol->attachPath(idx($path_map, $symbol->getPathID())); } } /** * @task internal */ private function loadRepositories(array $symbols) { assert_instances_of($symbols, 'PhabricatorRepositorySymbol'); $repos = id(new PhabricatorRepositoryQuery()) ->setViewer($this->viewer) ->withPHIDs(mpull($symbols, 'getRepositoryPHID')) ->execute(); $repos = mpull($repos, null, 'getPHID'); $visible = array(); foreach ($symbols as $symbol) { $repository = idx($repos, $symbol->getRepositoryPHID()); // repository is null mean "user can't view repo", so hide the symbol if ($repository) { $symbol->attachRepository($repository); $visible[] = $symbol; } } return $visible; } } diff --git a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelFilesizeQuery.php b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelFilesizeQuery.php new file mode 100644 index 0000000000..9d97a134ca --- /dev/null +++ b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelFilesizeQuery.php @@ -0,0 +1,125 @@ +identifier = $identifier; + return $this; + } + + protected function executeQuery() { + if (!strlen($this->identifier)) { + throw new PhutilInvalidStateException('withIdentifier'); + } + + $type = $this->getRepository()->getVersionControlSystem(); + switch ($type) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + $result = $this->loadGitFilesizes(); + break; + default: + throw new Exception(pht('Unsupported repository type "%s"!', $type)); + } + + return $result; + } + + private function loadGitFilesizes() { + $repository = $this->getRepository(); + $identifier = $this->identifier; + + $paths_future = $repository->getLocalCommandFuture( + 'diff-tree -z -r --no-commit-id %s --', + $identifier); + + // With "-z" we get "\0\0" for each line. Process the + // delimited text as ", " pairs. + + $path_lines = id(new LinesOfALargeExecFuture($paths_future)) + ->setDelimiter("\0"); + + $paths = array(); + + $path_pairs = new PhutilChunkedIterator($path_lines, 2); + foreach ($path_pairs as $path_pair) { + if (count($path_pair) != 2) { + throw new Exception( + pht( + 'Unexpected number of output lines from "git diff-tree" when '. + 'processing commit ("%s"): expected an even number of lines.', + $identifier)); + } + + list($fields, $pathname) = array_values($path_pair); + $fields = explode(' ', $fields); + + // Fields are: + // + // :100644 100644 aaaa bbbb M + // + // [0] Old file mode. + // [1] New file mode. + // [2] Old object hash. + // [3] New object hash. + // [4] Change mode. + + $paths[] = array( + 'path' => $pathname, + 'newHash' => $fields[3], + ); + } + + $path_sizes = array(); + + if (!$paths) { + return $path_sizes; + } + + $check_paths = array(); + foreach ($paths as $path) { + if ($path['newHash'] === DiffusionCommitHookEngine::EMPTY_HASH) { + $path_sizes[$path['path']] = 0; + continue; + } + $check_paths[$path['newHash']][] = $path['path']; + } + + if (!$check_paths) { + return $path_sizes; + } + + $future = $repository->getLocalCommandFuture( + 'cat-file --batch-check=%s', + '%(objectsize)'); + + $future->write(implode("\n", array_keys($check_paths))); + + $size_lines = id(new LinesOfALargeExecFuture($future)) + ->setDelimiter("\n"); + foreach ($size_lines as $line) { + $object_size = (int)$line; + + $object_hash = head_key($check_paths); + $path_names = $check_paths[$object_hash]; + unset($check_paths[$object_hash]); + + foreach ($path_names as $path_name) { + $path_sizes[$path_name] = $object_size; + } + } + + if ($check_paths) { + throw new Exception( + pht( + 'Unexpected number of output lines from "git cat-file" when '. + 'processing commit ("%s").', + $identifier)); + } + + return $path_sizes; + } + +} diff --git a/src/applications/diviner/publisher/DivinerLivePublisher.php b/src/applications/diviner/publisher/DivinerLivePublisher.php index 80f3bd7a1e..e9e1848d6e 100644 --- a/src/applications/diviner/publisher/DivinerLivePublisher.php +++ b/src/applications/diviner/publisher/DivinerLivePublisher.php @@ -1,175 +1,175 @@ book) { $book_name = $this->getConfig('name'); $book = id(new DivinerLiveBook())->loadOneWhere( 'name = %s', $book_name); if (!$book) { $book = id(new DivinerLiveBook()) ->setName($book_name) ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()) ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) ->save(); } $conn_w = $book->establishConnection('w'); $conn_w->openTransaction(); $book ->setRepositoryPHID($this->getRepositoryPHID()) ->setConfigurationData($this->getConfigurationData()) ->save(); // TODO: This is gross. Without this, the repository won't be updated for // atoms which have already been published. queryfx( $conn_w, 'UPDATE %T SET repositoryPHID = %s WHERE bookPHID = %s', id(new DivinerLiveSymbol())->getTableName(), $this->getRepositoryPHID(), $book->getPHID()); $conn_w->saveTransaction(); $this->book = $book; PhabricatorSearchWorker::queueDocumentForIndexing($book->getPHID()); } return $this->book; } private function loadSymbolForAtom(DivinerAtom $atom) { $symbol = id(new DivinerAtomQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBookPHIDs(array($this->getBook()->getPHID())) ->withTypes(array($atom->getType())) ->withNames(array($atom->getName())) ->withContexts(array($atom->getContext())) ->withIndexes(array($this->getAtomSimilarIndex($atom))) ->executeOne(); if ($symbol) { return $symbol; } return id(new DivinerLiveSymbol()) ->setBookPHID($this->getBook()->getPHID()) ->setType($atom->getType()) ->setName($atom->getName()) ->setContext($atom->getContext()) ->setAtomIndex($this->getAtomSimilarIndex($atom)); } private function loadAtomStorageForSymbol(DivinerLiveSymbol $symbol) { $storage = id(new DivinerLiveAtom())->loadOneWhere( 'symbolPHID = %s', $symbol->getPHID()); if ($storage) { return $storage; } return id(new DivinerLiveAtom()) ->setSymbolPHID($symbol->getPHID()); } protected function loadAllPublishedHashes() { $symbols = id(new DivinerAtomQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBookPHIDs(array($this->getBook()->getPHID())) ->withGhosts(false) ->execute(); return mpull($symbols, 'getGraphHash'); } protected function deleteDocumentsByHash(array $hashes) { $atom_table = new DivinerLiveAtom(); $symbol_table = new DivinerLiveSymbol(); $conn_w = $symbol_table->establishConnection('w'); $strings = array(); foreach ($hashes as $hash) { $strings[] = qsprintf($conn_w, '%s', $hash); } - foreach (PhabricatorLiskDAO::chunkSQL($strings, ', ') as $chunk) { + foreach (PhabricatorLiskDAO::chunkSQL($strings) as $chunk) { queryfx( $conn_w, 'UPDATE %T SET graphHash = NULL, nodeHash = NULL - WHERE graphHash IN (%Q)', + WHERE graphHash IN (%LQ)', $symbol_table->getTableName(), $chunk); } queryfx( $conn_w, 'DELETE a FROM %T a LEFT JOIN %T s ON a.symbolPHID = s.phid WHERE s.graphHash IS NULL', $atom_table->getTableName(), $symbol_table->getTableName()); } protected function createDocumentsByHash(array $hashes) { foreach ($hashes as $hash) { $atom = $this->getAtomFromGraphHash($hash); $ref = $atom->getRef(); $symbol = $this->loadSymbolForAtom($atom); $is_documentable = $this->shouldGenerateDocumentForAtom($atom); $symbol ->setRepositoryPHID($this->getRepositoryPHID()) ->setGraphHash($hash) ->setIsDocumentable((int)$is_documentable) ->setTitle($ref->getTitle()) ->setGroupName($ref->getGroup()) ->setNodeHash($atom->getHash()); if ($atom->getType() !== DivinerAtom::TYPE_FILE) { $renderer = $this->getRenderer(); $summary = $renderer->getAtomSummary($atom); $symbol->setSummary($summary); } else { $symbol->setSummary(''); } $symbol->save(); PhabricatorSearchWorker::queueDocumentForIndexing($symbol->getPHID()); // TODO: We probably need a finer-grained sense of what "documentable" // atoms are. Neither files nor methods are currently considered // documentable, but for different reasons: files appear nowhere, while // methods just don't appear at the top level. These are probably // separate concepts. Since we need atoms in order to build method // documentation, we insert them here. This also means we insert files, // which are unnecessary and unused. Make sure this makes sense, but then // probably introduce separate "isTopLevel" and "isDocumentable" flags? // TODO: Yeah do that soon ^^^ if ($atom->getType() !== DivinerAtom::TYPE_FILE) { $storage = $this->loadAtomStorageForSymbol($symbol) ->setAtomData($atom->toDictionary()) ->setContent(null) ->save(); } } } public function findAtomByRef(DivinerAtomRef $ref) { // TODO: Actually implement this. return null; } } diff --git a/src/applications/diviner/query/DivinerAtomQuery.php b/src/applications/diviner/query/DivinerAtomQuery.php index 5f4900d4dd..65a5634008 100644 --- a/src/applications/diviner/query/DivinerAtomQuery.php +++ b/src/applications/diviner/query/DivinerAtomQuery.php @@ -1,511 +1,511 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withBookPHIDs(array $phids) { $this->bookPHIDs = $phids; return $this; } public function withTypes(array $types) { $this->types = $types; return $this; } public function withNames(array $names) { $this->names = $names; return $this; } public function withContexts(array $contexts) { $this->contexts = $contexts; return $this; } public function withIndexes(array $indexes) { $this->indexes = $indexes; return $this; } public function withNodeHashes(array $hashes) { $this->nodeHashes = $hashes; return $this; } public function withTitles($titles) { $this->titles = $titles; return $this; } public function withNameContains($text) { $this->nameContains = $text; return $this; } public function needAtoms($need) { $this->needAtoms = $need; return $this; } public function needChildren($need) { $this->needChildren = $need; return $this; } /** * Include or exclude "ghosts", which are symbols which used to exist but do * not exist currently (for example, a function which existed in an older * version of the codebase but was deleted). * * These symbols had PHIDs assigned to them, and may have other sorts of * metadata that we don't want to lose (like comments or flags), so we don't * delete them outright. They might also come back in the future: the change * which deleted the symbol might be reverted, or the documentation might * have been generated incorrectly by accident. In these cases, we can * restore the original data. * * @param bool * @return this */ public function withGhosts($ghosts) { $this->isGhost = $ghosts; return $this; } public function needExtends($need) { $this->needExtends = $need; return $this; } public function withIsDocumentable($documentable) { $this->isDocumentable = $documentable; return $this; } public function withRepositoryPHIDs(array $repository_phids) { $this->repositoryPHIDs = $repository_phids; return $this; } public function needRepositories($need_repositories) { $this->needRepositories = $need_repositories; return $this; } protected function loadPage() { $table = new DivinerLiveSymbol(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } protected function willFilterPage(array $atoms) { assert_instances_of($atoms, 'DivinerLiveSymbol'); $books = array_unique(mpull($atoms, 'getBookPHID')); $books = id(new DivinerBookQuery()) ->setViewer($this->getViewer()) ->withPHIDs($books) ->execute(); $books = mpull($books, null, 'getPHID'); foreach ($atoms as $key => $atom) { $book = idx($books, $atom->getBookPHID()); if (!$book) { $this->didRejectResult($atom); unset($atoms[$key]); continue; } $atom->attachBook($book); } if ($this->needAtoms) { $atom_data = id(new DivinerLiveAtom())->loadAllWhere( 'symbolPHID IN (%Ls)', mpull($atoms, 'getPHID')); $atom_data = mpull($atom_data, null, 'getSymbolPHID'); foreach ($atoms as $key => $atom) { $data = idx($atom_data, $atom->getPHID()); $atom->attachAtom($data); } } // Load all of the symbols this symbol extends, recursively. Commonly, // this means all the ancestor classes and interfaces it extends and // implements. if ($this->needExtends) { // First, load all the matching symbols by name. This does 99% of the // work in most cases, assuming things are named at all reasonably. $names = array(); foreach ($atoms as $atom) { if (!$atom->getAtom()) { continue; } foreach ($atom->getAtom()->getExtends() as $xref) { $names[] = $xref->getName(); } } if ($names) { $xatoms = id(new DivinerAtomQuery()) ->setViewer($this->getViewer()) ->withNames($names) ->withGhosts(false) ->needExtends(true) ->needAtoms(true) ->needChildren($this->needChildren) ->execute(); $xatoms = mgroup($xatoms, 'getName', 'getType', 'getBookPHID'); } else { $xatoms = array(); } foreach ($atoms as $atom) { $atom_lang = null; $atom_extends = array(); if ($atom->getAtom()) { $atom_lang = $atom->getAtom()->getLanguage(); $atom_extends = $atom->getAtom()->getExtends(); } $extends = array(); foreach ($atom_extends as $xref) { // If there are no symbols of the matching name and type, we can't // resolve this. if (empty($xatoms[$xref->getName()][$xref->getType()])) { continue; } // If we found matches in the same documentation book, prefer them // over other matches. Otherwise, look at all the matches. $matches = $xatoms[$xref->getName()][$xref->getType()]; if (isset($matches[$atom->getBookPHID()])) { $maybe = $matches[$atom->getBookPHID()]; } else { $maybe = array_mergev($matches); } if (!$maybe) { continue; } // Filter out matches in a different language, since, e.g., PHP // classes can not implement JS classes. $same_lang = array(); foreach ($maybe as $xatom) { if ($xatom->getAtom()->getLanguage() == $atom_lang) { $same_lang[] = $xatom; } } if (!$same_lang) { continue; } // If we have duplicates remaining, just pick the first one. There's // nothing more we can do to figure out which is the real one. $extends[] = head($same_lang); } $atom->attachExtends($extends); } } if ($this->needChildren) { $child_hashes = $this->getAllChildHashes($atoms, $this->needExtends); if ($child_hashes) { $children = id(new DivinerAtomQuery()) ->setViewer($this->getViewer()) ->withNodeHashes($child_hashes) ->needAtoms($this->needAtoms) ->execute(); $children = mpull($children, null, 'getNodeHash'); } else { $children = array(); } $this->attachAllChildren($atoms, $children, $this->needExtends); } if ($this->needRepositories) { $repositories = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()) ->withPHIDs(mpull($atoms, 'getRepositoryPHID')) ->execute(); $repositories = mpull($repositories, null, 'getPHID'); foreach ($atoms as $key => $atom) { if ($atom->getRepositoryPHID() === null) { $atom->attachRepository(null); continue; } $repository = idx($repositories, $atom->getRepositoryPHID()); if (!$repository) { $this->didRejectResult($atom); unset($atom[$key]); continue; } $atom->attachRepository($repository); } } return $atoms; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } if ($this->bookPHIDs) { $where[] = qsprintf( - $conn_r, + $conn, 'bookPHID IN (%Ls)', $this->bookPHIDs); } if ($this->types) { $where[] = qsprintf( - $conn_r, + $conn, 'type IN (%Ls)', $this->types); } if ($this->names) { $where[] = qsprintf( - $conn_r, + $conn, 'name IN (%Ls)', $this->names); } if ($this->titles) { $hashes = array(); foreach ($this->titles as $title) { $slug = DivinerAtomRef::normalizeTitleString($title); $hash = PhabricatorHash::digestForIndex($slug); $hashes[] = $hash; } $where[] = qsprintf( - $conn_r, + $conn, 'titleSlugHash in (%Ls)', $hashes); } if ($this->contexts) { $with_null = false; $contexts = $this->contexts; foreach ($contexts as $key => $value) { if ($value === null) { unset($contexts[$key]); $with_null = true; continue; } } if ($contexts && $with_null) { $where[] = qsprintf( - $conn_r, + $conn, 'context IN (%Ls) OR context IS NULL', $contexts); } else if ($contexts) { $where[] = qsprintf( - $conn_r, + $conn, 'context IN (%Ls)', $contexts); } else if ($with_null) { $where[] = qsprintf( - $conn_r, + $conn, 'context IS NULL'); } } if ($this->indexes) { $where[] = qsprintf( - $conn_r, + $conn, 'atomIndex IN (%Ld)', $this->indexes); } if ($this->isDocumentable !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'isDocumentable = %d', (int)$this->isDocumentable); } if ($this->isGhost !== null) { if ($this->isGhost) { - $where[] = qsprintf($conn_r, 'graphHash IS NULL'); + $where[] = qsprintf($conn, 'graphHash IS NULL'); } else { - $where[] = qsprintf($conn_r, 'graphHash IS NOT NULL'); + $where[] = qsprintf($conn, 'graphHash IS NOT NULL'); } } if ($this->nodeHashes) { $where[] = qsprintf( - $conn_r, + $conn, 'nodeHash IN (%Ls)', $this->nodeHashes); } if ($this->nameContains) { // NOTE: This `CONVERT()` call makes queries case-insensitive, since // the column has binary collation. Eventually, this should move into // fulltext. $where[] = qsprintf( - $conn_r, + $conn, 'CONVERT(name USING utf8) LIKE %~', $this->nameContains); } if ($this->repositoryPHIDs) { $where[] = qsprintf( - $conn_r, + $conn, 'repositoryPHID IN (%Ls)', $this->repositoryPHIDs); } - $where[] = $this->buildPagingClause($conn_r); + $where[] = $this->buildPagingClause($conn); - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } /** * Walk a list of atoms and collect all the node hashes of the atoms' * children. When recursing, also walk up the tree and collect children of * atoms they extend. * * @param list List of symbols to collect child hashes of. * @param bool True to collect children of extended atoms, * as well. * @return map Hashes of atoms' children. */ private function getAllChildHashes(array $symbols, $recurse_up) { assert_instances_of($symbols, 'DivinerLiveSymbol'); $hashes = array(); foreach ($symbols as $symbol) { $child_hashes = array(); if ($symbol->getAtom()) { $child_hashes = $symbol->getAtom()->getChildHashes(); } foreach ($child_hashes as $hash) { $hashes[$hash] = $hash; } if ($recurse_up) { $hashes += $this->getAllChildHashes($symbol->getExtends(), true); } } return $hashes; } /** * Attach child atoms to existing atoms. In recursive mode, also attach child * atoms to atoms that these atoms extend. * * @param list List of symbols to attach children to. * @param map Map of symbols, keyed by node hash. * @param bool True to attach children to extended atoms, as well. * @return void */ private function attachAllChildren( array $symbols, array $children, $recurse_up) { assert_instances_of($symbols, 'DivinerLiveSymbol'); assert_instances_of($children, 'DivinerLiveSymbol'); foreach ($symbols as $symbol) { $child_hashes = array(); $symbol_children = array(); if ($symbol->getAtom()) { $child_hashes = $symbol->getAtom()->getChildHashes(); } foreach ($child_hashes as $hash) { if (isset($children[$hash])) { $symbol_children[] = $children[$hash]; } } $symbol->attachChildren($symbol_children); if ($recurse_up) { $this->attachAllChildren($symbol->getExtends(), $children, true); } } } public function getQueryApplicationClass() { return 'PhabricatorDivinerApplication'; } } diff --git a/src/applications/diviner/query/DivinerBookQuery.php b/src/applications/diviner/query/DivinerBookQuery.php index 8e6726ef5c..d540d971b0 100644 --- a/src/applications/diviner/query/DivinerBookQuery.php +++ b/src/applications/diviner/query/DivinerBookQuery.php @@ -1,201 +1,201 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withNameLike($name) { $this->nameLike = $name; return $this; } public function withNames(array $names) { $this->names = $names; return $this; } public function withNamePrefix($prefix) { $this->namePrefix = $prefix; return $this; } public function withRepositoryPHIDs(array $repository_phids) { $this->repositoryPHIDs = $repository_phids; return $this; } public function needProjectPHIDs($need_phids) { $this->needProjectPHIDs = $need_phids; return $this; } public function needRepositories($need_repositories) { $this->needRepositories = $need_repositories; return $this; } protected function loadPage() { $table = new DivinerLiveBook(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } protected function didFilterPage(array $books) { assert_instances_of($books, 'DivinerLiveBook'); if ($this->needRepositories) { $repositories = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()) ->withPHIDs(mpull($books, 'getRepositoryPHID')) ->execute(); $repositories = mpull($repositories, null, 'getPHID'); foreach ($books as $key => $book) { if ($book->getRepositoryPHID() === null) { $book->attachRepository(null); continue; } $repository = idx($repositories, $book->getRepositoryPHID()); if (!$repository) { $this->didRejectResult($book); unset($books[$key]); continue; } $book->attachRepository($repository); } } if ($this->needProjectPHIDs) { $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(mpull($books, 'getPHID')) ->withEdgeTypes( array( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, )); $edge_query->execute(); foreach ($books as $book) { $project_phids = $edge_query->getDestinationPHIDs( array( $book->getPHID(), )); $book->attachProjectPHIDs($project_phids); } } return $books; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } if (strlen($this->nameLike)) { $where[] = qsprintf( - $conn_r, + $conn, 'name LIKE %~', $this->nameLike); } if ($this->names !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'name IN (%Ls)', $this->names); } if (strlen($this->namePrefix)) { $where[] = qsprintf( - $conn_r, + $conn, 'name LIKE %>', $this->namePrefix); } if ($this->repositoryPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'repositoryPHID IN (%Ls)', $this->repositoryPHIDs); } - $where[] = $this->buildPagingClause($conn_r); + $where[] = $this->buildPagingClause($conn); - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } public function getQueryApplicationClass() { return 'PhabricatorDivinerApplication'; } public function getOrderableColumns() { return parent::getOrderableColumns() + array( 'name' => array( 'column' => 'name', 'type' => 'string', 'reverse' => true, 'unique' => true, ), ); } protected function getPagingValueMap($cursor, array $keys) { $book = $this->loadCursorObject($cursor); return array( 'name' => $book->getName(), ); } public function getBuiltinOrders() { return array( 'name' => array( 'vector' => array('name'), 'name' => pht('Name'), ), ) + parent::getBuiltinOrders(); } } diff --git a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php index c1672f99fb..d82f5c2c15 100644 --- a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php @@ -1,520 +1,542 @@ getViewer(); if ($this->shouldLimitAllocatingPoolSize($blueprint)) { return false; } // TODO: If we have a pending resource which is compatible with the // configuration for this lease, prevent a new allocation? Otherwise the // queue can fill up with copies of requests from the same lease. But // maybe we can deal with this with "pre-leasing"? return true; } public function canAcquireLeaseOnResource( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { // Don't hand out leases on working copies which have not activated, since // it may take an arbitrarily long time for them to acquire a host. if (!$resource->isActive()) { return false; } $need_map = $lease->getAttribute('repositories.map'); if (!is_array($need_map)) { return false; } $have_map = $resource->getAttribute('repositories.map'); if (!is_array($have_map)) { return false; } $have_as = ipull($have_map, 'phid'); $need_as = ipull($need_map, 'phid'); foreach ($need_as as $need_directory => $need_phid) { if (empty($have_as[$need_directory])) { // This resource is missing a required working copy. return false; } if ($have_as[$need_directory] != $need_phid) { // This resource has a required working copy, but it contains // the wrong repository. return false; } unset($have_as[$need_directory]); } if ($have_as && $lease->getAttribute('repositories.strict')) { // This resource has extra repositories, but the lease is strict about // which repositories are allowed to exist. return false; } if (!DrydockSlotLock::isLockFree($this->getLeaseSlotLock($resource))) { return false; } return true; } public function acquireLease( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { $lease ->needSlotLock($this->getLeaseSlotLock($resource)) ->acquireOnResource($resource); } private function getLeaseSlotLock(DrydockResource $resource) { $resource_phid = $resource->getPHID(); return "workingcopy.lease({$resource_phid})"; } public function allocateResource( DrydockBlueprint $blueprint, DrydockLease $lease) { $resource = $this->newResourceTemplate($blueprint); $resource_phid = $resource->getPHID(); $blueprint_phids = $blueprint->getFieldValue('blueprintPHIDs'); $host_lease = $this->newLease($blueprint) ->setResourceType('host') ->setOwnerPHID($resource_phid) ->setAttribute('workingcopy.resourcePHID', $resource_phid) ->setAllowedBlueprintPHIDs($blueprint_phids); $resource->setAttribute('host.leasePHID', $host_lease->getPHID()); $map = $lease->getAttribute('repositories.map'); foreach ($map as $key => $value) { $map[$key] = array_select_keys( $value, array( 'phid', )); } $resource->setAttribute('repositories.map', $map); $slot_lock = $this->getConcurrentResourceLimitSlotLock($blueprint); if ($slot_lock !== null) { $resource->needSlotLock($slot_lock); } $resource->allocateResource(); $host_lease->queueForActivation(); return $resource; } public function activateResource( DrydockBlueprint $blueprint, DrydockResource $resource) { $lease = $this->loadHostLease($resource); $this->requireActiveLease($lease); $command_type = DrydockCommandInterface::INTERFACE_TYPE; $interface = $lease->getInterface($command_type); // TODO: Make this configurable. $resource_id = $resource->getID(); $root = "/var/drydock/workingcopy-{$resource_id}"; $map = $resource->getAttribute('repositories.map'); + $futures = array(); $repositories = $this->loadRepositories(ipull($map, 'phid')); foreach ($map as $directory => $spec) { // TODO: Validate directory isn't goofy like "/etc" or "../../lol" // somewhere? $repository = $repositories[$spec['phid']]; $path = "{$root}/repo/{$directory}/"; - // TODO: Run these in parallel? - $interface->execx( + $future = $interface->getExecFuture( 'git clone -- %s %s', (string)$repository->getCloneURIObject(), $path); + + $future->setTimeout($repository->getEffectiveCopyTimeLimit()); + + $futures[$directory] = $future; + } + + foreach (new FutureIterator($futures) as $key => $future) { + $future->resolvex(); } $resource ->setAttribute('workingcopy.root', $root) ->activateResource(); } public function destroyResource( DrydockBlueprint $blueprint, DrydockResource $resource) { try { $lease = $this->loadHostLease($resource); } catch (Exception $ex) { // If we can't load the lease, assume we don't need to take any actions // to destroy it. return; } // Destroy the lease on the host. $lease->setReleaseOnDestruction(true); if ($lease->isActive()) { // Destroy the working copy on disk. $command_type = DrydockCommandInterface::INTERFACE_TYPE; $interface = $lease->getInterface($command_type); $root_key = 'workingcopy.root'; $root = $resource->getAttribute($root_key); if (strlen($root)) { $interface->execx('rm -rf -- %s', $root); } } } public function getResourceName( DrydockBlueprint $blueprint, DrydockResource $resource) { return pht('Working Copy'); } public function activateLease( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { $host_lease = $this->loadHostLease($resource); $command_type = DrydockCommandInterface::INTERFACE_TYPE; $interface = $host_lease->getInterface($command_type); $map = $lease->getAttribute('repositories.map'); $root = $resource->getAttribute('workingcopy.root'); + $repositories = $this->loadRepositories(ipull($map, 'phid')); + $default = null; foreach ($map as $directory => $spec) { + $repository = $repositories[$spec['phid']]; + $interface->pushWorkingDirectory("{$root}/repo/{$directory}/"); $cmd = array(); $arg = array(); $cmd[] = 'git clean -d --force'; $cmd[] = 'git fetch'; $commit = idx($spec, 'commit'); $branch = idx($spec, 'branch'); $ref = idx($spec, 'ref'); // Reset things first, in case previous builds left anything staged or // dirty. Note that we don't reset to "HEAD" because that does not work // in empty repositories. $cmd[] = 'git reset --hard'; if ($commit !== null) { $cmd[] = 'git checkout %s --'; $arg[] = $commit; } else if ($branch !== null) { $cmd[] = 'git checkout %s --'; $arg[] = $branch; $cmd[] = 'git reset --hard origin/%s'; $arg[] = $branch; } - $this->execxv($interface, $cmd, $arg); + $this->newExecvFuture($interface, $cmd, $arg) + ->setTimeout($repository->getEffectiveCopyTimeLimit()) + ->resolvex(); if (idx($spec, 'default')) { $default = $directory; } // If we're fetching a ref from a remote, do that separately so we can // raise a more tailored error. if ($ref) { $cmd = array(); $arg = array(); $ref_uri = $ref['uri']; $ref_ref = $ref['ref']; $cmd[] = 'git fetch --no-tags -- %s +%s:%s'; $arg[] = $ref_uri; $arg[] = $ref_ref; $arg[] = $ref_ref; $cmd[] = 'git checkout %s --'; $arg[] = $ref_ref; try { - $this->execxv($interface, $cmd, $arg); + $this->newExecvFuture($interface, $cmd, $arg) + ->setTimeout($repository->getEffectiveCopyTimeLimit()) + ->resolvex(); } catch (CommandException $ex) { $display_command = csprintf( 'git fetch %R %R', $ref_uri, $ref_ref); $error = DrydockCommandError::newFromCommandException($ex) ->setPhase(self::PHASE_REMOTEFETCH) ->setDisplayCommand($display_command); $lease->setAttribute( 'workingcopy.vcs.error', $error->toDictionary()); throw $ex; } } $merges = idx($spec, 'merges'); if ($merges) { foreach ($merges as $merge) { $this->applyMerge($lease, $interface, $merge); } } $interface->popWorkingDirectory(); } if ($default === null) { $default = head_key($map); } // TODO: Use working storage? $lease->setAttribute('workingcopy.default', "{$root}/repo/{$default}/"); $lease->activateOnResource($resource); } public function didReleaseLease( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { // We leave working copies around even if there are no leases on them, // since the cost to maintain them is nearly zero but rebuilding them is // moderately expensive and it's likely that they'll be reused. return; } public function destroyLease( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { // When we activate a lease we just reset the working copy state and do // not create any new state, so we don't need to do anything special when // destroying a lease. return; } public function getType() { return 'working-copy'; } public function getInterface( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease, $type) { switch ($type) { case DrydockCommandInterface::INTERFACE_TYPE: $host_lease = $this->loadHostLease($resource); $command_interface = $host_lease->getInterface($type); $path = $lease->getAttribute('workingcopy.default'); $command_interface->pushWorkingDirectory($path); return $command_interface; } } private function loadRepositories(array $phids) { $viewer = $this->getViewer(); $repositories = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withPHIDs($phids) ->execute(); $repositories = mpull($repositories, null, 'getPHID'); foreach ($phids as $phid) { if (empty($repositories[$phid])) { throw new Exception( pht( 'Repository PHID "%s" does not exist.', $phid)); } } foreach ($repositories as $repository) { $repository_vcs = $repository->getVersionControlSystem(); switch ($repository_vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: break; default: throw new Exception( pht( 'Repository ("%s") has unsupported VCS ("%s").', $repository->getPHID(), $repository_vcs)); } } return $repositories; } private function loadHostLease(DrydockResource $resource) { $viewer = $this->getViewer(); $lease_phid = $resource->getAttribute('host.leasePHID'); $lease = id(new DrydockLeaseQuery()) ->setViewer($viewer) ->withPHIDs(array($lease_phid)) ->executeOne(); if (!$lease) { throw new Exception( pht( 'Unable to load lease ("%s").', $lease_phid)); } return $lease; } protected function getCustomFieldSpecifications() { return array( 'blueprintPHIDs' => array( 'name' => pht('Use Blueprints'), 'type' => 'blueprints', 'required' => true, ), ); } protected function shouldUseConcurrentResourceLimit() { return true; } private function applyMerge( DrydockLease $lease, DrydockCommandInterface $interface, array $merge) { $src_uri = $merge['src.uri']; $src_ref = $merge['src.ref']; try { $interface->execx( 'git fetch --no-tags -- %s +%s:%s', $src_uri, $src_ref, $src_ref); } catch (CommandException $ex) { $display_command = csprintf( 'git fetch %R +%R:%R', $src_uri, $src_ref, $src_ref); $error = DrydockCommandError::newFromCommandException($ex) ->setPhase(self::PHASE_MERGEFETCH) ->setDisplayCommand($display_command); $lease->setAttribute('workingcopy.vcs.error', $error->toDictionary()); throw $ex; } // NOTE: This can never actually generate a commit because we pass // "--squash", but git sometimes runs code to check that a username and // email are configured anyway. $real_command = csprintf( 'git -c user.name=%s -c user.email=%s merge --no-stat --squash -- %R', 'drydock', 'drydock@phabricator', $src_ref); try { $interface->execx('%C', $real_command); } catch (CommandException $ex) { $display_command = csprintf( 'git merge --squash %R', $src_ref); $error = DrydockCommandError::newFromCommandException($ex) ->setPhase(self::PHASE_SQUASHMERGE) ->setDisplayCommand($display_command); $lease->setAttribute('workingcopy.vcs.error', $error->toDictionary()); throw $ex; } } public function getCommandError(DrydockLease $lease) { return $lease->getAttribute('workingcopy.vcs.error'); } private function execxv( DrydockCommandInterface $interface, array $commands, array $arguments) { + return $this->newExecvFuture($interface, $commands, $arguments)->resolvex(); + } + + private function newExecvFuture( + DrydockCommandInterface $interface, + array $commands, + array $arguments) { $commands = implode(' && ', $commands); $argv = array_merge(array($commands), $arguments); - return call_user_func_array(array($interface, 'execx'), $argv); + return call_user_func_array(array($interface, 'getExecFuture'), $argv); } - } diff --git a/src/applications/drydock/editor/DrydockBlueprintEditEngine.php b/src/applications/drydock/editor/DrydockBlueprintEditEngine.php index 6bd1366ef0..a3b6fdaf75 100644 --- a/src/applications/drydock/editor/DrydockBlueprintEditEngine.php +++ b/src/applications/drydock/editor/DrydockBlueprintEditEngine.php @@ -1,173 +1,173 @@ blueprintImplementation = $impl; return $this; } public function getBlueprintImplementation() { return $this->blueprintImplementation; } protected function newEditableObject() { $viewer = $this->getViewer(); $blueprint = DrydockBlueprint::initializeNewBlueprint($viewer); $impl = $this->getBlueprintImplementation(); if ($impl) { $blueprint ->setClassName(get_class($impl)) ->attachImplementation(clone $impl); } return $blueprint; } protected function newEditableObjectFromConduit(array $raw_xactions) { $type = null; foreach ($raw_xactions as $raw_xaction) { if ($raw_xaction['type'] !== 'type') { continue; } $type = $raw_xaction['value']; } if ($type === null) { throw new Exception( pht( 'When creating a new Drydock blueprint via the Conduit API, you '. 'must provide a "type" transaction to select a type.')); } $map = DrydockBlueprintImplementation::getAllBlueprintImplementations(); if (!isset($map[$type])) { throw new Exception( pht( 'Blueprint type "%s" is unrecognized. Valid types are: %s.', $type, implode(', ', array_keys($map)))); } $impl = clone $map[$type]; $this->setBlueprintImplementation($impl); return $this->newEditableObject(); } protected function newEditableObjectForDocumentation() { // In order to generate the proper list of fields/transactions for a // blueprint, a blueprint's type needs to be known upfront, and there's // currently no way to pre-specify the type. Hardcoding an implementation // here prevents the fatal on the Conduit API page and allows transactions // to be edited. $impl = new DrydockWorkingCopyBlueprintImplementation(); $this->setBlueprintImplementation($impl); return $this->newEditableObject(); } protected function newObjectQuery() { return new DrydockBlueprintQuery(); } protected function getObjectCreateTitleText($object) { return pht('Create Blueprint'); } protected function getObjectCreateButtonText($object) { return pht('Create Blueprint'); } protected function getObjectEditTitleText($object) { return pht('Edit Blueprint: %s', $object->getBlueprintName()); } protected function getObjectEditShortText($object) { return pht('Edit Blueprint'); } protected function getObjectCreateShortText() { return pht('Create Blueprint'); } protected function getObjectName() { return pht('Blueprint'); } protected function getEditorURI() { return '/drydock/blueprint/edit/'; } protected function getObjectCreateCancelURI($object) { return '/drydock/blueprint/'; } protected function getObjectViewURI($object) { $id = $object->getID(); return "/drydock/blueprint/{$id}/"; } protected function getCreateNewObjectPolicy() { return $this->getApplication()->getPolicy( DrydockCreateBlueprintsCapability::CAPABILITY); } protected function buildCustomEditFields($object) { $impl = $object->getImplementation(); return array( // This field appears in the web UI id(new PhabricatorStaticEditField()) ->setKey('displayType') ->setLabel(pht('Blueprint Type')) ->setDescription(pht('Type of blueprint.')) ->setValue($impl->getBlueprintName()), id(new PhabricatorTextEditField()) ->setKey('type') ->setLabel(pht('Type')) - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setTransactionType( DrydockBlueprintTypeTransaction::TRANSACTIONTYPE) ->setDescription(pht('When creating a blueprint, set the type.')) ->setConduitDescription(pht('Set the blueprint type.')) ->setConduitTypeDescription(pht('Blueprint type.')) ->setValue($object->getClassName()), id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) ->setDescription(pht('Name of the blueprint.')) ->setTransactionType(DrydockBlueprintNameTransaction::TRANSACTIONTYPE) ->setIsRequired(true) ->setValue($object->getBlueprintName()), ); } } diff --git a/src/applications/drydock/storage/DrydockSlotLock.php b/src/applications/drydock/storage/DrydockSlotLock.php index 7e4e320946..7a9bce8b3f 100644 --- a/src/applications/drydock/storage/DrydockSlotLock.php +++ b/src/applications/drydock/storage/DrydockSlotLock.php @@ -1,176 +1,176 @@ false, self::CONFIG_COLUMN_SCHEMA => array( 'lockIndex' => 'bytes12', 'lockKey' => 'text', ), self::CONFIG_KEY_SCHEMA => array( 'key_lock' => array( 'columns' => array('lockIndex'), 'unique' => true, ), 'key_owner' => array( 'columns' => array('ownerPHID'), ), ), ) + parent::getConfiguration(); } /* -( Getting Lock Information )------------------------------------------- */ /** * Load all locks held by a particular owner. * * @param phid Owner PHID. * @return list All held locks. * @task info */ public static function loadLocks($owner_phid) { return id(new DrydockSlotLock())->loadAllWhere( 'ownerPHID = %s', $owner_phid); } /** * Test if a lock is currently free. * * @param string Lock key to test. * @return bool True if the lock is currently free. * @task info */ public static function isLockFree($lock) { return self::areLocksFree(array($lock)); } /** * Test if a list of locks are all currently free. * * @param list List of lock keys to test. * @return bool True if all locks are currently free. * @task info */ public static function areLocksFree(array $locks) { $lock_map = self::loadHeldLocks($locks); return !$lock_map; } /** * Load named locks. * * @param list List of lock keys to load. * @return list List of held locks. * @task info */ public static function loadHeldLocks(array $locks) { if (!$locks) { return array(); } $table = new DrydockSlotLock(); $conn_r = $table->establishConnection('r'); $indexes = array(); foreach ($locks as $lock) { $indexes[] = PhabricatorHash::digestForIndex($lock); } return id(new DrydockSlotLock())->loadAllWhere( 'lockIndex IN (%Ls)', $indexes); } /* -( Acquiring and Releasing Locks )-------------------------------------- */ /** * Acquire a set of slot locks. * * This method either acquires all the locks or throws an exception (usually * because one or more locks are held). * * @param phid Lock owner PHID. * @param list List of locks to acquire. * @return void * @task locks */ public static function acquireLocks($owner_phid, array $locks) { if (!$locks) { return; } $table = new DrydockSlotLock(); $conn_w = $table->establishConnection('w'); $sql = array(); foreach ($locks as $lock) { $sql[] = qsprintf( $conn_w, '(%s, %s, %s)', $owner_phid, PhabricatorHash::digestForIndex($lock), $lock); } try { queryfx( $conn_w, - 'INSERT INTO %T (ownerPHID, lockIndex, lockKey) VALUES %Q', + 'INSERT INTO %T (ownerPHID, lockIndex, lockKey) VALUES %LQ', $table->getTableName(), - implode(', ', $sql)); + $sql); } catch (AphrontDuplicateKeyQueryException $ex) { // Try to improve the readability of the exception. We might miss on // this query if the lock has already been released, but most of the // time we should be able to figure out which locks are already held. $held = self::loadHeldLocks($locks); $held = mpull($held, 'getOwnerPHID', 'getLockKey'); throw new DrydockSlotLockException($held); } } /** * Release all locks held by an owner. * * @param phid Lock owner PHID. * @return void * @task locks */ public static function releaseLocks($owner_phid) { $table = new DrydockSlotLock(); $conn_w = $table->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE ownerPHID = %s', $table->getTableName(), $owner_phid); } } diff --git a/src/applications/fact/daemon/PhabricatorFactDaemon.php b/src/applications/fact/daemon/PhabricatorFactDaemon.php index 3fa6c6f0ff..57813b3021 100644 --- a/src/applications/fact/daemon/PhabricatorFactDaemon.php +++ b/src/applications/fact/daemon/PhabricatorFactDaemon.php @@ -1,201 +1,201 @@ setEngines(PhabricatorFactEngine::loadAllEngines()); while (!$this->shouldExit()) { PhabricatorCaches::destroyRequestCache(); $iterators = $this->getAllApplicationIterators(); foreach ($iterators as $iterator_name => $iterator) { $this->processIteratorWithCursor($iterator_name, $iterator); } $this->log(pht('Zzz...')); $this->sleep(60 * 5); } } public static function getAllApplicationIterators() { $apps = PhabricatorApplication::getAllInstalledApplications(); $iterators = array(); foreach ($apps as $app) { foreach ($app->getFactObjectsForAnalysis() as $object) { $iterator = new PhabricatorFactUpdateIterator($object); $iterators[get_class($object)] = $iterator; } } return $iterators; } public function processIteratorWithCursor($iterator_name, $iterator) { $this->log(pht("Processing cursor '%s'.", $iterator_name)); $cursor = id(new PhabricatorFactCursor())->loadOneWhere( 'name = %s', $iterator_name); if (!$cursor) { $cursor = new PhabricatorFactCursor(); $cursor->setName($iterator_name); $position = null; } else { $position = $cursor->getPosition(); } if ($position) { $iterator->setPosition($position); } $new_cursor_position = $this->processIterator($iterator); if ($new_cursor_position) { $cursor->setPosition($new_cursor_position); $cursor->save(); } } public function setEngines(array $engines) { assert_instances_of($engines, 'PhabricatorFactEngine'); $viewer = PhabricatorUser::getOmnipotentUser(); foreach ($engines as $engine) { $engine->setViewer($viewer); } $this->engines = $engines; return $this; } public function processIterator($iterator) { $result = null; $datapoints = array(); $count = 0; foreach ($iterator as $key => $object) { $phid = $object->getPHID(); $this->log(pht('Processing %s...', $phid)); $object_datapoints = $this->newDatapoints($object); $count += count($object_datapoints); $datapoints[$phid] = $object_datapoints; if ($count > 1024) { $this->updateDatapoints($datapoints); $datapoints = array(); $count = 0; } $result = $key; } if ($count) { $this->updateDatapoints($datapoints); $datapoints = array(); $count = 0; } return $result; } private function newDatapoints(PhabricatorLiskDAO $object) { $facts = array(); foreach ($this->engines as $engine) { if (!$engine->supportsDatapointsForObject($object)) { continue; } $facts[] = $engine->newDatapointsForObject($object); } return array_mergev($facts); } private function updateDatapoints(array $map) { foreach ($map as $phid => $facts) { assert_instances_of($facts, 'PhabricatorFactIntDatapoint'); } $phids = array_keys($map); if (!$phids) { return; } $fact_keys = array(); $objects = array(); foreach ($map as $phid => $facts) { foreach ($facts as $fact) { $fact_keys[$fact->getKey()] = true; $object_phid = $fact->getObjectPHID(); $objects[$object_phid] = $object_phid; $dimension_phid = $fact->getDimensionPHID(); if ($dimension_phid !== null) { $objects[$dimension_phid] = $dimension_phid; } } } $key_map = id(new PhabricatorFactKeyDimension()) ->newDimensionMap(array_keys($fact_keys), true); $object_map = id(new PhabricatorFactObjectDimension()) ->newDimensionMap(array_keys($objects), true); $table = new PhabricatorFactIntDatapoint(); $conn = $table->establishConnection('w'); $table_name = $table->getTableName(); $sql = array(); foreach ($map as $phid => $facts) { foreach ($facts as $fact) { $key_id = $key_map[$fact->getKey()]; $object_id = $object_map[$fact->getObjectPHID()]; $dimension_phid = $fact->getDimensionPHID(); if ($dimension_phid !== null) { $dimension_id = $object_map[$dimension_phid]; } else { $dimension_id = null; } $sql[] = qsprintf( $conn, '(%d, %d, %nd, %d, %d)', $key_id, $object_id, $dimension_id, $fact->getValue(), $fact->getEpoch()); } } $rebuilt_ids = array_select_keys($object_map, $phids); $table->openTransaction(); queryfx( $conn, 'DELETE FROM %T WHERE objectID IN (%Ld)', $table_name, $rebuilt_ids); if ($sql) { foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) { queryfx( $conn, 'INSERT INTO %T (keyID, objectID, dimensionID, value, epoch) - VALUES %Q', + VALUES %LQ', $table_name, $chunk); } } $table->saveTransaction(); } } diff --git a/src/applications/fact/query/PhabricatorFactDatapointQuery.php b/src/applications/fact/query/PhabricatorFactDatapointQuery.php index fee7c86d12..20945dbbd0 100644 --- a/src/applications/fact/query/PhabricatorFactDatapointQuery.php +++ b/src/applications/fact/query/PhabricatorFactDatapointQuery.php @@ -1,181 +1,181 @@ facts = $facts; return $this; } public function withObjectPHIDs(array $object_phids) { $this->objectPHIDs = $object_phids; return $this; } public function setLimit($limit) { $this->limit = $limit; return $this; } public function needVectors($need) { $this->needVectors = $need; return $this; } public function execute() { $facts = mpull($this->facts, null, 'getKey'); if (!$facts) { throw new Exception(pht('Executing a fact query requires facts.')); } $table_map = array(); foreach ($facts as $fact) { $datapoint = $fact->newDatapoint(); $table = $datapoint->getTableName(); if (!isset($table_map[$table])) { $table_map[$table] = array( 'table' => $datapoint, 'facts' => array(), ); } $table_map[$table]['facts'][] = $fact; } $rows = array(); foreach ($table_map as $spec) { $rows[] = $this->executeWithTable($spec); } $rows = array_mergev($rows); $key_unmap = array_flip($this->keyMap); $dimension_unmap = array_flip($this->dimensionMap); $groups = array(); $need_phids = array(); foreach ($rows as $row) { $groups[$row['keyID']][] = $row; $object_id = $row['objectID']; if (!isset($dimension_unmap[$object_id])) { $need_phids[$object_id] = $object_id; } $dimension_id = $row['dimensionID']; if ($dimension_id && !isset($dimension_unmap[$dimension_id])) { $need_phids[$dimension_id] = $dimension_id; } } $dimension_unmap += id(new PhabricatorFactObjectDimension()) ->newDimensionUnmap($need_phids); $results = array(); foreach ($groups as $key_id => $rows) { $key = $key_unmap[$key_id]; $fact = $facts[$key]; $datapoint = $fact->newDatapoint(); foreach ($rows as $row) { $dimension_id = $row['dimensionID']; if ($dimension_id) { if (!isset($dimension_unmap[$dimension_id])) { continue; } else { $dimension_phid = $dimension_unmap[$dimension_id]; } } else { $dimension_phid = null; } $object_id = $row['objectID']; if (!isset($dimension_unmap[$object_id])) { continue; } else { $object_phid = $dimension_unmap[$object_id]; } $result = array( 'key' => $key, 'objectPHID' => $object_phid, 'dimensionPHID' => $dimension_phid, 'value' => (int)$row['value'], 'epoch' => $row['epoch'], ); if ($this->needVectors) { $result['vector'] = $datapoint->newRawVector($result); } $results[] = $result; } } return $results; } private function executeWithTable(array $spec) { $table = $spec['table']; $facts = $spec['facts']; $conn = $table->establishConnection('r'); $fact_keys = mpull($facts, 'getKey'); $this->keyMap = id(new PhabricatorFactKeyDimension()) ->newDimensionMap($fact_keys); if (!$this->keyMap) { return array(); } $where = array(); $where[] = qsprintf( $conn, 'keyID IN (%Ld)', $this->keyMap); if ($this->objectPHIDs) { $object_map = id(new PhabricatorFactObjectDimension()) ->newDimensionMap($this->objectPHIDs); if (!$object_map) { return array(); } $this->dimensionMap = $object_map; $where[] = qsprintf( $conn, 'objectID IN (%Ld)', $this->dimensionMap); } - $where = '('.implode(') AND (', $where).')'; + $where = qsprintf($conn, '%LA', $where); if ($this->limit) { $limit = qsprintf( $conn, 'LIMIT %d', $this->limit); } else { - $limit = ''; + $limit = qsprintf($conn, ''); } return queryfx_all( $conn, 'SELECT keyID, objectID, dimensionID, value, epoch FROM %T WHERE %Q %Q', $table->getTableName(), $where, $limit); } } diff --git a/src/applications/fact/storage/PhabricatorFactDimension.php b/src/applications/fact/storage/PhabricatorFactDimension.php index 9c05121c9c..5644da331e 100644 --- a/src/applications/fact/storage/PhabricatorFactDimension.php +++ b/src/applications/fact/storage/PhabricatorFactDimension.php @@ -1,110 +1,110 @@ newDimensionMap(array($key), $create); return idx($map, $key); } final public function newDimensionUnmap(array $ids) { if (!$ids) { return array(); } $conn = $this->establishConnection('r'); $column = $this->getDimensionColumnName(); $rows = queryfx_all( $conn, 'SELECT id, %C FROM %T WHERE id IN (%Ld)', $column, $this->getTableName(), $ids); $rows = ipull($rows, $column, 'id'); return $rows; } final public function newDimensionMap(array $keys, $create = false) { if (!$keys) { return array(); } $conn = $this->establishConnection('r'); $column = $this->getDimensionColumnName(); $rows = queryfx_all( $conn, 'SELECT id, %C FROM %T WHERE %C IN (%Ls)', $column, $this->getTableName(), $column, $keys); $rows = ipull($rows, 'id', $column); $map = array(); $need = array(); foreach ($keys as $key) { if (isset($rows[$key])) { $map[$key] = (int)$rows[$key]; } else { $need[] = $key; } } if (!$need) { return $map; } if (!$create) { return $map; } $sql = array(); foreach ($need as $key) { $sql[] = qsprintf( $conn, '(%s)', $key); } $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) { queryfx( $conn, - 'INSERT IGNORE INTO %T (%C) VALUES %Q', + 'INSERT IGNORE INTO %T (%C) VALUES %LQ', $this->getTableName(), $column, $chunk); } unset($unguarded); $rows = queryfx_all( $conn, 'SELECT id, %C FROM %T WHERE %C IN (%Ls)', $column, $this->getTableName(), $column, $need); $rows = ipull($rows, 'id', $column); foreach ($need as $key) { if (isset($rows[$key])) { $map[$key] = (int)$rows[$key]; } else { throw new Exception( pht( 'Failed to load or generate dimension ID ("%s") for dimension '. 'key "%s".', get_class($this), $key)); } } return $map; } } diff --git a/src/applications/feed/PhabricatorFeedStoryPublisher.php b/src/applications/feed/PhabricatorFeedStoryPublisher.php index 47dde8c98a..70d9a2f61c 100644 --- a/src/applications/feed/PhabricatorFeedStoryPublisher.php +++ b/src/applications/feed/PhabricatorFeedStoryPublisher.php @@ -1,341 +1,341 @@ mailTags = $mail_tags; return $this; } public function getMailTags() { return $this->mailTags; } public function setNotifyAuthor($notify_author) { $this->notifyAuthor = $notify_author; return $this; } public function getNotifyAuthor() { return $this->notifyAuthor; } public function setRelatedPHIDs(array $phids) { $this->relatedPHIDs = $phids; return $this; } public function setSubscribedPHIDs(array $phids) { $this->subscribedPHIDs = $phids; return $this; } public function setPrimaryObjectPHID($phid) { $this->primaryObjectPHID = $phid; return $this; } public function setUnexpandablePHIDs(array $unexpandable_phids) { $this->unexpandablePHIDs = $unexpandable_phids; return $this; } public function getUnexpandablePHIDs() { return $this->unexpandablePHIDs; } public function setStoryType($story_type) { $this->storyType = $story_type; return $this; } public function setStoryData(array $data) { $this->storyData = $data; return $this; } public function setStoryTime($time) { $this->storyTime = $time; return $this; } public function setStoryAuthorPHID($phid) { $this->storyAuthorPHID = $phid; return $this; } public function setMailRecipientPHIDs(array $phids) { $this->mailRecipientPHIDs = $phids; return $this; } public function publish() { $class = $this->storyType; if (!$class) { throw new Exception( pht( 'Call %s before publishing!', 'setStoryType()')); } if (!class_exists($class)) { throw new Exception( pht( "Story type must be a valid class name and must subclass %s. ". "'%s' is not a loadable class.", 'PhabricatorFeedStory', $class)); } if (!is_subclass_of($class, 'PhabricatorFeedStory')) { throw new Exception( pht( "Story type must be a valid class name and must subclass %s. ". "'%s' is not a subclass of %s.", 'PhabricatorFeedStory', $class, 'PhabricatorFeedStory')); } $chrono_key = $this->generateChronologicalKey(); $story = new PhabricatorFeedStoryData(); $story->setStoryType($this->storyType); $story->setStoryData($this->storyData); $story->setAuthorPHID((string)$this->storyAuthorPHID); $story->setChronologicalKey($chrono_key); $story->save(); if ($this->relatedPHIDs) { $ref = new PhabricatorFeedStoryReference(); $sql = array(); $conn = $ref->establishConnection('w'); foreach (array_unique($this->relatedPHIDs) as $phid) { $sql[] = qsprintf( $conn, '(%s, %s)', $phid, $chrono_key); } queryfx( $conn, - 'INSERT INTO %T (objectPHID, chronologicalKey) VALUES %Q', + 'INSERT INTO %T (objectPHID, chronologicalKey) VALUES %LQ', $ref->getTableName(), - implode(', ', $sql)); + $sql); } $subscribed_phids = $this->subscribedPHIDs; if ($subscribed_phids) { $subscribed_phids = $this->filterSubscribedPHIDs($subscribed_phids); $this->insertNotifications($chrono_key, $subscribed_phids); $this->sendNotification($chrono_key, $subscribed_phids); } PhabricatorWorker::scheduleTask( 'FeedPublisherWorker', array( 'key' => $chrono_key, )); return $story; } private function insertNotifications($chrono_key, array $subscribed_phids) { if (!$this->primaryObjectPHID) { throw new Exception( pht( 'You must call %s if you %s!', 'setPrimaryObjectPHID()', 'setSubscribedPHIDs()')); } $notif = new PhabricatorFeedStoryNotification(); $sql = array(); $conn = $notif->establishConnection('w'); $will_receive_mail = array_fill_keys($this->mailRecipientPHIDs, true); $user_phids = array_unique($subscribed_phids); foreach ($user_phids as $user_phid) { if (isset($will_receive_mail[$user_phid])) { $mark_read = 1; } else { $mark_read = 0; } $sql[] = qsprintf( $conn, '(%s, %s, %s, %d)', $this->primaryObjectPHID, $user_phid, $chrono_key, $mark_read); } if ($sql) { queryfx( $conn, 'INSERT INTO %T '. '(primaryObjectPHID, userPHID, chronologicalKey, hasViewed) '. - 'VALUES %Q', + 'VALUES %LQ', $notif->getTableName(), - implode(', ', $sql)); + $sql); } PhabricatorUserCache::clearCaches( PhabricatorUserNotificationCountCacheType::KEY_COUNT, $user_phids); } private function sendNotification($chrono_key, array $subscribed_phids) { $data = array( 'key' => (string)$chrono_key, 'type' => 'notification', 'subscribers' => $subscribed_phids, ); PhabricatorNotificationClient::tryToPostMessage($data); } /** * Remove PHIDs who should not receive notifications from a subscriber list. * * @param list List of potential subscribers. * @return list List of actual subscribers. */ private function filterSubscribedPHIDs(array $phids) { $phids = $this->expandRecipients($phids); $tags = $this->getMailTags(); if ($tags) { $all_prefs = id(new PhabricatorUserPreferencesQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withUserPHIDs($phids) ->needSyntheticPreferences(true) ->execute(); $all_prefs = mpull($all_prefs, null, 'getUserPHID'); } $pref_default = PhabricatorEmailTagsSetting::VALUE_EMAIL; $pref_ignore = PhabricatorEmailTagsSetting::VALUE_IGNORE; $keep = array(); foreach ($phids as $phid) { if (($phid == $this->storyAuthorPHID) && !$this->getNotifyAuthor()) { continue; } if ($tags && isset($all_prefs[$phid])) { $mailtags = $all_prefs[$phid]->getSettingValue( PhabricatorEmailTagsSetting::SETTINGKEY); $notify = false; foreach ($tags as $tag) { // If this is set to "email" or "notify", notify the user. if ((int)idx($mailtags, $tag, $pref_default) != $pref_ignore) { $notify = true; break; } } if (!$notify) { continue; } } $keep[] = $phid; } return array_values(array_unique($keep)); } private function expandRecipients(array $phids) { $expanded_phids = id(new PhabricatorMetaMTAMemberQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs($phids) ->executeExpansion(); // Filter out unexpandable PHIDs from the results. The typical case for // this is that resigned reviewers should not be notified just because // they are a member of some project or package reviewer. $original_map = array_fuse($phids); $unexpandable_map = array_fuse($this->unexpandablePHIDs); foreach ($expanded_phids as $key => $phid) { // We can keep this expanded PHID if it was present originally. if (isset($original_map[$phid])) { continue; } // We can also keep it if it isn't marked as unexpandable. if (!isset($unexpandable_map[$phid])) { continue; } // If it's unexpandable and we produced it by expanding recipients, // throw it away. unset($expanded_phids[$key]); } $expanded_phids = array_values($expanded_phids); return $expanded_phids; } /** * We generate a unique chronological key for each story type because we want * to be able to page through the stream with a cursor (i.e., select stories * after ID = X) so we can efficiently perform filtering after selecting data, * and multiple stories with the same ID make this cumbersome without putting * a bunch of logic in the client. We could use the primary key, but that * would prevent publishing stories which happened in the past. Since it's * potentially useful to do that (e.g., if you're importing another data * source) build a unique key for each story which has chronological ordering. * * @return string A unique, time-ordered key which identifies the story. */ private function generateChronologicalKey() { // Use the epoch timestamp for the upper 32 bits of the key. Default to // the current time if the story doesn't have an explicit timestamp. $time = nonempty($this->storyTime, time()); // Generate a random number for the lower 32 bits of the key. $rand = head(unpack('L', Filesystem::readRandomBytes(4))); // On 32-bit machines, we have to get creative. if (PHP_INT_SIZE < 8) { // We're on a 32-bit machine. if (function_exists('bcadd')) { // Try to use the 'bc' extension. return bcadd(bcmul($time, bcpow(2, 32)), $rand); } else { // Do the math in MySQL. TODO: If we formalize a bc dependency, get // rid of this. $conn_r = id(new PhabricatorFeedStoryData())->establishConnection('r'); $result = queryfx_one( $conn_r, 'SELECT (%d << 32) + %d as N', $time, $rand); return $result['N']; } } else { // This is a 64 bit machine, so we can just do the math. return ($time << 32) + $rand; } } } diff --git a/src/applications/feed/query/PhabricatorFeedQuery.php b/src/applications/feed/query/PhabricatorFeedQuery.php index ae1da3aba0..ce472a6026 100644 --- a/src/applications/feed/query/PhabricatorFeedQuery.php +++ b/src/applications/feed/query/PhabricatorFeedQuery.php @@ -1,165 +1,163 @@ filterPHIDs = $phids; return $this; } public function withChronologicalKeys(array $keys) { $this->chronologicalKeys = $keys; return $this; } public function withEpochInRange($range_min, $range_max) { $this->rangeMin = $range_min; $this->rangeMax = $range_max; return $this; } public function newResultObject() { return new PhabricatorFeedStoryData(); } protected function loadPage() { // NOTE: We return raw rows from this method, which is a little unusual. return $this->loadStandardPageRows($this->newResultObject()); } protected function willFilterPage(array $data) { return PhabricatorFeedStory::loadAllFromRows($data, $this->getViewer()); } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { $joins = parent::buildJoinClauseParts($conn); // NOTE: We perform this join unconditionally (even if we have no filter // PHIDs) to omit rows which have no story references. These story data // rows are notifications or realtime alerts. $ref_table = new PhabricatorFeedStoryReference(); $joins[] = qsprintf( $conn, 'JOIN %T ref ON ref.chronologicalKey = story.chronologicalKey', $ref_table->getTableName()); return $joins; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->filterPHIDs !== null) { $where[] = qsprintf( $conn, 'ref.objectPHID IN (%Ls)', $this->filterPHIDs); } if ($this->chronologicalKeys !== null) { - // NOTE: We want to use integers in the query so we can take advantage - // of keys, but can't use %d on 32-bit systems. Make sure all the keys - // are integers and then format them raw. - - $keys = $this->chronologicalKeys; - foreach ($keys as $key) { - if (!ctype_digit($key)) { - throw new Exception( - pht("Key '%s' is not a valid chronological key!", $key)); - } - } + // NOTE: We can't use "%d" to format these large integers on 32-bit + // systems. Historically, we formatted these into integers in an + // awkward way because MySQL could sometimes (?) fail to use the proper + // keys if the values were formatted as strings instead of integers. + + // After the "qsprintf()" update to use PhutilQueryString, we can no + // longer do this in a sneaky way. However, the MySQL key issue also + // no longer appears to reproduce across several systems. So: just use + // strings until problems turn up? $where[] = qsprintf( $conn, - 'ref.chronologicalKey IN (%Q)', - implode(', ', $keys)); + 'ref.chronologicalKey IN (%Ls)', + $this->chronologicalKeys); } // NOTE: We may not have 64-bit PHP, so do the shifts in MySQL instead. // From EXPLAIN, it appears like MySQL is smart enough to compute the // result and make use of keys to execute the query. if ($this->rangeMin !== null) { $where[] = qsprintf( $conn, 'ref.chronologicalKey >= (%d << 32)', $this->rangeMin); } if ($this->rangeMax !== null) { $where[] = qsprintf( $conn, 'ref.chronologicalKey < (%d << 32)', $this->rangeMax); } return $where; } protected function buildGroupClause(AphrontDatabaseConnection $conn) { if ($this->filterPHIDs !== null) { return qsprintf($conn, 'GROUP BY ref.chronologicalKey'); } else { return qsprintf($conn, 'GROUP BY story.chronologicalKey'); } } protected function getDefaultOrderVector() { return array('key'); } public function getBuiltinOrders() { return array( 'newest' => array( 'vector' => array('key'), 'name' => pht('Creation (Newest First)'), 'aliases' => array('created'), ), 'oldest' => array( 'vector' => array('-key'), 'name' => pht('Creation (Oldest First)'), ), ); } public function getOrderableColumns() { $table = ($this->filterPHIDs ? 'ref' : 'story'); return array( 'key' => array( 'table' => $table, 'column' => 'chronologicalKey', 'type' => 'string', 'unique' => true, ), ); } protected function getPagingValueMap($cursor, array $keys) { return array( 'key' => $cursor, ); } protected function getResultCursor($item) { if ($item instanceof PhabricatorFeedStory) { return $item->getChronologicalKey(); } return $item['chronologicalKey']; } protected function getPrimaryTableAlias() { return 'story'; } public function getQueryApplicationClass() { return 'PhabricatorFeedApplication'; } } diff --git a/src/applications/files/query/PhabricatorFileChunkQuery.php b/src/applications/files/query/PhabricatorFileChunkQuery.php index b6fda13103..4398860569 100644 --- a/src/applications/files/query/PhabricatorFileChunkQuery.php +++ b/src/applications/files/query/PhabricatorFileChunkQuery.php @@ -1,134 +1,134 @@ chunkHandles = $handles; return $this; } public function withByteRange($start, $end) { $this->rangeStart = $start; $this->rangeEnd = $end; return $this; } public function withIsComplete($complete) { $this->isComplete = $complete; return $this; } public function needDataFiles($need) { $this->needDataFiles = $need; return $this; } protected function loadPage() { $table = new PhabricatorFileChunk(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } protected function willFilterPage(array $chunks) { if ($this->needDataFiles) { $file_phids = mpull($chunks, 'getDataFilePHID'); $file_phids = array_filter($file_phids); if ($file_phids) { $files = id(new PhabricatorFileQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); } else { $files = array(); } foreach ($chunks as $key => $chunk) { $data_phid = $chunk->getDataFilePHID(); if (!$data_phid) { $chunk->attachDataFile(null); continue; } $file = idx($files, $data_phid); if (!$file) { unset($chunks[$key]); $this->didRejectResult($chunk); continue; } $chunk->attachDataFile($file); } if (!$chunks) { return $chunks; } } return $chunks; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->chunkHandles !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'chunkHandle IN (%Ls)', $this->chunkHandles); } if ($this->rangeStart !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'byteEnd > %d', $this->rangeStart); } if ($this->rangeEnd !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'byteStart < %d', $this->rangeEnd); } if ($this->isComplete !== null) { if ($this->isComplete) { $where[] = qsprintf( - $conn_r, + $conn, 'dataFilePHID IS NOT NULL'); } else { $where[] = qsprintf( - $conn_r, + $conn, 'dataFilePHID IS NULL'); } } - $where[] = $this->buildPagingClause($conn_r); + $where[] = $this->buildPagingClause($conn); - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } public function getQueryApplicationClass() { return 'PhabricatorFilesApplication'; } } diff --git a/src/applications/files/query/PhabricatorFileQuery.php b/src/applications/files/query/PhabricatorFileQuery.php index 9f1659a7f3..4205ab5c5d 100644 --- a/src/applications/files/query/PhabricatorFileQuery.php +++ b/src/applications/files/query/PhabricatorFileQuery.php @@ -1,483 +1,483 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withAuthorPHIDs(array $phids) { $this->authorPHIDs = $phids; return $this; } public function withDateCreatedBefore($date_created_before) { $this->dateCreatedBefore = $date_created_before; return $this; } public function withDateCreatedAfter($date_created_after) { $this->dateCreatedAfter = $date_created_after; return $this; } public function withContentHashes(array $content_hashes) { $this->contentHashes = $content_hashes; return $this; } public function withBuiltinKeys(array $keys) { $this->builtinKeys = $keys; return $this; } public function withIsBuiltin($is_builtin) { $this->isBuiltin = $is_builtin; return $this; } /** * Select files which are transformations of some other file. For example, * you can use this query to find previously generated thumbnails of an image * file. * * As a parameter, provide a list of transformation specifications. Each * specification is a dictionary with the keys `originalPHID` and `transform`. * The `originalPHID` is the PHID of the original file (the file which was * transformed) and the `transform` is the name of the transform to query * for. If you pass `true` as the `transform`, all transformations of the * file will be selected. * * For example: * * array( * array( * 'originalPHID' => 'PHID-FILE-aaaa', * 'transform' => 'sepia', * ), * array( * 'originalPHID' => 'PHID-FILE-bbbb', * 'transform' => true, * ), * ) * * This selects the `"sepia"` transformation of the file with PHID * `PHID-FILE-aaaa` and all transformations of the file with PHID * `PHID-FILE-bbbb`. * * @param list List of transform specifications, described above. * @return this */ public function withTransforms(array $specs) { foreach ($specs as $spec) { if (!is_array($spec) || empty($spec['originalPHID']) || empty($spec['transform'])) { throw new Exception( pht( "Transform specification must be a dictionary with keys ". "'%s' and '%s'!", 'originalPHID', 'transform')); } } $this->transforms = $specs; return $this; } public function withLengthBetween($min, $max) { $this->minLength = $min; $this->maxLength = $max; return $this; } public function withNames(array $names) { $this->names = $names; return $this; } public function withIsPartial($partial) { $this->isPartial = $partial; return $this; } public function withIsDeleted($deleted) { $this->isDeleted = $deleted; return $this; } public function withNameNgrams($ngrams) { return $this->withNgramsConstraint( id(new PhabricatorFileNameNgrams()), $ngrams); } public function showOnlyExplicitUploads($explicit_uploads) { $this->explicitUploads = $explicit_uploads; return $this; } public function needTransforms(array $transforms) { $this->needTransforms = $transforms; return $this; } public function newResultObject() { return new PhabricatorFile(); } protected function loadPage() { $files = $this->loadStandardPage($this->newResultObject()); if (!$files) { return $files; } // Figure out which files we need to load attached objects for. In most // cases, we need to load attached objects to perform policy checks for // files. // However, in some special cases where we know files will always be // visible, we skip this. See T8478 and T13106. $need_objects = array(); $need_xforms = array(); foreach ($files as $file) { $always_visible = false; if ($file->getIsProfileImage()) { $always_visible = true; } if ($file->isBuiltin()) { $always_visible = true; } if ($always_visible) { // We just treat these files as though they aren't attached to // anything. This saves a query in common cases when we're loading // profile images or builtins. We could be slightly more nuanced // about this and distinguish between "not attached to anything" and // "might be attached but policy checks don't need to care". $file->attachObjectPHIDs(array()); continue; } $need_objects[] = $file; $need_xforms[] = $file; } $viewer = $this->getViewer(); $is_omnipotent = $viewer->isOmnipotent(); // If we have any files left which do need objects, load the edges now. $object_phids = array(); if ($need_objects) { $edge_type = PhabricatorFileHasObjectEdgeType::EDGECONST; $file_phids = mpull($need_objects, 'getPHID'); $edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($file_phids) ->withEdgeTypes(array($edge_type)) ->execute(); foreach ($need_objects as $file) { $phids = array_keys($edges[$file->getPHID()][$edge_type]); $file->attachObjectPHIDs($phids); if ($is_omnipotent) { // If the viewer is omnipotent, we don't need to load the associated // objects either since the viewer can certainly see the object. // Skipping this can improve performance and prevent cycles. This // could possibly become part of the profile/builtin code above which // short circuits attacment policy checks in cases where we know them // to be unnecessary. continue; } foreach ($phids as $phid) { $object_phids[$phid] = true; } } } // If this file is a transform of another file, load that file too. If you // can see the original file, you can see the thumbnail. // TODO: It might be nice to put this directly on PhabricatorFile and // remove the PhabricatorTransformedFile table, which would be a little // simpler. if ($need_xforms) { $xforms = id(new PhabricatorTransformedFile())->loadAllWhere( 'transformedPHID IN (%Ls)', mpull($need_xforms, 'getPHID')); $xform_phids = mpull($xforms, 'getOriginalPHID', 'getTransformedPHID'); foreach ($xform_phids as $derived_phid => $original_phid) { $object_phids[$original_phid] = true; } } else { $xform_phids = array(); } $object_phids = array_keys($object_phids); // Now, load the objects. $objects = array(); if ($object_phids) { // NOTE: We're explicitly turning policy exceptions off, since the rule // here is "you can see the file if you can see ANY associated object". // Without this explicit flag, we'll incorrectly throw unless you can // see ALL associated objects. $objects = id(new PhabricatorObjectQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($object_phids) ->setRaisePolicyExceptions(false) ->execute(); $objects = mpull($objects, null, 'getPHID'); } foreach ($files as $file) { $file_objects = array_select_keys($objects, $file->getObjectPHIDs()); $file->attachObjects($file_objects); } foreach ($files as $key => $file) { $original_phid = idx($xform_phids, $file->getPHID()); if ($original_phid == PhabricatorPHIDConstants::PHID_VOID) { // This is a special case for builtin files, which are handled // oddly. $original = null; } else if ($original_phid) { $original = idx($objects, $original_phid); if (!$original) { // If the viewer can't see the original file, also prevent them from // seeing the transformed file. $this->didRejectResult($file); unset($files[$key]); continue; } } else { $original = null; } $file->attachOriginalFile($original); } return $files; } protected function didFilterPage(array $files) { $xform_keys = $this->needTransforms; if ($xform_keys !== null) { $xforms = id(new PhabricatorTransformedFile())->loadAllWhere( 'originalPHID IN (%Ls) AND transform IN (%Ls)', mpull($files, 'getPHID'), $xform_keys); if ($xforms) { $xfiles = id(new PhabricatorFile())->loadAllWhere( 'phid IN (%Ls)', mpull($xforms, 'getTransformedPHID')); $xfiles = mpull($xfiles, null, 'getPHID'); } $xform_map = array(); foreach ($xforms as $xform) { $xfile = idx($xfiles, $xform->getTransformedPHID()); if (!$xfile) { continue; } $original_phid = $xform->getOriginalPHID(); $xform_key = $xform->getTransform(); $xform_map[$original_phid][$xform_key] = $xfile; } $default_xforms = array_fill_keys($xform_keys, null); foreach ($files as $file) { $file_xforms = idx($xform_map, $file->getPHID(), array()); $file_xforms += $default_xforms; $file->attachTransforms($file_xforms); } } return $files; } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { $joins = parent::buildJoinClauseParts($conn); if ($this->transforms) { $joins[] = qsprintf( $conn, 'JOIN %T t ON t.transformedPHID = f.phid', id(new PhabricatorTransformedFile())->getTableName()); } return $joins; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'f.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'f.phid IN (%Ls)', $this->phids); } if ($this->authorPHIDs !== null) { $where[] = qsprintf( $conn, 'f.authorPHID IN (%Ls)', $this->authorPHIDs); } if ($this->explicitUploads !== null) { $where[] = qsprintf( $conn, 'f.isExplicitUpload = %d', (int)$this->explicitUploads); } if ($this->transforms !== null) { $clauses = array(); foreach ($this->transforms as $transform) { if ($transform['transform'] === true) { $clauses[] = qsprintf( $conn, '(t.originalPHID = %s)', $transform['originalPHID']); } else { $clauses[] = qsprintf( $conn, '(t.originalPHID = %s AND t.transform = %s)', $transform['originalPHID'], $transform['transform']); } } - $where[] = qsprintf($conn, '(%Q)', implode(') OR (', $clauses)); + $where[] = qsprintf($conn, '%LO', $clauses); } if ($this->dateCreatedAfter !== null) { $where[] = qsprintf( $conn, 'f.dateCreated >= %d', $this->dateCreatedAfter); } if ($this->dateCreatedBefore !== null) { $where[] = qsprintf( $conn, 'f.dateCreated <= %d', $this->dateCreatedBefore); } if ($this->contentHashes !== null) { $where[] = qsprintf( $conn, 'f.contentHash IN (%Ls)', $this->contentHashes); } if ($this->minLength !== null) { $where[] = qsprintf( $conn, 'byteSize >= %d', $this->minLength); } if ($this->maxLength !== null) { $where[] = qsprintf( $conn, 'byteSize <= %d', $this->maxLength); } if ($this->names !== null) { $where[] = qsprintf( $conn, 'name in (%Ls)', $this->names); } if ($this->isPartial !== null) { $where[] = qsprintf( $conn, 'isPartial = %d', (int)$this->isPartial); } if ($this->isDeleted !== null) { $where[] = qsprintf( $conn, 'isDeleted = %d', (int)$this->isDeleted); } if ($this->builtinKeys !== null) { $where[] = qsprintf( $conn, 'builtinKey IN (%Ls)', $this->builtinKeys); } if ($this->isBuiltin !== null) { if ($this->isBuiltin) { $where[] = qsprintf( $conn, 'builtinKey IS NOT NULL'); } else { $where[] = qsprintf( $conn, 'builtinKey IS NULL'); } } return $where; } protected function getPrimaryTableAlias() { return 'f'; } public function getQueryApplicationClass() { return 'PhabricatorFilesApplication'; } } diff --git a/src/applications/flag/query/PhabricatorFlagQuery.php b/src/applications/flag/query/PhabricatorFlagQuery.php index ff154a512b..3418f10746 100644 --- a/src/applications/flag/query/PhabricatorFlagQuery.php +++ b/src/applications/flag/query/PhabricatorFlagQuery.php @@ -1,166 +1,166 @@ ownerPHIDs = $owner_phids; return $this; } public function withTypes(array $types) { $this->types = $types; return $this; } public function withObjectPHIDs(array $object_phids) { $this->objectPHIDs = $object_phids; return $this; } public function withColors(array $colors) { $this->colors = $colors; return $this; } /** * NOTE: this is done in PHP and not in MySQL, which means its inappropriate * for large datasets. Pragmatically, this is fine for user flags which are * typically well under 100 flags per user. */ public function setGroupBy($group) { $this->groupBy = $group; return $this; } public function needHandles($need) { $this->needHandles = $need; return $this; } public function needObjects($need) { $this->needObjects = $need; return $this; } public static function loadUserFlag(PhabricatorUser $user, $object_phid) { // Specifying the type in the query allows us to use a key. return id(new PhabricatorFlagQuery()) ->setViewer($user) ->withOwnerPHIDs(array($user->getPHID())) ->withTypes(array(phid_get_type($object_phid))) ->withObjectPHIDs(array($object_phid)) ->executeOne(); } protected function loadPage() { $table = new PhabricatorFlag(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T flag %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } protected function willFilterPage(array $flags) { if ($this->needObjects) { $objects = id(new PhabricatorObjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs(mpull($flags, 'getObjectPHID')) ->execute(); $objects = mpull($objects, null, 'getPHID'); foreach ($flags as $key => $flag) { $object = idx($objects, $flag->getObjectPHID()); if ($object) { $flags[$key]->attachObject($object); } else { unset($flags[$key]); } } } if ($this->needHandles) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->getViewer()) ->withPHIDs(mpull($flags, 'getObjectPHID')) ->execute(); foreach ($flags as $flag) { $flag->attachHandle($handles[$flag->getObjectPHID()]); } } switch ($this->groupBy) { case self::GROUP_COLOR: $flags = msort($flags, 'getColor'); break; case self::GROUP_NONE: break; default: throw new Exception( pht('Unknown groupBy parameter: %s', $this->groupBy)); break; } return $flags; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ownerPHIDs) { $where[] = qsprintf( - $conn_r, + $conn, 'flag.ownerPHID IN (%Ls)', $this->ownerPHIDs); } if ($this->types) { $where[] = qsprintf( - $conn_r, + $conn, 'flag.type IN (%Ls)', $this->types); } if ($this->objectPHIDs) { $where[] = qsprintf( - $conn_r, + $conn, 'flag.objectPHID IN (%Ls)', $this->objectPHIDs); } if ($this->colors) { $where[] = qsprintf( - $conn_r, + $conn, 'flag.color IN (%Ld)', $this->colors); } - $where[] = $this->buildPagingClause($conn_r); + $where[] = $this->buildPagingClause($conn); - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } public function getQueryApplicationClass() { return 'PhabricatorFlagsApplication'; } } diff --git a/src/applications/fund/query/FundBackerQuery.php b/src/applications/fund/query/FundBackerQuery.php index 5f7406f5ad..2d6e13dfd9 100644 --- a/src/applications/fund/query/FundBackerQuery.php +++ b/src/applications/fund/query/FundBackerQuery.php @@ -1,118 +1,118 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } public function withInitiativePHIDs(array $phids) { $this->initiativePHIDs = $phids; return $this; } public function withBackerPHIDs(array $phids) { $this->backerPHIDs = $phids; return $this; } protected function loadPage() { $table = new FundBacker(); $conn_r = $table->establishConnection('r'); $rows = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($rows); } protected function willFilterPage(array $backers) { $initiative_phids = mpull($backers, 'getInitiativePHID'); $initiatives = id(new PhabricatorObjectQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($initiative_phids) ->execute(); $initiatives = mpull($initiatives, null, 'getPHID'); foreach ($backers as $backer) { $initiative_phid = $backer->getInitiativePHID(); $initiative = idx($initiatives, $initiative_phid); $backer->attachInitiative($initiative); } return $backers; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); - $where[] = $this->buildPagingClause($conn_r); + $where[] = $this->buildPagingClause($conn); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } if ($this->initiativePHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'initiativePHID IN (%Ls)', $this->initiativePHIDs); } if ($this->backerPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'backerPHID IN (%Ls)', $this->backerPHIDs); } if ($this->statuses !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'status IN (%Ls)', $this->statuses); } - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } public function getQueryApplicationClass() { return 'PhabricatorFundApplication'; } } diff --git a/src/applications/harbormaster/conduit/HarbormasterBuildableSearchAPIMethod.php b/src/applications/harbormaster/conduit/HarbormasterBuildableSearchAPIMethod.php new file mode 100644 index 0000000000..28471976e2 --- /dev/null +++ b/src/applications/harbormaster/conduit/HarbormasterBuildableSearchAPIMethod.php @@ -0,0 +1,18 @@ +setLabel(pht('Builds')) + ->setKey('buildPHIDs') + ->setAliases(array('build', 'builds', 'buildPHID')) + ->setDescription( + pht('Search for targets of a given build.')) + ->setDatasource(new HarbormasterBuildPlanDatasource()), + ); + } + + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + + if ($map['buildPHIDs']) { + $query->withBuildPHIDs($map['buildPHIDs']); + } + + return $query; + } + + protected function getURI($path) { + return '/harbormaster/target/'.$path; + } + + protected function getBuiltinQueryNames() { + return array( + 'all' => pht('All Targets'), + ); + } + + public function buildSavedQueryFromBuiltin($query_key) { + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + + protected function renderResultList( + array $builds, + PhabricatorSavedQuery $query, + array $handles) { + assert_instances_of($builds, 'HarbormasterBuildTarget'); + + // Currently, this only supports the "harbormaster.target.search" + // API method. + throw new PhutilMethodNotImplementedException(); + } + +} diff --git a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php index e0dfe05cc0..3bbb72cd95 100644 --- a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php @@ -1,336 +1,356 @@ currentWorkerTaskID = $id; return $this; } public function getCurrentWorkerTaskID() { return $this->currentWorkerTaskID; } public static function getImplementations() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->execute(); } public static function getImplementation($class) { $base = idx(self::getImplementations(), $class); if ($base) { return (clone $base); } return null; } public static function requireImplementation($class) { if (!$class) { throw new Exception(pht('No implementation is specified!')); } $implementation = self::getImplementation($class); if (!$implementation) { throw new Exception(pht('No such implementation "%s" exists!', $class)); } return $implementation; } /** * The name of the implementation. */ abstract public function getName(); public function getBuildStepGroupKey() { return HarbormasterOtherBuildStepGroup::GROUPKEY; } /** * The generic description of the implementation. */ public function getGenericDescription() { return ''; } /** * The description of the implementation, based on the current settings. */ public function getDescription() { return $this->getGenericDescription(); } public function getEditInstructions() { return null; } /** * Run the build target against the specified build. */ abstract public function execute( HarbormasterBuild $build, HarbormasterBuildTarget $build_target); /** * Gets the settings for this build step. */ public function getSettings() { return $this->settings; } public function getSetting($key, $default = null) { return idx($this->settings, $key, $default); } /** * Loads the settings for this build step implementation from a build * step or target. */ final public function loadSettings($build_object) { $this->settings = $build_object->getDetails(); return $this; } /** * Return the name of artifacts produced by this command. * * Future steps will calculate all available artifact mappings * before them and filter on the type. * * @return array The mappings of artifact names to their types. */ public function getArtifactInputs() { return array(); } public function getArtifactOutputs() { return array(); } public function getDependencies(HarbormasterBuildStep $build_step) { $dependencies = $build_step->getDetail('dependsOn', array()); $inputs = $build_step->getStepImplementation()->getArtifactInputs(); $inputs = ipull($inputs, null, 'key'); $artifacts = $this->getAvailableArtifacts( $build_step->getBuildPlan(), $build_step, null); foreach ($artifacts as $key => $type) { if (!array_key_exists($key, $inputs)) { unset($artifacts[$key]); } } $artifact_steps = ipull($artifacts, 'step'); $artifact_steps = mpull($artifact_steps, 'getPHID'); $dependencies = array_merge($dependencies, $artifact_steps); return $dependencies; } /** * Returns a list of all artifacts made available in the build plan. */ public static function getAvailableArtifacts( HarbormasterBuildPlan $build_plan, $current_build_step, $artifact_type) { $steps = id(new HarbormasterBuildStepQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBuildPlanPHIDs(array($build_plan->getPHID())) ->execute(); $artifacts = array(); $artifact_arrays = array(); foreach ($steps as $step) { if ($current_build_step !== null && $step->getPHID() === $current_build_step->getPHID()) { continue; } $implementation = $step->getStepImplementation(); $array = $implementation->getArtifactOutputs(); $array = ipull($array, 'type', 'key'); foreach ($array as $name => $type) { if ($type !== $artifact_type && $artifact_type !== null) { continue; } $artifacts[$name] = array('type' => $type, 'step' => $step); } } return $artifacts; } /** * Convert a user-provided string with variables in it, like: * * ls ${dirname} * * ...into a string with variables merged into it safely: * * ls 'dir with spaces' * * @param string Name of a `vxsprintf` function, like @{function:vcsprintf}. * @param string User-provided pattern string containing `${variables}`. * @param dict List of available replacement variables. * @return string String with variables replaced safely into it. */ protected function mergeVariables($function, $pattern, array $variables) { $regexp = '@\\$\\{(?P[a-z\\./_-]+)\\}@'; $matches = null; preg_match_all($regexp, $pattern, $matches); $argv = array(); foreach ($matches['name'] as $name) { if (!array_key_exists($name, $variables)) { throw new Exception(pht("No such variable '%s'!", $name)); } $argv[] = $variables[$name]; } $pattern = str_replace('%', '%%', $pattern); $pattern = preg_replace($regexp, '%s', $pattern); return call_user_func($function, $pattern, $argv); } public function getFieldSpecifications() { return array(); } protected function formatSettingForDescription($key, $default = null) { return $this->formatValueForDescription($this->getSetting($key, $default)); } protected function formatValueForDescription($value) { if (strlen($value)) { return phutil_tag('strong', array(), $value); } else { return phutil_tag('em', array(), pht('(null)')); } } public function supportsWaitForMessage() { return false; } public function shouldWaitForMessage(HarbormasterBuildTarget $target) { if (!$this->supportsWaitForMessage()) { return false; } $wait = $target->getDetail('builtin.wait-for-message'); return ($wait == 'wait'); } protected function shouldAbort( HarbormasterBuild $build, HarbormasterBuildTarget $target) { return $build->getBuildGeneration() !== $target->getBuildGeneration(); } protected function resolveFutures( HarbormasterBuild $build, HarbormasterBuildTarget $target, array $futures) { + $did_close = false; + $wait_start = PhabricatorTime::getNow(); + $futures = new FutureIterator($futures); foreach ($futures->setUpdateInterval(5) as $key => $future) { - if ($future === null) { - $build->reload(); - if ($this->shouldAbort($build, $target)) { - throw new HarbormasterBuildAbortedException(); + if ($future !== null) { + continue; + } + + $build->reload(); + if ($this->shouldAbort($build, $target)) { + throw new HarbormasterBuildAbortedException(); + } + + // See PHI916. If we're waiting on a remote system for a while, clean + // up database connections to reduce the cost of having a large number + // of processes babysitting an `ssh ... ./run-huge-build.sh` process on + // a build host. + if (!$did_close) { + $now = PhabricatorTime::getNow(); + $elapsed = ($now - $wait_start); + $idle_limit = 5; + + if ($elapsed >= $idle_limit) { + LiskDAO::closeIdleConnections(); + $did_close = true; } } } } protected function logHTTPResponse( HarbormasterBuild $build, HarbormasterBuildTarget $build_target, BaseHTTPFuture $future, $label) { list($status, $body, $headers) = $future->resolve(); $header_lines = array(); // TODO: We don't currently preserve the entire "HTTP" response header, but // should. Once we do, reproduce it here faithfully. $status_code = $status->getStatusCode(); $header_lines[] = "HTTP {$status_code}"; foreach ($headers as $header) { list($head, $tail) = $header; $header_lines[] = "{$head}: {$tail}"; } $header_lines = implode("\n", $header_lines); $build_target ->newLog($label, 'http.head') ->append($header_lines); $build_target ->newLog($label, 'http.body') ->append($body); } protected function logSilencedCall( HarbormasterBuild $build, HarbormasterBuildTarget $build_target, $label) { $build_target ->newLog($label, 'silenced') ->append( pht( 'Declining to make service call because `phabricator.silent` is '. 'enabled in configuration.')); } public function willStartBuild( PhabricatorUser $viewer, HarbormasterBuildable $buildable, HarbormasterBuild $build, HarbormasterBuildPlan $plan, HarbormasterBuildStep $step) { return; } /* -( Automatic Targets )-------------------------------------------------- */ public function getBuildStepAutotargetStepKey() { return null; } public function getBuildStepAutotargetPlanKey() { throw new PhutilMethodNotImplementedException(); } public function shouldRequireAutotargeting() { return false; } } diff --git a/src/applications/harbormaster/storage/HarbormasterBuildable.php b/src/applications/harbormaster/storage/HarbormasterBuildable.php index b24c4805e2..98f374579b 100644 --- a/src/applications/harbormaster/storage/HarbormasterBuildable.php +++ b/src/applications/harbormaster/storage/HarbormasterBuildable.php @@ -1,386 +1,427 @@ setIsManualBuildable(0) ->setBuildableStatus(HarbormasterBuildableStatus::STATUS_PREPARING); } public function getMonogram() { return 'B'.$this->getID(); } public function getURI() { return '/'.$this->getMonogram(); } /** * Returns an existing buildable for the object's PHID or creates a * new buildable implicitly if needed. */ public static function createOrLoadExisting( PhabricatorUser $actor, $buildable_object_phid, $container_object_phid) { $buildable = id(new HarbormasterBuildableQuery()) ->setViewer($actor) ->withBuildablePHIDs(array($buildable_object_phid)) ->withManualBuildables(false) ->setLimit(1) ->executeOne(); if ($buildable) { return $buildable; } $buildable = self::initializeNewBuildable($actor) ->setBuildablePHID($buildable_object_phid) ->setContainerPHID($container_object_phid); $buildable->save(); return $buildable; } /** * Start builds for a given buildable. * * @param phid PHID of the object to build. * @param phid Container PHID for the buildable. * @param list List of builds to perform. * @return void */ public static function applyBuildPlans( $phid, $container_phid, array $requests) { assert_instances_of($requests, 'HarbormasterBuildRequest'); if (!$requests) { return; } // Skip all of this logic if the Harbormaster application // isn't currently installed. $harbormaster_app = 'PhabricatorHarbormasterApplication'; if (!PhabricatorApplication::isClassInstalled($harbormaster_app)) { return; } $viewer = PhabricatorUser::getOmnipotentUser(); $buildable = self::createOrLoadExisting( $viewer, $phid, $container_phid); $plan_phids = mpull($requests, 'getBuildPlanPHID'); $plans = id(new HarbormasterBuildPlanQuery()) ->setViewer($viewer) ->withPHIDs($plan_phids) ->execute(); $plans = mpull($plans, null, 'getPHID'); foreach ($requests as $request) { $plan_phid = $request->getBuildPlanPHID(); $plan = idx($plans, $plan_phid); if (!$plan) { throw new Exception( pht( 'Failed to load build plan ("%s").', $plan_phid)); } if ($plan->isDisabled()) { // TODO: This should be communicated more clearly -- maybe we should // create the build but set the status to "disabled" or "derelict". continue; } $parameters = $request->getBuildParameters(); $buildable->applyPlan($plan, $parameters, $request->getInitiatorPHID()); } } public function applyPlan( HarbormasterBuildPlan $plan, array $parameters, $initiator_phid) { $viewer = PhabricatorUser::getOmnipotentUser(); $build = HarbormasterBuild::initializeNewBuild($viewer) ->setBuildablePHID($this->getPHID()) ->setBuildPlanPHID($plan->getPHID()) ->setBuildParameters($parameters) ->setBuildStatus(HarbormasterBuildStatus::STATUS_PENDING); if ($initiator_phid) { $build->setInitiatorPHID($initiator_phid); } $auto_key = $plan->getPlanAutoKey(); if ($auto_key) { $build->setPlanAutoKey($auto_key); } $build->save(); $steps = id(new HarbormasterBuildStepQuery()) ->setViewer($viewer) ->withBuildPlanPHIDs(array($plan->getPHID())) ->execute(); foreach ($steps as $step) { $step->willStartBuild($viewer, $this, $build, $plan); } PhabricatorWorker::scheduleTask( 'HarbormasterBuildWorker', array( 'buildID' => $build->getID(), ), array( 'objectPHID' => $build->getPHID(), )); return $build; } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'containerPHID' => 'phid?', 'buildableStatus' => 'text32', 'isManualBuildable' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_buildable' => array( 'columns' => array('buildablePHID'), ), 'key_container' => array( 'columns' => array('containerPHID'), ), 'key_manual' => array( 'columns' => array('isManualBuildable'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( HarbormasterBuildablePHIDType::TYPECONST); } public function attachBuildableObject($buildable_object) { $this->buildableObject = $buildable_object; return $this; } public function getBuildableObject() { return $this->assertAttached($this->buildableObject); } public function attachContainerObject($container_object) { $this->containerObject = $container_object; return $this; } public function getContainerObject() { return $this->assertAttached($this->containerObject); } public function attachBuilds(array $builds) { assert_instances_of($builds, 'HarbormasterBuild'); $this->builds = $builds; return $this; } public function getBuilds() { return $this->assertAttached($this->builds); } /* -( Status )------------------------------------------------------------- */ public function getBuildableStatusObject() { $status = $this->getBuildableStatus(); return HarbormasterBuildableStatus::newBuildableStatusObject($status); } public function getStatusIcon() { return $this->getBuildableStatusObject()->getIcon(); } public function getStatusDisplayName() { return $this->getBuildableStatusObject()->getDisplayName(); } public function getStatusColor() { return $this->getBuildableStatusObject()->getColor(); } public function isPreparing() { return $this->getBuildableStatusObject()->isPreparing(); } public function isBuilding() { return $this->getBuildableStatusObject()->isBuilding(); } /* -( Messages )----------------------------------------------------------- */ public function sendMessage( PhabricatorUser $viewer, $message_type, $queue_update) { $message = HarbormasterBuildMessage::initializeNewMessage($viewer) ->setReceiverPHID($this->getPHID()) ->setType($message_type) ->save(); if ($queue_update) { PhabricatorWorker::scheduleTask( 'HarbormasterBuildWorker', array( 'buildablePHID' => $this->getPHID(), ), array( 'objectPHID' => $this->getPHID(), )); } return $message; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new HarbormasterBuildableTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new HarbormasterBuildableTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getBuildableObject()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBuildableObject()->hasAutomaticCapability( $capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('A buildable inherits policies from the underlying object.'); } /* -( HarbormasterBuildableInterface )------------------------------------- */ public function getHarbormasterBuildableDisplayPHID() { return $this->getBuildableObject()->getHarbormasterBuildableDisplayPHID(); } public function getHarbormasterBuildablePHID() { // NOTE: This is essentially just for convenience, as it allows you create // a copy of a buildable by specifying `B123` without bothering to go // look up the underlying object. return $this->getBuildablePHID(); } public function getHarbormasterContainerPHID() { return $this->getContainerPHID(); } public function getBuildVariables() { return array(); } public function getAvailableBuildVariables() { return array(); } public function newBuildableEngine() { return $this->getBuildableObject()->newBuildableEngine(); } +/* -( PhabricatorConduitResultInterface )---------------------------------- */ + + + public function getFieldSpecificationsForConduit() { + return array( + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('objectPHID') + ->setType('phid') + ->setDescription(pht('PHID of the object that is built.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('containerPHID') + ->setType('phid') + ->setDescription(pht('PHID of the object containing this buildable.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('buildableStatus') + ->setType('map') + ->setDescription(pht('The current status of this buildable.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('isManual') + ->setType('bool') + ->setDescription(pht('True if this is a manual buildable.')), + ); + } + + public function getFieldValuesForConduit() { + return array( + 'objectPHID' => $this->getBuildablePHID(), + 'containerPHID' => $this->getContainerPHID(), + 'buildableStatus' => array( + 'value' => $this->getBuildableStatus(), + ), + 'isManual' => (bool)$this->getIsManualBuildable(), + ); + } + + public function getConduitSearchAttachments() { + return array(); + } + + /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $viewer = $engine->getViewer(); $this->openTransaction(); $builds = id(new HarbormasterBuildQuery()) ->setViewer($viewer) ->withBuildablePHIDs(array($this->getPHID())) ->execute(); foreach ($builds as $build) { $engine->destroyObject($build); } $messages = id(new HarbormasterBuildMessageQuery()) ->setViewer($viewer) ->withReceiverPHIDs(array($this->getPHID())) ->execute(); foreach ($messages as $message) { $engine->destroyObject($message); } $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php b/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php index b559a66198..30b1bd79e4 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php @@ -1,415 +1,497 @@ setName($build_step->getName()) ->setBuildPHID($build->getPHID()) ->setBuildStepPHID($build_step->getPHID()) ->setClassName($build_step->getClassName()) ->setDetails($build_step->getDetails()) ->setTargetStatus(self::STATUS_PENDING) ->setVariables($variables) ->setBuildGeneration($build->getBuildGeneration()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'details' => self::SERIALIZATION_JSON, 'variables' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'className' => 'text255', 'targetStatus' => 'text64', 'dateStarted' => 'epoch?', 'dateCompleted' => 'epoch?', 'buildGeneration' => 'uint32', // T6203/NULLABILITY // This should not be nullable. 'name' => 'text255?', ), self::CONFIG_KEY_SCHEMA => array( 'key_build' => array( 'columns' => array('buildPHID', 'buildStepPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( HarbormasterBuildTargetPHIDType::TYPECONST); } public function attachBuild(HarbormasterBuild $build) { $this->build = $build; return $this; } public function getBuild() { return $this->assertAttached($this->build); } public function attachBuildStep(HarbormasterBuildStep $step = null) { $this->buildStep = $step; return $this; } public function getBuildStep() { return $this->assertAttached($this->buildStep); } public function getDetail($key, $default = null) { return idx($this->details, $key, $default); } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } public function getVariables() { return parent::getVariables() + $this->getBuildTargetVariables(); } public function getVariable($key, $default = null) { return idx($this->variables, $key, $default); } public function setVariable($key, $value) { $this->variables[$key] = $value; return $this; } public function getImplementation() { if ($this->implementation === null) { $obj = HarbormasterBuildStepImplementation::requireImplementation( $this->className); $obj->loadSettings($this); $this->implementation = $obj; } return $this->implementation; } public function isAutotarget() { try { return (bool)$this->getImplementation()->getBuildStepAutotargetPlanKey(); } catch (Exception $e) { return false; } } public function getName() { if (strlen($this->name) && !$this->isAutotarget()) { return $this->name; } try { return $this->getImplementation()->getName(); } catch (Exception $e) { return $this->getClassName(); } } private function getBuildTargetVariables() { return array( 'target.phid' => $this->getPHID(), ); } public function createArtifact( PhabricatorUser $actor, $artifact_key, $artifact_type, array $artifact_data) { $impl = HarbormasterArtifact::getArtifactType($artifact_type); if (!$impl) { throw new Exception( pht( 'There is no implementation available for artifacts of type "%s".', $artifact_type)); } $impl->validateArtifactData($artifact_data); $artifact = HarbormasterBuildArtifact::initializeNewBuildArtifact($this) ->setArtifactKey($artifact_key) ->setArtifactType($artifact_type) ->setArtifactData($artifact_data); $impl = $artifact->getArtifactImplementation(); $impl->willCreateArtifact($actor); return $artifact->save(); } public function loadArtifact($artifact_key) { $indexes = array(); $indexes[] = HarbormasterBuildArtifact::getArtifactIndex( $this, $artifact_key); $artifact = id(new HarbormasterBuildArtifactQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withArtifactIndexes($indexes) ->executeOne(); if ($artifact === null) { throw new Exception( pht( 'Artifact "%s" not found!', $artifact_key)); } return $artifact; } public function newLog($log_source, $log_type) { $log_source = id(new PhutilUTF8StringTruncator()) ->setMaximumBytes(250) ->truncateString($log_source); $log = HarbormasterBuildLog::initializeNewBuildLog($this) ->setLogSource($log_source) ->setLogType($log_type) ->openBuildLog(); return $log; } public function getFieldValue($key) { $field_list = PhabricatorCustomField::getObjectFields( $this->getBuildStep(), PhabricatorCustomField::ROLE_VIEW); $fields = $field_list->getFields(); $full_key = "std:harbormaster:core:{$key}"; $field = idx($fields, $full_key); if (!$field) { throw new Exception( pht( 'Unknown build step field "%s"!', $key)); } $field = clone $field; $field->setValueFromStorage($this->getDetail($key)); return $field->getBuildTargetFieldValue(); } /* -( Status )------------------------------------------------------------- */ public function isComplete() { switch ($this->getTargetStatus()) { case self::STATUS_PASSED: case self::STATUS_FAILED: case self::STATUS_ABORTED: return true; } return false; } public function isFailed() { switch ($this->getTargetStatus()) { case self::STATUS_FAILED: case self::STATUS_ABORTED: return true; } return false; } public function isWaiting() { switch ($this->getTargetStatus()) { case self::STATUS_WAITING: return true; } return false; } public function isUnderway() { switch ($this->getTargetStatus()) { case self::STATUS_PENDING: case self::STATUS_BUILDING: return true; } return false; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return $this->getBuild()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBuild()->hasAutomaticCapability( $capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('Users must be able to see a build to view its build targets.'); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $viewer = $engine->getViewer(); $this->openTransaction(); $lint_message = new HarbormasterBuildLintMessage(); $conn = $lint_message->establishConnection('w'); queryfx( $conn, 'DELETE FROM %T WHERE buildTargetPHID = %s', $lint_message->getTableName(), $this->getPHID()); $unit_message = new HarbormasterBuildUnitMessage(); $conn = $unit_message->establishConnection('w'); queryfx( $conn, 'DELETE FROM %T WHERE buildTargetPHID = %s', $unit_message->getTableName(), $this->getPHID()); $logs = id(new HarbormasterBuildLogQuery()) ->setViewer($viewer) ->withBuildTargetPHIDs(array($this->getPHID())) ->execute(); foreach ($logs as $log) { $engine->destroyObject($log); } $artifacts = id(new HarbormasterBuildArtifactQuery()) ->setViewer($viewer) ->withBuildTargetPHIDs(array($this->getPHID())) ->execute(); foreach ($artifacts as $artifact) { $engine->destroyObject($artifact); } $messages = id(new HarbormasterBuildMessageQuery()) ->setViewer($viewer) ->withReceiverPHIDs(array($this->getPHID())) ->execute(); foreach ($messages as $message) { $engine->destroyObject($message); } $this->delete(); $this->saveTransaction(); } +/* -( PhabricatorConduitResultInterface )---------------------------------- */ + + + public function getFieldSpecificationsForConduit() { + return array( + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('name') + ->setType('string') + ->setDescription(pht('The name of the build target.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('buildPHID') + ->setType('phid') + ->setDescription(pht('The build the target is associated with.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('buildStepPHID') + ->setType('phid') + ->setDescription(pht('The build step the target runs.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('status') + ->setType('map') + ->setDescription(pht('Status for the build target.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('epochStarted') + ->setType('epoch?') + ->setDescription( + pht( + 'Epoch timestamp for target start, if the target '. + 'has started.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('epochCompleted') + ->setType('epoch?') + ->setDescription( + pht( + 'Epoch timestamp for target completion, if the target '. + 'has completed.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('buildGeneration') + ->setType('int') + ->setDescription( + pht( + 'Build generation this target belongs to. When builds '. + 'restart, a new generation with new targets is created.')), + ); + } + + public function getFieldValuesForConduit() { + $status = $this->getTargetStatus(); + + $epoch_started = $this->getDateStarted(); + if ($epoch_started) { + $epoch_started = (int)$epoch_started; + } else { + $epoch_started = null; + } + + $epoch_completed = $this->getDateCompleted(); + if ($epoch_completed) { + $epoch_completed = (int)$epoch_completed; + } else { + $epoch_completed = null; + } + + return array( + 'name' => $this->getName(), + 'buildPHID' => $this->getBuildPHID(), + 'buildStepPHID' => $this->getBuildStepPHID(), + 'status' => array( + 'value' => $status, + 'name' => self::getBuildTargetStatusName($status), + ), + 'epochStarted' => $epoch_started, + 'epochCompleted' => $epoch_completed, + 'buildGeneration' => (int)$this->getBuildGeneration(), + ); + } + + public function getConduitSearchAttachments() { + return array(); + } + + } diff --git a/src/applications/herald/controller/HeraldDisableController.php b/src/applications/herald/controller/HeraldDisableController.php index bdbefa55ea..def87049f7 100644 --- a/src/applications/herald/controller/HeraldDisableController.php +++ b/src/applications/herald/controller/HeraldDisableController.php @@ -1,66 +1,66 @@ getViewer(); $id = $request->getURIData('id'); $action = $request->getURIData('action'); $rule = id(new HeraldRuleQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$rule) { return new Aphront404Response(); } if ($rule->isGlobalRule()) { $this->requireApplicationCapability( HeraldManageGlobalRulesCapability::CAPABILITY); } $view_uri = '/'.$rule->getMonogram(); $is_disable = ($action === 'disable'); if ($request->isFormPost()) { $xaction = id(new HeraldRuleTransaction()) ->setTransactionType(HeraldRuleTransaction::TYPE_DISABLE) ->setNewValue($is_disable); id(new HeraldRuleEditor()) ->setActor($viewer) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) ->applyTransactions($rule, array($xaction)); return id(new AphrontRedirectResponse())->setURI($view_uri); } if ($is_disable) { - $title = pht('Really archive this rule?'); + $title = pht('Really disable this rule?'); $body = pht('This rule will no longer activate.'); - $button = pht('Archive Rule'); + $button = pht('Disable Rule'); } else { - $title = pht('Really activate this rule?'); + $title = pht('Really enable this rule?'); $body = pht('This rule will become active again.'); - $button = pht('Activate Rule'); + $button = pht('Enable Rule'); } $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle($title) ->appendChild($body) ->addSubmitButton($button) ->addCancelButton($view_uri); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/herald/controller/HeraldRuleViewController.php b/src/applications/herald/controller/HeraldRuleViewController.php index 9e696c23c4..70a2d20224 100644 --- a/src/applications/herald/controller/HeraldRuleViewController.php +++ b/src/applications/herald/controller/HeraldRuleViewController.php @@ -1,162 +1,158 @@ getViewer(); $id = $request->getURIData('id'); $rule = id(new HeraldRuleQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needConditionsAndActions(true) + ->needValidateAuthors(true) ->executeOne(); if (!$rule) { return new Aphront404Response(); } $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($rule->getName()) ->setPolicyObject($rule) ->setHeaderIcon('fa-bullhorn'); if ($rule->getIsDisabled()) { - $header->setStatus( - 'fa-ban', - 'red', - pht('Archived')); + $header->setStatus('fa-ban', 'red', pht('Disabled')); + } else if (!$rule->hasValidAuthor()) { + $header->setStatus('fa-user', 'red', pht('Author Not Active')); } else { - $header->setStatus( - 'fa-check', - 'bluegrey', - pht('Active')); + $header->setStatus('fa-check', 'bluegrey', pht('Active')); } $curtain = $this->buildCurtain($rule); $details = $this->buildPropertySectionView($rule); $description = $this->buildDescriptionView($rule); $id = $rule->getID(); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb("H{$id}"); $crumbs->setBorder(true); $timeline = $this->buildTransactionTimeline( $rule, new HeraldTransactionQuery()); $timeline->setShouldTerminate(true); $title = $rule->getName(); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) ->setMainColumn($timeline) ->addPropertySection(pht('Details'), $details) ->addPropertySection(pht('Description'), $description); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild($view); } private function buildCurtain(HeraldRule $rule) { $viewer = $this->getViewer(); $id = $rule->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $rule, PhabricatorPolicyCapability::CAN_EDIT); $curtain = $this->newCurtainView($rule); $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Rule')) ->setHref($this->getApplicationURI("edit/{$id}/")) ->setIcon('fa-pencil') ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); if ($rule->getIsDisabled()) { $disable_uri = "disable/{$id}/enable/"; $disable_icon = 'fa-check'; - $disable_name = pht('Activate Rule'); + $disable_name = pht('Enable Rule'); } else { $disable_uri = "disable/{$id}/disable/"; $disable_icon = 'fa-ban'; - $disable_name = pht('Archive Rule'); + $disable_name = pht('Disable Rule'); } $curtain->addAction( id(new PhabricatorActionView()) - ->setName(pht('Disable Rule')) ->setHref($this->getApplicationURI($disable_uri)) ->setIcon($disable_icon) ->setName($disable_name) ->setDisabled(!$can_edit) ->setWorkflow(true)); return $curtain; } private function buildPropertySectionView( HeraldRule $rule) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer); $view->addProperty( pht('Rule Type'), idx(HeraldRuleTypeConfig::getRuleTypeMap(), $rule->getRuleType())); if ($rule->isPersonalRule()) { $view->addProperty( pht('Author'), $viewer->renderHandle($rule->getAuthorPHID())); } $adapter = HeraldAdapter::getAdapterForContentType($rule->getContentType()); if ($adapter) { $view->addProperty( pht('Applies To'), idx( HeraldAdapter::getEnabledAdapterMap($viewer), $rule->getContentType())); if ($rule->isObjectRule()) { $view->addProperty( pht('Trigger Object'), $viewer->renderHandle($rule->getTriggerObjectPHID())); } } return $view; } private function buildDescriptionView(HeraldRule $rule) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer); $adapter = HeraldAdapter::getAdapterForContentType($rule->getContentType()); if ($adapter) { $handles = $viewer->loadHandles(HeraldAdapter::getHandlePHIDs($rule)); $rule_text = $adapter->renderRuleAsText($rule, $handles, $viewer); $view->addTextContent($rule_text); return $view; } return null; } } diff --git a/src/applications/herald/management/HeraldManagementWorkflow.php b/src/applications/herald/management/HeraldManagementWorkflow.php new file mode 100644 index 0000000000..50559ec3bc --- /dev/null +++ b/src/applications/herald/management/HeraldManagementWorkflow.php @@ -0,0 +1,4 @@ +setName('test') + ->setExamples('**test** --object __object__ --type __type__') + ->setSynopsis( + pht( + 'Test content rules for an object. Executes a dry run, like the '. + 'web UI test console.')) + ->setArguments( + array( + array( + 'name' => 'object', + 'param' => 'object', + 'help' => pht('Run rules on this object.'), + ), + array( + 'name' => 'type', + 'param' => 'type', + 'help' => pht('Run rules for this content type.'), + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $viewer = $this->getViewer(); + + $object_name = $args->getArg('object'); + if (!strlen($object_name)) { + throw new PhutilArgumentUsageException( + pht('Specify an object to test rules for with "--object".')); + } + + $objects = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withNames(array($object_name)) + ->execute(); + if (!$objects) { + throw new PhutilArgumentUsageException( + pht( + 'Unable to load specified object ("%s").', + $object_name)); + } + $object = head($objects); + + $adapters = HeraldAdapter::getAllAdapters(); + + $can_select = array(); + $display_adapters = array(); + foreach ($adapters as $key => $adapter) { + if (!$adapter->isTestAdapterForObject($object)) { + continue; + } + + if (!$adapter->isAvailableToUser($viewer)) { + continue; + } + + $display_adapters[$key] = $adapter; + + if ($adapter->canCreateTestAdapterForObject($object)) { + $can_select[$key] = $adapter; + } + } + + + $content_type = $args->getArg('type'); + if (!strlen($content_type)) { + throw new PhutilArgumentUsageException( + pht( + 'Specify a content type to run rules for. For this object, valid '. + 'content types are: %s.', + implode(', ', array_keys($can_select)))); + } + + if (!isset($can_select[$content_type])) { + if (!isset($display_adapters[$content_type])) { + throw new PhutilArgumentUsageException( + pht( + 'Specify a content type to run rules for. The specified content '. + 'type ("%s") is not valid. For this object, valid content types '. + 'are: %s.', + $content_type, + implode(', ', array_keys($can_select)))); + } else { + throw new PhutilArgumentUsageException( + pht( + 'The specified content type ("%s") does not support dry runs. '. + 'Choose a testable content type. For this object, valid content '. + 'types are: %s.', + $content_type, + implode(', ', array_keys($can_select)))); + } + } + + $adapter = $can_select[$content_type]->newTestAdapter( + $viewer, + $object); + + $content_source = $this->newContentSource(); + + $adapter + ->setContentSource($content_source) + ->setIsNewObject(false) + ->setViewer($viewer); + + $rules = id(new HeraldRuleQuery()) + ->setViewer($viewer) + ->withContentTypes(array($adapter->getAdapterContentType())) + ->withDisabled(false) + ->needConditionsAndActions(true) + ->needAppliedToPHIDs(array($object->getPHID())) + ->needValidateAuthors(true) + ->execute(); + + $engine = id(new HeraldEngine()) + ->setDryRun(true); + + $effects = $engine->applyRules($rules, $adapter); + $engine->applyEffects($effects, $adapter, $rules); + + $xscript = $engine->getTranscript(); + + $uri = '/herald/transcript/'.$xscript->getID().'/'; + $uri = PhabricatorEnv::getProductionURI($uri); + + echo tsprintf( + "%s\n\n __%s__\n\n", + pht('Test run complete. Transcript:'), + $uri); + + return 0; + } + +} diff --git a/src/applications/herald/query/HeraldRuleQuery.php b/src/applications/herald/query/HeraldRuleQuery.php index 88df18db24..e6dba43c7a 100644 --- a/src/applications/herald/query/HeraldRuleQuery.php +++ b/src/applications/herald/query/HeraldRuleQuery.php @@ -1,284 +1,313 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withAuthorPHIDs(array $author_phids) { $this->authorPHIDs = $author_phids; return $this; } public function withRuleTypes(array $types) { $this->ruleTypes = $types; return $this; } public function withContentTypes(array $types) { $this->contentTypes = $types; return $this; } public function withDisabled($disabled) { $this->disabled = $disabled; return $this; } + public function withActive($active) { + $this->active = $active; + return $this; + } + public function withDatasourceQuery($query) { $this->datasourceQuery = $query; return $this; } public function withTriggerObjectPHIDs(array $phids) { $this->triggerObjectPHIDs = $phids; return $this; } public function needConditionsAndActions($need) { $this->needConditionsAndActions = $need; return $this; } public function needAppliedToPHIDs(array $phids) { $this->needAppliedToPHIDs = $phids; return $this; } public function needValidateAuthors($need) { $this->needValidateAuthors = $need; return $this; } + public function newResultObject() { + return new HeraldRule(); + } + protected function loadPage() { - $table = new HeraldRule(); - $conn_r = $table->establishConnection('r'); - - $data = queryfx_all( - $conn_r, - 'SELECT rule.* FROM %T rule %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); - - return $table->loadAllFromArray($data); + return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $rules) { $rule_ids = mpull($rules, 'getID'); // Filter out any rules that have invalid adapters, or have adapters the // viewer isn't permitted to see or use (for example, Differential rules // if the user can't use Differential or Differential is not installed). $types = HeraldAdapter::getEnabledAdapterMap($this->getViewer()); foreach ($rules as $key => $rule) { if (empty($types[$rule->getContentType()])) { $this->didRejectResult($rule); unset($rules[$key]); } } - if ($this->needValidateAuthors) { + if ($this->needValidateAuthors || ($this->active !== null)) { $this->validateRuleAuthors($rules); } + if ($this->active !== null) { + $need_active = (bool)$this->active; + foreach ($rules as $key => $rule) { + if ($rule->getIsDisabled()) { + $is_active = false; + } else if (!$rule->hasValidAuthor()) { + $is_active = false; + } else { + $is_active = true; + } + + if ($is_active != $need_active) { + unset($rules[$key]); + } + } + } + + if (!$rules) { + return array(); + } + if ($this->needConditionsAndActions) { $conditions = id(new HeraldCondition())->loadAllWhere( 'ruleID IN (%Ld)', $rule_ids); $conditions = mgroup($conditions, 'getRuleID'); $actions = id(new HeraldActionRecord())->loadAllWhere( 'ruleID IN (%Ld)', $rule_ids); $actions = mgroup($actions, 'getRuleID'); foreach ($rules as $rule) { $rule->attachActions(idx($actions, $rule->getID(), array())); $rule->attachConditions(idx($conditions, $rule->getID(), array())); } } if ($this->needAppliedToPHIDs) { $conn_r = id(new HeraldRule())->establishConnection('r'); $applied = queryfx_all( $conn_r, 'SELECT * FROM %T WHERE ruleID IN (%Ld) AND phid IN (%Ls)', HeraldRule::TABLE_RULE_APPLIED, $rule_ids, $this->needAppliedToPHIDs); $map = array(); foreach ($applied as $row) { $map[$row['ruleID']][$row['phid']] = true; } foreach ($rules as $rule) { foreach ($this->needAppliedToPHIDs as $phid) { $rule->setRuleApplied( $phid, isset($map[$rule->getID()][$phid])); } } } $object_phids = array(); foreach ($rules as $rule) { if ($rule->isObjectRule()) { $object_phids[] = $rule->getTriggerObjectPHID(); } } if ($object_phids) { $objects = id(new PhabricatorObjectQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($object_phids) ->execute(); $objects = mpull($objects, null, 'getPHID'); } else { $objects = array(); } foreach ($rules as $key => $rule) { if ($rule->isObjectRule()) { $object = idx($objects, $rule->getTriggerObjectPHID()); if (!$object) { unset($rules[$key]); continue; } $rule->attachTriggerObject($object); } } return $rules; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { - $where = array(); + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); - if ($this->ids) { + if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'rule.id IN (%Ld)', $this->ids); } - if ($this->phids) { + if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'rule.phid IN (%Ls)', $this->phids); } - if ($this->authorPHIDs) { + if ($this->authorPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'rule.authorPHID IN (%Ls)', $this->authorPHIDs); } - if ($this->ruleTypes) { + if ($this->ruleTypes !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'rule.ruleType IN (%Ls)', $this->ruleTypes); } - if ($this->contentTypes) { + if ($this->contentTypes !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'rule.contentType IN (%Ls)', $this->contentTypes); } if ($this->disabled !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'rule.isDisabled = %d', (int)$this->disabled); } - if ($this->datasourceQuery) { + if ($this->active !== null) { $where[] = qsprintf( - $conn_r, + $conn, + 'rule.isDisabled = %d', + (int)(!$this->active)); + } + + if ($this->datasourceQuery !== null) { + $where[] = qsprintf( + $conn, 'rule.name LIKE %>', $this->datasourceQuery); } - if ($this->triggerObjectPHIDs) { + if ($this->triggerObjectPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'rule.triggerObjectPHID IN (%Ls)', $this->triggerObjectPHIDs); } - $where[] = $this->buildPagingClause($conn_r); - - return $this->formatWhereClause($where); + return $where; } private function validateRuleAuthors(array $rules) { // "Global" and "Object" rules always have valid authors. foreach ($rules as $key => $rule) { if ($rule->isGlobalRule() || $rule->isObjectRule()) { $rule->attachValidAuthor(true); unset($rules[$key]); continue; } } if (!$rules) { return; } // For personal rules, the author needs to exist and not be disabled. $user_phids = mpull($rules, 'getAuthorPHID'); $users = id(new PhabricatorPeopleQuery()) ->setViewer($this->getViewer()) ->withPHIDs($user_phids) ->execute(); $users = mpull($users, null, 'getPHID'); foreach ($rules as $key => $rule) { $author_phid = $rule->getAuthorPHID(); if (empty($users[$author_phid])) { $rule->attachValidAuthor(false); continue; } if (!$users[$author_phid]->isUserActivated()) { $rule->attachValidAuthor(false); continue; } $rule->attachValidAuthor(true); $rule->attachAuthor($users[$author_phid]); } } public function getQueryApplicationClass() { return 'PhabricatorHeraldApplication'; } + protected function getPrimaryTableAlias() { + return 'rule'; + } + } diff --git a/src/applications/herald/query/HeraldRuleSearchEngine.php b/src/applications/herald/query/HeraldRuleSearchEngine.php index 3b8196c13a..47a6832731 100644 --- a/src/applications/herald/query/HeraldRuleSearchEngine.php +++ b/src/applications/herald/query/HeraldRuleSearchEngine.php @@ -1,234 +1,200 @@ needValidateAuthors(true); + } - $saved->setParameter( - 'authorPHIDs', - $this->readUsersFromRequest($request, 'authors')); + protected function buildCustomSearchFields() { + $viewer = $this->requireViewer(); - $saved->setParameter('contentType', $request->getStr('contentType')); - $saved->setParameter('ruleType', $request->getStr('ruleType')); - $saved->setParameter( - 'disabled', - $this->readBoolFromRequest($request, 'disabled')); + $rule_types = HeraldRuleTypeConfig::getRuleTypeMap(); + $content_types = HeraldAdapter::getEnabledAdapterMap($viewer); - return $saved; + return array( + id(new PhabricatorUsersSearchField()) + ->setLabel(pht('Authors')) + ->setKey('authorPHIDs') + ->setAliases(array('author', 'authors', 'authorPHID')) + ->setDescription( + pht('Search for rules with given authors.')), + id(new PhabricatorSearchCheckboxesField()) + ->setKey('ruleTypes') + ->setAliases(array('ruleType')) + ->setLabel(pht('Rule Type')) + ->setDescription( + pht('Search for rules of given types.')) + ->setOptions($rule_types), + id(new PhabricatorSearchCheckboxesField()) + ->setKey('contentTypes') + ->setLabel(pht('Content Type')) + ->setDescription( + pht('Search for rules affecting given types of content.')) + ->setOptions($content_types), + id(new PhabricatorSearchThreeStateField()) + ->setLabel(pht('Active Rules')) + ->setKey('active') + ->setOptions( + pht('(Show All)'), + pht('Show Only Active Rules'), + pht('Show Only Inactive Rules')), + id(new PhabricatorSearchThreeStateField()) + ->setLabel(pht('Disabled Rules')) + ->setKey('disabled') + ->setOptions( + pht('(Show All)'), + pht('Show Only Disabled Rules'), + pht('Show Only Enabled Rules')), + ); } - public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { - $query = id(new HeraldRuleQuery()); + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); - $author_phids = $saved->getParameter('authorPHIDs'); - if ($author_phids) { - $query->withAuthorPHIDs($author_phids); + if ($map['authorPHIDs']) { + $query->withAuthorPHIDs($map['authorPHIDs']); } - $content_type = $saved->getParameter('contentType'); - $content_type = idx($this->getContentTypeValues(), $content_type); - if ($content_type) { - $query->withContentTypes(array($content_type)); + if ($map['contentTypes']) { + $query->withContentTypes($map['contentTypes']); } - $rule_type = $saved->getParameter('ruleType'); - $rule_type = idx($this->getRuleTypeValues(), $rule_type); - if ($rule_type) { - $query->withRuleTypes(array($rule_type)); + if ($map['ruleTypes']) { + $query->withRuleTypes($map['ruleTypes']); } - $disabled = $saved->getParameter('disabled'); - if ($disabled !== null) { - $query->withDisabled($disabled); + if ($map['disabled'] !== null) { + $query->withDisabled($map['disabled']); } - return $query; - } + if ($map['active'] !== null) { + $query->withActive($map['active']); + } - public function buildSearchForm( - AphrontFormView $form, - PhabricatorSavedQuery $saved_query) { - - $author_phids = $saved_query->getParameter('authorPHIDs', array()); - $content_type = $saved_query->getParameter('contentType'); - $rule_type = $saved_query->getParameter('ruleType'); - - $form - ->appendControl( - id(new AphrontFormTokenizerControl()) - ->setDatasource(new PhabricatorPeopleDatasource()) - ->setName('authors') - ->setLabel(pht('Authors')) - ->setValue($author_phids)) - ->appendChild( - id(new AphrontFormSelectControl()) - ->setName('contentType') - ->setLabel(pht('Content Type')) - ->setValue($content_type) - ->setOptions($this->getContentTypeOptions())) - ->appendChild( - id(new AphrontFormSelectControl()) - ->setName('ruleType') - ->setLabel(pht('Rule Type')) - ->setValue($rule_type) - ->setOptions($this->getRuleTypeOptions())) - ->appendChild( - id(new AphrontFormSelectControl()) - ->setName('disabled') - ->setLabel(pht('Rule Status')) - ->setValue($this->getBoolFromQuery($saved_query, 'disabled')) - ->setOptions( - array( - '' => pht('Show Enabled and Disabled Rules'), - 'false' => pht('Show Only Enabled Rules'), - 'true' => pht('Show Only Disabled Rules'), - ))); + return $query; } protected function getURI($path) { return '/herald/'.$path; } protected function getBuiltinQueryNames() { $names = array(); if ($this->requireViewer()->isLoggedIn()) { $names['authored'] = pht('Authored'); } $names['active'] = pht('Active'); $names['all'] = pht('All'); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); $viewer_phid = $this->requireViewer()->getPHID(); switch ($query_key) { case 'all': return $query; case 'active': - return $query->setParameter('disabled', false); + return $query + ->setParameter('active', true); case 'authored': return $query ->setParameter('authorPHIDs', array($viewer_phid)) ->setParameter('disabled', false); } return parent::buildSavedQueryFromBuiltin($query_key); } - private function getContentTypeOptions() { - return array( - '' => pht('(All Content Types)'), - ) + HeraldAdapter::getEnabledAdapterMap($this->requireViewer()); - } - - private function getContentTypeValues() { - return array_fuse( - array_keys( - HeraldAdapter::getEnabledAdapterMap($this->requireViewer()))); - } - - private function getRuleTypeOptions() { - return array( - '' => pht('(All Rule Types)'), - ) + HeraldRuleTypeConfig::getRuleTypeMap(); - } - - private function getRuleTypeValues() { - return array_fuse(array_keys(HeraldRuleTypeConfig::getRuleTypeMap())); - } - - protected function getRequiredHandlePHIDsForResultList( - array $rules, - PhabricatorSavedQuery $query) { - - return mpull($rules, 'getAuthorPHID'); - } - protected function renderResultList( array $rules, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($rules, 'HeraldRule'); $viewer = $this->requireViewer(); + $handles = $viewer->loadHandles(mpull($rules, 'getAuthorPHID')); $content_type_map = HeraldAdapter::getEnabledAdapterMap($viewer); $list = id(new PHUIObjectItemListView()) ->setUser($viewer); foreach ($rules as $rule) { $monogram = $rule->getMonogram(); $item = id(new PHUIObjectItemView()) ->setObjectName($monogram) ->setHeader($rule->getName()) ->setHref("/{$monogram}"); if ($rule->isPersonalRule()) { $item->addIcon('fa-user', pht('Personal Rule')); $item->addByline( pht( 'Authored by %s', $handles[$rule->getAuthorPHID()]->renderLink())); } else if ($rule->isObjectRule()) { $item->addIcon('fa-briefcase', pht('Object Rule')); } else { $item->addIcon('fa-globe', pht('Global Rule')); } if ($rule->getIsDisabled()) { $item->setDisabled(true); $item->addIcon('fa-lock grey', pht('Disabled')); + } else if (!$rule->hasValidAuthor()) { + $item->setDisabled(true); + $item->addIcon('fa-user grey', pht('Author Not Active')); } $content_type_name = idx($content_type_map, $rule->getContentType()); $item->addAttribute(pht('Affects: %s', $content_type_name)); $list->addItem($item); } $result = new PhabricatorApplicationSearchResultView(); $result->setObjectList($list); $result->setNoDataString(pht('No rules found.')); return $result; } protected function getNewUserBody() { $create_button = id(new PHUIButtonView()) ->setTag('a') ->setText(pht('Create Herald Rule')) ->setHref('/herald/create/') ->setColor(PHUIButtonView::GREEN); $icon = $this->getApplication()->getIcon(); $app_name = $this->getApplication()->getName(); $view = id(new PHUIBigInfoView()) ->setIcon($icon) ->setTitle(pht('Welcome to %s', $app_name)) ->setDescription( pht('A flexible rules engine that can notify and act on '. 'other actions such as tasks, diffs, and commits.')) ->addAction($create_button); return $view; } } diff --git a/src/applications/herald/query/HeraldTranscriptQuery.php b/src/applications/herald/query/HeraldTranscriptQuery.php index 7d0fdfc59e..71789e07c5 100644 --- a/src/applications/herald/query/HeraldTranscriptQuery.php +++ b/src/applications/herald/query/HeraldTranscriptQuery.php @@ -1,127 +1,126 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withObjectPHIDs(array $phids) { $this->objectPHIDs = $phids; return $this; } public function needPartialRecords($need_partial) { $this->needPartialRecords = $need_partial; return $this; } protected function loadPage() { $transcript = new HeraldTranscript(); - $conn_r = $transcript->establishConnection('r'); + $conn = $transcript->establishConnection('r'); // NOTE: Transcripts include a potentially enormous amount of serialized // data, so we're loading only some of the fields here if the caller asked // for partial records. if ($this->needPartialRecords) { - $fields = implode( - ', ', - array( - 'id', - 'phid', - 'objectPHID', - 'time', - 'duration', - 'dryRun', - 'host', - )); + $fields = array( + 'id', + 'phid', + 'objectPHID', + 'time', + 'duration', + 'dryRun', + 'host', + ); + $fields = qsprintf($conn, '%LC', $fields); } else { - $fields = '*'; + $fields = qsprintf($conn, '*'); } $rows = queryfx_all( - $conn_r, + $conn, 'SELECT %Q FROM %T t %Q %Q %Q', $fields, $transcript->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); + $this->buildWhereClause($conn), + $this->buildOrderClause($conn), + $this->buildLimitClause($conn)); $transcripts = $transcript->loadAllFromArray($rows); if ($this->needPartialRecords) { // Make sure nothing tries to write these; they aren't complete. foreach ($transcripts as $transcript) { $transcript->makeEphemeral(); } } return $transcripts; } protected function willFilterPage(array $transcripts) { $phids = mpull($transcripts, 'getObjectPHID'); $objects = id(new PhabricatorObjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs($phids) ->execute(); foreach ($transcripts as $key => $transcript) { if (empty($objects[$transcript->getObjectPHID()])) { $this->didRejectResult($transcript); unset($transcripts[$key]); } } return $transcripts; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } if ($this->objectPHIDs) { $where[] = qsprintf( - $conn_r, + $conn, 'objectPHID in (%Ls)', $this->objectPHIDs); } - $where[] = $this->buildPagingClause($conn_r); + $where[] = $this->buildPagingClause($conn); - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } public function getQueryApplicationClass() { return 'PhabricatorHeraldApplication'; } } diff --git a/src/applications/legalpad/controller/LegalpadDocumentSignController.php b/src/applications/legalpad/controller/LegalpadDocumentSignController.php index 1748dba452..8ef35e3493 100644 --- a/src/applications/legalpad/controller/LegalpadDocumentSignController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentSignController.php @@ -1,701 +1,702 @@ getUser(); $document = id(new LegalpadDocumentQuery()) ->setViewer($viewer) ->withIDs(array($request->getURIData('id'))) ->needDocumentBodies(true) ->executeOne(); if (!$document) { return new Aphront404Response(); } $information = $this->readSignerInformation( $document, $request); if ($information instanceof AphrontResponse) { return $information; } list($signer_phid, $signature_data) = $information; $signature = null; $type_individual = LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL; $is_individual = ($document->getSignatureType() == $type_individual); switch ($document->getSignatureType()) { case LegalpadDocument::SIGNATURE_TYPE_NONE: // nothing to sign means this should be true $has_signed = true; // this is a status UI element $signed_status = null; break; case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL: if ($signer_phid) { // TODO: This is odd and should probably be adjusted after // grey/external accounts work better, but use the omnipotent // viewer to check for a signature so we can pick up // anonymous/grey signatures. $signature = id(new LegalpadDocumentSignatureQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withDocumentPHIDs(array($document->getPHID())) ->withSignerPHIDs(array($signer_phid)) ->executeOne(); if ($signature && !$viewer->isLoggedIn()) { return $this->newDialog() ->setTitle(pht('Already Signed')) ->appendParagraph(pht('You have already signed this document!')) ->addCancelButton('/'.$document->getMonogram(), pht('Okay')); } } $signed_status = null; if (!$signature) { $has_signed = false; $signature = id(new LegalpadDocumentSignature()) ->setSignerPHID($signer_phid) ->setDocumentPHID($document->getPHID()) ->setDocumentVersion($document->getVersions()); // If the user is logged in, show a notice that they haven't signed. // If they aren't logged in, we can't be as sure, so don't show // anything. if ($viewer->isLoggedIn()) { $signed_status = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setErrors( array( pht('You have not signed this document yet.'), )); } } else { $has_signed = true; $signature_data = $signature->getSignatureData(); // In this case, we know they've signed. $signed_at = $signature->getDateCreated(); if ($signature->getIsExemption()) { $exemption_phid = $signature->getExemptionPHID(); $handles = $this->loadViewerHandles(array($exemption_phid)); $exemption_handle = $handles[$exemption_phid]; $signed_text = pht( 'You do not need to sign this document. '. '%s added a signature exemption for you on %s.', $exemption_handle->renderLink(), phabricator_datetime($signed_at, $viewer)); } else { $signed_text = pht( 'You signed this document on %s.', phabricator_datetime($signed_at, $viewer)); } $signed_status = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->setErrors(array($signed_text)); } $field_errors = array( 'name' => true, 'email' => true, 'agree' => true, ); $signature->setSignatureData($signature_data); break; case LegalpadDocument::SIGNATURE_TYPE_CORPORATION: $signature = id(new LegalpadDocumentSignature()) ->setDocumentPHID($document->getPHID()) ->setDocumentVersion($document->getVersions()); if ($viewer->isLoggedIn()) { $has_signed = false; $signed_status = null; } else { // This just hides the form. $has_signed = true; $login_text = pht( 'This document requires a corporate signatory. You must log in to '. 'accept this document on behalf of a company you represent.'); $signed_status = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setErrors(array($login_text)); } $field_errors = array( 'name' => true, 'address' => true, 'contact.name' => true, 'email' => true, ); $signature->setSignatureData($signature_data); break; } $errors = array(); + $hisec_token = null; if ($request->isFormOrHisecPost() && !$has_signed) { // Require two-factor auth to sign legal documents. if ($viewer->isLoggedIn()) { - $engine = new PhabricatorAuthSessionEngine(); - $engine->requireHighSecuritySession( - $viewer, - $request, - '/'.$document->getMonogram()); + $hisec_token = id(new PhabricatorAuthSessionEngine()) + ->requireHighSecurityToken( + $viewer, + $request, + $document->getURI()); } list($form_data, $errors, $field_errors) = $this->readSignatureForm( $document, $request); $signature_data = $form_data + $signature_data; $signature->setSignatureData($signature_data); $signature->setSignatureType($document->getSignatureType()); $signature->setSignerName((string)idx($signature_data, 'name')); $signature->setSignerEmail((string)idx($signature_data, 'email')); $agree = $request->getExists('agree'); if (!$agree) { $errors[] = pht( 'You must check "I agree to the terms laid forth above."'); $field_errors['agree'] = pht('Required'); } if ($viewer->isLoggedIn() && $is_individual) { $verified = LegalpadDocumentSignature::VERIFIED; } else { $verified = LegalpadDocumentSignature::UNVERIFIED; } $signature->setVerified($verified); if (!$errors) { $signature->save(); // If the viewer is logged in, signing for themselves, send them to // the document page, which will show that they have signed the // document. Unless of course they were required to sign the // document to use Phabricator; in that case try really hard to // re-direct them to where they wanted to go. // // Otherwise, send them to a completion page. if ($viewer->isLoggedIn() && $is_individual) { $next_uri = '/'.$document->getMonogram(); if ($document->getRequireSignature()) { $request_uri = $request->getRequestURI(); $next_uri = (string)$request_uri; } } else { $this->sendVerifySignatureEmail( $document, $signature); $next_uri = $this->getApplicationURI('done/'); } return id(new AphrontRedirectResponse())->setURI($next_uri); } } $document_body = $document->getDocumentBody(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer); $engine->addObject( $document_body, LegalpadDocumentBody::MARKUP_FIELD_TEXT); $engine->process(); $document_markup = $engine->getOutput( $document_body, LegalpadDocumentBody::MARKUP_FIELD_TEXT); $title = $document_body->getTitle(); $manage_uri = $this->getApplicationURI('view/'.$document->getID().'/'); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $document, PhabricatorPolicyCapability::CAN_EDIT); // Use the last content update as the modified date. We don't want to // show that a document like a TOS was "updated" by an incidental change // to a field like the preamble or privacy settings which does not actually // affect the content of the agreement. $content_updated = $document_body->getDateCreated(); // NOTE: We're avoiding `setPolicyObject()` here so we don't pick up // extra UI elements that are unnecessary and clutter the signature page. // These details are available on the "Manage" page. $header = id(new PHUIHeaderView()) ->setHeader($title) ->setUser($viewer) ->setEpoch($content_updated) ->addActionLink( id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-pencil') ->setText(pht('Manage')) ->setHref($manage_uri) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $preamble_box = null; if (strlen($document->getPreamble())) { $preamble_text = new PHUIRemarkupView($viewer, $document->getPreamble()); // NOTE: We're avoiding `setObject()` here so we don't pick up extra UI // elements like "Subscribers". This information is available on the // "Manage" page, but just clutters up the "Signature" page. $preamble = id(new PHUIPropertyListView()) ->setUser($viewer) ->addSectionHeader(pht('Preamble')) ->addTextContent($preamble_text); $preamble_box = new PHUIPropertyGroupView(); $preamble_box->addPropertyList($preamble); } $content = id(new PHUIDocumentView()) ->addClass('legalpad') ->setHeader($header) ->appendChild( array( $signed_status, $preamble_box, $document_markup, )); $signature_box = null; if (!$has_signed) { $error_view = null; if ($errors) { $error_view = id(new PHUIInfoView()) ->setErrors($errors); } $signature_form = $this->buildSignatureForm( $document, $signature, $field_errors); switch ($document->getSignatureType()) { default: break; case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL: case LegalpadDocument::SIGNATURE_TYPE_CORPORATION: $box = id(new PHUIObjectBoxView()) ->addClass('document-sign-box') ->setHeaderText(pht('Agree and Sign Document')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setForm($signature_form); if ($error_view) { $box->setInfoView($error_view); } $signature_box = phutil_tag_div( 'phui-document-view-pro-box plt', $box); break; } } $crumbs = $this->buildApplicationCrumbs(); $crumbs->setBorder(true); $crumbs->addTextCrumb($document->getMonogram()); $box = id(new PHUITwoColumnView()) ->setFooter($signature_box); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->setPageObjectPHIDs(array($document->getPHID())) ->appendChild(array( $content, $box, )); } private function readSignerInformation( LegalpadDocument $document, AphrontRequest $request) { $viewer = $request->getUser(); $signer_phid = null; $signature_data = array(); switch ($document->getSignatureType()) { case LegalpadDocument::SIGNATURE_TYPE_NONE: break; case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL: if ($viewer->isLoggedIn()) { $signer_phid = $viewer->getPHID(); $signature_data = array( 'name' => $viewer->getRealName(), 'email' => $viewer->loadPrimaryEmailAddress(), ); } else if ($request->isFormPost()) { $email = new PhutilEmailAddress($request->getStr('email')); if (strlen($email->getDomainName())) { $email_obj = id(new PhabricatorUserEmail()) ->loadOneWhere('address = %s', $email->getAddress()); if ($email_obj) { return $this->signInResponse(); } $external_account = id(new PhabricatorExternalAccountQuery()) ->setViewer($viewer) ->withAccountTypes(array('email')) ->withAccountDomains(array($email->getDomainName())) ->withAccountIDs(array($email->getAddress())) ->loadOneOrCreate(); if ($external_account->getUserPHID()) { return $this->signInResponse(); } $signer_phid = $external_account->getPHID(); } } break; case LegalpadDocument::SIGNATURE_TYPE_CORPORATION: $signer_phid = $viewer->getPHID(); if ($signer_phid) { $signature_data = array( 'contact.name' => $viewer->getRealName(), 'email' => $viewer->loadPrimaryEmailAddress(), 'actorPHID' => $viewer->getPHID(), ); } break; } return array($signer_phid, $signature_data); } private function buildSignatureForm( LegalpadDocument $document, LegalpadDocumentSignature $signature, array $errors) { $viewer = $this->getRequest()->getUser(); $data = $signature->getSignatureData(); $form = id(new AphrontFormView()) ->setUser($viewer); $signature_type = $document->getSignatureType(); switch ($signature_type) { case LegalpadDocument::SIGNATURE_TYPE_NONE: // bail out of here quick return; case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL: $this->buildIndividualSignatureForm( $form, $document, $signature, $errors); break; case LegalpadDocument::SIGNATURE_TYPE_CORPORATION: $this->buildCorporateSignatureForm( $form, $document, $signature, $errors); break; default: throw new Exception( pht( 'This document has an unknown signature type ("%s").', $signature_type)); } $form ->appendChild( id(new AphrontFormCheckboxControl()) ->setError(idx($errors, 'agree', null)) ->addCheckbox( 'agree', 'agree', pht('I agree to the terms laid forth above.'), false)); if ($document->getRequireSignature()) { $cancel_uri = '/logout/'; $cancel_text = pht('Log Out'); } else { $cancel_uri = $this->getApplicationURI(); $cancel_text = pht('Cancel'); } $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Sign Document')) ->addCancelButton($cancel_uri, $cancel_text)); return $form; } private function buildIndividualSignatureForm( AphrontFormView $form, LegalpadDocument $document, LegalpadDocumentSignature $signature, array $errors) { $data = $signature->getSignatureData(); $form ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setValue(idx($data, 'name', '')) ->setName('name') ->setError(idx($errors, 'name', null))); $viewer = $this->getRequest()->getUser(); if (!$viewer->isLoggedIn()) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Email')) ->setValue(idx($data, 'email', '')) ->setName('email') ->setError(idx($errors, 'email', null))); } return $form; } private function buildCorporateSignatureForm( AphrontFormView $form, LegalpadDocument $document, LegalpadDocumentSignature $signature, array $errors) { $data = $signature->getSignatureData(); $form ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Company Name')) ->setValue(idx($data, 'name', '')) ->setName('name') ->setError(idx($errors, 'name', null))) ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Company Address')) ->setValue(idx($data, 'address', '')) ->setName('address') ->setError(idx($errors, 'address', null))) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Contact Name')) ->setValue(idx($data, 'contact.name', '')) ->setName('contact.name') ->setError(idx($errors, 'contact.name', null))) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Contact Email')) ->setValue(idx($data, 'email', '')) ->setName('email') ->setError(idx($errors, 'email', null))); return $form; } private function readSignatureForm( LegalpadDocument $document, AphrontRequest $request) { $signature_type = $document->getSignatureType(); switch ($signature_type) { case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL: $result = $this->readIndividualSignatureForm( $document, $request); break; case LegalpadDocument::SIGNATURE_TYPE_CORPORATION: $result = $this->readCorporateSignatureForm( $document, $request); break; default: throw new Exception( pht( 'This document has an unknown signature type ("%s").', $signature_type)); } return $result; } private function readIndividualSignatureForm( LegalpadDocument $document, AphrontRequest $request) { $signature_data = array(); $errors = array(); $field_errors = array(); $name = $request->getStr('name'); if (!strlen($name)) { $field_errors['name'] = pht('Required'); $errors[] = pht('Name field is required.'); } else { $field_errors['name'] = null; } $signature_data['name'] = $name; $viewer = $request->getUser(); if ($viewer->isLoggedIn()) { $email = $viewer->loadPrimaryEmailAddress(); } else { $email = $request->getStr('email'); $addr_obj = null; if (!strlen($email)) { $field_errors['email'] = pht('Required'); $errors[] = pht('Email field is required.'); } else { $addr_obj = new PhutilEmailAddress($email); $domain = $addr_obj->getDomainName(); if (!$domain) { $field_errors['email'] = pht('Invalid'); $errors[] = pht('A valid email is required.'); } else { $field_errors['email'] = null; } } } $signature_data['email'] = $email; return array($signature_data, $errors, $field_errors); } private function readCorporateSignatureForm( LegalpadDocument $document, AphrontRequest $request) { $viewer = $request->getUser(); if (!$viewer->isLoggedIn()) { throw new Exception( pht( 'You can not sign a document on behalf of a corporation unless '. 'you are logged in.')); } $signature_data = array(); $errors = array(); $field_errors = array(); $name = $request->getStr('name'); if (!strlen($name)) { $field_errors['name'] = pht('Required'); $errors[] = pht('Company name is required.'); } else { $field_errors['name'] = null; } $signature_data['name'] = $name; $address = $request->getStr('address'); if (!strlen($address)) { $field_errors['address'] = pht('Required'); $errors[] = pht('Company address is required.'); } else { $field_errors['address'] = null; } $signature_data['address'] = $address; $contact_name = $request->getStr('contact.name'); if (!strlen($contact_name)) { $field_errors['contact.name'] = pht('Required'); $errors[] = pht('Contact name is required.'); } else { $field_errors['contact.name'] = null; } $signature_data['contact.name'] = $contact_name; $email = $request->getStr('email'); $addr_obj = null; if (!strlen($email)) { $field_errors['email'] = pht('Required'); $errors[] = pht('Contact email is required.'); } else { $addr_obj = new PhutilEmailAddress($email); $domain = $addr_obj->getDomainName(); if (!$domain) { $field_errors['email'] = pht('Invalid'); $errors[] = pht('A valid email is required.'); } else { $field_errors['email'] = null; } } $signature_data['email'] = $email; return array($signature_data, $errors, $field_errors); } private function sendVerifySignatureEmail( LegalpadDocument $doc, LegalpadDocumentSignature $signature) { $signature_data = $signature->getSignatureData(); $email = new PhutilEmailAddress($signature_data['email']); $doc_name = $doc->getTitle(); $doc_link = PhabricatorEnv::getProductionURI('/'.$doc->getMonogram()); $path = $this->getApplicationURI(sprintf( '/verify/%s/', $signature->getSecretKey())); $link = PhabricatorEnv::getProductionURI($path); $name = idx($signature_data, 'name'); $body = pht( "%s:\n\n". "This email address was used to sign a Legalpad document ". "in Phabricator:\n\n". " %s\n\n". "Please verify you own this email address and accept the ". "agreement by clicking this link:\n\n". " %s\n\n". "Your signature is not valid until you complete this ". "verification step.\n\nYou can review the document here:\n\n". " %s\n", $name, $doc_name, $link, $doc_link); id(new PhabricatorMetaMTAMail()) ->addRawTos(array($email->getAddress())) ->setSubject(pht('[Legalpad] Signature Verification')) ->setForceDelivery(true) ->setBody($body) ->setRelatedPHID($signature->getDocumentPHID()) ->saveAndSend(); } private function signInResponse() { return id(new Aphront403Response()) ->setForbiddenText( pht( 'The email address specified is associated with an account. '. 'Please login to that account and sign this document again.')); } } diff --git a/src/applications/legalpad/query/LegalpadDocumentSignatureQuery.php b/src/applications/legalpad/query/LegalpadDocumentSignatureQuery.php index 452ea6759b..c310dd3d64 100644 --- a/src/applications/legalpad/query/LegalpadDocumentSignatureQuery.php +++ b/src/applications/legalpad/query/LegalpadDocumentSignatureQuery.php @@ -1,150 +1,150 @@ ids = $ids; return $this; } public function withDocumentPHIDs(array $phids) { $this->documentPHIDs = $phids; return $this; } public function withSignerPHIDs(array $phids) { $this->signerPHIDs = $phids; return $this; } public function withDocumentVersions(array $versions) { $this->documentVersions = $versions; return $this; } public function withSecretKeys(array $keys) { $this->secretKeys = $keys; return $this; } public function withNameContains($text) { $this->nameContains = $text; return $this; } public function withEmailContains($text) { $this->emailContains = $text; return $this; } protected function loadPage() { $table = new LegalpadDocumentSignature(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); $signatures = $table->loadAllFromArray($data); return $signatures; } protected function willFilterPage(array $signatures) { $document_phids = mpull($signatures, 'getDocumentPHID'); $documents = id(new LegalpadDocumentQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($document_phids) ->execute(); $documents = mpull($documents, null, 'getPHID'); foreach ($signatures as $key => $signature) { $document_phid = $signature->getDocumentPHID(); $document = idx($documents, $document_phid); if ($document) { $signature->attachDocument($document); } else { unset($signatures[$key]); } } return $signatures; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); - $where[] = $this->buildPagingClause($conn_r); + $where[] = $this->buildPagingClause($conn); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->documentPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'documentPHID IN (%Ls)', $this->documentPHIDs); } if ($this->signerPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'signerPHID IN (%Ls)', $this->signerPHIDs); } if ($this->documentVersions !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'documentVersion IN (%Ld)', $this->documentVersions); } if ($this->secretKeys !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'secretKey IN (%Ls)', $this->secretKeys); } if ($this->nameContains !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'signerName LIKE %~', $this->nameContains); } if ($this->emailContains !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'signerEmail LIKE %~', $this->emailContains); } - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } public function getQueryApplicationClass() { return 'PhabricatorLegalpadApplication'; } } diff --git a/src/applications/legalpad/storage/LegalpadDocument.php b/src/applications/legalpad/storage/LegalpadDocument.php index 55d1ef9fa9..004802de39 100644 --- a/src/applications/legalpad/storage/LegalpadDocument.php +++ b/src/applications/legalpad/storage/LegalpadDocument.php @@ -1,254 +1,254 @@ setViewer($actor) ->withClasses(array('PhabricatorLegalpadApplication')) ->executeOne(); $view_policy = $app->getPolicy(LegalpadDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy(LegalpadDefaultEditCapability::CAPABILITY); return id(new LegalpadDocument()) ->setVersions(0) ->setCreatorPHID($actor->getPHID()) ->setContributorCount(0) ->setRecentContributorPHIDs(array()) ->attachSignatures(array()) ->setSignatureType(self::SIGNATURE_TYPE_INDIVIDUAL) ->setPreamble('') ->setRequireSignature(0) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'recentContributorPHIDs' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255', 'contributorCount' => 'uint32', 'versions' => 'uint32', 'mailKey' => 'bytes20', 'signatureType' => 'text4', 'preamble' => 'text', 'requireSignature' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_creator' => array( 'columns' => array('creatorPHID', 'dateModified'), ), 'key_required' => array( 'columns' => array('requireSignature', 'dateModified'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorLegalpadDocumentPHIDType::TYPECONST); } public function getDocumentBody() { return $this->assertAttached($this->documentBody); } public function attachDocumentBody(LegalpadDocumentBody $body) { $this->documentBody = $body; return $this; } public function getContributors() { return $this->assertAttached($this->contributors); } public function attachContributors(array $contributors) { $this->contributors = $contributors; return $this; } public function getSignatures() { return $this->assertAttached($this->signatures); } public function attachSignatures(array $signatures) { $this->signatures = $signatures; return $this; } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function getMonogram() { return 'L'.$this->getID(); } - public function getViewURI() { + public function getURI() { return '/'.$this->getMonogram(); } public function getUserSignature($phid) { return $this->assertAttachedKey($this->userSignatures, $phid); } public function attachUserSignature( $user_phid, LegalpadDocumentSignature $signature = null) { $this->userSignatures[$user_phid] = $signature; return $this; } public static function getSignatureTypeMap() { return array( self::SIGNATURE_TYPE_INDIVIDUAL => pht('Individuals'), self::SIGNATURE_TYPE_CORPORATION => pht('Corporations'), self::SIGNATURE_TYPE_NONE => pht('No One'), ); } public function getSignatureTypeName() { $type = $this->getSignatureType(); return idx(self::getSignatureTypeMap(), $type, $type); } public function getSignatureTypeIcon() { $type = $this->getSignatureType(); $map = array( self::SIGNATURE_TYPE_NONE => '', self::SIGNATURE_TYPE_INDIVIDUAL => 'fa-user grey', self::SIGNATURE_TYPE_CORPORATION => 'fa-building-o grey', ); return idx($map, $type, 'fa-user grey'); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($this->creatorPHID == $phid); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $policy = $this->viewPolicy; break; case PhabricatorPolicyCapability::CAN_EDIT: $policy = $this->editPolicy; break; default: $policy = PhabricatorPolicies::POLICY_NOONE; break; } return $policy; } public function hasAutomaticCapability($capability, PhabricatorUser $user) { return ($user->getPHID() == $this->getCreatorPHID()); } public function describeAutomaticCapability($capability) { return pht('The author of a document can always view and edit it.'); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new LegalpadDocumentEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new LegalpadTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $bodies = id(new LegalpadDocumentBody())->loadAllWhere( 'documentPHID = %s', $this->getPHID()); foreach ($bodies as $body) { $body->delete(); } $signatures = id(new LegalpadDocumentSignature())->loadAllWhere( 'documentPHID = %s', $this->getPHID()); foreach ($signatures as $signature) { $signature->delete(); } $this->saveTransaction(); } } diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php index c270104034..0b0b4d6758 100644 --- a/src/applications/maniphest/editor/ManiphestEditEngine.php +++ b/src/applications/maniphest/editor/ManiphestEditEngine.php @@ -1,529 +1,527 @@ addInt(100); } protected function newEditableObject() { return ManiphestTask::initializeNewTask($this->getViewer()); } protected function newObjectQuery() { return id(new ManiphestTaskQuery()); } protected function getObjectCreateTitleText($object) { return pht('Create New Task'); } protected function getObjectEditTitleText($object) { return pht('Edit Task: %s', $object->getTitle()); } protected function getObjectEditShortText($object) { return $object->getMonogram(); } protected function getObjectCreateShortText() { return pht('Create Task'); } protected function getObjectName() { return pht('Task'); } protected function getEditorURI() { return $this->getApplication()->getApplicationURI('task/edit/'); } protected function getCommentViewHeaderText($object) { return pht('Weigh In'); } protected function getCommentViewButtonText($object) { return pht('Set Sail for Adventure'); } protected function getObjectViewURI($object) { return '/'.$object->getMonogram(); } protected function buildCustomEditFields($object) { $status_map = $this->getTaskStatusMap($object); $priority_map = $this->getTaskPriorityMap($object); $alias_map = ManiphestTaskPriority::getTaskPriorityAliasMap(); if ($object->isClosed()) { $default_status = ManiphestTaskStatus::getDefaultStatus(); } else { $default_status = ManiphestTaskStatus::getDefaultClosedStatus(); } if ($object->getOwnerPHID()) { $owner_value = array($object->getOwnerPHID()); } else { $owner_value = array($this->getViewer()->getPHID()); } $column_documentation = pht(<<getColumnMap($object); $fields = array( id(new PhabricatorHandlesEditField()) ->setKey('parent') ->setLabel(pht('Parent Task')) ->setDescription(pht('Task to make this a subtask of.')) ->setConduitDescription(pht('Create as a subtask of another task.')) ->setConduitTypeDescription(pht('PHID of the parent task.')) ->setAliases(array('parentPHID')) ->setTransactionType(ManiphestTaskParentTransaction::TRANSACTIONTYPE) ->setHandleParameterType(new ManiphestTaskListHTTPParameterType()) ->setSingleValue(null) ->setIsReorderable(false) ->setIsDefaultable(false) ->setIsLockable(false), id(new PhabricatorColumnsEditField()) ->setKey('column') ->setLabel(pht('Column')) ->setDescription(pht('Create a task in a workboard column.')) ->setConduitDescription( pht('Move a task to one or more workboard columns.')) ->setConduitTypeDescription( pht('List of columns to move the task to.')) ->setConduitDocumentation($column_documentation) ->setAliases(array('columnPHID', 'columns', 'columnPHIDs')) ->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS) - ->setIsReorderable(false) - ->setIsDefaultable(false) - ->setIsLockable(false) + ->setIsFormField(false) ->setCommentActionLabel(pht('Move on Workboard')) ->setCommentActionOrder(2000) ->setColumnMap($column_map), id(new PhabricatorTextEditField()) ->setKey('title') ->setLabel(pht('Title')) ->setBulkEditLabel(pht('Set title to')) ->setDescription(pht('Name of the task.')) ->setConduitDescription(pht('Rename the task.')) ->setConduitTypeDescription(pht('New task name.')) ->setTransactionType(ManiphestTaskTitleTransaction::TRANSACTIONTYPE) ->setIsRequired(true) ->setValue($object->getTitle()), id(new PhabricatorUsersEditField()) ->setKey('owner') ->setAliases(array('ownerPHID', 'assign', 'assigned')) ->setLabel(pht('Assigned To')) ->setBulkEditLabel(pht('Assign to')) ->setDescription(pht('User who is responsible for the task.')) ->setConduitDescription(pht('Reassign the task.')) ->setConduitTypeDescription( pht('New task owner, or `null` to unassign.')) ->setTransactionType(ManiphestTaskOwnerTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setIsNullable(true) ->setSingleValue($object->getOwnerPHID()) ->setCommentActionLabel(pht('Assign / Claim')) ->setCommentActionValue($owner_value), id(new PhabricatorSelectEditField()) ->setKey('status') ->setLabel(pht('Status')) ->setBulkEditLabel(pht('Set status to')) ->setDescription(pht('Status of the task.')) ->setConduitDescription(pht('Change the task status.')) ->setConduitTypeDescription(pht('New task status constant.')) ->setTransactionType(ManiphestTaskStatusTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setValue($object->getStatus()) ->setOptions($status_map) ->setCommentActionLabel(pht('Change Status')) ->setCommentActionValue($default_status), id(new PhabricatorSelectEditField()) ->setKey('priority') ->setLabel(pht('Priority')) ->setBulkEditLabel(pht('Set priority to')) ->setDescription(pht('Priority of the task.')) ->setConduitDescription(pht('Change the priority of the task.')) ->setConduitTypeDescription(pht('New task priority constant.')) ->setTransactionType(ManiphestTaskPriorityTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setValue($object->getPriorityKeyword()) ->setOptions($priority_map) ->setOptionAliases($alias_map) ->setCommentActionLabel(pht('Change Priority')), ); if (ManiphestTaskPoints::getIsEnabled()) { $points_label = ManiphestTaskPoints::getPointsLabel(); $action_label = ManiphestTaskPoints::getPointsActionLabel(); $fields[] = id(new PhabricatorPointsEditField()) ->setKey('points') ->setLabel($points_label) ->setBulkEditLabel($action_label) ->setDescription(pht('Point value of the task.')) ->setConduitDescription(pht('Change the task point value.')) ->setConduitTypeDescription(pht('New task point value.')) ->setTransactionType(ManiphestTaskPointsTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setValue($object->getPoints()) ->setCommentActionLabel($action_label); } $fields[] = id(new PhabricatorRemarkupEditField()) ->setKey('description') ->setLabel(pht('Description')) ->setBulkEditLabel(pht('Set description to')) ->setDescription(pht('Task description.')) ->setConduitDescription(pht('Update the task description.')) ->setConduitTypeDescription(pht('New task description.')) ->setTransactionType(ManiphestTaskDescriptionTransaction::TRANSACTIONTYPE) ->setValue($object->getDescription()) ->setPreviewPanel( id(new PHUIRemarkupPreviewPanel()) ->setHeader(pht('Description Preview'))); $parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST; $subtask_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST; $src_phid = $object->getPHID(); if ($src_phid) { $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($src_phid)) ->withEdgeTypes( array( $parent_type, $subtask_type, )); $edge_query->execute(); $parent_phids = $edge_query->getDestinationPHIDs( array($src_phid), array($parent_type)); $subtask_phids = $edge_query->getDestinationPHIDs( array($src_phid), array($subtask_type)); } else { $parent_phids = array(); $subtask_phids = array(); } $fields[] = id(new PhabricatorHandlesEditField()) ->setKey('parents') ->setLabel(pht('Parents')) ->setDescription(pht('Parent tasks.')) ->setConduitDescription(pht('Change the parents of this task.')) ->setConduitTypeDescription(pht('List of parent task PHIDs.')) ->setUseEdgeTransactions(true) - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $parent_type) ->setValue($parent_phids); $fields[] = id(new PhabricatorHandlesEditField()) ->setKey('subtasks') ->setLabel(pht('Subtasks')) ->setDescription(pht('Subtasks.')) ->setConduitDescription(pht('Change the subtasks of this task.')) ->setConduitTypeDescription(pht('List of subtask PHIDs.')) ->setUseEdgeTransactions(true) - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $subtask_type) ->setValue($parent_phids); return $fields; } private function getTaskStatusMap(ManiphestTask $task) { $status_map = ManiphestTaskStatus::getTaskStatusMap(); $current_status = $task->getStatus(); // If the current status is something we don't recognize (maybe an older // status which was deleted), put a dummy entry in the status map so that // saving the form doesn't destroy any data by accident. if (idx($status_map, $current_status) === null) { $status_map[$current_status] = pht('', $current_status); } $dup_status = ManiphestTaskStatus::getDuplicateStatus(); foreach ($status_map as $status => $status_name) { // Always keep the task's current status. if ($status == $current_status) { continue; } // Don't allow tasks to be changed directly into "Closed, Duplicate" // status. Instead, you have to merge them. See T4819. if ($status == $dup_status) { unset($status_map[$status]); continue; } // Don't let new or existing tasks be moved into a disabled status. if (ManiphestTaskStatus::isDisabledStatus($status)) { unset($status_map[$status]); continue; } } return $status_map; } private function getTaskPriorityMap(ManiphestTask $task) { $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); $priority_keywords = ManiphestTaskPriority::getTaskPriorityKeywordsMap(); $current_priority = $task->getPriority(); $results = array(); foreach ($priority_map as $priority => $priority_name) { $disabled = ManiphestTaskPriority::isDisabledPriority($priority); if ($disabled && !($priority == $current_priority)) { continue; } $keyword = head(idx($priority_keywords, $priority)); $results[$keyword] = $priority_name; } // If the current value isn't a legitimate one, put it in the dropdown // anyway so saving the form doesn't cause any side effects. if (idx($priority_map, $current_priority) === null) { $results[ManiphestTaskPriority::UNKNOWN_PRIORITY_KEYWORD] = pht( '', $current_priority); } return $results; } protected function newEditResponse( AphrontRequest $request, $object, array $xactions) { if ($request->isAjax()) { // Reload the task to make sure we pick up the final task state. $viewer = $this->getViewer(); $task = id(new ManiphestTaskQuery()) ->setViewer($viewer) ->withIDs(array($object->getID())) ->needSubscriberPHIDs(true) ->needProjectPHIDs(true) ->executeOne(); switch ($request->getStr('responseType')) { case 'card': return $this->buildCardResponse($task); default: return $this->buildListResponse($task); } } return parent::newEditResponse($request, $object, $xactions); } private function buildListResponse(ManiphestTask $task) { $controller = $this->getController(); $payload = array( 'tasks' => $controller->renderSingleTask($task), 'data' => array(), ); return id(new AphrontAjaxResponse())->setContent($payload); } private function buildCardResponse(ManiphestTask $task) { $controller = $this->getController(); $request = $controller->getRequest(); $viewer = $request->getViewer(); $column_phid = $request->getStr('columnPHID'); $visible_phids = $request->getStrList('visiblePHIDs'); if (!$visible_phids) { $visible_phids = array(); } $column = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withPHIDs(array($column_phid)) ->executeOne(); if (!$column) { return new Aphront404Response(); } $board_phid = $column->getProjectPHID(); $object_phid = $task->getPHID(); return id(new PhabricatorBoardResponseEngine()) ->setViewer($viewer) ->setBoardPHID($board_phid) ->setObjectPHID($object_phid) ->setVisiblePHIDs($visible_phids) ->buildResponse(); } private function getColumnMap(ManiphestTask $task) { $phid = $task->getPHID(); if (!$phid) { return array(); } $board_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $phid, PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); if (!$board_phids) { return array(); } $viewer = $this->getViewer(); $layout_engine = id(new PhabricatorBoardLayoutEngine()) ->setViewer($viewer) ->setBoardPHIDs($board_phids) ->setObjectPHIDs(array($task->getPHID())) ->executeLayout(); $map = array(); foreach ($board_phids as $board_phid) { $in_columns = $layout_engine->getObjectColumns($board_phid, $phid); $in_columns = mpull($in_columns, null, 'getPHID'); $all_columns = $layout_engine->getColumns($board_phid); if (!$all_columns) { // This could be a project with no workboard, or a project the viewer // does not have permission to see. continue; } $board = head($all_columns)->getProject(); $options = array(); foreach ($all_columns as $column) { $name = $column->getDisplayName(); $is_hidden = $column->isHidden(); $is_selected = isset($in_columns[$column->getPHID()]); // Don't show hidden, subproject or milestone columns in this map // unless the object is currently in the column. $skip_column = ($is_hidden || $column->getProxyPHID()); if ($skip_column) { if (!$is_selected) { continue; } } if ($is_hidden) { $name = pht('(%s)', $name); } if ($is_selected) { $name = pht("\xE2\x97\x8F %s", $name); } else { $name = pht("\xE2\x97\x8B %s", $name); } $option = array( 'key' => $column->getPHID(), 'label' => $name, 'selected' => (bool)$is_selected, ); $options[] = $option; } $map[] = array( 'label' => $board->getDisplayName(), 'options' => $options, ); } $map = isort($map, 'label'); $map = array_values($map); return $map; } } diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 66c523573f..4b6c7707f6 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -1,1014 +1,1015 @@ getTransactionType()) { case PhabricatorTransactions::TYPE_COLUMNS: return null; } } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COLUMNS: return $xaction->getNewValue(); } } protected function transactionHasEffect( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COLUMNS: return (bool)$new; } return parent::transactionHasEffect($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COLUMNS: return; } } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COLUMNS: foreach ($xaction->getNewValue() as $move) { $this->applyBoardMove($object, $move); } break; } } protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { // When we change the status of a task, update tasks this tasks blocks // with a message to the effect of "alincoln resolved blocking task Txxx." $unblock_xaction = null; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case ManiphestTaskStatusTransaction::TRANSACTIONTYPE: $unblock_xaction = $xaction; break; } } if ($unblock_xaction !== null) { $blocked_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $object->getPHID(), ManiphestTaskDependedOnByTaskEdgeType::EDGECONST); if ($blocked_phids) { // In theory we could apply these through policies, but that seems a // little bit surprising. For now, use the actor's vision. $blocked_tasks = id(new ManiphestTaskQuery()) ->setViewer($this->getActor()) ->withPHIDs($blocked_phids) ->needSubscriberPHIDs(true) ->needProjectPHIDs(true) ->execute(); $old = $unblock_xaction->getOldValue(); $new = $unblock_xaction->getNewValue(); foreach ($blocked_tasks as $blocked_task) { $parent_xaction = id(new ManiphestTransaction()) ->setTransactionType( ManiphestTaskUnblockTransaction::TRANSACTIONTYPE) ->setOldValue(array($object->getPHID() => $old)) ->setNewValue(array($object->getPHID() => $new)); if ($this->getIsNewObject()) { $parent_xaction->setMetadataValue('blocker.new', true); } id(new ManiphestTransactionEditor()) ->setActor($this->getActor()) ->setActingAsPHID($this->getActingAsPHID()) ->setContentSource($this->getContentSource()) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($blocked_task, array($parent_xaction)); } } } return $xactions; } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function getMailSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.maniphest.subject-prefix'); } protected function getMailThreadID(PhabricatorLiskDAO $object) { return 'maniphest-task-'.$object->getPHID(); } protected function getMailTo(PhabricatorLiskDAO $object) { $phids = array(); if ($object->getOwnerPHID()) { $phids[] = $object->getOwnerPHID(); } $phids[] = $this->getActingAsPHID(); return $phids; } public function getMailTagsMap() { return array( ManiphestTransaction::MAILTAG_STATUS => pht("A task's status changes."), ManiphestTransaction::MAILTAG_OWNER => pht("A task's owner changes."), ManiphestTransaction::MAILTAG_PRIORITY => pht("A task's priority changes."), ManiphestTransaction::MAILTAG_CC => pht("A task's subscribers change."), ManiphestTransaction::MAILTAG_PROJECTS => pht("A task's associated projects change."), ManiphestTransaction::MAILTAG_UNBLOCK => pht("One of a task's subtasks changes status."), ManiphestTransaction::MAILTAG_COLUMN => pht('A task is moved between columns on a workboard.'), ManiphestTransaction::MAILTAG_COMMENT => pht('Someone comments on a task.'), ManiphestTransaction::MAILTAG_OTHER => pht('Other task activity not listed above occurs.'), ); } protected function buildReplyHandler(PhabricatorLiskDAO $object) { return id(new ManiphestReplyHandler()) ->setMailReceiver($object); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $id = $object->getID(); $title = $object->getTitle(); return id(new PhabricatorMetaMTAMail()) ->setSubject("T{$id}: {$title}"); } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = parent::buildMailBody($object, $xactions); if ($this->getIsNewObject()) { $body->addRemarkupSection( pht('TASK DESCRIPTION'), $object->getDescription()); } $body->addLinkSection( pht('TASK DETAIL'), PhabricatorEnv::getProductionURI('/T'.$object->getID())); $board_phids = array(); $type_columns = PhabricatorTransactions::TYPE_COLUMNS; foreach ($xactions as $xaction) { if ($xaction->getTransactionType() == $type_columns) { $moves = $xaction->getNewValue(); foreach ($moves as $move) { $board_phids[] = $move['boardPHID']; } } } if ($board_phids) { $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->requireActor()) ->withPHIDs($board_phids) ->execute(); foreach ($projects as $project) { $body->addLinkSection( pht('WORKBOARD'), PhabricatorEnv::getProductionURI( '/project/board/'.$project->getID().'/')); } } return $body; } protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function supportsSearch() { return true; } protected function shouldApplyHeraldRules( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function buildHeraldAdapter( PhabricatorLiskDAO $object, array $xactions) { return id(new HeraldManiphestTaskAdapter()) ->setTask($object); } protected function adjustObjectForPolicyChecks( PhabricatorLiskDAO $object, array $xactions) { $copy = parent::adjustObjectForPolicyChecks($object, $xactions); foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE: $copy->setOwnerPHID($xaction->getNewValue()); break; default: break; } } return $copy; } /** * Get priorities for moving a task to a new priority. */ public static function getEdgeSubpriority( $priority, $is_end) { $query = id(new ManiphestTaskQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPriorities(array($priority)) ->setLimit(1); if ($is_end) { $query->setOrderVector(array('-priority', '-subpriority', '-id')); } else { $query->setOrderVector(array('priority', 'subpriority', 'id')); } $result = $query->executeOne(); $step = (double)(2 << 32); if ($result) { $base = $result->getSubpriority(); if ($is_end) { $sub = ($base - $step); } else { $sub = ($base + $step); } } else { $sub = 0; } return array($priority, $sub); } /** * Get priorities for moving a task before or after another task. */ public static function getAdjacentSubpriority( ManiphestTask $dst, $is_after) { $query = id(new ManiphestTaskQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->setOrder(ManiphestTaskQuery::ORDER_PRIORITY) ->withPriorities(array($dst->getPriority())) ->setLimit(1); if ($is_after) { $query->setAfterID($dst->getID()); } else { $query->setBeforeID($dst->getID()); } $adjacent = $query->executeOne(); $base = $dst->getSubpriority(); $step = (double)(2 << 32); // If we find an adjacent task, we average the two subpriorities and // return the result. if ($adjacent) { $epsilon = 1.0; // If the adjacent task has a subpriority that is identical or very // close to the task we're looking at, we're going to spread out all // the nearby tasks. $adjacent_sub = $adjacent->getSubpriority(); if ((abs($adjacent_sub - $base) < $epsilon)) { $base = self::disperseBlock( $dst, $epsilon * 2); if ($is_after) { $sub = $base - $epsilon; } else { $sub = $base + $epsilon; } } else { $sub = ($adjacent_sub + $base) / 2; } } else { // Otherwise, we take a step away from the target's subpriority and // use that. if ($is_after) { $sub = ($base - $step); } else { $sub = ($base + $step); } } return array($dst->getPriority(), $sub); } /** * Distribute a cluster of tasks with similar subpriorities. */ private static function disperseBlock( ManiphestTask $task, $spacing) { $conn = $task->establishConnection('w'); // Find a block of subpriority space which is, on average, sparse enough // to hold all the tasks that are inside it with a reasonable level of // separation between them. // We'll start by looking near the target task for a range of numbers // which has more space available than tasks. For example, if the target // task has subpriority 33 and we want to separate each task by at least 1, // we might start by looking in the range [23, 43]. // If we find fewer than 20 tasks there, we have room to reassign them // with the desired level of separation. We space them out, then we're // done. // However: if we find more than 20 tasks, we don't have enough room to // distribute them. We'll widen our search and look in a bigger range, // maybe [13, 53]. This range has more space, so if we find fewer than // 40 tasks in this range we can spread them out. If we still find too // many tasks, we keep widening the search. $base = $task->getSubpriority(); $scale = 4.0; while (true) { $range = ($spacing * $scale) / 2.0; $min = ($base - $range); $max = ($base + $range); $result = queryfx_one( $conn, 'SELECT COUNT(*) N FROM %T WHERE priority = %d AND subpriority BETWEEN %f AND %f', $task->getTableName(), $task->getPriority(), $min, $max); $count = $result['N']; if ($count < $scale) { // We have found a block which we can make sparse enough, so bail and // continue below with our selection. break; } // This block had too many tasks for its size, so try again with a // bigger block. $scale *= 2.0; } $rows = queryfx_all( $conn, 'SELECT id FROM %T WHERE priority = %d AND subpriority BETWEEN %f AND %f ORDER BY priority, subpriority, id', $task->getTableName(), $task->getPriority(), $min, $max); $task_id = $task->getID(); $result = null; // NOTE: In strict mode (which we encourage enabling) we can't structure // this bulk update as an "INSERT ... ON DUPLICATE KEY UPDATE" unless we // provide default values for ALL of the columns that don't have defaults. // This is gross, but we may be moving enough rows that individual // queries are unreasonably slow. An alternate construction which might // be worth evaluating is to use "CASE". Another approach is to disable // strict mode for this query. + $default_str = qsprintf($conn, '%s', ''); + $default_int = qsprintf($conn, '%d', 0); + $extra_columns = array( - 'phid' => '""', - 'authorPHID' => '""', - 'status' => '""', - 'priority' => 0, - 'title' => '""', - 'description' => '""', - 'dateCreated' => 0, - 'dateModified' => 0, - 'mailKey' => '""', - 'viewPolicy' => '""', - 'editPolicy' => '""', - 'ownerOrdering' => '""', - 'spacePHID' => '""', - 'bridgedObjectPHID' => '""', - 'properties' => '""', - 'points' => 0, - 'subtype' => '""', + 'phid' => $default_str, + 'authorPHID' => $default_str, + 'status' => $default_str, + 'priority' => $default_int, + 'title' => $default_str, + 'description' => $default_str, + 'dateCreated' => $default_int, + 'dateModified' => $default_int, + 'mailKey' => $default_str, + 'viewPolicy' => $default_str, + 'editPolicy' => $default_str, + 'ownerOrdering' => $default_str, + 'spacePHID' => $default_str, + 'bridgedObjectPHID' => $default_str, + 'properties' => $default_str, + 'points' => $default_int, + 'subtype' => $default_str, ); - $defaults = implode(', ', $extra_columns); - $sql = array(); $offset = 0; // Often, we'll have more room than we need in the range. Distribute the // tasks evenly over the whole range so that we're less likely to end up // with tasks spaced exactly the minimum distance apart, which may // get shifted again later. We have one fewer space to distribute than we // have tasks. $divisor = (double)(count($rows) - 1.0); if ($divisor > 0) { $available_distance = (($max - $min) / $divisor); } else { $available_distance = 0.0; } foreach ($rows as $row) { $subpriority = $min + ($offset * $available_distance); // If this is the task that we're spreading out relative to, keep track // of where it is ending up so we can return the new subpriority. $id = $row['id']; if ($id == $task_id) { $result = $subpriority; } $sql[] = qsprintf( $conn, - '(%d, %Q, %f)', + '(%d, %LQ, %f)', $id, - $defaults, + $extra_columns, $subpriority); $offset++; } foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) { queryfx( $conn, - 'INSERT INTO %T (id, %Q, subpriority) VALUES %Q + 'INSERT INTO %T (id, %LC, subpriority) VALUES %LQ ON DUPLICATE KEY UPDATE subpriority = VALUES(subpriority)', $task->getTableName(), - implode(', ', array_keys($extra_columns)), + array_keys($extra_columns), $chunk); } return $result; } protected function validateAllTransactions( PhabricatorLiskDAO $object, array $xactions) { $errors = parent::validateAllTransactions($object, $xactions); if ($this->moreValidationErrors) { $errors = array_merge($errors, $this->moreValidationErrors); } return $errors; } protected function expandTransactions( PhabricatorLiskDAO $object, array $xactions) { $actor = $this->getActor(); $actor_phid = $actor->getPHID(); $results = parent::expandTransactions($object, $xactions); $is_unassigned = ($object->getOwnerPHID() === null); $any_assign = false; foreach ($xactions as $xaction) { if ($xaction->getTransactionType() == ManiphestTaskOwnerTransaction::TRANSACTIONTYPE) { $any_assign = true; break; } } $is_open = !$object->isClosed(); $new_status = null; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case ManiphestTaskStatusTransaction::TRANSACTIONTYPE: $new_status = $xaction->getNewValue(); break; } } if ($new_status === null) { $is_closing = false; } else { $is_closing = ManiphestTaskStatus::isClosedStatus($new_status); } // If the task is not assigned, not being assigned, currently open, and // being closed, try to assign the actor as the owner. if ($is_unassigned && !$any_assign && $is_open && $is_closing) { $is_claim = ManiphestTaskStatus::isClaimStatus($new_status); // Don't assign the actor if they aren't a real user. // Don't claim the task if the status is configured to not claim. if ($actor_phid && $is_claim) { $results[] = id(new ManiphestTransaction()) ->setTransactionType(ManiphestTaskOwnerTransaction::TRANSACTIONTYPE) ->setNewValue($actor_phid); } } // Automatically subscribe the author when they create a task. if ($this->getIsNewObject()) { if ($actor_phid) { $results[] = id(new ManiphestTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) ->setNewValue( array( '+' => array($actor_phid => $actor_phid), )); } } return $results; } protected function expandTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $results = parent::expandTransaction($object, $xaction); $type = $xaction->getTransactionType(); switch ($type) { case PhabricatorTransactions::TYPE_COLUMNS: try { $more_xactions = $this->buildMoveTransaction($object, $xaction); foreach ($more_xactions as $more_xaction) { $results[] = $more_xaction; } } catch (Exception $ex) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), $ex->getMessage(), $xaction); $this->moreValidationErrors[] = $error; } break; case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE: // If this is a no-op update, don't expand it. $old_value = $object->getOwnerPHID(); $new_value = $xaction->getNewValue(); if ($old_value === $new_value) { continue; } // When a task is reassigned, move the old owner to the subscriber // list so they're still in the loop. if ($old_value) { $results[] = id(new ManiphestTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) ->setIgnoreOnNoEffect(true) ->setNewValue( array( '+' => array($old_value => $old_value), )); } break; } return $results; } private function buildMoveTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $new = $xaction->getNewValue(); if (!is_array($new)) { $this->validateColumnPHID($new); $new = array($new); } $nearby_phids = array(); foreach ($new as $key => $value) { if (!is_array($value)) { $this->validateColumnPHID($value); $value = array( 'columnPHID' => $value, ); } PhutilTypeSpec::checkMap( $value, array( 'columnPHID' => 'string', 'beforePHID' => 'optional string', 'afterPHID' => 'optional string', )); $new[$key] = $value; if (!empty($value['beforePHID'])) { $nearby_phids[] = $value['beforePHID']; } if (!empty($value['afterPHID'])) { $nearby_phids[] = $value['afterPHID']; } } if ($nearby_phids) { $nearby_objects = id(new PhabricatorObjectQuery()) ->setViewer($this->getActor()) ->withPHIDs($nearby_phids) ->execute(); $nearby_objects = mpull($nearby_objects, null, 'getPHID'); } else { $nearby_objects = array(); } $column_phids = ipull($new, 'columnPHID'); if ($column_phids) { $columns = id(new PhabricatorProjectColumnQuery()) ->setViewer($this->getActor()) ->withPHIDs($column_phids) ->execute(); $columns = mpull($columns, null, 'getPHID'); } else { $columns = array(); } $board_phids = mpull($columns, 'getProjectPHID'); $object_phid = $object->getPHID(); $object_phids = $nearby_phids; // Note that we may not have an object PHID if we're creating a new // object. if ($object_phid) { $object_phids[] = $object_phid; } if ($object_phids) { $layout_engine = id(new PhabricatorBoardLayoutEngine()) ->setViewer($this->getActor()) ->setBoardPHIDs($board_phids) ->setObjectPHIDs($object_phids) ->setFetchAllBoards(true) ->executeLayout(); } foreach ($new as $key => $spec) { $column_phid = $spec['columnPHID']; $column = idx($columns, $column_phid); if (!$column) { throw new Exception( pht( 'Column move transaction specifies column PHID "%s", but there '. 'is no corresponding column with this PHID.', $column_phid)); } $board_phid = $column->getProjectPHID(); $nearby = array(); if (!empty($spec['beforePHID'])) { $nearby['beforePHID'] = $spec['beforePHID']; } if (!empty($spec['afterPHID'])) { $nearby['afterPHID'] = $spec['afterPHID']; } if (count($nearby) > 1) { throw new Exception( pht( 'Column move transaction moves object to multiple positions. '. 'Specify only "beforePHID" or "afterPHID", not both.')); } foreach ($nearby as $where => $nearby_phid) { if (empty($nearby_objects[$nearby_phid])) { throw new Exception( pht( 'Column move transaction specifies object "%s" as "%s", but '. 'there is no corresponding object with this PHID.', $object_phid, $where)); } $nearby_columns = $layout_engine->getObjectColumns( $board_phid, $nearby_phid); $nearby_columns = mpull($nearby_columns, null, 'getPHID'); if (empty($nearby_columns[$column_phid])) { throw new Exception( pht( 'Column move transaction specifies object "%s" as "%s" in '. 'column "%s", but this object is not in that column!', $nearby_phid, $where, $column_phid)); } } if ($object_phid) { $old_columns = $layout_engine->getObjectColumns( $board_phid, $object_phid); $old_column_phids = mpull($old_columns, 'getPHID'); } else { $old_column_phids = array(); } $spec += array( 'boardPHID' => $board_phid, 'fromColumnPHIDs' => $old_column_phids, ); // Check if the object is already in this column, and isn't being moved. // We can just drop this column change if it has no effect. $from_map = array_fuse($spec['fromColumnPHIDs']); $already_here = isset($from_map[$column_phid]); $is_reordering = (bool)$nearby; if ($already_here && !$is_reordering) { unset($new[$key]); } else { $new[$key] = $spec; } } $new = array_values($new); $xaction->setNewValue($new); $more = array(); // If we're moving the object into a column and it does not already belong // in the column, add the appropriate board. For normal columns, this // is the board PHID. For proxy columns, it is the proxy PHID, unless the // object is already a member of some descendant of the proxy PHID. // The major case where this can happen is moves via the API, but it also // happens when a user drags a task from the "Backlog" to a milestone // column. if ($object_phid) { $current_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $object_phid, PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); $current_phids = array_fuse($current_phids); } else { $current_phids = array(); } $add_boards = array(); foreach ($new as $move) { $column_phid = $move['columnPHID']; $board_phid = $move['boardPHID']; $column = $columns[$column_phid]; $proxy_phid = $column->getProxyPHID(); // If this is a normal column, add the board if the object isn't already // associated. if (!$proxy_phid) { if (!isset($current_phids[$board_phid])) { $add_boards[] = $board_phid; } continue; } // If this is a proxy column but the object is already associated with // the proxy board, we don't need to do anything. if (isset($current_phids[$proxy_phid])) { continue; } // If this a proxy column and the object is already associated with some // descendant of the proxy board, we also don't need to do anything. $descendants = id(new PhabricatorProjectQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withAncestorProjectPHIDs(array($proxy_phid)) ->execute(); $found_descendant = false; foreach ($descendants as $descendant) { if (isset($current_phids[$descendant->getPHID()])) { $found_descendant = true; break; } } if ($found_descendant) { continue; } // Otherwise, we're moving the object to a proxy column which it is not // a member of yet, so add an association to the column's proxy board. $add_boards[] = $proxy_phid; } if ($add_boards) { $more[] = id(new ManiphestTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue( 'edge:type', PhabricatorProjectObjectHasProjectEdgeType::EDGECONST) ->setIgnoreOnNoEffect(true) ->setNewValue( array( '+' => array_fuse($add_boards), )); } return $more; } private function applyBoardMove($object, array $move) { $board_phid = $move['boardPHID']; $column_phid = $move['columnPHID']; $before_phid = idx($move, 'beforePHID'); $after_phid = idx($move, 'afterPHID'); $object_phid = $object->getPHID(); // We're doing layout with the omnipotent viewer to make sure we don't // remove positions in columns that exist, but which the actual actor // can't see. $omnipotent_viewer = PhabricatorUser::getOmnipotentUser(); $select_phids = array($board_phid); $descendants = id(new PhabricatorProjectQuery()) ->setViewer($omnipotent_viewer) ->withAncestorProjectPHIDs($select_phids) ->execute(); foreach ($descendants as $descendant) { $select_phids[] = $descendant->getPHID(); } $board_tasks = id(new ManiphestTaskQuery()) ->setViewer($omnipotent_viewer) ->withEdgeLogicPHIDs( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, PhabricatorQueryConstraint::OPERATOR_ANCESTOR, array($select_phids)) ->execute(); $board_tasks = mpull($board_tasks, null, 'getPHID'); $board_tasks[$object_phid] = $object; // Make sure tasks are sorted by ID, so we lay out new positions in // a consistent way. $board_tasks = msort($board_tasks, 'getID'); $object_phids = array_keys($board_tasks); $engine = id(new PhabricatorBoardLayoutEngine()) ->setViewer($omnipotent_viewer) ->setBoardPHIDs(array($board_phid)) ->setObjectPHIDs($object_phids) ->executeLayout(); // TODO: This logic needs to be revised when we legitimately support // multiple column positions. $columns = $engine->getObjectColumns($board_phid, $object_phid); foreach ($columns as $column) { $engine->queueRemovePosition( $board_phid, $column->getPHID(), $object_phid); } if ($before_phid) { $engine->queueAddPositionBefore( $board_phid, $column_phid, $object_phid, $before_phid); } else if ($after_phid) { $engine->queueAddPositionAfter( $board_phid, $column_phid, $object_phid, $after_phid); } else { $engine->queueAddPosition( $board_phid, $column_phid, $object_phid); } $engine->applyPositionUpdates(); } private function validateColumnPHID($value) { if (phid_get_type($value) == PhabricatorProjectColumnPHIDType::TYPECONST) { return; } throw new Exception( pht( 'When moving objects between columns on a board, columns must '. 'be identified by PHIDs. This transaction uses "%s" to identify '. 'a column, but that is not a valid column PHID.', $value)); } } diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php index ccf93c4a2d..d1932a7597 100644 --- a/src/applications/maniphest/query/ManiphestTaskQuery.php +++ b/src/applications/maniphest/query/ManiphestTaskQuery.php @@ -1,995 +1,995 @@ authorPHIDs = $authors; return $this; } public function withIDs(array $ids) { $this->taskIDs = $ids; return $this; } public function withPHIDs(array $phids) { $this->taskPHIDs = $phids; return $this; } public function withOwners(array $owners) { if ($owners === array()) { throw new Exception(pht('Empty withOwners() constraint is not valid.')); } $no_owner = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN; $any_owner = PhabricatorPeopleAnyOwnerDatasource::FUNCTION_TOKEN; foreach ($owners as $k => $phid) { if ($phid === $no_owner || $phid === null) { $this->noOwner = true; unset($owners[$k]); break; } if ($phid === $any_owner) { $this->anyOwner = true; unset($owners[$k]); break; } } if ($owners) { $this->ownerPHIDs = $owners; } return $this; } public function withStatus($status) { $this->status = $status; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } public function withPriorities(array $priorities) { $this->priorities = $priorities; return $this; } public function withSubpriorities(array $subpriorities) { $this->subpriorities = $subpriorities; return $this; } public function withSubscribers(array $subscribers) { $this->subscriberPHIDs = $subscribers; return $this; } public function setGroupBy($group) { $this->groupBy = $group; switch ($this->groupBy) { case self::GROUP_NONE: $vector = array(); break; case self::GROUP_PRIORITY: $vector = array('priority'); break; case self::GROUP_OWNER: $vector = array('owner'); break; case self::GROUP_STATUS: $vector = array('status'); break; case self::GROUP_PROJECT: $vector = array('project'); break; } $this->setGroupVector($vector); return $this; } public function withOpenSubtasks($value) { $this->hasOpenSubtasks = $value; return $this; } public function withOpenParents($value) { $this->hasOpenParents = $value; return $this; } public function withParentTaskIDs(array $ids) { $this->parentTaskIDs = $ids; return $this; } public function withSubtaskIDs(array $ids) { $this->subtaskIDs = $ids; return $this; } public function withDateCreatedBefore($date_created_before) { $this->dateCreatedBefore = $date_created_before; return $this; } public function withDateCreatedAfter($date_created_after) { $this->dateCreatedAfter = $date_created_after; return $this; } public function withDateModifiedBefore($date_modified_before) { $this->dateModifiedBefore = $date_modified_before; return $this; } public function withDateModifiedAfter($date_modified_after) { $this->dateModifiedAfter = $date_modified_after; return $this; } public function withClosedEpochBetween($min, $max) { $this->closedEpochMin = $min; $this->closedEpochMax = $max; return $this; } public function withCloserPHIDs(array $phids) { $this->closerPHIDs = $phids; return $this; } public function needSubscriberPHIDs($bool) { $this->needSubscriberPHIDs = $bool; return $this; } public function needProjectPHIDs($bool) { $this->needProjectPHIDs = $bool; return $this; } public function withBridgedObjectPHIDs(array $phids) { $this->bridgedObjectPHIDs = $phids; return $this; } public function withSubtypes(array $subtypes) { $this->subtypes = $subtypes; return $this; } public function withColumnPHIDs(array $column_phids) { $this->columnPHIDs = $column_phids; return $this; } public function newResultObject() { return new ManiphestTask(); } protected function loadPage() { $task_dao = new ManiphestTask(); $conn = $task_dao->establishConnection('r'); $where = $this->buildWhereClause($conn); - $group_column = ''; + $group_column = qsprintf($conn, ''); switch ($this->groupBy) { case self::GROUP_PROJECT: $group_column = qsprintf( $conn, ', projectGroupName.indexedObjectPHID projectGroupPHID'); break; } $rows = queryfx_all( $conn, '%Q %Q FROM %T task %Q %Q %Q %Q %Q %Q', $this->buildSelectClause($conn), $group_column, $task_dao->getTableName(), $this->buildJoinClause($conn), $where, $this->buildGroupClause($conn), $this->buildHavingClause($conn), $this->buildOrderClause($conn), $this->buildLimitClause($conn)); switch ($this->groupBy) { case self::GROUP_PROJECT: $data = ipull($rows, null, 'id'); break; default: $data = $rows; break; } $data = $this->didLoadRawRows($data); $tasks = $task_dao->loadAllFromArray($data); switch ($this->groupBy) { case self::GROUP_PROJECT: $results = array(); foreach ($rows as $row) { $task = clone $tasks[$row['id']]; $task->attachGroupByProjectPHID($row['projectGroupPHID']); $results[] = $task; } $tasks = $results; break; } return $tasks; } protected function willFilterPage(array $tasks) { if ($this->groupBy == self::GROUP_PROJECT) { // We should only return project groups which the user can actually see. $project_phids = mpull($tasks, 'getGroupByProjectPHID'); $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs($project_phids) ->execute(); $projects = mpull($projects, null, 'getPHID'); foreach ($tasks as $key => $task) { if (!$task->getGroupByProjectPHID()) { // This task is either not tagged with any projects, or only tagged // with projects which we're ignoring because they're being queried // for explicitly. continue; } if (empty($projects[$task->getGroupByProjectPHID()])) { unset($tasks[$key]); } } } return $tasks; } protected function didFilterPage(array $tasks) { $phids = mpull($tasks, 'getPHID'); if ($this->needProjectPHIDs) { $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($phids) ->withEdgeTypes( array( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, )); $edge_query->execute(); foreach ($tasks as $task) { $project_phids = $edge_query->getDestinationPHIDs( array($task->getPHID())); $task->attachProjectPHIDs($project_phids); } } if ($this->needSubscriberPHIDs) { $subscriber_sets = id(new PhabricatorSubscribersQuery()) ->withObjectPHIDs($phids) ->execute(); foreach ($tasks as $task) { $subscribers = idx($subscriber_sets, $task->getPHID(), array()); $task->attachSubscriberPHIDs($subscribers); } } return $tasks; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); $where[] = $this->buildStatusWhereClause($conn); $where[] = $this->buildOwnerWhereClause($conn); if ($this->taskIDs !== null) { $where[] = qsprintf( $conn, 'task.id in (%Ld)', $this->taskIDs); } if ($this->taskPHIDs !== null) { $where[] = qsprintf( $conn, 'task.phid in (%Ls)', $this->taskPHIDs); } if ($this->statuses !== null) { $where[] = qsprintf( $conn, 'task.status IN (%Ls)', $this->statuses); } if ($this->authorPHIDs !== null) { $where[] = qsprintf( $conn, 'task.authorPHID in (%Ls)', $this->authorPHIDs); } if ($this->dateCreatedAfter) { $where[] = qsprintf( $conn, 'task.dateCreated >= %d', $this->dateCreatedAfter); } if ($this->dateCreatedBefore) { $where[] = qsprintf( $conn, 'task.dateCreated <= %d', $this->dateCreatedBefore); } if ($this->dateModifiedAfter) { $where[] = qsprintf( $conn, 'task.dateModified >= %d', $this->dateModifiedAfter); } if ($this->dateModifiedBefore) { $where[] = qsprintf( $conn, 'task.dateModified <= %d', $this->dateModifiedBefore); } if ($this->closedEpochMin !== null) { $where[] = qsprintf( $conn, 'task.closedEpoch >= %d', $this->closedEpochMin); } if ($this->closedEpochMax !== null) { $where[] = qsprintf( $conn, 'task.closedEpoch <= %d', $this->closedEpochMax); } if ($this->closerPHIDs !== null) { $where[] = qsprintf( $conn, 'task.closerPHID IN (%Ls)', $this->closerPHIDs); } if ($this->priorities !== null) { $where[] = qsprintf( $conn, 'task.priority IN (%Ld)', $this->priorities); } if ($this->subpriorities !== null) { $where[] = qsprintf( $conn, 'task.subpriority IN (%Lf)', $this->subpriorities); } if ($this->bridgedObjectPHIDs !== null) { $where[] = qsprintf( $conn, 'task.bridgedObjectPHID IN (%Ls)', $this->bridgedObjectPHIDs); } if ($this->subtypes !== null) { $where[] = qsprintf( $conn, 'task.subtype IN (%Ls)', $this->subtypes); } if ($this->columnPHIDs !== null) { $viewer = $this->getViewer(); $columns = id(new PhabricatorProjectColumnQuery()) ->setParentQuery($this) ->setViewer($viewer) ->withPHIDs($this->columnPHIDs) ->execute(); if (!$columns) { throw new PhabricatorEmptyQueryException(); } // We must do board layout before we move forward because the column // positions may not yet exist otherwise. An example is that newly // created tasks may not yet be positioned in the backlog column. $projects = mpull($columns, 'getProject'); $projects = mpull($projects, null, 'getPHID'); // The board layout engine needs to know about every object that it's // going to be asked to do layout for. For now, we're just doing layout // on every object on the boards. In the future, we could do layout on a // smaller set of objects by using the constraints on this Query. For // example, if the caller is only asking for open tasks, we only need // to do layout on open tasks. // This fetches too many objects (every type of object tagged with the // project, not just tasks). We could narrow it by querying the edge // table on the Maniphest side, but there's currently no way to build // that query with EdgeQuery. $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array_keys($projects)) ->withEdgeTypes( array( PhabricatorProjectProjectHasObjectEdgeType::EDGECONST, )); $edge_query->execute(); $all_phids = $edge_query->getDestinationPHIDs(); // Since we overfetched PHIDs, filter out any non-tasks we got back. foreach ($all_phids as $key => $phid) { if (phid_get_type($phid) !== ManiphestTaskPHIDType::TYPECONST) { unset($all_phids[$key]); } } // If there are no tasks on the relevant boards, this query can't // possibly hit anything so we're all done. $task_phids = array_fuse($all_phids); if (!$task_phids) { throw new PhabricatorEmptyQueryException(); } // We know everything we need to know, so perform board layout. $engine = id(new PhabricatorBoardLayoutEngine()) ->setViewer($viewer) ->setFetchAllBoards(true) ->setBoardPHIDs(array_keys($projects)) ->setObjectPHIDs($task_phids) ->executeLayout(); // Find the tasks that are in the constraint columns after board layout // completes. $select_phids = array(); foreach ($columns as $column) { $in_column = $engine->getColumnObjectPHIDs( $column->getProjectPHID(), $column->getPHID()); foreach ($in_column as $phid) { $select_phids[$phid] = $phid; } } if (!$select_phids) { throw new PhabricatorEmptyQueryException(); } $where[] = qsprintf( $conn, 'task.phid IN (%Ls)', $select_phids); } return $where; } private function buildStatusWhereClause(AphrontDatabaseConnection $conn) { static $map = array( self::STATUS_RESOLVED => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, self::STATUS_WONTFIX => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX, self::STATUS_INVALID => ManiphestTaskStatus::STATUS_CLOSED_INVALID, self::STATUS_SPITE => ManiphestTaskStatus::STATUS_CLOSED_SPITE, self::STATUS_DUPLICATE => ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE, ); switch ($this->status) { case self::STATUS_ANY: return null; case self::STATUS_OPEN: return qsprintf( $conn, 'task.status IN (%Ls)', ManiphestTaskStatus::getOpenStatusConstants()); case self::STATUS_CLOSED: return qsprintf( $conn, 'task.status IN (%Ls)', ManiphestTaskStatus::getClosedStatusConstants()); default: $constant = idx($map, $this->status); if (!$constant) { throw new Exception(pht("Unknown status query '%s'!", $this->status)); } return qsprintf( $conn, 'task.status = %s', $constant); } } private function buildOwnerWhereClause(AphrontDatabaseConnection $conn) { $subclause = array(); if ($this->noOwner) { $subclause[] = qsprintf( $conn, 'task.ownerPHID IS NULL'); } if ($this->anyOwner) { $subclause[] = qsprintf( $conn, 'task.ownerPHID IS NOT NULL'); } if ($this->ownerPHIDs !== null) { $subclause[] = qsprintf( $conn, 'task.ownerPHID IN (%Ls)', $this->ownerPHIDs); } if (!$subclause) { - return ''; + return qsprintf($conn, ''); } - return '('.implode(') OR (', $subclause).')'; + return qsprintf($conn, '%LO', $subclause); } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { $open_statuses = ManiphestTaskStatus::getOpenStatusConstants(); $edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE; $task_table = $this->newResultObject()->getTableName(); $parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST; $subtask_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST; $joins = array(); if ($this->hasOpenParents !== null) { if ($this->hasOpenParents) { $join_type = 'JOIN'; } else { $join_type = 'LEFT JOIN'; } $joins[] = qsprintf( $conn, '%Q %T e_parent ON e_parent.src = task.phid AND e_parent.type = %d %Q %T parent ON e_parent.dst = parent.phid AND parent.status IN (%Ls)', $join_type, $edge_table, $parent_type, $join_type, $task_table, $open_statuses); } if ($this->hasOpenSubtasks !== null) { if ($this->hasOpenSubtasks) { $join_type = 'JOIN'; } else { $join_type = 'LEFT JOIN'; } $joins[] = qsprintf( $conn, '%Q %T e_subtask ON e_subtask.src = task.phid AND e_subtask.type = %d %Q %T subtask ON e_subtask.dst = subtask.phid AND subtask.status IN (%Ls)', $join_type, $edge_table, $subtask_type, $join_type, $task_table, $open_statuses); } if ($this->subscriberPHIDs !== null) { $joins[] = qsprintf( $conn, 'JOIN %T e_ccs ON e_ccs.src = task.phid '. 'AND e_ccs.type = %s '. 'AND e_ccs.dst in (%Ls)', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorObjectHasSubscriberEdgeType::EDGECONST, $this->subscriberPHIDs); } switch ($this->groupBy) { case self::GROUP_PROJECT: $ignore_group_phids = $this->getIgnoreGroupedProjectPHIDs(); if ($ignore_group_phids) { $joins[] = qsprintf( $conn, 'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src AND projectGroup.type = %d AND projectGroup.dst NOT IN (%Ls)', $edge_table, PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, $ignore_group_phids); } else { $joins[] = qsprintf( $conn, 'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src AND projectGroup.type = %d', $edge_table, PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); } $joins[] = qsprintf( $conn, 'LEFT JOIN %T projectGroupName ON projectGroup.dst = projectGroupName.indexedObjectPHID', id(new ManiphestNameIndex())->getTableName()); break; } if ($this->parentTaskIDs !== null) { $joins[] = qsprintf( $conn, 'JOIN %T e_has_parent ON e_has_parent.src = task.phid AND e_has_parent.type = %d JOIN %T has_parent ON e_has_parent.dst = has_parent.phid AND has_parent.id IN (%Ld)', $edge_table, $parent_type, $task_table, $this->parentTaskIDs); } if ($this->subtaskIDs !== null) { $joins[] = qsprintf( $conn, 'JOIN %T e_has_subtask ON e_has_subtask.src = task.phid AND e_has_subtask.type = %d JOIN %T has_subtask ON e_has_subtask.dst = has_subtask.phid AND has_subtask.id IN (%Ld)', $edge_table, $subtask_type, $task_table, $this->subtaskIDs); } $joins[] = parent::buildJoinClauseParts($conn); return $joins; } - protected function buildGroupClause(AphrontDatabaseConnection $conn_r) { + protected function buildGroupClause(AphrontDatabaseConnection $conn) { $joined_multiple_rows = ($this->hasOpenParents !== null) || ($this->hasOpenSubtasks !== null) || ($this->parentTaskIDs !== null) || ($this->subtaskIDs !== null) || $this->shouldGroupQueryResultRows(); $joined_project_name = ($this->groupBy == self::GROUP_PROJECT); // If we're joining multiple rows, we need to group the results by the // task IDs. if ($joined_multiple_rows) { if ($joined_project_name) { - return 'GROUP BY task.phid, projectGroup.dst'; + return qsprintf($conn, 'GROUP BY task.phid, projectGroup.dst'); } else { - return 'GROUP BY task.phid'; + return qsprintf($conn, 'GROUP BY task.phid'); } - } else { - return ''; } + + return qsprintf($conn, ''); } protected function buildHavingClauseParts(AphrontDatabaseConnection $conn) { $having = parent::buildHavingClauseParts($conn); if ($this->hasOpenParents !== null) { if (!$this->hasOpenParents) { $having[] = qsprintf( $conn, 'COUNT(parent.phid) = 0'); } } if ($this->hasOpenSubtasks !== null) { if (!$this->hasOpenSubtasks) { $having[] = qsprintf( $conn, 'COUNT(subtask.phid) = 0'); } } return $having; } /** * Return project PHIDs which we should ignore when grouping tasks by * project. For example, if a user issues a query like: * * Tasks tagged with all projects: Frontend, Bugs * * ...then we don't show "Frontend" or "Bugs" groups in the result set, since * they're meaningless as all results are in both groups. * * Similarly, for queries like: * * Tasks tagged with any projects: Public Relations * * ...we ignore the single project, as every result is in that project. (In * the case that there are several "any" projects, we do not ignore them.) * * @return list Project PHIDs which should be ignored in query * construction. */ private function getIgnoreGroupedProjectPHIDs() { // Maybe we should also exclude the "OPERATOR_NOT" PHIDs? It won't // impact the results, but we might end up with a better query plan. // Investigate this on real data? This is likely very rare. $edge_types = array( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, ); $phids = array(); $phids[] = $this->getEdgeLogicValues( $edge_types, array( PhabricatorQueryConstraint::OPERATOR_AND, )); $any = $this->getEdgeLogicValues( $edge_types, array( PhabricatorQueryConstraint::OPERATOR_OR, )); if (count($any) == 1) { $phids[] = $any; } return array_mergev($phids); } protected function getResultCursor($result) { $id = $result->getID(); if ($this->groupBy == self::GROUP_PROJECT) { return rtrim($id.'.'.$result->getGroupByProjectPHID(), '.'); } return $id; } public function getBuiltinOrders() { $orders = array( 'priority' => array( 'vector' => array('priority', 'subpriority', 'id'), 'name' => pht('Priority'), 'aliases' => array(self::ORDER_PRIORITY), ), 'updated' => array( 'vector' => array('updated', 'id'), 'name' => pht('Date Updated (Latest First)'), 'aliases' => array(self::ORDER_MODIFIED), ), 'outdated' => array( 'vector' => array('-updated', '-id'), 'name' => pht('Date Updated (Oldest First)'), ), 'closed' => array( 'vector' => array('closed', 'id'), 'name' => pht('Date Closed (Latest First)'), ), 'title' => array( 'vector' => array('title', 'id'), 'name' => pht('Title'), 'aliases' => array(self::ORDER_TITLE), ), ) + parent::getBuiltinOrders(); // Alias the "newest" builtin to the historical key for it. $orders['newest']['aliases'][] = self::ORDER_CREATED; $orders = array_select_keys( $orders, array( 'priority', 'updated', 'outdated', 'newest', 'oldest', 'closed', 'title', )) + $orders; return $orders; } public function getOrderableColumns() { return parent::getOrderableColumns() + array( 'priority' => array( 'table' => 'task', 'column' => 'priority', 'type' => 'int', ), 'owner' => array( 'table' => 'task', 'column' => 'ownerOrdering', 'null' => 'head', 'reverse' => true, 'type' => 'string', ), 'status' => array( 'table' => 'task', 'column' => 'status', 'type' => 'string', 'reverse' => true, ), 'project' => array( 'table' => 'projectGroupName', 'column' => 'indexedObjectName', 'type' => 'string', 'null' => 'head', 'reverse' => true, ), 'title' => array( 'table' => 'task', 'column' => 'title', 'type' => 'string', 'reverse' => true, ), 'subpriority' => array( 'table' => 'task', 'column' => 'subpriority', 'type' => 'float', ), 'updated' => array( 'table' => 'task', 'column' => 'dateModified', 'type' => 'int', ), 'closed' => array( 'table' => 'task', 'column' => 'closedEpoch', 'type' => 'int', 'null' => 'tail', ), ); } protected function getPagingValueMap($cursor, array $keys) { $cursor_parts = explode('.', $cursor, 2); $task_id = $cursor_parts[0]; $group_id = idx($cursor_parts, 1); $task = $this->loadCursorObject($task_id); $map = array( 'id' => $task->getID(), 'priority' => $task->getPriority(), 'subpriority' => $task->getSubpriority(), 'owner' => $task->getOwnerOrdering(), 'status' => $task->getStatus(), 'title' => $task->getTitle(), 'updated' => $task->getDateModified(), 'closed' => $task->getClosedEpoch(), ); foreach ($keys as $key) { switch ($key) { case 'project': $value = null; if ($group_id) { $paging_projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs(array($group_id)) ->execute(); if ($paging_projects) { $value = head($paging_projects)->getName(); } } $map[$key] = $value; break; } } foreach ($keys as $key) { if ($this->isCustomFieldOrderKey($key)) { $map += $this->getPagingValueMapForCustomFields($task); break; } } return $map; } protected function getPrimaryTableAlias() { return 'task'; } public function getQueryApplicationClass() { return 'PhabricatorManiphestApplication'; } } diff --git a/src/applications/meta/xactions/PhabricatorApplicationPolicyChangeTransaction.php b/src/applications/meta/xactions/PhabricatorApplicationPolicyChangeTransaction.php index 91b5c73249..5364a3d4fa 100644 --- a/src/applications/meta/xactions/PhabricatorApplicationPolicyChangeTransaction.php +++ b/src/applications/meta/xactions/PhabricatorApplicationPolicyChangeTransaction.php @@ -1,197 +1,206 @@ getCapabilityName(); return $application->getPolicy($capability); } public function applyExternalEffects($object, $value) { $application = $object; $user = $this->getActor(); $key = 'phabricator.application-settings'; $config_entry = PhabricatorConfigEntry::loadConfigEntry($key); $current_value = $config_entry->getValue(); $phid = $application->getPHID(); if (empty($current_value[$phid])) { $current_value[$application->getPHID()] = array(); } if (empty($current_value[$phid]['policy'])) { $current_value[$phid]['policy'] = array(); } $new = array($this->getCapabilityName() => $value); $current_value[$phid]['policy'] = $new + $current_value[$phid]['policy']; $editor = $this->getEditor(); $content_source = $editor->getContentSource(); + + // NOTE: We allow applications to have custom edit policies, but they are + // currently stored in the Config application. The ability to edit Config + // values is always restricted to administrators, today. Empower this + // particular edit to punch through possible stricter policies, so normal + // users can change application configuration if the application allows + // them to do so. + PhabricatorConfigEditor::storeNewValue( - $user, + PhabricatorUser::getOmnipotentUser(), $config_entry, $current_value, - $content_source); + $content_source, + $user->getPHID()); } public function getTitle() { - $old = $this->renderPolicy($this->getOldValue()); - $new = $this->renderPolicy($this->getNewValue()); + $old = $this->renderApplicationPolicy($this->getOldValue()); + $new = $this->renderApplicationPolicy($this->getNewValue()); return pht( '%s changed the "%s" policy from "%s" to "%s".', $this->renderAuthor(), $this->renderCapability(), $old, $new); } public function getTitleForFeed() { - $old = $this->renderPolicy($this->getOldValue()); - $new = $this->renderPolicy($this->getNewValue()); + $old = $this->renderApplicationPolicy($this->getOldValue()); + $new = $this->renderApplicationPolicy($this->getNewValue()); return pht( '%s changed the "%s" policy for application %s from "%s" to "%s".', $this->renderAuthor(), $this->renderCapability(), $this->renderObject(), $old, $new); } public function validateTransactions($object, array $xactions) { $user = $this->getActor(); $application = $object; $policies = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($application) ->execute(); $errors = array(); foreach ($xactions as $xaction) { $new = $xaction->getNewValue(); $capability = $xaction->getMetadataValue(self::METADATA_ATTRIBUTE); if (empty($policies[$new])) { // Not a standard policy, check for a custom policy. $policy = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->withPHIDs(array($new)) ->executeOne(); if (!$policy) { $errors[] = $this->newInvalidError( pht('Policy does not exist.')); continue; } } else { $policy = idx($policies, $new); } if (!$policy->isValidPolicyForEdit()) { $errors[] = $this->newInvalidError( pht('Can\'t set the policy to a policy you can\'t view!')); continue; } if ($new == PhabricatorPolicies::POLICY_PUBLIC) { $capobj = PhabricatorPolicyCapability::getCapabilityByKey( $capability); if (!$capobj || !$capobj->shouldAllowPublicPolicySetting()) { $errors[] = $this->newInvalidError( pht('Can\'t set non-public policies to public.')); continue; } } if (!$application->isCapabilityEditable($capability)) { $errors[] = $this->newInvalidError( pht('Capability "%s" is not editable for this application.', $capability)); continue; } } // If we're changing these policies, the viewer needs to still be able to // view or edit the application under the new policy. $validate_map = array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); $validate_map = array_fill_keys($validate_map, array()); foreach ($xactions as $xaction) { $capability = $xaction->getMetadataValue(self::METADATA_ATTRIBUTE); if (!isset($validate_map[$capability])) { continue; } $validate_map[$capability][] = $xaction; } foreach ($validate_map as $capability => $cap_xactions) { if (!$cap_xactions) { continue; } $editor = $this->getEditor(); $policy_errors = $editor->validatePolicyTransaction( $object, $cap_xactions, self::TRANSACTIONTYPE, $capability); foreach ($policy_errors as $error) { $errors[] = $error; } } return $errors; } - private function renderPolicy($name) { + private function renderApplicationPolicy($name) { $policies = $this->getAllPolicies(); if (empty($policies[$name])) { // Not a standard policy, check for a custom policy. $policy = id(new PhabricatorPolicyQuery()) ->setViewer($this->getViewer()) ->withPHIDs(array($name)) ->executeOne(); $policies[$name] = $policy; } $policy = idx($policies, $name); return $this->renderValue($policy->getFullName()); } private function getAllPolicies() { if (!$this->policies) { $viewer = $this->getViewer(); $application = $this->getObject(); $this->policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($application) ->execute(); } return $this->policies; } private function renderCapability() { $application = $this->getObject(); $capability = $this->getCapabilityName(); return $application->getCapabilityLabel($capability); } private function getCapabilityName() { return $this->getMetadataValue(self::METADATA_ATTRIBUTE); } } diff --git a/src/applications/meta/xactions/PhabricatorApplicationUninstallTransaction.php b/src/applications/meta/xactions/PhabricatorApplicationUninstallTransaction.php index 12fbc8ebd4..b76e63b5c0 100644 --- a/src/applications/meta/xactions/PhabricatorApplicationUninstallTransaction.php +++ b/src/applications/meta/xactions/PhabricatorApplicationUninstallTransaction.php @@ -1,79 +1,83 @@ getValue(); $uninstalled = PhabricatorEnv::getEnvConfig($key); if (isset($uninstalled[get_class($object)])) { return 'uninstalled'; } else { return 'installed'; } } public function generateNewValue($object, $value) { if ($value === 'uninstall') { return 'uninstalled'; } else { return 'installed'; } } public function applyExternalEffects($object, $value) { $application = $object; $user = $this->getActor(); $key = 'phabricator.uninstalled-applications'; $config_entry = PhabricatorConfigEntry::loadConfigEntry($key); $list = $config_entry->getValue(); $uninstalled = PhabricatorEnv::getEnvConfig($key); if (isset($uninstalled[get_class($application)])) { unset($list[get_class($application)]); } else { $list[get_class($application)] = true; } $editor = $this->getEditor(); $content_source = $editor->getContentSource(); + + // Today, changing config requires "Administrator", but "Can Edit" on + // applications to let you uninstall them may be granted to any user. PhabricatorConfigEditor::storeNewValue( - $user, + PhabricatorUser::getOmnipotentUser(), $config_entry, $list, - $content_source); + $content_source, + $user->getPHID()); } public function getTitle() { if ($this->getNewValue() === 'uninstalled') { return pht( '%s uninstalled this application.', $this->renderAuthor()); } else { return pht( '%s installed this application.', $this->renderAuthor()); } } public function getTitleForFeed() { if ($this->getNewValue() === 'uninstalled') { return pht( '%s uninstalled %s.', $this->renderAuthor(), $this->renderObject()); } else { return pht( '%s installed %s.', $this->renderAuthor(), $this->renderObject()); } } } diff --git a/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php b/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php index 6aaf4fb052..778e05b052 100644 --- a/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php +++ b/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php @@ -1,241 +1,223 @@ viewer; } public function setViewer($viewer) { $this->viewer = $viewer; return $this; } public function setContextObject($context_object) { $this->contextObject = $context_object; return $this; } public function getContextObject() { return $this->contextObject; } /* -( Composition )-------------------------------------------------------- */ /** * Add a raw block of text to the email. This will be rendered as-is. * * @param string Block of text. * @return this * @task compose */ public function addRawSection($text) { if (strlen($text)) { $text = rtrim($text); $this->sections[] = $text; $this->htmlSections[] = phutil_escape_html_newlines( phutil_tag('div', array(), $text)); } return $this; } public function addRemarkupSection($header, $text) { try { $engine = $this->newMarkupEngine() ->setMode(PhutilRemarkupEngine::MODE_TEXT); $styled_text = $engine->markupText($text); $this->addPlaintextSection($header, $styled_text); } catch (Exception $ex) { phlog($ex); $this->addTextSection($header, $text); } try { $mail_engine = $this->newMarkupEngine() ->setMode(PhutilRemarkupEngine::MODE_HTML_MAIL); $html = $mail_engine->markupText($text); $this->addHTMLSection($header, $html); } catch (Exception $ex) { phlog($ex); $this->addHTMLSection($header, $text); } return $this; } public function addRawPlaintextSection($text) { if (strlen($text)) { $text = rtrim($text); $this->sections[] = $text; } return $this; } public function addRawHTMLSection($html) { $this->htmlSections[] = phutil_safe_html($html); return $this; } /** * Add a block of text with a section header. This is rendered like this: * * HEADER * Text is indented. * * @param string Header text. * @param string Section text. * @return this * @task compose */ public function addTextSection($header, $section) { if ($section instanceof PhabricatorMetaMTAMailSection) { $plaintext = $section->getPlaintext(); $html = $section->getHTML(); } else { $plaintext = $section; $html = phutil_escape_html_newlines(phutil_tag('div', array(), $section)); } $this->addPlaintextSection($header, $plaintext); $this->addHTMLSection($header, $html); return $this; } public function addPlaintextSection($header, $text, $indent = true) { if ($indent) { $text = $this->indent($text); } $this->sections[] = $header."\n".$text; return $this; } public function addHTMLSection($header, $html_fragment) { if ($header !== null) { $header = phutil_tag('strong', array(), $header); } $this->htmlSections[] = array( phutil_tag( 'div', array(), array( $header, phutil_tag('div', array(), $html_fragment), )), ); return $this; } public function addLinkSection($header, $link) { $html = phutil_tag('a', array('href' => $link), $link); $this->addPlaintextSection($header, $link); $this->addHTMLSection($header, $html); return $this; } - /** - * Add a Herald section with a rule management URI and a transcript URI. - * - * @param string URI to rule transcripts. - * @return this - * @task compose - */ - public function addHeraldSection($xscript_uri) { - if (!PhabricatorEnv::getEnvConfig('metamta.herald.show-hints')) { - return $this; - } - - $this->addLinkSection( - pht('WHY DID I GET THIS EMAIL?'), - PhabricatorEnv::getProductionURI($xscript_uri)); - - return $this; - } /** * Add an attachment. * * @param PhabricatorMetaMTAAttachment Attachment. * @return this * @task compose */ public function addAttachment(PhabricatorMetaMTAAttachment $attachment) { $this->attachments[] = $attachment; return $this; } /* -( Rendering )---------------------------------------------------------- */ /** * Render the email body. * * @return string Rendered body. * @task render */ public function render() { return implode("\n\n", $this->sections)."\n"; } public function renderHTML() { $br = phutil_tag('br'); $body = phutil_implode_html($br, $this->htmlSections); return (string)hsprintf('%s', array($body, $br)); } /** * Retrieve attachments. * * @return list Attachments. * @task render */ public function getAttachments() { return $this->attachments; } /** * Indent a block of text for rendering under a section heading. * * @param string Text to indent. * @return string Indented text. * @task render */ private function indent($text) { return rtrim(" ".str_replace("\n", "\n ", $text)); } private function newMarkupEngine() { $engine = PhabricatorMarkupEngine::newMarkupEngine(array()) ->setConfig('viewer', $this->getViewer()) ->setConfig('uri.base', PhabricatorEnv::getProductionURI('/')); $context = $this->getContextObject(); if ($context) { $engine->setConfig('contextObject', $context); } return $engine; } } diff --git a/src/applications/metamta/view/__tests__/PhabricatorMetaMTAMailBodyTestCase.php b/src/applications/metamta/view/__tests__/PhabricatorMetaMTAMailBodyTestCase.php index e8415c7675..5753802996 100644 --- a/src/applications/metamta/view/__tests__/PhabricatorMetaMTAMailBodyTestCase.php +++ b/src/applications/metamta/view/__tests__/PhabricatorMetaMTAMailBodyTestCase.php @@ -1,47 +1,26 @@ assertEmail($expect, true); + $this->assertEmail($expect); } - public function testBodyRenderNoHerald() { - $expect = <<assertEmail($expect, false); - } - - private function assertEmail($expect, $herald_hints) { - $env = PhabricatorEnv::beginScopedEnv(); - $env->overrideEnvConfig('phabricator.production-uri', 'http://test.com/'); - $env->overrideEnvConfig('metamta.herald.show-hints', $herald_hints); - + private function assertEmail($expect) { $body = new PhabricatorMetaMTAMailBody(); $body->addRawSection('salmon'); $body->addTextSection('HEADER', "bass\ntrout\n"); - $body->addHeraldSection('/xscript/'); $this->assertEqual($expect, $body->render()); } } diff --git a/src/applications/multimeter/controller/MultimeterSampleController.php b/src/applications/multimeter/controller/MultimeterSampleController.php index 0023038185..da09641d22 100644 --- a/src/applications/multimeter/controller/MultimeterSampleController.php +++ b/src/applications/multimeter/controller/MultimeterSampleController.php @@ -1,351 +1,349 @@ getViewer(); $group_map = $this->getColumnMap(); $group = explode('.', $request->getStr('group')); $group = array_intersect($group, array_keys($group_map)); $group = array_fuse($group); if (empty($group['type'])) { $group['type'] = 'type'; } $now = PhabricatorTime::getNow(); $ago = ($now - phutil_units('24 hours in seconds')); $table = new MultimeterEvent(); $conn = $table->establishConnection('r'); $where = array(); $where[] = qsprintf( $conn, 'epoch >= %d AND epoch <= %d', $ago, $now); $with = array(); foreach ($group_map as $key => $column) { // Don't let non-admins filter by viewers, this feels a little too // invasive of privacy. if ($key == 'viewer') { if (!$viewer->getIsAdmin()) { continue; } } $with[$key] = $request->getStrList($key); if ($with[$key]) { $where[] = qsprintf( $conn, '%T IN (%Ls)', $column, $with[$key]); } } - $where = '('.implode(') AND (', $where).')'; - $data = queryfx_all( $conn, 'SELECT *, count(*) AS N, SUM(sampleRate * resourceCost) AS totalCost, SUM(sampleRate * resourceCost) / SUM(sampleRate) AS averageCost FROM %T - WHERE %Q - GROUP BY %Q + WHERE %LA + GROUP BY %LC ORDER BY totalCost DESC, MAX(id) DESC LIMIT 100', $table->getTableName(), $where, - implode(', ', array_select_keys($group_map, $group))); + array_select_keys($group_map, $group)); $this->loadDimensions($data); $phids = array(); foreach ($data as $row) { $viewer_name = $this->getViewerDimension($row['eventViewerID']) ->getName(); $viewer_phid = $this->getEventViewerPHID($viewer_name); if ($viewer_phid) { $phids[] = $viewer_phid; } } $handles = $viewer->loadHandles($phids); $rows = array(); foreach ($data as $row) { if ($row['N'] == 1) { $events_col = $row['id']; } else { $events_col = $this->renderGroupingLink( $group, 'id', pht('%s Event(s)', new PhutilNumber($row['N']))); } if (isset($group['request'])) { $request_col = $row['requestKey']; if (!$with['request']) { $request_col = $this->renderSelectionLink( 'request', $row['requestKey'], $request_col); } } else { $request_col = $this->renderGroupingLink($group, 'request'); } if (isset($group['viewer'])) { if ($viewer->getIsAdmin()) { $viewer_col = $this->getViewerDimension($row['eventViewerID']) ->getName(); $viewer_phid = $this->getEventViewerPHID($viewer_col); if ($viewer_phid) { $viewer_col = $handles[$viewer_phid]->getName(); } if (!$with['viewer']) { $viewer_col = $this->renderSelectionLink( 'viewer', $row['eventViewerID'], $viewer_col); } } else { $viewer_col = phutil_tag('em', array(), pht('(Masked)')); } } else { $viewer_col = $this->renderGroupingLink($group, 'viewer'); } if (isset($group['context'])) { $context_col = $this->getContextDimension($row['eventContextID']) ->getName(); if (!$with['context']) { $context_col = $this->renderSelectionLink( 'context', $row['eventContextID'], $context_col); } } else { $context_col = $this->renderGroupingLink($group, 'context'); } if (isset($group['host'])) { $host_col = $this->getHostDimension($row['eventHostID']) ->getName(); if (!$with['host']) { $host_col = $this->renderSelectionLink( 'host', $row['eventHostID'], $host_col); } } else { $host_col = $this->renderGroupingLink($group, 'host'); } if (isset($group['label'])) { $label_col = $this->getLabelDimension($row['eventLabelID']) ->getName(); if (!$with['label']) { $label_col = $this->renderSelectionLink( 'label', $row['eventLabelID'], $label_col); } } else { $label_col = $this->renderGroupingLink($group, 'label'); } if ($with['type']) { $type_col = MultimeterEvent::getEventTypeName($row['eventType']); } else { $type_col = $this->renderSelectionLink( 'type', $row['eventType'], MultimeterEvent::getEventTypeName($row['eventType'])); } $rows[] = array( $events_col, $request_col, $viewer_col, $context_col, $host_col, $type_col, $label_col, MultimeterEvent::formatResourceCost( $viewer, $row['eventType'], $row['averageCost']), MultimeterEvent::formatResourceCost( $viewer, $row['eventType'], $row['totalCost']), ($row['N'] == 1) ? $row['sampleRate'] : '-', phabricator_datetime($row['epoch'], $viewer), ); } $table = id(new AphrontTableView($rows)) ->setHeaders( array( pht('ID'), pht('Request'), pht('Viewer'), pht('Context'), pht('Host'), pht('Type'), pht('Label'), pht('Avg'), pht('Cost'), pht('Rate'), pht('Epoch'), )) ->setColumnClasses( array( null, null, null, null, null, null, 'wide', 'n', 'n', 'n', null, )); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Samples')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($table); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb( pht('Samples'), $this->getGroupURI(array(), true)); $crumbs->setBorder(true); $crumb_map = array( 'host' => pht('By Host'), 'context' => pht('By Context'), 'viewer' => pht('By Viewer'), 'request' => pht('By Request'), 'label' => pht('By Label'), 'id' => pht('By ID'), ); $parts = array(); foreach ($group as $item) { if ($item == 'type') { continue; } $parts[$item] = $item; $crumbs->addTextCrumb( idx($crumb_map, $item, $item), $this->getGroupURI($parts, true)); } $header = id(new PHUIHeaderView()) ->setHeader( pht( 'Samples (%s - %s)', phabricator_datetime($ago, $viewer), phabricator_datetime($now, $viewer))) ->setHeaderIcon('fa-motorcycle'); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter($box); return $this->newPage() ->setTitle(pht('Samples')) ->setCrumbs($crumbs) ->appendChild($view); } private function renderGroupingLink(array $group, $key, $name = null) { $group[] = $key; $uri = $this->getGroupURI($group); if ($name === null) { $name = pht('(All)'); } return phutil_tag( 'a', array( 'href' => $uri, 'style' => 'font-weight: bold', ), $name); } private function getGroupURI(array $group, $wipe = false) { unset($group['type']); $uri = clone $this->getRequest()->getRequestURI(); $group = implode('.', $group); if (!strlen($group)) { $group = null; } $uri->setQueryParam('group', $group); if ($wipe) { foreach ($this->getColumnMap() as $key => $column) { $uri->setQueryParam($key, null); } } return $uri; } private function renderSelectionLink($key, $value, $link_text) { $value = (array)$value; $uri = clone $this->getRequest()->getRequestURI(); $uri->setQueryParam($key, implode(',', $value)); return phutil_tag( 'a', array( 'href' => $uri, ), $link_text); } private function getColumnMap() { return array( 'type' => 'eventType', 'host' => 'eventHostID', 'context' => 'eventContextID', 'viewer' => 'eventViewerID', 'request' => 'requestKey', 'label' => 'eventLabelID', 'id' => 'id', ); } private function getEventViewerPHID($viewer_name) { if (!strncmp($viewer_name, 'user.', 5)) { return substr($viewer_name, 5); } return null; } } diff --git a/src/applications/oauthserver/query/PhabricatorOAuthServerClientQuery.php b/src/applications/oauthserver/query/PhabricatorOAuthServerClientQuery.php index b5da9ecd5f..46fc514824 100644 --- a/src/applications/oauthserver/query/PhabricatorOAuthServerClientQuery.php +++ b/src/applications/oauthserver/query/PhabricatorOAuthServerClientQuery.php @@ -1,73 +1,73 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withCreatorPHIDs(array $phids) { $this->creatorPHIDs = $phids; return $this; } protected function loadPage() { $table = new PhabricatorOAuthServerClient(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T client %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } if ($this->creatorPHIDs) { $where[] = qsprintf( - $conn_r, + $conn, 'creatorPHID IN (%Ls)', $this->creatorPHIDs); } - $where[] = $this->buildPagingClause($conn_r); + $where[] = $this->buildPagingClause($conn); - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } public function getQueryApplicationClass() { return 'PhabricatorOAuthServerApplication'; } } diff --git a/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php b/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php index d2eee5685e..044cb8beda 100644 --- a/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php +++ b/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php @@ -1,190 +1,190 @@ getViewer()); } protected function newObjectQuery() { return id(new PhabricatorOwnersPackageQuery()) ->needPaths(true); } protected function getObjectCreateTitleText($object) { return pht('Create New Package'); } protected function getObjectEditTitleText($object) { return pht('Edit Package: %s', $object->getName()); } protected function getObjectEditShortText($object) { return pht('Package %d', $object->getID()); } protected function getObjectCreateShortText() { return pht('Create Package'); } protected function getObjectName() { return pht('Package'); } protected function getObjectViewURI($object) { return $object->getURI(); } protected function buildCustomEditFields($object) { $paths_help = pht(<<setKey('name') ->setLabel(pht('Name')) ->setDescription(pht('Name of the package.')) ->setTransactionType( PhabricatorOwnersPackageNameTransaction::TRANSACTIONTYPE) ->setIsRequired(true) ->setValue($object->getName()), id(new PhabricatorDatasourceEditField()) ->setKey('owners') ->setLabel(pht('Owners')) ->setDescription(pht('Users and projects which own the package.')) ->setTransactionType( PhabricatorOwnersPackageOwnersTransaction::TRANSACTIONTYPE) ->setDatasource(new PhabricatorProjectOrUserDatasource()) ->setIsCopyable(true) ->setValue($object->getOwnerPHIDs()), id(new PhabricatorSelectEditField()) ->setKey('dominion') ->setLabel(pht('Dominion')) ->setDescription( pht('Change package dominion rules.')) ->setTransactionType( PhabricatorOwnersPackageDominionTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setValue($object->getDominion()) ->setOptions($dominion_map), id(new PhabricatorSelectEditField()) ->setKey('autoReview') ->setLabel(pht('Auto Review')) ->setDescription( pht( 'Automatically trigger reviews for commits affecting files in '. 'this package.')) ->setTransactionType( PhabricatorOwnersPackageAutoreviewTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setValue($object->getAutoReview()) ->setOptions($autoreview_map), id(new PhabricatorSelectEditField()) ->setKey('auditing') ->setLabel(pht('Auditing')) ->setDescription( pht( 'Automatically trigger audits for commits affecting files in '. 'this package.')) ->setTransactionType( PhabricatorOwnersPackageAuditingTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setValue($object->getAuditingEnabled()) ->setOptions( array( '' => pht('Disabled'), '1' => pht('Enabled'), )), id(new PhabricatorRemarkupEditField()) ->setKey('description') ->setLabel(pht('Description')) ->setDescription(pht('Human-readable description of the package.')) ->setTransactionType( PhabricatorOwnersPackageDescriptionTransaction::TRANSACTIONTYPE) ->setValue($object->getDescription()), id(new PhabricatorSelectEditField()) ->setKey('status') ->setLabel(pht('Status')) ->setDescription(pht('Archive or enable the package.')) ->setTransactionType( PhabricatorOwnersPackageStatusTransaction::TRANSACTIONTYPE) - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setValue($object->getStatus()) ->setOptions($object->getStatusNameMap()), id(new PhabricatorCheckboxesEditField()) ->setKey('ignored') ->setLabel(pht('Ignored Attributes')) ->setDescription(pht('Ignore paths with any of these attributes.')) ->setTransactionType( PhabricatorOwnersPackageIgnoredTransaction::TRANSACTIONTYPE) ->setValue(array_keys($object->getIgnoredPathAttributes())) ->setOptions( array( 'generated' => pht('Ignore generated files (review only).'), )), id(new PhabricatorConduitEditField()) ->setKey('paths.set') ->setLabel(pht('Paths')) - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setTransactionType( PhabricatorOwnersPackagePathsTransaction::TRANSACTIONTYPE) ->setConduitDescription( pht('Overwrite existing package paths with new paths.')) ->setConduitTypeDescription( pht('List of dictionaries, each describing a path.')) ->setConduitDocumentation($paths_help), ); } } diff --git a/src/applications/owners/query/PhabricatorOwnersPackageQuery.php b/src/applications/owners/query/PhabricatorOwnersPackageQuery.php index ff0665b344..6d6ccb2ed2 100644 --- a/src/applications/owners/query/PhabricatorOwnersPackageQuery.php +++ b/src/applications/owners/query/PhabricatorOwnersPackageQuery.php @@ -1,452 +1,452 @@ ownerPHIDs = $phids; return $this; } /** * Query owner authority. This will expand authorities, so a user PHID will * match both packages they own directly and packages owned by a project they * are a member of. */ public function withAuthorityPHIDs(array $phids) { $this->authorityPHIDs = $phids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withIDs(array $ids) { $this->ids = $ids; return $this; } public function withRepositoryPHIDs(array $phids) { $this->repositoryPHIDs = $phids; return $this; } public function withPaths(array $paths) { $this->paths = $paths; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } public function withControl($repository_phid, array $paths) { if (empty($this->controlMap[$repository_phid])) { $this->controlMap[$repository_phid] = array(); } foreach ($paths as $path) { $path = (string)$path; $this->controlMap[$repository_phid][$path] = $path; } // We need to load paths to execute control queries. $this->needPaths = true; return $this; } public function withNameNgrams($ngrams) { return $this->withNgramsConstraint( new PhabricatorOwnersPackageNameNgrams(), $ngrams); } public function needPaths($need_paths) { $this->needPaths = $need_paths; return $this; } public function newResultObject() { return new PhabricatorOwnersPackage(); } protected function willExecute() { $this->controlResults = array(); } protected function loadPage() { return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $packages) { $package_ids = mpull($packages, 'getID'); $owners = id(new PhabricatorOwnersOwner())->loadAllWhere( 'packageID IN (%Ld)', $package_ids); $owners = mgroup($owners, 'getPackageID'); foreach ($packages as $package) { $package->attachOwners(idx($owners, $package->getID(), array())); } return $packages; } protected function didFilterPage(array $packages) { $package_ids = mpull($packages, 'getID'); if ($this->needPaths) { $paths = id(new PhabricatorOwnersPath())->loadAllWhere( 'packageID IN (%Ld)', $package_ids); $paths = mgroup($paths, 'getPackageID'); foreach ($packages as $package) { $package->attachPaths(idx($paths, $package->getID(), array())); } } if ($this->controlMap) { foreach ($packages as $package) { // If this package is archived, it's no longer a controlling package // for any path. In particular, it can not force active packages with // weak dominion to give up control. if ($package->isArchived()) { continue; } $this->controlResults[$package->getID()] = $package; } } return $packages; } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { $joins = parent::buildJoinClauseParts($conn); if ($this->shouldJoinOwnersTable()) { $joins[] = qsprintf( $conn, 'JOIN %T o ON o.packageID = p.id', id(new PhabricatorOwnersOwner())->getTableName()); } if ($this->shouldJoinPathTable()) { $joins[] = qsprintf( $conn, 'JOIN %T rpath ON rpath.packageID = p.id', id(new PhabricatorOwnersPath())->getTableName()); } return $joins; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->phids !== null) { $where[] = qsprintf( $conn, 'p.phid IN (%Ls)', $this->phids); } if ($this->ids !== null) { $where[] = qsprintf( $conn, 'p.id IN (%Ld)', $this->ids); } if ($this->repositoryPHIDs !== null) { $where[] = qsprintf( $conn, 'rpath.repositoryPHID IN (%Ls)', $this->repositoryPHIDs); } if ($this->authorityPHIDs !== null) { $authority_phids = $this->expandAuthority($this->authorityPHIDs); $where[] = qsprintf( $conn, 'o.userPHID IN (%Ls)', $authority_phids); } if ($this->ownerPHIDs !== null) { $where[] = qsprintf( $conn, 'o.userPHID IN (%Ls)', $this->ownerPHIDs); } if ($this->paths !== null) { $where[] = qsprintf( $conn, 'rpath.pathIndex IN (%Ls)', $this->getFragmentIndexesForPaths($this->paths)); } if ($this->statuses !== null) { $where[] = qsprintf( $conn, 'p.status IN (%Ls)', $this->statuses); } if ($this->controlMap) { $clauses = array(); foreach ($this->controlMap as $repository_phid => $paths) { $indexes = $this->getFragmentIndexesForPaths($paths); $clauses[] = qsprintf( $conn, '(rpath.repositoryPHID = %s AND rpath.pathIndex IN (%Ls))', $repository_phid, $indexes); } - $where[] = implode(' OR ', $clauses); + $where[] = qsprintf($conn, '%LO', $clauses); } return $where; } protected function shouldGroupQueryResultRows() { if ($this->shouldJoinOwnersTable()) { return true; } if ($this->shouldJoinPathTable()) { return true; } return parent::shouldGroupQueryResultRows(); } public function getBuiltinOrders() { return array( 'name' => array( 'vector' => array('name'), 'name' => pht('Name'), ), ) + parent::getBuiltinOrders(); } public function getOrderableColumns() { return parent::getOrderableColumns() + array( 'name' => array( 'table' => $this->getPrimaryTableAlias(), 'column' => 'name', 'type' => 'string', 'unique' => true, 'reverse' => true, ), ); } protected function getPagingValueMap($cursor, array $keys) { $package = $this->loadCursorObject($cursor); return array( 'id' => $package->getID(), 'name' => $package->getName(), ); } public function getQueryApplicationClass() { return 'PhabricatorOwnersApplication'; } protected function getPrimaryTableAlias() { return 'p'; } private function shouldJoinOwnersTable() { if ($this->ownerPHIDs !== null) { return true; } if ($this->authorityPHIDs !== null) { return true; } return false; } private function shouldJoinPathTable() { if ($this->repositoryPHIDs !== null) { return true; } if ($this->paths !== null) { return true; } if ($this->controlMap) { return true; } return false; } private function expandAuthority(array $phids) { $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withMemberPHIDs($phids) ->execute(); $project_phids = mpull($projects, 'getPHID'); return array_fuse($phids) + array_fuse($project_phids); } private function getFragmentsForPaths(array $paths) { $fragments = array(); foreach ($paths as $path) { foreach (PhabricatorOwnersPackage::splitPath($path) as $fragment) { $fragments[$fragment] = $fragment; } } return $fragments; } private function getFragmentIndexesForPaths(array $paths) { $indexes = array(); foreach ($this->getFragmentsForPaths($paths) as $fragment) { $indexes[] = PhabricatorHash::digestForIndex($fragment); } return $indexes; } /* -( Path Control )------------------------------------------------------- */ /** * Get a list of all packages which control a path or its parent directories, * ordered from weakest to strongest. * * The first package has the most specific claim on the path; the last * package has the most general claim. Multiple packages may have claims of * equal strength, so this ordering is primarily one of usability and * convenience. * * @return list List of controlling packages. */ public function getControllingPackagesForPath( $repository_phid, $path, $ignore_dominion = false) { $path = (string)$path; if (!isset($this->controlMap[$repository_phid][$path])) { throw new PhutilInvalidStateException('withControl'); } if ($this->controlResults === null) { throw new PhutilInvalidStateException('execute'); } $packages = $this->controlResults; $weak_dominion = PhabricatorOwnersPackage::DOMINION_WEAK; $path_fragments = PhabricatorOwnersPackage::splitPath($path); $fragment_count = count($path_fragments); $matches = array(); foreach ($packages as $package_id => $package) { $best_match = null; $include = false; $repository_paths = $package->getPathsForRepository($repository_phid); foreach ($repository_paths as $package_path) { $strength = $package_path->getPathMatchStrength( $path_fragments, $fragment_count); if ($strength > $best_match) { $best_match = $strength; $include = !$package_path->getExcluded(); } } 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' => $is_weak, 'package' => $package, ); } } // At each strength level, drop weak packages if there are also strong // packages of the same strength. $strength_map = igroup($matches, 'strength'); foreach ($strength_map as $strength => $package_list) { $any_strong = false; foreach ($package_list as $package_id => $package) { if (!$package['weak']) { $any_strong = true; break; } } if ($any_strong) { foreach ($package_list as $package_id => $package) { if ($package['weak']) { unset($matches[$package_id]); } } } } $matches = isort($matches, 'strength'); $matches = array_reverse($matches); $strongest = null; foreach ($matches as $package_id => $match) { if ($strongest === null) { $strongest = $match['strength']; } if ($match['strength'] === $strongest) { continue; } if ($match['weak']) { unset($matches[$package_id]); } } return array_values(ipull($matches, 'package')); } } diff --git a/src/applications/packages/query/PhabricatorPackagesQuery.php b/src/applications/packages/query/PhabricatorPackagesQuery.php index 58b90599eb..e7e882bdad 100644 --- a/src/applications/packages/query/PhabricatorPackagesQuery.php +++ b/src/applications/packages/query/PhabricatorPackagesQuery.php @@ -1,38 +1,38 @@ getViewer()); } protected function newObjectQuery() { return id(new PhabricatorPasteQuery()) ->needRawContent(true); } protected function getObjectCreateTitleText($object) { return pht('Create New Paste'); } protected function getObjectEditTitleText($object) { return pht('Edit Paste: %s', $object->getTitle()); } protected function getObjectEditShortText($object) { return $object->getMonogram(); } protected function getObjectCreateShortText() { return pht('Create Paste'); } protected function getObjectName() { return pht('Paste'); } protected function getCommentViewHeaderText($object) { return pht('Eat Paste'); } protected function getCommentViewButtonText($object) { return pht('Nom Nom Nom Nom Nom'); } protected function getObjectViewURI($object) { return '/P'.$object->getID(); } protected function buildCustomEditFields($object) { return array( id(new PhabricatorTextEditField()) ->setKey('title') ->setLabel(pht('Title')) ->setTransactionType(PhabricatorPasteTitleTransaction::TRANSACTIONTYPE) ->setDescription(pht('The title of the paste.')) ->setConduitDescription(pht('Retitle the paste.')) ->setConduitTypeDescription(pht('New paste title.')) ->setValue($object->getTitle()), id(new PhabricatorDatasourceEditField()) ->setKey('language') ->setLabel(pht('Language')) ->setTransactionType( PhabricatorPasteLanguageTransaction::TRANSACTIONTYPE) ->setAliases(array('lang')) ->setIsCopyable(true) ->setDatasource(new PasteLanguageSelectDatasource()) ->setDescription( pht( 'Language used for syntax highlighting. By default, inferred '. 'from the title.')) ->setConduitDescription( pht('Change language used for syntax highlighting.')) ->setConduitTypeDescription(pht('New highlighting language.')) ->setSingleValue($object->getLanguage()), id(new PhabricatorTextAreaEditField()) ->setKey('text') ->setLabel(pht('Text')) ->setTransactionType( PhabricatorPasteContentTransaction::TRANSACTIONTYPE) ->setMonospaced(true) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setDescription(pht('The main body text of the paste.')) ->setConduitDescription(pht('Change the paste content.')) ->setConduitTypeDescription(pht('New body content.')) ->setValue($object->getRawContent()), id(new PhabricatorSelectEditField()) ->setKey('status') ->setLabel(pht('Status')) ->setTransactionType( PhabricatorPasteStatusTransaction::TRANSACTIONTYPE) - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setOptions(PhabricatorPaste::getStatusNameMap()) ->setDescription(pht('Active or archived status.')) ->setConduitDescription(pht('Active or archive the paste.')) ->setConduitTypeDescription(pht('New paste status constant.')) ->setValue($object->getStatus()), ); } } diff --git a/src/applications/people/editor/PhabricatorUserEditEngine.php b/src/applications/people/editor/PhabricatorUserEditEngine.php index c547426b12..c4c1abf1e3 100644 --- a/src/applications/people/editor/PhabricatorUserEditEngine.php +++ b/src/applications/people/editor/PhabricatorUserEditEngine.php @@ -1,81 +1,81 @@ getUsername()); } protected function getObjectEditShortText($object) { return $object->getMonogram(); } protected function getObjectCreateShortText() { return pht('Create User'); } protected function getObjectName() { return pht('User'); } protected function getObjectViewURI($object) { return $object->getURI(); } protected function getCreateNewObjectPolicy() { // At least for now, forbid creating new users via EditEngine. This is // primarily enforcing that "user.edit" can not create users via the API. return PhabricatorPolicies::POLICY_NOONE; } protected function buildCustomEditFields($object) { return array( id(new PhabricatorBoolEditField()) ->setKey('disabled') ->setOptions(pht('Active'), pht('Disabled')) ->setLabel(pht('Disabled')) ->setDescription(pht('Disable the user.')) ->setTransactionType(PhabricatorUserDisableTransaction::TRANSACTIONTYPE) - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setConduitDescription(pht('Disable or enable the user.')) ->setConduitTypeDescription(pht('True to disable the user.')) ->setValue($object->getIsDisabled()), ); } } diff --git a/src/applications/people/query/PhabricatorPeopleQuery.php b/src/applications/people/query/PhabricatorPeopleQuery.php index 35ff480833..131dc7c588 100644 --- a/src/applications/people/query/PhabricatorPeopleQuery.php +++ b/src/applications/people/query/PhabricatorPeopleQuery.php @@ -1,642 +1,642 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withEmails(array $emails) { $this->emails = $emails; return $this; } public function withRealnames(array $realnames) { $this->realnames = $realnames; return $this; } public function withUsernames(array $usernames) { $this->usernames = $usernames; return $this; } public function withDateCreatedBefore($date_created_before) { $this->dateCreatedBefore = $date_created_before; return $this; } public function withDateCreatedAfter($date_created_after) { $this->dateCreatedAfter = $date_created_after; return $this; } public function withIsAdmin($admin) { $this->isAdmin = $admin; return $this; } public function withIsSystemAgent($system_agent) { $this->isSystemAgent = $system_agent; return $this; } public function withIsMailingList($mailing_list) { $this->isMailingList = $mailing_list; return $this; } public function withIsDisabled($disabled) { $this->isDisabled = $disabled; return $this; } public function withIsApproved($approved) { $this->isApproved = $approved; return $this; } public function withNameLike($like) { $this->nameLike = $like; return $this; } public function withNameTokens(array $tokens) { $this->nameTokens = array_values($tokens); return $this; } public function withNamePrefixes(array $prefixes) { $this->namePrefixes = $prefixes; return $this; } public function withIsEnrolledInMultiFactor($enrolled) { $this->isEnrolledInMultiFactor = $enrolled; return $this; } public function needPrimaryEmail($need) { $this->needPrimaryEmail = $need; return $this; } public function needProfile($need) { $this->needProfile = $need; return $this; } public function needProfileImage($need) { $cache_key = PhabricatorUserProfileImageCacheType::KEY_URI; if ($need) { $this->cacheKeys[$cache_key] = true; } else { unset($this->cacheKeys[$cache_key]); } return $this; } public function needAvailability($need) { $this->needAvailability = $need; return $this; } public function needUserSettings($need) { $cache_key = PhabricatorUserPreferencesCacheType::KEY_PREFERENCES; if ($need) { $this->cacheKeys[$cache_key] = true; } else { unset($this->cacheKeys[$cache_key]); } return $this; } public function needBadgeAwards($need) { $cache_key = PhabricatorUserBadgesCacheType::KEY_BADGES; if ($need) { $this->cacheKeys[$cache_key] = true; } else { unset($this->cacheKeys[$cache_key]); } return $this; } public function newResultObject() { return new PhabricatorUser(); } protected function loadPage() { $table = new PhabricatorUser(); $data = $this->loadStandardPageRows($table); if ($this->needPrimaryEmail) { $table->putInSet(new LiskDAOSet()); } return $table->loadAllFromArray($data); } protected function didFilterPage(array $users) { if ($this->needProfile) { $user_list = mpull($users, null, 'getPHID'); $profiles = new PhabricatorUserProfile(); $profiles = $profiles->loadAllWhere( 'userPHID IN (%Ls)', array_keys($user_list)); $profiles = mpull($profiles, null, 'getUserPHID'); foreach ($user_list as $user_phid => $user) { $profile = idx($profiles, $user_phid); if (!$profile) { $profile = PhabricatorUserProfile::initializeNewProfile($user); } $user->attachUserProfile($profile); } } if ($this->needAvailability) { $rebuild = array(); foreach ($users as $user) { $cache = $user->getAvailabilityCache(); if ($cache !== null) { $user->attachAvailability($cache); } else { $rebuild[] = $user; } } if ($rebuild) { $this->rebuildAvailabilityCache($rebuild); } } $this->fillUserCaches($users); return $users; } protected function shouldGroupQueryResultRows() { if ($this->nameTokens) { return true; } return parent::shouldGroupQueryResultRows(); } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { $joins = parent::buildJoinClauseParts($conn); if ($this->emails) { $email_table = new PhabricatorUserEmail(); $joins[] = qsprintf( $conn, 'JOIN %T email ON email.userPHID = user.PHID', $email_table->getTableName()); } if ($this->nameTokens) { foreach ($this->nameTokens as $key => $token) { $token_table = 'token_'.$key; $joins[] = qsprintf( $conn, 'JOIN %T %T ON %T.userID = user.id AND %T.token LIKE %>', PhabricatorUser::NAMETOKEN_TABLE, $token_table, $token_table, $token_table, $token); } } return $joins; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->usernames !== null) { $where[] = qsprintf( $conn, 'user.userName IN (%Ls)', $this->usernames); } if ($this->namePrefixes) { $parts = array(); foreach ($this->namePrefixes as $name_prefix) { $parts[] = qsprintf( $conn, 'user.username LIKE %>', $name_prefix); } - $where[] = '('.implode(' OR ', $parts).')'; + $where[] = qsprintf($conn, '%LO', $parts); } if ($this->emails !== null) { $where[] = qsprintf( $conn, 'email.address IN (%Ls)', $this->emails); } if ($this->realnames !== null) { $where[] = qsprintf( $conn, 'user.realName IN (%Ls)', $this->realnames); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'user.phid IN (%Ls)', $this->phids); } if ($this->ids !== null) { $where[] = qsprintf( $conn, 'user.id IN (%Ld)', $this->ids); } if ($this->dateCreatedAfter) { $where[] = qsprintf( $conn, 'user.dateCreated >= %d', $this->dateCreatedAfter); } if ($this->dateCreatedBefore) { $where[] = qsprintf( $conn, 'user.dateCreated <= %d', $this->dateCreatedBefore); } if ($this->isAdmin !== null) { $where[] = qsprintf( $conn, 'user.isAdmin = %d', (int)$this->isAdmin); } if ($this->isDisabled !== null) { $where[] = qsprintf( $conn, 'user.isDisabled = %d', (int)$this->isDisabled); } if ($this->isApproved !== null) { $where[] = qsprintf( $conn, 'user.isApproved = %d', (int)$this->isApproved); } if ($this->isSystemAgent !== null) { $where[] = qsprintf( $conn, 'user.isSystemAgent = %d', (int)$this->isSystemAgent); } if ($this->isMailingList !== null) { $where[] = qsprintf( $conn, 'user.isMailingList = %d', (int)$this->isMailingList); } if (strlen($this->nameLike)) { $where[] = qsprintf( $conn, 'user.username LIKE %~ OR user.realname LIKE %~', $this->nameLike, $this->nameLike); } if ($this->isEnrolledInMultiFactor !== null) { $where[] = qsprintf( $conn, 'user.isEnrolledInMultiFactor = %d', (int)$this->isEnrolledInMultiFactor); } return $where; } protected function getPrimaryTableAlias() { return 'user'; } public function getQueryApplicationClass() { return 'PhabricatorPeopleApplication'; } public function getOrderableColumns() { return parent::getOrderableColumns() + array( 'username' => array( 'table' => 'user', 'column' => 'username', 'type' => 'string', 'reverse' => true, 'unique' => true, ), ); } protected function getPagingValueMap($cursor, array $keys) { $user = $this->loadCursorObject($cursor); return array( 'id' => $user->getID(), 'username' => $user->getUsername(), ); } private function rebuildAvailabilityCache(array $rebuild) { $rebuild = mpull($rebuild, null, 'getPHID'); // Limit the window we look at because far-future events are largely // irrelevant and this makes the cache cheaper to build and allows it to // self-heal over time. $min_range = PhabricatorTime::getNow(); $max_range = $min_range + phutil_units('72 hours in seconds'); // NOTE: We don't need to generate ghosts here, because we only care if // the user is attending, and you can't attend a ghost event: RSVP'ing // to it creates a real event. $events = id(new PhabricatorCalendarEventQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withInvitedPHIDs(array_keys($rebuild)) ->withIsCancelled(false) ->withDateRange($min_range, $max_range) ->execute(); // Group all the events by invited user. Only examine events that users // are actually attending. $map = array(); $invitee_map = array(); foreach ($events as $event) { foreach ($event->getInvitees() as $invitee) { if (!$invitee->isAttending()) { continue; } // If the user is set to "Available" for this event, don't consider it // when computing their away status. if (!$invitee->getDisplayAvailability($event)) { continue; } $invitee_phid = $invitee->getInviteePHID(); if (!isset($rebuild[$invitee_phid])) { continue; } $map[$invitee_phid][] = $event; $event_phid = $event->getPHID(); $invitee_map[$invitee_phid][$event_phid] = $invitee; } } // We need to load these users' timezone settings to figure out their // availability if they're attending all-day events. $this->needUserSettings(true); $this->fillUserCaches($rebuild); foreach ($rebuild as $phid => $user) { $events = idx($map, $phid, array()); // We loaded events with the omnipotent user, but want to shift them // into the user's timezone before building the cache because they will // be unavailable during their own local day. foreach ($events as $event) { $event->applyViewerTimezone($user); } $cursor = $min_range; $next_event = null; if ($events) { // Find the next time when the user has no meetings. If we move forward // because of an event, we check again for events after that one ends. while (true) { foreach ($events as $event) { $from = $event->getStartDateTimeEpochForCache(); $to = $event->getEndDateTimeEpochForCache(); if (($from <= $cursor) && ($to > $cursor)) { $cursor = $to; if (!$next_event) { $next_event = $event; } continue 2; } } break; } } if ($cursor > $min_range) { $invitee = $invitee_map[$phid][$next_event->getPHID()]; $availability_type = $invitee->getDisplayAvailability($next_event); $availability = array( 'until' => $cursor, 'eventPHID' => $next_event->getPHID(), 'availability' => $availability_type, ); // We only cache this availability until the end of the current event, // since the event PHID (and possibly the availability type) are only // valid for that long. // NOTE: This doesn't handle overlapping events with the greatest // possible care. In theory, if you're attending multiple events // simultaneously we should accommodate that. However, it's complex // to compute, rare, and probably not confusing most of the time. $availability_ttl = $next_event->getEndDateTimeEpochForCache(); } else { $availability = array( 'until' => null, 'eventPHID' => null, 'availability' => null, ); // Cache that the user is available until the next event they are // invited to starts. $availability_ttl = $max_range; foreach ($events as $event) { $from = $event->getStartDateTimeEpochForCache(); if ($from > $cursor) { $availability_ttl = min($from, $availability_ttl); } } } // Never TTL the cache to longer than the maximum range we examined. $availability_ttl = min($availability_ttl, $max_range); $user->writeAvailabilityCache($availability, $availability_ttl); $user->attachAvailability($availability); } } private function fillUserCaches(array $users) { if (!$this->cacheKeys) { return; } $user_map = mpull($users, null, 'getPHID'); $keys = array_keys($this->cacheKeys); $hashes = array(); foreach ($keys as $key) { $hashes[] = PhabricatorHash::digestForIndex($key); } $types = PhabricatorUserCacheType::getAllCacheTypes(); // First, pull any available caches. If we wanted to be particularly clever // we could do this with JOINs in the main query. $cache_table = new PhabricatorUserCache(); $cache_conn = $cache_table->establishConnection('r'); $cache_data = queryfx_all( $cache_conn, 'SELECT cacheKey, userPHID, cacheData, cacheType FROM %T WHERE cacheIndex IN (%Ls) AND userPHID IN (%Ls)', $cache_table->getTableName(), $hashes, array_keys($user_map)); $skip_validation = array(); // After we read caches from the database, discard any which have data that // invalid or out of date. This allows cache types to implement TTLs or // versions instead of or in addition to explicit cache clears. foreach ($cache_data as $row_key => $row) { $cache_type = $row['cacheType']; if (isset($skip_validation[$cache_type])) { continue; } if (empty($types[$cache_type])) { unset($cache_data[$row_key]); continue; } $type = $types[$cache_type]; if (!$type->shouldValidateRawCacheData()) { $skip_validation[$cache_type] = true; continue; } $user = $user_map[$row['userPHID']]; $raw_data = $row['cacheData']; if (!$type->isRawCacheDataValid($user, $row['cacheKey'], $raw_data)) { unset($cache_data[$row_key]); continue; } } $need = array(); $cache_data = igroup($cache_data, 'userPHID'); foreach ($user_map as $user_phid => $user) { $raw_rows = idx($cache_data, $user_phid, array()); $raw_data = ipull($raw_rows, 'cacheData', 'cacheKey'); foreach ($keys as $key) { if (isset($raw_data[$key]) || array_key_exists($key, $raw_data)) { continue; } $need[$key][$user_phid] = $user; } $user->attachRawCacheData($raw_data); } // If we missed any cache values, bulk-construct them now. This is // usually much cheaper than generating them on-demand for each user // record. if (!$need) { return; } $writes = array(); foreach ($need as $cache_key => $need_users) { $type = PhabricatorUserCacheType::getCacheTypeForKey($cache_key); if (!$type) { continue; } $data = $type->newValueForUsers($cache_key, $need_users); foreach ($data as $user_phid => $raw_value) { $data[$user_phid] = $raw_value; $writes[] = array( 'userPHID' => $user_phid, 'key' => $cache_key, 'type' => $type, 'value' => $raw_value, ); } foreach ($need_users as $user_phid => $user) { if (isset($data[$user_phid]) || array_key_exists($user_phid, $data)) { $user->attachRawCacheData( array( $cache_key => $data[$user_phid], )); } } } PhabricatorUserCache::writeCaches($writes); } } diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index e2a0f9a2a2..651d1b75a6 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -1,1687 +1,1690 @@ isAdmin; case 'isDisabled': return (bool)$this->isDisabled; case 'isSystemAgent': return (bool)$this->isSystemAgent; case 'isMailingList': return (bool)$this->isMailingList; case 'isEmailVerified': return (bool)$this->isEmailVerified; case 'isApproved': return (bool)$this->isApproved; default: return parent::readField($field); } } /** * Is this a live account which has passed required approvals? Returns true * if this is an enabled, verified (if required), approved (if required) * account, and false otherwise. * * @return bool True if this is a standard, usable account. */ public function isUserActivated() { if (!$this->isLoggedIn()) { return false; } if ($this->isOmnipotent()) { return true; } if ($this->getIsDisabled()) { return false; } if (!$this->getIsApproved()) { return false; } if (PhabricatorUserEmail::isEmailVerificationRequired()) { if (!$this->getIsEmailVerified()) { return false; } } return true; } /** * Is this a user who we can reasonably expect to respond to requests? * * This is used to provide a grey "disabled/unresponsive" dot cue when * rendering handles and tags, so it isn't a surprise if you get ignored * when you ask things of users who will not receive notifications or could * not respond to them (because they are disabled, unapproved, do not have * verified email addresses, etc). * * @return bool True if this user can receive and respond to requests from * other humans. */ public function isResponsive() { if (!$this->isUserActivated()) { return false; } if (!$this->getIsEmailVerified()) { return false; } return true; } public function canEstablishWebSessions() { if ($this->getIsMailingList()) { return false; } if ($this->getIsSystemAgent()) { return false; } return true; } public function canEstablishAPISessions() { if ($this->getIsDisabled()) { return false; } // Intracluster requests are permitted even if the user is logged out: // in particular, public users are allowed to issue intracluster requests // when browsing Diffusion. if (PhabricatorEnv::isClusterRemoteAddress()) { if (!$this->isLoggedIn()) { return true; } } if (!$this->isUserActivated()) { return false; } if ($this->getIsMailingList()) { return false; } return true; } public function canEstablishSSHSessions() { if (!$this->isUserActivated()) { return false; } if ($this->getIsMailingList()) { return false; } return true; } /** * Returns `true` if this is a standard user who is logged in. Returns `false` * for logged out, anonymous, or external users. * * @return bool `true` if the user is a standard user who is logged in with * a normal session. */ public function getIsStandardUser() { $type_user = PhabricatorPeopleUserPHIDType::TYPECONST; return $this->getPHID() && (phid_get_type($this->getPHID()) == $type_user); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'userName' => 'sort64', 'realName' => 'text128', 'profileImagePHID' => 'phid?', 'conduitCertificate' => 'text255', 'isSystemAgent' => 'bool', 'isMailingList' => 'bool', 'isDisabled' => 'bool', 'isAdmin' => 'bool', 'isEmailVerified' => 'uint32', 'isApproved' => 'uint32', 'accountSecret' => 'bytes64', 'isEnrolledInMultiFactor' => 'bool', 'availabilityCache' => 'text255?', 'availabilityCacheTTL' => 'uint32?', 'defaultProfileImagePHID' => 'phid?', 'defaultProfileImageVersion' => 'text64?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'userName' => array( 'columns' => array('userName'), 'unique' => true, ), 'realName' => array( 'columns' => array('realName'), ), 'key_approved' => array( 'columns' => array('isApproved'), ), ), self::CONFIG_NO_MUTATE => array( 'availabilityCache' => true, 'availabilityCacheTTL' => true, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPeopleUserPHIDType::TYPECONST); } public function getMonogram() { return '@'.$this->getUsername(); } public function isLoggedIn() { return !($this->getPHID() === null); } public function saveWithoutIndex() { return parent::save(); } public function save() { if (!$this->getConduitCertificate()) { $this->setConduitCertificate($this->generateConduitCertificate()); } if (!strlen($this->getAccountSecret())) { $this->setAccountSecret(Filesystem::readRandomCharacters(64)); } $result = $this->saveWithoutIndex(); if ($this->profile) { $this->profile->save(); } $this->updateNameTokens(); PhabricatorSearchWorker::queueDocumentForIndexing($this->getPHID()); return $result; } public function attachSession(PhabricatorAuthSession $session) { $this->session = $session; return $this; } public function getSession() { return $this->assertAttached($this->session); } public function hasSession() { return ($this->session !== self::ATTACHABLE); } public function hasHighSecuritySession() { if (!$this->hasSession()) { return false; } return $this->getSession()->isHighSecuritySession(); } private function generateConduitCertificate() { return Filesystem::readRandomCharacters(255); } const CSRF_CYCLE_FREQUENCY = 3600; const CSRF_SALT_LENGTH = 8; const CSRF_TOKEN_LENGTH = 16; const CSRF_BREACH_PREFIX = 'B@'; const EMAIL_CYCLE_FREQUENCY = 86400; const EMAIL_TOKEN_LENGTH = 24; private function getRawCSRFToken($offset = 0) { return $this->generateToken( time() + (self::CSRF_CYCLE_FREQUENCY * $offset), self::CSRF_CYCLE_FREQUENCY, PhabricatorEnv::getEnvConfig('phabricator.csrf-key'), self::CSRF_TOKEN_LENGTH); } public function getCSRFToken() { if ($this->isOmnipotent()) { // We may end up here when called from the daemons. The omnipotent user // has no meaningful CSRF token, so just return `null`. return null; } if ($this->csrfSalt === null) { $this->csrfSalt = Filesystem::readRandomCharacters( self::CSRF_SALT_LENGTH); } $salt = $this->csrfSalt; // Generate a token hash to mitigate BREACH attacks against SSL. See // discussion in T3684. $token = $this->getRawCSRFToken(); $hash = PhabricatorHash::weakDigest($token, $salt); return self::CSRF_BREACH_PREFIX.$salt.substr( $hash, 0, self::CSRF_TOKEN_LENGTH); } public function validateCSRFToken($token) { // We expect a BREACH-mitigating token. See T3684. $breach_prefix = self::CSRF_BREACH_PREFIX; $breach_prelen = strlen($breach_prefix); if (strncmp($token, $breach_prefix, $breach_prelen) !== 0) { return false; } $salt = substr($token, $breach_prelen, self::CSRF_SALT_LENGTH); $token = substr($token, $breach_prelen + self::CSRF_SALT_LENGTH); // When the user posts a form, we check that it contains a valid CSRF token. // Tokens cycle each hour (every CSRF_CYCLE_FREQUENCY seconds) and we accept // either the current token, the next token (users can submit a "future" // token if you have two web frontends that have some clock skew) or any of // the last 6 tokens. This means that pages are valid for up to 7 hours. // There is also some Javascript which periodically refreshes the CSRF // tokens on each page, so theoretically pages should be valid indefinitely. // However, this code may fail to run (if the user loses their internet // connection, or there's a JS problem, or they don't have JS enabled). // Choosing the size of the window in which we accept old CSRF tokens is // an issue of balancing concerns between security and usability. We could // choose a very narrow (e.g., 1-hour) window to reduce vulnerability to // attacks using captured CSRF tokens, but it's also more likely that real // users will be affected by this, e.g. if they close their laptop for an // hour, open it back up, and try to submit a form before the CSRF refresh // can kick in. Since the user experience of submitting a form with expired // CSRF is often quite bad (you basically lose data, or it's a big pain to // recover at least) and I believe we gain little additional protection // by keeping the window very short (the overwhelming value here is in // preventing blind attacks, and most attacks which can capture CSRF tokens // can also just capture authentication information [sniffing networks] // or act as the user [xss]) the 7 hour default seems like a reasonable // balance. Other major platforms have much longer CSRF token lifetimes, // like Rails (session duration) and Django (forever), which suggests this // is a reasonable analysis. $csrf_window = 6; for ($ii = -$csrf_window; $ii <= 1; $ii++) { $valid = $this->getRawCSRFToken($ii); $digest = PhabricatorHash::weakDigest($valid, $salt); $digest = substr($digest, 0, self::CSRF_TOKEN_LENGTH); if (phutil_hashes_are_identical($digest, $token)) { return true; } } return false; } private function generateToken($epoch, $frequency, $key, $len) { if ($this->getPHID()) { $vec = $this->getPHID().$this->getAccountSecret(); } else { $vec = $this->getAlternateCSRFString(); } if ($this->hasSession()) { $vec = $vec.$this->getSession()->getSessionKey(); } $time_block = floor($epoch / $frequency); $vec = $vec.$key.$time_block; return substr(PhabricatorHash::weakDigest($vec), 0, $len); } public function getUserProfile() { return $this->assertAttached($this->profile); } public function attachUserProfile(PhabricatorUserProfile $profile) { $this->profile = $profile; return $this; } public function loadUserProfile() { if ($this->profile) { return $this->profile; } $profile_dao = new PhabricatorUserProfile(); $this->profile = $profile_dao->loadOneWhere('userPHID = %s', $this->getPHID()); if (!$this->profile) { $this->profile = PhabricatorUserProfile::initializeNewProfile($this); } return $this->profile; } public function loadPrimaryEmailAddress() { $email = $this->loadPrimaryEmail(); if (!$email) { throw new Exception(pht('User has no primary email address!')); } return $email->getAddress(); } public function loadPrimaryEmail() { + $email = new PhabricatorUserEmail(); + $conn = $email->establishConnection('r'); + return $this->loadOneRelative( - new PhabricatorUserEmail(), + $email, 'userPHID', 'getPHID', - '(isPrimary = 1)'); + qsprintf($conn, '(isPrimary = 1)')); } /* -( Settings )----------------------------------------------------------- */ public function getUserSetting($key) { // NOTE: We store available keys and cached values separately to make it // faster to check for `null` in the cache, which is common. if (isset($this->settingCacheKeys[$key])) { return $this->settingCache[$key]; } $settings_key = PhabricatorUserPreferencesCacheType::KEY_PREFERENCES; if ($this->getPHID()) { $settings = $this->requireCacheData($settings_key); } else { $settings = $this->loadGlobalSettings(); } if (array_key_exists($key, $settings)) { $value = $settings[$key]; return $this->writeUserSettingCache($key, $value); } $cache = PhabricatorCaches::getRuntimeCache(); $cache_key = "settings.defaults({$key})"; $cache_map = $cache->getKeys(array($cache_key)); if ($cache_map) { $value = $cache_map[$cache_key]; } else { $defaults = PhabricatorSetting::getAllSettings(); if (isset($defaults[$key])) { $value = id(clone $defaults[$key]) ->setViewer($this) ->getSettingDefaultValue(); } else { $value = null; } $cache->setKey($cache_key, $value); } return $this->writeUserSettingCache($key, $value); } /** * Test if a given setting is set to a particular value. * * @param const Setting key. * @param wild Value to compare. * @return bool True if the setting has the specified value. * @task settings */ public function compareUserSetting($key, $value) { $actual = $this->getUserSetting($key); return ($actual == $value); } private function writeUserSettingCache($key, $value) { $this->settingCacheKeys[$key] = true; $this->settingCache[$key] = $value; return $value; } public function getTranslation() { return $this->getUserSetting(PhabricatorTranslationSetting::SETTINGKEY); } public function getTimezoneIdentifier() { return $this->getUserSetting(PhabricatorTimezoneSetting::SETTINGKEY); } public static function getGlobalSettingsCacheKey() { return 'user.settings.globals.v1'; } private function loadGlobalSettings() { $cache_key = self::getGlobalSettingsCacheKey(); $cache = PhabricatorCaches::getMutableStructureCache(); $settings = $cache->getKey($cache_key); if (!$settings) { $preferences = PhabricatorUserPreferences::loadGlobalPreferences($this); $settings = $preferences->getPreferences(); $cache->setKey($cache_key, $settings); } return $settings; } /** * Override the user's timezone identifier. * * This is primarily useful for unit tests. * * @param string New timezone identifier. * @return this * @task settings */ public function overrideTimezoneIdentifier($identifier) { $timezone_key = PhabricatorTimezoneSetting::SETTINGKEY; $this->settingCacheKeys[$timezone_key] = true; $this->settingCache[$timezone_key] = $identifier; return $this; } public function getGender() { return $this->getUserSetting(PhabricatorPronounSetting::SETTINGKEY); } public function loadEditorLink( $path, $line, PhabricatorRepository $repository = null) { $editor = $this->getUserSetting(PhabricatorEditorSetting::SETTINGKEY); if (is_array($path)) { $multi_key = PhabricatorEditorMultipleSetting::SETTINGKEY; $multiedit = $this->getUserSetting($multi_key); switch ($multiedit) { case PhabricatorEditorMultipleSetting::VALUE_SPACES: $path = implode(' ', $path); break; case PhabricatorEditorMultipleSetting::VALUE_SINGLE: default: return null; } } if (!strlen($editor)) { return null; } if ($repository) { $callsign = $repository->getCallsign(); } else { $callsign = null; } $uri = strtr($editor, array( '%%' => '%', '%f' => phutil_escape_uri($path), '%l' => phutil_escape_uri($line), '%r' => phutil_escape_uri($callsign), )); // The resulting URI must have an allowed protocol. Otherwise, we'll return // a link to an error page explaining the misconfiguration. $ok = PhabricatorHelpEditorProtocolController::hasAllowedProtocol($uri); if (!$ok) { return '/help/editorprotocol/'; } return (string)$uri; } public function getAlternateCSRFString() { return $this->assertAttached($this->alternateCSRFString); } public function attachAlternateCSRFString($string) { $this->alternateCSRFString = $string; return $this; } /** * Populate the nametoken table, which used to fetch typeahead results. When * a user types "linc", we want to match "Abraham Lincoln" from on-demand * typeahead sources. To do this, we need a separate table of name fragments. */ public function updateNameTokens() { $table = self::NAMETOKEN_TABLE; $conn_w = $this->establishConnection('w'); $tokens = PhabricatorTypeaheadDatasource::tokenizeString( $this->getUserName().' '.$this->getRealName()); $sql = array(); foreach ($tokens as $token) { $sql[] = qsprintf( $conn_w, '(%d, %s)', $this->getID(), $token); } queryfx( $conn_w, 'DELETE FROM %T WHERE userID = %d', $table, $this->getID()); if ($sql) { queryfx( $conn_w, - 'INSERT INTO %T (userID, token) VALUES %Q', + 'INSERT INTO %T (userID, token) VALUES %LQ', $table, - implode(', ', $sql)); + $sql); } } public function sendWelcomeEmail(PhabricatorUser $admin) { if (!$this->canEstablishWebSessions()) { throw new Exception( pht( 'Can not send welcome mail to users who can not establish '. 'web sessions!')); } $admin_username = $admin->getUserName(); $admin_realname = $admin->getRealName(); $user_username = $this->getUserName(); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $base_uri = PhabricatorEnv::getProductionURI('/'); $engine = new PhabricatorAuthSessionEngine(); $uri = $engine->getOneTimeLoginURI( $this, $this->loadPrimaryEmail(), PhabricatorAuthSessionEngine::ONETIME_WELCOME); $body = pht( "Welcome to Phabricator!\n\n". "%s (%s) has created an account for you.\n\n". " Username: %s\n\n". "To login to Phabricator, follow this link and set a password:\n\n". " %s\n\n". "After you have set a password, you can login in the future by ". "going here:\n\n". " %s\n", $admin_username, $admin_realname, $user_username, $uri, $base_uri); if (!$is_serious) { $body .= sprintf( "\n%s\n", pht("Love,\nPhabricator")); } $mail = id(new PhabricatorMetaMTAMail()) ->addTos(array($this->getPHID())) ->setForceDelivery(true) ->setSubject(pht('[Phabricator] Welcome to Phabricator')) ->setBody($body) ->saveAndSend(); } public function sendUsernameChangeEmail( PhabricatorUser $admin, $old_username) { $admin_username = $admin->getUserName(); $admin_realname = $admin->getRealName(); $new_username = $this->getUserName(); $password_instructions = null; if (PhabricatorPasswordAuthProvider::getPasswordProvider()) { $engine = new PhabricatorAuthSessionEngine(); $uri = $engine->getOneTimeLoginURI( $this, null, PhabricatorAuthSessionEngine::ONETIME_USERNAME); $password_instructions = sprintf( "%s\n\n %s\n\n%s\n", pht( "If you use a password to login, you'll need to reset it ". "before you can login again. You can reset your password by ". "following this link:"), $uri, pht( "And, of course, you'll need to use your new username to login ". "from now on. If you use OAuth to login, nothing should change.")); } $body = sprintf( "%s\n\n %s\n %s\n\n%s", pht( '%s (%s) has changed your Phabricator username.', $admin_username, $admin_realname), pht( 'Old Username: %s', $old_username), pht( 'New Username: %s', $new_username), $password_instructions); $mail = id(new PhabricatorMetaMTAMail()) ->addTos(array($this->getPHID())) ->setForceDelivery(true) ->setSubject(pht('[Phabricator] Username Changed')) ->setBody($body) ->saveAndSend(); } public static function describeValidUsername() { return pht( 'Usernames must contain only numbers, letters, period, underscore and '. 'hyphen, and can not end with a period. They must have no more than %d '. 'characters.', new PhutilNumber(self::MAXIMUM_USERNAME_LENGTH)); } public static function validateUsername($username) { // NOTE: If you update this, make sure to update: // // - Remarkup rule for @mentions. // - Routing rule for "/p/username/". // - Unit tests, obviously. // - describeValidUsername() method, above. if (strlen($username) > self::MAXIMUM_USERNAME_LENGTH) { return false; } return (bool)preg_match('/^[a-zA-Z0-9._-]*[a-zA-Z0-9_-]\z/', $username); } public static function getDefaultProfileImageURI() { return celerity_get_resource_uri('/rsrc/image/avatar.png'); } public function getProfileImageURI() { $uri_key = PhabricatorUserProfileImageCacheType::KEY_URI; return $this->requireCacheData($uri_key); } public function getUnreadNotificationCount() { $notification_key = PhabricatorUserNotificationCountCacheType::KEY_COUNT; return $this->requireCacheData($notification_key); } public function getUnreadMessageCount() { $message_key = PhabricatorUserMessageCountCacheType::KEY_COUNT; return $this->requireCacheData($message_key); } public function getRecentBadgeAwards() { $badges_key = PhabricatorUserBadgesCacheType::KEY_BADGES; return $this->requireCacheData($badges_key); } public function getFullName() { if (strlen($this->getRealName())) { return $this->getUsername().' ('.$this->getRealName().')'; } else { return $this->getUsername(); } } public function getTimeZone() { return new DateTimeZone($this->getTimezoneIdentifier()); } public function getTimeZoneOffset() { $timezone = $this->getTimeZone(); $now = new DateTime('@'.PhabricatorTime::getNow()); $offset = $timezone->getOffset($now); // Javascript offsets are in minutes and have the opposite sign. $offset = -(int)($offset / 60); return $offset; } public function getTimeZoneOffsetInHours() { $offset = $this->getTimeZoneOffset(); $offset = (int)round($offset / 60); $offset = -$offset; return $offset; } public function formatShortDateTime($when, $now = null) { if ($now === null) { $now = PhabricatorTime::getNow(); } try { $when = new DateTime('@'.$when); $now = new DateTime('@'.$now); } catch (Exception $ex) { return null; } $zone = $this->getTimeZone(); $when->setTimeZone($zone); $now->setTimeZone($zone); if ($when->format('Y') !== $now->format('Y')) { // Different year, so show "Feb 31 2075". $format = 'M j Y'; } else if ($when->format('Ymd') !== $now->format('Ymd')) { // Same year but different month and day, so show "Feb 31". $format = 'M j'; } else { // Same year, month and day so show a time of day. $pref_time = PhabricatorTimeFormatSetting::SETTINGKEY; $format = $this->getUserSetting($pref_time); } return $when->format($format); } public function __toString() { return $this->getUsername(); } public static function loadOneWithEmailAddress($address) { $email = id(new PhabricatorUserEmail())->loadOneWhere( 'address = %s', $address); if (!$email) { return null; } return id(new PhabricatorUser())->loadOneWhere( 'phid = %s', $email->getUserPHID()); } public function getDefaultSpacePHID() { // TODO: We might let the user switch which space they're "in" later on; // for now just use the global space if one exists. // If the viewer has access to the default space, use that. $spaces = PhabricatorSpacesNamespaceQuery::getViewerActiveSpaces($this); foreach ($spaces as $space) { if ($space->getIsDefaultNamespace()) { return $space->getPHID(); } } // Otherwise, use the space with the lowest ID that they have access to. // This just tends to keep the default stable and predictable over time, // so adding a new space won't change behavior for users. if ($spaces) { $spaces = msort($spaces, 'getID'); return head($spaces)->getPHID(); } return null; } /** * Grant a user a source of authority, to let them bypass policy checks they * could not otherwise. */ public function grantAuthority($authority) { $this->authorities[] = $authority; return $this; } /** * Get authorities granted to the user. */ public function getAuthorities() { return $this->authorities; } public function hasConduitClusterToken() { return ($this->conduitClusterToken !== self::ATTACHABLE); } public function attachConduitClusterToken(PhabricatorConduitToken $token) { $this->conduitClusterToken = $token; return $this; } public function getConduitClusterToken() { return $this->assertAttached($this->conduitClusterToken); } /* -( Availability )------------------------------------------------------- */ /** * @task availability */ public function attachAvailability(array $availability) { $this->availability = $availability; return $this; } /** * Get the timestamp the user is away until, if they are currently away. * * @return int|null Epoch timestamp, or `null` if the user is not away. * @task availability */ public function getAwayUntil() { $availability = $this->availability; $this->assertAttached($availability); if (!$availability) { return null; } return idx($availability, 'until'); } public function getDisplayAvailability() { $availability = $this->availability; $this->assertAttached($availability); if (!$availability) { return null; } $busy = PhabricatorCalendarEventInvitee::AVAILABILITY_BUSY; return idx($availability, 'availability', $busy); } public function getAvailabilityEventPHID() { $availability = $this->availability; $this->assertAttached($availability); if (!$availability) { return null; } return idx($availability, 'eventPHID'); } /** * Get cached availability, if present. * * @return wild|null Cache data, or null if no cache is available. * @task availability */ public function getAvailabilityCache() { $now = PhabricatorTime::getNow(); if ($this->availabilityCacheTTL <= $now) { return null; } try { return phutil_json_decode($this->availabilityCache); } catch (Exception $ex) { return null; } } /** * Write to the availability cache. * * @param wild Availability cache data. * @param int|null Cache TTL. * @return this * @task availability */ public function writeAvailabilityCache(array $availability, $ttl) { if (PhabricatorEnv::isReadOnly()) { return $this; } $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); queryfx( $this->establishConnection('w'), 'UPDATE %T SET availabilityCache = %s, availabilityCacheTTL = %nd WHERE id = %d', $this->getTableName(), phutil_json_encode($availability), $ttl, $this->getID()); unset($unguarded); return $this; } /* -( Multi-Factor Authentication )---------------------------------------- */ /** * Update the flag storing this user's enrollment in multi-factor auth. * * With certain settings, we need to check if a user has MFA on every page, * so we cache MFA enrollment on the user object for performance. Calling this * method synchronizes the cache by examining enrollment records. After * updating the cache, use @{method:getIsEnrolledInMultiFactor} to check if * the user is enrolled. * * This method should be called after any changes are made to a given user's * multi-factor configuration. * * @return void * @task factors */ public function updateMultiFactorEnrollment() { $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere( 'userPHID = %s', $this->getPHID()); $enrolled = count($factors) ? 1 : 0; if ($enrolled !== $this->isEnrolledInMultiFactor) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); queryfx( $this->establishConnection('w'), 'UPDATE %T SET isEnrolledInMultiFactor = %d WHERE id = %d', $this->getTableName(), $enrolled, $this->getID()); unset($unguarded); $this->isEnrolledInMultiFactor = $enrolled; } } /** * Check if the user is enrolled in multi-factor authentication. * * Enrolled users have one or more multi-factor authentication sources * attached to their account. For performance, this value is cached. You * can use @{method:updateMultiFactorEnrollment} to update the cache. * * @return bool True if the user is enrolled. * @task factors */ public function getIsEnrolledInMultiFactor() { return $this->isEnrolledInMultiFactor; } /* -( Omnipotence )-------------------------------------------------------- */ /** * Returns true if this user is omnipotent. Omnipotent users bypass all policy * checks. * * @return bool True if the user bypasses policy checks. */ public function isOmnipotent() { return $this->omnipotent; } /** * Get an omnipotent user object for use in contexts where there is no acting * user, notably daemons. * * @return PhabricatorUser An omnipotent user. */ public static function getOmnipotentUser() { static $user = null; if (!$user) { $user = new PhabricatorUser(); $user->omnipotent = true; $user->makeEphemeral(); } return $user; } /** * Get a scalar string identifying this user. * * This is similar to using the PHID, but distinguishes between omnipotent * and public users explicitly. This allows safe construction of cache keys * or cache buckets which do not conflate public and omnipotent users. * * @return string Scalar identifier. */ public function getCacheFragment() { if ($this->isOmnipotent()) { return 'u.omnipotent'; } $phid = $this->getPHID(); if ($phid) { return 'u.'.$phid; } return 'u.public'; } /* -( Managing Handles )--------------------------------------------------- */ /** * Get a @{class:PhabricatorHandleList} which benefits from this viewer's * internal handle pool. * * @param list List of PHIDs to load. * @return PhabricatorHandleList Handle list object. * @task handle */ public function loadHandles(array $phids) { if ($this->handlePool === null) { $this->handlePool = id(new PhabricatorHandlePool()) ->setViewer($this); } return $this->handlePool->newHandleList($phids); } /** * Get a @{class:PHUIHandleView} for a single handle. * * This benefits from the viewer's internal handle pool. * * @param phid PHID to render a handle for. * @return PHUIHandleView View of the handle. * @task handle */ public function renderHandle($phid) { return $this->loadHandles(array($phid))->renderHandle($phid); } /** * Get a @{class:PHUIHandleListView} for a list of handles. * * This benefits from the viewer's internal handle pool. * * @param list List of PHIDs to render. * @return PHUIHandleListView View of the handles. * @task handle */ public function renderHandleList(array $phids) { return $this->loadHandles($phids)->renderList(); } public function attachBadgePHIDs(array $phids) { $this->badgePHIDs = $phids; return $this; } public function getBadgePHIDs() { return $this->assertAttached($this->badgePHIDs); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicies::POLICY_PUBLIC; case PhabricatorPolicyCapability::CAN_EDIT: if ($this->getIsSystemAgent() || $this->getIsMailingList()) { return PhabricatorPolicies::POLICY_ADMIN; } else { return PhabricatorPolicies::POLICY_NOONE; } } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getPHID() && ($viewer->getPHID() === $this->getPHID()); } public function describeAutomaticCapability($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_EDIT: return pht('Only you can edit your information.'); default: return null; } } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('user.fields'); } public function getCustomFieldBaseClass() { return 'PhabricatorUserCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $externals = id(new PhabricatorExternalAccount())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($externals as $external) { $external->delete(); } $prefs = id(new PhabricatorUserPreferencesQuery()) ->setViewer($engine->getViewer()) ->withUsers(array($this)) ->execute(); foreach ($prefs as $pref) { $engine->destroyObject($pref); } $profiles = id(new PhabricatorUserProfile())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($profiles as $profile) { $profile->delete(); } $keys = id(new PhabricatorAuthSSHKeyQuery()) ->setViewer($engine->getViewer()) ->withObjectPHIDs(array($this->getPHID())) ->execute(); foreach ($keys as $key) { $engine->destroyObject($key); } $emails = id(new PhabricatorUserEmail())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($emails as $email) { $email->delete(); } $sessions = id(new PhabricatorAuthSession())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($sessions as $session) { $session->delete(); } $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($factors as $factor) { $factor->delete(); } $this->saveTransaction(); } /* -( PhabricatorSSHPublicKeyInterface )----------------------------------- */ public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer) { if ($viewer->getPHID() == $this->getPHID()) { // If the viewer is managing their own keys, take them to the normal // panel. return '/settings/panel/ssh/'; } else { // Otherwise, take them to the administrative panel for this user. return '/settings/user/'.$this->getUsername().'/page/ssh/'; } } public function getSSHKeyDefaultName() { return 'id_rsa_phabricator'; } public function getSSHKeyNotifyPHIDs() { return array( $this->getPHID(), ); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorUserTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorUserTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PhabricatorUserFulltextEngine(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new PhabricatorUserFerretEngine(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('username') ->setType('string') ->setDescription(pht("The user's username.")), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('realName') ->setType('string') ->setDescription(pht("The user's real name.")), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('roles') ->setType('list') ->setDescription(pht('List of account roles.')), ); } public function getFieldValuesForConduit() { $roles = array(); if ($this->getIsDisabled()) { $roles[] = 'disabled'; } if ($this->getIsSystemAgent()) { $roles[] = 'bot'; } if ($this->getIsMailingList()) { $roles[] = 'list'; } if ($this->getIsAdmin()) { $roles[] = 'admin'; } if ($this->getIsEmailVerified()) { $roles[] = 'verified'; } if ($this->getIsApproved()) { $roles[] = 'approved'; } if ($this->isUserActivated()) { $roles[] = 'activated'; } return array( 'username' => $this->getUsername(), 'realName' => $this->getRealName(), 'roles' => $roles, ); } public function getConduitSearchAttachments() { return array(); } /* -( User Cache )--------------------------------------------------------- */ /** * @task cache */ public function attachRawCacheData(array $data) { $this->rawCacheData = $data + $this->rawCacheData; return $this; } public function setAllowInlineCacheGeneration($allow_cache_generation) { $this->allowInlineCacheGeneration = $allow_cache_generation; return $this; } /** * @task cache */ protected function requireCacheData($key) { if (isset($this->usableCacheData[$key])) { return $this->usableCacheData[$key]; } $type = PhabricatorUserCacheType::requireCacheTypeForKey($key); if (isset($this->rawCacheData[$key])) { $raw_value = $this->rawCacheData[$key]; $usable_value = $type->getValueFromStorage($raw_value); $this->usableCacheData[$key] = $usable_value; return $usable_value; } // By default, we throw if a cache isn't available. This is consistent // with the standard `needX()` + `attachX()` + `getX()` interaction. if (!$this->allowInlineCacheGeneration) { throw new PhabricatorDataNotAttachedException($this); } $user_phid = $this->getPHID(); // Try to read the actual cache before we generate a new value. We can // end up here via Conduit, which does not use normal sessions and can // not pick up a free cache load during session identification. if ($user_phid) { $raw_data = PhabricatorUserCache::readCaches( $type, $key, array($user_phid)); if (array_key_exists($user_phid, $raw_data)) { $raw_value = $raw_data[$user_phid]; $usable_value = $type->getValueFromStorage($raw_value); $this->rawCacheData[$key] = $raw_value; $this->usableCacheData[$key] = $usable_value; return $usable_value; } } $usable_value = $type->getDefaultValue(); if ($user_phid) { $map = $type->newValueForUsers($key, array($this)); if (array_key_exists($user_phid, $map)) { $raw_value = $map[$user_phid]; $usable_value = $type->getValueFromStorage($raw_value); $this->rawCacheData[$key] = $raw_value; PhabricatorUserCache::writeCache( $type, $key, $user_phid, $raw_value); } } $this->usableCacheData[$key] = $usable_value; return $usable_value; } /** * @task cache */ public function clearCacheData($key) { unset($this->rawCacheData[$key]); unset($this->usableCacheData[$key]); return $this; } public function getCSSValue($variable_key) { $preference = PhabricatorAccessibilitySetting::SETTINGKEY; $key = $this->getUserSetting($preference); $postprocessor = CelerityPostprocessor::getPostprocessor($key); $variables = $postprocessor->getVariables(); if (!isset($variables[$variable_key])) { throw new Exception( pht( 'Unknown CSS variable "%s"!', $variable_key)); } return $variables[$variable_key]; } /* -( PhabricatorAuthPasswordHashInterface )------------------------------- */ public function newPasswordDigest( PhutilOpaqueEnvelope $envelope, PhabricatorAuthPassword $password) { // Before passwords are hashed, they are digested. The goal of digestion // is twofold: to reduce the length of very long passwords to something // reasonable; and to salt the password in case the best available hasher // does not include salt automatically. // Users may choose arbitrarily long passwords, and attackers may try to // attack the system by probing it with very long passwords. When large // inputs are passed to hashers -- which are intentionally slow -- it // can result in unacceptably long runtimes. The classic attack here is // to try to log in with a 64MB password and see if that locks up the // machine for the next century. By digesting passwords to a standard // length first, the length of the raw input does not impact the runtime // of the hashing algorithm. // Some hashers like bcrypt are self-salting, while other hashers are not. // Applying salt while digesting passwords ensures that hashes are salted // whether we ultimately select a self-salting hasher or not. // For legacy compatibility reasons, old VCS and Account password digest // algorithms are significantly more complicated than necessary to achieve // these goals. This is because they once used a different hashing and // salting process. When we upgraded to the modern modular hasher // infrastructure, we just bolted it onto the end of the existing pipelines // so that upgrading didn't break all users' credentials. // New implementations can (and, generally, should) safely select the // simple HMAC SHA256 digest at the bottom of the function, which does // everything that a digest callback should without any needless legacy // baggage on top. if ($password->getLegacyDigestFormat() == 'v1') { switch ($password->getPasswordType()) { case PhabricatorAuthPassword::PASSWORD_TYPE_VCS: // Old VCS passwords use an iterated HMAC SHA1 as a digest algorithm. // They originally used this as a hasher, but it became a digest // algorithm once hashing was upgraded to include bcrypt. $digest = $envelope->openEnvelope(); $salt = $this->getPHID(); for ($ii = 0; $ii < 1000; $ii++) { $digest = PhabricatorHash::weakDigest($digest, $salt); } return new PhutilOpaqueEnvelope($digest); case PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT: // Account passwords previously used this weird mess of salt and did // not digest the input to a standard length. // Beyond this being a weird special case, there are two actual // problems with this, although neither are particularly severe: // First, because we do not normalize the length of passwords, this // algorithm may make us vulnerable to DOS attacks where an attacker // attempts to use a very long input to slow down hashers. // Second, because the username is part of the hash algorithm, // renaming a user breaks their password. This isn't a huge deal but // it's pretty silly. There's no security justification for this // behavior, I just didn't think about the implication when I wrote // it originally. $parts = array( $this->getUsername(), $envelope->openEnvelope(), $this->getPHID(), $password->getPasswordSalt(), ); return new PhutilOpaqueEnvelope(implode('', $parts)); } } // For passwords which do not have some crazy legacy reason to use some // other digest algorithm, HMAC SHA256 is an excellent choice. It satisfies // the digest requirements and is simple. $digest = PhabricatorHash::digestHMACSHA256( $envelope->openEnvelope(), $password->getPasswordSalt()); return new PhutilOpaqueEnvelope($digest); } public function newPasswordBlocklist( PhabricatorUser $viewer, PhabricatorAuthPasswordEngine $engine) { $list = array(); $list[] = $this->getUsername(); $list[] = $this->getRealName(); $emails = id(new PhabricatorUserEmail())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($emails as $email) { $list[] = $email->getAddress(); } return $list; } } diff --git a/src/applications/people/storage/PhabricatorUserCache.php b/src/applications/people/storage/PhabricatorUserCache.php index 2afb06a437..71b3e4816b 100644 --- a/src/applications/people/storage/PhabricatorUserCache.php +++ b/src/applications/people/storage/PhabricatorUserCache.php @@ -1,166 +1,166 @@ false, self::CONFIG_COLUMN_SCHEMA => array( 'cacheIndex' => 'bytes12', 'cacheKey' => 'text255', 'cacheData' => 'text', 'cacheType' => 'text32', ), self::CONFIG_KEY_SCHEMA => array( 'key_usercache' => array( 'columns' => array('userPHID', 'cacheIndex'), 'unique' => true, ), 'key_cachekey' => array( 'columns' => array('cacheIndex'), ), 'key_cachetype' => array( 'columns' => array('cacheType'), ), ), ) + parent::getConfiguration(); } public function save() { $this->cacheIndex = Filesystem::digestForIndex($this->getCacheKey()); return parent::save(); } public static function writeCache( PhabricatorUserCacheType $type, $key, $user_phid, $raw_value) { self::writeCaches( array( array( 'type' => $type, 'key' => $key, 'userPHID' => $user_phid, 'value' => $raw_value, ), )); } public static function writeCaches(array $values) { if (PhabricatorEnv::isReadOnly()) { return; } if (!$values) { return; } $table = new self(); $conn_w = $table->establishConnection('w'); $sql = array(); foreach ($values as $value) { $key = $value['key']; $sql[] = qsprintf( $conn_w, '(%s, %s, %s, %s, %s)', $value['userPHID'], PhabricatorHash::digestForIndex($key), $key, $value['value'], $value['type']->getUserCacheType()); } $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) { queryfx( $conn_w, 'INSERT INTO %T (userPHID, cacheIndex, cacheKey, cacheData, cacheType) - VALUES %Q + VALUES %LQ ON DUPLICATE KEY UPDATE cacheData = VALUES(cacheData), cacheType = VALUES(cacheType)', $table->getTableName(), $chunk); } unset($unguarded); } public static function readCaches( PhabricatorUserCacheType $type, $key, array $user_phids) { $table = new self(); $conn = $table->establishConnection('r'); $rows = queryfx_all( $conn, 'SELECT userPHID, cacheData FROM %T WHERE userPHID IN (%Ls) AND cacheType = %s AND cacheIndex = %s', $table->getTableName(), $user_phids, $type->getUserCacheType(), PhabricatorHash::digestForIndex($key)); return ipull($rows, 'cacheData', 'userPHID'); } public static function clearCache($key, $user_phid) { return self::clearCaches($key, array($user_phid)); } public static function clearCaches($key, array $user_phids) { if (PhabricatorEnv::isReadOnly()) { return; } if (!$user_phids) { return; } $table = new self(); $conn_w = $table->establishConnection('w'); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); queryfx( $conn_w, 'DELETE FROM %T WHERE cacheIndex = %s AND userPHID IN (%Ls)', $table->getTableName(), PhabricatorHash::digestForIndex($key), $user_phids); unset($unguarded); } public static function clearCacheForAllUsers($key) { if (PhabricatorEnv::isReadOnly()) { return; } $table = new self(); $conn_w = $table->establishConnection('w'); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); queryfx( $conn_w, 'DELETE FROM %T WHERE cacheIndex = %s', $table->getTableName(), PhabricatorHash::digestForIndex($key)); unset($unguarded); } } diff --git a/src/applications/phame/editor/PhameBlogEditEngine.php b/src/applications/phame/editor/PhameBlogEditEngine.php index 9b6be308c4..e11dec6847 100644 --- a/src/applications/phame/editor/PhameBlogEditEngine.php +++ b/src/applications/phame/editor/PhameBlogEditEngine.php @@ -1,138 +1,138 @@ getViewer()); } protected function newObjectQuery() { return id(new PhameBlogQuery()) ->needProfileImage(true); } protected function getObjectCreateTitleText($object) { return pht('Create New Blog'); } protected function getObjectEditTitleText($object) { return pht('Edit %s', $object->getName()); } protected function getObjectEditShortText($object) { return $object->getName(); } protected function getObjectCreateShortText() { return pht('Create Blog'); } protected function getObjectName() { return pht('Blog'); } protected function getObjectCreateCancelURI($object) { return $this->getApplication()->getApplicationURI('blog/'); } protected function getEditorURI() { return $this->getApplication()->getApplicationURI('blog/edit/'); } protected function getObjectViewURI($object) { return $object->getManageURI(); } protected function getCreateNewObjectPolicy() { return $this->getApplication()->getPolicy( PhameBlogCreateCapability::CAPABILITY); } protected function buildCustomEditFields($object) { return array( id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) ->setDescription(pht('Blog name.')) ->setConduitDescription(pht('Retitle the blog.')) ->setConduitTypeDescription(pht('New blog title.')) ->setTransactionType(PhameBlogNameTransaction::TRANSACTIONTYPE) ->setIsRequired(true) ->setValue($object->getName()), id(new PhabricatorTextEditField()) ->setKey('subtitle') ->setLabel(pht('Subtitle')) ->setDescription(pht('Blog subtitle.')) ->setConduitDescription(pht('Change the blog subtitle.')) ->setConduitTypeDescription(pht('New blog subtitle.')) ->setTransactionType(PhameBlogSubtitleTransaction::TRANSACTIONTYPE) ->setValue($object->getSubtitle()), id(new PhabricatorRemarkupEditField()) ->setKey('description') ->setLabel(pht('Description')) ->setDescription(pht('Blog description.')) ->setConduitDescription(pht('Change the blog description.')) ->setConduitTypeDescription(pht('New blog description.')) ->setTransactionType(PhameBlogDescriptionTransaction::TRANSACTIONTYPE) ->setValue($object->getDescription()), id(new PhabricatorTextEditField()) ->setKey('domainFullURI') ->setLabel(pht('Full Domain URI')) ->setControlInstructions(pht('Set Full Domain URI if you plan to '. 'serve this blog on another hosted domain. Parent Site Name and '. 'Parent Site URI are optional but helpful since they provide '. 'a link from the blog back to your parent site.')) ->setDescription(pht('Blog full domain URI.')) ->setConduitDescription(pht('Change the blog full domain URI.')) ->setConduitTypeDescription(pht('New blog full domain URI.')) ->setValue($object->getDomainFullURI()) ->setTransactionType(PhameBlogFullDomainTransaction::TRANSACTIONTYPE), id(new PhabricatorTextEditField()) ->setKey('parentSite') ->setLabel(pht('Parent Site Name')) ->setDescription(pht('Blog parent site name.')) ->setConduitDescription(pht('Change the blog parent site name.')) ->setConduitTypeDescription(pht('New blog parent site name.')) ->setValue($object->getParentSite()) ->setTransactionType(PhameBlogParentSiteTransaction::TRANSACTIONTYPE), id(new PhabricatorTextEditField()) ->setKey('parentDomain') ->setLabel(pht('Parent Site URI')) ->setDescription(pht('Blog parent domain name.')) ->setConduitDescription(pht('Change the blog parent domain.')) ->setConduitTypeDescription(pht('New blog parent domain.')) ->setValue($object->getParentDomain()) ->setTransactionType(PhameBlogParentDomainTransaction::TRANSACTIONTYPE), id(new PhabricatorSelectEditField()) ->setKey('status') ->setLabel(pht('Status')) ->setTransactionType(PhameBlogStatusTransaction::TRANSACTIONTYPE) - ->setIsConduitOnly(true) + ->setIsFormField(false) ->setOptions(PhameBlog::getStatusNameMap()) ->setDescription(pht('Active or archived status.')) ->setConduitDescription(pht('Active or archive the blog.')) ->setConduitTypeDescription(pht('New blog status constant.')) ->setValue($object->getStatus()), ); } } diff --git a/src/applications/phlux/query/PhluxVariableQuery.php b/src/applications/phlux/query/PhluxVariableQuery.php index 82072485d4..75abd044d0 100644 --- a/src/applications/phlux/query/PhluxVariableQuery.php +++ b/src/applications/phlux/query/PhluxVariableQuery.php @@ -1,95 +1,95 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withKeys(array $keys) { $this->keys = $keys; return $this; } protected function loadPage() { $table = new PhluxVariable(); $conn_r = $table->establishConnection('r'); $rows = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($rows); } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->keys !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'variableKey IN (%Ls)', $this->keys); } if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } - $where[] = $this->buildPagingClause($conn_r); + $where[] = $this->buildPagingClause($conn); - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } protected function getDefaultOrderVector() { return array('key'); } public function getOrderableColumns() { return array( 'key' => array( 'column' => 'variableKey', 'type' => 'string', 'reverse' => true, 'unique' => true, ), ); } protected function getPagingValueMap($cursor, array $keys) { $object = $this->loadCursorObject($cursor); return array( 'key' => $object->getVariableKey(), ); } public function getQueryApplicationClass() { return 'PhabricatorPhluxApplication'; } } diff --git a/src/applications/pholio/query/PholioImageQuery.php b/src/applications/pholio/query/PholioImageQuery.php index 8c722660cb..79ffdc56d7 100644 --- a/src/applications/pholio/query/PholioImageQuery.php +++ b/src/applications/pholio/query/PholioImageQuery.php @@ -1,168 +1,168 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withMockIDs(array $mock_ids) { $this->mockIDs = $mock_ids; return $this; } public function withObsolete($obsolete) { $this->obsolete = $obsolete; return $this; } public function needInlineComments($need_inline_comments) { $this->needInlineComments = $need_inline_comments; return $this; } public function setMockCache($mock_cache) { $this->mockCache = $mock_cache; return $this; } public function getMockCache() { return $this->mockCache; } protected function loadPage() { $table = new PholioImage(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); $images = $table->loadAllFromArray($data); return $images; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); - $where[] = $this->buildPagingClause($conn_r); + $where[] = $this->buildPagingClause($conn); if ($this->ids) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } if ($this->mockIDs) { $where[] = qsprintf( - $conn_r, + $conn, 'mockID IN (%Ld)', $this->mockIDs); } if ($this->obsolete !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'isObsolete = %d', $this->obsolete); } - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } protected function willFilterPage(array $images) { assert_instances_of($images, 'PholioImage'); if ($this->getMockCache()) { $mocks = $this->getMockCache(); } else { $mock_ids = mpull($images, 'getMockID'); // DO NOT set needImages to true; recursion results! $mocks = id(new PholioMockQuery()) ->setViewer($this->getViewer()) ->withIDs($mock_ids) ->execute(); $mocks = mpull($mocks, null, 'getID'); } foreach ($images as $index => $image) { $mock = idx($mocks, $image->getMockID()); if ($mock) { $image->attachMock($mock); } else { // mock is missing or we can't see it unset($images[$index]); } } return $images; } protected function didFilterPage(array $images) { assert_instances_of($images, 'PholioImage'); $file_phids = mpull($images, 'getFilePHID'); $all_files = id(new PhabricatorFileQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($file_phids) ->execute(); $all_files = mpull($all_files, null, 'getPHID'); if ($this->needInlineComments) { // Only load inline comments the viewer has permission to see. $all_inline_comments = id(new PholioTransactionComment())->loadAllWhere( 'imageID IN (%Ld) AND (transactionPHID IS NOT NULL OR authorPHID = %s)', mpull($images, 'getID'), $this->getViewer()->getPHID()); $all_inline_comments = mgroup($all_inline_comments, 'getImageID'); } foreach ($images as $image) { $file = idx($all_files, $image->getFilePHID()); if (!$file) { $file = PhabricatorFile::loadBuiltin($this->getViewer(), 'missing.png'); } $image->attachFile($file); if ($this->needInlineComments) { $inlines = idx($all_inline_comments, $image->getID(), array()); $image->attachInlineComments($inlines); } } return $images; } public function getQueryApplicationClass() { return 'PhabricatorPholioApplication'; } } diff --git a/src/applications/phortune/query/PhortuneAccountQuery.php b/src/applications/phortune/query/PhortuneAccountQuery.php index c91e5f0111..ee8291218c 100644 --- a/src/applications/phortune/query/PhortuneAccountQuery.php +++ b/src/applications/phortune/query/PhortuneAccountQuery.php @@ -1,123 +1,117 @@ setViewer($user) ->withMemberPHIDs(array($user->getPHID())) ->execute(); if (!$accounts) { $accounts = array( PhortuneAccount::createNewAccount($user, $content_source), ); } $accounts = mpull($accounts, null, 'getPHID'); return $accounts; } public function withIDs(array $ids) { $this->ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withMemberPHIDs(array $phids) { $this->memberPHIDs = $phids; return $this; } + public function newResultObject() { + return new PhortuneAccount(); + } + protected function loadPage() { - $table = new PhortuneAccount(); - $conn = $table->establishConnection('r'); - - $rows = queryfx_all( - $conn, - 'SELECT a.* FROM %T a %Q %Q %Q %Q', - $table->getTableName(), - $this->buildJoinClause($conn), - $this->buildWhereClause($conn), - $this->buildOrderClause($conn), - $this->buildLimitClause($conn)); - - return $table->loadAllFromArray($rows); + return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $accounts) { $query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(mpull($accounts, 'getPHID')) ->withEdgeTypes(array(PhortuneAccountHasMemberEdgeType::EDGECONST)); $query->execute(); foreach ($accounts as $account) { $member_phids = $query->getDestinationPHIDs(array($account->getPHID())); $member_phids = array_reverse($member_phids); $account->attachMemberPHIDs($member_phids); } return $accounts; } - protected function buildWhereClause(AphrontDatabaseConnection $conn) { - $where = array(); - - $where[] = $this->buildPagingClause($conn); + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); - if ($this->ids) { + if ($this->ids !== null) { $where[] = qsprintf( $conn, 'a.id IN (%Ld)', $this->ids); } - if ($this->phids) { + if ($this->phids !== null) { $where[] = qsprintf( $conn, 'a.phid IN (%Ls)', $this->phids); } - if ($this->memberPHIDs) { + if ($this->memberPHIDs !== null) { $where[] = qsprintf( $conn, 'm.dst IN (%Ls)', $this->memberPHIDs); } - return $this->formatWhereClause($where); + return $where; } - protected function buildJoinClause(AphrontDatabaseConnection $conn) { - $joins = array(); + protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { + $joins = parent::buildJoinClauseParts($conn); - if ($this->memberPHIDs) { + if ($this->memberPHIDs !== null) { $joins[] = qsprintf( $conn, 'LEFT JOIN %T m ON a.phid = m.src AND m.type = %d', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhortuneAccountHasMemberEdgeType::EDGECONST); } - return implode(' ', $joins); + return $joins; } public function getQueryApplicationClass() { return 'PhabricatorPhortuneApplication'; } + protected function getPrimaryTableAlias() { + return 'a'; + } + } diff --git a/src/applications/phortune/query/PhortuneCartQuery.php b/src/applications/phortune/query/PhortuneCartQuery.php index 5009d3a685..0b3325b932 100644 --- a/src/applications/phortune/query/PhortuneCartQuery.php +++ b/src/applications/phortune/query/PhortuneCartQuery.php @@ -1,223 +1,223 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withAccountPHIDs(array $account_phids) { $this->accountPHIDs = $account_phids; return $this; } public function withMerchantPHIDs(array $merchant_phids) { $this->merchantPHIDs = $merchant_phids; return $this; } public function withSubscriptionPHIDs(array $subscription_phids) { $this->subscriptionPHIDs = $subscription_phids; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } /** * Include or exclude carts which represent invoices with payments due. * * @param bool `true` to select invoices; `false` to exclude invoices. * @return this */ public function withInvoices($invoices) { $this->invoices = $invoices; return $this; } public function needPurchases($need_purchases) { $this->needPurchases = $need_purchases; return $this; } protected function loadPage() { $table = new PhortuneCart(); $conn = $table->establishConnection('r'); $rows = queryfx_all( $conn, 'SELECT cart.* FROM %T cart %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn), $this->buildOrderClause($conn), $this->buildLimitClause($conn)); return $table->loadAllFromArray($rows); } protected function willFilterPage(array $carts) { $accounts = id(new PhortuneAccountQuery()) ->setViewer($this->getViewer()) ->withPHIDs(mpull($carts, 'getAccountPHID')) ->execute(); $accounts = mpull($accounts, null, 'getPHID'); foreach ($carts as $key => $cart) { $account = idx($accounts, $cart->getAccountPHID()); if (!$account) { unset($carts[$key]); continue; } $cart->attachAccount($account); } if (!$carts) { return array(); } $merchants = id(new PhortuneMerchantQuery()) ->setViewer($this->getViewer()) ->withPHIDs(mpull($carts, 'getMerchantPHID')) ->execute(); $merchants = mpull($merchants, null, 'getPHID'); foreach ($carts as $key => $cart) { $merchant = idx($merchants, $cart->getMerchantPHID()); if (!$merchant) { unset($carts[$key]); continue; } $cart->attachMerchant($merchant); } if (!$carts) { return array(); } $implementations = array(); $cart_map = mgroup($carts, 'getCartClass'); foreach ($cart_map as $class => $class_carts) { $implementations += newv($class, array())->loadImplementationsForCarts( $this->getViewer(), $class_carts); } foreach ($carts as $key => $cart) { $implementation = idx($implementations, $key); if (!$implementation) { unset($carts[$key]); continue; } $cart->attachImplementation($implementation); } return $carts; } protected function didFilterPage(array $carts) { if ($this->needPurchases) { $purchases = id(new PhortunePurchaseQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withCartPHIDs(mpull($carts, 'getPHID')) ->execute(); $purchases = mgroup($purchases, 'getCartPHID'); foreach ($carts as $cart) { $cart->attachPurchases(idx($purchases, $cart->getPHID(), array())); } } return $carts; } protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); $where[] = $this->buildPagingClause($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'cart.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'cart.phid IN (%Ls)', $this->phids); } if ($this->accountPHIDs !== null) { $where[] = qsprintf( $conn, 'cart.accountPHID IN (%Ls)', $this->accountPHIDs); } if ($this->merchantPHIDs !== null) { $where[] = qsprintf( $conn, 'cart.merchantPHID IN (%Ls)', $this->merchantPHIDs); } if ($this->subscriptionPHIDs !== null) { $where[] = qsprintf( $conn, 'cart.subscriptionPHID IN (%Ls)', $this->subscriptionPHIDs); } if ($this->statuses !== null) { $where[] = qsprintf( $conn, 'cart.status IN (%Ls)', $this->statuses); } if ($this->invoices !== null) { if ($this->invoices) { $where[] = qsprintf( $conn, 'cart.status = %s AND cart.isInvoice = 1', PhortuneCart::STATUS_READY); } else { $where[] = qsprintf( $conn, 'cart.status != %s OR cart.isInvoice = 0', PhortuneCart::STATUS_READY); } } - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } public function getQueryApplicationClass() { return 'PhabricatorPhortuneApplication'; } } diff --git a/src/applications/phortune/query/PhortuneChargeQuery.php b/src/applications/phortune/query/PhortuneChargeQuery.php index 6b11cbe95e..a7eda9d6a6 100644 --- a/src/applications/phortune/query/PhortuneChargeQuery.php +++ b/src/applications/phortune/query/PhortuneChargeQuery.php @@ -1,144 +1,144 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withAccountPHIDs(array $account_phids) { $this->accountPHIDs = $account_phids; return $this; } public function withCartPHIDs(array $cart_phids) { $this->cartPHIDs = $cart_phids; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } public function needCarts($need_carts) { $this->needCarts = $need_carts; return $this; } protected function loadPage() { $table = new PhortuneCharge(); $conn = $table->establishConnection('r'); $rows = queryfx_all( $conn, 'SELECT charge.* FROM %T charge %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn), $this->buildOrderClause($conn), $this->buildLimitClause($conn)); return $table->loadAllFromArray($rows); } protected function willFilterPage(array $charges) { $accounts = id(new PhortuneAccountQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withPHIDs(mpull($charges, 'getAccountPHID')) ->execute(); $accounts = mpull($accounts, null, 'getPHID'); foreach ($charges as $key => $charge) { $account = idx($accounts, $charge->getAccountPHID()); if (!$account) { unset($charges[$key]); continue; } $charge->attachAccount($account); } return $charges; } protected function didFilterPage(array $charges) { if ($this->needCarts) { $carts = id(new PhortuneCartQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withPHIDs(mpull($charges, 'getCartPHID')) ->execute(); $carts = mpull($carts, null, 'getPHID'); foreach ($charges as $charge) { $cart = idx($carts, $charge->getCartPHID()); $charge->attachCart($cart); } } return $charges; } protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); $where[] = $this->buildPagingClause($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'charge.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'charge.phid IN (%Ls)', $this->phids); } if ($this->accountPHIDs !== null) { $where[] = qsprintf( $conn, 'charge.accountPHID IN (%Ls)', $this->accountPHIDs); } if ($this->cartPHIDs !== null) { $where[] = qsprintf( $conn, 'charge.cartPHID IN (%Ls)', $this->cartPHIDs); } if ($this->statuses !== null) { $where[] = qsprintf( $conn, 'charge.status IN (%Ls)', $this->statuses); } - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } public function getQueryApplicationClass() { return 'PhabricatorPhortuneApplication'; } } diff --git a/src/applications/phortune/query/PhortuneMerchantQuery.php b/src/applications/phortune/query/PhortuneMerchantQuery.php index c91267b73c..b6cab7dbc2 100644 --- a/src/applications/phortune/query/PhortuneMerchantQuery.php +++ b/src/applications/phortune/query/PhortuneMerchantQuery.php @@ -1,138 +1,132 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withMemberPHIDs(array $member_phids) { $this->memberPHIDs = $member_phids; return $this; } public function needProfileImage($need) { $this->needProfileImage = $need; return $this; } + public function newResultObject() { + return new PhortuneMerchant(); + } + protected function loadPage() { - $table = new PhortuneMerchant(); - $conn = $table->establishConnection('r'); - - $rows = queryfx_all( - $conn, - 'SELECT m.* FROM %T m %Q %Q %Q %Q', - $table->getTableName(), - $this->buildJoinClause($conn), - $this->buildWhereClause($conn), - $this->buildOrderClause($conn), - $this->buildLimitClause($conn)); - - return $table->loadAllFromArray($rows); + return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $merchants) { $query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(mpull($merchants, 'getPHID')) ->withEdgeTypes(array(PhortuneMerchantHasMemberEdgeType::EDGECONST)); $query->execute(); foreach ($merchants as $merchant) { $member_phids = $query->getDestinationPHIDs(array($merchant->getPHID())); $member_phids = array_reverse($member_phids); $merchant->attachMemberPHIDs($member_phids); } if ($this->needProfileImage) { $default = null; $file_phids = mpull($merchants, 'getProfileImagePHID'); $file_phids = array_filter($file_phids); if ($file_phids) { $files = id(new PhabricatorFileQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); } else { $files = array(); } foreach ($merchants as $merchant) { $file = idx($files, $merchant->getProfileImagePHID()); if (!$file) { if (!$default) { $default = PhabricatorFile::loadBuiltin( $this->getViewer(), 'merchant.png'); } $file = $default; } $merchant->attachProfileImageFile($file); } } return $merchants; } - protected function buildWhereClause(AphrontDatabaseConnection $conn) { - $where = array(); + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'phid IN (%Ls)', $this->phids); } if ($this->memberPHIDs !== null) { $where[] = qsprintf( $conn, 'e.dst IN (%Ls)', $this->memberPHIDs); } - $where[] = $this->buildPagingClause($conn); - - return $this->formatWhereClause($where); + return $where; } - protected function buildJoinClause(AphrontDatabaseConnection $conn) { - $joins = array(); + protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { + $joins = parent::buildJoinClauseParts($conn); if ($this->memberPHIDs !== null) { $joins[] = qsprintf( $conn, 'LEFT JOIN %T e ON m.phid = e.src AND e.type = %d', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhortuneMerchantHasMemberEdgeType::EDGECONST); } - return implode(' ', $joins); + return $joins; } public function getQueryApplicationClass() { return 'PhabricatorPhortuneApplication'; } + protected function getPrimaryTableAlias() { + return 'm'; + } + } diff --git a/src/applications/phortune/query/PhortunePaymentProviderConfigQuery.php b/src/applications/phortune/query/PhortunePaymentProviderConfigQuery.php index d80b0e90d0..a850acec28 100644 --- a/src/applications/phortune/query/PhortunePaymentProviderConfigQuery.php +++ b/src/applications/phortune/query/PhortunePaymentProviderConfigQuery.php @@ -1,95 +1,95 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withMerchantPHIDs(array $phids) { $this->merchantPHIDs = $phids; return $this; } protected function loadPage() { $table = new PhortunePaymentProviderConfig(); $conn = $table->establishConnection('r'); $rows = queryfx_all( $conn, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn), $this->buildOrderClause($conn), $this->buildLimitClause($conn)); return $table->loadAllFromArray($rows); } protected function willFilterPage(array $provider_configs) { $merchant_phids = mpull($provider_configs, 'getMerchantPHID'); $merchants = id(new PhortuneMerchantQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withPHIDs($merchant_phids) ->execute(); $merchants = mpull($merchants, null, 'getPHID'); foreach ($provider_configs as $key => $config) { $merchant = idx($merchants, $config->getMerchantPHID()); if (!$merchant) { $this->didRejectResult($config); unset($provider_configs[$key]); continue; } $config->attachMerchant($merchant); } return $provider_configs; } protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'phid IN (%Ls)', $this->phids); } if ($this->merchantPHIDs !== null) { $where[] = qsprintf( $conn, 'merchantPHID IN (%Ls)', $this->merchantPHIDs); } $where[] = $this->buildPagingClause($conn); - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } public function getQueryApplicationClass() { return 'PhabricatorPhortuneApplication'; } } diff --git a/src/applications/phortune/query/PhortuneProductQuery.php b/src/applications/phortune/query/PhortuneProductQuery.php index 99ba535585..30701d4e7b 100644 --- a/src/applications/phortune/query/PhortuneProductQuery.php +++ b/src/applications/phortune/query/PhortuneProductQuery.php @@ -1,120 +1,120 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withClassAndRef($class, $ref) { $this->refMap = array($class => array($ref)); return $this; } protected function loadPage() { $table = new PhortuneProduct(); $conn = $table->establishConnection('r'); $rows = queryfx_all( $conn, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn), $this->buildOrderClause($conn), $this->buildLimitClause($conn)); $page = $table->loadAllFromArray($rows); // NOTE: We're loading product implementations here, but also creating any // products which do not yet exist. $class_map = mgroup($page, 'getProductClass'); if ($this->refMap) { $class_map += array_fill_keys(array_keys($this->refMap), array()); } foreach ($class_map as $class => $products) { $refs = mpull($products, null, 'getProductRef'); if (isset($this->refMap[$class])) { $refs += array_fill_keys($this->refMap[$class], null); } $implementations = newv($class, array())->loadImplementationsForRefs( $this->getViewer(), array_keys($refs)); $implementations = mpull($implementations, null, 'getRef'); foreach ($implementations as $ref => $implementation) { $product = idx($refs, $ref); if ($product === null) { // If this product does not exist yet, create it and add it to the // result page. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $product = PhortuneProduct::initializeNewProduct() ->setProductClass($class) ->setProductRef($ref) ->save(); unset($unguarded); $page[] = $product; } $product->attachImplementation($implementation); } } return $page; } protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'phid IN (%Ls)', $this->phids); } if ($this->refMap !== null) { $sql = array(); foreach ($this->refMap as $class => $refs) { foreach ($refs as $ref) { $sql[] = qsprintf( $conn, '(productClassKey = %s AND productRefKey = %s)', PhabricatorHash::digestForIndex($class), PhabricatorHash::digestForIndex($ref)); } } - $where[] = implode(' OR ', $sql); + $where[] = qsprintf($conn, '%LO', $sql); } $where[] = $this->buildPagingClause($conn); - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } public function getQueryApplicationClass() { return 'PhabricatorPhortuneApplication'; } } diff --git a/src/applications/phortune/query/PhortunePurchaseQuery.php b/src/applications/phortune/query/PhortunePurchaseQuery.php index 6e9e599240..275537c351 100644 --- a/src/applications/phortune/query/PhortunePurchaseQuery.php +++ b/src/applications/phortune/query/PhortunePurchaseQuery.php @@ -1,110 +1,110 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withCartPHIDs(array $cart_phids) { $this->cartPHIDs = $cart_phids; return $this; } protected function loadPage() { $table = new PhortunePurchase(); $conn = $table->establishConnection('r'); $rows = queryfx_all( $conn, 'SELECT purchase.* FROM %T purchase %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn), $this->buildOrderClause($conn), $this->buildLimitClause($conn)); return $table->loadAllFromArray($rows); } protected function willFilterPage(array $purchases) { $carts = id(new PhortuneCartQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withPHIDs(mpull($purchases, 'getCartPHID')) ->execute(); $carts = mpull($carts, null, 'getPHID'); foreach ($purchases as $key => $purchase) { $cart = idx($carts, $purchase->getCartPHID()); if (!$cart) { unset($purchases[$key]); continue; } $purchase->attachCart($cart); } $products = id(new PhortuneProductQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withPHIDs(mpull($purchases, 'getProductPHID')) ->execute(); $products = mpull($products, null, 'getPHID'); foreach ($purchases as $key => $purchase) { $product = idx($products, $purchase->getProductPHID()); if (!$product) { unset($purchases[$key]); continue; } $purchase->attachProduct($product); } return $purchases; } protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); $where[] = $this->buildPagingClause($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'purchase.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'purchase.phid IN (%Ls)', $this->phids); } if ($this->cartPHIDs !== null) { $where[] = qsprintf( $conn, 'purchase.cartPHID IN (%Ls)', $this->cartPHIDs); } - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } public function getQueryApplicationClass() { return 'PhabricatorPhortuneApplication'; } } diff --git a/src/applications/phortune/query/PhortuneSubscriptionQuery.php b/src/applications/phortune/query/PhortuneSubscriptionQuery.php index e8b39c0fee..6919e6a169 100644 --- a/src/applications/phortune/query/PhortuneSubscriptionQuery.php +++ b/src/applications/phortune/query/PhortuneSubscriptionQuery.php @@ -1,192 +1,192 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withAccountPHIDs(array $account_phids) { $this->accountPHIDs = $account_phids; return $this; } public function withMerchantPHIDs(array $merchant_phids) { $this->merchantPHIDs = $merchant_phids; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } public function needTriggers($need_triggers) { $this->needTriggers = $need_triggers; return $this; } protected function loadPage() { $table = new PhortuneSubscription(); $conn = $table->establishConnection('r'); $rows = queryfx_all( $conn, 'SELECT subscription.* FROM %T subscription %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn), $this->buildOrderClause($conn), $this->buildLimitClause($conn)); return $table->loadAllFromArray($rows); } protected function willFilterPage(array $subscriptions) { $accounts = id(new PhortuneAccountQuery()) ->setViewer($this->getViewer()) ->withPHIDs(mpull($subscriptions, 'getAccountPHID')) ->execute(); $accounts = mpull($accounts, null, 'getPHID'); foreach ($subscriptions as $key => $subscription) { $account = idx($accounts, $subscription->getAccountPHID()); if (!$account) { unset($subscriptions[$key]); continue; } $subscription->attachAccount($account); } if (!$subscriptions) { return $subscriptions; } $merchants = id(new PhortuneMerchantQuery()) ->setViewer($this->getViewer()) ->withPHIDs(mpull($subscriptions, 'getMerchantPHID')) ->execute(); $merchants = mpull($merchants, null, 'getPHID'); foreach ($subscriptions as $key => $subscription) { $merchant = idx($merchants, $subscription->getMerchantPHID()); if (!$merchant) { unset($subscriptions[$key]); continue; } $subscription->attachMerchant($merchant); } if (!$subscriptions) { return $subscriptions; } $implementations = array(); $subscription_map = mgroup($subscriptions, 'getSubscriptionClass'); foreach ($subscription_map as $class => $class_subscriptions) { $sub = newv($class, array()); $impl_objects = $sub->loadImplementationsForRefs( $this->getViewer(), mpull($class_subscriptions, 'getSubscriptionRef')); $implementations += mpull($impl_objects, null, 'getRef'); } foreach ($subscriptions as $key => $subscription) { $ref = $subscription->getSubscriptionRef(); $implementation = idx($implementations, $ref); if (!$implementation) { unset($subscriptions[$key]); continue; } $subscription->attachImplementation($implementation); } if (!$subscriptions) { return $subscriptions; } if ($this->needTriggers) { $trigger_phids = mpull($subscriptions, 'getTriggerPHID'); $triggers = id(new PhabricatorWorkerTriggerQuery()) ->setViewer($this->getViewer()) ->withPHIDs($trigger_phids) ->needEvents(true) ->execute(); $triggers = mpull($triggers, null, 'getPHID'); foreach ($subscriptions as $key => $subscription) { $trigger = idx($triggers, $subscription->getTriggerPHID()); if (!$trigger) { unset($subscriptions[$key]); continue; } $subscription->attachTrigger($trigger); } } return $subscriptions; } protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); $where[] = $this->buildPagingClause($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'subscription.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'subscription.phid IN (%Ls)', $this->phids); } if ($this->accountPHIDs !== null) { $where[] = qsprintf( $conn, 'subscription.accountPHID IN (%Ls)', $this->accountPHIDs); } if ($this->merchantPHIDs !== null) { $where[] = qsprintf( $conn, 'subscription.merchantPHID IN (%Ls)', $this->merchantPHIDs); } if ($this->statuses !== null) { $where[] = qsprintf( $conn, 'subscription.status IN (%Ls)', $this->statuses); } - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } public function getQueryApplicationClass() { return 'PhabricatorPhortuneApplication'; } } diff --git a/src/applications/phragment/query/PhragmentFragmentQuery.php b/src/applications/phragment/query/PhragmentFragmentQuery.php index d6dc67609d..56444217db 100644 --- a/src/applications/phragment/query/PhragmentFragmentQuery.php +++ b/src/applications/phragment/query/PhragmentFragmentQuery.php @@ -1,130 +1,130 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withPaths(array $paths) { $this->paths = $paths; return $this; } public function withLeadingPath($path) { $this->leadingPath = $path; return $this; } public function withDepths($depths) { $this->depths = $depths; return $this; } public function needLatestVersion($need_latest_version) { $this->needLatestVersion = $need_latest_version; return $this; } protected function loadPage() { $table = new PhragmentFragment(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } if ($this->paths) { $where[] = qsprintf( - $conn_r, + $conn, 'path IN (%Ls)', $this->paths); } if ($this->leadingPath) { $where[] = qsprintf( - $conn_r, + $conn, 'path LIKE %>', $this->leadingPath); } if ($this->depths) { $where[] = qsprintf( - $conn_r, + $conn, 'depth IN (%Ld)', $this->depths); } - $where[] = $this->buildPagingClause($conn_r); + $where[] = $this->buildPagingClause($conn); - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } protected function didFilterPage(array $page) { if ($this->needLatestVersion) { $versions = array(); $version_phids = array_filter(mpull($page, 'getLatestVersionPHID')); if ($version_phids) { $versions = id(new PhabricatorObjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs($version_phids) ->setParentQuery($this) ->execute(); $versions = mpull($versions, null, 'getPHID'); } foreach ($page as $key => $fragment) { $version_phid = $fragment->getLatestVersionPHID(); if (empty($versions[$version_phid])) { continue; } $fragment->attachLatestVersion($versions[$version_phid]); } } return $page; } public function getQueryApplicationClass() { return 'PhabricatorPhragmentApplication'; } } diff --git a/src/applications/phragment/query/PhragmentFragmentVersionQuery.php b/src/applications/phragment/query/PhragmentFragmentVersionQuery.php index a874a8be1a..e95c3260a8 100644 --- a/src/applications/phragment/query/PhragmentFragmentVersionQuery.php +++ b/src/applications/phragment/query/PhragmentFragmentVersionQuery.php @@ -1,123 +1,123 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withFragmentPHIDs(array $fragment_phids) { $this->fragmentPHIDs = $fragment_phids; return $this; } public function withSequences(array $sequences) { $this->sequences = $sequences; return $this; } public function withSequenceBefore($current) { $this->sequenceBefore = $current; return $this; } protected function loadPage() { $table = new PhragmentFragmentVersion(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } if ($this->fragmentPHIDs) { $where[] = qsprintf( - $conn_r, + $conn, 'fragmentPHID IN (%Ls)', $this->fragmentPHIDs); } if ($this->sequences) { $where[] = qsprintf( - $conn_r, + $conn, 'sequence IN (%Ld)', $this->sequences); } if ($this->sequenceBefore !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'sequence < %d', $this->sequenceBefore); } - $where[] = $this->buildPagingClause($conn_r); + $where[] = $this->buildPagingClause($conn); - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } protected function willFilterPage(array $page) { $fragments = array(); $fragment_phids = array_filter(mpull($page, 'getFragmentPHID')); if ($fragment_phids) { $fragments = id(new PhabricatorObjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs($fragment_phids) ->setParentQuery($this) ->execute(); $fragments = mpull($fragments, null, 'getPHID'); } foreach ($page as $key => $version) { $fragment_phid = $version->getFragmentPHID(); if (empty($fragments[$fragment_phid])) { unset($page[$key]); continue; } $version->attachFragment($fragments[$fragment_phid]); } return $page; } public function getQueryApplicationClass() { return 'PhabricatorPhragmentApplication'; } } diff --git a/src/applications/phragment/query/PhragmentSnapshotChildQuery.php b/src/applications/phragment/query/PhragmentSnapshotChildQuery.php index 032fb0c49c..faa3493499 100644 --- a/src/applications/phragment/query/PhragmentSnapshotChildQuery.php +++ b/src/applications/phragment/query/PhragmentSnapshotChildQuery.php @@ -1,174 +1,174 @@ ids = $ids; return $this; } public function withSnapshotPHIDs(array $snapshot_phids) { $this->snapshotPHIDs = $snapshot_phids; return $this; } public function withFragmentPHIDs(array $fragment_phids) { $this->fragmentPHIDs = $fragment_phids; return $this; } public function withFragmentVersionPHIDs(array $fragment_version_phids) { $this->fragmentVersionPHIDs = $fragment_version_phids; return $this; } public function needFragments($need_fragments) { $this->needFragments = $need_fragments; return $this; } public function needFragmentVersions($need_fragment_versions) { $this->needFragmentVersions = $need_fragment_versions; return $this; } protected function loadPage() { $table = new PhragmentSnapshotChild(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->snapshotPHIDs) { $where[] = qsprintf( - $conn_r, + $conn, 'snapshotPHID IN (%Ls)', $this->snapshotPHIDs); } if ($this->fragmentPHIDs) { $where[] = qsprintf( - $conn_r, + $conn, 'fragmentPHID IN (%Ls)', $this->fragmentPHIDs); } if ($this->fragmentVersionPHIDs) { $where[] = qsprintf( - $conn_r, + $conn, 'fragmentVersionPHID IN (%Ls)', $this->fragmentVersionPHIDs); } - $where[] = $this->buildPagingClause($conn_r); + $where[] = $this->buildPagingClause($conn); - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } protected function willFilterPage(array $page) { $snapshots = array(); $snapshot_phids = array_filter(mpull($page, 'getSnapshotPHID')); if ($snapshot_phids) { $snapshots = id(new PhabricatorObjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs($snapshot_phids) ->setParentQuery($this) ->execute(); $snapshots = mpull($snapshots, null, 'getPHID'); } foreach ($page as $key => $child) { $snapshot_phid = $child->getSnapshotPHID(); if (empty($snapshots[$snapshot_phid])) { unset($page[$key]); continue; } $child->attachSnapshot($snapshots[$snapshot_phid]); } return $page; } protected function didFilterPage(array $page) { if ($this->needFragments) { $fragments = array(); $fragment_phids = array_filter(mpull($page, 'getFragmentPHID')); if ($fragment_phids) { $fragments = id(new PhabricatorObjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs($fragment_phids) ->setParentQuery($this) ->execute(); $fragments = mpull($fragments, null, 'getPHID'); } foreach ($page as $key => $child) { $fragment_phid = $child->getFragmentPHID(); if (empty($fragments[$fragment_phid])) { unset($page[$key]); continue; } $child->attachFragment($fragments[$fragment_phid]); } } if ($this->needFragmentVersions) { $fragment_versions = array(); $fragment_version_phids = array_filter(mpull( $page, 'getFragmentVersionPHID')); if ($fragment_version_phids) { $fragment_versions = id(new PhabricatorObjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs($fragment_version_phids) ->setParentQuery($this) ->execute(); $fragment_versions = mpull($fragment_versions, null, 'getPHID'); } foreach ($page as $key => $child) { $fragment_version_phid = $child->getFragmentVersionPHID(); if (empty($fragment_versions[$fragment_version_phid])) { continue; } $child->attachFragmentVersion( $fragment_versions[$fragment_version_phid]); } } return $page; } public function getQueryApplicationClass() { return 'PhabricatorPhragmentApplication'; } } diff --git a/src/applications/phragment/query/PhragmentSnapshotQuery.php b/src/applications/phragment/query/PhragmentSnapshotQuery.php index d6f9d3422f..a4805650fc 100644 --- a/src/applications/phragment/query/PhragmentSnapshotQuery.php +++ b/src/applications/phragment/query/PhragmentSnapshotQuery.php @@ -1,111 +1,111 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withPrimaryFragmentPHIDs(array $primary_fragment_phids) { $this->primaryFragmentPHIDs = $primary_fragment_phids; return $this; } public function withNames(array $names) { $this->names = $names; return $this; } protected function loadPage() { $table = new PhragmentSnapshot(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); - if ($this->ids) { + if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } - if ($this->phids) { + if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } - if ($this->primaryFragmentPHIDs) { + if ($this->primaryFragmentPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'primaryFragmentPHID IN (%Ls)', $this->primaryFragmentPHIDs); } - if ($this->names) { + if ($this->names !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'name IN (%Ls)', $this->names); } - $where[] = $this->buildPagingClause($conn_r); + $where[] = $this->buildPagingClause($conn); - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } protected function willFilterPage(array $page) { $fragments = array(); $fragment_phids = array_filter(mpull($page, 'getPrimaryFragmentPHID')); if ($fragment_phids) { $fragments = id(new PhabricatorObjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs($fragment_phids) ->setParentQuery($this) ->execute(); $fragments = mpull($fragments, null, 'getPHID'); } foreach ($page as $key => $snapshot) { $fragment_phid = $snapshot->getPrimaryFragmentPHID(); if (empty($fragments[$fragment_phid])) { unset($page[$key]); continue; } $snapshot->attachPrimaryFragment($fragments[$fragment_phid]); } return $page; } public function getQueryApplicationClass() { return 'PhabricatorPhragmentApplication'; } } diff --git a/src/applications/phrequent/query/PhrequentUserTimeQuery.php b/src/applications/phrequent/query/PhrequentUserTimeQuery.php index d0d1160df0..cf5122c020 100644 --- a/src/applications/phrequent/query/PhrequentUserTimeQuery.php +++ b/src/applications/phrequent/query/PhrequentUserTimeQuery.php @@ -1,333 +1,333 @@ ids = $ids; return $this; } public function withUserPHIDs(array $user_phids) { $this->userPHIDs = $user_phids; return $this; } public function withObjectPHIDs(array $object_phids) { $this->objectPHIDs = $object_phids; return $this; } public function withEnded($ended) { $this->ended = $ended; return $this; } public function setOrder($order) { switch ($order) { case self::ORDER_ID_ASC: $this->setOrderVector(array('-id')); break; case self::ORDER_ID_DESC: $this->setOrderVector(array('id')); break; case self::ORDER_STARTED_ASC: $this->setOrderVector(array('-start', '-id')); break; case self::ORDER_STARTED_DESC: $this->setOrderVector(array('start', 'id')); break; case self::ORDER_ENDED_ASC: $this->setOrderVector(array('-end', '-id')); break; case self::ORDER_ENDED_DESC: $this->setOrderVector(array('end', 'id')); break; default: throw new Exception(pht('Unknown order "%s".', $order)); } return $this; } public function needPreemptingEvents($need_events) { $this->needPreemptingEvents = $need_events; return $this; } protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'id IN (%Ld)', $this->ids); } if ($this->userPHIDs !== null) { $where[] = qsprintf( $conn, 'userPHID IN (%Ls)', $this->userPHIDs); } if ($this->objectPHIDs !== null) { $where[] = qsprintf( $conn, 'objectPHID IN (%Ls)', $this->objectPHIDs); } switch ($this->ended) { case self::ENDED_ALL: break; case self::ENDED_YES: $where[] = qsprintf( $conn, 'dateEnded IS NOT NULL'); break; case self::ENDED_NO: $where[] = qsprintf( $conn, 'dateEnded IS NULL'); break; default: throw new Exception(pht("Unknown ended '%s'!", $this->ended)); } $where[] = $this->buildPagingClause($conn); - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } public function getOrderableColumns() { return parent::getOrderableColumns() + array( 'start' => array( 'column' => 'dateStarted', 'type' => 'int', ), 'end' => array( 'column' => 'dateEnded', 'type' => 'int', 'null' => 'head', ), ); } protected function getPagingValueMap($cursor, array $keys) { $usertime = $this->loadCursorObject($cursor); return array( 'id' => $usertime->getID(), 'start' => $usertime->getDateStarted(), 'end' => $usertime->getDateEnded(), ); } protected function loadPage() { $usertime = new PhrequentUserTime(); $conn = $usertime->establishConnection('r'); $data = queryfx_all( $conn, 'SELECT usertime.* FROM %T usertime %Q %Q %Q', $usertime->getTableName(), $this->buildWhereClause($conn), $this->buildOrderClause($conn), $this->buildLimitClause($conn)); return $usertime->loadAllFromArray($data); } protected function didFilterPage(array $page) { if ($this->needPreemptingEvents) { $usertime = new PhrequentUserTime(); $conn_r = $usertime->establishConnection('r'); $preempt = array(); foreach ($page as $event) { $preempt[] = qsprintf( $conn_r, '(userPHID = %s AND (dateStarted BETWEEN %d AND %d) AND (dateEnded IS NULL OR dateEnded > %d))', $event->getUserPHID(), $event->getDateStarted(), nonempty($event->getDateEnded(), PhabricatorTime::getNow()), $event->getDateStarted()); } $preempting_events = queryfx_all( $conn_r, - 'SELECT * FROM %T WHERE %Q ORDER BY dateStarted ASC, id ASC', + 'SELECT * FROM %T WHERE %LO ORDER BY dateStarted ASC, id ASC', $usertime->getTableName(), - implode(' OR ', $preempt)); + $preempt); $preempting_events = $usertime->loadAllFromArray($preempting_events); $preempting_events = mgroup($preempting_events, 'getUserPHID'); foreach ($page as $event) { $e_start = $event->getDateStarted(); $e_end = $event->getDateEnded(); $select = array(); $user_events = idx($preempting_events, $event->getUserPHID(), array()); foreach ($user_events as $u_event) { if ($u_event->getID() == $event->getID()) { // Don't allow an event to preempt itself. continue; } $u_start = $u_event->getDateStarted(); $u_end = $u_event->getDateEnded(); if ($u_start < $e_start) { // This event started before our event started, so it's not // preempting us. continue; } if ($u_start == $e_start) { if ($u_event->getID() < $event->getID()) { // This event started at the same time as our event started, // but has a lower ID, so it's not preempting us. continue; } } if (($e_end !== null) && ($u_start > $e_end)) { // Our event has ended, and this event started after it ended. continue; } if (($u_end !== null) && ($u_end < $e_start)) { // This event ended before our event began. continue; } $select[] = $u_event; } $event->attachPreemptingEvents($select); } } return $page; } /* -( Helper Functions ) --------------------------------------------------- */ public static function getEndedSearchOptions() { return array( self::ENDED_ALL => pht('All'), self::ENDED_NO => pht('No'), self::ENDED_YES => pht('Yes'), ); } public static function getOrderSearchOptions() { return array( self::ORDER_STARTED_ASC => pht('by furthest start date'), self::ORDER_STARTED_DESC => pht('by nearest start date'), self::ORDER_ENDED_ASC => pht('by furthest end date'), self::ORDER_ENDED_DESC => pht('by nearest end date'), ); } public static function getUserTotalObjectsTracked( PhabricatorUser $user, $limit = PHP_INT_MAX) { $usertime_dao = new PhrequentUserTime(); $conn = $usertime_dao->establishConnection('r'); $count = queryfx_one( $conn, 'SELECT COUNT(usertime.id) N FROM %T usertime '. 'WHERE usertime.userPHID = %s '. 'AND usertime.dateEnded IS NULL '. 'LIMIT %d', $usertime_dao->getTableName(), $user->getPHID(), $limit); return $count['N']; } public static function isUserTrackingObject( PhabricatorUser $user, $phid) { $usertime_dao = new PhrequentUserTime(); $conn = $usertime_dao->establishConnection('r'); $count = queryfx_one( $conn, 'SELECT COUNT(usertime.id) N FROM %T usertime '. 'WHERE usertime.userPHID = %s '. 'AND usertime.objectPHID = %s '. 'AND usertime.dateEnded IS NULL', $usertime_dao->getTableName(), $user->getPHID(), $phid); return $count['N'] > 0; } public static function getUserTimeSpentOnObject( PhabricatorUser $user, $phid) { $usertime_dao = new PhrequentUserTime(); $conn = $usertime_dao->establishConnection('r'); // First calculate all the time spent where the // usertime blocks have ended. $sum_ended = queryfx_one( $conn, 'SELECT SUM(usertime.dateEnded - usertime.dateStarted) N '. 'FROM %T usertime '. 'WHERE usertime.userPHID = %s '. 'AND usertime.objectPHID = %s '. 'AND usertime.dateEnded IS NOT NULL', $usertime_dao->getTableName(), $user->getPHID(), $phid); // Now calculate the time spent where the usertime // blocks have not yet ended. $sum_not_ended = queryfx_one( $conn, 'SELECT SUM(UNIX_TIMESTAMP() - usertime.dateStarted) N '. 'FROM %T usertime '. 'WHERE usertime.userPHID = %s '. 'AND usertime.objectPHID = %s '. 'AND usertime.dateEnded IS NULL', $usertime_dao->getTableName(), $user->getPHID(), $phid); return $sum_ended['N'] + $sum_not_ended['N']; } public function getQueryApplicationClass() { return 'PhabricatorPhrequentApplication'; } } diff --git a/src/applications/phriction/query/PhrictionDocumentQuery.php b/src/applications/phriction/query/PhrictionDocumentQuery.php index 8a0fa325ae..5f508ad804 100644 --- a/src/applications/phriction/query/PhrictionDocumentQuery.php +++ b/src/applications/phriction/query/PhrictionDocumentQuery.php @@ -1,390 +1,394 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withSlugs(array $slugs) { $this->slugs = $slugs; return $this; } public function withDepths(array $depths) { $this->depths = $depths; return $this; } public function withSlugPrefix($slug_prefix) { $this->slugPrefix = $slug_prefix; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } public function withParentPaths(array $paths) { $this->parentPaths = $paths; return $this; } public function withAncestorPaths(array $paths) { $this->ancestorPaths = $paths; return $this; } public function needContent($need_content) { $this->needContent = $need_content; return $this; } protected function loadPage() { return $this->loadStandardPage($this->newResultObject()); } public function newResultObject() { return new PhrictionDocument(); } protected function willFilterPage(array $documents) { if ($documents) { $ancestor_slugs = array(); foreach ($documents as $key => $document) { $document_slug = $document->getSlug(); foreach (PhabricatorSlug::getAncestry($document_slug) as $ancestor) { $ancestor_slugs[$ancestor][] = $key; } } if ($ancestor_slugs) { $table = new PhrictionDocument(); $conn_r = $table->establishConnection('r'); $ancestors = queryfx_all( $conn_r, 'SELECT * FROM %T WHERE slug IN (%Ls)', $document->getTableName(), array_keys($ancestor_slugs)); $ancestors = $table->loadAllFromArray($ancestors); $ancestors = mpull($ancestors, null, 'getSlug'); foreach ($ancestor_slugs as $ancestor_slug => $document_keys) { $ancestor = idx($ancestors, $ancestor_slug); foreach ($document_keys as $document_key) { $documents[$document_key]->attachAncestor( $ancestor_slug, $ancestor); } } } } // To view a Phriction document, you must also be able to view all of the // ancestor documents. Filter out documents which have ancestors that are // not visible. $document_map = array(); foreach ($documents as $document) { $document_map[$document->getSlug()] = $document; foreach ($document->getAncestors() as $key => $ancestor) { if ($ancestor) { $document_map[$key] = $ancestor; } } } $filtered_map = $this->applyPolicyFilter( $document_map, array(PhabricatorPolicyCapability::CAN_VIEW)); // Filter all of the documents where a parent is not visible. foreach ($documents as $document_key => $document) { // If the document itself is not visible, filter it. if (!isset($filtered_map[$document->getSlug()])) { $this->didRejectResult($documents[$document_key]); unset($documents[$document_key]); continue; } // If an ancestor exists but is not visible, filter the document. foreach ($document->getAncestors() as $ancestor_key => $ancestor) { if (!$ancestor) { continue; } if (!isset($filtered_map[$ancestor_key])) { $this->didRejectResult($documents[$document_key]); unset($documents[$document_key]); break; } } } if (!$documents) { return $documents; } if ($this->needContent) { $contents = id(new PhrictionContentQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withPHIDs(mpull($documents, 'getContentPHID')) ->execute(); $contents = mpull($contents, null, 'getPHID'); foreach ($documents as $key => $document) { $content_phid = $document->getContentPHID(); if (empty($contents[$content_phid])) { unset($documents[$key]); continue; } $document->attachContent($contents[$content_phid]); } } return $documents; } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { $joins = parent::buildJoinClauseParts($conn); if ($this->getOrderVector()->containsKey('updated')) { $content_dao = new PhrictionContent(); $joins[] = qsprintf( $conn, 'JOIN %T c ON d.contentPHID = c.phid', $content_dao->getTableName()); } return $joins; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'd.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'd.phid IN (%Ls)', $this->phids); } if ($this->slugs !== null) { $where[] = qsprintf( $conn, 'd.slug IN (%Ls)', $this->slugs); } if ($this->statuses !== null) { $where[] = qsprintf( $conn, 'd.status IN (%Ls)', $this->statuses); } if ($this->slugPrefix !== null) { $where[] = qsprintf( $conn, 'd.slug LIKE %>', $this->slugPrefix); } if ($this->depths !== null) { $where[] = qsprintf( $conn, 'd.depth IN (%Ld)', $this->depths); } if ($this->parentPaths !== null || $this->ancestorPaths !== null) { $sets = array( array( 'paths' => $this->parentPaths, 'parents' => true, ), array( 'paths' => $this->ancestorPaths, 'parents' => false, ), ); $paths = array(); foreach ($sets as $set) { $set_paths = $set['paths']; if ($set_paths === null) { continue; } if (!$set_paths) { throw new PhabricatorEmptyQueryException( pht('No parent/ancestor paths specified.')); } $is_parents = $set['parents']; foreach ($set_paths as $path) { $path_normal = PhabricatorSlug::normalize($path); if ($path !== $path_normal) { throw new Exception( pht( 'Document path "%s" is not a valid path. The normalized '. 'form of this path is "%s".', $path, $path_normal)); } $depth = PhabricatorSlug::getDepth($path_normal); if ($is_parents) { $min_depth = $depth + 1; $max_depth = $depth + 1; } else { $min_depth = $depth + 1; $max_depth = null; } $paths[] = array( $path_normal, $min_depth, $max_depth, ); } } $path_clauses = array(); foreach ($paths as $path) { $parts = array(); list($prefix, $min, $max) = $path; // If we're getting children or ancestors of the root document, they // aren't actually stored with the leading "/" in the database, so // just skip this part of the clause. if ($prefix !== '/') { $parts[] = qsprintf( $conn, 'd.slug LIKE %>', $prefix); } if ($min !== null) { $parts[] = qsprintf( $conn, 'd.depth >= %d', $min); } if ($max !== null) { $parts[] = qsprintf( $conn, 'd.depth <= %d', $max); } - $path_clauses[] = '('.implode(') AND (', $parts).')'; + if ($parts) { + $path_clauses[] = qsprintf($conn, '%LA', $parts); + } } - $where[] = '('.implode(') OR (', $path_clauses).')'; + if ($path_clauses) { + $where[] = qsprintf($conn, '%LO', $path_clauses); + } } return $where; } public function getBuiltinOrders() { return parent::getBuiltinOrders() + array( self::ORDER_HIERARCHY => array( 'vector' => array('depth', 'title', 'updated', 'id'), 'name' => pht('Hierarchy'), ), ); } public function getOrderableColumns() { return parent::getOrderableColumns() + array( 'depth' => array( 'table' => 'd', 'column' => 'depth', 'reverse' => true, 'type' => 'int', ), 'title' => array( 'table' => 'c', 'column' => 'title', 'reverse' => true, 'type' => 'string', ), 'updated' => array( 'table' => 'd', 'column' => 'editedEpoch', 'type' => 'int', 'unique' => false, ), ); } protected function getPagingValueMap($cursor, array $keys) { $document = $this->loadCursorObject($cursor); $map = array( 'id' => $document->getID(), 'depth' => $document->getDepth(), 'updated' => $document->getEditedEpoch(), ); foreach ($keys as $key) { switch ($key) { case 'title': $map[$key] = $document->getContent()->getTitle(); break; } } return $map; } protected function willExecuteCursorQuery( PhabricatorCursorPagedPolicyAwareQuery $query) { $vector = $this->getOrderVector(); if ($vector->containsKey('title')) { $query->needContent(true); } } protected function getPrimaryTableAlias() { return 'd'; } public function getQueryApplicationClass() { return 'PhabricatorPhrictionApplication'; } } diff --git a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php index 248677fbec..863f8c0e8c 100644 --- a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php +++ b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php @@ -1,612 +1,612 @@ viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setBoardPHIDs(array $board_phids) { $this->boardPHIDs = array_fuse($board_phids); return $this; } public function getBoardPHIDs() { return $this->boardPHIDs; } public function setObjectPHIDs(array $object_phids) { $this->objectPHIDs = array_fuse($object_phids); return $this; } public function getObjectPHIDs() { return $this->objectPHIDs; } /** * Fetch all boards, even if the board is disabled. */ public function setFetchAllBoards($fetch_all) { $this->fetchAllBoards = $fetch_all; return $this; } public function getFetchAllBoards() { return $this->fetchAllBoards; } public function executeLayout() { $viewer = $this->getViewer(); $boards = $this->loadBoards(); if (!$boards) { return $this; } $columns = $this->loadColumns($boards); $positions = $this->loadPositions($boards); foreach ($boards as $board_phid => $board) { $board_columns = idx($columns, $board_phid); // Don't layout boards with no columns. These boards need to be formally // created first. if (!$columns) { continue; } $board_positions = idx($positions, $board_phid, array()); $this->layoutBoard($board, $board_columns, $board_positions); } return $this; } public function getColumns($board_phid) { $columns = idx($this->boardLayout, $board_phid, array()); return array_select_keys($this->columnMap, array_keys($columns)); } public function getColumnObjectPositions($board_phid, $column_phid) { $columns = idx($this->boardLayout, $board_phid, array()); return idx($columns, $column_phid, array()); } public function getColumnObjectPHIDs($board_phid, $column_phid) { $positions = $this->getColumnObjectPositions($board_phid, $column_phid); return mpull($positions, 'getObjectPHID'); } public function getObjectColumns($board_phid, $object_phid) { $board_map = idx($this->objectColumnMap, $board_phid, array()); $column_phids = idx($board_map, $object_phid); if (!$column_phids) { return array(); } return array_select_keys($this->columnMap, $column_phids); } public function queueRemovePosition( $board_phid, $column_phid, $object_phid) { $board_layout = idx($this->boardLayout, $board_phid, array()); $positions = idx($board_layout, $column_phid, array()); $position = idx($positions, $object_phid); if ($position) { $this->remQueue[] = $position; // If this position hasn't been saved yet, get it out of the add queue. if (!$position->getID()) { foreach ($this->addQueue as $key => $add_position) { if ($add_position === $position) { unset($this->addQueue[$key]); } } } } unset($this->boardLayout[$board_phid][$column_phid][$object_phid]); return $this; } public function queueAddPositionBefore( $board_phid, $column_phid, $object_phid, $before_phid) { return $this->queueAddPositionRelative( $board_phid, $column_phid, $object_phid, $before_phid, true); } public function queueAddPositionAfter( $board_phid, $column_phid, $object_phid, $after_phid) { return $this->queueAddPositionRelative( $board_phid, $column_phid, $object_phid, $after_phid, false); } public function queueAddPosition( $board_phid, $column_phid, $object_phid) { return $this->queueAddPositionRelative( $board_phid, $column_phid, $object_phid, null, true); } private function queueAddPositionRelative( $board_phid, $column_phid, $object_phid, $relative_phid, $is_before) { $board_layout = idx($this->boardLayout, $board_phid, array()); $positions = idx($board_layout, $column_phid, array()); // Check if the object is already in the column, and remove it if it is. $object_position = idx($positions, $object_phid); unset($positions[$object_phid]); if (!$object_position) { $object_position = id(new PhabricatorProjectColumnPosition()) ->setBoardPHID($board_phid) ->setColumnPHID($column_phid) ->setObjectPHID($object_phid); } $found = false; if (!$positions) { $object_position->setSequence(0); } else { foreach ($positions as $position) { if (!$found) { if ($relative_phid === null) { $is_match = true; } else { $position_phid = $position->getObjectPHID(); $is_match = ($relative_phid == $position_phid); } if ($is_match) { $found = true; $sequence = $position->getSequence(); if (!$is_before) { $sequence++; } $object_position->setSequence($sequence++); if (!$is_before) { // If we're inserting after this position, continue the loop so // we don't update it. continue; } } } if ($found) { $position->setSequence($sequence++); $this->addQueue[] = $position; } } } if ($relative_phid && !$found) { throw new Exception( pht( 'Unable to find object "%s" in column "%s" on board "%s".', $relative_phid, $column_phid, $board_phid)); } $this->addQueue[] = $object_position; $positions[$object_phid] = $object_position; $positions = msort($positions, 'getOrderingKey'); $this->boardLayout[$board_phid][$column_phid] = $positions; return $this; } public function applyPositionUpdates() { foreach ($this->remQueue as $position) { if ($position->getID()) { $position->delete(); } } $this->remQueue = array(); $adds = array(); $updates = array(); foreach ($this->addQueue as $position) { $id = $position->getID(); if ($id) { $updates[$id] = $position; } else { $adds[] = $position; } } $this->addQueue = array(); $table = new PhabricatorProjectColumnPosition(); $conn_w = $table->establishConnection('w'); $pairs = array(); foreach ($updates as $id => $position) { // This is ugly because MySQL gets upset with us if it is configured // strictly and we attempt inserts which can't work. We'll never actually // do these inserts since they'll always collide (triggering the ON // DUPLICATE KEY logic), so we just provide dummy values in order to get // there. $pairs[] = qsprintf( $conn_w, '(%d, %d, "", "", "")', $id, $position->getSequence()); } if ($pairs) { queryfx( $conn_w, 'INSERT INTO %T (id, sequence, boardPHID, columnPHID, objectPHID) - VALUES %Q ON DUPLICATE KEY UPDATE sequence = VALUES(sequence)', + VALUES %LQ ON DUPLICATE KEY UPDATE sequence = VALUES(sequence)', $table->getTableName(), - implode(', ', $pairs)); + $pairs); } foreach ($adds as $position) { $position->save(); } return $this; } private function loadBoards() { $viewer = $this->getViewer(); $board_phids = $this->getBoardPHIDs(); $boards = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs($board_phids) ->execute(); $boards = mpull($boards, null, 'getPHID'); if (!$this->fetchAllBoards) { foreach ($boards as $key => $board) { if (!$board->getHasWorkboard()) { unset($boards[$key]); } } } return $boards; } private function loadColumns(array $boards) { $viewer = $this->getViewer(); $columns = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withProjectPHIDs(array_keys($boards)) ->execute(); $columns = msort($columns, 'getOrderingKey'); $columns = mpull($columns, null, 'getPHID'); $need_children = array(); foreach ($boards as $phid => $board) { if ($board->getHasMilestones() || $board->getHasSubprojects()) { $need_children[] = $phid; } } if ($need_children) { $children = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withParentProjectPHIDs($need_children) ->execute(); $children = mpull($children, null, 'getPHID'); $children = mgroup($children, 'getParentProjectPHID'); } else { $children = array(); } $columns = mgroup($columns, 'getProjectPHID'); foreach ($boards as $board_phid => $board) { $board_columns = idx($columns, $board_phid, array()); // If the project has milestones, create any missing columns. if ($board->getHasMilestones() || $board->getHasSubprojects()) { $child_projects = idx($children, $board_phid, array()); if ($board_columns) { $next_sequence = last($board_columns)->getSequence() + 1; } else { $next_sequence = 1; } $proxy_columns = mpull($board_columns, null, 'getProxyPHID'); foreach ($child_projects as $child_phid => $child) { if (isset($proxy_columns[$child_phid])) { continue; } $new_column = PhabricatorProjectColumn::initializeNewColumn($viewer) ->attachProject($board) ->attachProxy($child) ->setSequence($next_sequence++) ->setProjectPHID($board_phid) ->setProxyPHID($child_phid); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $new_column->save(); unset($unguarded); $board_columns[$new_column->getPHID()] = $new_column; } } $board_columns = msort($board_columns, 'getOrderingKey'); $columns[$board_phid] = $board_columns; } foreach ($columns as $board_phid => $board_columns) { foreach ($board_columns as $board_column) { $column_phid = $board_column->getPHID(); $this->columnMap[$column_phid] = $board_column; } } return $columns; } private function loadPositions(array $boards) { $viewer = $this->getViewer(); $object_phids = $this->getObjectPHIDs(); if (!$object_phids) { return array(); } $positions = id(new PhabricatorProjectColumnPositionQuery()) ->setViewer($viewer) ->withBoardPHIDs(array_keys($boards)) ->withObjectPHIDs($object_phids) ->execute(); $positions = msort($positions, 'getOrderingKey'); $positions = mgroup($positions, 'getBoardPHID'); return $positions; } private function layoutBoard( $board, array $columns, array $positions) { $viewer = $this->getViewer(); $board_phid = $board->getPHID(); $position_groups = mgroup($positions, 'getObjectPHID'); $layout = array(); $default_phid = null; foreach ($columns as $column) { $column_phid = $column->getPHID(); $layout[$column_phid] = array(); if ($column->isDefaultColumn()) { $default_phid = $column_phid; } } // Find all the columns which are proxies for other objects. $proxy_map = array(); foreach ($columns as $column) { $proxy_phid = $column->getProxyPHID(); if ($proxy_phid) { $proxy_map[$proxy_phid] = $column->getPHID(); } } $object_phids = $this->getObjectPHIDs(); // If we have proxies, we need to force cards into the correct proxy // columns. if ($proxy_map && $object_phids) { $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($object_phids) ->withEdgeTypes( array( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, )); $edge_query->execute(); $project_phids = $edge_query->getDestinationPHIDs(); $project_phids = array_fuse($project_phids); } else { $project_phids = array(); } if ($project_phids) { $projects = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withPHIDs($project_phids) ->execute(); $projects = mpull($projects, null, 'getPHID'); } else { $projects = array(); } // Build a map from every project that any task is tagged with to the // ancestor project which has a column on this board, if one exists. $ancestor_map = array(); foreach ($projects as $phid => $project) { if (isset($proxy_map[$phid])) { $ancestor_map[$phid] = $proxy_map[$phid]; } else { $seen = array($phid); foreach ($project->getAncestorProjects() as $ancestor) { $ancestor_phid = $ancestor->getPHID(); $seen[] = $ancestor_phid; if (isset($proxy_map[$ancestor_phid])) { foreach ($seen as $project_phid) { $ancestor_map[$project_phid] = $proxy_map[$ancestor_phid]; } } } } } $view_sequence = 1; foreach ($object_phids as $object_phid) { $positions = idx($position_groups, $object_phid, array()); // First, check for objects that have corresponding proxy columns. We're // going to overwrite normal column positions if a tag belongs to a proxy // column, since you can't be in normal columns if you're in proxy // columns. $proxy_hits = array(); if ($proxy_map) { $object_project_phids = $edge_query->getDestinationPHIDs( array( $object_phid, )); foreach ($object_project_phids as $project_phid) { if (isset($ancestor_map[$project_phid])) { $proxy_hits[] = $ancestor_map[$project_phid]; } } } if ($proxy_hits) { // TODO: For now, only one column hit is permissible. $proxy_hits = array_slice($proxy_hits, 0, 1); $proxy_hits = array_fuse($proxy_hits); // Check the object positions: we hope to find a position in each // column the object should be part of. We're going to drop any // invalid positions and create new positions where positions are // missing. foreach ($positions as $key => $position) { $column_phid = $position->getColumnPHID(); if (isset($proxy_hits[$column_phid])) { // Valid column, mark the position as found. unset($proxy_hits[$column_phid]); } else { // Invalid column, ignore the position. unset($positions[$key]); } } // Create new positions for anything we haven't found. foreach ($proxy_hits as $proxy_hit) { $new_position = id(new PhabricatorProjectColumnPosition()) ->setBoardPHID($board_phid) ->setColumnPHID($proxy_hit) ->setObjectPHID($object_phid) ->setSequence(0) ->setViewSequence($view_sequence++); $this->addQueue[] = $new_position; $positions[] = $new_position; } } else { // Ignore any positions in columns which no longer exist. We don't // actively destory them because the rest of the code ignores them and // there's no real need to destroy the data. foreach ($positions as $key => $position) { $column_phid = $position->getColumnPHID(); if (empty($columns[$column_phid])) { unset($positions[$key]); } } // If the object has no position, put it on the default column if // one exists. if (!$positions && $default_phid) { $new_position = id(new PhabricatorProjectColumnPosition()) ->setBoardPHID($board_phid) ->setColumnPHID($default_phid) ->setObjectPHID($object_phid) ->setSequence(0) ->setViewSequence($view_sequence++); $this->addQueue[] = $new_position; $positions = array( $new_position, ); } } foreach ($positions as $position) { $column_phid = $position->getColumnPHID(); $layout[$column_phid][$object_phid] = $position; } } foreach ($layout as $column_phid => $map) { $map = msort($map, 'getOrderingKey'); $layout[$column_phid] = $map; foreach ($map as $object_phid => $position) { $this->objectColumnMap[$board_phid][$object_phid][] = $column_phid; } } $this->boardLayout[$board_phid] = $layout; } } diff --git a/src/applications/project/engine/PhabricatorProjectEditEngine.php b/src/applications/project/engine/PhabricatorProjectEditEngine.php index 5eb6b49b87..1c84932656 100644 --- a/src/applications/project/engine/PhabricatorProjectEditEngine.php +++ b/src/applications/project/engine/PhabricatorProjectEditEngine.php @@ -1,326 +1,326 @@ parentProject = $parent_project; return $this; } public function getParentProject() { return $this->parentProject; } public function setMilestoneProject(PhabricatorProject $milestone_project) { $this->milestoneProject = $milestone_project; return $this; } public function getMilestoneProject() { return $this->milestoneProject; } public function isDefaultQuickCreateEngine() { return true; } public function getQuickCreateOrderVector() { return id(new PhutilSortVector())->addInt(200); } public function getEngineName() { return pht('Projects'); } public function getSummaryHeader() { return pht('Configure Project Forms'); } public function getSummaryText() { return pht('Configure forms for creating projects.'); } public function getEngineApplicationClass() { return 'PhabricatorProjectApplication'; } protected function newEditableObject() { $parent = nonempty($this->parentProject, $this->milestoneProject); return PhabricatorProject::initializeNewProject( $this->getViewer(), $parent); } protected function newObjectQuery() { return id(new PhabricatorProjectQuery()) ->needSlugs(true); } protected function getObjectCreateTitleText($object) { return pht('Create New Project'); } protected function getObjectEditTitleText($object) { return pht('Edit Project: %s', $object->getName()); } protected function getObjectEditShortText($object) { return $object->getName(); } protected function getObjectCreateShortText() { return pht('Create Project'); } protected function getObjectName() { return pht('Project'); } protected function getObjectViewURI($object) { if ($this->getIsCreate()) { return $object->getURI(); } else { $id = $object->getID(); return "/project/manage/{$id}/"; } } protected function getObjectCreateCancelURI($object) { $parent = $this->getParentProject(); $milestone = $this->getMilestoneProject(); if ($parent || $milestone) { $id = nonempty($parent, $milestone)->getID(); return "/project/subprojects/{$id}/"; } return parent::getObjectCreateCancelURI($object); } protected function getCreateNewObjectPolicy() { return $this->getApplication()->getPolicy( ProjectCreateProjectsCapability::CAPABILITY); } protected function willConfigureFields($object, array $fields) { $is_milestone = ($this->getMilestoneProject() || $object->isMilestone()); $unavailable = array( PhabricatorTransactions::TYPE_VIEW_POLICY, PhabricatorTransactions::TYPE_EDIT_POLICY, PhabricatorTransactions::TYPE_JOIN_POLICY, PhabricatorTransactions::TYPE_SPACE, PhabricatorProjectIconTransaction::TRANSACTIONTYPE, PhabricatorProjectColorTransaction::TRANSACTIONTYPE, ); $unavailable = array_fuse($unavailable); if ($is_milestone) { foreach ($fields as $key => $field) { $xaction_type = $field->getTransactionType(); if (isset($unavailable[$xaction_type])) { unset($fields[$key]); } } } return $fields; } protected function newBuiltinEngineConfigurations() { $configuration = head(parent::newBuiltinEngineConfigurations()); // TODO: This whole method is clumsy, and the ordering for the custom // field is especially clumsy. Maybe try to make this more natural to // express. $configuration ->setFieldOrder( array( 'parent', 'milestone', 'milestone.previous', 'name', 'std:project:internal:description', 'icon', 'color', 'slugs', )); return array( $configuration, ); } protected function buildCustomEditFields($object) { $slugs = mpull($object->getSlugs(), 'getSlug'); $slugs = array_fuse($slugs); unset($slugs[$object->getPrimarySlug()]); $slugs = array_values($slugs); $milestone = $this->getMilestoneProject(); $parent = $this->getParentProject(); if ($parent) { $parent_phid = $parent->getPHID(); } else { $parent_phid = null; } $previous_milestone_phid = null; if ($milestone) { $milestone_phid = $milestone->getPHID(); // Load the current milestone so we can show the user a hint about what // it was called, so they don't have to remember if the next one should // be "Sprint 287" or "Sprint 278". $number = ($milestone->loadNextMilestoneNumber() - 1); if ($number > 0) { $previous_milestone = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withParentProjectPHIDs(array($milestone->getPHID())) ->withIsMilestone(true) ->withMilestoneNumberBetween($number, $number) ->executeOne(); if ($previous_milestone) { $previous_milestone_phid = $previous_milestone->getPHID(); } } } else { $milestone_phid = null; } $fields = array( id(new PhabricatorHandlesEditField()) ->setKey('parent') ->setLabel(pht('Parent')) ->setDescription(pht('Create a subproject of an existing project.')) ->setConduitDescription( pht('Choose a parent project to create a subproject beneath.')) ->setConduitTypeDescription(pht('PHID of the parent project.')) ->setAliases(array('parentPHID')) ->setTransactionType( PhabricatorProjectParentTransaction::TRANSACTIONTYPE) ->setHandleParameterType(new AphrontPHIDHTTPParameterType()) ->setSingleValue($parent_phid) ->setIsReorderable(false) ->setIsDefaultable(false) ->setIsLockable(false) ->setIsLocked(true), id(new PhabricatorHandlesEditField()) ->setKey('milestone') ->setLabel(pht('Milestone Of')) ->setDescription(pht('Parent project to create a milestone for.')) ->setConduitDescription( pht('Choose a parent project to create a new milestone for.')) ->setConduitTypeDescription(pht('PHID of the parent project.')) ->setAliases(array('milestonePHID')) ->setTransactionType( PhabricatorProjectMilestoneTransaction::TRANSACTIONTYPE) ->setHandleParameterType(new AphrontPHIDHTTPParameterType()) ->setSingleValue($milestone_phid) ->setIsReorderable(false) ->setIsDefaultable(false) ->setIsLockable(false) ->setIsLocked(true), id(new PhabricatorHandlesEditField()) ->setKey('milestone.previous') ->setLabel(pht('Previous Milestone')) ->setSingleValue($previous_milestone_phid) ->setIsReorderable(false) ->setIsDefaultable(false) ->setIsLockable(false) ->setIsLocked(true), id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) ->setTransactionType(PhabricatorProjectNameTransaction::TRANSACTIONTYPE) ->setIsRequired(true) ->setDescription(pht('Project name.')) ->setConduitDescription(pht('Rename the project')) ->setConduitTypeDescription(pht('New project name.')) ->setValue($object->getName()), id(new PhabricatorIconSetEditField()) ->setKey('icon') ->setLabel(pht('Icon')) ->setTransactionType( PhabricatorProjectIconTransaction::TRANSACTIONTYPE) ->setIconSet(new PhabricatorProjectIconSet()) ->setDescription(pht('Project icon.')) ->setConduitDescription(pht('Change the project icon.')) ->setConduitTypeDescription(pht('New project icon.')) ->setValue($object->getIcon()), id(new PhabricatorSelectEditField()) ->setKey('color') ->setLabel(pht('Color')) ->setTransactionType( PhabricatorProjectColorTransaction::TRANSACTIONTYPE) ->setOptions(PhabricatorProjectIconSet::getColorMap()) ->setDescription(pht('Project tag color.')) ->setConduitDescription(pht('Change the project tag color.')) ->setConduitTypeDescription(pht('New project tag color.')) ->setValue($object->getColor()), id(new PhabricatorStringListEditField()) ->setKey('slugs') ->setLabel(pht('Additional Hashtags')) ->setTransactionType( PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE) ->setDescription(pht('Additional project slugs.')) ->setConduitDescription(pht('Change project slugs.')) ->setConduitTypeDescription(pht('New list of slugs.')) ->setValue($slugs), ); $can_edit_members = (!$milestone) && (!$object->isMilestone()) && (!$object->getHasSubprojects()); if ($can_edit_members) { // Show this on the web UI when creating a project, but not when editing // one. It is always available via Conduit. - $conduit_only = !$this->getIsCreate(); + $show_field = (bool)$this->getIsCreate(); $members_field = id(new PhabricatorUsersEditField()) ->setKey('members') ->setAliases(array('memberPHIDs')) ->setLabel(pht('Initial Members')) - ->setIsConduitOnly($conduit_only) + ->setIsFormField($show_field) ->setUseEdgeTransactions(true) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue( 'edge:type', PhabricatorProjectProjectHasMemberEdgeType::EDGECONST) ->setDescription(pht('Initial project members.')) ->setConduitDescription(pht('Set project members.')) ->setConduitTypeDescription(pht('New list of members.')) ->setValue(array()); $members_field->setViewer($this->getViewer()); $edit_add = $members_field->getConduitEditType('members.add') ->setConduitDescription(pht('Add members.')); $edit_set = $members_field->getConduitEditType('members.set') ->setConduitDescription( pht('Set members, overwriting the current value.')); $edit_rem = $members_field->getConduitEditType('members.remove') ->setConduitDescription(pht('Remove members.')); $fields[] = $members_field; } return $fields; } } diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php index 98713078fb..9b051c00dd 100644 --- a/src/applications/project/query/PhabricatorProjectQuery.php +++ b/src/applications/project/query/PhabricatorProjectQuery.php @@ -1,859 +1,859 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withStatus($status) { $this->status = $status; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } public function withMemberPHIDs(array $member_phids) { $this->memberPHIDs = $member_phids; return $this; } public function withWatcherPHIDs(array $watcher_phids) { $this->watcherPHIDs = $watcher_phids; return $this; } public function withSlugs(array $slugs) { $this->slugs = $slugs; return $this; } public function withNames(array $names) { $this->names = $names; return $this; } public function withNamePrefixes(array $prefixes) { $this->namePrefixes = $prefixes; return $this; } public function withNameTokens(array $tokens) { $this->nameTokens = array_values($tokens); return $this; } public function withIcons(array $icons) { $this->icons = $icons; return $this; } public function withColors(array $colors) { $this->colors = $colors; return $this; } public function withParentProjectPHIDs($parent_phids) { $this->parentPHIDs = $parent_phids; return $this; } public function withAncestorProjectPHIDs($ancestor_phids) { $this->ancestorPHIDs = $ancestor_phids; return $this; } public function withIsMilestone($is_milestone) { $this->isMilestone = $is_milestone; return $this; } public function withHasSubprojects($has_subprojects) { $this->hasSubprojects = $has_subprojects; return $this; } public function withDepthBetween($min, $max) { $this->minDepth = $min; $this->maxDepth = $max; return $this; } public function withMilestoneNumberBetween($min, $max) { $this->minMilestoneNumber = $min; $this->maxMilestoneNumber = $max; return $this; } public function needMembers($need_members) { $this->needMembers = $need_members; return $this; } public function needAncestorMembers($need_ancestor_members) { $this->needAncestorMembers = $need_ancestor_members; return $this; } public function needWatchers($need_watchers) { $this->needWatchers = $need_watchers; return $this; } public function needImages($need_images) { $this->needImages = $need_images; return $this; } public function needSlugs($need_slugs) { $this->needSlugs = $need_slugs; return $this; } public function newResultObject() { return new PhabricatorProject(); } protected function getDefaultOrderVector() { return array('name'); } public function getBuiltinOrders() { return array( 'name' => array( 'vector' => array('name'), 'name' => pht('Name'), ), ) + parent::getBuiltinOrders(); } public function getOrderableColumns() { return parent::getOrderableColumns() + array( 'name' => array( 'table' => $this->getPrimaryTableAlias(), 'column' => 'name', 'reverse' => true, 'type' => 'string', 'unique' => true, ), 'milestoneNumber' => array( 'table' => $this->getPrimaryTableAlias(), 'column' => 'milestoneNumber', 'type' => 'int', ), ); } protected function getPagingValueMap($cursor, array $keys) { $project = $this->loadCursorObject($cursor); return array( 'id' => $project->getID(), 'name' => $project->getName(), ); } public function getSlugMap() { if ($this->slugMap === null) { throw new PhutilInvalidStateException('execute'); } return $this->slugMap; } protected function willExecute() { $this->slugMap = array(); $this->slugNormals = array(); $this->allSlugs = array(); if ($this->slugs) { foreach ($this->slugs as $slug) { if (PhabricatorSlug::isValidProjectSlug($slug)) { $normal = PhabricatorSlug::normalizeProjectSlug($slug); $this->slugNormals[$slug] = $normal; $this->allSlugs[$normal] = $normal; } // NOTE: At least for now, we query for the normalized slugs but also // for the slugs exactly as entered. This allows older projects with // slugs that are no longer valid to continue to work. $this->allSlugs[$slug] = $slug; } } } protected function loadPage() { return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $projects) { $ancestor_paths = array(); foreach ($projects as $project) { foreach ($project->getAncestorProjectPaths() as $path) { $ancestor_paths[$path] = $path; } } if ($ancestor_paths) { $ancestors = id(new PhabricatorProject())->loadAllWhere( 'projectPath IN (%Ls)', $ancestor_paths); } else { $ancestors = array(); } $projects = $this->linkProjectGraph($projects, $ancestors); $viewer_phid = $this->getViewer()->getPHID(); $material_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST; $watcher_type = PhabricatorObjectHasWatcherEdgeType::EDGECONST; $types = array(); $types[] = $material_type; if ($this->needWatchers) { $types[] = $watcher_type; } $all_graph = $this->getAllReachableAncestors($projects); // NOTE: Although we may not need much information about ancestors, we // always need to test if the viewer is a member, because we will return // ancestor projects to the policy filter via ExtendedPolicy calls. If // we skip populating membership data on a parent, the policy framework // will think the user is not a member of the parent project. $all_sources = array(); foreach ($all_graph as $project) { // For milestones, we need parent members. if ($project->isMilestone()) { $parent_phid = $project->getParentProjectPHID(); $all_sources[$parent_phid] = $parent_phid; } $phid = $project->getPHID(); $all_sources[$phid] = $phid; } $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($all_sources) ->withEdgeTypes($types); $need_all_edges = $this->needMembers || $this->needWatchers || $this->needAncestorMembers; // If we only need to know if the viewer is a member, we can restrict // the query to just their PHID. $any_edges = true; if (!$need_all_edges) { if ($viewer_phid) { $edge_query->withDestinationPHIDs(array($viewer_phid)); } else { // If we don't need members or watchers and don't have a viewer PHID // (viewer is logged-out or omnipotent), they'll never be a member // so we don't need to issue this query at all. $any_edges = false; } } if ($any_edges) { $edge_query->execute(); } $membership_projects = array(); foreach ($all_graph as $project) { $project_phid = $project->getPHID(); if ($project->isMilestone()) { $source_phids = array($project->getParentProjectPHID()); } else { $source_phids = array($project_phid); } if ($any_edges) { $member_phids = $edge_query->getDestinationPHIDs( $source_phids, array($material_type)); } else { $member_phids = array(); } if (in_array($viewer_phid, $member_phids)) { $membership_projects[$project_phid] = $project; } if ($this->needMembers || $this->needAncestorMembers) { $project->attachMemberPHIDs($member_phids); } if ($this->needWatchers) { $watcher_phids = $edge_query->getDestinationPHIDs( array($project_phid), array($watcher_type)); $project->attachWatcherPHIDs($watcher_phids); $project->setIsUserWatcher( $viewer_phid, in_array($viewer_phid, $watcher_phids)); } } // If we loaded ancestor members, we've already populated membership // lists above, so we can skip this step. if (!$this->needAncestorMembers) { $member_graph = $this->getAllReachableAncestors($membership_projects); foreach ($all_graph as $phid => $project) { $is_member = isset($member_graph[$phid]); $project->setIsUserMember($viewer_phid, $is_member); } } return $projects; } protected function didFilterPage(array $projects) { $viewer = $this->getViewer(); if ($this->needImages) { $need_images = $projects; // First, try to load custom profile images for any projects with custom // images. $file_phids = array(); foreach ($need_images as $key => $project) { $image_phid = $project->getProfileImagePHID(); if ($image_phid) { $file_phids[$key] = $image_phid; } } if ($file_phids) { $files = id(new PhabricatorFileQuery()) ->setParentQuery($this) ->setViewer($viewer) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); foreach ($file_phids as $key => $image_phid) { $file = idx($files, $image_phid); if (!$file) { continue; } $need_images[$key]->attachProfileImageFile($file); unset($need_images[$key]); } } // For projects with default images, or projects where the custom image // failed to load, load a builtin image. if ($need_images) { $builtin_map = array(); $builtins = array(); foreach ($need_images as $key => $project) { $icon = $project->getIcon(); $builtin_name = PhabricatorProjectIconSet::getIconImage($icon); $builtin_name = 'projects/'.$builtin_name; $builtin = id(new PhabricatorFilesOnDiskBuiltinFile()) ->setName($builtin_name); $builtin_key = $builtin->getBuiltinFileKey(); $builtins[] = $builtin; $builtin_map[$key] = $builtin_key; } $builtin_files = PhabricatorFile::loadBuiltins( $viewer, $builtins); foreach ($need_images as $key => $project) { $builtin_key = $builtin_map[$key]; $builtin_file = $builtin_files[$builtin_key]; $project->attachProfileImageFile($builtin_file); } } } $this->loadSlugs($projects); return $projects; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->status != self::STATUS_ANY) { switch ($this->status) { case self::STATUS_OPEN: case self::STATUS_ACTIVE: $filter = array( PhabricatorProjectStatus::STATUS_ACTIVE, ); break; case self::STATUS_CLOSED: case self::STATUS_ARCHIVED: $filter = array( PhabricatorProjectStatus::STATUS_ARCHIVED, ); break; default: throw new Exception( pht( "Unknown project status '%s'!", $this->status)); } $where[] = qsprintf( $conn, 'status IN (%Ld)', $filter); } if ($this->statuses !== null) { $where[] = qsprintf( $conn, 'status IN (%Ls)', $this->statuses); } if ($this->ids !== null) { $where[] = qsprintf( $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'phid IN (%Ls)', $this->phids); } if ($this->memberPHIDs !== null) { $where[] = qsprintf( $conn, 'e.dst IN (%Ls)', $this->memberPHIDs); } if ($this->watcherPHIDs !== null) { $where[] = qsprintf( $conn, 'w.dst IN (%Ls)', $this->watcherPHIDs); } if ($this->slugs !== null) { $where[] = qsprintf( $conn, 'slug.slug IN (%Ls)', $this->allSlugs); } if ($this->names !== null) { $where[] = qsprintf( $conn, 'name IN (%Ls)', $this->names); } if ($this->namePrefixes) { $parts = array(); foreach ($this->namePrefixes as $name_prefix) { $parts[] = qsprintf( $conn, 'name LIKE %>', $name_prefix); } - $where[] = '('.implode(' OR ', $parts).')'; + $where[] = qsprintf($conn, '%LO', $parts); } if ($this->icons !== null) { $where[] = qsprintf( $conn, 'icon IN (%Ls)', $this->icons); } if ($this->colors !== null) { $where[] = qsprintf( $conn, 'color IN (%Ls)', $this->colors); } if ($this->parentPHIDs !== null) { $where[] = qsprintf( $conn, 'parentProjectPHID IN (%Ls)', $this->parentPHIDs); } if ($this->ancestorPHIDs !== null) { $ancestor_paths = queryfx_all( $conn, 'SELECT projectPath, projectDepth FROM %T WHERE phid IN (%Ls)', id(new PhabricatorProject())->getTableName(), $this->ancestorPHIDs); if (!$ancestor_paths) { throw new PhabricatorEmptyQueryException(); } $sql = array(); foreach ($ancestor_paths as $ancestor_path) { $sql[] = qsprintf( $conn, '(projectPath LIKE %> AND projectDepth > %d)', $ancestor_path['projectPath'], $ancestor_path['projectDepth']); } - $where[] = '('.implode(' OR ', $sql).')'; + $where[] = qsprintf($conn, '%LO', $sql); $where[] = qsprintf( $conn, 'parentProjectPHID IS NOT NULL'); } if ($this->isMilestone !== null) { if ($this->isMilestone) { $where[] = qsprintf( $conn, 'milestoneNumber IS NOT NULL'); } else { $where[] = qsprintf( $conn, 'milestoneNumber IS NULL'); } } if ($this->hasSubprojects !== null) { $where[] = qsprintf( $conn, 'hasSubprojects = %d', (int)$this->hasSubprojects); } if ($this->minDepth !== null) { $where[] = qsprintf( $conn, 'projectDepth >= %d', $this->minDepth); } if ($this->maxDepth !== null) { $where[] = qsprintf( $conn, 'projectDepth <= %d', $this->maxDepth); } if ($this->minMilestoneNumber !== null) { $where[] = qsprintf( $conn, 'milestoneNumber >= %d', $this->minMilestoneNumber); } if ($this->maxMilestoneNumber !== null) { $where[] = qsprintf( $conn, 'milestoneNumber <= %d', $this->maxMilestoneNumber); } return $where; } protected function shouldGroupQueryResultRows() { if ($this->memberPHIDs || $this->watcherPHIDs || $this->nameTokens) { return true; } return parent::shouldGroupQueryResultRows(); } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { $joins = parent::buildJoinClauseParts($conn); if ($this->memberPHIDs !== null) { $joins[] = qsprintf( $conn, 'JOIN %T e ON e.src = p.phid AND e.type = %d', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorProjectMaterializedMemberEdgeType::EDGECONST); } if ($this->watcherPHIDs !== null) { $joins[] = qsprintf( $conn, 'JOIN %T w ON w.src = p.phid AND w.type = %d', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorObjectHasWatcherEdgeType::EDGECONST); } if ($this->slugs !== null) { $joins[] = qsprintf( $conn, 'JOIN %T slug on slug.projectPHID = p.phid', id(new PhabricatorProjectSlug())->getTableName()); } if ($this->nameTokens !== null) { $name_tokens = $this->getNameTokensForQuery($this->nameTokens); foreach ($name_tokens as $key => $token) { $token_table = 'token_'.$key; $joins[] = qsprintf( $conn, 'JOIN %T %T ON %T.projectID = p.id AND %T.token LIKE %>', PhabricatorProject::TABLE_DATASOURCE_TOKEN, $token_table, $token_table, $token_table, $token); } } return $joins; } public function getQueryApplicationClass() { return 'PhabricatorProjectApplication'; } protected function getPrimaryTableAlias() { return 'p'; } private function linkProjectGraph(array $projects, array $ancestors) { $ancestor_map = mpull($ancestors, null, 'getPHID'); $projects_map = mpull($projects, null, 'getPHID'); $all_map = $projects_map + $ancestor_map; $done = array(); foreach ($projects as $key => $project) { $seen = array($project->getPHID() => true); if (!$this->linkProject($project, $all_map, $done, $seen)) { $this->didRejectResult($project); unset($projects[$key]); continue; } foreach ($project->getAncestorProjects() as $ancestor) { $seen[$ancestor->getPHID()] = true; } } return $projects; } private function linkProject($project, array $all, array $done, array $seen) { $parent_phid = $project->getParentProjectPHID(); // This project has no parent, so just attach `null` and return. if (!$parent_phid) { $project->attachParentProject(null); return true; } // This project has a parent, but it failed to load. if (empty($all[$parent_phid])) { return false; } // Test for graph cycles. If we encounter one, we're going to hide the // entire cycle since we can't meaningfully resolve it. if (isset($seen[$parent_phid])) { return false; } $seen[$parent_phid] = true; $parent = $all[$parent_phid]; $project->attachParentProject($parent); if (!empty($done[$parent_phid])) { return true; } return $this->linkProject($parent, $all, $done, $seen); } private function getAllReachableAncestors(array $projects) { $ancestors = array(); $seen = mpull($projects, null, 'getPHID'); $stack = $projects; while ($stack) { $project = array_pop($stack); $phid = $project->getPHID(); $ancestors[$phid] = $project; $parent_phid = $project->getParentProjectPHID(); if (!$parent_phid) { continue; } if (isset($seen[$parent_phid])) { continue; } $seen[$parent_phid] = true; $stack[] = $project->getParentProject(); } return $ancestors; } private function loadSlugs(array $projects) { // Build a map from primary slugs to projects. $primary_map = array(); foreach ($projects as $project) { $primary_slug = $project->getPrimarySlug(); if ($primary_slug === null) { continue; } $primary_map[$primary_slug] = $project; } // Link up all of the queried slugs which correspond to primary // slugs. If we can link up everything from this (no slugs were queried, // or only primary slugs were queried) we don't need to load anything // else. $unknown = $this->slugNormals; foreach ($unknown as $input => $normal) { if (isset($primary_map[$input])) { $match = $input; } else if (isset($primary_map[$normal])) { $match = $normal; } else { continue; } $this->slugMap[$input] = array( 'slug' => $match, 'projectPHID' => $primary_map[$match]->getPHID(), ); unset($unknown[$input]); } // If we need slugs, we have to load everything. // If we still have some queried slugs which we haven't mapped, we only // need to look for them. // If we've mapped everything, we don't have to do any work. $project_phids = mpull($projects, 'getPHID'); if ($this->needSlugs) { $slugs = id(new PhabricatorProjectSlug())->loadAllWhere( 'projectPHID IN (%Ls)', $project_phids); } else if ($unknown) { $slugs = id(new PhabricatorProjectSlug())->loadAllWhere( 'projectPHID IN (%Ls) AND slug IN (%Ls)', $project_phids, $unknown); } else { $slugs = array(); } // Link up any slugs we were not able to link up earlier. $extra_map = mpull($slugs, 'getProjectPHID', 'getSlug'); foreach ($unknown as $input => $normal) { if (isset($extra_map[$input])) { $match = $input; } else if (isset($extra_map[$normal])) { $match = $normal; } else { continue; } $this->slugMap[$input] = array( 'slug' => $match, 'projectPHID' => $extra_map[$match], ); unset($unknown[$input]); } if ($this->needSlugs) { $slug_groups = mgroup($slugs, 'getProjectPHID'); foreach ($projects as $project) { $project_slugs = idx($slug_groups, $project->getPHID(), array()); $project->attachSlugs($project_slugs); } } } private function getNameTokensForQuery(array $tokens) { // When querying for projects by name, only actually search for the five // longest tokens. MySQL can get grumpy with a large number of JOINs // with LIKEs and queries for more than 5 tokens are essentially never // legitimate searches for projects, but users copy/pasting nonsense. // See also PHI47. $length_map = array(); foreach ($tokens as $token) { $length_map[$token] = strlen($token); } arsort($length_map); $length_map = array_slice($length_map, 0, 5, true); return array_keys($length_map); } } diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php index a049354ad7..f134b2c633 100644 --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -1,887 +1,887 @@ setViewer(PhabricatorUser::getOmnipotentUser()) ->withClasses(array('PhabricatorProjectApplication')) ->executeOne(); $view_policy = $app->getPolicy( ProjectDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy( ProjectDefaultEditCapability::CAPABILITY); $join_policy = $app->getPolicy( ProjectDefaultJoinCapability::CAPABILITY); // If this is the child of some other project, default the Space to the // Space of the parent. if ($parent) { $space_phid = $parent->getSpacePHID(); } else { $space_phid = $actor->getDefaultSpacePHID(); } $default_icon = PhabricatorProjectIconSet::getDefaultIconKey(); $default_color = PhabricatorProjectIconSet::getDefaultColorKey(); return id(new PhabricatorProject()) ->setAuthorPHID($actor->getPHID()) ->setIcon($default_icon) ->setColor($default_color) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setJoinPolicy($join_policy) ->setSpacePHID($space_phid) ->setIsMembershipLocked(0) ->attachMemberPHIDs(array()) ->attachSlugs(array()) ->setHasWorkboard(0) ->setHasMilestones(0) ->setHasSubprojects(0) ->attachParentProject(null); } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, PhabricatorPolicyCapability::CAN_JOIN, ); } public function getPolicy($capability) { if ($this->isMilestone()) { return $this->getParentProject()->getPolicy($capability); } switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); case PhabricatorPolicyCapability::CAN_JOIN: return $this->getJoinPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if ($this->isMilestone()) { return $this->getParentProject()->hasAutomaticCapability( $capability, $viewer); } $can_edit = PhabricatorPolicyCapability::CAN_EDIT; switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if ($this->isUserMember($viewer->getPHID())) { // Project members can always view a project. return true; } break; case PhabricatorPolicyCapability::CAN_EDIT: $parent = $this->getParentProject(); if ($parent) { $can_edit_parent = PhabricatorPolicyFilter::hasCapability( $viewer, $parent, $can_edit); if ($can_edit_parent) { return true; } } break; case PhabricatorPolicyCapability::CAN_JOIN: if (PhabricatorPolicyFilter::hasCapability($viewer, $this, $can_edit)) { // Project editors can always join a project. return true; } break; } return false; } public function describeAutomaticCapability($capability) { // TODO: Clarify the additional rules that parent and subprojects imply. switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return pht('Members of a project can always view it.'); case PhabricatorPolicyCapability::CAN_JOIN: return pht('Users who can edit a project can always join it.'); } return null; } public function getExtendedPolicy($capability, PhabricatorUser $viewer) { $extended = array(); switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $parent = $this->getParentProject(); if ($parent) { $extended[] = array( $parent, PhabricatorPolicyCapability::CAN_VIEW, ); } break; } return $extended; } public function isUserMember($user_phid) { if ($this->memberPHIDs !== self::ATTACHABLE) { return in_array($user_phid, $this->memberPHIDs); } return $this->assertAttachedKey($this->sparseMembers, $user_phid); } public function setIsUserMember($user_phid, $is_member) { if ($this->sparseMembers === self::ATTACHABLE) { $this->sparseMembers = array(); } $this->sparseMembers[$user_phid] = $is_member; return $this; } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort128', 'status' => 'text32', 'primarySlug' => 'text128?', 'isMembershipLocked' => 'bool', 'profileImagePHID' => 'phid?', 'icon' => 'text32', 'color' => 'text32', 'mailKey' => 'bytes20', 'joinPolicy' => 'policy', 'parentProjectPHID' => 'phid?', 'hasWorkboard' => 'bool', 'hasMilestones' => 'bool', 'hasSubprojects' => 'bool', 'milestoneNumber' => 'uint32?', 'projectPath' => 'hashpath64', 'projectDepth' => 'uint32', 'projectPathKey' => 'bytes4', ), self::CONFIG_KEY_SCHEMA => array( 'key_icon' => array( 'columns' => array('icon'), ), 'key_color' => array( 'columns' => array('color'), ), 'key_milestone' => array( 'columns' => array('parentProjectPHID', 'milestoneNumber'), 'unique' => true, ), 'key_primaryslug' => array( 'columns' => array('primarySlug'), 'unique' => true, ), 'key_path' => array( 'columns' => array('projectPath', 'projectDepth'), ), 'key_pathkey' => array( 'columns' => array('projectPathKey'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorProjectProjectPHIDType::TYPECONST); } public function attachMemberPHIDs(array $phids) { $this->memberPHIDs = $phids; return $this; } public function getMemberPHIDs() { return $this->assertAttached($this->memberPHIDs); } public function isArchived() { return ($this->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED); } public function getProfileImageURI() { return $this->getProfileImageFile()->getBestURI(); } public function attachProfileImageFile(PhabricatorFile $file) { $this->profileImageFile = $file; return $this; } public function getProfileImageFile() { return $this->assertAttached($this->profileImageFile); } public function isUserWatcher($user_phid) { if ($this->watcherPHIDs !== self::ATTACHABLE) { return in_array($user_phid, $this->watcherPHIDs); } return $this->assertAttachedKey($this->sparseWatchers, $user_phid); } public function isUserAncestorWatcher($user_phid) { $is_watcher = $this->isUserWatcher($user_phid); if (!$is_watcher) { $parent = $this->getParentProject(); if ($parent) { return $parent->isUserWatcher($user_phid); } } return $is_watcher; } public function getWatchedAncestorPHID($user_phid) { if ($this->isUserWatcher($user_phid)) { return $this->getPHID(); } $parent = $this->getParentProject(); if ($parent) { return $parent->getWatchedAncestorPHID($user_phid); } return null; } public function setIsUserWatcher($user_phid, $is_watcher) { if ($this->sparseWatchers === self::ATTACHABLE) { $this->sparseWatchers = array(); } $this->sparseWatchers[$user_phid] = $is_watcher; return $this; } public function attachWatcherPHIDs(array $phids) { $this->watcherPHIDs = $phids; return $this; } public function getWatcherPHIDs() { return $this->assertAttached($this->watcherPHIDs); } public function getAllAncestorWatcherPHIDs() { $parent = $this->getParentProject(); if ($parent) { $watchers = $parent->getAllAncestorWatcherPHIDs(); } else { $watchers = array(); } foreach ($this->getWatcherPHIDs() as $phid) { $watchers[$phid] = $phid; } return $watchers; } public function attachSlugs(array $slugs) { $this->slugs = $slugs; return $this; } public function getSlugs() { return $this->assertAttached($this->slugs); } public function getColor() { if ($this->isArchived()) { return PHUITagView::COLOR_DISABLED; } return $this->color; } public function getURI() { $id = $this->getID(); return "/project/view/{$id}/"; } public function getProfileURI() { $id = $this->getID(); return "/project/profile/{$id}/"; } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } if (!strlen($this->getPHID())) { $this->setPHID($this->generatePHID()); } if (!strlen($this->getProjectPathKey())) { $hash = PhabricatorHash::digestForIndex($this->getPHID()); $hash = substr($hash, 0, 4); $this->setProjectPathKey($hash); } $path = array(); $depth = 0; if ($this->parentProjectPHID) { $parent = $this->getParentProject(); $path[] = $parent->getProjectPath(); $depth = $parent->getProjectDepth() + 1; } $path[] = $this->getProjectPathKey(); $path = implode('', $path); $limit = self::getProjectDepthLimit(); if ($depth >= $limit) { throw new Exception(pht('Project depth is too great.')); } $this->setProjectPath($path); $this->setProjectDepth($depth); $this->openTransaction(); $result = parent::save(); $this->updateDatasourceTokens(); $this->saveTransaction(); return $result; } public static function getProjectDepthLimit() { // This is limited by how many path hashes we can fit in the path // column. return 16; } public function updateDatasourceTokens() { $table = self::TABLE_DATASOURCE_TOKEN; $conn_w = $this->establishConnection('w'); $id = $this->getID(); $slugs = queryfx_all( $conn_w, 'SELECT * FROM %T WHERE projectPHID = %s', id(new PhabricatorProjectSlug())->getTableName(), $this->getPHID()); $all_strings = ipull($slugs, 'slug'); $all_strings[] = $this->getDisplayName(); $all_strings = implode(' ', $all_strings); $tokens = PhabricatorTypeaheadDatasource::tokenizeString($all_strings); $sql = array(); foreach ($tokens as $token) { $sql[] = qsprintf($conn_w, '(%d, %s)', $id, $token); } $this->openTransaction(); queryfx( $conn_w, 'DELETE FROM %T WHERE projectID = %d', $table, $id); foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) { queryfx( $conn_w, - 'INSERT INTO %T (projectID, token) VALUES %Q', + 'INSERT INTO %T (projectID, token) VALUES %LQ', $table, $chunk); } $this->saveTransaction(); } public function isMilestone() { return ($this->getMilestoneNumber() !== null); } public function getParentProject() { return $this->assertAttached($this->parentProject); } public function attachParentProject(PhabricatorProject $project = null) { $this->parentProject = $project; return $this; } public function getAncestorProjectPaths() { $parts = array(); $path = $this->getProjectPath(); $parent_length = (strlen($path) - 4); for ($ii = $parent_length; $ii > 0; $ii -= 4) { $parts[] = substr($path, 0, $ii); } return $parts; } public function getAncestorProjects() { $ancestors = array(); $cursor = $this->getParentProject(); while ($cursor) { $ancestors[] = $cursor; $cursor = $cursor->getParentProject(); } return $ancestors; } public function supportsEditMembers() { if ($this->isMilestone()) { return false; } if ($this->getHasSubprojects()) { return false; } return true; } public function supportsMilestones() { if ($this->isMilestone()) { return false; } return true; } public function supportsSubprojects() { if ($this->isMilestone()) { return false; } return true; } public function loadNextMilestoneNumber() { $current = queryfx_one( $this->establishConnection('w'), 'SELECT MAX(milestoneNumber) n FROM %T WHERE parentProjectPHID = %s', $this->getTableName(), $this->getPHID()); if (!$current) { $number = 1; } else { $number = (int)$current['n'] + 1; } return $number; } public function getDisplayName() { $name = $this->getName(); // If this is a milestone, show it as "Parent > Sprint 99". if ($this->isMilestone()) { $name = pht( '%s (%s)', $this->getParentProject()->getName(), $name); } return $name; } public function getDisplayIconKey() { if ($this->isMilestone()) { $key = PhabricatorProjectIconSet::getMilestoneIconKey(); } else { $key = $this->getIcon(); } return $key; } public function getDisplayIconIcon() { $key = $this->getDisplayIconKey(); return PhabricatorProjectIconSet::getIconIcon($key); } public function getDisplayIconName() { $key = $this->getDisplayIconKey(); return PhabricatorProjectIconSet::getIconName($key); } public function getDisplayColor() { if ($this->isMilestone()) { return $this->getParentProject()->getColor(); } return $this->getColor(); } public function getDisplayIconComposeIcon() { $icon = $this->getDisplayIconIcon(); return $icon; } public function getDisplayIconComposeColor() { $color = $this->getDisplayColor(); $map = array( 'grey' => 'charcoal', 'checkered' => 'backdrop', ); return idx($map, $color, $color); } public function getProperty($key, $default = null) { return idx($this->properties, $key, $default); } public function setProperty($key, $value) { $this->properties[$key] = $value; return $this; } public function getDefaultWorkboardSort() { return $this->getProperty('workboard.sort.default'); } public function setDefaultWorkboardSort($sort) { return $this->setProperty('workboard.sort.default', $sort); } public function getDefaultWorkboardFilter() { return $this->getProperty('workboard.filter.default'); } public function setDefaultWorkboardFilter($filter) { return $this->setProperty('workboard.filter.default', $filter); } public function getWorkboardBackgroundColor() { return $this->getProperty('workboard.background'); } public function setWorkboardBackgroundColor($color) { return $this->setProperty('workboard.background', $color); } public function getDisplayWorkboardBackgroundColor() { $color = $this->getWorkboardBackgroundColor(); if ($color === null) { $parent = $this->getParentProject(); if ($parent) { return $parent->getDisplayWorkboardBackgroundColor(); } } if ($color === 'none') { $color = null; } return $color; } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('projects.fields'); } public function getCustomFieldBaseClass() { return 'PhabricatorProjectCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorProjectTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorProjectTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { if ($this->isMilestone()) { return $this->getParentProject()->getSpacePHID(); } return $this->spacePHID; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $columns = id(new PhabricatorProjectColumn()) ->loadAllWhere('projectPHID = %s', $this->getPHID()); foreach ($columns as $column) { $engine->destroyObject($column); } $slugs = id(new PhabricatorProjectSlug()) ->loadAllWhere('projectPHID = %s', $this->getPHID()); foreach ($slugs as $slug) { $slug->delete(); } $this->saveTransaction(); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PhabricatorProjectFulltextEngine(); } /* -( PhabricatorFerretInterface )--------------------------------------- */ public function newFerretEngine() { return new PhabricatorProjectFerretEngine(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of the project.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('slug') ->setType('string') ->setDescription(pht('Primary slug/hashtag.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('milestone') ->setType('int?') ->setDescription(pht('For milestones, milestone sequence number.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('parent') ->setType('map?') ->setDescription( pht( 'For subprojects and milestones, a brief description of the '. 'parent project.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('depth') ->setType('int') ->setDescription( pht( 'For subprojects and milestones, depth of this project in the '. 'tree. Root projects have depth 0.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('icon') ->setType('map') ->setDescription(pht('Information about the project icon.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('color') ->setType('map') ->setDescription(pht('Information about the project color.')), ); } public function getFieldValuesForConduit() { $color_key = $this->getColor(); $color_name = PhabricatorProjectIconSet::getColorName($color_key); if ($this->isMilestone()) { $milestone = (int)$this->getMilestoneNumber(); } else { $milestone = null; } $parent = $this->getParentProject(); if ($parent) { $parent_ref = $parent->getRefForConduit(); } else { $parent_ref = null; } return array( 'name' => $this->getName(), 'slug' => $this->getPrimarySlug(), 'milestone' => $milestone, 'depth' => (int)$this->getProjectDepth(), 'parent' => $parent_ref, 'icon' => array( 'key' => $this->getDisplayIconKey(), 'name' => $this->getDisplayIconName(), 'icon' => $this->getDisplayIconIcon(), ), 'color' => array( 'key' => $color_key, 'name' => $color_name, ), ); } public function getConduitSearchAttachments() { return array( id(new PhabricatorProjectsMembersSearchEngineAttachment()) ->setAttachmentKey('members'), id(new PhabricatorProjectsWatchersSearchEngineAttachment()) ->setAttachmentKey('watchers'), id(new PhabricatorProjectsAncestorsSearchEngineAttachment()) ->setAttachmentKey('ancestors'), ); } /** * Get an abbreviated representation of this project for use in providing * "parent" and "ancestor" information. */ public function getRefForConduit() { return array( 'id' => (int)$this->getID(), 'phid' => $this->getPHID(), 'name' => $this->getName(), ); } /* -( PhabricatorColumnProxyInterface )------------------------------------ */ public function getProxyColumnName() { return $this->getName(); } public function getProxyColumnIcon() { return $this->getDisplayIconIcon(); } public function getProxyColumnClass() { if ($this->isMilestone()) { return 'phui-workboard-column-milestone'; } return null; } } diff --git a/src/applications/releeph/query/ReleephBranchQuery.php b/src/applications/releeph/query/ReleephBranchQuery.php index 9d7c884012..97e47bdcaf 100644 --- a/src/applications/releeph/query/ReleephBranchQuery.php +++ b/src/applications/releeph/query/ReleephBranchQuery.php @@ -1,152 +1,152 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function needCutPointCommits($need_commits) { $this->needCutPointCommits = $need_commits; return $this; } public function withStatus($status) { $this->status = $status; return $this; } public function withProductPHIDs($product_phids) { $this->productPHIDs = $product_phids; return $this; } protected function loadPage() { $table = new ReleephBranch(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } protected function willExecute() { if ($this->productPHIDs !== null) { $products = id(new ReleephProductQuery()) ->setViewer($this->getViewer()) ->withPHIDs($this->productPHIDs) ->execute(); if (!$products) { throw new PhabricatorEmptyQueryException(); } $this->productIDs = mpull($products, 'getID'); } } protected function willFilterPage(array $branches) { $project_ids = mpull($branches, 'getReleephProjectID'); $projects = id(new ReleephProductQuery()) ->withIDs($project_ids) ->setViewer($this->getViewer()) ->execute(); foreach ($branches as $key => $branch) { $project_id = $project_ids[$key]; if (isset($projects[$project_id])) { $branch->attachProject($projects[$project_id]); } else { unset($branches[$key]); } } if ($this->needCutPointCommits) { $commit_phids = mpull($branches, 'getCutPointCommitPHID'); $commits = id(new DiffusionCommitQuery()) ->setViewer($this->getViewer()) ->withPHIDs($commit_phids) ->execute(); $commits = mpull($commits, null, 'getPHID'); foreach ($branches as $branch) { $commit = idx($commits, $branch->getCutPointCommitPHID()); $branch->attachCutPointCommit($commit); } } return $branches; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } if ($this->productIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'releephProjectID IN (%Ld)', $this->productIDs); } $status = $this->status; switch ($status) { case self::STATUS_ALL: break; case self::STATUS_OPEN: $where[] = qsprintf( - $conn_r, + $conn, 'isActive = 1'); break; default: throw new Exception(pht("Unknown status constant '%s'!", $status)); } - $where[] = $this->buildPagingClause($conn_r); + $where[] = $this->buildPagingClause($conn); - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } public function getQueryApplicationClass() { return 'PhabricatorReleephApplication'; } } diff --git a/src/applications/releeph/query/ReleephProductQuery.php b/src/applications/releeph/query/ReleephProductQuery.php index acfc39c1c2..c039950379 100644 --- a/src/applications/releeph/query/ReleephProductQuery.php +++ b/src/applications/releeph/query/ReleephProductQuery.php @@ -1,146 +1,146 @@ active = $active; return $this; } public function setOrder($order) { switch ($order) { case self::ORDER_ID: $this->setOrderVector(array('id')); break; case self::ORDER_NAME: $this->setOrderVector(array('name')); break; default: throw new Exception(pht('Order "%s" not supported.', $order)); } return $this; } public function withIDs(array $ids) { $this->ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withRepositoryPHIDs(array $repository_phids) { $this->repositoryPHIDs = $repository_phids; return $this; } protected function loadPage() { $table = new ReleephProject(); $conn_r = $table->establishConnection('r'); $rows = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($rows); } protected function willFilterPage(array $projects) { assert_instances_of($projects, 'ReleephProject'); $repository_phids = mpull($projects, 'getRepositoryPHID'); $repositories = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()) ->withPHIDs($repository_phids) ->execute(); $repositories = mpull($repositories, null, 'getPHID'); foreach ($projects as $key => $project) { $repo = idx($repositories, $project->getRepositoryPHID()); if (!$repo) { unset($projects[$key]); continue; } $project->attachRepository($repo); } return $projects; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->active !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'isActive = %d', (int)$this->active); } if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ls)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } if ($this->repositoryPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'repositoryPHID IN (%Ls)', $this->repositoryPHIDs); } - $where[] = $this->buildPagingClause($conn_r); + $where[] = $this->buildPagingClause($conn); - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } public function getOrderableColumns() { return parent::getOrderableColumns() + array( 'name' => array( 'column' => 'name', 'unique' => true, 'reverse' => true, 'type' => 'string', ), ); } protected function getPagingValueMap($cursor, array $keys) { $product = $this->loadCursorObject($cursor); return array( 'id' => $product->getID(), 'name' => $product->getName(), ); } public function getQueryApplicationClass() { return 'PhabricatorReleephApplication'; } } diff --git a/src/applications/releeph/query/ReleephRequestQuery.php b/src/applications/releeph/query/ReleephRequestQuery.php index 7d4f19e624..3042260387 100644 --- a/src/applications/releeph/query/ReleephRequestQuery.php +++ b/src/applications/releeph/query/ReleephRequestQuery.php @@ -1,247 +1,247 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withBranchIDs(array $branch_ids) { $this->branchIDs = $branch_ids; return $this; } public function withStatus($status) { $this->status = $status; return $this; } public function withRequestedCommitPHIDs(array $requested_commit_phids) { $this->requestedCommitPHIDs = $requested_commit_phids; return $this; } public function withRequestorPHIDs(array $phids) { $this->requestorPHIDs = $phids; return $this; } public function withSeverities(array $severities) { $this->severities = $severities; return $this; } public function withRequestedObjectPHIDs(array $phids) { $this->requestedObjectPHIDs = $phids; return $this; } protected function loadPage() { $table = new ReleephRequest(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } protected function willFilterPage(array $requests) { // Load requested objects: you must be able to see an object to see // requests for it. $object_phids = mpull($requests, 'getRequestedObjectPHID'); $objects = id(new PhabricatorObjectQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withPHIDs($object_phids) ->execute(); foreach ($requests as $key => $request) { $object_phid = $request->getRequestedObjectPHID(); $object = idx($objects, $object_phid); if (!$object) { unset($requests[$key]); continue; } $request->attachRequestedObject($object); } if ($this->severities) { $severities = array_fuse($this->severities); foreach ($requests as $key => $request) { // NOTE: Facebook uses a custom field here. if (ReleephDefaultFieldSelector::isFacebook()) { $severity = $request->getDetail('severity'); } else { $severity = $request->getDetail('releeph:severity'); } if (empty($severities[$severity])) { unset($requests[$key]); } } } $branch_ids = array_unique(mpull($requests, 'getBranchID')); $branches = id(new ReleephBranchQuery()) ->withIDs($branch_ids) ->setViewer($this->getViewer()) ->execute(); $branches = mpull($branches, null, 'getID'); foreach ($requests as $key => $request) { $branch = idx($branches, $request->getBranchID()); if (!$branch) { unset($requests[$key]); continue; } $request->attachBranch($branch); } // TODO: These should be serviced by the query, but are not currently // denormalized anywhere. For now, filter them here instead. Note that // we must perform this filtering *after* querying and attaching branches, // because request status depends on the product. $keep_status = array_fuse($this->getKeepStatusConstants()); if ($keep_status) { foreach ($requests as $key => $request) { if (empty($keep_status[$request->getStatus()])) { unset($requests[$key]); } } } return $requests; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } if ($this->branchIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'branchID IN (%Ld)', $this->branchIDs); } if ($this->requestedCommitPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'requestCommitPHID IN (%Ls)', $this->requestedCommitPHIDs); } if ($this->requestorPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'requestUserPHID IN (%Ls)', $this->requestorPHIDs); } if ($this->requestedObjectPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'requestedObjectPHID IN (%Ls)', $this->requestedObjectPHIDs); } - $where[] = $this->buildPagingClause($conn_r); + $where[] = $this->buildPagingClause($conn); - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } private function getKeepStatusConstants() { switch ($this->status) { case self::STATUS_ALL: return array(); case self::STATUS_OPEN: return array( ReleephRequestStatus::STATUS_REQUESTED, ReleephRequestStatus::STATUS_NEEDS_PICK, ReleephRequestStatus::STATUS_NEEDS_REVERT, ); case self::STATUS_REQUESTED: return array( ReleephRequestStatus::STATUS_REQUESTED, ); case self::STATUS_NEEDS_PULL: return array( ReleephRequestStatus::STATUS_NEEDS_PICK, ); case self::STATUS_REJECTED: return array( ReleephRequestStatus::STATUS_REJECTED, ); case self::STATUS_ABANDONED: return array( ReleephRequestStatus::STATUS_ABANDONED, ); case self::STATUS_PULLED: return array( ReleephRequestStatus::STATUS_PICKED, ); case self::STATUS_NEEDS_REVERT: return array( ReleephRequestStatus::STATUS_NEEDS_REVERT, ); case self::STATUS_REVERTED: return array( ReleephRequestStatus::STATUS_REVERTED, ); default: throw new Exception(pht("Unknown status '%s'!", $this->status)); } } public function getQueryApplicationClass() { return 'PhabricatorReleephApplication'; } } diff --git a/src/applications/repository/editor/PhabricatorRepositoryEditor.php b/src/applications/repository/editor/PhabricatorRepositoryEditor.php index 882c1a11fa..0955d54b98 100644 --- a/src/applications/repository/editor/PhabricatorRepositoryEditor.php +++ b/src/applications/repository/editor/PhabricatorRepositoryEditor.php @@ -1,618 +1,91 @@ getTransactionType()) { - case PhabricatorRepositoryTransaction::TYPE_VCS: - return $object->getVersionControlSystem(); - case PhabricatorRepositoryTransaction::TYPE_ACTIVATE: - return $object->isTracked(); - case PhabricatorRepositoryTransaction::TYPE_NAME: - return $object->getName(); - case PhabricatorRepositoryTransaction::TYPE_DESCRIPTION: - return $object->getDetail('description'); - case PhabricatorRepositoryTransaction::TYPE_ENCODING: - return $object->getDetail('encoding'); - case PhabricatorRepositoryTransaction::TYPE_DEFAULT_BRANCH: - return $object->getDetail('default-branch'); - case PhabricatorRepositoryTransaction::TYPE_TRACK_ONLY: - return array_keys($object->getDetail('branch-filter', array())); - case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE_ONLY: - return array_keys($object->getDetail('close-commits-filter', array())); - case PhabricatorRepositoryTransaction::TYPE_UUID: - return $object->getUUID(); - case PhabricatorRepositoryTransaction::TYPE_SVN_SUBPATH: - return $object->getDetail('svn-subpath'); - case PhabricatorRepositoryTransaction::TYPE_NOTIFY: - return (int)!$object->getDetail('herald-disabled'); - case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE: - return (int)!$object->getDetail('disable-autoclose'); - case PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY: - return $object->getPushPolicy(); - case PhabricatorRepositoryTransaction::TYPE_DANGEROUS: - return $object->shouldAllowDangerousChanges(); - case PhabricatorRepositoryTransaction::TYPE_ENORMOUS: - return $object->shouldAllowEnormousChanges(); - case PhabricatorRepositoryTransaction::TYPE_SLUG: - return $object->getRepositorySlug(); - case PhabricatorRepositoryTransaction::TYPE_SERVICE: - return $object->getAlmanacServicePHID(); - case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_LANGUAGE: - return $object->getSymbolLanguages(); - case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES: - return $object->getSymbolSources(); - case PhabricatorRepositoryTransaction::TYPE_STAGING_URI: - return $object->getDetail('staging-uri'); - case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS: - return $object->getDetail('automation.blueprintPHIDs', array()); - case PhabricatorRepositoryTransaction::TYPE_CALLSIGN: - return $object->getCallsign(); - } - } - - protected function getCustomTransactionNewValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorRepositoryTransaction::TYPE_ACTIVATE: - case PhabricatorRepositoryTransaction::TYPE_NAME: - case PhabricatorRepositoryTransaction::TYPE_DESCRIPTION: - case PhabricatorRepositoryTransaction::TYPE_ENCODING: - case PhabricatorRepositoryTransaction::TYPE_DEFAULT_BRANCH: - case PhabricatorRepositoryTransaction::TYPE_TRACK_ONLY: - case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE_ONLY: - case PhabricatorRepositoryTransaction::TYPE_UUID: - case PhabricatorRepositoryTransaction::TYPE_SVN_SUBPATH: - case PhabricatorRepositoryTransaction::TYPE_VCS: - case PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY: - case PhabricatorRepositoryTransaction::TYPE_DANGEROUS: - case PhabricatorRepositoryTransaction::TYPE_ENORMOUS: - case PhabricatorRepositoryTransaction::TYPE_SERVICE: - case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_LANGUAGE: - case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES: - case PhabricatorRepositoryTransaction::TYPE_STAGING_URI: - case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS: - return $xaction->getNewValue(); - case PhabricatorRepositoryTransaction::TYPE_SLUG: - case PhabricatorRepositoryTransaction::TYPE_CALLSIGN: - $name = $xaction->getNewValue(); - if (strlen($name)) { - return $name; - } - return null; - case PhabricatorRepositoryTransaction::TYPE_NOTIFY: - case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE: - return (int)$xaction->getNewValue(); - } - } - - protected function applyCustomInternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorRepositoryTransaction::TYPE_VCS: - $object->setVersionControlSystem($xaction->getNewValue()); - break; - case PhabricatorRepositoryTransaction::TYPE_ACTIVATE: - $active = $xaction->getNewValue(); - - // The first time a repository is activated, clear the "new repository" - // flag so we stop showing setup hints. - if ($active) { - $object->setDetail('newly-initialized', false); - } - - $object->setDetail('tracking-enabled', $active); - break; - case PhabricatorRepositoryTransaction::TYPE_NAME: - $object->setName($xaction->getNewValue()); - break; - case PhabricatorRepositoryTransaction::TYPE_DESCRIPTION: - $object->setDetail('description', $xaction->getNewValue()); - break; - case PhabricatorRepositoryTransaction::TYPE_DEFAULT_BRANCH: - $object->setDetail('default-branch', $xaction->getNewValue()); - break; - case PhabricatorRepositoryTransaction::TYPE_TRACK_ONLY: - $object->setDetail( - 'branch-filter', - array_fill_keys($xaction->getNewValue(), true)); - break; - case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE_ONLY: - $object->setDetail( - 'close-commits-filter', - array_fill_keys($xaction->getNewValue(), true)); - break; - case PhabricatorRepositoryTransaction::TYPE_UUID: - $object->setUUID($xaction->getNewValue()); - break; - case PhabricatorRepositoryTransaction::TYPE_SVN_SUBPATH: - $object->setDetail('svn-subpath', $xaction->getNewValue()); - break; - case PhabricatorRepositoryTransaction::TYPE_NOTIFY: - $object->setDetail('herald-disabled', (int)!$xaction->getNewValue()); - break; - case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE: - $object->setDetail('disable-autoclose', (int)!$xaction->getNewValue()); - break; - case PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY: - return $object->setPushPolicy($xaction->getNewValue()); - case PhabricatorRepositoryTransaction::TYPE_DANGEROUS: - $object->setDetail('allow-dangerous-changes', $xaction->getNewValue()); - return; - case PhabricatorRepositoryTransaction::TYPE_ENORMOUS: - $object->setDetail('allow-enormous-changes', $xaction->getNewValue()); - return; - case PhabricatorRepositoryTransaction::TYPE_SLUG: - $object->setRepositorySlug($xaction->getNewValue()); - return; - case PhabricatorRepositoryTransaction::TYPE_SERVICE: - $object->setAlmanacServicePHID($xaction->getNewValue()); - return; - case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_LANGUAGE: - $object->setDetail('symbol-languages', $xaction->getNewValue()); - return; - case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES: - $object->setDetail('symbol-sources', $xaction->getNewValue()); - return; - case PhabricatorRepositoryTransaction::TYPE_STAGING_URI: - $object->setDetail('staging-uri', $xaction->getNewValue()); - return; - case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS: - $object->setDetail( - 'automation.blueprintPHIDs', - $xaction->getNewValue()); - return; - case PhabricatorRepositoryTransaction::TYPE_CALLSIGN: - $object->setCallsign($xaction->getNewValue()); - return; - case PhabricatorRepositoryTransaction::TYPE_ENCODING: - $object->setDetail('encoding', $xaction->getNewValue()); - break; - } - } - - protected function applyCustomExternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS: - DrydockAuthorization::applyAuthorizationChanges( - $this->getActor(), - $object->getPHID(), - $xaction->getOldValue(), - $xaction->getNewValue()); - break; - } - - } - - protected function validateTransaction( - PhabricatorLiskDAO $object, - $type, - array $xactions) { - - $errors = parent::validateTransaction($object, $type, $xactions); - - switch ($type) { - case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE_ONLY: - case PhabricatorRepositoryTransaction::TYPE_TRACK_ONLY: - foreach ($xactions as $xaction) { - foreach ($xaction->getNewValue() as $pattern) { - // Check for invalid regular expressions. - $regexp = PhabricatorRepository::extractBranchRegexp($pattern); - if ($regexp !== null) { - $ok = @preg_match($regexp, ''); - if ($ok === false) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - pht( - 'Expression "%s" is not a valid regular expression. Note '. - 'that you must include delimiters.', - $regexp), - $xaction); - $errors[] = $error; - continue; - } - } - - // Check for formatting mistakes like `regex(...)` instead of - // `regexp(...)`. - $matches = null; - if (preg_match('/^([^(]+)\\(.*\\)\z/', $pattern, $matches)) { - switch ($matches[1]) { - case 'regexp': - break; - default: - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - pht( - 'Matching function "%s(...)" is not recognized. Valid '. - 'functions are: regexp(...).', - $matches[1]), - $xaction); - $errors[] = $error; - break; - } - } - } - } - break; - - case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS: - foreach ($xactions as $xaction) { - $old = nonempty($xaction->getOldValue(), array()); - $new = nonempty($xaction->getNewValue(), array()); - - $add = array_diff($new, $old); - - $invalid = PhabricatorObjectQuery::loadInvalidPHIDsForViewer( - $this->getActor(), - $add); - if ($invalid) { - $errors[] = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - pht( - 'Some of the selected automation blueprints are invalid '. - 'or restricted: %s.', - implode(', ', $invalid)), - $xaction); - } - } - break; - - case PhabricatorRepositoryTransaction::TYPE_VCS: - $vcs_map = PhabricatorRepositoryType::getAllRepositoryTypes(); - $current_vcs = $object->getVersionControlSystem(); - - if (!$this->getIsNewObject()) { - foreach ($xactions as $xaction) { - if ($xaction->getNewValue() == $current_vcs) { - continue; - } - - $errors[] = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Immutable'), - pht( - 'You can not change the version control system an existing '. - 'repository uses. It can only be set when a repository is '. - 'first created.'), - $xaction); - } - } else { - $value = $object->getVersionControlSystem(); - foreach ($xactions as $xaction) { - $value = $xaction->getNewValue(); - - if (empty($vcs_map[$value])) { - $errors[] = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - pht( - 'Specified version control system must be a VCS '. - 'recognized by Phabricator: %s.', - implode(', ', array_keys($vcs_map))), - $xaction); - } - } - - if (!strlen($value)) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Required'), - pht( - 'When creating a repository, you must specify a valid '. - 'underlying version control system: %s.', - implode(', ', array_keys($vcs_map))), - nonempty(last($xactions), null)); - $error->setIsMissingFieldError(true); - $errors[] = $error; - } - } - break; - - case PhabricatorRepositoryTransaction::TYPE_NAME: - $missing = $this->validateIsEmptyTextField( - $object->getName(), - $xactions); - - if ($missing) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Required'), - pht('Repository name is required.'), - nonempty(last($xactions), null)); - - $error->setIsMissingFieldError(true); - $errors[] = $error; - } - break; - - case PhabricatorRepositoryTransaction::TYPE_ACTIVATE: - $status_map = PhabricatorRepository::getStatusMap(); - foreach ($xactions as $xaction) { - $status = $xaction->getNewValue(); - if (empty($status_map[$status])) { - $errors[] = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - pht( - 'Repository status "%s" is not valid.', - $status), - $xaction); - } - } - break; - - case PhabricatorRepositoryTransaction::TYPE_ENCODING: - foreach ($xactions as $xaction) { - // Make sure the encoding is valid by converting to UTF-8. This tests - // that the user has mbstring installed, and also that they didn't - // type a garbage encoding name. Note that we're converting from - // UTF-8 to the target encoding, because mbstring is fine with - // converting from a nonsense encoding. - $encoding = $xaction->getNewValue(); - if (!strlen($encoding)) { - continue; - } - - try { - phutil_utf8_convert('.', $encoding, 'UTF-8'); - } catch (Exception $ex) { - $errors[] = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - pht( - 'Repository encoding "%s" is not valid: %s', - $encoding, - $ex->getMessage()), - $xaction); - } - } - break; - - case PhabricatorRepositoryTransaction::TYPE_SLUG: - foreach ($xactions as $xaction) { - $old = $xaction->getOldValue(); - $new = $xaction->getNewValue(); - - if (!strlen($new)) { - continue; - } - - if ($new === $old) { - continue; - } - - try { - PhabricatorRepository::assertValidRepositorySlug($new); - } catch (Exception $ex) { - $errors[] = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - $ex->getMessage(), - $xaction); - continue; - } - - $other = id(new PhabricatorRepositoryQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withSlugs(array($new)) - ->executeOne(); - if ($other && ($other->getID() !== $object->getID())) { - $errors[] = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Duplicate'), - pht( - 'The selected repository short name is already in use by '. - 'another repository. Choose a unique short name.'), - $xaction); - continue; - } - } - break; - - case PhabricatorRepositoryTransaction::TYPE_CALLSIGN: - foreach ($xactions as $xaction) { - $old = $xaction->getOldValue(); - $new = $xaction->getNewValue(); - - if (!strlen($new)) { - continue; - } - - if ($new === $old) { - continue; - } - - try { - PhabricatorRepository::assertValidCallsign($new); - } catch (Exception $ex) { - $errors[] = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - $ex->getMessage(), - $xaction); - continue; - } - - $other = id(new PhabricatorRepositoryQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withCallsigns(array($new)) - ->executeOne(); - if ($other && ($other->getID() !== $object->getID())) { - $errors[] = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Duplicate'), - pht( - 'The selected callsign ("%s") is already in use by another '. - 'repository. Choose a unique callsign.', - $new), - $xaction); - continue; - } - } - break; - - case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES: - foreach ($xactions as $xaction) { - $old = $object->getSymbolSources(); - $new = $xaction->getNewValue(); - - // If the viewer is adding new repositories, make sure they are - // valid and visible. - $add = array_diff($new, $old); - if (!$add) { - continue; - } - - $repositories = id(new PhabricatorRepositoryQuery()) - ->setViewer($this->getActor()) - ->withPHIDs($add) - ->execute(); - $repositories = mpull($repositories, null, 'getPHID'); - - foreach ($add as $phid) { - if (isset($repositories[$phid])) { - continue; - } - - $errors[] = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - pht( - 'Repository ("%s") does not exist, or you do not have '. - 'permission to see it.', - $phid), - $xaction); - break; - } - } - break; - - } - - return $errors; - } - protected function didCatchDuplicateKeyException( PhabricatorLiskDAO $object, array $xactions, Exception $ex) { $errors = array(); $errors[] = new PhabricatorApplicationTransactionValidationError( null, pht('Invalid'), pht( 'The chosen callsign or repository short name is already in '. 'use by another repository.'), null); throw new PhabricatorApplicationTransactionValidationException($errors); } protected function supportsSearch() { return true; } protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { // If the repository does not have a local path yet, assign it one based // on its ID. We can't do this earlier because we won't have an ID yet. $local_path = $object->getLocalPath(); if (!strlen($local_path)) { $local_key = 'repository.default-local-path'; $local_root = PhabricatorEnv::getEnvConfig($local_key); $local_root = rtrim($local_root, '/'); $id = $object->getID(); $local_path = "{$local_root}/{$id}/"; $object->setLocalPath($local_path); $object->save(); } if ($this->getIsNewObject()) { // The default state of repositories is to be hosted, if they are // enabled without configuring any "Observe" URIs. $object->setHosted(true); $object->save(); // Create this repository's builtin URIs. $builtin_uris = $object->newBuiltinURIs(); foreach ($builtin_uris as $uri) { $uri->save(); } id(new DiffusionRepositoryClusterEngine()) ->setViewer($this->getActor()) ->setRepository($object) ->synchronizeWorkingCopyAfterCreation(); } $object->writeStatusMessage( PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE, null); return $xactions; } } diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementClusterizeWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementClusterizeWorkflow.php index 7178564067..2e2c3fcc92 100644 --- a/src/applications/repository/management/PhabricatorRepositoryManagementClusterizeWorkflow.php +++ b/src/applications/repository/management/PhabricatorRepositoryManagementClusterizeWorkflow.php @@ -1,150 +1,151 @@ setName('clusterize') ->setExamples('**clusterize** [options] __repository__ ...') ->setSynopsis( pht('Convert existing repositories into cluster repositories.')) ->setArguments( array( array( 'name' => 'service', 'param' => 'service', 'help' => pht( 'Cluster repository service in Almanac to move repositories '. 'into.'), ), array( 'name' => 'remove-service', 'help' => pht('Take repositories out of a cluster.'), ), array( 'name' => 'repositories', 'wildcard' => true, ), )); } public function execute(PhutilArgumentParser $args) { $viewer = $this->getViewer(); $repositories = $this->loadRepositories($args, 'repositories'); if (!$repositories) { throw new PhutilArgumentUsageException( pht('Specify one or more repositories to clusterize.')); } $service_name = $args->getArg('service'); $remove_service = $args->getArg('remove-service'); if ($remove_service && $service_name) { throw new PhutilArgumentUsageException( pht('Specify --service or --remove-service, but not both.')); } if (!$service_name && !$remove_service) { throw new PhutilArgumentUsageException( pht('Specify --service or --remove-service.')); } if ($remove_service) { $service = null; } else { $service = id(new AlmanacServiceQuery()) ->setViewer($viewer) ->withNames(array($service_name)) ->withServiceTypes( array( AlmanacClusterRepositoryServiceType::SERVICETYPE, )) ->needBindings(true) ->executeOne(); if (!$service) { throw new PhutilArgumentUsageException( pht( 'No repository service "%s" exists.', $service_name)); } } if ($service) { $service_phid = $service->getPHID(); $bindings = $service->getActiveBindings(); $unique_devices = array(); foreach ($bindings as $binding) { $unique_devices[$binding->getDevicePHID()] = $binding->getDevice(); } if (count($unique_devices) > 1) { $device_names = mpull($unique_devices, 'getName'); echo id(new PhutilConsoleBlock()) ->addParagraph( pht( 'Service "%s" is actively bound to more than one device (%s).', $service_name, implode(', ', $device_names))) ->addParagraph( pht( 'If you clusterize a repository onto this service it may be '. 'unclear which devices have up-to-date copies of the '. 'repository. If so, leader/follower ambiguity will freeze the '. 'repository. You may need to manually promote a device to '. 'unfreeze it. See "Ambiguous Leaders" in the documentation '. 'for discussion.')) ->drawConsoleString(); $prompt = pht('Continue anyway?'); if (!phutil_console_confirm($prompt)) { throw new PhutilArgumentUsageException( pht('User aborted the workflow.')); } } } else { $service_phid = null; } $content_source = $this->newContentSource(); $diffusion_phid = id(new PhabricatorDiffusionApplication())->getPHID(); foreach ($repositories as $repository) { $xactions = array(); $xactions[] = id(new PhabricatorRepositoryTransaction()) - ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_SERVICE) + ->setTransactionType( + PhabricatorRepositoryServiceTransaction::TRANSACTIONTYPE) ->setNewValue($service_phid); id(new PhabricatorRepositoryEditor()) ->setActor($viewer) ->setActingAsPHID($diffusion_phid) ->setContentSource($content_source) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($repository, $xactions); if ($service) { echo tsprintf( "%s\n", pht( 'Moved repository "%s" to cluster service "%s".', $repository->getDisplayName(), $service->getName())); } else { echo tsprintf( "%s\n", pht( 'Removed repository "%s" from cluster service.', $repository->getDisplayName())); } } return 0; } } diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementParentsWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementParentsWorkflow.php index 671287c3d6..e201ef40fc 100644 --- a/src/applications/repository/management/PhabricatorRepositoryManagementParentsWorkflow.php +++ b/src/applications/repository/management/PhabricatorRepositoryManagementParentsWorkflow.php @@ -1,181 +1,181 @@ setName('parents') ->setExamples('**parents** [options] [__repository__] ...') ->setSynopsis( pht( 'Build parent caches in repositories that are missing the data, '. 'or rebuild them in a specific __repository__.')) ->setArguments( array( array( 'name' => 'repos', 'wildcard' => true, ), )); } public function execute(PhutilArgumentParser $args) { $repos = $this->loadRepositories($args, 'repos'); if (!$repos) { $repos = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()) ->execute(); } $console = PhutilConsole::getConsole(); foreach ($repos as $repo) { $monogram = $repo->getMonogram(); if ($repo->isSVN()) { $console->writeOut( "%s\n", pht( 'Skipping "%s": Subversion repositories do not require this '. 'cache to be built.', $monogram)); continue; } $this->rebuildRepository($repo); } return 0; } private function rebuildRepository(PhabricatorRepository $repo) { $console = PhutilConsole::getConsole(); $console->writeOut("%s\n", pht('Rebuilding "%s"...', $repo->getMonogram())); $refs = id(new PhabricatorRepositoryRefCursorQuery()) ->setViewer($this->getViewer()) ->withRefTypes(array(PhabricatorRepositoryRefCursor::TYPE_BRANCH)) ->withRepositoryPHIDs(array($repo->getPHID())) ->needPositions(true) ->execute(); $graph = array(); foreach ($refs as $ref) { if (!$repo->shouldTrackBranch($ref->getRefName())) { continue; } $console->writeOut( "%s\n", pht('Rebuilding branch "%s"...', $ref->getRefName())); foreach ($ref->getPositionIdentifiers() as $commit) { if ($repo->isGit()) { $stream = new PhabricatorGitGraphStream($repo, $commit); } else { $stream = new PhabricatorMercurialGraphStream($repo, $commit); } $discover = array($commit); while ($discover) { $target = array_pop($discover); if (isset($graph[$target])) { continue; } $graph[$target] = $stream->getParents($target); foreach ($graph[$target] as $parent) { $discover[] = $parent; } } } } $console->writeOut( "%s\n", pht( 'Found %s total commit(s); updating...', phutil_count($graph))); $commit_table = id(new PhabricatorRepositoryCommit()); $commit_table_name = $commit_table->getTableName(); $conn_w = $commit_table->establishConnection('w'); $bar = id(new PhutilConsoleProgressBar()) ->setTotal(count($graph)); $need = array(); foreach ($graph as $child => $parents) { foreach ($parents as $parent) { $need[$parent] = $parent; } $need[$child] = $child; } $map = array(); foreach (array_chunk($need, 2048) as $chunk) { $rows = queryfx_all( $conn_w, 'SELECT id, commitIdentifier FROM %T WHERE commitIdentifier IN (%Ls) AND repositoryID = %d', $commit_table_name, $chunk, $repo->getID()); foreach ($rows as $row) { $map[$row['commitIdentifier']] = $row['id']; } } $insert_sql = array(); $delete_sql = array(); foreach ($graph as $child => $parents) { $names = $parents; $names[] = $child; foreach ($names as $name) { if (empty($map[$name])) { throw new Exception(pht('Unknown commit "%s"!', $name)); } } if (!$parents) { // Write an explicit 0 to indicate "no parents" instead of "no data". $insert_sql[] = qsprintf( $conn_w, '(%d, 0)', $map[$child]); } else { foreach ($parents as $parent) { $insert_sql[] = qsprintf( $conn_w, '(%d, %d)', $map[$child], $map[$parent]); } } $delete_sql[] = $map[$child]; $bar->update(1); } $commit_table->openTransaction(); foreach (PhabricatorLiskDAO::chunkSQL($delete_sql) as $chunk) { queryfx( $conn_w, - 'DELETE FROM %T WHERE childCommitID IN (%Q)', + 'DELETE FROM %T WHERE childCommitID IN (%LQ)', PhabricatorRepository::TABLE_PARENTS, $chunk); } foreach (PhabricatorLiskDAO::chunkSQL($insert_sql) as $chunk) { queryfx( $conn_w, - 'INSERT INTO %T (childCommitID, parentCommitID) VALUES %Q', + 'INSERT INTO %T (childCommitID, parentCommitID) VALUES %LQ', PhabricatorRepository::TABLE_PARENTS, $chunk); } $commit_table->saveTransaction(); $bar->done(); } } diff --git a/src/applications/repository/query/PhabricatorRepositoryQuery.php b/src/applications/repository/query/PhabricatorRepositoryQuery.php index 035693ff75..56c62cc8fb 100644 --- a/src/applications/repository/query/PhabricatorRepositoryQuery.php +++ b/src/applications/repository/query/PhabricatorRepositoryQuery.php @@ -1,726 +1,726 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withCallsigns(array $callsigns) { $this->callsigns = $callsigns; return $this; } public function withIdentifiers(array $identifiers) { $identifiers = array_fuse($identifiers); $ids = array(); $callsigns = array(); $phids = array(); $monograms = array(); $slugs = array(); foreach ($identifiers as $identifier) { if (ctype_digit((string)$identifier)) { $ids[$identifier] = $identifier; continue; } if (preg_match('/^(r[A-Z]+|R[1-9]\d*)\z/', $identifier)) { $monograms[$identifier] = $identifier; continue; } $repository_type = PhabricatorRepositoryRepositoryPHIDType::TYPECONST; if (phid_get_type($identifier) === $repository_type) { $phids[$identifier] = $identifier; continue; } if (preg_match('/^[A-Z]+\z/', $identifier)) { $callsigns[$identifier] = $identifier; continue; } $slugs[$identifier] = $identifier; } $this->numericIdentifiers = $ids; $this->callsignIdentifiers = $callsigns; $this->phidIdentifiers = $phids; $this->monogramIdentifiers = $monograms; $this->slugIdentifiers = $slugs; return $this; } public function withStatus($status) { $this->status = $status; return $this; } public function withHosted($hosted) { $this->hosted = $hosted; return $this; } public function withTypes(array $types) { $this->types = $types; return $this; } public function withUUIDs(array $uuids) { $this->uuids = $uuids; return $this; } public function withURIs(array $uris) { $this->uris = $uris; return $this; } public function withDatasourceQuery($query) { $this->datasourceQuery = $query; return $this; } public function withSlugs(array $slugs) { $this->slugs = $slugs; return $this; } public function withAlmanacServicePHIDs(array $phids) { $this->almanacServicePHIDs = $phids; return $this; } public function needCommitCounts($need_counts) { $this->needCommitCounts = $need_counts; return $this; } public function needMostRecentCommits($need_commits) { $this->needMostRecentCommits = $need_commits; return $this; } public function needProjectPHIDs($need_phids) { $this->needProjectPHIDs = $need_phids; return $this; } public function needURIs($need_uris) { $this->needURIs = $need_uris; return $this; } public function needProfileImage($need) { $this->needProfileImage = $need; return $this; } public function getBuiltinOrders() { return array( 'committed' => array( 'vector' => array('committed', 'id'), 'name' => pht('Most Recent Commit'), ), 'name' => array( 'vector' => array('name', 'id'), 'name' => pht('Name'), ), 'callsign' => array( 'vector' => array('callsign'), 'name' => pht('Callsign'), ), 'size' => array( 'vector' => array('size', 'id'), 'name' => pht('Size'), ), ) + parent::getBuiltinOrders(); } public function getIdentifierMap() { if ($this->identifierMap === null) { throw new PhutilInvalidStateException('execute'); } return $this->identifierMap; } protected function willExecute() { $this->identifierMap = array(); } public function newResultObject() { return new PhabricatorRepository(); } protected function loadPage() { $table = $this->newResultObject(); $data = $this->loadStandardPageRows($table); $repositories = $table->loadAllFromArray($data); if ($this->needCommitCounts) { $sizes = ipull($data, 'size', 'id'); foreach ($repositories as $id => $repository) { $repository->attachCommitCount(nonempty($sizes[$id], 0)); } } if ($this->needMostRecentCommits) { $commit_ids = ipull($data, 'lastCommitID', 'id'); $commit_ids = array_filter($commit_ids); if ($commit_ids) { $commits = id(new DiffusionCommitQuery()) ->setViewer($this->getViewer()) ->withIDs($commit_ids) ->execute(); } else { $commits = array(); } foreach ($repositories as $id => $repository) { $commit = null; if (idx($commit_ids, $id)) { $commit = idx($commits, $commit_ids[$id]); } $repository->attachMostRecentCommit($commit); } } return $repositories; } protected function willFilterPage(array $repositories) { assert_instances_of($repositories, 'PhabricatorRepository'); // TODO: Denormalize repository status into the PhabricatorRepository // table so we can do this filtering in the database. foreach ($repositories as $key => $repo) { $status = $this->status; switch ($status) { case self::STATUS_OPEN: if (!$repo->isTracked()) { unset($repositories[$key]); } break; case self::STATUS_CLOSED: if ($repo->isTracked()) { unset($repositories[$key]); } break; case self::STATUS_ALL: break; default: throw new Exception("Unknown status '{$status}'!"); } // TODO: This should also be denormalized. $hosted = $this->hosted; switch ($hosted) { case self::HOSTED_PHABRICATOR: if (!$repo->isHosted()) { unset($repositories[$key]); } break; case self::HOSTED_REMOTE: if ($repo->isHosted()) { unset($repositories[$key]); } break; case self::HOSTED_ALL: break; default: throw new Exception(pht("Unknown hosted failed '%s'!", $hosted)); } } // Build the identifierMap if ($this->numericIdentifiers) { foreach ($this->numericIdentifiers as $id) { if (isset($repositories[$id])) { $this->identifierMap[$id] = $repositories[$id]; } } } if ($this->callsignIdentifiers) { $repository_callsigns = mpull($repositories, null, 'getCallsign'); foreach ($this->callsignIdentifiers as $callsign) { if (isset($repository_callsigns[$callsign])) { $this->identifierMap[$callsign] = $repository_callsigns[$callsign]; } } } if ($this->phidIdentifiers) { $repository_phids = mpull($repositories, null, 'getPHID'); foreach ($this->phidIdentifiers as $phid) { if (isset($repository_phids[$phid])) { $this->identifierMap[$phid] = $repository_phids[$phid]; } } } if ($this->monogramIdentifiers) { $monogram_map = array(); foreach ($repositories as $repository) { foreach ($repository->getAllMonograms() as $monogram) { $monogram_map[$monogram] = $repository; } } foreach ($this->monogramIdentifiers as $monogram) { if (isset($monogram_map[$monogram])) { $this->identifierMap[$monogram] = $monogram_map[$monogram]; } } } if ($this->slugIdentifiers) { $slug_map = array(); foreach ($repositories as $repository) { $slug = $repository->getRepositorySlug(); if ($slug === null) { continue; } $normal = phutil_utf8_strtolower($slug); $slug_map[$normal] = $repository; } foreach ($this->slugIdentifiers as $slug) { $normal = phutil_utf8_strtolower($slug); if (isset($slug_map[$normal])) { $this->identifierMap[$slug] = $slug_map[$normal]; } } } return $repositories; } protected function didFilterPage(array $repositories) { if ($this->needProjectPHIDs) { $type_project = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(mpull($repositories, 'getPHID')) ->withEdgeTypes(array($type_project)); $edge_query->execute(); foreach ($repositories as $repository) { $project_phids = $edge_query->getDestinationPHIDs( array( $repository->getPHID(), )); $repository->attachProjectPHIDs($project_phids); } } $viewer = $this->getViewer(); if ($this->needURIs) { $uris = id(new PhabricatorRepositoryURIQuery()) ->setViewer($viewer) ->withRepositories($repositories) ->execute(); $uri_groups = mgroup($uris, 'getRepositoryPHID'); foreach ($repositories as $repository) { $repository_uris = idx($uri_groups, $repository->getPHID(), array()); $repository->attachURIs($repository_uris); } } if ($this->needProfileImage) { $default = null; $file_phids = mpull($repositories, 'getProfileImagePHID'); $file_phids = array_filter($file_phids); if ($file_phids) { $files = id(new PhabricatorFileQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); } else { $files = array(); } foreach ($repositories as $repository) { $file = idx($files, $repository->getProfileImagePHID()); if (!$file) { if (!$default) { $default = PhabricatorFile::loadBuiltin( $this->getViewer(), 'repo/code.png'); } $file = $default; } $repository->attachProfileImageFile($file); } } return $repositories; } protected function getPrimaryTableAlias() { return 'r'; } public function getOrderableColumns() { return parent::getOrderableColumns() + array( 'committed' => array( 'table' => 's', 'column' => 'epoch', 'type' => 'int', 'null' => 'tail', ), 'callsign' => array( 'table' => 'r', 'column' => 'callsign', 'type' => 'string', 'unique' => true, 'reverse' => true, 'null' => 'tail', ), 'name' => array( 'table' => 'r', 'column' => 'name', 'type' => 'string', 'reverse' => true, ), 'size' => array( 'table' => 's', 'column' => 'size', 'type' => 'int', 'null' => 'tail', ), ); } protected function willExecuteCursorQuery( PhabricatorCursorPagedPolicyAwareQuery $query) { $vector = $this->getOrderVector(); if ($vector->containsKey('committed')) { $query->needMostRecentCommits(true); } if ($vector->containsKey('size')) { $query->needCommitCounts(true); } } protected function getPagingValueMap($cursor, array $keys) { $repository = $this->loadCursorObject($cursor); $map = array( 'id' => $repository->getID(), 'callsign' => $repository->getCallsign(), 'name' => $repository->getName(), ); foreach ($keys as $key) { switch ($key) { case 'committed': $commit = $repository->getMostRecentCommit(); if ($commit) { $map[$key] = $commit->getEpoch(); } else { $map[$key] = null; } break; case 'size': $count = $repository->getCommitCount(); if ($count) { $map[$key] = $count; } else { $map[$key] = null; } break; } } return $map; } protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) { $parts = parent::buildSelectClauseParts($conn); - $parts[] = 'r.*'; + $parts[] = qsprintf($conn, 'r.*'); if ($this->shouldJoinSummaryTable()) { - $parts[] = 's.*'; + $parts[] = qsprintf($conn, 's.*'); } return $parts; } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { $joins = parent::buildJoinClauseParts($conn); if ($this->shouldJoinSummaryTable()) { $joins[] = qsprintf( $conn, 'LEFT JOIN %T s ON r.id = s.repositoryID', PhabricatorRepository::TABLE_SUMMARY); } if ($this->shouldJoinURITable()) { $joins[] = qsprintf( $conn, - 'LEFT JOIN %T uri ON r.phid = uri.repositoryPHID', - id(new PhabricatorRepositoryURIIndex())->getTableName()); + 'LEFT JOIN %R uri ON r.phid = uri.repositoryPHID', + new PhabricatorRepositoryURIIndex()); } return $joins; } protected function shouldGroupQueryResultRows() { if ($this->shouldJoinURITable()) { return true; } return parent::shouldGroupQueryResultRows(); } private function shouldJoinURITable() { return ($this->uris !== null); } private function shouldJoinSummaryTable() { if ($this->needCommitCounts) { return true; } if ($this->needMostRecentCommits) { return true; } $vector = $this->getOrderVector(); if ($vector->containsKey('committed')) { return true; } if ($vector->containsKey('size')) { return true; } return false; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'r.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'r.phid IN (%Ls)', $this->phids); } if ($this->callsigns !== null) { $where[] = qsprintf( $conn, 'r.callsign IN (%Ls)', $this->callsigns); } if ($this->numericIdentifiers || $this->callsignIdentifiers || $this->phidIdentifiers || $this->monogramIdentifiers || $this->slugIdentifiers) { $identifier_clause = array(); if ($this->numericIdentifiers) { $identifier_clause[] = qsprintf( $conn, 'r.id IN (%Ld)', $this->numericIdentifiers); } if ($this->callsignIdentifiers) { $identifier_clause[] = qsprintf( $conn, 'r.callsign IN (%Ls)', $this->callsignIdentifiers); } if ($this->phidIdentifiers) { $identifier_clause[] = qsprintf( $conn, 'r.phid IN (%Ls)', $this->phidIdentifiers); } if ($this->monogramIdentifiers) { $monogram_callsigns = array(); $monogram_ids = array(); foreach ($this->monogramIdentifiers as $identifier) { if ($identifier[0] == 'r') { $monogram_callsigns[] = substr($identifier, 1); } else { $monogram_ids[] = substr($identifier, 1); } } if ($monogram_ids) { $identifier_clause[] = qsprintf( $conn, 'r.id IN (%Ld)', $monogram_ids); } if ($monogram_callsigns) { $identifier_clause[] = qsprintf( $conn, 'r.callsign IN (%Ls)', $monogram_callsigns); } } if ($this->slugIdentifiers) { $identifier_clause[] = qsprintf( $conn, 'r.repositorySlug IN (%Ls)', $this->slugIdentifiers); } - $where = array('('.implode(' OR ', $identifier_clause).')'); + $where[] = qsprintf($conn, '%LO', $identifier_clause); } if ($this->types) { $where[] = qsprintf( $conn, 'r.versionControlSystem IN (%Ls)', $this->types); } if ($this->uuids) { $where[] = qsprintf( $conn, 'r.uuid IN (%Ls)', $this->uuids); } if (strlen($this->datasourceQuery)) { // This handles having "rP" match callsigns starting with "P...". $query = trim($this->datasourceQuery); if (preg_match('/^r/', $query)) { $callsign = substr($query, 1); } else { $callsign = $query; } $where[] = qsprintf( $conn, 'r.name LIKE %> OR r.callsign LIKE %> OR r.repositorySlug LIKE %>', $query, $callsign, $query); } if ($this->slugs !== null) { $where[] = qsprintf( $conn, 'r.repositorySlug IN (%Ls)', $this->slugs); } if ($this->uris !== null) { $try_uris = $this->getNormalizedURIs(); $try_uris = array_fuse($try_uris); $where[] = qsprintf( $conn, 'uri.repositoryURI IN (%Ls)', $try_uris); } if ($this->almanacServicePHIDs !== null) { $where[] = qsprintf( $conn, 'r.almanacServicePHID IN (%Ls)', $this->almanacServicePHIDs); } return $where; } public function getQueryApplicationClass() { return 'PhabricatorDiffusionApplication'; } private function getNormalizedURIs() { $normalized_uris = array(); // Since we don't know which type of repository this URI is in the general // case, just generate all the normalizations. We could refine this in some // cases: if the query specifies VCS types, or the URI is a git-style URI // or an `svn+ssh` URI, we could deduce how to normalize it. However, this // would be more complicated and it's not clear if it matters in practice. $types = PhabricatorRepositoryURINormalizer::getAllURITypes(); foreach ($this->uris as $uri) { foreach ($types as $type) { $normalized_uri = new PhabricatorRepositoryURINormalizer($type, $uri); $normalized_uris[] = $normalized_uri->getNormalizedURI(); } } return array_unique($normalized_uris); } } diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index 52824c2aa4..3b7918cd59 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -1,2767 +1,2812 @@ setViewer($actor) ->withClasses(array('PhabricatorDiffusionApplication')) ->executeOne(); $view_policy = $app->getPolicy(DiffusionDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy(DiffusionDefaultEditCapability::CAPABILITY); $push_policy = $app->getPolicy(DiffusionDefaultPushCapability::CAPABILITY); $repository = id(new PhabricatorRepository()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setPushPolicy($push_policy) ->setSpacePHID($actor->getDefaultSpacePHID()); // Put the repository in "Importing" mode until we finish // parsing it. $repository->setDetail('importing', true); return $repository; } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'details' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort255', 'callsign' => 'sort32?', 'repositorySlug' => 'sort64?', 'versionControlSystem' => 'text32', 'uuid' => 'text64?', 'pushPolicy' => 'policy', 'credentialPHID' => 'phid?', 'almanacServicePHID' => 'phid?', 'localPath' => 'text128?', 'profileImagePHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'callsign' => array( 'columns' => array('callsign'), 'unique' => true, ), 'key_name' => array( 'columns' => array('name(128)'), ), 'key_vcs' => array( 'columns' => array('versionControlSystem'), ), 'key_slug' => array( 'columns' => array('repositorySlug'), 'unique' => true, ), 'key_local' => array( 'columns' => array('localPath'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorRepositoryRepositoryPHIDType::TYPECONST); } public static function getStatusMap() { return array( self::STATUS_ACTIVE => array( 'name' => pht('Active'), 'isTracked' => 1, ), self::STATUS_INACTIVE => array( 'name' => pht('Inactive'), 'isTracked' => 0, ), ); } public static function getStatusNameMap() { return ipull(self::getStatusMap(), 'name'); } public function getStatus() { if ($this->isTracked()) { return self::STATUS_ACTIVE; } else { return self::STATUS_INACTIVE; } } public function toDictionary() { return array( 'id' => $this->getID(), 'name' => $this->getName(), 'phid' => $this->getPHID(), 'callsign' => $this->getCallsign(), 'monogram' => $this->getMonogram(), 'vcs' => $this->getVersionControlSystem(), 'uri' => PhabricatorEnv::getProductionURI($this->getURI()), 'remoteURI' => (string)$this->getRemoteURI(), 'description' => $this->getDetail('description'), 'isActive' => $this->isTracked(), 'isHosted' => $this->isHosted(), 'isImporting' => $this->isImporting(), 'encoding' => $this->getDefaultTextEncoding(), 'staging' => array( 'supported' => $this->supportsStaging(), 'prefix' => 'phabricator', 'uri' => $this->getStagingURI(), ), ); } public function getDefaultTextEncoding() { return $this->getDetail('encoding', 'UTF-8'); } public function getMonogram() { $callsign = $this->getCallsign(); if (strlen($callsign)) { return "r{$callsign}"; } $id = $this->getID(); return "R{$id}"; } public function getDisplayName() { $slug = $this->getRepositorySlug(); if (strlen($slug)) { return $slug; } return $this->getMonogram(); } public function getAllMonograms() { $monograms = array(); $monograms[] = 'R'.$this->getID(); $callsign = $this->getCallsign(); if (strlen($callsign)) { $monograms[] = 'r'.$callsign; } return $monograms; } public function setLocalPath($path) { // Convert any extra slashes ("//") in the path to a single slash ("/"). $path = preg_replace('(//+)', '/', $path); return parent::setLocalPath($path); } public function getDetail($key, $default = null) { return idx($this->details, $key, $default); } public function getHumanReadableDetail($key, $default = null) { $value = $this->getDetail($key, $default); switch ($key) { case 'branch-filter': case 'close-commits-filter': $value = array_keys($value); $value = implode(', ', $value); break; } return $value; } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } public function attachCommitCount($count) { $this->commitCount = $count; return $this; } public function getCommitCount() { return $this->assertAttached($this->commitCount); } public function attachMostRecentCommit( PhabricatorRepositoryCommit $commit = null) { $this->mostRecentCommit = $commit; return $this; } public function getMostRecentCommit() { return $this->assertAttached($this->mostRecentCommit); } public function getDiffusionBrowseURIForPath( PhabricatorUser $user, $path, $line = null, $branch = null) { $drequest = DiffusionRequest::newFromDictionary( array( 'user' => $user, 'repository' => $this, 'path' => $path, 'branch' => $branch, )); return $drequest->generateURI( array( 'action' => 'browse', 'line' => $line, )); } public function getSubversionBaseURI($commit = null) { $subpath = $this->getDetail('svn-subpath'); if (!strlen($subpath)) { $subpath = null; } return $this->getSubversionPathURI($subpath, $commit); } public function getSubversionPathURI($path = null, $commit = null) { $vcs = $this->getVersionControlSystem(); if ($vcs != PhabricatorRepositoryType::REPOSITORY_TYPE_SVN) { throw new Exception(pht('Not a subversion repository!')); } if ($this->isHosted()) { $uri = 'file://'.$this->getLocalPath(); } else { $uri = $this->getDetail('remote-uri'); } $uri = rtrim($uri, '/'); if (strlen($path)) { $path = rawurlencode($path); $path = str_replace('%2F', '/', $path); $uri = $uri.'/'.ltrim($path, '/'); } if ($path !== null || $commit !== null) { $uri .= '@'; } if ($commit !== null) { $uri .= $commit; } return $uri; } public function attachProjectPHIDs(array $project_phids) { $this->projectPHIDs = $project_phids; return $this; } public function getProjectPHIDs() { return $this->assertAttached($this->projectPHIDs); } /** * Get the name of the directory this repository should clone or checkout * into. For example, if the repository name is "Example Repository", a * reasonable name might be "example-repository". This is used to help users * get reasonable results when cloning repositories, since they generally do * not want to clone into directories called "X/" or "Example Repository/". * * @return string */ public function getCloneName() { $name = $this->getRepositorySlug(); // Make some reasonable effort to produce reasonable default directory // names from repository names. if (!strlen($name)) { $name = $this->getName(); $name = phutil_utf8_strtolower($name); $name = preg_replace('@[ -/:->]+@', '-', $name); $name = trim($name, '-'); if (!strlen($name)) { $name = $this->getCallsign(); } } return $name; } public static function isValidRepositorySlug($slug) { try { self::assertValidRepositorySlug($slug); return true; } catch (Exception $ex) { return false; } } public static function assertValidRepositorySlug($slug) { if (!strlen($slug)) { throw new Exception( pht( 'The empty string is not a valid repository short name. '. 'Repository short names must be at least one character long.')); } if (strlen($slug) > 64) { throw new Exception( pht( 'The name "%s" is not a valid repository short name. Repository '. 'short names must not be longer than 64 characters.', $slug)); } if (preg_match('/[^a-zA-Z0-9._-]/', $slug)) { throw new Exception( pht( 'The name "%s" is not a valid repository short name. Repository '. 'short names may only contain letters, numbers, periods, hyphens '. 'and underscores.', $slug)); } if (!preg_match('/^[a-zA-Z0-9]/', $slug)) { throw new Exception( pht( 'The name "%s" is not a valid repository short name. Repository '. 'short names must begin with a letter or number.', $slug)); } if (!preg_match('/[a-zA-Z0-9]\z/', $slug)) { throw new Exception( pht( 'The name "%s" is not a valid repository short name. Repository '. 'short names must end with a letter or number.', $slug)); } if (preg_match('/__|--|\\.\\./', $slug)) { throw new Exception( pht( 'The name "%s" is not a valid repository short name. Repository '. 'short names must not contain multiple consecutive underscores, '. 'hyphens, or periods.', $slug)); } if (preg_match('/^[A-Z]+\z/', $slug)) { throw new Exception( pht( 'The name "%s" is not a valid repository short name. Repository '. 'short names may not contain only uppercase letters.', $slug)); } if (preg_match('/^\d+\z/', $slug)) { throw new Exception( pht( 'The name "%s" is not a valid repository short name. Repository '. 'short names may not contain only numbers.', $slug)); } if (preg_match('/\\.git/', $slug)) { throw new Exception( pht( 'The name "%s" is not a valid repository short name. Repository '. 'short names must not end in ".git". This suffix will be added '. 'automatically in appropriate contexts.', $slug)); } } public static function assertValidCallsign($callsign) { if (!strlen($callsign)) { throw new Exception( pht( 'A repository callsign must be at least one character long.')); } if (strlen($callsign) > 32) { throw new Exception( pht( 'The callsign "%s" is not a valid repository callsign. Callsigns '. 'must be no more than 32 bytes long.', $callsign)); } if (!preg_match('/^[A-Z]+\z/', $callsign)) { throw new Exception( pht( 'The callsign "%s" is not a valid repository callsign. Callsigns '. 'may only contain UPPERCASE letters.', $callsign)); } } public function getProfileImageURI() { return $this->getProfileImageFile()->getBestURI(); } public function attachProfileImageFile(PhabricatorFile $file) { $this->profileImageFile = $file; return $this; } public function getProfileImageFile() { return $this->assertAttached($this->profileImageFile); } /* -( Remote Command Execution )------------------------------------------- */ public function execRemoteCommand($pattern /* , $arg, ... */) { $args = func_get_args(); return $this->newRemoteCommandFuture($args)->resolve(); } public function execxRemoteCommand($pattern /* , $arg, ... */) { $args = func_get_args(); return $this->newRemoteCommandFuture($args)->resolvex(); } public function getRemoteCommandFuture($pattern /* , $arg, ... */) { $args = func_get_args(); return $this->newRemoteCommandFuture($args); } public function passthruRemoteCommand($pattern /* , $arg, ... */) { $args = func_get_args(); return $this->newRemoteCommandPassthru($args)->execute(); } private function newRemoteCommandFuture(array $argv) { return $this->newRemoteCommandEngine($argv) ->newFuture(); } private function newRemoteCommandPassthru(array $argv) { return $this->newRemoteCommandEngine($argv) ->setPassthru(true) ->newFuture(); } private function newRemoteCommandEngine(array $argv) { return DiffusionCommandEngine::newCommandEngine($this) ->setArgv($argv) ->setCredentialPHID($this->getCredentialPHID()) ->setURI($this->getRemoteURIObject()); } /* -( Local Command Execution )-------------------------------------------- */ public function execLocalCommand($pattern /* , $arg, ... */) { $args = func_get_args(); return $this->newLocalCommandFuture($args)->resolve(); } public function execxLocalCommand($pattern /* , $arg, ... */) { $args = func_get_args(); return $this->newLocalCommandFuture($args)->resolvex(); } public function getLocalCommandFuture($pattern /* , $arg, ... */) { $args = func_get_args(); return $this->newLocalCommandFuture($args); } public function passthruLocalCommand($pattern /* , $arg, ... */) { $args = func_get_args(); return $this->newLocalCommandPassthru($args)->execute(); } private function newLocalCommandFuture(array $argv) { $this->assertLocalExists(); $future = DiffusionCommandEngine::newCommandEngine($this) ->setArgv($argv) ->newFuture(); if ($this->usesLocalWorkingCopy()) { $future->setCWD($this->getLocalPath()); } return $future; } private function newLocalCommandPassthru(array $argv) { $this->assertLocalExists(); $future = DiffusionCommandEngine::newCommandEngine($this) ->setArgv($argv) ->setPassthru(true) ->newFuture(); if ($this->usesLocalWorkingCopy()) { $future->setCWD($this->getLocalPath()); } return $future; } public function getURI() { $short_name = $this->getRepositorySlug(); if (strlen($short_name)) { return "/source/{$short_name}/"; } $callsign = $this->getCallsign(); if (strlen($callsign)) { return "/diffusion/{$callsign}/"; } $id = $this->getID(); return "/diffusion/{$id}/"; } public function getPathURI($path) { return $this->getURI().ltrim($path, '/'); } public function getCommitURI($identifier) { $callsign = $this->getCallsign(); if (strlen($callsign)) { return "/r{$callsign}{$identifier}"; } $id = $this->getID(); return "/R{$id}:{$identifier}"; } public static function parseRepositoryServicePath($request_path, $vcs) { $is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT); $patterns = array( '(^'. '(?P/?(?:diffusion|source)/(?P[^/]+))'. '(?P.*)'. '\z)', ); $identifier = null; foreach ($patterns as $pattern) { $matches = null; if (!preg_match($pattern, $request_path, $matches)) { continue; } $identifier = $matches['identifier']; if ($is_git) { $identifier = preg_replace('/\\.git\z/', '', $identifier); } $base = $matches['base']; $path = $matches['path']; break; } if ($identifier === null) { return null; } return array( 'identifier' => $identifier, 'base' => $base, 'path' => $path, ); } public function getCanonicalPath($request_path) { $standard_pattern = '(^'. '(?P/(?:diffusion|source)/)'. '(?P[^/]+)'. '(?P(?:/.*)?)'. '\z)'; $matches = null; if (preg_match($standard_pattern, $request_path, $matches)) { $suffix = $matches['suffix']; return $this->getPathURI($suffix); } $commit_pattern = '(^'. '(?P/)'. '(?P'. '(?:'. 'r(?P[A-Z]+)'. '|'. 'R(?P[1-9]\d*):'. ')'. '(?P[a-f0-9]+)'. ')'. '\z)'; $matches = null; if (preg_match($commit_pattern, $request_path, $matches)) { $commit = $matches['commit']; return $this->getCommitURI($commit); } return null; } public function generateURI(array $params) { $req_branch = false; $req_commit = false; $action = idx($params, 'action'); switch ($action) { case 'history': case 'graph': case 'clone': case 'blame': case 'browse': case 'document': case 'change': case 'lastmodified': case 'tags': case 'branches': case 'lint': case 'pathtree': case 'refs': case 'compare': break; case 'branch': // NOTE: This does not actually require a branch, and won't have one // in Subversion. Possibly this should be more clear. break; case 'commit': case 'rendering-ref': $req_commit = true; break; default: throw new Exception( pht( 'Action "%s" is not a valid repository URI action.', $action)); } $path = idx($params, 'path'); $branch = idx($params, 'branch'); $commit = idx($params, 'commit'); $line = idx($params, 'line'); $head = idx($params, 'head'); $against = idx($params, 'against'); if ($req_commit && !strlen($commit)) { throw new Exception( pht( 'Diffusion URI action "%s" requires commit!', $action)); } if ($req_branch && !strlen($branch)) { throw new Exception( pht( 'Diffusion URI action "%s" requires branch!', $action)); } if ($action === 'commit') { return $this->getCommitURI($commit); } if (strlen($path)) { $path = ltrim($path, '/'); $path = str_replace(array(';', '$'), array(';;', '$$'), $path); $path = phutil_escape_uri($path); } $raw_branch = $branch; if (strlen($branch)) { $branch = phutil_escape_uri_path_component($branch); $path = "{$branch}/{$path}"; } $raw_commit = $commit; if (strlen($commit)) { $commit = str_replace('$', '$$', $commit); $commit = ';'.phutil_escape_uri($commit); } if (strlen($line)) { $line = '$'.phutil_escape_uri($line); } $query = array(); switch ($action) { case 'change': case 'history': case 'graph': case 'blame': case 'browse': case 'document': case 'lastmodified': case 'tags': case 'branches': case 'lint': case 'pathtree': case 'refs': $uri = $this->getPathURI("/{$action}/{$path}{$commit}{$line}"); break; case 'compare': $uri = $this->getPathURI("/{$action}/"); if (strlen($head)) { $query['head'] = $head; } else if (strlen($raw_commit)) { $query['commit'] = $raw_commit; } else if (strlen($raw_branch)) { $query['head'] = $raw_branch; } if (strlen($against)) { $query['against'] = $against; } break; case 'branch': if (strlen($path)) { $uri = $this->getPathURI("/repository/{$path}"); } else { $uri = $this->getPathURI('/'); } break; case 'external': $commit = ltrim($commit, ';'); $uri = "/diffusion/external/{$commit}/"; break; case 'rendering-ref': // This isn't a real URI per se, it's passed as a query parameter to // the ajax changeset stuff but then we parse it back out as though // it came from a URI. $uri = rawurldecode("{$path}{$commit}"); break; case 'clone': $uri = $this->getPathURI("/{$action}/"); break; } if ($action == 'rendering-ref') { return $uri; } $uri = new PhutilURI($uri); if (isset($params['lint'])) { $params['params'] = idx($params, 'params', array()) + array( 'lint' => $params['lint'], ); } $query = idx($params, 'params', array()) + $query; if ($query) { $uri->setQueryParams($query); } return $uri; } public function updateURIIndex() { $indexes = array(); $uris = $this->getURIs(); foreach ($uris as $uri) { if ($uri->getIsDisabled()) { continue; } $indexes[] = $uri->getNormalizedURI(); } PhabricatorRepositoryURIIndex::updateRepositoryURIs( $this->getPHID(), $indexes); return $this; } public function isTracked() { $status = $this->getDetail('tracking-enabled'); $map = self::getStatusMap(); $spec = idx($map, $status); if (!$spec) { if ($status) { $status = self::STATUS_ACTIVE; } else { $status = self::STATUS_INACTIVE; } $spec = idx($map, $status); } return (bool)idx($spec, 'isTracked', false); } public function getDefaultBranch() { $default = $this->getDetail('default-branch'); if (strlen($default)) { return $default; } $default_branches = array( PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'master', PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => 'default', ); return idx($default_branches, $this->getVersionControlSystem()); } public function getDefaultArcanistBranch() { return coalesce($this->getDefaultBranch(), 'svn'); } private function isBranchInFilter($branch, $filter_key) { $vcs = $this->getVersionControlSystem(); $is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT); $use_filter = ($is_git); if (!$use_filter) { // If this VCS doesn't use filters, pass everything through. return true; } $filter = $this->getDetail($filter_key, array()); // If there's no filter set, let everything through. if (!$filter) { return true; } // If this branch isn't literally named `regexp(...)`, and it's in the // filter list, let it through. if (isset($filter[$branch])) { if (self::extractBranchRegexp($branch) === null) { return true; } } // If the branch matches a regexp, let it through. foreach ($filter as $pattern => $ignored) { $regexp = self::extractBranchRegexp($pattern); if ($regexp !== null) { if (preg_match($regexp, $branch)) { return true; } } } // Nothing matched, so filter this branch out. return false; } public static function extractBranchRegexp($pattern) { $matches = null; if (preg_match('/^regexp\\((.*)\\)\z/', $pattern, $matches)) { return $matches[1]; } return null; } public function shouldTrackRef(DiffusionRepositoryRef $ref) { // At least for now, don't track the staging area tags. if ($ref->isTag()) { if (preg_match('(^phabricator/)', $ref->getShortName())) { return false; } } if (!$ref->isBranch()) { return true; } return $this->shouldTrackBranch($ref->getShortName()); } public function shouldTrackBranch($branch) { return $this->isBranchInFilter($branch, 'branch-filter'); } public function formatCommitName($commit_identifier, $local = false) { $vcs = $this->getVersionControlSystem(); $type_git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; $type_hg = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL; $is_git = ($vcs == $type_git); $is_hg = ($vcs == $type_hg); if ($is_git || $is_hg) { $name = substr($commit_identifier, 0, 12); $need_scope = false; } else { $name = $commit_identifier; $need_scope = true; } if (!$local) { $need_scope = true; } if ($need_scope) { $callsign = $this->getCallsign(); if ($callsign) { $scope = "r{$callsign}"; } else { $id = $this->getID(); $scope = "R{$id}:"; } $name = $scope.$name; } return $name; } public function isImporting() { return (bool)$this->getDetail('importing', false); } public function isNewlyInitialized() { return (bool)$this->getDetail('newly-initialized', false); } public function loadImportProgress() { $progress = queryfx_all( $this->establishConnection('r'), 'SELECT importStatus, count(*) N FROM %T WHERE repositoryID = %d GROUP BY importStatus', id(new PhabricatorRepositoryCommit())->getTableName(), $this->getID()); $done = 0; $total = 0; foreach ($progress as $row) { $total += $row['N'] * 4; $status = $row['importStatus']; if ($status & PhabricatorRepositoryCommit::IMPORTED_MESSAGE) { $done += $row['N']; } if ($status & PhabricatorRepositoryCommit::IMPORTED_CHANGE) { $done += $row['N']; } if ($status & PhabricatorRepositoryCommit::IMPORTED_OWNERS) { $done += $row['N']; } if ($status & PhabricatorRepositoryCommit::IMPORTED_HERALD) { $done += $row['N']; } } if ($total) { $ratio = ($done / $total); } else { $ratio = 0; } // Cap this at "99.99%", because it's confusing to users when the actual // fraction is "99.996%" and it rounds up to "100.00%". if ($ratio > 0.9999) { $ratio = 0.9999; } return $ratio; } /** * Should this repository publish feed, notifications, audits, and email? * * We do not publish information about repositories during initial import, * or if the repository has been set not to publish. */ public function shouldPublish() { if ($this->isImporting()) { return false; } if ($this->getDetail('herald-disabled')) { return false; } return true; } /* -( Autoclose )---------------------------------------------------------- */ public function shouldAutocloseRef(DiffusionRepositoryRef $ref) { if (!$ref->isBranch()) { return false; } return $this->shouldAutocloseBranch($ref->getShortName()); } /** * Determine if autoclose is active for a branch. * * For more details about why, use @{method:shouldSkipAutocloseBranch}. * * @param string Branch name to check. * @return bool True if autoclose is active for the branch. * @task autoclose */ public function shouldAutocloseBranch($branch) { return ($this->shouldSkipAutocloseBranch($branch) === null); } /** * Determine if autoclose is active for a commit. * * For more details about why, use @{method:shouldSkipAutocloseCommit}. * * @param PhabricatorRepositoryCommit Commit to check. * @return bool True if autoclose is active for the commit. * @task autoclose */ public function shouldAutocloseCommit(PhabricatorRepositoryCommit $commit) { return ($this->shouldSkipAutocloseCommit($commit) === null); } /** * Determine why autoclose should be skipped for a branch. * * This method gives a detailed reason why autoclose will be skipped. To * perform a simple test, use @{method:shouldAutocloseBranch}. * * @param string Branch name to check. * @return const|null Constant identifying reason to skip this branch, or null * if autoclose is active. * @task autoclose */ public function shouldSkipAutocloseBranch($branch) { $all_reason = $this->shouldSkipAllAutoclose(); if ($all_reason) { return $all_reason; } if (!$this->shouldTrackBranch($branch)) { return self::BECAUSE_BRANCH_UNTRACKED; } if (!$this->isBranchInFilter($branch, 'close-commits-filter')) { return self::BECAUSE_BRANCH_NOT_AUTOCLOSE; } return null; } /** * Determine why autoclose should be skipped for a commit. * * This method gives a detailed reason why autoclose will be skipped. To * perform a simple test, use @{method:shouldAutocloseCommit}. * * @param PhabricatorRepositoryCommit Commit to check. * @return const|null Constant identifying reason to skip this commit, or null * if autoclose is active. * @task autoclose */ public function shouldSkipAutocloseCommit( PhabricatorRepositoryCommit $commit) { $all_reason = $this->shouldSkipAllAutoclose(); if ($all_reason) { return $all_reason; } switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: return null; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: break; default: throw new Exception(pht('Unrecognized version control system.')); } $closeable_flag = PhabricatorRepositoryCommit::IMPORTED_CLOSEABLE; if (!$commit->isPartiallyImported($closeable_flag)) { return self::BECAUSE_NOT_ON_AUTOCLOSE_BRANCH; } return null; } /** * Determine why all autoclose operations should be skipped for this * repository. * * @return const|null Constant identifying reason to skip all autoclose * operations, or null if autoclose operations are not blocked at the * repository level. * @task autoclose */ private function shouldSkipAllAutoclose() { if ($this->isImporting()) { return self::BECAUSE_REPOSITORY_IMPORTING; } if ($this->getDetail('disable-autoclose', false)) { return self::BECAUSE_AUTOCLOSE_DISABLED; } return null; } /* -( Repository URI Management )------------------------------------------ */ /** * Get the remote URI for this repository. * * @return string * @task uri */ public function getRemoteURI() { return (string)$this->getRemoteURIObject(); } /** * Get the remote URI for this repository, including credentials if they're * used by this repository. * * @return PhutilOpaqueEnvelope URI, possibly including credentials. * @task uri */ public function getRemoteURIEnvelope() { $uri = $this->getRemoteURIObject(); $remote_protocol = $this->getRemoteProtocol(); if ($remote_protocol == 'http' || $remote_protocol == 'https') { // For SVN, we use `--username` and `--password` flags separately, so // don't add any credentials here. if (!$this->isSVN()) { $credential_phid = $this->getCredentialPHID(); if ($credential_phid) { $key = PassphrasePasswordKey::loadFromPHID( $credential_phid, PhabricatorUser::getOmnipotentUser()); $uri->setUser($key->getUsernameEnvelope()->openEnvelope()); $uri->setPass($key->getPasswordEnvelope()->openEnvelope()); } } } return new PhutilOpaqueEnvelope((string)$uri); } /** * Get the clone (or checkout) URI for this repository, without authentication * information. * * @return string Repository URI. * @task uri */ public function getPublicCloneURI() { return (string)$this->getCloneURIObject(); } /** * Get the protocol for the repository's remote. * * @return string Protocol, like "ssh" or "git". * @task uri */ public function getRemoteProtocol() { $uri = $this->getRemoteURIObject(); return $uri->getProtocol(); } /** * Get a parsed object representation of the repository's remote URI.. * * @return wild A @{class@libphutil:PhutilURI}. * @task uri */ public function getRemoteURIObject() { $raw_uri = $this->getDetail('remote-uri'); if (!strlen($raw_uri)) { return new PhutilURI(''); } if (!strncmp($raw_uri, '/', 1)) { return new PhutilURI('file://'.$raw_uri); } return new PhutilURI($raw_uri); } /** * Get the "best" clone/checkout URI for this repository, on any protocol. */ public function getCloneURIObject() { if (!$this->isHosted()) { if ($this->isSVN()) { // Make sure we pick up the "Import Only" path for Subversion, so // the user clones the repository starting at the correct path, not // from the root. $base_uri = $this->getSubversionBaseURI(); $base_uri = new PhutilURI($base_uri); $path = $base_uri->getPath(); if (!$path) { $path = '/'; } // If the trailing "@" is not required to escape the URI, strip it for // readability. if (!preg_match('/@.*@/', $path)) { $path = rtrim($path, '@'); } $base_uri->setPath($path); return $base_uri; } else { return $this->getRemoteURIObject(); } } // TODO: This should be cleaned up to deal with all the new URI handling. $another_copy = id(new PhabricatorRepositoryQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($this->getPHID())) ->needURIs(true) ->executeOne(); $clone_uris = $another_copy->getCloneURIs(); if (!$clone_uris) { return null; } return head($clone_uris)->getEffectiveURI(); } private function getRawHTTPCloneURIObject() { $uri = PhabricatorEnv::getProductionURI($this->getURI()); $uri = new PhutilURI($uri); if ($this->isGit()) { $uri->setPath($uri->getPath().$this->getCloneName().'.git'); } else if ($this->isHg()) { $uri->setPath($uri->getPath().$this->getCloneName().'/'); } return $uri; } /** * Determine if we should connect to the remote using SSH flags and * credentials. * * @return bool True to use the SSH protocol. * @task uri */ private function shouldUseSSH() { if ($this->isHosted()) { return false; } $protocol = $this->getRemoteProtocol(); if ($this->isSSHProtocol($protocol)) { return true; } return false; } /** * Determine if we should connect to the remote using HTTP flags and * credentials. * * @return bool True to use the HTTP protocol. * @task uri */ private function shouldUseHTTP() { if ($this->isHosted()) { return false; } $protocol = $this->getRemoteProtocol(); return ($protocol == 'http' || $protocol == 'https'); } /** * Determine if we should connect to the remote using SVN flags and * credentials. * * @return bool True to use the SVN protocol. * @task uri */ private function shouldUseSVNProtocol() { if ($this->isHosted()) { return false; } $protocol = $this->getRemoteProtocol(); return ($protocol == 'svn'); } /** * Determine if a protocol is SSH or SSH-like. * * @param string A protocol string, like "http" or "ssh". * @return bool True if the protocol is SSH-like. * @task uri */ private function isSSHProtocol($protocol) { return ($protocol == 'ssh' || $protocol == 'svn+ssh'); } public function delete() { $this->openTransaction(); $paths = id(new PhabricatorOwnersPath()) ->loadAllWhere('repositoryPHID = %s', $this->getPHID()); foreach ($paths as $path) { $path->delete(); } queryfx( $this->establishConnection('w'), 'DELETE FROM %T WHERE repositoryPHID = %s', id(new PhabricatorRepositorySymbol())->getTableName(), $this->getPHID()); $commits = id(new PhabricatorRepositoryCommit()) ->loadAllWhere('repositoryID = %d', $this->getID()); foreach ($commits as $commit) { // note PhabricatorRepositoryAuditRequests and // PhabricatorRepositoryCommitData are deleted here too. $commit->delete(); } $uris = id(new PhabricatorRepositoryURI()) ->loadAllWhere('repositoryPHID = %s', $this->getPHID()); foreach ($uris as $uri) { $uri->delete(); } $ref_cursors = id(new PhabricatorRepositoryRefCursor()) ->loadAllWhere('repositoryPHID = %s', $this->getPHID()); foreach ($ref_cursors as $cursor) { $cursor->delete(); } $conn_w = $this->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE repositoryID = %d', self::TABLE_FILESYSTEM, $this->getID()); queryfx( $conn_w, 'DELETE FROM %T WHERE repositoryID = %d', self::TABLE_PATHCHANGE, $this->getID()); queryfx( $conn_w, 'DELETE FROM %T WHERE repositoryID = %d', self::TABLE_SUMMARY, $this->getID()); $result = parent::delete(); $this->saveTransaction(); return $result; } public function isGit() { $vcs = $this->getVersionControlSystem(); return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT); } public function isSVN() { $vcs = $this->getVersionControlSystem(); return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_SVN); } public function isHg() { $vcs = $this->getVersionControlSystem(); return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL); } public function isHosted() { return (bool)$this->getDetail('hosting-enabled', false); } public function setHosted($enabled) { return $this->setDetail('hosting-enabled', $enabled); } public function canServeProtocol($protocol, $write) { if (!$this->isTracked()) { return false; } $clone_uris = $this->getCloneURIs(); foreach ($clone_uris as $uri) { if ($uri->getBuiltinProtocol() !== $protocol) { continue; } $io_type = $uri->getEffectiveIoType(); if ($io_type == PhabricatorRepositoryURI::IO_READWRITE) { return true; } if (!$write) { if ($io_type == PhabricatorRepositoryURI::IO_READ) { return true; } } } return false; } public function hasLocalWorkingCopy() { try { self::assertLocalExists(); return true; } catch (Exception $ex) { return false; } } /** * Raise more useful errors when there are basic filesystem problems. */ private function assertLocalExists() { if (!$this->usesLocalWorkingCopy()) { return; } $local = $this->getLocalPath(); Filesystem::assertExists($local); Filesystem::assertIsDirectory($local); Filesystem::assertReadable($local); } /** * Determine if the working copy is bare or not. In Git, this corresponds * to `--bare`. In Mercurial, `--noupdate`. */ public function isWorkingCopyBare() { switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: return false; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $local = $this->getLocalPath(); if (Filesystem::pathExists($local.'/.git')) { return false; } else { return true; } } } public function usesLocalWorkingCopy() { switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: return $this->isHosted(); case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: return true; } } public function getHookDirectories() { $directories = array(); if (!$this->isHosted()) { return $directories; } $root = $this->getLocalPath(); switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: if ($this->isWorkingCopyBare()) { $directories[] = $root.'/hooks/pre-receive-phabricator.d/'; } else { $directories[] = $root.'/.git/hooks/pre-receive-phabricator.d/'; } break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $directories[] = $root.'/hooks/pre-commit-phabricator.d/'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: // NOTE: We don't support custom Mercurial hooks for now because they're // messy and we can't easily just drop a `hooks.d/` directory next to // the hooks. break; } return $directories; } public function canDestroyWorkingCopy() { if ($this->isHosted()) { // Never destroy hosted working copies. return false; } $default_path = PhabricatorEnv::getEnvConfig( 'repository.default-local-path'); return Filesystem::isDescendant($this->getLocalPath(), $default_path); } public function canUsePathTree() { return !$this->isSVN(); } public function canUseGitLFS() { if (!$this->isGit()) { return false; } if (!$this->isHosted()) { return false; } if (!PhabricatorEnv::getEnvConfig('diffusion.allow-git-lfs')) { return false; } return true; } public function getGitLFSURI($path = null) { if (!$this->canUseGitLFS()) { throw new Exception( pht( 'This repository does not support Git LFS, so Git LFS URIs can '. 'not be generated for it.')); } $uri = $this->getRawHTTPCloneURIObject(); $uri = (string)$uri; $uri = $uri.'/'.$path; return $uri; } public function canMirror() { if ($this->isGit() || $this->isHg()) { return true; } return false; } public function canAllowDangerousChanges() { if (!$this->isHosted()) { return false; } // In Git and Mercurial, ref deletions and rewrites are dangerous. // In Subversion, editing revprops is dangerous. return true; } public function shouldAllowDangerousChanges() { return (bool)$this->getDetail('allow-dangerous-changes'); } public function canAllowEnormousChanges() { if (!$this->isHosted()) { return false; } return true; } public function shouldAllowEnormousChanges() { return (bool)$this->getDetail('allow-enormous-changes'); } public function writeStatusMessage( $status_type, $status_code, array $parameters = array()) { $table = new PhabricatorRepositoryStatusMessage(); $conn_w = $table->establishConnection('w'); $table_name = $table->getTableName(); if ($status_code === null) { queryfx( $conn_w, 'DELETE FROM %T WHERE repositoryID = %d AND statusType = %s', $table_name, $this->getID(), $status_type); } else { // If the existing message has the same code (e.g., we just hit an // error and also previously hit an error) we increment the message // count. This allows us to determine how many times in a row we've // run into an error. // NOTE: The assignments in "ON DUPLICATE KEY UPDATE" are evaluated // in order, so the "messageCount" assignment must occur before the // "statusCode" assignment. See T11705. queryfx( $conn_w, 'INSERT INTO %T (repositoryID, statusType, statusCode, parameters, epoch, messageCount) VALUES (%d, %s, %s, %s, %d, %d) ON DUPLICATE KEY UPDATE messageCount = IF( statusCode = VALUES(statusCode), messageCount + VALUES(messageCount), VALUES(messageCount)), statusCode = VALUES(statusCode), parameters = VALUES(parameters), epoch = VALUES(epoch)', $table_name, $this->getID(), $status_type, $status_code, json_encode($parameters), time(), 1); } return $this; } public static function assertValidRemoteURI($uri) { if (trim($uri) != $uri) { throw new Exception( pht('The remote URI has leading or trailing whitespace.')); } $uri_object = new PhutilURI($uri); $protocol = $uri_object->getProtocol(); // Catch confusion between Git/SCP-style URIs and normal URIs. See T3619 // for discussion. This is usually a user adding "ssh://" to an implicit // SSH Git URI. if ($protocol == 'ssh') { if (preg_match('(^[^:@]+://[^/:]+:[^\d])', $uri)) { throw new Exception( pht( "The remote URI is not formatted correctly. Remote URIs ". "with an explicit protocol should be in the form ". "'%s', not '%s'. The '%s' syntax is only valid in SCP-style URIs.", 'proto://domain/path', 'proto://domain:/path', ':/path')); } } switch ($protocol) { case 'ssh': case 'http': case 'https': case 'git': case 'svn': case 'svn+ssh': break; default: // NOTE: We're explicitly rejecting 'file://' because it can be // used to clone from the working copy of another repository on disk // that you don't normally have permission to access. throw new Exception( pht( 'The URI protocol is unrecognized. It should begin with '. '"%s", "%s", "%s", "%s", "%s", "%s", or be in the form "%s".', 'ssh://', 'http://', 'https://', 'git://', 'svn://', 'svn+ssh://', 'git@domain.com:path')); } return true; } /** * Load the pull frequency for this repository, based on the time since the * last activity. * * We pull rarely used repositories less frequently. This finds the most * recent commit which is older than the current time (which prevents us from * spinning on repositories with a silly commit post-dated to some time in * 2037). We adjust the pull frequency based on when the most recent commit * occurred. * * @param int The minimum update interval to use, in seconds. * @return int Repository update interval, in seconds. */ public function loadUpdateInterval($minimum = 15) { // First, check if we've hit errors recently. If we have, wait one period // for each consecutive error. Normally, this corresponds to a backoff of // 15s, 30s, 45s, etc. $message_table = new PhabricatorRepositoryStatusMessage(); $conn = $message_table->establishConnection('r'); $error_count = queryfx_one( $conn, 'SELECT MAX(messageCount) error_count FROM %T WHERE repositoryID = %d AND statusType IN (%Ls) AND statusCode IN (%Ls)', $message_table->getTableName(), $this->getID(), array( PhabricatorRepositoryStatusMessage::TYPE_INIT, PhabricatorRepositoryStatusMessage::TYPE_FETCH, ), array( PhabricatorRepositoryStatusMessage::CODE_ERROR, )); $error_count = (int)$error_count['error_count']; if ($error_count > 0) { return (int)($minimum * $error_count); } // If a repository is still importing, always pull it as frequently as // possible. This prevents us from hanging for a long time at 99.9% when // importing an inactive repository. if ($this->isImporting()) { return $minimum; } $window_start = (PhabricatorTime::getNow() + $minimum); $table = id(new PhabricatorRepositoryCommit()); $last_commit = queryfx_one( $table->establishConnection('r'), 'SELECT epoch FROM %T WHERE repositoryID = %d AND epoch <= %d ORDER BY epoch DESC LIMIT 1', $table->getTableName(), $this->getID(), $window_start); if ($last_commit) { $time_since_commit = ($window_start - $last_commit['epoch']); } else { // If the repository has no commits, treat the creation date as // though it were the date of the last commit. This makes empty // repositories update quickly at first but slow down over time // if they don't see any activity. $time_since_commit = ($window_start - $this->getDateCreated()); } $last_few_days = phutil_units('3 days in seconds'); if ($time_since_commit <= $last_few_days) { // For repositories with activity in the recent past, we wait one // extra second for every 10 minutes since the last commit. This // shorter backoff is intended to handle weekends and other short // breaks from development. $smart_wait = ($time_since_commit / 600); } else { // For repositories without recent activity, we wait one extra second // for every 4 minutes since the last commit. This longer backoff // handles rarely used repositories, up to the maximum. $smart_wait = ($time_since_commit / 240); } // We'll never wait more than 6 hours to pull a repository. $longest_wait = phutil_units('6 hours in seconds'); $smart_wait = min($smart_wait, $longest_wait); $smart_wait = max($minimum, $smart_wait); return (int)$smart_wait; } + /** + * Time limit for cloning or copying this repository. + * + * This limit is used to timeout operations like `git clone` or `git fetch` + * when doing intracluster synchronization, building working copies, etc. + * + * @return int Maximum number of seconds to spend copying this repository. + */ + public function getCopyTimeLimit() { + return $this->getDetail('limit.copy'); + } + + public function setCopyTimeLimit($limit) { + return $this->setDetail('limit.copy', $limit); + } + + public function getDefaultCopyTimeLimit() { + return phutil_units('15 minutes in seconds'); + } + + public function getEffectiveCopyTimeLimit() { + $limit = $this->getCopyTimeLimit(); + if ($limit) { + return $limit; + } + + return $this->getDefaultCopyTimeLimit(); + } + + public function getFilesizeLimit() { + return $this->getDetail('limit.filesize'); + } + + public function setFilesizeLimit($limit) { + return $this->setDetail('limit.filesize', $limit); + } + + public function getTouchLimit() { + return $this->getDetail('limit.touch'); + } + + public function setTouchLimit($limit) { + return $this->setDetail('limit.touch', $limit); + } + /** * Retrieve the service URI for the device hosting this repository. * * See @{method:newConduitClient} for a general discussion of interacting * with repository services. This method provides lower-level resolution of * services, returning raw URIs. * * @param PhabricatorUser Viewing user. * @param map Constraints on selectable services. * @return string|null URI, or `null` for local repositories. */ public function getAlmanacServiceURI( PhabricatorUser $viewer, array $options) { PhutilTypeSpec::checkMap( $options, array( 'neverProxy' => 'bool', 'protocols' => 'list', 'writable' => 'optional bool', )); $never_proxy = $options['neverProxy']; $protocols = $options['protocols']; $writable = idx($options, 'writable', false); $cache_key = $this->getAlmanacServiceCacheKey(); if (!$cache_key) { return null; } $cache = PhabricatorCaches::getMutableStructureCache(); $uris = $cache->getKey($cache_key, false); // If we haven't built the cache yet, build it now. if ($uris === false) { $uris = $this->buildAlmanacServiceURIs(); $cache->setKey($cache_key, $uris); } if ($uris === null) { return null; } $local_device = AlmanacKeys::getDeviceID(); if ($never_proxy && !$local_device) { throw new Exception( pht( 'Unable to handle proxied service request. This device is not '. 'registered, so it can not identify local services. Register '. 'this device before sending requests here.')); } $protocol_map = array_fuse($protocols); $results = array(); foreach ($uris as $uri) { // If we're never proxying this and it's locally satisfiable, return // `null` to tell the caller to handle it locally. If we're allowed to // proxy, we skip this check and may proxy the request to ourselves. // (That proxied request will end up here with proxying forbidden, // return `null`, and then the request will actually run.) if ($local_device && $never_proxy) { if ($uri['device'] == $local_device) { return null; } } if (isset($protocol_map[$uri['protocol']])) { $results[] = $uri; } } if (!$results) { throw new Exception( pht( 'The Almanac service for this repository is not bound to any '. 'interfaces which support the required protocols (%s).', implode(', ', $protocols))); } if ($never_proxy) { throw new Exception( pht( 'Refusing to proxy a repository request from a cluster host. '. 'Cluster hosts must correctly route their intracluster requests.')); } if (count($results) > 1) { if (!$this->supportsSynchronization()) { throw new Exception( pht( 'Repository "%s" is bound to multiple active repository hosts, '. 'but this repository does not support cluster synchronization. '. 'Declusterize this repository or move it to a service with only '. 'one host.', $this->getDisplayName())); } } // If we require a writable device, remove URIs which aren't writable. if ($writable) { foreach ($results as $key => $uri) { if (!$uri['writable']) { unset($results[$key]); } } if (!$results) { throw new Exception( pht( 'This repository ("%s") is not writable with the given '. 'protocols (%s). The Almanac service for this repository has no '. 'writable bindings that support these protocols.', $this->getDisplayName(), implode(', ', $protocols))); } } if ($writable) { $results = $this->sortWritableAlmanacServiceURIs($results); } else { shuffle($results); } $result = head($results); return $result['uri']; } private function sortWritableAlmanacServiceURIs(array $results) { // See T13109 for discussion of how this method routes requests. // In the absence of other rules, we'll send traffic to devices randomly. // We also want to select randomly among nodes which are equally good // candidates to receive the write, and accomplish that by shuffling the // list up front. shuffle($results); $order = array(); // If some device is currently holding the write lock, send all requests // to that device. We're trying to queue writes on a single device so they // do not need to wait for read synchronization after earlier writes // complete. $writer = PhabricatorRepositoryWorkingCopyVersion::loadWriter( $this->getPHID()); if ($writer) { $device_phid = $writer->getWriteProperty('devicePHID'); foreach ($results as $key => $result) { if ($result['devicePHID'] === $device_phid) { $order[] = $key; } } } // If no device is currently holding the write lock, try to send requests // to a device which is already up to date and will not need to synchronize // before it can accept the write. $versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions( $this->getPHID()); if ($versions) { $max_version = (int)max(mpull($versions, 'getRepositoryVersion')); $max_devices = array(); foreach ($versions as $version) { if ($version->getRepositoryVersion() == $max_version) { $max_devices[] = $version->getDevicePHID(); } } $max_devices = array_fuse($max_devices); foreach ($results as $key => $result) { if (isset($max_devices[$result['devicePHID']])) { $order[] = $key; } } } // Reorder the results, putting any we've selected as preferred targets for // the write at the head of the list. $results = array_select_keys($results, $order) + $results; return $results; } public function supportsSynchronization() { // TODO: For now, this is only supported for Git. if (!$this->isGit()) { return false; } return true; } public function getAlmanacServiceCacheKey() { $service_phid = $this->getAlmanacServicePHID(); if (!$service_phid) { return null; } $repository_phid = $this->getPHID(); $parts = array( "repo({$repository_phid})", "serv({$service_phid})", 'v3', ); return implode('.', $parts); } private function buildAlmanacServiceURIs() { $service = $this->loadAlmanacService(); if (!$service) { return null; } $bindings = $service->getActiveBindings(); if (!$bindings) { throw new Exception( pht( 'The Almanac service for this repository is not bound to any '. 'interfaces.')); } $uris = array(); foreach ($bindings as $binding) { $iface = $binding->getInterface(); $uri = $this->getClusterRepositoryURIFromBinding($binding); $protocol = $uri->getProtocol(); $device_name = $iface->getDevice()->getName(); $device_phid = $iface->getDevice()->getPHID(); $uris[] = array( 'protocol' => $protocol, 'uri' => (string)$uri, 'device' => $device_name, 'writable' => (bool)$binding->getAlmanacPropertyValue('writable'), 'devicePHID' => $device_phid, ); } return $uris; } /** * Build a new Conduit client in order to make a service call to this * repository. * * If the repository is hosted locally, this method may return `null`. The * caller should use `ConduitCall` or other local logic to complete the * request. * * By default, we will return a @{class:ConduitClient} for any repository with * a service, even if that service is on the current device. * * We do this because this configuration does not make very much sense in a * production context, but is very common in a test/development context * (where the developer's machine is both the web host and the repository * service). By proxying in development, we get more consistent behavior * between development and production, and don't have a major untested * codepath. * * The `$never_proxy` parameter can be used to prevent this local proxying. * If the flag is passed: * * - The method will return `null` (implying a local service call) * if the repository service is hosted on the current device. * - The method will throw if it would need to return a client. * * This is used to prevent loops in Conduit: the first request will proxy, * even in development, but the second request will be identified as a * cluster request and forced not to proxy. * * For lower-level service resolution, see @{method:getAlmanacServiceURI}. * * @param PhabricatorUser Viewing user. * @param bool `true` to throw if a client would be returned. * @return ConduitClient|null Client, or `null` for local repositories. */ public function newConduitClient( PhabricatorUser $viewer, $never_proxy = false) { $uri = $this->getAlmanacServiceURI( $viewer, array( 'neverProxy' => $never_proxy, 'protocols' => array( 'http', 'https', ), // At least today, no Conduit call can ever write to a repository, // so it's fine to send anything to a read-only node. 'writable' => false, )); if ($uri === null) { return null; } $domain = id(new PhutilURI(PhabricatorEnv::getURI('/')))->getDomain(); $client = id(new ConduitClient($uri)) ->setHost($domain); if ($viewer->isOmnipotent()) { // If the caller is the omnipotent user (normally, a daemon), we will // sign the request with this host's asymmetric keypair. $public_path = AlmanacKeys::getKeyPath('device.pub'); try { $public_key = Filesystem::readFile($public_path); } catch (Exception $ex) { throw new PhutilAggregateException( pht( 'Unable to read device public key while attempting to make '. 'authenticated method call within the Phabricator cluster. '. 'Use `%s` to register keys for this device. Exception: %s', 'bin/almanac register', $ex->getMessage()), array($ex)); } $private_path = AlmanacKeys::getKeyPath('device.key'); try { $private_key = Filesystem::readFile($private_path); $private_key = new PhutilOpaqueEnvelope($private_key); } catch (Exception $ex) { throw new PhutilAggregateException( pht( 'Unable to read device private key while attempting to make '. 'authenticated method call within the Phabricator cluster. '. 'Use `%s` to register keys for this device. Exception: %s', 'bin/almanac register', $ex->getMessage()), array($ex)); } $client->setSigningKeys($public_key, $private_key); } else { // If the caller is a normal user, we generate or retrieve a cluster // API token. $token = PhabricatorConduitToken::loadClusterTokenForUser($viewer); if ($token) { $client->setConduitToken($token->getToken()); } } return $client; } public function getPassthroughEnvironmentalVariables() { $env = $_ENV; if ($this->isGit()) { // $_ENV does not populate in CLI contexts if "E" is missing from // "variables_order" in PHP config. Currently, we do not require this // to be configured. Since it may not be, explicitly bring expected Git // environmental variables into scope. This list is not exhaustive, but // only lists variables with a known impact on commit hook behavior. // This can be removed if we later require "E" in "variables_order". $git_env = array( 'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_QUARANTINE_PATH', ); foreach ($git_env as $key) { $value = getenv($key); if (strlen($value)) { $env[$key] = $value; } } $key = 'GIT_PUSH_OPTION_COUNT'; $git_count = getenv($key); if (strlen($git_count)) { $git_count = (int)$git_count; $env[$key] = $git_count; for ($ii = 0; $ii < $git_count; $ii++) { $key = 'GIT_PUSH_OPTION_'.$ii; $env[$key] = getenv($key); } } } $result = array(); foreach ($env as $key => $value) { // In Git, pass anything matching "GIT_*" though. Some of these variables // need to be preserved to allow `git` operations to work properly when // running from commit hooks. if ($this->isGit()) { if (preg_match('/^GIT_/', $key)) { $result[$key] = $value; } } } return $result; } public function supportsBranchComparison() { return $this->isGit(); } /* -( Repository URIs )---------------------------------------------------- */ public function attachURIs(array $uris) { $custom_map = array(); foreach ($uris as $key => $uri) { $builtin_key = $uri->getRepositoryURIBuiltinKey(); if ($builtin_key !== null) { $custom_map[$builtin_key] = $key; } } $builtin_uris = $this->newBuiltinURIs(); $seen_builtins = array(); foreach ($builtin_uris as $builtin_uri) { $builtin_key = $builtin_uri->getRepositoryURIBuiltinKey(); $seen_builtins[$builtin_key] = true; // If this builtin URI is disabled, don't attach it and remove the // persisted version if it exists. if ($builtin_uri->getIsDisabled()) { if (isset($custom_map[$builtin_key])) { unset($uris[$custom_map[$builtin_key]]); } continue; } // If the URI exists, make sure it's marked as not being disabled. if (isset($custom_map[$builtin_key])) { $uris[$custom_map[$builtin_key]]->setIsDisabled(false); } } // Remove any builtins which no longer exist. foreach ($custom_map as $builtin_key => $key) { if (empty($seen_builtins[$builtin_key])) { unset($uris[$key]); } } $this->uris = $uris; return $this; } public function getURIs() { return $this->assertAttached($this->uris); } public function getCloneURIs() { $uris = $this->getURIs(); $clone = array(); foreach ($uris as $uri) { if (!$uri->isBuiltin()) { continue; } if ($uri->getIsDisabled()) { continue; } $io_type = $uri->getEffectiveIoType(); $is_clone = ($io_type == PhabricatorRepositoryURI::IO_READ) || ($io_type == PhabricatorRepositoryURI::IO_READWRITE); if (!$is_clone) { continue; } $clone[] = $uri; } $clone = msort($clone, 'getURIScore'); $clone = array_reverse($clone); return $clone; } public function newBuiltinURIs() { $has_callsign = ($this->getCallsign() !== null); $has_shortname = ($this->getRepositorySlug() !== null); $identifier_map = array( PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_CALLSIGN => $has_callsign, PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_SHORTNAME => $has_shortname, PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_ID => true, ); // If the view policy of the repository is public, support anonymous HTTP // even if authenticated HTTP is not supported. if ($this->getViewPolicy() === PhabricatorPolicies::POLICY_PUBLIC) { $allow_http = true; } else { $allow_http = PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth'); } $base_uri = PhabricatorEnv::getURI('/'); $base_uri = new PhutilURI($base_uri); $has_https = ($base_uri->getProtocol() == 'https'); $has_https = ($has_https && $allow_http); $has_http = !PhabricatorEnv::getEnvConfig('security.require-https'); $has_http = ($has_http && $allow_http); // HTTP is not supported for Subversion. if ($this->isSVN()) { $has_http = false; $has_https = false; } $has_ssh = (bool)strlen(PhabricatorEnv::getEnvConfig('phd.user')); $protocol_map = array( PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH => $has_ssh, PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTPS => $has_https, PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTP => $has_http, ); $uris = array(); foreach ($protocol_map as $protocol => $proto_supported) { foreach ($identifier_map as $identifier => $id_supported) { // This is just a dummy value because it can't be empty; we'll force // it to a proper value when using it in the UI. $builtin_uri = "{$protocol}://{$identifier}"; $uris[] = PhabricatorRepositoryURI::initializeNewURI() ->setRepositoryPHID($this->getPHID()) ->attachRepository($this) ->setBuiltinProtocol($protocol) ->setBuiltinIdentifier($identifier) ->setURI($builtin_uri) ->setIsDisabled((int)(!$proto_supported || !$id_supported)); } } return $uris; } public function getClusterRepositoryURIFromBinding( AlmanacBinding $binding) { $protocol = $binding->getAlmanacPropertyValue('protocol'); if ($protocol === null) { $protocol = 'https'; } $iface = $binding->getInterface(); $address = $iface->renderDisplayAddress(); $path = $this->getURI(); return id(new PhutilURI("{$protocol}://{$address}")) ->setPath($path); } public function loadAlmanacService() { $service_phid = $this->getAlmanacServicePHID(); if (!$service_phid) { // No service, so this is a local repository. return null; } $service = id(new AlmanacServiceQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($service_phid)) ->needBindings(true) ->needProperties(true) ->executeOne(); if (!$service) { throw new Exception( pht( 'The Almanac service for this repository is invalid or could not '. 'be loaded.')); } $service_type = $service->getServiceImplementation(); if (!($service_type instanceof AlmanacClusterRepositoryServiceType)) { throw new Exception( pht( 'The Almanac service for this repository does not have the correct '. 'service type.')); } return $service; } public function markImporting() { $this->openTransaction(); $this->beginReadLocking(); $repository = $this->reload(); $repository->setDetail('importing', true); $repository->save(); $this->endReadLocking(); $this->saveTransaction(); return $repository; } /* -( Symbols )-------------------------------------------------------------*/ public function getSymbolSources() { return $this->getDetail('symbol-sources', array()); } public function getSymbolLanguages() { return $this->getDetail('symbol-languages', array()); } /* -( Staging )------------------------------------------------------------ */ public function supportsStaging() { return $this->isGit(); } public function getStagingURI() { if (!$this->supportsStaging()) { return null; } return $this->getDetail('staging-uri', null); } /* -( Automation )--------------------------------------------------------- */ public function supportsAutomation() { return $this->isGit(); } public function canPerformAutomation() { if (!$this->supportsAutomation()) { return false; } if (!$this->getAutomationBlueprintPHIDs()) { return false; } return true; } public function getAutomationBlueprintPHIDs() { if (!$this->supportsAutomation()) { return array(); } return $this->getDetail('automation.blueprintPHIDs', array()); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorRepositoryEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorRepositoryTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, DiffusionPushCapability::CAPABILITY, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); case DiffusionPushCapability::CAPABILITY: return $this->getPushPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { return false; } /* -( PhabricatorMarkupInterface )----------------------------------------- */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digestForIndex($this->getMarkupText($field)); return "repo:{$hash}"; } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newMarkupEngine(array()); } public function getMarkupText($field) { return $this->getDetail('description'); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { require_celerity_resource('phabricator-remarkup-css'); return phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $output); } public function shouldUseMarkupCache($field) { return true; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $phid = $this->getPHID(); $this->openTransaction(); $this->delete(); PhabricatorRepositoryURIIndex::updateRepositoryURIs($phid, array()); $books = id(new DivinerBookQuery()) ->setViewer($engine->getViewer()) ->withRepositoryPHIDs(array($phid)) ->execute(); foreach ($books as $book) { $engine->destroyObject($book); } $atoms = id(new DivinerAtomQuery()) ->setViewer($engine->getViewer()) ->withRepositoryPHIDs(array($phid)) ->execute(); foreach ($atoms as $atom) { $engine->destroyObject($atom); } $lfs_refs = id(new PhabricatorRepositoryGitLFSRefQuery()) ->setViewer($engine->getViewer()) ->withRepositoryPHIDs(array($phid)) ->execute(); foreach ($lfs_refs as $ref) { $engine->destroyObject($ref); } $this->saveTransaction(); } /* -( PhabricatorDestructibleCodexInterface )------------------------------ */ public function newDestructibleCodex() { return new PhabricatorRepositoryDestructibleCodex(); } /* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The repository name.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('vcs') ->setType('string') ->setDescription( pht('The VCS this repository uses ("git", "hg" or "svn").')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('callsign') ->setType('string') ->setDescription(pht('The repository callsign, if it has one.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('shortName') ->setType('string') ->setDescription(pht('Unique short name, if the repository has one.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('status') ->setType('string') ->setDescription(pht('Active or inactive status.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('isImporting') ->setType('bool') ->setDescription( pht( 'True if the repository is importing initial commits.')), ); } public function getFieldValuesForConduit() { return array( 'name' => $this->getName(), 'vcs' => $this->getVersionControlSystem(), 'callsign' => $this->getCallsign(), 'shortName' => $this->getRepositorySlug(), 'status' => $this->getStatus(), 'isImporting' => (bool)$this->isImporting(), ); } public function getConduitSearchAttachments() { return array( id(new DiffusionRepositoryURIsSearchEngineAttachment()) ->setAttachmentKey('uris'), ); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PhabricatorRepositoryFulltextEngine(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new PhabricatorRepositoryFerretEngine(); } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryPushLog.php b/src/applications/repository/storage/PhabricatorRepositoryPushLog.php index c2d3456da6..cfb5402c2a 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryPushLog.php +++ b/src/applications/repository/storage/PhabricatorRepositoryPushLog.php @@ -1,231 +1,239 @@ setPusherPHID($viewer->getPHID()); } public static function getFlagDisplayNames() { return array( self::CHANGEFLAG_ADD => pht('Create'), self::CHANGEFLAG_DELETE => pht('Delete'), self::CHANGEFLAG_APPEND => pht('Append'), self::CHANGEFLAG_REWRITE => pht('Rewrite'), self::CHANGEFLAG_DANGEROUS => pht('Dangerous'), self::CHANGEFLAG_ENORMOUS => pht('Enormous'), + self::CHANGEFLAG_OVERSIZED => pht('Oversized'), + self::CHANGEFLAG_TOUCHES => pht('Touches Too Many Paths'), ); } public static function getRejectCodeDisplayNames() { return array( self::REJECT_ACCEPT => pht('Accepted'), self::REJECT_DANGEROUS => pht('Rejected: Dangerous'), self::REJECT_HERALD => pht('Rejected: Herald'), self::REJECT_EXTERNAL => pht('Rejected: External Hook'), self::REJECT_BROKEN => pht('Rejected: Broken'), self::REJECT_ENORMOUS => pht('Rejected: Enormous'), + self::REJECT_OVERSIZED => pht('Rejected: Oversized File'), + self::REJECT_TOUCHES => pht('Rejected: Touches Too Many Paths'), ); } public static function getHeraldChangeFlagConditionOptions() { return array( self::CHANGEFLAG_ADD => pht('change creates ref'), self::CHANGEFLAG_DELETE => pht('change deletes ref'), self::CHANGEFLAG_REWRITE => pht('change rewrites ref'), self::CHANGEFLAG_DANGEROUS => pht('dangerous change'), ); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_TIMESTAMPS => false, self::CONFIG_BINARY => array( 'refNameRaw' => true, ), self::CONFIG_COLUMN_SCHEMA => array( 'refType' => 'text12', 'refNameHash' => 'bytes12?', 'refNameRaw' => 'bytes?', 'refNameEncoding' => 'text16?', 'refOld' => 'text40?', 'refNew' => 'text40', 'mergeBase' => 'text40?', 'changeFlags' => 'uint32', 'devicePHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_repository' => array( 'columns' => array('repositoryPHID'), ), 'key_ref' => array( 'columns' => array('repositoryPHID', 'refNew'), ), 'key_name' => array( 'columns' => array('repositoryPHID', 'refNameHash'), ), 'key_event' => array( 'columns' => array('pushEventPHID'), ), 'key_pusher' => array( 'columns' => array('pusherPHID'), ), 'key_epoch' => array( 'columns' => array('epoch'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorRepositoryPushLogPHIDType::TYPECONST); } public function attachPushEvent(PhabricatorRepositoryPushEvent $push_event) { $this->pushEvent = $push_event; return $this; } public function getPushEvent() { return $this->assertAttached($this->pushEvent); } public function getRefName() { return $this->getUTF8StringFromStorage( $this->getRefNameRaw(), $this->getRefNameEncoding()); } public function setRefName($ref_raw) { $this->setRefNameRaw($ref_raw); $this->setRefNameHash(PhabricatorHash::digestForIndex($ref_raw)); $this->setRefNameEncoding($this->detectEncodingForStorage($ref_raw)); return $this; } public function getRefOldShort() { if ($this->getRepository()->isSVN()) { return $this->getRefOld(); } return substr($this->getRefOld(), 0, 12); } public function getRefNewShort() { if ($this->getRepository()->isSVN()) { return $this->getRefNew(); } return substr($this->getRefNew(), 0, 12); } public function hasChangeFlags($mask) { return ($this->changeFlags & $mask); } public function attachDangerousChangeDescription($description) { $this->dangerousChangeDescription = $description; return $this; } public function getDangerousChangeDescription() { return $this->assertAttached($this->dangerousChangeDescription); } public function attachRepository(PhabricatorRepository $repository) { // NOTE: Some gymnastics around this because of object construction order // in the hook engine. Particularly, web build the logs before we build // their push event. $this->repository = $repository; return $this; } public function getRepository() { if ($this->repository == self::ATTACHABLE) { return $this->getPushEvent()->getRepository(); } return $this->assertAttached($this->repository); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { // NOTE: We're passing through the repository rather than the push event // mostly because we need to do policy checks in Herald before we create // the event. The two approaches are equivalent in practice. return $this->getRepository()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getRepository()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht( "A repository's push logs are visible to users who can see the ". "repository."); } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryTransaction.php b/src/applications/repository/storage/PhabricatorRepositoryTransaction.php index 866800161e..85c354ba67 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryTransaction.php +++ b/src/applications/repository/storage/PhabricatorRepositoryTransaction.php @@ -1,527 +1,22 @@ getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_PUSH_POLICY: - case self::TYPE_SERVICE: - if ($old) { - $phids[] = $old; - } - if ($new) { - $phids[] = $new; - } - break; - case self::TYPE_SYMBOLS_SOURCES: - case self::TYPE_AUTOMATION_BLUEPRINTS: - if ($old) { - $phids = array_merge($phids, $old); - } - if ($new) { - $phids = array_merge($phids, $new); - } - break; - } - - return $phids; - } - - public function shouldHide() { - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_REMOTE_URI: - case self::TYPE_SSH_LOGIN: - case self::TYPE_SSH_KEY: - case self::TYPE_SSH_KEYFILE: - case self::TYPE_HTTP_LOGIN: - case self::TYPE_HTTP_PASS: - // Hide null vs empty string changes. - return (!strlen($old) && !strlen($new)); - case self::TYPE_LOCAL_PATH: - case self::TYPE_NAME: - // Hide these on create, they aren't interesting and we have an - // explicit "create" transaction. - if (!strlen($old)) { - return true; - } - break; - } - - return parent::shouldHide(); - } - - public function getIcon() { - switch ($this->getTransactionType()) { - case self::TYPE_VCS: - return 'fa-plus'; - } - return parent::getIcon(); - } - - public function getColor() { - switch ($this->getTransactionType()) { - case self::TYPE_VCS: - return 'green'; - } - return parent::getIcon(); - } - - public function getTitle() { - $author_phid = $this->getAuthorPHID(); - - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_VCS: - return pht( - '%s created this repository.', - $this->renderHandleLink($author_phid)); - case self::TYPE_ACTIVATE: - // TODO: Old versions of this transaction use a boolean value, but - // should be migrated. - $is_deactivate = - (!$new) || - ($new == PhabricatorRepository::STATUS_INACTIVE); - - if (!$is_deactivate) { - return pht( - '%s activated this repository.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s deactivated this repository.', - $this->renderHandleLink($author_phid)); - } - case self::TYPE_NAME: - return pht( - '%s renamed this repository from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - case self::TYPE_DESCRIPTION: - return pht( - '%s updated the description of this repository.', - $this->renderHandleLink($author_phid)); - case self::TYPE_ENCODING: - if (strlen($old) && !strlen($new)) { - return pht( - '%s removed the "%s" encoding configured for this repository.', - $this->renderHandleLink($author_phid), - $old); - } else if (strlen($new) && !strlen($old)) { - return pht( - '%s set the encoding for this repository to "%s".', - $this->renderHandleLink($author_phid), - $new); - } else { - return pht( - '%s changed the repository encoding from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - } - case self::TYPE_DEFAULT_BRANCH: - if (!strlen($new)) { - return pht( - '%s removed "%s" as the default branch.', - $this->renderHandleLink($author_phid), - $old); - } else if (!strlen($old)) { - return pht( - '%s set the default branch to "%s".', - $this->renderHandleLink($author_phid), - $new); - } else { - return pht( - '%s changed the default branch from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - } - break; - case self::TYPE_TRACK_ONLY: - if (!$new) { - return pht( - '%s set this repository to track all branches.', - $this->renderHandleLink($author_phid)); - } else if (!$old) { - return pht( - '%s set this repository to track branches: %s.', - $this->renderHandleLink($author_phid), - implode(', ', $new)); - } else { - return pht( - '%s changed track branches from "%s" to "%s".', - $this->renderHandleLink($author_phid), - implode(', ', $old), - implode(', ', $new)); - } - break; - case self::TYPE_AUTOCLOSE_ONLY: - if (!$new) { - return pht( - '%s set this repository to autoclose on all branches.', - $this->renderHandleLink($author_phid)); - } else if (!$old) { - return pht( - '%s set this repository to autoclose on branches: %s.', - $this->renderHandleLink($author_phid), - implode(', ', $new)); - } else { - return pht( - '%s changed autoclose branches from "%s" to "%s".', - $this->renderHandleLink($author_phid), - implode(', ', $old), - implode(', ', $new)); - } - break; - case self::TYPE_UUID: - if (!strlen($new)) { - return pht( - '%s removed "%s" as the repository UUID.', - $this->renderHandleLink($author_phid), - $old); - } else if (!strlen($old)) { - return pht( - '%s set the repository UUID to "%s".', - $this->renderHandleLink($author_phid), - $new); - } else { - return pht( - '%s changed the repository UUID from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - } - break; - case self::TYPE_SVN_SUBPATH: - if (!strlen($new)) { - return pht( - '%s removed "%s" as the Import Only path.', - $this->renderHandleLink($author_phid), - $old); - } else if (!strlen($old)) { - return pht( - '%s set the repository to import only "%s".', - $this->renderHandleLink($author_phid), - $new); - } else { - return pht( - '%s changed the import path from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - } - break; - case self::TYPE_NOTIFY: - if ($new) { - return pht( - '%s enabled notifications and publishing for this repository.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s disabled notifications and publishing for this repository.', - $this->renderHandleLink($author_phid)); - } - break; - case self::TYPE_AUTOCLOSE: - if ($new) { - return pht( - '%s enabled autoclose for this repository.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s disabled autoclose for this repository.', - $this->renderHandleLink($author_phid)); - } - break; - case self::TYPE_REMOTE_URI: - if (!strlen($old)) { - return pht( - '%s set the remote URI for this repository to "%s".', - $this->renderHandleLink($author_phid), - $new); - } else if (!strlen($new)) { - return pht( - '%s removed the remote URI for this repository.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s changed the remote URI for this repository from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - } - break; - case self::TYPE_SSH_LOGIN: - return pht( - '%s updated the SSH login for this repository.', - $this->renderHandleLink($author_phid)); - case self::TYPE_SSH_KEY: - return pht( - '%s updated the SSH key for this repository.', - $this->renderHandleLink($author_phid)); - case self::TYPE_SSH_KEYFILE: - return pht( - '%s updated the SSH keyfile for this repository.', - $this->renderHandleLink($author_phid)); - case self::TYPE_HTTP_LOGIN: - return pht( - '%s updated the HTTP login for this repository.', - $this->renderHandleLink($author_phid)); - case self::TYPE_HTTP_PASS: - return pht( - '%s updated the HTTP password for this repository.', - $this->renderHandleLink($author_phid)); - case self::TYPE_LOCAL_PATH: - return pht( - '%s changed the local path from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - case self::TYPE_HOSTING: - if ($new) { - return pht( - '%s changed this repository to be hosted on Phabricator.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s changed this repository to track a remote elsewhere.', - $this->renderHandleLink($author_phid)); - } - case self::TYPE_PROTOCOL_HTTP: - return pht( - '%s changed the availability of this repository over HTTP from '. - '"%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - case self::TYPE_PROTOCOL_SSH: - return pht( - '%s changed the availability of this repository over SSH from '. - '"%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - case self::TYPE_PUSH_POLICY: - return pht( - '%s changed the push policy of this repository from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $this->renderPolicyName($old, 'old'), - $this->renderPolicyName($new, 'new')); - case self::TYPE_DANGEROUS: - if ($new) { - return pht( - '%s disabled protection against dangerous changes.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s enabled protection against dangerous changes.', - $this->renderHandleLink($author_phid)); - } - case self::TYPE_ENORMOUS: - if ($new) { - return pht( - '%s disabled protection against enormous changes.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s enabled protection against enormous changes.', - $this->renderHandleLink($author_phid)); - } - case self::TYPE_SLUG: - if (strlen($old) && !strlen($new)) { - return pht( - '%s removed the short name of this repository.', - $this->renderHandleLink($author_phid)); - } else if (strlen($new) && !strlen($old)) { - return pht( - '%s set the short name of this repository to "%s".', - $this->renderHandleLink($author_phid), - $new); - } else { - return pht( - '%s changed the short name of this repository from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - } - case self::TYPE_SERVICE: - if (strlen($old) && !strlen($new)) { - return pht( - '%s moved storage for this repository from %s to local.', - $this->renderHandleLink($author_phid), - $this->renderHandleLink($old)); - } else if (!strlen($old) && strlen($new)) { - // TODO: Possibly, we should distinguish between automatic assignment - // on creation vs explicit adjustment. - return pht( - '%s set storage for this repository to %s.', - $this->renderHandleLink($author_phid), - $this->renderHandleLink($new)); - } else { - return pht( - '%s moved storage for this repository from %s to %s.', - $this->renderHandleLink($author_phid), - $this->renderHandleLink($old), - $this->renderHandleLink($new)); - } - case self::TYPE_SYMBOLS_SOURCES: - return pht( - '%s changed symbol sources from %s to %s.', - $this->renderHandleLink($author_phid), - empty($old) ? pht('None') : $this->renderHandleList($old), - empty($new) ? pht('None') : $this->renderHandleList($new)); - - case self::TYPE_SYMBOLS_LANGUAGE: - return pht('%s changed indexed languages from %s to %s.', - $this->renderHandleLink($author_phid), - $old ? implode(', ', $old) : pht('Any'), - $new ? implode(', ', $new) : pht('Any')); - - case self::TYPE_STAGING_URI: - if (!$old) { - return pht( - '%s set "%s" as the staging area for this repository.', - $this->renderHandleLink($author_phid), - $new); - } else if (!$new) { - return pht( - '%s removed "%s" as the staging area for this repository.', - $this->renderHandleLink($author_phid), - $old); - } else { - return pht( - '%s changed the staging area for this repository from '. - '"%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - } - - case self::TYPE_AUTOMATION_BLUEPRINTS: - $add = array_diff($new, $old); - $rem = array_diff($old, $new); - - if ($add && $rem) { - return pht( - '%s changed %s automation blueprint(s), '. - 'added %s: %s; removed %s: %s.', - $this->renderHandleLink($author_phid), - new PhutilNumber(count($add) + count($rem)), - new PhutilNumber(count($add)), - $this->renderHandleList($add), - new PhutilNumber(count($rem)), - $this->renderHandleList($rem)); - } else if ($add) { - return pht( - '%s added %s automation blueprint(s): %s.', - $this->renderHandleLink($author_phid), - new PhutilNumber(count($add)), - $this->renderHandleList($add)); - } else { - return pht( - '%s removed %s automation blueprint(s): %s.', - $this->renderHandleLink($author_phid), - new PhutilNumber(count($rem)), - $this->renderHandleList($rem)); - } - - case self::TYPE_CALLSIGN: - if ($old === null) { - return pht( - '%s set the callsign for this repository to "%s".', - $this->renderHandleLink($author_phid), - $new); - } else if ($new === null) { - return pht( - '%s removed the callsign ("%s") for this repository.', - $this->renderHandleLink($author_phid), - $old); - } else { - return pht( - '%s changed the callsign for this repository from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - } - - } - - return parent::getTitle(); - } - - public function hasChangeDetails() { - switch ($this->getTransactionType()) { - case self::TYPE_DESCRIPTION: - return true; - } - return parent::hasChangeDetails(); - } - - public function renderChangeDetails(PhabricatorUser $viewer) { - return $this->renderTextCorpusChangeDetails( - $viewer, - $this->getOldValue(), - $this->getNewValue()); + public function getBaseTransactionClass() { + return 'PhabricatorRepositoryTransactionType'; } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryURIIndex.php b/src/applications/repository/storage/PhabricatorRepositoryURIIndex.php index 2cfa0c4471..3ef41f8089 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryURIIndex.php +++ b/src/applications/repository/storage/PhabricatorRepositoryURIIndex.php @@ -1,67 +1,67 @@ false, self::CONFIG_COLUMN_SCHEMA => array( 'repositoryURI' => 'text', ), self::CONFIG_KEY_SCHEMA => array( 'key_repository' => array( 'columns' => array('repositoryPHID'), ), 'key_uri' => array( 'columns' => array('repositoryURI(128)'), ), ), ) + parent::getConfiguration(); } public static function updateRepositoryURIs( $repository_phid, array $uris) { $table = new self(); $conn_w = $table->establishConnection('w'); $sql = array(); foreach ($uris as $key => $uri) { if (!strlen($uri)) { unset($uris[$key]); continue; } $sql[] = qsprintf( $conn_w, '(%s, %s)', $repository_phid, $uri); } $table->openTransaction(); queryfx( $conn_w, - 'DELETE FROM %T WHERE repositoryPHID = %s', - $table->getTableName(), + 'DELETE FROM %R WHERE repositoryPHID = %s', + $table, $repository_phid); if ($sql) { queryfx( $conn_w, - 'INSERT INTO %T (repositoryPHID, repositoryURI) VALUES %Q', - $table->getTableName(), - implode(', ', $sql)); + 'INSERT INTO %R (repositoryPHID, repositoryURI) VALUES %LQ', + $table, + $sql); } $table->saveTransaction(); } } diff --git a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php index 6d79742a32..b544228561 100644 --- a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php +++ b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php @@ -1,160 +1,160 @@ log("%s\n", pht('Parsing "%s"...', $commit->getMonogram())); $hint = $this->loadCommitHint($commit); if ($hint && $hint->isUnreadable()) { $this->log( pht( 'This commit is marked as unreadable, so changes will not be '. 'parsed.')); return; } if (!$this->shouldSkipImportStep()) { $results = $this->parseCommitChanges($repository, $commit); if ($results) { $this->writeCommitChanges($repository, $commit, $results); } $commit->writeImportStatusFlag($this->getImportStepFlag()); PhabricatorSearchWorker::queueDocumentForIndexing($commit->getPHID()); } $this->finishParse(); } public function parseChangesForUnitTest( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { return $this->parseCommitChanges($repository, $commit); } public static function lookupOrCreatePaths(array $paths) { $repository = new PhabricatorRepository(); $conn_w = $repository->establishConnection('w'); $result_map = self::lookupPaths($paths); $missing_paths = array_fill_keys($paths, true); $missing_paths = array_diff_key($missing_paths, $result_map); $missing_paths = array_keys($missing_paths); if ($missing_paths) { foreach (array_chunk($missing_paths, 128) as $path_chunk) { $sql = array(); foreach ($path_chunk as $path) { $sql[] = qsprintf($conn_w, '(%s, %s)', $path, md5($path)); } queryfx( $conn_w, - 'INSERT IGNORE INTO %T (path, pathHash) VALUES %Q', + 'INSERT IGNORE INTO %T (path, pathHash) VALUES %LQ', PhabricatorRepository::TABLE_PATH, - implode(', ', $sql)); + $sql); } $result_map += self::lookupPaths($missing_paths); } return $result_map; } private static function lookupPaths(array $paths) { $repository = new PhabricatorRepository(); $conn_w = $repository->establishConnection('w'); $result_map = array(); foreach (array_chunk($paths, 128) as $path_chunk) { $chunk_map = queryfx_all( $conn_w, 'SELECT path, id FROM %T WHERE pathHash IN (%Ls)', PhabricatorRepository::TABLE_PATH, array_map('md5', $path_chunk)); foreach ($chunk_map as $row) { $result_map[$row['path']] = $row['id']; } } return $result_map; } protected function finishParse() { $commit = $this->commit; if ($this->shouldQueueFollowupTasks()) { $this->queueTask( 'PhabricatorRepositoryCommitOwnersWorker', array( 'commitID' => $commit->getID(), )); } } private function writeCommitChanges( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, array $changes) { $repository_id = (int)$repository->getID(); $commit_id = (int)$commit->getID(); // NOTE: This SQL is being built manually instead of with qsprintf() // because some SVN changes affect an enormous number of paths (millions) // and this showed up as significantly slow on a profile at some point. $changes_sql = array(); foreach ($changes as $change) { $values = array( $repository_id, (int)$change->getPathID(), $commit_id, nonempty((int)$change->getTargetPathID(), 'null'), nonempty((int)$change->getTargetCommitID(), 'null'), (int)$change->getChangeType(), (int)$change->getFileType(), (int)$change->getIsDirect(), (int)$change->getCommitSequence(), ); $changes_sql[] = '('.implode(', ', $values).')'; } $conn_w = $repository->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE commitID = %d', PhabricatorRepository::TABLE_PATHCHANGE, $commit_id); foreach (PhabricatorLiskDAO::chunkSQL($changes_sql) as $chunk) { queryfx( $conn_w, 'INSERT INTO %T (repositoryID, pathID, commitID, targetPathID, targetCommitID, changeType, fileType, isDirect, commitSequence) - VALUES %Q', + VALUES %LQ', PhabricatorRepository::TABLE_PATHCHANGE, $chunk); } } } diff --git a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositorySvnCommitChangeParserWorker.php b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositorySvnCommitChangeParserWorker.php index 6725ddb1d1..6be2251f1e 100644 --- a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositorySvnCommitChangeParserWorker.php +++ b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositorySvnCommitChangeParserWorker.php @@ -1,808 +1,808 @@ getSubversionPathURI(); $svn_commit = $commit->getCommitIdentifier(); // Pull the top-level path changes out of "svn log". This is pretty // straightforward; just parse the XML log. $log = $this->getSVNLogXMLObject($repository, $uri, $svn_commit); $entry = $log->logentry[0]; if (!$entry->paths) { // TODO: Explicitly mark this commit as broken elsewhere? This isn't // supposed to happen but we have some cases like rE27 and rG935 in the // Facebook repositories where things got all clowned up. return array(); } $raw_paths = array(); foreach ($entry->paths->path as $path) { $name = trim((string)$path); $raw_paths[$name] = array( 'rawPath' => $name, 'rawTargetPath' => (string)$path['copyfrom-path'], 'rawChangeType' => (string)$path['action'], 'rawTargetCommit' => (string)$path['copyfrom-rev'], ); } $copied_or_moved_map = array(); $deleted_paths = array(); $add_paths = array(); foreach ($raw_paths as $path => $raw_info) { if ($raw_info['rawTargetPath']) { $copied_or_moved_map[$raw_info['rawTargetPath']][] = $raw_info; } switch ($raw_info['rawChangeType']) { case 'D': $deleted_paths[$path] = $raw_info; break; case 'A': case 'R': $add_paths[$path] = $raw_info; break; } } // If a path was deleted, we need to look in the repository history to // figure out where the former valid location for it is so we can figure out // if it was a directory or not, among other things. $lookup_here = array(); foreach ($raw_paths as $path => $raw_info) { if ($raw_info['rawChangeType'] != 'D') { continue; } // If a change copies a directory and then deletes something from it, // we need to look at the old location for information about the path, not // the new location. This workflow is pretty ridiculous -- so much so that // Trac gets it wrong. See Facebook rO6 for an example, if you happen to // work at Facebook. $parents = $this->expandAllParentPaths($path, $include_self = true); foreach ($parents as $parent) { if (isset($add_paths[$parent])) { $relative_path = substr($path, strlen($parent)); $lookup_here[$path] = array( 'rawPath' => $add_paths[$parent]['rawTargetPath'].$relative_path, 'rawCommit' => $add_paths[$parent]['rawTargetCommit'], ); continue 2; } } // Otherwise we can just look at the previous revision. $lookup_here[$path] = array( 'rawPath' => $path, 'rawCommit' => $svn_commit - 1, ); } $lookup = array(); foreach ($raw_paths as $path => $raw_info) { if ($raw_info['rawChangeType'] == 'D') { $lookup[$path] = $lookup_here[$path]; } else { // For everything that wasn't deleted, we can just look it up directly. $lookup[$path] = array( 'rawPath' => $path, 'rawCommit' => $svn_commit, ); } } $effects = array(); $path_file_types = $this->lookupPathFileTypes($repository, $lookup); foreach ($raw_paths as $path => $raw_info) { if ($raw_info['rawChangeType'] == 'D' && $path_file_types[$path] == DifferentialChangeType::FILE_DIRECTORY) { // Bad. Child paths aren't enumerated in "svn log" so we need // to go fishing. $list = $this->lookupRecursiveFileList( $repository, $lookup[$path]); foreach ($list as $deleted_path => $path_file_type) { $deleted_path = rtrim($path.'/'.$deleted_path, '/'); if (!empty($raw_paths[$deleted_path])) { // We somehow learned about this deletion explicitly? // TODO: Unclear how this is possible. continue; } $effect_type = DifferentialChangeType::TYPE_DELETE; $effect_target_path = null; if (isset($copied_or_moved_map[$deleted_path])) { $effect_target_path = $path; if (count($copied_or_moved_map[$deleted_path]) > 1) { $effect_type = DifferentialChangeType::TYPE_MULTICOPY; } else { $effect_type = DifferentialChangeType::TYPE_MOVE_AWAY; } } $effects[$deleted_path] = array( 'rawPath' => $deleted_path, 'rawTargetPath' => $effect_target_path, 'rawTargetCommit' => null, 'rawDirect' => true, 'changeType' => $effect_type, 'fileType' => $path_file_type, ); $deleted_paths[$deleted_path] = $effects[$deleted_path]; } } } $resolved_types = array(); $supplemental = array(); foreach ($raw_paths as $path => $raw_info) { if (isset($resolved_types[$path])) { $type = $resolved_types[$path]; } else { switch ($raw_info['rawChangeType']) { case 'D': if (isset($copied_or_moved_map[$path])) { if (count($copied_or_moved_map[$path]) > 1) { $type = DifferentialChangeType::TYPE_MULTICOPY; } else { $type = DifferentialChangeType::TYPE_MOVE_AWAY; } } else { $type = DifferentialChangeType::TYPE_DELETE; } break; case 'A': $copy_from = $raw_info['rawTargetPath']; $copy_rev = $raw_info['rawTargetCommit']; if (!strlen($copy_from)) { $type = DifferentialChangeType::TYPE_ADD; } else { if (isset($deleted_paths[$copy_from])) { $type = DifferentialChangeType::TYPE_MOVE_HERE; $other_type = DifferentialChangeType::TYPE_MOVE_AWAY; } else { $type = DifferentialChangeType::TYPE_COPY_HERE; $other_type = DifferentialChangeType::TYPE_COPY_AWAY; } $source_file_type = $this->lookupPathFileType( $repository, $copy_from, array( 'rawPath' => $copy_from, 'rawCommit' => $copy_rev, )); if ($source_file_type == DifferentialChangeType::FILE_DELETED) { throw new Exception( pht('Something is wrong; source of a copy must exist.')); } if ($source_file_type != DifferentialChangeType::FILE_DIRECTORY) { if (isset($raw_paths[$copy_from]) || isset($effects[$copy_from])) { break; } $effects[$copy_from] = array( 'rawPath' => $copy_from, 'rawTargetPath' => null, 'rawTargetCommit' => null, 'rawDirect' => false, 'changeType' => $other_type, 'fileType' => $source_file_type, ); } else { // ULTRADISASTER. We've added a directory which was copied // or moved from somewhere else. This is the most complex and // ridiculous case. $list = $this->lookupRecursiveFileList( $repository, array( 'rawPath' => $copy_from, 'rawCommit' => $copy_rev, )); foreach ($list as $from_path => $from_file_type) { $full_from = rtrim($copy_from.'/'.$from_path, '/'); $full_to = rtrim($path.'/'.$from_path, '/'); if (empty($raw_paths[$full_to])) { $effects[$full_to] = array( 'rawPath' => $full_to, 'rawTargetPath' => $full_from, 'rawTargetCommit' => $copy_rev, 'rawDirect' => true, 'changeType' => $type, 'fileType' => $from_file_type, ); } else { // This means we picked the file up explicitly elsewhere. // If the file as modified, SVN will drop the copy // information. We need to restore it. $supplemental[$full_to]['rawTargetPath'] = $full_from; $supplemental[$full_to]['rawTargetCommit'] = $copy_rev; if ($raw_paths[$full_to]['rawChangeType'] == 'M') { $resolved_types[$full_to] = $type; } } if (empty($raw_paths[$full_from]) && empty($effects[$full_from])) { if ($other_type == DifferentialChangeType::TYPE_COPY_AWAY) { // Add an indirect effect for the copied file, if we // don't already have an entry for it (e.g., a separate // change). $effects[$full_from] = array( 'rawPath' => $full_from, 'rawTargetPath' => null, 'rawTargetCommit' => null, 'rawDirect' => false, 'changeType' => $other_type, 'fileType' => $from_file_type, ); } } } } } break; // This is "replaced", caused by "svn rm"-ing a file, putting another // in its place, and then "svn add"-ing it. We do not distinguish // between this and "M". case 'R': case 'M': if (isset($copied_or_moved_map[$path])) { $type = DifferentialChangeType::TYPE_COPY_AWAY; } else { $type = DifferentialChangeType::TYPE_CHANGE; } break; } } $resolved_types[$path] = $type; } foreach ($raw_paths as $path => $raw_info) { $raw_paths[$path]['changeType'] = $resolved_types[$path]; if (isset($supplemental[$path])) { foreach ($supplemental[$path] as $key => $value) { $raw_paths[$path][$key] = $value; } } } foreach ($raw_paths as $path => $raw_info) { $effects[$path] = array( 'rawPath' => $path, 'rawTargetPath' => $raw_info['rawTargetPath'], 'rawTargetCommit' => $raw_info['rawTargetCommit'], 'rawDirect' => true, 'changeType' => $raw_info['changeType'], 'fileType' => $path_file_types[$path], ); } $parents = array(); foreach ($effects as $path => $effect) { foreach ($this->expandAllParentPaths($path) as $parent_path) { $parents[$parent_path] = true; } } $parents = array_keys($parents); foreach ($parents as $parent) { if (isset($effects[$parent])) { continue; } $effects[$parent] = array( 'rawPath' => $parent, 'rawTargetPath' => null, 'rawTargetCommit' => null, 'rawDirect' => false, 'changeType' => DifferentialChangeType::TYPE_CHILD, 'fileType' => DifferentialChangeType::FILE_DIRECTORY, ); } $lookup_paths = array(); foreach ($effects as $effect) { $lookup_paths[$effect['rawPath']] = true; if ($effect['rawTargetPath']) { $lookup_paths[$effect['rawTargetPath']] = true; } } $lookup_paths = array_keys($lookup_paths); $lookup_commits = array(); foreach ($effects as $effect) { if ($effect['rawTargetCommit']) { $lookup_commits[$effect['rawTargetCommit']] = true; } } $lookup_commits = array_keys($lookup_commits); $path_map = $this->lookupOrCreatePaths($lookup_paths); $commit_map = $this->lookupSvnCommits($repository, $lookup_commits); $this->writeBrowse($repository, $commit, $effects, $path_map); return $this->buildChanges( $repository, $commit, $effects, $path_map, $commit_map); } private function buildChanges( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, array $effects, array $path_map, array $commit_map) { $results = array(); foreach ($effects as $effect) { $path_id = $path_map[$effect['rawPath']]; $target_path_id = null; if ($effect['rawTargetPath']) { $target_path_id = $path_map[$effect['rawTargetPath']]; } $target_commit_id = null; if ($effect['rawTargetCommit']) { $target_commit_id = $commit_map[$effect['rawTargetCommit']]; } $result = id(new PhabricatorRepositoryParsedChange()) ->setPathID($path_id) ->setTargetPathID($target_path_id) ->setTargetCommitID($target_commit_id) ->setChangeType($effect['changeType']) ->setFileType($effect['fileType']) ->setIsDirect($effect['rawDirect']) ->setCommitSequence($commit->getCommitIdentifier()); $results[] = $result; } return $results; } private function writeBrowse( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, array $effects, array $path_map) { $conn_w = $repository->establishConnection('w'); $sql = array(); foreach ($effects as $effect) { $type = $effect['changeType']; if (!$effect['rawDirect']) { if ($type == DifferentialChangeType::TYPE_COPY_AWAY) { // Don't write COPY_AWAY to the filesystem table if it isn't a direct // event. continue; } if ($type == DifferentialChangeType::TYPE_CHILD) { // Don't write CHILD to the filesystem table. Although doing these // writes has the nice property of letting you see when a directory's // contents were last changed, it explodes the table tremendously // and makes Diffusion far slower. continue; } } if ($effect['rawPath'] == '/') { // Don't write any events on '/' to the filesystem table; in // particular, it doesn't have a meaningful parentID. continue; } $existed = !DifferentialChangeType::isDeleteChangeType($type); $sql[] = qsprintf( $conn_w, '(%d, %d, %d, %d, %d, %d)', $repository->getID(), $path_map[$this->getParentPath($effect['rawPath'])], $commit->getCommitIdentifier(), $path_map[$effect['rawPath']], $existed ? 1 : 0, $effect['fileType']); } queryfx( $conn_w, 'DELETE FROM %T WHERE repositoryID = %d AND svnCommit = %d', PhabricatorRepository::TABLE_FILESYSTEM, $repository->getID(), $commit->getCommitIdentifier()); foreach (array_chunk($sql, 512) as $sql_chunk) { queryfx( $conn_w, 'INSERT INTO %T (repositoryID, parentID, svnCommit, pathID, existed, fileType) - VALUES %Q', + VALUES %LQ', PhabricatorRepository::TABLE_FILESYSTEM, - implode(', ', $sql_chunk)); + $sql_chunk); } } private function lookupSvnCommits( PhabricatorRepository $repository, array $commits) { if (!$commits) { return array(); } $commit_table = new PhabricatorRepositoryCommit(); $commit_data = queryfx_all( $commit_table->establishConnection('w'), 'SELECT id, commitIdentifier FROM %T WHERE repositoryID = %d AND commitIdentifier in (%Ls)', $commit_table->getTableName(), $repository->getID(), $commits); $commit_map = ipull($commit_data, 'id', 'commitIdentifier'); $need = array(); foreach ($commits as $commit) { if (empty($commit_map[$commit])) { $need[] = $commit; } } // If we are parsing a Subversion repository and have been configured to // import only some subdirectory of it, we may find commits which reference // other foreign commits outside of the directory (for instance, because of // a move or copy). Rather than trying to execute full parses on them, just // create stub commits and identify the stubs as foreign commits. if ($need) { $subpath = $repository->getDetail('svn-subpath'); if (!$subpath) { throw new Exception( pht( 'Missing commits (%s) in a SVN repository which is not '. 'configured for subdirectory-only parsing!', implode(', ', $need))); } foreach ($need as $foreign_commit) { $commit = new PhabricatorRepositoryCommit(); $commit->setRepositoryID($repository->getID()); $commit->setCommitIdentifier($foreign_commit); $commit->setEpoch(0); // Mark this commit as imported so it doesn't prevent the repository // from transitioning into the "Imported" state. $commit->setImportStatus(PhabricatorRepositoryCommit::IMPORTED_ALL); $commit->save(); $data = new PhabricatorRepositoryCommitData(); $data->setCommitID($commit->getID()); $data->setAuthorName(''); $data->setCommitMessage(''); $data->setCommitDetails( array( 'foreign-svn-stub' => true, // Denormalize this to make it easier to debug cases where someone // did half a parse and then changed the subdirectory or something // like that. 'svn-subpath' => $subpath, )); $data->save(); $commit_map[$foreign_commit] = $commit->getID(); } } return $commit_map; } private function lookupPathFileType( PhabricatorRepository $repository, $path, array $path_info) { $result = $this->lookupPathFileTypes( $repository, array( $path => $path_info, )); return $result[$path]; } private function lookupPathFileTypes( PhabricatorRepository $repository, array $paths) { $result_map = array(); $repository_uri = $repository->getSubversionPathURI(); if (isset($paths['/'])) { $result_map['/'] = DifferentialChangeType::FILE_DIRECTORY; unset($paths['/']); } $parents = array(); $path_mapping = array(); foreach ($paths as $path => $lookup) { $parent = dirname($lookup['rawPath']); $parent = $repository->getSubversionPathURI( $parent, $lookup['rawCommit']); $parent = escapeshellarg($parent); $parents[$parent] = true; $path_mapping[$parent][] = dirname($path); } // Reverse this list so we can pop $path_mapping, as that's more efficient // than shifting it. We need to associate these maps positionally because // a change can copy the same source path from multiple revisions via // "svn cp path@1 a; svn cp path@2 b;" and the XML output gives us no way // to distinguish which revision we're looking at except based on its // position in the document. $all_paths = array_reverse(array_keys($parents)); foreach (array_chunk($all_paths, 64) as $path_chunk) { list($raw_xml) = $repository->execxRemoteCommand( '--xml ls %C', implode(' ', $path_chunk)); $xml = new SimpleXMLElement($raw_xml); foreach ($xml->list as $list) { $list_path = (string)$list['path']; // SVN is a big mess. See Facebook rG8 (a revision which adds files // with spaces in their names) for an example. $list_path = rawurldecode($list_path); if ($list_path == $repository_uri) { $base = '/'; } else { $base = substr($list_path, strlen($repository_uri)); } $mapping = array_pop($path_mapping); foreach ($list->entry as $entry) { $val = $this->getFileTypeFromSVNKind($entry['kind']); foreach ($mapping as $base_path) { // rtrim() causes us to handle top-level directories correctly. $key = rtrim($base_path, '/').'/'.$entry->name; $result_map[$key] = $val; } } } } foreach ($paths as $path => $lookup) { if (empty($result_map[$path])) { $result_map[$path] = DifferentialChangeType::FILE_DELETED; } } return $result_map; } private function getFileTypeFromSVNKind($kind) { $kind = (string)$kind; switch ($kind) { case 'dir': return DifferentialChangeType::FILE_DIRECTORY; case 'file': return DifferentialChangeType::FILE_NORMAL; default: throw new Exception(pht("Unknown SVN file kind '%s'.", $kind)); } } private function lookupRecursiveFileList( PhabricatorRepository $repository, array $info) { $path = $info['rawPath']; $rev = $info['rawCommit']; $path_uri = $repository->getSubversionPathURI($path, $rev); $hashkey = md5($path_uri); // This method is quite horrible. The underlying challenge is that some // commits in the Facebook repository are enormous, taking multiple hours // to 'ls -R' out of the repository and producing XML files >1GB in size. // If we try to SimpleXML them, the object exhausts available memory on a // 64G machine. Instead, cache the XML output and then parse it line by line // to limit space requirements. $cache_loc = sys_get_temp_dir().'/diffusion.'.$hashkey.'.svnls'; if (!Filesystem::pathExists($cache_loc)) { $tmp = new TempFile(); $repository->execxRemoteCommand( '--xml ls -R %s > %s', $path_uri, $tmp); execx( 'mv %s %s', $tmp, $cache_loc); } $map = $this->parseRecursiveListFileData($cache_loc); Filesystem::remove($cache_loc); return $map; } private function parseRecursiveListFileData($file_path) { $map = array(); $mode = 'xml'; $done = false; $entry = null; foreach (new LinesOfALargeFile($file_path) as $lno => $line) { switch ($mode) { case 'entry': if ($line == '') { $entry = implode('', $entry); $pattern = '@^\s+kind="(file|dir)">'. '(.*?)'. '((.*?))?@'; $matches = null; if (!preg_match($pattern, $entry, $matches)) { throw new Exception(pht('Unable to parse entry!')); } $map[html_entity_decode($matches[2])] = $this->getFileTypeFromSVNKind($matches[1]); $mode = 'entry-or-end'; } else { $entry[] = $line; } break; case 'entry-or-end': if ($line == '') { $done = true; break 2; } else if ($line == '', '= 1) { array_pop($parts); $parents[] = '/'.implode('/', $parts); } return $parents; } private function getSVNLogXMLObject( PhabricatorRepository $repository, $uri, $revision) { list($xml) = $repository->execxRemoteCommand( 'log --xml --verbose --limit 1 %s@%d', $uri, $revision); // Subversion may send us back commit messages which won't parse because // they have non UTF-8 garbage in them. Slam them into valid UTF-8. $xml = phutil_utf8ize($xml); return new SimpleXMLElement($xml); } } diff --git a/src/applications/repository/xaction/PhabricatorRepositoryActivateTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryActivateTransaction.php new file mode 100644 index 0000000000..31298c55aa --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositoryActivateTransaction.php @@ -0,0 +1,61 @@ +isTracked(); + } + + public function applyInternalEffects($object, $value) { + // The first time a repository is activated, clear the "new repository" + // flag so we stop showing setup hints. + if ($value) { + $object->setDetail('newly-initialized', false); + } + + $object->setDetail('tracking-enabled', $value); + } + + public function getTitle() { + $new = $this->getNewValue(); + + // TODO: Old versions of this transaction use a boolean value, but + // should be migrated. + $is_deactivate = + (!$new) || + ($new == PhabricatorRepository::STATUS_INACTIVE); + + if (!$is_deactivate) { + return pht( + '%s activated this repository.', + $this->renderAuthor()); + } else { + return pht( + '%s deactivated this repository.', + $this->renderAuthor()); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $status_map = PhabricatorRepository::getStatusMap(); + foreach ($xactions as $xaction) { + $status = $xaction->getNewValue(); + if (empty($status_map[$status])) { + $errors[] = $this->newInvalidError( + pht( + 'Repository status "%s" is not valid. Valid statuses are: %s.', + $status, + implode(', ', array_keys($status_map))), + $xaction); + } + } + + return $errors; + } + +} diff --git a/src/applications/repository/xaction/PhabricatorRepositoryAutocloseOnlyTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryAutocloseOnlyTransaction.php new file mode 100644 index 0000000000..feacc73c44 --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositoryAutocloseOnlyTransaction.php @@ -0,0 +1,42 @@ +getDetail('close-commits-filter', array())); + } + + public function applyInternalEffects($object, $value) { + $object->setDetail('close-commits-filter', array_fill_keys($value, true)); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if (!$new) { + return pht( + '%s set this repository to autoclose on all branches.', + $this->renderAuthor()); + } else if (!$old) { + return pht( + '%s set this repository to autoclose on branches: %s.', + $this->renderAuthor(), + $this->renderValue(implode(', ', $new))); + } else { + return pht( + '%s changed autoclose branches from %s to %s.', + $this->renderAuthor(), + $this->renderValue(implode(', ', $old)), + $this->renderValue(implode(', ', $new))); + } + } + + public function validateTransactions($object, array $xactions) { + return $this->validateRefList($object, $xactions); + } + +} diff --git a/src/applications/repository/xaction/PhabricatorRepositoryAutocloseTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryAutocloseTransaction.php new file mode 100644 index 0000000000..6ab677fb4a --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositoryAutocloseTransaction.php @@ -0,0 +1,34 @@ +getDetail('disable-autoclose'); + } + + public function generateNewValue($object, $value) { + return (int)$value; + } + + public function applyInternalEffects($object, $value) { + $object->setDetail('disable-autoclose', (int)!$value); + } + + public function getTitle() { + $new = $this->getNewValue(); + + if ($new) { + return pht( + '%s enabled autoclose for this repository.', + $this->renderAuthor()); + } else { + return pht( + '%s disabled autoclose for this repository.', + $this->renderAuthor()); + } + } + +} diff --git a/src/applications/repository/xaction/PhabricatorRepositoryBlueprintsTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryBlueprintsTransaction.php new file mode 100644 index 0000000000..aae164d706 --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositoryBlueprintsTransaction.php @@ -0,0 +1,81 @@ +getDetail('automation.blueprintPHIDs', array()); + } + + public function applyInternalEffects($object, $value) { + $object->setDetail('automation.blueprintPHIDs', $value); + } + + public function applyExternalEffects($object, $value) { + DrydockAuthorization::applyAuthorizationChanges( + $this->getActor(), + $object->getPHID(), + $this->getOldValue(), + $this->getNewValue()); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + $add = array_diff($new, $old); + $rem = array_diff($old, $new); + + if ($add && $rem) { + return pht( + '%s changed %s automation blueprint(s), '. + 'added %s: %s; removed %s: %s.', + $this->renderAuthor(), + new PhutilNumber(count($add) + count($rem)), + new PhutilNumber(count($add)), + $this->renderHandleList($add), + new PhutilNumber(count($rem)), + $this->renderHandleList($rem)); + } else if ($add) { + return pht( + '%s added %s automation blueprint(s): %s.', + $this->renderAuthor(), + new PhutilNumber(count($add)), + $this->renderHandleList($add)); + } else { + return pht( + '%s removed %s automation blueprint(s): %s.', + $this->renderAuthor(), + new PhutilNumber(count($rem)), + $this->renderHandleList($rem)); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + foreach ($xactions as $xaction) { + $old = nonempty($xaction->getOldValue(), array()); + $new = nonempty($xaction->getNewValue(), array()); + + $add = array_diff($new, $old); + + $invalid = PhabricatorObjectQuery::loadInvalidPHIDsForViewer( + $this->getActor(), + $add); + if ($invalid) { + $errors[] = $this->newInvalidError( + pht( + 'Some of the selected automation blueprints are invalid '. + 'or restricted: %s.', + implode(', ', $invalid)), + $xaction); + } + } + + return $errors; + } + +} diff --git a/src/applications/repository/xaction/PhabricatorRepositoryCallsignTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryCallsignTransaction.php new file mode 100644 index 0000000000..ab13c6a571 --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositoryCallsignTransaction.php @@ -0,0 +1,90 @@ +getCallsign(); + } + + public function generateNewValue($object, $value) { + if (strlen($value)) { + return $value; + } + + return null; + } + + public function applyInternalEffects($object, $value) { + $object->setCallsign($value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if (!strlen($old)) { + return pht( + '%s set the callsign for this repository to %s.', + $this->renderAuthor(), + $this->renderNewValue()); + } else if (!strlen($new)) { + return pht( + '%s removed the callsign (%s) for this repository.', + $this->renderAuthor(), + $this->renderOldValue()); + } else { + return pht( + '%s changed the callsign for this repository from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + foreach ($xactions as $xaction) { + $old = $xaction->getOldValue(); + $new = $xaction->getNewValue(); + + if (!strlen($new)) { + continue; + } + + if ($new === $old) { + continue; + } + + try { + PhabricatorRepository::assertValidCallsign($new); + } catch (Exception $ex) { + $errors[] = $this->newInvalidError( + $ex->getMessage(), + $xaction); + continue; + } + + $other = id(new PhabricatorRepositoryQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withCallsigns(array($new)) + ->executeOne(); + if ($other && ($other->getID() !== $object->getID())) { + $errors[] = $this->newError( + pht('Duplicate'), + pht( + 'The selected callsign ("%s") is already in use by another '. + 'repository. Choose a unique callsign.', + $new), + $xaction); + continue; + } + } + + return $errors; + } + +} diff --git a/src/applications/repository/xaction/PhabricatorRepositoryCopyTimeLimitTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryCopyTimeLimitTransaction.php new file mode 100644 index 0000000000..6be3466ae7 --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositoryCopyTimeLimitTransaction.php @@ -0,0 +1,77 @@ +getCopyTimeLimit(); + } + + public function generateNewValue($object, $value) { + if (!strlen($value)) { + return null; + } + + $value = (int)$value; + if (!$value) { + return null; + } + + return $value; + } + + public function applyInternalEffects($object, $value) { + $object->setCopyTimeLimit($value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if ($old && $new) { + return pht( + '%s changed the copy time limit for this repository from %s seconds '. + 'to %s seconds.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } else if ($new) { + return pht( + '%s set the copy time limit for this repository to %s seconds.', + $this->renderAuthor(), + $this->renderNewValue()); + } else { + return pht( + '%s reset the copy time limit (%s seconds) for this repository '. + 'to the default value.', + $this->renderAuthor(), + $this->renderOldValue()); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + foreach ($xactions as $xaction) { + $new = $xaction->getNewValue(); + + if (!strlen($new)) { + continue; + } + + if (!preg_match('/^\d+\z/', $new)) { + $errors[] = $this->newInvalidError( + pht( + 'Unable to parse copy time limit, specify a positive number '. + 'of seconds.'), + $xaction); + continue; + } + } + + return $errors; + } + +} diff --git a/src/applications/repository/xaction/PhabricatorRepositoryDangerousTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryDangerousTransaction.php new file mode 100644 index 0000000000..218e1d1182 --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositoryDangerousTransaction.php @@ -0,0 +1,30 @@ +shouldAllowDangerousChanges(); + } + + public function applyInternalEffects($object, $value) { + $object->setDetail('allow-dangerous-changes', $value); + } + + public function getTitle() { + $new = $this->getNewValue(); + + if ($new) { + return pht( + '%s disabled protection against dangerous changes.', + $this->renderAuthor()); + } else { + return pht( + '%s enabled protection against dangerous changes.', + $this->renderAuthor()); + } + } + +} diff --git a/src/applications/repository/xaction/PhabricatorRepositoryDefaultBranchTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryDefaultBranchTransaction.php new file mode 100644 index 0000000000..23eebf60c8 --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositoryDefaultBranchTransaction.php @@ -0,0 +1,39 @@ +getDetail('default-branch'); + } + + public function applyInternalEffects($object, $value) { + $object->setDetail('default-branch', $value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if (!strlen($new)) { + return pht( + '%s removed %s as the default branch.', + $this->renderAuthor(), + $this->renderOldValue()); + } else if (!strlen($old)) { + return pht( + '%s set the default branch to %s.', + $this->renderAuthor(), + $this->renderNewValue()); + } else { + return pht( + '%s changed the default branch from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + } + +} diff --git a/src/applications/repository/xaction/PhabricatorRepositoryDescriptionTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryDescriptionTransaction.php new file mode 100644 index 0000000000..9e86d80d06 --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositoryDescriptionTransaction.php @@ -0,0 +1,49 @@ +getDetail('description'); + } + + public function applyInternalEffects($object, $value) { + $object->setDetail('description', $value); + } + + public function getTitle() { + return pht( + '%s updated the description for this repository.', + $this->renderAuthor()); + } + + public function hasChangeDetailView() { + return true; + } + + public function getMailDiffSectionHeader() { + return pht('CHANGES TO REPOSITORY DESCRIPTION'); + } + + public function newChangeDetailView() { + $viewer = $this->getViewer(); + + return id(new PhabricatorApplicationTransactionTextDiffDetailView()) + ->setViewer($viewer) + ->setOldText($this->getOldValue()) + ->setNewText($this->getNewValue()); + } + + public function newRemarkupChanges() { + $changes = array(); + + $changes[] = $this->newRemarkupChange() + ->setOldValue($this->getOldValue()) + ->setNewValue($this->getNewValue()); + + return $changes; + } + +} diff --git a/src/applications/repository/xaction/PhabricatorRepositoryEncodingTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryEncodingTransaction.php new file mode 100644 index 0000000000..ef129da832 --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositoryEncodingTransaction.php @@ -0,0 +1,68 @@ +getDetail('encoding'); + } + + public function applyInternalEffects($object, $value) { + $object->setDetail('encoding', $value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if (strlen($old) && !strlen($new)) { + return pht( + '%s removed the %s encoding configured for this repository.', + $this->renderAuthor(), + $this->renderOldValue()); + } else if (strlen($new) && !strlen($old)) { + return pht( + '%s set the encoding for this repository to %s.', + $this->renderAuthor(), + $this->renderNewValue()); + } else { + return pht( + '%s changed the repository encoding from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + foreach ($xactions as $xaction) { + // Make sure the encoding is valid by converting to UTF-8. This tests + // that the user has mbstring installed, and also that they didn't + // type a garbage encoding name. Note that we're converting from + // UTF-8 to the target encoding, because mbstring is fine with + // converting from a nonsense encoding. + $encoding = $xaction->getNewValue(); + if (!strlen($encoding)) { + continue; + } + + try { + phutil_utf8_convert('.', $encoding, 'UTF-8'); + } catch (Exception $ex) { + $errors[] = $this->newInvalidError( + pht( + 'Repository encoding "%s" is not valid: %s', + $encoding, + $ex->getMessage()), + $xaction); + } + } + + return $errors; + } + +} diff --git a/src/applications/repository/xaction/PhabricatorRepositoryEnormousTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryEnormousTransaction.php new file mode 100644 index 0000000000..aae18ba8ff --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositoryEnormousTransaction.php @@ -0,0 +1,30 @@ +shouldAllowEnormousChanges(); + } + + public function applyInternalEffects($object, $value) { + $object->setDetail('allow-enormous-changes', $value); + } + + public function getTitle() { + $new = $this->getNewValue(); + + if ($new) { + return pht( + '%s disabled protection against enormous changes.', + $this->renderAuthor()); + } else { + return pht( + '%s enabled protection against enormous changes.', + $this->renderAuthor()); + } + } + +} diff --git a/src/applications/repository/xaction/PhabricatorRepositoryFilesizeLimitTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryFilesizeLimitTransaction.php new file mode 100644 index 0000000000..6bf74cc757 --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositoryFilesizeLimitTransaction.php @@ -0,0 +1,78 @@ +getFilesizeLimit(); + } + + public function generateNewValue($object, $value) { + if (!strlen($value)) { + return null; + } + + $value = phutil_parse_bytes($value); + if (!$value) { + return null; + } + + return $value; + } + + public function applyInternalEffects($object, $value) { + $object->setFilesizeLimit($value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if ($old && $new) { + return pht( + '%s changed the filesize limit for this repository from %s bytes to '. + '%s bytes.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } else if ($new) { + return pht( + '%s set the filesize limit for this repository to %s bytes.', + $this->renderAuthor(), + $this->renderNewValue()); + } else { + return pht( + '%s removed the filesize limit (%s bytes) for this repository.', + $this->renderAuthor(), + $this->renderOldValue()); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + foreach ($xactions as $xaction) { + $new = $xaction->getNewValue(); + + if (!strlen($new)) { + continue; + } + + try { + $value = phutil_parse_bytes($new); + } catch (Exception $ex) { + $errors[] = $this->newInvalidError( + pht( + 'Unable to parse filesize limit: %s', + $ex->getMessage()), + $xaction); + continue; + } + } + + return $errors; + } + +} diff --git a/src/applications/repository/xaction/PhabricatorRepositoryNameTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryNameTransaction.php new file mode 100644 index 0000000000..c168037c84 --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositoryNameTransaction.php @@ -0,0 +1,35 @@ +getName(); + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + } + + public function getTitle() { + return pht( + '%s renamed this repository from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + if ($this->isEmptyTextTransaction($object->getName(), $xactions)) { + $errors[] = $this->newRequiredError( + pht('Repositories must have a name.')); + } + + return $errors; + } + +} diff --git a/src/applications/repository/xaction/PhabricatorRepositoryNotifyTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryNotifyTransaction.php new file mode 100644 index 0000000000..4ffe1918ff --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositoryNotifyTransaction.php @@ -0,0 +1,34 @@ +getDetail('herald-disabled'); + } + + public function generateNewValue($object, $value) { + return (int)$value; + } + + public function applyInternalEffects($object, $value) { + $object->setDetail('herald-disabled', (int)!$value); + } + + public function getTitle() { + $new = $this->getNewValue(); + + if ($new) { + return pht( + '%s enabled notifications and publishing for this repository.', + $this->renderAuthor()); + } else { + return pht( + '%s disabled notifications and publishing for this repository.', + $this->renderAuthor()); + } + } + +} diff --git a/src/applications/repository/xaction/PhabricatorRepositoryPushPolicyTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryPushPolicyTransaction.php new file mode 100644 index 0000000000..1bd8ce9e47 --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositoryPushPolicyTransaction.php @@ -0,0 +1,24 @@ +getPushPolicy(); + } + + public function applyInternalEffects($object, $value) { + $object->setPushPolicy($value); + } + + public function getTitle() { + return pht( + '%s changed the push policy of this repository from %s to %s.', + $this->renderAuthor(), + $this->renderOldPolicy(), + $this->renderNewPolicy()); + } + +} diff --git a/src/applications/repository/xaction/PhabricatorRepositorySVNSubpathTransaction.php b/src/applications/repository/xaction/PhabricatorRepositorySVNSubpathTransaction.php new file mode 100644 index 0000000000..10de2d9980 --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositorySVNSubpathTransaction.php @@ -0,0 +1,39 @@ +getDetail('svn-subpath'); + } + + public function applyInternalEffects($object, $value) { + $object->setDetail('svn-subpath', $value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if (!strlen($new)) { + return pht( + '%s removed %s as the "Import Only" path.', + $this->renderAuthor(), + $this->renderOldValue()); + } else if (!strlen($old)) { + return pht( + '%s set the repository "Import Only" path to %s.', + $this->renderAuthor(), + $this->renderNewValue()); + } else { + return pht( + '%s changed the "Import Only" path from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + } + +} diff --git a/src/applications/repository/xaction/PhabricatorRepositoryServiceTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryServiceTransaction.php new file mode 100644 index 0000000000..f6d89a61f5 --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositoryServiceTransaction.php @@ -0,0 +1,59 @@ +getAlmanacServicePHID(); + } + + public function generateNewValue($object, $value) { + if (strlen($value)) { + return $value; + } + + return null; + } + + public function applyInternalEffects($object, $value) { + $object->setAlmanacServicePHID($value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getOldValue(); + + if (strlen($old) && !strlen($new)) { + return pht( + '%s moved storage for this repository from %s to local.', + $this->renderAuthor(), + $this->renderOldHandle()); + } else if (!strlen($old) && strlen($new)) { + // TODO: Possibly, we should distinguish between automatic assignment + // on creation vs explicit adjustment. + return pht( + '%s set storage for this repository to %s.', + $this->renderAuthor(), + $this->renderNewHandle()); + } else { + return pht( + '%s moved storage for this repository from %s to %s.', + $this->renderAuthor(), + $this->renderOldHandle(), + $this->renderNewHandle()); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + // TODO: This could use some validation, values should be valid Almanac + // services of appropriate types. It's only reachable via the CLI so it's + // difficult to get wrong in practice. + + return $errors; + } + +} diff --git a/src/applications/repository/xaction/PhabricatorRepositorySlugTransaction.php b/src/applications/repository/xaction/PhabricatorRepositorySlugTransaction.php new file mode 100644 index 0000000000..79e8f7121e --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositorySlugTransaction.php @@ -0,0 +1,88 @@ +getRepositorySlug(); + } + + public function generateNewValue($object, $value) { + if (strlen($value)) { + return $value; + } + + return null; + } + + public function applyInternalEffects($object, $value) { + $object->setRepositorySlug($value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if (strlen($old) && !strlen($new)) { + return pht( + '%s removed the short name of this repository.', + $this->renderAuthor()); + } else if (strlen($new) && !strlen($old)) { + return pht( + '%s set the short name of this repository to %s.', + $this->renderAuthor(), + $this->renderNewValue()); + } else { + return pht( + '%s changed the short name of this repository from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + foreach ($xactions as $xaction) { + $old = $xaction->getOldValue(); + $new = $xaction->getNewValue(); + + if (!strlen($new)) { + continue; + } + + if ($new === $old) { + continue; + } + + try { + PhabricatorRepository::assertValidRepositorySlug($new); + } catch (Exception $ex) { + $errors[] = $this->newInvalidError( + $ex->getMessage(), + $xaction); + continue; + } + + $other = id(new PhabricatorRepositoryQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withSlugs(array($new)) + ->executeOne(); + if ($other && ($other->getID() !== $object->getID())) { + $errors[] = $this->newError( + pht('Duplicate'), + pht( + 'The selected repository short name is already in use by '. + 'another repository. Choose a unique short name.'), + $xaction); + continue; + } + } + + return $errors; + } + +} diff --git a/src/applications/repository/xaction/PhabricatorRepositoryStagingURITransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryStagingURITransaction.php new file mode 100644 index 0000000000..4297d5e244 --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositoryStagingURITransaction.php @@ -0,0 +1,68 @@ +getDetail('staging-uri'); + } + + public function applyInternalEffects($object, $value) { + $object->setDetail('staging-uri', $value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if (!strlen($old)) { + return pht( + '%s set %s as the staging area for this repository.', + $this->renderAuthor(), + $this->renderNewValue()); + } else if (!strlen($new)) { + return pht( + '%s removed %s as the staging area for this repository.', + $this->renderAuthor(), + $this->renderOldValue()); + } else { + return pht( + '%s changed the staging area for this repository from '. + '%s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $old = $this->generateOldValue($object); + foreach ($xactions as $xaction) { + $new = $xaction->getNewValue(); + + if (!strlen($new)) { + continue; + } + + if ($new === $old) { + continue; + } + + try { + PhabricatorRepository::assertValidRemoteURI($new); + } catch (Exception $ex) { + $errors[] = $this->newInvalidError( + $ex->getMessage(), + $xaction); + continue; + } + } + + return $errors; + } + +} diff --git a/src/applications/repository/xaction/PhabricatorRepositorySymbolLanguagesTransaction.php b/src/applications/repository/xaction/PhabricatorRepositorySymbolLanguagesTransaction.php new file mode 100644 index 0000000000..18653c78fd --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositorySymbolLanguagesTransaction.php @@ -0,0 +1,39 @@ +getSymbolLanguages(); + } + + public function applyInternalEffects($object, $value) { + $object->setDetail('symbol-languages', $value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if ($old) { + $display_old = implode(', ', $old); + } else { + $display_old = pht('Any'); + } + + if ($new) { + $display_new = implode(', ', $new); + } else { + $display_new = pht('Any'); + } + + return pht( + '%s changed indexed languages from %s to %s.', + $this->renderAuthor(), + $this->renderValue($display_old), + $this->renderValue($display_new)); + } + +} diff --git a/src/applications/repository/xaction/PhabricatorRepositorySymbolSourcesTransaction.php b/src/applications/repository/xaction/PhabricatorRepositorySymbolSourcesTransaction.php new file mode 100644 index 0000000000..781fad9d99 --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositorySymbolSourcesTransaction.php @@ -0,0 +1,78 @@ +getSymbolSources(); + } + + public function applyInternalEffects($object, $value) { + $object->setDetail('symbol-sources', $value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if ($old) { + $display_old = $this->renderHandleList($old); + } else { + $display_old = $this->renderValue(pht('None')); + } + + if ($new) { + $display_new = $this->renderHandleList($new); + } else { + $display_new = $this->renderValue(pht('None')); + } + + return pht( + '%s changed symbol sources from %s to %s.', + $this->renderAuthor(), + $display_old, + $display_new); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + foreach ($xactions as $xaction) { + $old = $object->getSymbolSources(); + $new = $xaction->getNewValue(); + + // If the viewer is adding new repositories, make sure they are + // valid and visible. + $add = array_diff($new, $old); + if (!$add) { + continue; + } + + $repositories = id(new PhabricatorRepositoryQuery()) + ->setViewer($this->getActor()) + ->withPHIDs($add) + ->execute(); + $repositories = mpull($repositories, null, 'getPHID'); + + foreach ($add as $phid) { + if (isset($repositories[$phid])) { + continue; + } + + $errors[] = $this->newInvalidError( + pht( + 'Repository ("%s") does not exist, or you do not have '. + 'permission to see it.', + $phid), + $xaction); + break; + } + } + + return $errors; + } + + +} diff --git a/src/applications/repository/xaction/PhabricatorRepositoryTouchLimitTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryTouchLimitTransaction.php new file mode 100644 index 0000000000..e3052d0894 --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositoryTouchLimitTransaction.php @@ -0,0 +1,76 @@ +getTouchLimit(); + } + + public function generateNewValue($object, $value) { + if (!strlen($value)) { + return null; + } + + $value = (int)$value; + if (!$value) { + return null; + } + + return $value; + } + + public function applyInternalEffects($object, $value) { + $object->setTouchLimit($value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if ($old && $new) { + return pht( + '%s changed the touch limit for this repository from %s paths to '. + '%s paths.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } else if ($new) { + return pht( + '%s set the touch limit for this repository to %s paths.', + $this->renderAuthor(), + $this->renderNewValue()); + } else { + return pht( + '%s removed the touch limit (%s paths) for this repository.', + $this->renderAuthor(), + $this->renderOldValue()); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + foreach ($xactions as $xaction) { + $new = $xaction->getNewValue(); + + if (!strlen($new)) { + continue; + } + + if (!preg_match('/^\d+\z/', $new)) { + $errors[] = $this->newInvalidError( + pht( + 'Unable to parse touch limit, specify a positive number of '. + 'paths.'), + $xaction); + continue; + } + } + + return $errors; + } + +} diff --git a/src/applications/repository/xaction/PhabricatorRepositoryTrackOnlyTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryTrackOnlyTransaction.php new file mode 100644 index 0000000000..20ff1baf08 --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositoryTrackOnlyTransaction.php @@ -0,0 +1,42 @@ +getDetail('branch-filter', array())); + } + + public function applyInternalEffects($object, $value) { + $object->setDetail('branch-filter', array_fill_keys($value, true)); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if (!$new) { + return pht( + '%s set this repository to track all branches.', + $this->renderAuthor()); + } else if (!$old) { + return pht( + '%s set this repository to track branches: %s.', + $this->renderAuthor(), + $this->renderValue(implode(', ', $new))); + } else { + return pht( + '%s changed tracked branches from %s to %s.', + $this->renderAuthor(), + $this->renderValue(implode(', ', $old)), + $this->renderValue(implode(', ', $new))); + } + } + + public function validateTransactions($object, array $xactions) { + return $this->validateRefList($object, $xactions); + } + +} diff --git a/src/applications/repository/xaction/PhabricatorRepositoryTransactionType.php b/src/applications/repository/xaction/PhabricatorRepositoryTransactionType.php new file mode 100644 index 0000000000..6a2bd44feb --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositoryTransactionType.php @@ -0,0 +1,49 @@ +getNewValue() as $pattern) { + // Check for invalid regular expressions. + $regexp = PhabricatorRepository::extractBranchRegexp($pattern); + if ($regexp !== null) { + $ok = @preg_match($regexp, ''); + if ($ok === false) { + $errors[] = $this->newInvalidError( + pht( + 'Expression "%s" is not a valid regular expression. Note '. + 'that you must include delimiters.', + $regexp), + $xaction); + continue; + } + } + + // Check for formatting mistakes like `regex(...)` instead of + // `regexp(...)`. + $matches = null; + if (preg_match('/^([^(]+)\\(.*\\)\z/', $pattern, $matches)) { + switch ($matches[1]) { + case 'regexp': + break; + default: + $errors[] = $this->newInvalidError( + pht( + 'Matching function "%s(...)" is not recognized. Valid '. + 'functions are: regexp(...).', + $matches[1]), + $xaction); + break; + } + } + } + } + + return $errors; + } + +} diff --git a/src/applications/repository/xaction/PhabricatorRepositoryVCSTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryVCSTransaction.php new file mode 100644 index 0000000000..d5944d5425 --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositoryVCSTransaction.php @@ -0,0 +1,66 @@ +getVersionControlSystem(); + } + + public function applyInternalEffects($object, $value) { + $object->setVersionControlSystem($value); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $vcs_map = PhabricatorRepositoryType::getAllRepositoryTypes(); + $current_vcs = $object->getVersionControlSystem(); + + if (!$this->isNewObject()) { + foreach ($xactions as $xaction) { + if ($xaction->getNewValue() == $current_vcs) { + continue; + } + + $errors[] = $this->newInvalidError( + pht( + 'You can not change the version control system an existing '. + 'repository uses. It can only be set when a repository is '. + 'first created.'), + $xaction); + } + + return $errors; + } + + $value = $object->getVersionControlSystem(); + + foreach ($xactions as $xaction) { + $value = $xaction->getNewValue(); + + if (isset($vcs_map[$value])) { + continue; + } + + $errors[] = $this->newInvalidError( + pht( + 'Specified version control system must be a VCS '. + 'recognized by Phabricator. Valid systems are: %s.', + implode(', ', array_keys($vcs_map))), + $xaction); + } + + if ($value === null) { + $errors[] = $this->newRequiredError( + pht( + 'When creating a repository, you must specify a valid '. + 'underlying version control system. Valid systems are: %s.', + implode(', ', array_keys($vcs_map)))); + } + + return $errors; + } +} diff --git a/src/applications/search/engineextension/PhabricatorFerretFulltextEngineExtension.php b/src/applications/search/engineextension/PhabricatorFerretFulltextEngineExtension.php index fe50518f3f..8647bc83eb 100644 --- a/src/applications/search/engineextension/PhabricatorFerretFulltextEngineExtension.php +++ b/src/applications/search/engineextension/PhabricatorFerretFulltextEngineExtension.php @@ -1,255 +1,255 @@ getPHID(); $engine = $object->newFerretEngine(); $is_closed = 0; $author_phid = null; $owner_phid = null; foreach ($document->getRelationshipData() as $relationship) { list($related_type, $related_phid) = $relationship; switch ($related_type) { case PhabricatorSearchRelationship::RELATIONSHIP_OPEN: $is_closed = 0; break; case PhabricatorSearchRelationship::RELATIONSHIP_CLOSED: $is_closed = 1; break; case PhabricatorSearchRelationship::RELATIONSHIP_OWNER: $owner_phid = $related_phid; break; case PhabricatorSearchRelationship::RELATIONSHIP_UNOWNED: $owner_phid = null; break; case PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR: $author_phid = $related_phid; break; } } $stemmer = $engine->newStemmer(); // Copy all of the "title" and "body" fields to create new "core" fields. // This allows users to search "in title or body" with the "core:" prefix. $document_fields = $document->getFieldData(); $virtual_fields = array(); foreach ($document_fields as $field) { $virtual_fields[] = $field; list($key, $raw_corpus) = $field; switch ($key) { case PhabricatorSearchDocumentFieldType::FIELD_TITLE: case PhabricatorSearchDocumentFieldType::FIELD_BODY: $virtual_fields[] = array( PhabricatorSearchDocumentFieldType::FIELD_CORE, $raw_corpus, ); break; } $virtual_fields[] = array( PhabricatorSearchDocumentFieldType::FIELD_ALL, $raw_corpus, ); } $empty_template = array( 'raw' => array(), 'term' => array(), 'normal' => array(), ); $ferret_corpus_map = array(); foreach ($virtual_fields as $field) { list($key, $raw_corpus) = $field; if (!strlen($raw_corpus)) { continue; } $term_corpus = $engine->newTermsCorpus($raw_corpus); $normal_corpus = $stemmer->stemCorpus($raw_corpus); $normal_corpus = $engine->newTermsCorpus($normal_corpus); if (!isset($ferret_corpus_map[$key])) { $ferret_corpus_map[$key] = $empty_template; } $ferret_corpus_map[$key]['raw'][] = $raw_corpus; $ferret_corpus_map[$key]['term'][] = $term_corpus; $ferret_corpus_map[$key]['normal'][] = $normal_corpus; } $ferret_fields = array(); $ngrams_source = array(); foreach ($ferret_corpus_map as $key => $fields) { $raw_corpus = $fields['raw']; $raw_corpus = implode("\n", $raw_corpus); if (strlen($raw_corpus)) { $ngrams_source[] = $raw_corpus; } $normal_corpus = $fields['normal']; $normal_corpus = implode("\n", $normal_corpus); if (strlen($normal_corpus)) { $ngrams_source[] = $normal_corpus; } $term_corpus = $fields['term']; $term_corpus = implode("\n", $term_corpus); if (strlen($term_corpus)) { $ngrams_source[] = $term_corpus; } $ferret_fields[] = array( 'fieldKey' => $key, 'rawCorpus' => $raw_corpus, 'termCorpus' => $term_corpus, 'normalCorpus' => $normal_corpus, ); } $ngrams_source = implode("\n", $ngrams_source); $ngrams = $engine->getTermNgramsFromString($ngrams_source); $object->openTransaction(); try { $conn = $object->establishConnection('w'); $this->deleteOldDocument($engine, $object, $document); queryfx( $conn, 'INSERT INTO %T (objectPHID, isClosed, epochCreated, epochModified, authorPHID, ownerPHID) VALUES (%s, %d, %d, %d, %ns, %ns)', $engine->getDocumentTableName(), $object->getPHID(), $is_closed, $document->getDocumentCreated(), $document->getDocumentModified(), $author_phid, $owner_phid); $document_id = $conn->getInsertID(); foreach ($ferret_fields as $ferret_field) { queryfx( $conn, 'INSERT INTO %T (documentID, fieldKey, rawCorpus, termCorpus, normalCorpus) VALUES (%d, %s, %s, %s, %s)', $engine->getFieldTableName(), $document_id, $ferret_field['fieldKey'], $ferret_field['rawCorpus'], $ferret_field['termCorpus'], $ferret_field['normalCorpus']); } if ($ngrams) { $common = queryfx_all( $conn, 'SELECT ngram FROM %T WHERE ngram IN (%Ls)', $engine->getCommonNgramsTableName(), $ngrams); $common = ipull($common, 'ngram', 'ngram'); foreach ($ngrams as $key => $ngram) { if (isset($common[$ngram])) { unset($ngrams[$key]); continue; } // NOTE: MySQL discards trailing whitespace in CHAR(X) columns. $trim_ngram = rtrim($ngram, ' '); if (isset($common[$ngram])) { unset($ngrams[$key]); continue; } } } if ($ngrams) { $sql = array(); foreach ($ngrams as $ngram) { $sql[] = qsprintf( $conn, '(%d, %s)', $document_id, $ngram); } foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) { queryfx( $conn, - 'INSERT INTO %T (documentID, ngram) VALUES %Q', + 'INSERT INTO %T (documentID, ngram) VALUES %LQ', $engine->getNgramsTableName(), $chunk); } } } catch (Exception $ex) { $object->killTransaction(); throw $ex; } $object->saveTransaction(); } private function deleteOldDocument( PhabricatorFerretEngine $engine, $object, PhabricatorSearchAbstractDocument $document) { $conn = $object->establishConnection('w'); $old_document = queryfx_one( $conn, 'SELECT * FROM %T WHERE objectPHID = %s', $engine->getDocumentTableName(), $object->getPHID()); if (!$old_document) { return; } $old_id = $old_document['id']; queryfx( $conn, 'DELETE FROM %T WHERE id = %d', $engine->getDocumentTableName(), $old_id); queryfx( $conn, 'DELETE FROM %T WHERE documentID = %d', $engine->getFieldTableName(), $old_id); queryfx( $conn, 'DELETE FROM %T WHERE documentID = %d', $engine->getNgramsTableName(), $old_id); } } diff --git a/src/applications/search/index/PhabricatorIndexEngine.php b/src/applications/search/index/PhabricatorIndexEngine.php index 1dde3ce9ab..1e1781f169 100644 --- a/src/applications/search/index/PhabricatorIndexEngine.php +++ b/src/applications/search/index/PhabricatorIndexEngine.php @@ -1,150 +1,150 @@ parameters = $parameters; return $this; } public function getParameters() { return $this->parameters; } public function setObject($object) { $this->object = $object; return $this; } public function getObject() { return $this->object; } public function shouldIndexObject() { $extensions = $this->newExtensions(); $parameters = $this->getParameters(); foreach ($extensions as $extension) { $extension->setParameters($parameters); } $object = $this->getObject(); $versions = array(); foreach ($extensions as $key => $extension) { $version = $extension->getIndexVersion($object); if ($version !== null) { $versions[$key] = (string)$version; } } if (idx($parameters, 'force')) { $current_versions = array(); } else { $keys = array_keys($versions); $current_versions = $this->loadIndexVersions($keys); } foreach ($versions as $key => $version) { $current_version = idx($current_versions, $key); if ($current_version === null) { continue; } // If nothing has changed since we built the current index, we do not // need to rebuild the index. if ($current_version === $version) { unset($extensions[$key]); } } $this->extensions = $extensions; $this->versions = $versions; // We should index the object only if there is any work to be done. return (bool)$this->extensions; } public function indexObject() { $extensions = $this->extensions; $object = $this->getObject(); foreach ($extensions as $key => $extension) { $extension->indexObject($this, $object); } $this->saveIndexVersions($this->versions); return $this; } private function newExtensions() { $object = $this->getObject(); $extensions = PhabricatorIndexEngineExtension::getAllExtensions(); foreach ($extensions as $key => $extension) { if (!$extension->shouldIndexObject($object)) { unset($extensions[$key]); } } return $extensions; } private function loadIndexVersions(array $extension_keys) { if (!$extension_keys) { return array(); } $object = $this->getObject(); $object_phid = $object->getPHID(); $table = new PhabricatorSearchIndexVersion(); $conn_r = $table->establishConnection('w'); $rows = queryfx_all( $conn_r, 'SELECT * FROM %T WHERE objectPHID = %s AND extensionKey IN (%Ls)', $table->getTableName(), $object_phid, $extension_keys); return ipull($rows, 'version', 'extensionKey'); } private function saveIndexVersions(array $versions) { if (!$versions) { return; } $object = $this->getObject(); $object_phid = $object->getPHID(); $table = new PhabricatorSearchIndexVersion(); $conn_w = $table->establishConnection('w'); $sql = array(); foreach ($versions as $key => $version) { $sql[] = qsprintf( $conn_w, '(%s, %s, %s)', $object_phid, $key, $version); } queryfx( $conn_w, 'INSERT INTO %T (objectPHID, extensionKey, version) - VALUES %Q + VALUES %LQ ON DUPLICATE KEY UPDATE version = VALUES(version)', $table->getTableName(), - implode(', ', $sql)); + $sql); } } diff --git a/src/applications/search/management/PhabricatorSearchManagementNgramsWorkflow.php b/src/applications/search/management/PhabricatorSearchManagementNgramsWorkflow.php index 1a1124b4d8..3d87378849 100644 --- a/src/applications/search/management/PhabricatorSearchManagementNgramsWorkflow.php +++ b/src/applications/search/management/PhabricatorSearchManagementNgramsWorkflow.php @@ -1,139 +1,139 @@ setName('ngrams') ->setSynopsis( pht( 'Recompute common ngrams. This is an advanced workflow that '. 'can harm search quality if used improperly.')) ->setArguments( array( array( 'name' => 'reset', 'help' => pht('Reset all common ngram records.'), ), array( 'name' => 'threshold', 'param' => 'threshold', 'help' => pht( 'Prune ngrams present in more than this fraction of '. 'documents. Provide a value between 0.0 and 1.0.'), ), )); } public function execute(PhutilArgumentParser $args) { $min_documents = 4096; $is_reset = $args->getArg('reset'); $threshold = $args->getArg('threshold'); if ($is_reset && $threshold !== null) { throw new PhutilArgumentUsageException( pht('Specify either --reset or --threshold, not both.')); } if (!$is_reset && $threshold === null) { throw new PhutilArgumentUsageException( pht('Specify either --reset or --threshold.')); } if (!$is_reset) { if (!is_numeric($threshold)) { throw new PhutilArgumentUsageException( pht('Specify a numeric threshold between 0 and 1.')); } $threshold = (double)$threshold; if ($threshold <= 0 || $threshold >= 1) { throw new PhutilArgumentUsageException( pht('Threshold must be greater than 0.0 and less than 1.0.')); } } $all_objects = id(new PhutilClassMapQuery()) ->setAncestorClass('PhabricatorFerretInterface') ->execute(); foreach ($all_objects as $object) { $engine = $object->newFerretEngine(); $conn = $object->establishConnection('w'); $display_name = get_class($object); if ($is_reset) { echo tsprintf( "%s\n", pht( 'Resetting common ngrams for "%s".', $display_name)); queryfx( $conn, 'DELETE FROM %T', $engine->getCommonNgramsTableName()); continue; } $document_count = queryfx_one( $conn, 'SELECT COUNT(*) N FROM %T', $engine->getDocumentTableName()); $document_count = $document_count['N']; if ($document_count < $min_documents) { echo tsprintf( "%s\n", pht( 'Too few documents of type "%s" for any ngrams to be common.', $display_name)); continue; } $min_frequency = (int)ceil($document_count * $threshold); $common_ngrams = queryfx_all( $conn, 'SELECT ngram, COUNT(*) N FROM %T GROUP BY ngram HAVING N >= %d', $engine->getNgramsTableName(), $min_frequency); if (!$common_ngrams) { echo tsprintf( "%s\n", pht( 'No new common ngrams exist for "%s".', $display_name)); continue; } $sql = array(); foreach ($common_ngrams as $ngram) { $sql[] = qsprintf( $conn, '(%s, 1)', $ngram['ngram']); } foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) { queryfx( $conn, 'INSERT IGNORE INTO %T (ngram, needsCollection) - VALUES %Q', + VALUES %LQ', $engine->getCommonNgramsTableName(), $chunk); } echo tsprintf( "%s\n", pht( 'Updated common ngrams for "%s".', $display_name)); } } } diff --git a/src/applications/search/ngrams/PhabricatorSearchNgrams.php b/src/applications/search/ngrams/PhabricatorSearchNgrams.php index 9ff8157e9e..fc55af5eb0 100644 --- a/src/applications/search/ngrams/PhabricatorSearchNgrams.php +++ b/src/applications/search/ngrams/PhabricatorSearchNgrams.php @@ -1,113 +1,113 @@ value = $value; return $this; } final public function getValue() { return $this->value; } protected function getConfiguration() { return array( self::CONFIG_TIMESTAMPS => false, self::CONFIG_COLUMN_SCHEMA => array( 'objectID' => 'uint32', 'ngram' => 'char3', ), self::CONFIG_KEY_SCHEMA => array( 'key_ngram' => array( 'columns' => array('ngram', 'objectID'), ), 'key_object' => array( 'columns' => array('objectID'), ), ), ) + parent::getConfiguration(); } public function getTableName() { $application = $this->getApplicationName(); $key = $this->getNgramKey(); return "{$application}_{$key}_ngrams"; } final public function tokenizeString($value) { $value = trim($value, ' '); $value = preg_split('/ +/', $value); return $value; } final public function getNgramsFromString($value, $mode) { $tokens = $this->tokenizeString($value); $ngrams = array(); foreach ($tokens as $token) { $token = phutil_utf8_strtolower($token); switch ($mode) { case 'query': break; case 'index': $token = ' '.$token.' '; break; case 'prefix': $token = ' '.$token; break; } $len = (strlen($token) - 2); for ($ii = 0; $ii < $len; $ii++) { $ngram = substr($token, $ii, 3); $ngrams[$ngram] = $ngram; } } ksort($ngrams); return array_keys($ngrams); } final public function writeNgram($object_id) { $ngrams = $this->getNgramsFromString($this->getValue(), 'index'); $conn_w = $this->establishConnection('w'); $sql = array(); foreach ($ngrams as $ngram) { $sql[] = qsprintf( $conn_w, '(%d, %s)', $object_id, $ngram); } queryfx( $conn_w, 'DELETE FROM %T WHERE objectID = %d', $this->getTableName(), $object_id); if ($sql) { queryfx( $conn_w, - 'INSERT INTO %T (objectID, ngram) VALUES %Q', + 'INSERT INTO %T (objectID, ngram) VALUES %LQ', $this->getTableName(), - implode(', ', $sql)); + $sql); } return $this; } } diff --git a/src/applications/search/query/PhabricatorSavedQueryQuery.php b/src/applications/search/query/PhabricatorSavedQueryQuery.php index 623b001662..765c751940 100644 --- a/src/applications/search/query/PhabricatorSavedQueryQuery.php +++ b/src/applications/search/query/PhabricatorSavedQueryQuery.php @@ -1,73 +1,73 @@ ids = $ids; return $this; } public function withEngineClassNames(array $engine_class_names) { $this->engineClassNames = $engine_class_names; return $this; } public function withQueryKeys(array $query_keys) { $this->queryKeys = $query_keys; return $this; } protected function loadPage() { $table = new PhabricatorSavedQuery(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->engineClassNames !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'engineClassName IN (%Ls)', $this->engineClassNames); } if ($this->queryKeys !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'queryKey IN (%Ls)', $this->queryKeys); } - $where[] = $this->buildPagingClause($conn_r); + $where[] = $this->buildPagingClause($conn); - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } public function getQueryApplicationClass() { return 'PhabricatorSearchApplication'; } } diff --git a/src/applications/system/engine/PhabricatorSystemActionEngine.php b/src/applications/system/engine/PhabricatorSystemActionEngine.php index b6a8e26711..6b8352a29e 100644 --- a/src/applications/system/engine/PhabricatorSystemActionEngine.php +++ b/src/applications/system/engine/PhabricatorSystemActionEngine.php @@ -1,201 +1,201 @@ List of actors. * @param PhabricatorSystemAction Action being taken. * @param float Score or credit, see above. * @return void */ public static function willTakeAction( array $actors, PhabricatorSystemAction $action, $score) { // If the score for this action is negative, we're giving the user a credit, // so don't bother checking if they're blocked or not. if ($score >= 0) { $blocked = self::loadBlockedActors($actors, $action, $score); if ($blocked) { foreach ($blocked as $actor => $actor_score) { throw new PhabricatorSystemActionRateLimitException( $action, $actor_score); } } } if ($score != 0) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); self::recordAction($actors, $action, $score); unset($unguarded); } } public static function loadBlockedActors( array $actors, PhabricatorSystemAction $action, $score) { $scores = self::loadScores($actors, $action); $window = self::getWindow(); $blocked = array(); foreach ($scores as $actor => $actor_score) { // For the purposes of checking for a block, we just use the raw // persistent score and do not include the score for this action. This // allows callers to test for a block without adding any points and get // the same result they would if they were adding points: we only // trigger a rate limit when the persistent score exceeds the threshold. if ($action->shouldBlockActor($actor, $actor_score)) { // When reporting the results, we do include the points for this // action. This makes the error messages more clear, since they // more accurately report the number of actions the user has really // tried to take. $blocked[$actor] = $actor_score + ($score / $window); } } return $blocked; } public static function loadScores( array $actors, PhabricatorSystemAction $action) { if (!$actors) { return array(); } $actor_hashes = array(); foreach ($actors as $actor) { $actor_hashes[] = PhabricatorHash::digestForIndex($actor); } $log = new PhabricatorSystemActionLog(); $window = self::getWindow(); $conn_r = $log->establishConnection('r'); $scores = queryfx_all( $conn_r, 'SELECT actorIdentity, SUM(score) totalScore FROM %T WHERE action = %s AND actorHash IN (%Ls) AND epoch >= %d GROUP BY actorHash', $log->getTableName(), $action->getActionConstant(), $actor_hashes, (time() - $window)); $scores = ipull($scores, 'totalScore', 'actorIdentity'); foreach ($scores as $key => $score) { $scores[$key] = $score / $window; } $scores = $scores + array_fill_keys($actors, 0); return $scores; } private static function recordAction( array $actors, PhabricatorSystemAction $action, $score) { $log = new PhabricatorSystemActionLog(); $conn_w = $log->establishConnection('w'); $sql = array(); foreach ($actors as $actor) { $sql[] = qsprintf( $conn_w, '(%s, %s, %s, %f, %d)', PhabricatorHash::digestForIndex($actor), $actor, $action->getActionConstant(), $score, time()); } foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) { queryfx( $conn_w, 'INSERT INTO %T (actorHash, actorIdentity, action, score, epoch) - VALUES %Q', + VALUES %LQ', $log->getTableName(), $chunk); } } private static function getWindow() { // Limit queries to the last hour of data so we don't need to look at as // many rows. We can use an arbitrarily larger window instead (we normalize // scores to actions per second) but all the actions we care about limiting // have a limit much higher than one action per hour. return phutil_units('1 hour in seconds'); } /** * Reset all action counts for actions taken by some set of actors in the * previous action window. * * @param list Actors to reset counts for. * @return int Number of actions cleared. */ public static function resetActions(array $actors) { $log = new PhabricatorSystemActionLog(); $conn_w = $log->establishConnection('w'); $now = PhabricatorTime::getNow(); $hashes = array(); foreach ($actors as $actor) { $hashes[] = PhabricatorHash::digestForIndex($actor); } queryfx( $conn_w, 'DELETE FROM %T WHERE actorHash IN (%Ls) AND epoch BETWEEN %d AND %d', $log->getTableName(), $hashes, $now - self::getWindow(), $now); return $conn_w->getAffectedRows(); } } diff --git a/src/applications/tokens/query/PhabricatorTokenCountQuery.php b/src/applications/tokens/query/PhabricatorTokenCountQuery.php index 64333715fd..c4694af607 100644 --- a/src/applications/tokens/query/PhabricatorTokenCountQuery.php +++ b/src/applications/tokens/query/PhabricatorTokenCountQuery.php @@ -1,40 +1,40 @@ objectPHIDs = $object_phids; return $this; } public function execute() { $table = new PhabricatorTokenCount(); $conn_r = $table->establishConnection('r'); $rows = queryfx_all( $conn_r, 'SELECT objectPHID, tokenCount FROM %T %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildLimitClause($conn_r)); return ipull($rows, 'tokenCount', 'objectPHID'); } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->objectPHIDs) { $where[] = qsprintf( - $conn_r, + $conn, 'objectPHID IN (%Ls)', $this->objectPHIDs); } - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } } diff --git a/src/applications/transactions/commentaction/PhabricatorEditEngineCommentAction.php b/src/applications/transactions/commentaction/PhabricatorEditEngineCommentAction.php index 4d3b9ca5df..d363e5a14e 100644 --- a/src/applications/transactions/commentaction/PhabricatorEditEngineCommentAction.php +++ b/src/applications/transactions/commentaction/PhabricatorEditEngineCommentAction.php @@ -1,84 +1,94 @@ key = $key; return $this; } public function getKey() { return $this->key; } public function setGroupKey($group_key) { $this->groupKey = $group_key; return $this; } public function getGroupKey() { return $this->groupKey; } public function setConflictKey($conflict_key) { $this->conflictKey = $conflict_key; return $this; } public function getConflictKey() { return $this->conflictKey; } public function setLabel($label) { $this->label = $label; return $this; } public function getLabel() { return $this->label; } public function setValue($value) { $this->value = $value; return $this; } public function getValue() { return $this->value; } public function setOrder($order) { $this->order = $order; return $this; } public function getOrder() { return $this->order; } public function getSortVector() { return id(new PhutilSortVector()) ->addInt($this->getOrder()); } public function setInitialValue($initial_value) { $this->initialValue = $initial_value; return $this; } public function getInitialValue() { return $this->initialValue; } + public function setSubmitButtonText($text) { + $this->submitButtonText = $text; + return $this; + } + + public function getSubmitButtonText() { + return $this->submitButtonText; + } + } diff --git a/src/applications/transactions/controller/PhabricatorApplicationTransactionValueController.php b/src/applications/transactions/controller/PhabricatorApplicationTransactionValueController.php index 553bfa3e10..bef6fef5a8 100644 --- a/src/applications/transactions/controller/PhabricatorApplicationTransactionValueController.php +++ b/src/applications/transactions/controller/PhabricatorApplicationTransactionValueController.php @@ -1,145 +1,145 @@ getViewer(); $phid = $request->getURIData('phid'); $type = $request->getURIData('value'); $xaction = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs(array($phid)) ->executeOne(); if (!$xaction) { return new Aphront404Response(); } // For now, this pathway only supports policy transactions // to show the details of custom policies. If / when this pathway // supports more transaction types, rendering coding should be moved // into PhabricatorTransactions e.g. feed rendering code. // TODO: This should be some kind of "hey do you support this?" thing on // the transactions themselves. switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: - case PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY: + case PhabricatorRepositoryPushPolicyTransaction::TRANSACTIONTYPE: break; default: return new Aphront404Response(); break; } if ($type == 'old') { $value = $xaction->getOldValue(); } else { $value = $xaction->getNewValue(); } $policy = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->withPHIDs(array($value)) ->executeOne(); if (!$policy) { return new Aphront404Response(); } if ($policy->getType() != PhabricatorPolicyType::TYPE_CUSTOM) { return new Aphront404Response(); } $rule_objects = array(); foreach ($policy->getCustomRuleClasses() as $class) { $rule_objects[$class] = newv($class, array()); } $policy->attachRuleObjects($rule_objects); $this->requireResource('policy-transaction-detail-css'); $cancel_uri = $this->guessCancelURI($viewer, $xaction); return $this->newDialog() ->setTitle($policy->getFullName()) ->setWidth(AphrontDialogView::WIDTH_FORM) ->appendChild($this->renderPolicyDetails($policy, $rule_objects)) ->addCancelButton($cancel_uri, pht('Close')); } private function extractPHIDs( PhabricatorPolicy $policy, array $rule_objects) { $phids = array(); foreach ($policy->getRules() as $rule) { $rule_object = $rule_objects[$rule['rule']]; $phids[] = $rule_object->getRequiredHandlePHIDsForSummary($rule['value']); } return array_filter(array_mergev($phids)); } private function renderPolicyDetails( PhabricatorPolicy $policy, array $rule_objects) { $details = array(); $details[] = phutil_tag( 'p', array( 'class' => 'policy-transaction-detail-intro', ), pht('These rules are processed in order:')); foreach ($policy->getRules() as $index => $rule) { $rule_object = $rule_objects[$rule['rule']]; if ($rule['action'] == 'allow') { $icon = 'fa-check-circle green'; } else { $icon = 'fa-minus-circle red'; } $icon = id(new PHUIIconView()) ->setIcon($icon) ->setText( ucfirst($rule['action']).' '.$rule_object->getRuleDescription()); $handle_phids = $rule_object->getRequiredHandlePHIDsForSummary( $rule['value']); if ($handle_phids) { $value = $this->getViewer() ->renderHandleList($handle_phids) ->setAsInline(true); } else { $value = $rule['value']; } $details[] = phutil_tag('div', array( 'class' => 'policy-transaction-detail-row', ), array( $icon, $value, )); } $details[] = phutil_tag( 'p', array( 'class' => 'policy-transaction-detail-end', ), pht( 'If no rules match, %s all other users.', phutil_tag('b', array(), $policy->getDefaultAction()))); return $details; } } diff --git a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultsController.php b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultsController.php index 340431dd19..f7361d50cf 100644 --- a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultsController.php +++ b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultsController.php @@ -1,123 +1,128 @@ getURIData('engineKey'); $this->setEngineKey($engine_key); $key = $request->getURIData('key'); $viewer = $this->getViewer(); $config = id(new PhabricatorEditEngineConfigurationQuery()) ->setViewer($viewer) ->withEngineKeys(array($engine_key)) ->withIdentifiers(array($key)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$config) { return id(new Aphront404Response()); } $cancel_uri = "/transactions/editengine/{$engine_key}/view/{$key}/"; $engine = $config->getEngine(); $fields = $engine->getFieldsForConfig($config); foreach ($fields as $key => $field) { + if (!$field->getIsFormField()) { + unset($fields[$key]); + continue; + } + if (!$field->getIsDefaultable()) { unset($fields[$key]); continue; } } foreach ($fields as $field) { $field->setIsEditDefaults(true); } if ($request->isFormPost()) { $xactions = array(); foreach ($fields as $field) { $field->readValueFromSubmit($request); } $type = PhabricatorEditEngineConfigurationTransaction::TYPE_DEFAULT; $xactions = array(); foreach ($fields as $field) { $new_value = $field->getValueForDefaults(); $xactions[] = id(new PhabricatorEditEngineConfigurationTransaction()) ->setTransactionType($type) ->setMetadataValue('field.key', $field->getKey()) ->setNewValue($new_value); } $editor = id(new PhabricatorEditEngineConfigurationEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true); $editor->applyTransactions($config, $xactions); return id(new AphrontRedirectResponse()) ->setURI($cancel_uri); } $title = pht('Edit Form Defaults'); $form = id(new AphrontFormView()) ->setUser($viewer); foreach ($fields as $field) { $field->appendToForm($form); } $form ->appendControl( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Defaults')) ->addCancelButton($cancel_uri)); $info = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setErrors( array( pht('You are editing the default values for this form.'), )); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Form')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Form %d', $config->getID()), $cancel_uri); $crumbs->addTextCrumb(pht('Edit Defaults')); $crumbs->setBorder(true); $header = id(new PHUIHeaderView()) ->setHeader(pht('Edit Form Defaults')) ->setHeaderIcon('fa-pencil'); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter(array( $info, $box, )); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild($view); } } diff --git a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationLockController.php b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationLockController.php index 790eaccb47..34b099b9f0 100644 --- a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationLockController.php +++ b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationLockController.php @@ -1,117 +1,121 @@ getURIData('engineKey'); $this->setEngineKey($engine_key); $key = $request->getURIData('key'); $viewer = $this->getViewer(); $config = id(new PhabricatorEditEngineConfigurationQuery()) ->setViewer($viewer) ->withEngineKeys(array($engine_key)) ->withIdentifiers(array($key)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$config) { return id(new Aphront404Response()); } $cancel_uri = "/transactions/editengine/{$engine_key}/view/{$key}/"; if ($request->isFormPost()) { $xactions = array(); $locks = $request->getArr('locks'); $type_locks = PhabricatorEditEngineConfigurationTransaction::TYPE_LOCKS; $xactions[] = id(new PhabricatorEditEngineConfigurationTransaction()) ->setTransactionType($type_locks) ->setNewValue($locks); $editor = id(new PhabricatorEditEngineConfigurationEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true); $editor->applyTransactions($config, $xactions); return id(new AphrontRedirectResponse()) ->setURI($cancel_uri); } $engine = $config->getEngine(); $fields = $engine->getFieldsForConfig($config); $help = pht(<<setUser($viewer) ->appendRemarkupInstructions($help); $locks = $config->getFieldLocks(); $lock_visible = PhabricatorEditEngineConfiguration::LOCK_VISIBLE; $lock_locked = PhabricatorEditEngineConfiguration::LOCK_LOCKED; $lock_hidden = PhabricatorEditEngineConfiguration::LOCK_HIDDEN; $map = array( $lock_visible => pht('Visible'), $lock_locked => pht("\xF0\x9F\x94\x92 Locked"), $lock_hidden => pht("\xE2\x9C\x98 Hidden"), ); foreach ($fields as $field) { + if (!$field->getIsFormField()) { + continue; + } + if (!$field->getIsLockable()) { continue; } $key = $field->getKey(); $label = $field->getLabel(); if (!strlen($label)) { $label = $key; } if ($field->getIsHidden()) { $value = $lock_hidden; } else if ($field->getIsLocked()) { $value = $lock_locked; } else { $value = $lock_visible; } $form->appendControl( id(new AphrontFormSelectControl()) ->setName('locks['.$key.']') ->setLabel($label) ->setValue($value) ->setOptions($map)); } return $this->newDialog() ->setTitle(pht('Lock / Hide Fields')) ->setWidth(AphrontDialogView::WIDTH_FORM) ->appendForm($form) ->addSubmitButton(pht('Save Changes')) ->addCancelButton($cancel_uri); } } diff --git a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationReorderController.php b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationReorderController.php index 15eb9530fd..6ff36cdfa4 100644 --- a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationReorderController.php +++ b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationReorderController.php @@ -1,123 +1,127 @@ getURIData('engineKey'); $this->setEngineKey($engine_key); $key = $request->getURIData('key'); $viewer = $this->getViewer(); $config = id(new PhabricatorEditEngineConfigurationQuery()) ->setViewer($viewer) ->withEngineKeys(array($engine_key)) ->withIdentifiers(array($key)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$config) { return id(new Aphront404Response()); } $cancel_uri = "/transactions/editengine/{$engine_key}/view/{$key}/"; $reorder_uri = "/transactions/editengine/{$engine_key}/reorder/{$key}/"; if ($request->isFormPost()) { $xactions = array(); $key_order = $request->getStrList('keyOrder'); $type_order = PhabricatorEditEngineConfigurationTransaction::TYPE_ORDER; $xactions[] = id(new PhabricatorEditEngineConfigurationTransaction()) ->setTransactionType($type_order) ->setNewValue($key_order); $editor = id(new PhabricatorEditEngineConfigurationEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true); $editor->applyTransactions($config, $xactions); return id(new AphrontRedirectResponse()) ->setURI($cancel_uri); } $engine = $config->getEngine(); $fields = $engine->getFieldsForConfig($config); $list_id = celerity_generate_unique_node_id(); $input_id = celerity_generate_unique_node_id(); $list = id(new PHUIObjectItemListView()) ->setUser($viewer) ->setID($list_id) ->setFlush(true); $key_order = array(); foreach ($fields as $field) { + if (!$field->getIsFormField()) { + continue; + } + if (!$field->getIsReorderable()) { continue; } $label = $field->getLabel(); $key = $field->getKey(); if ($label !== null) { $header = $label; } else { $header = $key; } $item = id(new PHUIObjectItemView()) ->setHeader($header) ->setGrippable(true) ->addSigil('editengine-form-field') ->setMetadata( array( 'fieldKey' => $key, )); $list->addItem($item); $key_order[] = $key; } Javelin::initBehavior( 'editengine-reorder-fields', array( 'listID' => $list_id, 'inputID' => $input_id, 'reorderURI' => $reorder_uri, )); $note = id(new PHUIInfoView()) ->appendChild(pht('Drag and drop fields to reorder them.')) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE); $input = phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'keyOrder', 'value' => implode(', ', $key_order), 'id' => $input_id, )); return $this->newDialog() ->setTitle(pht('Reorder Fields')) ->setWidth(AphrontDialogView::WIDTH_FORM) ->appendChild($note) ->appendChild($list) ->appendChild($input) ->addSubmitButton(pht('Save Changes')) ->addCancelButton($cancel_uri); } } diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php index 579b53a989..4e189df164 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngine.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php @@ -1,2618 +1,2622 @@ viewer = $viewer; return $this; } final public function getViewer() { return $this->viewer; } final public function setController(PhabricatorController $controller) { $this->controller = $controller; $this->setViewer($controller->getViewer()); return $this; } final public function getController() { return $this->controller; } final public function getEngineKey() { $key = $this->getPhobjectClassConstant('ENGINECONST', 64); if (strpos($key, '/') !== false) { throw new Exception( pht( 'EditEngine ("%s") contains an invalid key character "/".', get_class($this))); } return $key; } final public function getApplication() { $app_class = $this->getEngineApplicationClass(); return PhabricatorApplication::getByClass($app_class); } final public function addContextParameter($key) { $this->contextParameters[] = $key; return $this; } public function isEngineConfigurable() { return true; } public function isEngineExtensible() { return true; } public function isDefaultQuickCreateEngine() { return false; } public function getDefaultQuickCreateFormKeys() { $keys = array(); if ($this->isDefaultQuickCreateEngine()) { $keys[] = self::EDITENGINECONFIG_DEFAULT; } foreach ($keys as $idx => $key) { $keys[$idx] = $this->getEngineKey().'/'.$key; } return $keys; } public static function splitFullKey($full_key) { return explode('/', $full_key, 2); } public function getQuickCreateOrderVector() { return id(new PhutilSortVector()) ->addString($this->getObjectCreateShortText()); } /** * Force the engine to edit a particular object. */ public function setTargetObject($target_object) { $this->targetObject = $target_object; return $this; } public function getTargetObject() { return $this->targetObject; } public function setNavigation(AphrontSideNavFilterView $navigation) { $this->navigation = $navigation; return $this; } public function getNavigation() { return $this->navigation; } /* -( Managing Fields )---------------------------------------------------- */ abstract public function getEngineApplicationClass(); abstract protected function buildCustomEditFields($object); public function getFieldsForConfig( PhabricatorEditEngineConfiguration $config) { $object = $this->newEditableObject(); $this->editEngineConfiguration = $config; // This is mostly making sure that we fill in default values. $this->setIsCreate(true); return $this->buildEditFields($object); } final protected function buildEditFields($object) { $viewer = $this->getViewer(); $fields = $this->buildCustomEditFields($object); foreach ($fields as $field) { $field ->setViewer($viewer) ->setObject($object); } $fields = mpull($fields, null, 'getKey'); if ($this->isEngineExtensible()) { $extensions = PhabricatorEditEngineExtension::getAllEnabledExtensions(); } else { $extensions = array(); } foreach ($extensions as $extension) { $extension->setViewer($viewer); if (!$extension->supportsObject($this, $object)) { continue; } $extension_fields = $extension->buildCustomEditFields($this, $object); // TODO: Validate this in more detail with a more tailored error. assert_instances_of($extension_fields, 'PhabricatorEditField'); foreach ($extension_fields as $field) { $field ->setViewer($viewer) ->setObject($object); $group_key = $field->getBulkEditGroupKey(); if ($group_key === null) { $field->setBulkEditGroupKey('extension'); } } $extension_fields = mpull($extension_fields, null, 'getKey'); foreach ($extension_fields as $key => $field) { $fields[$key] = $field; } } $config = $this->getEditEngineConfiguration(); $fields = $this->willConfigureFields($object, $fields); $fields = $config->applyConfigurationToFields($this, $object, $fields); $fields = $this->applyPageToFields($object, $fields); return $fields; } protected function willConfigureFields($object, array $fields) { return $fields; } final public function supportsSubtypes() { try { $object = $this->newEditableObject(); } catch (Exception $ex) { return false; } return ($object instanceof PhabricatorEditEngineSubtypeInterface); } final public function newSubtypeMap() { return $this->newEditableObject()->newEditEngineSubtypeMap(); } /* -( Display Text )------------------------------------------------------- */ /** * @task text */ abstract public function getEngineName(); /** * @task text */ abstract protected function getObjectCreateTitleText($object); /** * @task text */ protected function getFormHeaderText($object) { $config = $this->getEditEngineConfiguration(); return $config->getName(); } /** * @task text */ abstract protected function getObjectEditTitleText($object); /** * @task text */ abstract protected function getObjectCreateShortText(); /** * @task text */ abstract protected function getObjectName(); /** * @task text */ abstract protected function getObjectEditShortText($object); /** * @task text */ protected function getObjectCreateButtonText($object) { return $this->getObjectCreateTitleText($object); } /** * @task text */ protected function getObjectEditButtonText($object) { return pht('Save Changes'); } /** * @task text */ protected function getCommentViewSeriousHeaderText($object) { return pht('Take Action'); } /** * @task text */ protected function getCommentViewSeriousButtonText($object) { return pht('Submit'); } /** * @task text */ protected function getCommentViewHeaderText($object) { return $this->getCommentViewSeriousHeaderText($object); } /** * @task text */ protected function getCommentViewButtonText($object) { return $this->getCommentViewSeriousButtonText($object); } /** * @task text */ protected function getPageHeader($object) { return null; } /** * Return a human-readable header describing what this engine is used to do, * like "Configure Maniphest Task Forms". * * @return string Human-readable description of the engine. * @task text */ abstract public function getSummaryHeader(); /** * Return a human-readable summary of what this engine is used to do. * * @return string Human-readable description of the engine. * @task text */ abstract public function getSummaryText(); /* -( Edit Engine Configuration )------------------------------------------ */ protected function supportsEditEngineConfiguration() { return true; } final protected function getEditEngineConfiguration() { return $this->editEngineConfiguration; } private function newConfigurationQuery() { return id(new PhabricatorEditEngineConfigurationQuery()) ->setViewer($this->getViewer()) ->withEngineKeys(array($this->getEngineKey())); } private function loadEditEngineConfigurationWithQuery( PhabricatorEditEngineConfigurationQuery $query, $sort_method) { if ($sort_method) { $results = $query->execute(); $results = msort($results, $sort_method); $result = head($results); } else { $result = $query->executeOne(); } if (!$result) { return null; } $this->editEngineConfiguration = $result; return $result; } private function loadEditEngineConfigurationWithIdentifier($identifier) { $query = $this->newConfigurationQuery() ->withIdentifiers(array($identifier)); return $this->loadEditEngineConfigurationWithQuery($query, null); } private function loadDefaultConfiguration() { $query = $this->newConfigurationQuery() ->withIdentifiers( array( self::EDITENGINECONFIG_DEFAULT, )) ->withIgnoreDatabaseConfigurations(true); return $this->loadEditEngineConfigurationWithQuery($query, null); } private function loadDefaultCreateConfiguration() { $query = $this->newConfigurationQuery() ->withIsDefault(true) ->withIsDisabled(false); return $this->loadEditEngineConfigurationWithQuery( $query, 'getCreateSortKey'); } public function loadDefaultEditConfiguration($object) { $query = $this->newConfigurationQuery() ->withIsEdit(true) ->withIsDisabled(false); // If this object supports subtyping, we edit it with a form of the same // subtype: so "bug" tasks get edited with "bug" forms. if ($object instanceof PhabricatorEditEngineSubtypeInterface) { $query->withSubtypes( array( $object->getEditEngineSubtype(), )); } return $this->loadEditEngineConfigurationWithQuery( $query, 'getEditSortKey'); } final public function getBuiltinEngineConfigurations() { $configurations = $this->newBuiltinEngineConfigurations(); if (!$configurations) { throw new Exception( pht( 'EditEngine ("%s") returned no builtin engine configurations, but '. 'an edit engine must have at least one configuration.', get_class($this))); } assert_instances_of($configurations, 'PhabricatorEditEngineConfiguration'); $has_default = false; foreach ($configurations as $config) { if ($config->getBuiltinKey() == self::EDITENGINECONFIG_DEFAULT) { $has_default = true; } } if (!$has_default) { $first = head($configurations); if (!$first->getBuiltinKey()) { $first ->setBuiltinKey(self::EDITENGINECONFIG_DEFAULT) ->setIsDefault(true) ->setIsEdit(true); if (!strlen($first->getName())) { $first->setName($this->getObjectCreateShortText()); } } else { throw new Exception( pht( 'EditEngine ("%s") returned builtin engine configurations, '. 'but none are marked as default and the first configuration has '. 'a different builtin key already. Mark a builtin as default or '. 'omit the key from the first configuration', get_class($this))); } } $builtins = array(); foreach ($configurations as $key => $config) { $builtin_key = $config->getBuiltinKey(); if ($builtin_key === null) { throw new Exception( pht( 'EditEngine ("%s") returned builtin engine configurations, '. 'but one (with key "%s") is missing a builtin key. Provide a '. 'builtin key for each configuration (you can omit it from the '. 'first configuration in the list to automatically assign the '. 'default key).', get_class($this), $key)); } if (isset($builtins[$builtin_key])) { throw new Exception( pht( 'EditEngine ("%s") returned builtin engine configurations, '. 'but at least two specify the same builtin key ("%s"). Engines '. 'must have unique builtin keys.', get_class($this), $builtin_key)); } $builtins[$builtin_key] = $config; } return $builtins; } protected function newBuiltinEngineConfigurations() { return array( $this->newConfiguration(), ); } final protected function newConfiguration() { return PhabricatorEditEngineConfiguration::initializeNewConfiguration( $this->getViewer(), $this); } /* -( Managing URIs )------------------------------------------------------ */ /** * @task uri */ abstract protected function getObjectViewURI($object); /** * @task uri */ protected function getObjectCreateCancelURI($object) { return $this->getApplication()->getApplicationURI(); } /** * @task uri */ protected function getEditorURI() { return $this->getApplication()->getApplicationURI('edit/'); } /** * @task uri */ protected function getObjectEditCancelURI($object) { return $this->getObjectViewURI($object); } /** * @task uri */ public function getEditURI($object = null, $path = null) { $parts = array(); $parts[] = $this->getEditorURI(); if ($object && $object->getID()) { $parts[] = $object->getID().'/'; } if ($path !== null) { $parts[] = $path; } return implode('', $parts); } public function getEffectiveObjectViewURI($object) { if ($this->getIsCreate()) { return $this->getObjectViewURI($object); } $page = $this->getSelectedPage(); if ($page) { $view_uri = $page->getViewURI(); if ($view_uri !== null) { return $view_uri; } } return $this->getObjectViewURI($object); } public function getEffectiveObjectEditDoneURI($object) { return $this->getEffectiveObjectViewURI($object); } public function getEffectiveObjectEditCancelURI($object) { $page = $this->getSelectedPage(); if ($page) { $view_uri = $page->getViewURI(); if ($view_uri !== null) { return $view_uri; } } return $this->getObjectEditCancelURI($object); } /* -( Creating and Loading Objects )--------------------------------------- */ /** * Initialize a new object for creation. * * @return object Newly initialized object. * @task load */ abstract protected function newEditableObject(); /** * Build an empty query for objects. * * @return PhabricatorPolicyAwareQuery Query. * @task load */ abstract protected function newObjectQuery(); /** * Test if this workflow is creating a new object or editing an existing one. * * @return bool True if a new object is being created. * @task load */ final public function getIsCreate() { return $this->isCreate; } /** * Initialize a new object for object creation via Conduit. * * @return object Newly initialized object. * @param list Raw transactions. * @task load */ protected function newEditableObjectFromConduit(array $raw_xactions) { return $this->newEditableObject(); } /** * Initialize a new object for documentation creation. * * @return object Newly initialized object. * @task load */ protected function newEditableObjectForDocumentation() { return $this->newEditableObject(); } /** * Flag this workflow as a create or edit. * * @param bool True if this is a create workflow. * @return this * @task load */ private function setIsCreate($is_create) { $this->isCreate = $is_create; return $this; } /** * Try to load an object by ID, PHID, or monogram. This is done primarily * to make Conduit a little easier to use. * * @param wild ID, PHID, or monogram. * @param list List of required capability constants, or omit for * defaults. * @return object Corresponding editable object. * @task load */ private function newObjectFromIdentifier( $identifier, array $capabilities = array()) { if (is_int($identifier) || ctype_digit($identifier)) { $object = $this->newObjectFromID($identifier, $capabilities); if (!$object) { throw new Exception( pht( 'No object exists with ID "%s".', $identifier)); } return $object; } $type_unknown = PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN; if (phid_get_type($identifier) != $type_unknown) { $object = $this->newObjectFromPHID($identifier, $capabilities); if (!$object) { throw new Exception( pht( 'No object exists with PHID "%s".', $identifier)); } return $object; } $target = id(new PhabricatorObjectQuery()) ->setViewer($this->getViewer()) ->withNames(array($identifier)) ->executeOne(); if (!$target) { throw new Exception( pht( 'Monogram "%s" does not identify a valid object.', $identifier)); } $expect = $this->newEditableObject(); $expect_class = get_class($expect); $target_class = get_class($target); if ($expect_class !== $target_class) { throw new Exception( pht( 'Monogram "%s" identifies an object of the wrong type. Loaded '. 'object has class "%s", but this editor operates on objects of '. 'type "%s".', $identifier, $target_class, $expect_class)); } // Load the object by PHID using this engine's standard query. This makes // sure it's really valid, goes through standard policy check logic, and // picks up any `need...()` clauses we want it to load with. $object = $this->newObjectFromPHID($target->getPHID(), $capabilities); if (!$object) { throw new Exception( pht( 'Failed to reload object identified by monogram "%s" when '. 'querying by PHID.', $identifier)); } return $object; } /** * Load an object by ID. * * @param int Object ID. * @param list List of required capability constants, or omit for * defaults. * @return object|null Object, or null if no such object exists. * @task load */ private function newObjectFromID($id, array $capabilities = array()) { $query = $this->newObjectQuery() ->withIDs(array($id)); return $this->newObjectFromQuery($query, $capabilities); } /** * Load an object by PHID. * * @param phid Object PHID. * @param list List of required capability constants, or omit for * defaults. * @return object|null Object, or null if no such object exists. * @task load */ private function newObjectFromPHID($phid, array $capabilities = array()) { $query = $this->newObjectQuery() ->withPHIDs(array($phid)); return $this->newObjectFromQuery($query, $capabilities); } /** * Load an object given a configured query. * * @param PhabricatorPolicyAwareQuery Configured query. * @param list List of required capability constants, or omit for * defaults. * @return object|null Object, or null if no such object exists. * @task load */ private function newObjectFromQuery( PhabricatorPolicyAwareQuery $query, array $capabilities = array()) { $viewer = $this->getViewer(); if (!$capabilities) { $capabilities = array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } $object = $query ->setViewer($viewer) ->requireCapabilities($capabilities) ->executeOne(); if (!$object) { return null; } return $object; } /** * Verify that an object is appropriate for editing. * * @param wild Loaded value. * @return void * @task load */ private function validateObject($object) { if (!$object || !is_object($object)) { throw new Exception( pht( 'EditEngine "%s" created or loaded an invalid object: object must '. 'actually be an object, but is of some other type ("%s").', get_class($this), gettype($object))); } if (!($object instanceof PhabricatorApplicationTransactionInterface)) { throw new Exception( pht( 'EditEngine "%s" created or loaded an invalid object: object (of '. 'class "%s") must implement "%s", but does not.', get_class($this), get_class($object), 'PhabricatorApplicationTransactionInterface')); } } /* -( Responding to Web Requests )----------------------------------------- */ final public function buildResponse() { $viewer = $this->getViewer(); $controller = $this->getController(); $request = $controller->getRequest(); $action = $this->getEditAction(); $capabilities = array(); $use_default = false; $require_create = true; switch ($action) { case 'comment': $capabilities = array( PhabricatorPolicyCapability::CAN_VIEW, ); $use_default = true; break; case 'parameters': $use_default = true; break; case 'nodefault': case 'nocreate': case 'nomanage': $require_create = false; break; default: break; } $object = $this->getTargetObject(); if (!$object) { $id = $request->getURIData('id'); if ($id) { $this->setIsCreate(false); $object = $this->newObjectFromID($id, $capabilities); if (!$object) { return new Aphront404Response(); } } else { // Make sure the viewer has permission to create new objects of // this type if we're going to create a new object. if ($require_create) { $this->requireCreateCapability(); } $this->setIsCreate(true); $object = $this->newEditableObject(); } } else { $id = $object->getID(); } $this->validateObject($object); if ($use_default) { $config = $this->loadDefaultConfiguration(); if (!$config) { return new Aphront404Response(); } } else { $form_key = $request->getURIData('formKey'); if (strlen($form_key)) { $config = $this->loadEditEngineConfigurationWithIdentifier($form_key); if (!$config) { return new Aphront404Response(); } if ($id && !$config->getIsEdit()) { return $this->buildNotEditFormRespose($object, $config); } } else { if ($id) { $config = $this->loadDefaultEditConfiguration($object); if (!$config) { return $this->buildNoEditResponse($object); } } else { $config = $this->loadDefaultCreateConfiguration(); if (!$config) { return $this->buildNoCreateResponse($object); } } } } if ($config->getIsDisabled()) { return $this->buildDisabledFormResponse($object, $config); } $page_key = $request->getURIData('pageKey'); if (!strlen($page_key)) { $pages = $this->getPages($object); if ($pages) { $page_key = head_key($pages); } } if (strlen($page_key)) { $page = $this->selectPage($object, $page_key); if (!$page) { return new Aphront404Response(); } } switch ($action) { case 'parameters': return $this->buildParametersResponse($object); case 'nodefault': return $this->buildNoDefaultResponse($object); case 'nocreate': return $this->buildNoCreateResponse($object); case 'nomanage': return $this->buildNoManageResponse($object); case 'comment': return $this->buildCommentResponse($object); default: return $this->buildEditResponse($object); } } private function buildCrumbs($object, $final = false) { $controller = $this->getController(); $crumbs = $controller->buildApplicationCrumbsForEditEngine(); if ($this->getIsCreate()) { $create_text = $this->getObjectCreateShortText(); if ($final) { $crumbs->addTextCrumb($create_text); } else { $edit_uri = $this->getEditURI($object); $crumbs->addTextCrumb($create_text, $edit_uri); } } else { $crumbs->addTextCrumb( $this->getObjectEditShortText($object), $this->getEffectiveObjectViewURI($object)); $edit_text = pht('Edit'); if ($final) { $crumbs->addTextCrumb($edit_text); } else { $edit_uri = $this->getEditURI($object); $crumbs->addTextCrumb($edit_text, $edit_uri); } } return $crumbs; } private function buildEditResponse($object) { $viewer = $this->getViewer(); $controller = $this->getController(); $request = $controller->getRequest(); $fields = $this->buildEditFields($object); $template = $object->getApplicationTransactionTemplate(); if ($this->getIsCreate()) { $cancel_uri = $this->getObjectCreateCancelURI($object); $submit_button = $this->getObjectCreateButtonText($object); } else { $cancel_uri = $this->getEffectiveObjectEditCancelURI($object); $submit_button = $this->getObjectEditButtonText($object); } $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')) { $lock = PhabricatorEditEngineLock::newForObject($viewer, $object); $dialog = $this->getController() ->newDialog() ->addHiddenInput('overrideLock', true) ->setDisableWorkflowOnSubmit(true) ->addCancelButton($cancel_uri); return $lock->willPromptUserForLockOverrideWithDialog($dialog); } $validation_exception = null; if ($request->isFormPost() && $request->getBool('editEngine')) { $submit_fields = $fields; foreach ($submit_fields as $key => $field) { if (!$field->shouldGenerateTransactionsFromSubmit()) { unset($submit_fields[$key]); continue; } } // Before we read the submitted values, store a copy of what we would // use if the form was empty so we can figure out which transactions are // just setting things to their default values for the current form. $defaults = array(); foreach ($submit_fields as $key => $field) { $defaults[$key] = $field->getValueForTransaction(); } foreach ($submit_fields as $key => $field) { $field->setIsSubmittedForm(true); if (!$field->shouldReadValueFromSubmit()) { continue; } $field->readValueFromSubmit($request); } $xactions = array(); if ($this->getIsCreate()) { $xactions[] = id(clone $template) ->setTransactionType(PhabricatorTransactions::TYPE_CREATE); if ($this->supportsSubtypes()) { $xactions[] = id(clone $template) ->setTransactionType(PhabricatorTransactions::TYPE_SUBTYPE) ->setNewValue($config->getSubtype()); } } foreach ($submit_fields as $key => $field) { $field_value = $field->getValueForTransaction(); $type_xactions = $field->generateTransactions( clone $template, array( 'value' => $field_value, )); foreach ($type_xactions as $type_xaction) { $default = $defaults[$key]; if ($default === $field->getValueForTransaction()) { $type_xaction->setIsDefaultTransaction(true); } $xactions[] = $type_xaction; } } $editor = $object->getApplicationTransactionEditor() ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true); try { $xactions = $this->willApplyTransactions($object, $xactions); $editor->applyTransactions($object, $xactions); $this->didApplyTransactions($object, $xactions); return $this->newEditResponse($request, $object, $xactions); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; foreach ($fields as $field) { $message = $this->getValidationExceptionShortMessage($ex, $field); if ($message === null) { continue; } $field->setControlError($message); } } } else { if ($this->getIsCreate()) { $template = $request->getStr('template'); if (strlen($template)) { $template_object = $this->newObjectFromIdentifier( $template, array( PhabricatorPolicyCapability::CAN_VIEW, )); if (!$template_object) { return new Aphront404Response(); } } else { $template_object = null; } if ($template_object) { $copy_fields = $this->buildEditFields($template_object); $copy_fields = mpull($copy_fields, null, 'getKey'); foreach ($copy_fields as $copy_key => $copy_field) { if (!$copy_field->getIsCopyable()) { unset($copy_fields[$copy_key]); } } } else { $copy_fields = array(); } foreach ($fields as $field) { if (!$field->shouldReadValueFromRequest()) { continue; } $field_key = $field->getKey(); if (isset($copy_fields[$field_key])) { $field->readValueFromField($copy_fields[$field_key]); } $field->readValueFromRequest($request); } } } $action_button = $this->buildEditFormActionButton($object); if ($this->getIsCreate()) { $header_text = $this->getFormHeaderText($object); } else { $header_text = $this->getObjectEditTitleText($object); } $show_preview = !$request->isAjax(); if ($show_preview) { $previews = array(); foreach ($fields as $field) { $preview = $field->getPreviewPanel(); if (!$preview) { continue; } $control_id = $field->getControlID(); $preview ->setControlID($control_id) ->setPreviewURI('/transactions/remarkuppreview/'); $previews[] = $preview; } } else { $previews = array(); } $form = $this->buildEditForm($object, $fields); $crumbs = $this->buildCrumbs($object, $final = true); $crumbs->setBorder(true); if ($request->isAjax()) { return $this->getController() ->newDialog() ->setWidth(AphrontDialogView::WIDTH_FULL) ->setTitle($header_text) ->setValidationException($validation_exception) ->appendForm($form) ->addCancelButton($cancel_uri) ->addSubmitButton($submit_button); } $box_header = id(new PHUIHeaderView()) ->setHeader($header_text); if ($action_button) { $box_header->addActionLink($action_button); } $box = id(new PHUIObjectBoxView()) ->setUser($viewer) ->setHeader($box_header) ->setValidationException($validation_exception) ->setBackground(PHUIObjectBoxView::WHITE_CONFIG) ->appendChild($form); // This is fairly questionable, but in use by Settings. if ($request->getURIData('formSaved')) { $box->setFormSaved(true); } $content = array( $box, $previews, ); $view = new PHUITwoColumnView(); $page_header = $this->getPageHeader($object); if ($page_header) { $view->setHeader($page_header); } $page = $controller->newPage() ->setTitle($header_text) ->setCrumbs($crumbs) ->appendChild($view); $navigation = $this->getNavigation(); if ($navigation) { $view->setFixed(true); $view->setNavigation($navigation); $view->setMainColumn($content); } else { $view->setFooter($content); } return $page; } protected function newEditResponse( AphrontRequest $request, $object, array $xactions) { return id(new AphrontRedirectResponse()) ->setURI($this->getEffectiveObjectEditDoneURI($object)); } private function buildEditForm($object, array $fields) { $viewer = $this->getViewer(); $controller = $this->getController(); $request = $controller->getRequest(); $fields = $this->willBuildEditForm($object, $fields); $form = id(new AphrontFormView()) ->setUser($viewer) ->addHiddenInput('editEngine', 'true'); foreach ($this->contextParameters as $param) { $form->addHiddenInput($param, $request->getStr($param)); } foreach ($fields as $field) { + if (!$field->getIsFormField()) { + continue; + } + $field->appendToForm($form); } if ($this->getIsCreate()) { $cancel_uri = $this->getObjectCreateCancelURI($object); $submit_button = $this->getObjectCreateButtonText($object); } else { $cancel_uri = $this->getEffectiveObjectEditCancelURI($object); $submit_button = $this->getObjectEditButtonText($object); } if (!$request->isAjax()) { $buttons = id(new AphrontFormSubmitControl()) ->setValue($submit_button); if ($cancel_uri) { $buttons->addCancelButton($cancel_uri); } $form->appendControl($buttons); } return $form; } protected function willBuildEditForm($object, array $fields) { return $fields; } private function buildEditFormActionButton($object) { if (!$this->isEngineConfigurable()) { return null; } $viewer = $this->getViewer(); $action_view = id(new PhabricatorActionListView()) ->setUser($viewer); foreach ($this->buildEditFormActions($object) as $action) { $action_view->addAction($action); } $action_button = id(new PHUIButtonView()) ->setTag('a') ->setText(pht('Configure Form')) ->setHref('#') ->setIcon('fa-gear') ->setDropdownMenu($action_view); return $action_button; } private function buildEditFormActions($object) { $actions = array(); if ($this->supportsEditEngineConfiguration()) { $engine_key = $this->getEngineKey(); $config = $this->getEditEngineConfiguration(); $can_manage = PhabricatorPolicyFilter::hasCapability( $this->getViewer(), $config, PhabricatorPolicyCapability::CAN_EDIT); if ($can_manage) { $manage_uri = $config->getURI(); } else { $manage_uri = $this->getEditURI(null, 'nomanage/'); } $view_uri = "/transactions/editengine/{$engine_key}/"; $actions[] = id(new PhabricatorActionView()) ->setLabel(true) ->setName(pht('Configuration')); $actions[] = id(new PhabricatorActionView()) ->setName(pht('View Form Configurations')) ->setIcon('fa-list-ul') ->setHref($view_uri); $actions[] = id(new PhabricatorActionView()) ->setName(pht('Edit Form Configuration')) ->setIcon('fa-pencil') ->setHref($manage_uri) ->setDisabled(!$can_manage) ->setWorkflow(!$can_manage); } $actions[] = id(new PhabricatorActionView()) ->setLabel(true) ->setName(pht('Documentation')); $actions[] = id(new PhabricatorActionView()) ->setName(pht('Using HTTP Parameters')) ->setIcon('fa-book') ->setHref($this->getEditURI($object, 'parameters/')); $doc_href = PhabricatorEnv::getDoclink('User Guide: Customizing Forms'); $actions[] = id(new PhabricatorActionView()) ->setName(pht('User Guide: Customizing Forms')) ->setIcon('fa-book') ->setHref($doc_href); return $actions; } public function newNUXButton($text) { $specs = $this->newCreateActionSpecifications(array()); $head = head($specs); return id(new PHUIButtonView()) ->setTag('a') ->setText($text) ->setHref($head['uri']) ->setDisabled($head['disabled']) ->setWorkflow($head['workflow']) ->setColor(PHUIButtonView::GREEN); } final public function addActionToCrumbs( PHUICrumbsView $crumbs, array $parameters = array()) { $viewer = $this->getViewer(); $specs = $this->newCreateActionSpecifications($parameters); $head = head($specs); $menu_uri = $head['uri']; $dropdown = null; if (count($specs) > 1) { $menu_icon = 'fa-caret-square-o-down'; $menu_name = $this->getObjectCreateShortText(); $workflow = false; $disabled = false; $dropdown = id(new PhabricatorActionListView()) ->setUser($viewer); foreach ($specs as $spec) { $dropdown->addAction( id(new PhabricatorActionView()) ->setName($spec['name']) ->setIcon($spec['icon']) ->setHref($spec['uri']) ->setDisabled($head['disabled']) ->setWorkflow($head['workflow'])); } } else { $menu_icon = $head['icon']; $menu_name = $head['name']; $workflow = $head['workflow']; $disabled = $head['disabled']; } $action = id(new PHUIListItemView()) ->setName($menu_name) ->setHref($menu_uri) ->setIcon($menu_icon) ->setWorkflow($workflow) ->setDisabled($disabled); if ($dropdown) { $action->setDropdownMenu($dropdown); } $crumbs->addAction($action); } /** * Build a raw description of available "Create New Object" UI options so * other methods can build menus or buttons. */ public function newCreateActionSpecifications(array $parameters) { $viewer = $this->getViewer(); $can_create = $this->hasCreateCapability(); if ($can_create) { $configs = $this->loadUsableConfigurationsForCreate(); } else { $configs = array(); } $disabled = false; $workflow = false; $menu_icon = 'fa-plus-square'; $specs = array(); if (!$configs) { if ($viewer->isLoggedIn()) { $disabled = true; } else { // If the viewer isn't logged in, assume they'll get hit with a login // dialog and are likely able to create objects after they log in. $disabled = false; } $workflow = true; if ($can_create) { $create_uri = $this->getEditURI(null, 'nodefault/'); } else { $create_uri = $this->getEditURI(null, 'nocreate/'); } $specs[] = array( 'name' => $this->getObjectCreateShortText(), 'uri' => $create_uri, 'icon' => $menu_icon, 'disabled' => $disabled, 'workflow' => $workflow, ); } else { foreach ($configs as $config) { $config_uri = $config->getCreateURI(); if ($parameters) { $config_uri = (string)id(new PhutilURI($config_uri)) ->setQueryParams($parameters); } $specs[] = array( 'name' => $config->getDisplayName(), 'uri' => $config_uri, 'icon' => 'fa-plus', 'disabled' => false, 'workflow' => false, ); } } return $specs; } final public function buildEditEngineCommentView($object) { $config = $this->loadDefaultEditConfiguration($object); if (!$config) { // TODO: This just nukes the entire comment form if you don't have access // to any edit forms. We might want to tailor this UX a bit. return id(new PhabricatorApplicationTransactionCommentView()) ->setNoPermission(true); } $viewer = $this->getViewer(); $can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object); if (!$can_interact) { $lock = PhabricatorEditEngineLock::newForObject($viewer, $object); return id(new PhabricatorApplicationTransactionCommentView()) ->setEditEngineLock($lock); } $object_phid = $object->getPHID(); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); if ($is_serious) { $header_text = $this->getCommentViewSeriousHeaderText($object); $button_text = $this->getCommentViewSeriousButtonText($object); } else { $header_text = $this->getCommentViewHeaderText($object); $button_text = $this->getCommentViewButtonText($object); } $comment_uri = $this->getEditURI($object, 'comment/'); $view = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($viewer) ->setObjectPHID($object_phid) ->setHeaderText($header_text) ->setAction($comment_uri) ->setSubmitButtonName($button_text); $draft = PhabricatorVersionedDraft::loadDraft( $object_phid, $viewer->getPHID()); if ($draft) { $view->setVersionedDraft($draft); } $view->setCurrentVersion($this->loadDraftVersion($object)); $fields = $this->buildEditFields($object); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $object, PhabricatorPolicyCapability::CAN_EDIT); $comment_actions = array(); foreach ($fields as $field) { if (!$field->shouldGenerateTransactionsFromComment()) { continue; } if (!$can_edit) { if (!$field->getCanApplyWithoutEditCapability()) { continue; } } $comment_action = $field->getCommentAction(); if (!$comment_action) { continue; } $key = $comment_action->getKey(); // TODO: Validate these better. $comment_actions[$key] = $comment_action; } $comment_actions = msortv($comment_actions, 'getSortVector'); $view->setCommentActions($comment_actions); $comment_groups = $this->newCommentActionGroups(); $view->setCommentActionGroups($comment_groups); return $view; } protected function loadDraftVersion($object) { $viewer = $this->getViewer(); if (!$viewer->isLoggedIn()) { return null; } $template = $object->getApplicationTransactionTemplate(); $conn_r = $template->establishConnection('r'); // Find the most recent transaction the user has written. We'll use this // as a version number to make sure that out-of-date drafts get discarded. $result = queryfx_one( $conn_r, 'SELECT id AS version FROM %T WHERE objectPHID = %s AND authorPHID = %s ORDER BY id DESC LIMIT 1', $template->getTableName(), $object->getPHID(), $viewer->getPHID()); if ($result) { return (int)$result['version']; } else { return null; } } /* -( Responding to HTTP Parameter Requests )------------------------------ */ /** * Respond to a request for documentation on HTTP parameters. * * @param object Editable object. * @return AphrontResponse Response object. * @task http */ private function buildParametersResponse($object) { $controller = $this->getController(); $viewer = $this->getViewer(); $request = $controller->getRequest(); $fields = $this->buildEditFields($object); $crumbs = $this->buildCrumbs($object); $crumbs->addTextCrumb(pht('HTTP Parameters')); $crumbs->setBorder(true); $header_text = pht( 'HTTP Parameters: %s', $this->getObjectCreateShortText()); $header = id(new PHUIHeaderView()) ->setHeader($header_text); $help_view = id(new PhabricatorApplicationEditHTTPParameterHelpView()) ->setUser($viewer) ->setFields($fields); $document = id(new PHUIDocumentView()) ->setUser($viewer) ->setHeader($header) ->appendChild($help_view); return $controller->newPage() ->setTitle(pht('HTTP Parameters')) ->setCrumbs($crumbs) ->appendChild($document); } private function buildError($object, $title, $body) { $cancel_uri = $this->getObjectCreateCancelURI($object); $dialog = $this->getController() ->newDialog() ->addCancelButton($cancel_uri); if ($title !== null) { $dialog->setTitle($title); } if ($body !== null) { $dialog->appendParagraph($body); } return $dialog; } private function buildNoDefaultResponse($object) { return $this->buildError( $object, pht('No Default Create Forms'), pht( 'This application is not configured with any forms for creating '. 'objects that are visible to you and enabled.')); } private function buildNoCreateResponse($object) { return $this->buildError( $object, pht('No Create Permission'), pht('You do not have permission to create these objects.')); } private function buildNoManageResponse($object) { return $this->buildError( $object, pht('No Manage Permission'), pht( 'You do not have permission to configure forms for this '. 'application.')); } private function buildNoEditResponse($object) { return $this->buildError( $object, pht('No Edit Forms'), pht( 'You do not have access to any forms which are enabled and marked '. 'as edit forms.')); } private function buildNotEditFormRespose($object, $config) { return $this->buildError( $object, pht('Not an Edit Form'), pht( 'This form ("%s") is not marked as an edit form, so '. 'it can not be used to edit objects.', $config->getName())); } private function buildDisabledFormResponse($object, $config) { return $this->buildError( $object, pht('Form Disabled'), pht( 'This form ("%s") has been disabled, so it can not be used.', $config->getName())); } private function buildLockedObjectResponse($object) { $dialog = $this->buildError($object, null, null); $viewer = $this->getViewer(); $lock = PhabricatorEditEngineLock::newForObject($viewer, $object); return $lock->willBlockUserInteractionWithDialog($dialog); } private function buildCommentResponse($object) { $viewer = $this->getViewer(); if ($this->getIsCreate()) { return new Aphront404Response(); } $controller = $this->getController(); $request = $controller->getRequest(); if (!$request->isFormPost()) { return new Aphront400Response(); } $can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object); if (!$can_interact) { return $this->buildLockedObjectResponse($object); } $config = $this->loadDefaultEditConfiguration($object); if (!$config) { return new Aphront404Response(); } $fields = $this->buildEditFields($object); $is_preview = $request->isPreviewRequest(); $view_uri = $this->getEffectiveObjectViewURI($object); $template = $object->getApplicationTransactionTemplate(); $comment_template = $template->getApplicationTransactionCommentObject(); $comment_text = $request->getStr('comment'); $actions = $request->getStr('editengine.actions'); if ($actions) { $actions = phutil_json_decode($actions); } if ($is_preview) { $version_key = PhabricatorVersionedDraft::KEY_VERSION; $request_version = $request->getInt($version_key); $current_version = $this->loadDraftVersion($object); if ($request_version >= $current_version) { $draft = PhabricatorVersionedDraft::loadOrCreateDraft( $object->getPHID(), $viewer->getPHID(), $current_version); $is_empty = (!strlen($comment_text) && !$actions); $draft ->setProperty('comment', $comment_text) ->setProperty('actions', $actions) ->save(); $draft_engine = $this->newDraftEngine($object); if ($draft_engine) { $draft_engine ->setVersionedDraft($draft) ->synchronize(); } } } $xactions = array(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $object, PhabricatorPolicyCapability::CAN_EDIT); if ($actions) { $action_map = array(); foreach ($actions as $action) { $type = idx($action, 'type'); if (!$type) { continue; } if (empty($fields[$type])) { continue; } $action_map[$type] = $action; } foreach ($action_map as $type => $action) { $field = $fields[$type]; if (!$field->shouldGenerateTransactionsFromComment()) { continue; } // If you don't have edit permission on the object, you're limited in // which actions you can take via the comment form. Most actions // need edit permission, but some actions (like "Accept Revision") // can be applied by anyone with view permission. if (!$can_edit) { if (!$field->getCanApplyWithoutEditCapability()) { // We know the user doesn't have the capability, so this will // raise a policy exception. PhabricatorPolicyFilter::requireCapability( $viewer, $object, PhabricatorPolicyCapability::CAN_EDIT); } } if (array_key_exists('initialValue', $action)) { $field->setInitialValue($action['initialValue']); } $field->readValueFromComment(idx($action, 'value')); $type_xactions = $field->generateTransactions( clone $template, array( 'value' => $field->getValueForTransaction(), )); foreach ($type_xactions as $type_xaction) { $xactions[] = $type_xaction; } } } $auto_xactions = $this->newAutomaticCommentTransactions($object); foreach ($auto_xactions as $xaction) { $xactions[] = $xaction; } if (strlen($comment_text) || !$xactions) { $xactions[] = id(clone $template) ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->attachComment( id(clone $comment_template) ->setContent($comment_text)); } $editor = $object->getApplicationTransactionEditor() ->setActor($viewer) ->setContinueOnNoEffect($request->isContinueRequest()) ->setContinueOnMissingFields(true) ->setContentSourceFromRequest($request) ->setRaiseWarnings(!$request->getBool('editEngine.warnings')) ->setIsPreview($is_preview); try { $xactions = $editor->applyTransactions($object, $xactions); } catch (PhabricatorApplicationTransactionValidationException $ex) { return id(new PhabricatorApplicationTransactionValidationResponse()) ->setCancelURI($view_uri) ->setException($ex); } catch (PhabricatorApplicationTransactionNoEffectException $ex) { return id(new PhabricatorApplicationTransactionNoEffectResponse()) ->setCancelURI($view_uri) ->setException($ex); } catch (PhabricatorApplicationTransactionWarningException $ex) { return id(new PhabricatorApplicationTransactionWarningResponse()) ->setCancelURI($view_uri) ->setException($ex); } if (!$is_preview) { PhabricatorVersionedDraft::purgeDrafts( $object->getPHID(), $viewer->getPHID()); $draft_engine = $this->newDraftEngine($object); if ($draft_engine) { $draft_engine ->setVersionedDraft(null) ->synchronize(); } } if ($request->isAjax() && $is_preview) { $preview_content = $this->newCommentPreviewContent($object, $xactions); return id(new PhabricatorApplicationTransactionResponse()) ->setViewer($viewer) ->setTransactions($xactions) ->setIsPreview($is_preview) ->setPreviewContent($preview_content); } else { return id(new AphrontRedirectResponse()) ->setURI($view_uri); } } protected function newDraftEngine($object) { $viewer = $this->getViewer(); if ($object instanceof PhabricatorDraftInterface) { $engine = $object->newDraftEngine(); } else { $engine = new PhabricatorBuiltinDraftEngine(); } return $engine ->setObject($object) ->setViewer($viewer); } /* -( Conduit )------------------------------------------------------------ */ /** * Respond to a Conduit edit request. * * This method accepts a list of transactions to apply to an object, and * either edits an existing object or creates a new one. * * @task conduit */ final public function buildConduitResponse(ConduitAPIRequest $request) { $viewer = $this->getViewer(); $config = $this->loadDefaultConfiguration(); if (!$config) { throw new Exception( pht( 'Unable to load configuration for this EditEngine ("%s").', get_class($this))); } $raw_xactions = $this->getRawConduitTransactions($request); $identifier = $request->getValue('objectIdentifier'); if ($identifier) { $this->setIsCreate(false); // After T13186, each transaction can individually weaken or replace the // capabilities required to apply it, so we no longer need CAN_EDIT to // attempt to apply transactions to objects. In practice, almost all // transactions require CAN_EDIT so we won't get very far if we don't // have it. $capabilities = array( PhabricatorPolicyCapability::CAN_VIEW, ); $object = $this->newObjectFromIdentifier( $identifier, $capabilities); } else { $this->requireCreateCapability(); $this->setIsCreate(true); $object = $this->newEditableObjectFromConduit($raw_xactions); } $this->validateObject($object); $fields = $this->buildEditFields($object); $types = $this->getConduitEditTypesFromFields($fields); $template = $object->getApplicationTransactionTemplate(); $xactions = $this->getConduitTransactions( $request, $raw_xactions, $types, $template); $editor = $object->getApplicationTransactionEditor() ->setActor($viewer) ->setContentSource($request->newContentSource()) ->setContinueOnNoEffect(true); if (!$this->getIsCreate()) { $editor->setContinueOnMissingFields(true); } $xactions = $editor->applyTransactions($object, $xactions); $xactions_struct = array(); foreach ($xactions as $xaction) { $xactions_struct[] = array( 'phid' => $xaction->getPHID(), ); } return array( 'object' => array( 'id' => (int)$object->getID(), 'phid' => $object->getPHID(), ), 'transactions' => $xactions_struct, ); } private function getRawConduitTransactions(ConduitAPIRequest $request) { $transactions_key = 'transactions'; $xactions = $request->getValue($transactions_key); if (!is_array($xactions)) { throw new Exception( pht( 'Parameter "%s" is not a list of transactions.', $transactions_key)); } foreach ($xactions as $key => $xaction) { if (!is_array($xaction)) { throw new Exception( pht( 'Parameter "%s" must contain a list of transaction descriptions, '. 'but item with key "%s" is not a dictionary.', $transactions_key, $key)); } if (!array_key_exists('type', $xaction)) { throw new Exception( pht( 'Parameter "%s" must contain a list of transaction descriptions, '. 'but item with key "%s" is missing a "type" field. Each '. 'transaction must have a type field.', $transactions_key, $key)); } } return $xactions; } /** * Generate transactions which can be applied from edit actions in a Conduit * request. * * @param ConduitAPIRequest The request. * @param list Raw conduit transactions. * @param list Supported edit types. * @param PhabricatorApplicationTransaction Template transaction. * @return list Generated transactions. * @task conduit */ private function getConduitTransactions( ConduitAPIRequest $request, array $xactions, array $types, PhabricatorApplicationTransaction $template) { $viewer = $request->getUser(); $results = array(); foreach ($xactions as $key => $xaction) { $type = $xaction['type']; if (empty($types[$type])) { throw new Exception( pht( 'Transaction with key "%s" has invalid type "%s". This type is '. 'not recognized. Valid types are: %s.', $key, $type, implode(', ', array_keys($types)))); } } if ($this->getIsCreate()) { $results[] = id(clone $template) ->setTransactionType(PhabricatorTransactions::TYPE_CREATE); } $is_strict = $request->getIsStrictlyTyped(); foreach ($xactions as $xaction) { $type = $types[$xaction['type']]; // Let the parameter type interpret the value. This allows you to // use usernames in list fields, for example. $parameter_type = $type->getConduitParameterType(); $parameter_type->setViewer($viewer); try { $value = $xaction['value']; $value = $parameter_type->getValue($xaction, 'value', $is_strict); $value = $type->getTransactionValueFromConduit($value); $xaction['value'] = $value; } catch (Exception $ex) { throw new PhutilProxyException( pht( 'Exception when processing transaction of type "%s": %s', $xaction['type'], $ex->getMessage()), $ex); } $type_xactions = $type->generateTransactions( clone $template, $xaction); foreach ($type_xactions as $type_xaction) { $results[] = $type_xaction; } } return $results; } /** * @return map * @task conduit */ private function getConduitEditTypesFromFields(array $fields) { $types = array(); foreach ($fields as $field) { $field_types = $field->getConduitEditTypes(); if ($field_types === null) { continue; } foreach ($field_types as $field_type) { $types[$field_type->getEditType()] = $field_type; } } return $types; } public function getConduitEditTypes() { $config = $this->loadDefaultConfiguration(); if (!$config) { return array(); } $object = $this->newEditableObjectForDocumentation(); $fields = $this->buildEditFields($object); return $this->getConduitEditTypesFromFields($fields); } final public static function getAllEditEngines() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setUniqueMethod('getEngineKey') ->execute(); } final public static function getByKey(PhabricatorUser $viewer, $key) { return id(new PhabricatorEditEngineQuery()) ->setViewer($viewer) ->withEngineKeys(array($key)) ->executeOne(); } public function getIcon() { $application = $this->getApplication(); return $application->getIcon(); } private function loadUsableConfigurationsForCreate() { $viewer = $this->getViewer(); $configs = id(new PhabricatorEditEngineConfigurationQuery()) ->setViewer($viewer) ->withEngineKeys(array($this->getEngineKey())) ->withIsDefault(true) ->withIsDisabled(false) ->execute(); $configs = msort($configs, 'getCreateSortKey'); // Attach this specific engine to configurations we load so they can access // any runtime configuration. For example, this allows us to generate the // correct "Create Form" buttons when editing forms, see T12301. foreach ($configs as $config) { $config->attachEngine($this); } return $configs; } protected function getValidationExceptionShortMessage( PhabricatorApplicationTransactionValidationException $ex, PhabricatorEditField $field) { $xaction_type = $field->getTransactionType(); if ($xaction_type === null) { return null; } return $ex->getShortMessage($xaction_type); } protected function getCreateNewObjectPolicy() { return PhabricatorPolicies::POLICY_USER; } private function requireCreateCapability() { PhabricatorPolicyFilter::requireCapability( $this->getViewer(), $this, PhabricatorPolicyCapability::CAN_EDIT); } private function hasCreateCapability() { return PhabricatorPolicyFilter::hasCapability( $this->getViewer(), $this, PhabricatorPolicyCapability::CAN_EDIT); } public function isCommentAction() { return ($this->getEditAction() == 'comment'); } public function getEditAction() { $controller = $this->getController(); $request = $controller->getRequest(); return $request->getURIData('editAction'); } protected function newCommentActionGroups() { return array(); } protected function newAutomaticCommentTransactions($object) { return array(); } protected function newCommentPreviewContent($object, array $xactions) { return null; } /* -( Form Pages )--------------------------------------------------------- */ public function getSelectedPage() { return $this->page; } private function selectPage($object, $page_key) { $pages = $this->getPages($object); if (empty($pages[$page_key])) { return null; } $this->page = $pages[$page_key]; return $this->page; } protected function newPages($object) { return array(); } protected function getPages($object) { if ($this->pages === null) { $pages = $this->newPages($object); assert_instances_of($pages, 'PhabricatorEditPage'); $pages = mpull($pages, null, 'getKey'); $this->pages = $pages; } return $this->pages; } private function applyPageToFields($object, array $fields) { $pages = $this->getPages($object); if (!$pages) { return $fields; } if (!$this->getSelectedPage()) { return $fields; } $page_picks = array(); $default_key = head($pages)->getKey(); foreach ($pages as $page_key => $page) { foreach ($page->getFieldKeys() as $field_key) { $page_picks[$field_key] = $page_key; } if ($page->getIsDefault()) { $default_key = $page_key; } } $page_map = array_fill_keys(array_keys($pages), array()); foreach ($fields as $field_key => $field) { if (isset($page_picks[$field_key])) { $page_map[$page_picks[$field_key]][$field_key] = $field; continue; } // TODO: Maybe let the field pick a page to associate itself with so // extensions can force themselves onto a particular page? $page_map[$default_key][$field_key] = $field; } $page = $this->getSelectedPage(); if (!$page) { $page = head($pages); } $selected_key = $page->getKey(); return $page_map[$selected_key]; } protected function willApplyTransactions($object, array $xactions) { return $xactions; } protected function didApplyTransactions($object, array $xactions) { return; } /* -( Bulk Edits )--------------------------------------------------------- */ final public function newBulkEditGroupMap() { $groups = $this->newBulkEditGroups(); $map = array(); foreach ($groups as $group) { $key = $group->getKey(); if (isset($map[$key])) { throw new Exception( pht( 'Two bulk edit groups have the same key ("%s"). Each bulk edit '. 'group must have a unique key.', $key)); } $map[$key] = $group; } if ($this->isEngineExtensible()) { $extensions = PhabricatorEditEngineExtension::getAllEnabledExtensions(); } else { $extensions = array(); } foreach ($extensions as $extension) { $extension_groups = $extension->newBulkEditGroups($this); foreach ($extension_groups as $group) { $key = $group->getKey(); if (isset($map[$key])) { throw new Exception( pht( 'Extension "%s" defines a bulk edit group with the same key '. '("%s") as the main editor or another extension. Each bulk '. 'edit group must have a unique key.')); } $map[$key] = $group; } } return $map; } protected function newBulkEditGroups() { return array( id(new PhabricatorBulkEditGroup()) ->setKey('default') ->setLabel(pht('Primary Fields')), id(new PhabricatorBulkEditGroup()) ->setKey('extension') ->setLabel(pht('Support Applications')), ); } final public function newBulkEditMap() { $config = $this->loadDefaultConfiguration(); if (!$config) { throw new Exception( pht('No default edit engine configuration for bulk edit.')); } $object = $this->newEditableObject(); $fields = $this->buildEditFields($object); $groups = $this->newBulkEditGroupMap(); $edit_types = $this->getBulkEditTypesFromFields($fields); $map = array(); foreach ($edit_types as $key => $type) { $bulk_type = $type->getBulkParameterType(); if ($bulk_type === null) { continue; } $bulk_label = $type->getBulkEditLabel(); if ($bulk_label === null) { continue; } $group_key = $type->getBulkEditGroupKey(); if (!$group_key) { $group_key = 'default'; } if (!isset($groups[$group_key])) { throw new Exception( pht( 'Field "%s" has a bulk edit group key ("%s") with no '. 'corresponding bulk edit group.', $key, $group_key)); } $map[] = array( 'label' => $bulk_label, 'xaction' => $key, 'group' => $group_key, 'control' => array( 'type' => $bulk_type->getPHUIXControlType(), 'spec' => (object)$bulk_type->getPHUIXControlSpecification(), ), ); } return $map; } final public function newRawBulkTransactions(array $xactions) { $config = $this->loadDefaultConfiguration(); if (!$config) { throw new Exception( pht('No default edit engine configuration for bulk edit.')); } $object = $this->newEditableObject(); $fields = $this->buildEditFields($object); $edit_types = $this->getBulkEditTypesFromFields($fields); $template = $object->getApplicationTransactionTemplate(); $raw_xactions = array(); foreach ($xactions as $key => $xaction) { PhutilTypeSpec::checkMap( $xaction, array( 'type' => 'string', 'value' => 'optional wild', )); $type = $xaction['type']; if (!isset($edit_types[$type])) { throw new Exception( pht( 'Unsupported bulk edit type "%s".', $type)); } $edit_type = $edit_types[$type]; // Replace the edit type with the underlying transaction type. Usually // these are 1:1 and the transaction type just has more internal noise, // but it's possible that this isn't the case. $xaction['type'] = $edit_type->getTransactionType(); $value = $xaction['value']; $value = $edit_type->getTransactionValueFromBulkEdit($value); $xaction['value'] = $value; $xaction_objects = $edit_type->generateTransactions( clone $template, $xaction); foreach ($xaction_objects as $xaction_object) { $raw_xaction = array( 'type' => $xaction_object->getTransactionType(), 'metadata' => $xaction_object->getMetadata(), 'new' => $xaction_object->getNewValue(), ); if ($xaction_object->hasOldValue()) { $raw_xaction['old'] = $xaction_object->getOldValue(); } if ($xaction_object->hasComment()) { $comment = $xaction_object->getComment(); $raw_xaction['comment'] = $comment->getContent(); } $raw_xactions[] = $raw_xaction; } } return $raw_xactions; } private function getBulkEditTypesFromFields(array $fields) { $types = array(); foreach ($fields as $field) { $field_types = $field->getBulkEditTypes(); if ($field_types === null) { continue; } foreach ($field_types as $field_type) { $types[$field_type->getEditType()] = $field_type; } } return $types; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getPHID() { return get_class($this); } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicies::getMostOpenPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getCreateNewObjectPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } } diff --git a/src/applications/transactions/editfield/PhabricatorApplyEditField.php b/src/applications/transactions/editfield/PhabricatorApplyEditField.php index d349767f94..02d4cad31f 100644 --- a/src/applications/transactions/editfield/PhabricatorApplyEditField.php +++ b/src/applications/transactions/editfield/PhabricatorApplyEditField.php @@ -1,72 +1,84 @@ actionDescription = $action_description; return $this; } public function getActionDescription() { return $this->actionDescription; } public function setActionConflictKey($action_conflict_key) { $this->actionConflictKey = $action_conflict_key; return $this; } public function getActionConflictKey() { return $this->actionConflictKey; } + public function setActionSubmitButtonText($text) { + $this->actionSubmitButtonText = $text; + return $this; + } + + public function getActionSubmitButtonText() { + return $this->actionSubmitButtonText; + } + public function setOptions(array $options) { $this->options = $options; return $this; } public function getOptions() { return $this->options; } protected function newHTTPParameterType() { if ($this->getOptions()) { return new AphrontPHIDListHTTPParameterType(); } else { return new AphrontBoolHTTPParameterType(); } } protected function newConduitParameterType() { return new ConduitBoolParameterType(); } public function shouldGenerateTransactionsFromSubmit() { // This type of edit field just applies a prebuilt action, like "Accept // Revision", and can not be submitted as part of an "Edit Object" form. return false; } protected function newCommentAction() { $options = $this->getOptions(); if ($options) { - return id(new PhabricatorEditEngineCheckboxesCommentAction()) - ->setConflictKey($this->getActionConflictKey()) + $action = id(new PhabricatorEditEngineCheckboxesCommentAction()) ->setOptions($options); } else { - return id(new PhabricatorEditEngineStaticCommentAction()) - ->setConflictKey($this->getActionConflictKey()) + $action = id(new PhabricatorEditEngineStaticCommentAction()) ->setDescription($this->getActionDescription()); } + + return $action + ->setConflictKey($this->getActionConflictKey()) + ->setSubmitButtonText($this->getActionSubmitButtonText()); } } diff --git a/src/applications/transactions/editfield/PhabricatorEditField.php b/src/applications/transactions/editfield/PhabricatorEditField.php index 07bf3589a8..7eafbc60cf 100644 --- a/src/applications/transactions/editfield/PhabricatorEditField.php +++ b/src/applications/transactions/editfield/PhabricatorEditField.php @@ -1,920 +1,920 @@ key = $key; return $this; } public function getKey() { return $this->key; } public function setLabel($label) { $this->label = $label; return $this; } public function getLabel() { return $this->label; } public function setBulkEditLabel($bulk_edit_label) { $this->bulkEditLabel = $bulk_edit_label; return $this; } public function getBulkEditLabel() { return $this->bulkEditLabel; } public function setBulkEditGroupKey($key) { $this->bulkEditGroupKey = $key; return $this; } public function getBulkEditGroupKey() { return $this->bulkEditGroupKey; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setAliases(array $aliases) { $this->aliases = $aliases; return $this; } public function getAliases() { return $this->aliases; } public function setObject($object) { $this->object = $object; return $this; } public function getObject() { return $this->object; } public function setIsLocked($is_locked) { $this->isLocked = $is_locked; return $this; } public function getIsLocked() { return $this->isLocked; } public function setIsPreview($preview) { $this->isPreview = $preview; return $this; } public function getIsPreview() { return $this->isPreview; } public function setIsReorderable($is_reorderable) { $this->isReorderable = $is_reorderable; return $this; } public function getIsReorderable() { return $this->isReorderable; } - public function setIsConduitOnly($is_conduit_only) { - $this->isConduitOnly = $is_conduit_only; + public function setIsFormField($is_form_field) { + $this->isFormField = $is_form_field; return $this; } - public function getIsConduitOnly() { - return $this->isConduitOnly; + public function getIsFormField() { + return $this->isFormField; } public function setDescription($description) { $this->description = $description; return $this; } public function getDescription() { return $this->description; } public function setConduitDescription($conduit_description) { $this->conduitDescription = $conduit_description; return $this; } public function getConduitDescription() { if ($this->conduitDescription === null) { return $this->getDescription(); } return $this->conduitDescription; } public function setConduitDocumentation($conduit_documentation) { $this->conduitDocumentation = $conduit_documentation; return $this; } public function getConduitDocumentation() { return $this->conduitDocumentation; } public function setConduitTypeDescription($conduit_type_description) { $this->conduitTypeDescription = $conduit_type_description; return $this; } public function getConduitTypeDescription() { return $this->conduitTypeDescription; } public function setIsEditDefaults($is_edit_defaults) { $this->isEditDefaults = $is_edit_defaults; return $this; } public function getIsEditDefaults() { return $this->isEditDefaults; } public function setIsDefaultable($is_defaultable) { $this->isDefaultable = $is_defaultable; return $this; } public function getIsDefaultable() { return $this->isDefaultable; } public function setIsLockable($is_lockable) { $this->isLockable = $is_lockable; return $this; } public function getIsLockable() { return $this->isLockable; } public function setIsHidden($is_hidden) { $this->isHidden = $is_hidden; return $this; } public function getIsHidden() { return $this->isHidden; } public function setIsCopyable($is_copyable) { $this->isCopyable = $is_copyable; return $this; } public function getIsCopyable() { return $this->isCopyable; } public function setIsSubmittedForm($is_submitted) { $this->isSubmittedForm = $is_submitted; return $this; } public function getIsSubmittedForm() { return $this->isSubmittedForm; } public function setIsRequired($is_required) { $this->isRequired = $is_required; return $this; } public function getIsRequired() { return $this->isRequired; } public function setControlError($control_error) { $this->controlError = $control_error; return $this; } public function getControlError() { return $this->controlError; } public function setCommentActionLabel($label) { $this->commentActionLabel = $label; return $this; } public function getCommentActionLabel() { return $this->commentActionLabel; } public function setCommentActionGroupKey($key) { $this->commentActionGroupKey = $key; return $this; } public function getCommentActionGroupKey() { return $this->commentActionGroupKey; } public function setCommentActionOrder($order) { $this->commentActionOrder = $order; return $this; } public function getCommentActionOrder() { return $this->commentActionOrder; } public function setCommentActionValue($comment_action_value) { $this->hasCommentActionValue = true; $this->commentActionValue = $comment_action_value; return $this; } public function getCommentActionValue() { return $this->commentActionValue; } public function setPreviewPanel(PHUIRemarkupPreviewPanel $preview_panel) { $this->previewPanel = $preview_panel; return $this; } public function getPreviewPanel() { if ($this->getIsHidden()) { return null; } if ($this->getIsLocked()) { return null; } return $this->previewPanel; } public function setControlInstructions($control_instructions) { $this->controlInstructions = $control_instructions; return $this; } public function getControlInstructions() { return $this->controlInstructions; } public function setCanApplyWithoutEditCapability($can_apply) { $this->canApplyWithoutEditCapability = $can_apply; return $this; } public function getCanApplyWithoutEditCapability() { return $this->canApplyWithoutEditCapability; } protected function newControl() { throw new PhutilMethodNotImplementedException(); } protected function buildControl() { - if ($this->getIsConduitOnly()) { + if (!$this->getIsFormField()) { return null; } $control = $this->newControl(); if ($control === null) { return null; } $control ->setValue($this->getValueForControl()) ->setName($this->getKey()); if (!$control->getLabel()) { $control->setLabel($this->getLabel()); } if ($this->getIsSubmittedForm()) { $error = $this->getControlError(); if ($error !== null) { $control->setError($error); } } else if ($this->getIsRequired()) { $control->setError(true); } return $control; } public function getControlID() { if (!$this->controlID) { $this->controlID = celerity_generate_unique_node_id(); } return $this->controlID; } protected function renderControl() { $control = $this->buildControl(); if ($control === null) { return null; } if ($this->getIsPreview()) { $disabled = true; $hidden = false; } else if ($this->getIsEditDefaults()) { $disabled = false; $hidden = false; } else { $disabled = $this->getIsLocked(); $hidden = $this->getIsHidden(); } if ($hidden) { return null; } $control->setDisabled($disabled); if ($this->controlID) { $control->setID($this->controlID); } return $control; } public function appendToForm(AphrontFormView $form) { $control = $this->renderControl(); if ($control !== null) { if ($this->getIsPreview()) { if ($this->getIsHidden()) { $control ->addClass('aphront-form-preview-hidden') ->setError(pht('Hidden')); } else if ($this->getIsLocked()) { $control ->setError(pht('Locked')); } } $instructions = $this->getControlInstructions(); if (strlen($instructions)) { $form->appendRemarkupInstructions($instructions); } $form->appendControl($control); } return $this; } protected function getValueForControl() { return $this->getValue(); } public function getValueForDefaults() { $value = $this->getValue(); // By default, just treat the empty string like `null` since they're // equivalent for almost all fields and this reduces the number of // meaningless transactions we generate when adjusting defaults. if ($value === '') { return null; } return $value; } protected function getValue() { return $this->value; } public function setValue($value) { $this->hasValue = true; $this->value = $value; // If we don't have an initial value set yet, use the value as the // initial value. $initial_value = $this->getInitialValue(); if ($initial_value === null) { $this->initialValue = $value; } return $this; } public function setMetadataValue($key, $value) { $this->metadata[$key] = $value; return $this; } public function getMetadata() { return $this->metadata; } public function getValueForTransaction() { return $this->getValue(); } public function getTransactionType() { return $this->transactionType; } public function setTransactionType($type) { $this->transactionType = $type; return $this; } public function readValueFromRequest(AphrontRequest $request) { $check = $this->getAllReadValueFromRequestKeys(); foreach ($check as $key) { if (!$this->getValueExistsInRequest($request, $key)) { continue; } $this->value = $this->getValueFromRequest($request, $key); break; } return $this; } public function readValueFromComment($value) { $this->value = $this->getValueFromComment($value); return $this; } protected function getValueFromComment($value) { return $value; } public function getAllReadValueFromRequestKeys() { $keys = array(); $keys[] = $this->getKey(); foreach ($this->getAliases() as $alias) { $keys[] = $alias; } return $keys; } public function readDefaultValueFromConfiguration($value) { $this->value = $this->getDefaultValueFromConfiguration($value); return $this; } protected function getDefaultValueFromConfiguration($value) { return $value; } protected function getValueFromObject($object) { if ($this->hasValue) { return $this->value; } else { return $this->getDefaultValue(); } } protected function getValueExistsInRequest(AphrontRequest $request, $key) { return $this->getHTTPParameterValueExists($request, $key); } protected function getValueFromRequest(AphrontRequest $request, $key) { return $this->getHTTPParameterValue($request, $key); } public function readValueFromField(PhabricatorEditField $other) { $this->value = $this->getValueFromField($other); return $this; } protected function getValueFromField(PhabricatorEditField $other) { return $other->getValue(); } /** * Read and return the value the object had when the user first loaded the * form. * * This is the initial value from the user's point of view when they started * the edit process, and used primarily to prevent race conditions for fields * like "Projects" and "Subscribers" that use tokenizers and support edge * transactions. * * Most fields do not need to store these values or deal with initial value * handling. * * @param AphrontRequest Request to read from. * @param string Key to read. * @return wild Value read from request. */ protected function getInitialValueFromSubmit(AphrontRequest $request, $key) { return null; } public function getInitialValue() { return $this->initialValue; } public function setInitialValue($initial_value) { $this->initialValue = $initial_value; return $this; } public function readValueFromSubmit(AphrontRequest $request) { $key = $this->getKey(); if ($this->getValueExistsInSubmit($request, $key)) { $value = $this->getValueFromSubmit($request, $key); } else { $value = $this->getDefaultValue(); } $this->value = $value; $initial_value = $this->getInitialValueFromSubmit($request, $key); $this->initialValue = $initial_value; return $this; } protected function getValueExistsInSubmit(AphrontRequest $request, $key) { return $this->getHTTPParameterValueExists($request, $key); } protected function getValueFromSubmit(AphrontRequest $request, $key) { return $this->getHTTPParameterValue($request, $key); } protected function getHTTPParameterValueExists( AphrontRequest $request, $key) { $type = $this->getHTTPParameterType(); if ($type) { return $type->getExists($request, $key); } return false; } protected function getHTTPParameterValue($request, $key) { $type = $this->getHTTPParameterType(); if ($type) { return $type->getValue($request, $key); } return null; } protected function getDefaultValue() { $type = $this->getHTTPParameterType(); if ($type) { return $type->getDefaultValue(); } return null; } final public function getHTTPParameterType() { - if ($this->getIsConduitOnly()) { + if (!$this->getIsFormField()) { return null; } $type = $this->newHTTPParameterType(); if ($type) { $type->setViewer($this->getViewer()); } return $type; } protected function newHTTPParameterType() { return new AphrontStringHTTPParameterType(); } protected function getBulkParameterType() { $type = $this->newBulkParameterType(); if (!$type) { return null; } $type ->setField($this) ->setViewer($this->getViewer()); return $type; } protected function newBulkParameterType() { return null; } public function getConduitParameterType() { $type = $this->newConduitParameterType(); if (!$type) { return null; } $type->setViewer($this->getViewer()); return $type; } abstract protected function newConduitParameterType(); public function setEditTypeKey($edit_type_key) { $this->editTypeKey = $edit_type_key; return $this; } public function getEditTypeKey() { if ($this->editTypeKey === null) { return $this->getKey(); } return $this->editTypeKey; } protected function newEditType() { return new PhabricatorSimpleEditType(); } protected function getEditType() { $transaction_type = $this->getTransactionType(); if ($transaction_type === null) { return null; } $edit_type = $this->newEditType(); if (!$edit_type) { return null; } $type_key = $this->getEditTypeKey(); $edit_type ->setEditField($this) ->setTransactionType($transaction_type) ->setEditType($type_key) ->setMetadata($this->getMetadata()); if (!$edit_type->getConduitParameterType()) { $conduit_parameter = $this->getConduitParameterType(); if ($conduit_parameter) { $edit_type->setConduitParameterType($conduit_parameter); } } if (!$edit_type->getBulkParameterType()) { $bulk_parameter = $this->getBulkParameterType(); if ($bulk_parameter) { $edit_type->setBulkParameterType($bulk_parameter); } } return $edit_type; } final public function getConduitEditTypes() { if ($this->conduitEditTypes === null) { $edit_types = $this->newConduitEditTypes(); $edit_types = mpull($edit_types, null, 'getEditType'); $this->conduitEditTypes = $edit_types; } return $this->conduitEditTypes; } final public function getConduitEditType($key) { $edit_types = $this->getConduitEditTypes(); if (empty($edit_types[$key])) { throw new Exception( pht( 'This EditField does not provide a Conduit EditType with key "%s".', $key)); } return $edit_types[$key]; } protected function newConduitEditTypes() { $edit_type = $this->getEditType(); if (!$edit_type) { return array(); } return array($edit_type); } final public function getBulkEditTypes() { if ($this->bulkEditTypes === null) { $edit_types = $this->newBulkEditTypes(); $edit_types = mpull($edit_types, null, 'getEditType'); $this->bulkEditTypes = $edit_types; } return $this->bulkEditTypes; } final public function getBulkEditType($key) { $edit_types = $this->getBulkEditTypes(); if (empty($edit_types[$key])) { throw new Exception( pht( 'This EditField does not provide a Bulk EditType with key "%s".', $key)); } return $edit_types[$key]; } protected function newBulkEditTypes() { $edit_type = $this->getEditType(); if (!$edit_type) { return array(); } return array($edit_type); } public function getCommentAction() { $label = $this->getCommentActionLabel(); if ($label === null) { return null; } $action = $this->newCommentAction(); if ($action === null) { return null; } if ($this->hasCommentActionValue) { $value = $this->getCommentActionValue(); } else { $value = $this->getValue(); } $action ->setKey($this->getKey()) ->setLabel($label) ->setValue($this->getValueForCommentAction($value)) ->setOrder($this->getCommentActionOrder()) ->setGroupKey($this->getCommentActionGroupKey()); return $action; } protected function newCommentAction() { return null; } protected function getValueForCommentAction($value) { return $value; } public function shouldGenerateTransactionsFromSubmit() { - if ($this->getIsConduitOnly()) { + if (!$this->getIsFormField()) { return false; } $edit_type = $this->getEditType(); if (!$edit_type) { return false; } return true; } public function shouldReadValueFromRequest() { - if ($this->getIsConduitOnly()) { + if (!$this->getIsFormField()) { return false; } if ($this->getIsLocked()) { return false; } if ($this->getIsHidden()) { return false; } return true; } public function shouldReadValueFromSubmit() { - if ($this->getIsConduitOnly()) { + if (!$this->getIsFormField()) { return false; } if ($this->getIsLocked()) { return false; } if ($this->getIsHidden()) { return false; } return true; } public function shouldGenerateTransactionsFromComment() { - if ($this->getIsConduitOnly()) { + if (!$this->getCommentActionLabel()) { return false; } if ($this->getIsLocked()) { return false; } if ($this->getIsHidden()) { return false; } return true; } public function generateTransactions( PhabricatorApplicationTransaction $template, array $spec) { $edit_type = $this->getEditType(); if (!$edit_type) { throw new Exception( pht( 'EditField (with key "%s", of class "%s") is generating '. 'transactions, but has no EditType.', $this->getKey(), get_class($this))); } return $edit_type->generateTransactions($template, $spec); } } diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index b4dd09ed10..a77b7f84a4 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -1,4576 +1,4597 @@ actingAsPHID = $acting_as_phid; return $this; } public function getActingAsPHID() { if ($this->actingAsPHID) { return $this->actingAsPHID; } return $this->getActor()->getPHID(); } /** * When the editor tries to apply transactions that have no effect, should * it raise an exception (default) or drop them and continue? * * Generally, you will set this flag for edits coming from "Edit" interfaces, * and leave it cleared for edits coming from "Comment" interfaces, so the * user will get a useful error if they try to submit a comment that does * nothing (e.g., empty comment with a status change that has already been * performed by another user). * * @param bool True to drop transactions without effect and continue. * @return this */ public function setContinueOnNoEffect($continue) { $this->continueOnNoEffect = $continue; return $this; } public function getContinueOnNoEffect() { return $this->continueOnNoEffect; } /** * When the editor tries to apply transactions which don't populate all of * an object's required fields, should it raise an exception (default) or * drop them and continue? * * For example, if a user adds a new required custom field (like "Severity") * to a task, all existing tasks won't have it populated. When users * manually edit existing tasks, it's usually desirable to have them provide * a severity. However, other operations (like batch editing just the * owner of a task) will fail by default. * * By setting this flag for edit operations which apply to specific fields * (like the priority, batch, and merge editors in Maniphest), these * operations can continue to function even if an object is outdated. * * @param bool True to continue when transactions don't completely satisfy * all required fields. * @return this */ public function setContinueOnMissingFields($continue_on_missing_fields) { $this->continueOnMissingFields = $continue_on_missing_fields; return $this; } public function getContinueOnMissingFields() { return $this->continueOnMissingFields; } /** * Not strictly necessary, but reply handlers ideally set this value to * make email threading work better. */ public function setParentMessageID($parent_message_id) { $this->parentMessageID = $parent_message_id; return $this; } public function getParentMessageID() { return $this->parentMessageID; } public function getIsNewObject() { return $this->isNewObject; } public function getMentionedPHIDs() { return $this->mentionedPHIDs; } public function setIsPreview($is_preview) { $this->isPreview = $is_preview; return $this; } public function getIsPreview() { return $this->isPreview; } public function setIsSilent($silent) { $this->silent = $silent; return $this; } public function getIsSilent() { return $this->silent; } public function getMustEncrypt() { return $this->mustEncrypt; } public function getHeraldRuleMonograms() { // Convert the stored "<123>, <456>" string into a list: "H123", "H456". $list = $this->heraldHeader; $list = preg_split('/[, ]+/', $list); foreach ($list as $key => $item) { $item = trim($item, '<>'); if (!is_numeric($item)) { unset($list[$key]); continue; } $list[$key] = 'H'.$item; } return $list; } public function setIsInverseEdgeEditor($is_inverse_edge_editor) { $this->isInverseEdgeEditor = $is_inverse_edge_editor; return $this; } public function getIsInverseEdgeEditor() { return $this->isInverseEdgeEditor; } public function setIsHeraldEditor($is_herald_editor) { $this->isHeraldEditor = $is_herald_editor; return $this; } public function getIsHeraldEditor() { return $this->isHeraldEditor; } public function setUnmentionablePHIDMap(array $map) { $this->unmentionablePHIDMap = $map; return $this; } public function getUnmentionablePHIDMap() { return $this->unmentionablePHIDMap; } protected function shouldEnableMentions( PhabricatorLiskDAO $object, array $xactions) { return true; } public function setApplicationEmail( PhabricatorMetaMTAApplicationEmail $email) { $this->applicationEmail = $email; return $this; } public function getApplicationEmail() { return $this->applicationEmail; } public function setRaiseWarnings($raise_warnings) { $this->raiseWarnings = $raise_warnings; return $this; } public function getRaiseWarnings() { return $this->raiseWarnings; } public function getTransactionTypesForObject($object) { $old = $this->object; try { $this->object = $object; $result = $this->getTransactionTypes(); $this->object = $old; } catch (Exception $ex) { $this->object = $old; throw $ex; } return $result; } public function getTransactionTypes() { $types = array(); $types[] = PhabricatorTransactions::TYPE_CREATE; $types[] = PhabricatorTransactions::TYPE_HISTORY; if ($this->object instanceof PhabricatorEditEngineSubtypeInterface) { $types[] = PhabricatorTransactions::TYPE_SUBTYPE; } if ($this->object instanceof PhabricatorSubscribableInterface) { $types[] = PhabricatorTransactions::TYPE_SUBSCRIBERS; } if ($this->object instanceof PhabricatorCustomFieldInterface) { $types[] = PhabricatorTransactions::TYPE_CUSTOMFIELD; } if ($this->object instanceof PhabricatorTokenReceiverInterface) { $types[] = PhabricatorTransactions::TYPE_TOKEN; } if ($this->object instanceof PhabricatorProjectInterface || $this->object instanceof PhabricatorMentionableInterface) { $types[] = PhabricatorTransactions::TYPE_EDGE; } if ($this->object instanceof PhabricatorSpacesInterface) { $types[] = PhabricatorTransactions::TYPE_SPACE; } $template = $this->object->getApplicationTransactionTemplate(); if ($template instanceof PhabricatorModularTransaction) { $xtypes = $template->newModularTransactionTypes(); foreach ($xtypes as $xtype) { $types[] = $xtype->getTransactionTypeConstant(); } } if ($template) { try { $comment = $template->getApplicationTransactionCommentObject(); } catch (PhutilMethodNotImplementedException $ex) { $comment = null; } if ($comment) { $types[] = PhabricatorTransactions::TYPE_COMMENT; } } return $types; } private function adjustTransactionValues( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { if ($xaction->shouldGenerateOldValue()) { $old = $this->getTransactionOldValue($object, $xaction); $xaction->setOldValue($old); } $new = $this->getTransactionNewValue($object, $xaction); $xaction->setNewValue($new); } private function getTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $type = $xaction->getTransactionType(); $xtype = $this->getModularTransactionType($type); if ($xtype) { $xtype = clone $xtype; $xtype->setStorage($xaction); return $xtype->generateOldValue($object); } switch ($type) { case PhabricatorTransactions::TYPE_CREATE: case PhabricatorTransactions::TYPE_HISTORY: return null; case PhabricatorTransactions::TYPE_SUBTYPE: return $object->getEditEngineSubtype(); case PhabricatorTransactions::TYPE_SUBSCRIBERS: return array_values($this->subscribers); case PhabricatorTransactions::TYPE_VIEW_POLICY: if ($this->getIsNewObject()) { return null; } return $object->getViewPolicy(); case PhabricatorTransactions::TYPE_EDIT_POLICY: if ($this->getIsNewObject()) { return null; } return $object->getEditPolicy(); case PhabricatorTransactions::TYPE_JOIN_POLICY: if ($this->getIsNewObject()) { return null; } return $object->getJoinPolicy(); case PhabricatorTransactions::TYPE_SPACE: if ($this->getIsNewObject()) { return null; } $space_phid = $object->getSpacePHID(); if ($space_phid === null) { $default_space = PhabricatorSpacesNamespaceQuery::getDefaultSpace(); if ($default_space) { $space_phid = $default_space->getPHID(); } } return $space_phid; case PhabricatorTransactions::TYPE_EDGE: $edge_type = $xaction->getMetadataValue('edge:type'); if (!$edge_type) { throw new Exception( pht( "Edge transaction has no '%s'!", 'edge:type')); } $old_edges = array(); if ($object->getPHID()) { $edge_src = $object->getPHID(); $old_edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($edge_src)) ->withEdgeTypes(array($edge_type)) ->needEdgeData(true) ->execute(); $old_edges = $old_edges[$edge_src][$edge_type]; } return $old_edges; case PhabricatorTransactions::TYPE_CUSTOMFIELD: // NOTE: Custom fields have their old value pre-populated when they are // built by PhabricatorCustomFieldList. return $xaction->getOldValue(); case PhabricatorTransactions::TYPE_COMMENT: return null; default: return $this->getCustomTransactionOldValue($object, $xaction); } } private function getTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $type = $xaction->getTransactionType(); $xtype = $this->getModularTransactionType($type); if ($xtype) { $xtype = clone $xtype; $xtype->setStorage($xaction); return $xtype->generateNewValue($object, $xaction->getNewValue()); } switch ($type) { case PhabricatorTransactions::TYPE_CREATE: return null; case PhabricatorTransactions::TYPE_SUBSCRIBERS: return $this->getPHIDTransactionNewValue($xaction); case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: case PhabricatorTransactions::TYPE_TOKEN: case PhabricatorTransactions::TYPE_INLINESTATE: case PhabricatorTransactions::TYPE_SUBTYPE: case PhabricatorTransactions::TYPE_HISTORY: return $xaction->getNewValue(); case PhabricatorTransactions::TYPE_SPACE: $space_phid = $xaction->getNewValue(); if (!strlen($space_phid)) { // If an install has no Spaces or the Spaces controls are not visible // to the viewer, we might end up with the empty string here instead // of a strict `null`, because some controller just used `getStr()` // to read the space PHID from the request. // Just make this work like callers might reasonably expect so we // don't need to handle this specially in every EditController. return $this->getActor()->getDefaultSpacePHID(); } else { return $space_phid; } case PhabricatorTransactions::TYPE_EDGE: $new_value = $this->getEdgeTransactionNewValue($xaction); $edge_type = $xaction->getMetadataValue('edge:type'); $type_project = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; if ($edge_type == $type_project) { $new_value = $this->applyProjectConflictRules($new_value); } return $new_value; case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getCustomFieldForTransaction($object, $xaction); return $field->getNewValueFromApplicationTransactions($xaction); case PhabricatorTransactions::TYPE_COMMENT: return null; default: return $this->getCustomTransactionNewValue($object, $xaction); } } protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { throw new Exception(pht('Capability not supported!')); } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { throw new Exception(pht('Capability not supported!')); } protected function transactionHasEffect( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_CREATE: case PhabricatorTransactions::TYPE_HISTORY: return true; case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getCustomFieldForTransaction($object, $xaction); return $field->getApplicationTransactionHasEffect($xaction); case PhabricatorTransactions::TYPE_EDGE: // A straight value comparison here doesn't always get the right // result, because newly added edges aren't fully populated. Instead, // compare the changes in a more granular way. $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $old_dst = array_keys($old); $new_dst = array_keys($new); // NOTE: For now, we don't consider edge reordering to be a change. // We have very few order-dependent edges and effectively no order // oriented UI. This might change in the future. sort($old_dst); sort($new_dst); if ($old_dst !== $new_dst) { // We've added or removed edges, so this transaction definitely // has an effect. return true; } // We haven't added or removed edges, but we might have changed // edge data. foreach ($old as $key => $old_value) { $new_value = $new[$key]; if ($old_value['data'] !== $new_value['data']) { return true; } } return false; } $type = $xaction->getTransactionType(); $xtype = $this->getModularTransactionType($type); if ($xtype) { return $xtype->getTransactionHasEffect( $object, $xaction->getOldValue(), $xaction->getNewValue()); } if ($xaction->hasComment()) { return true; } return ($xaction->getOldValue() !== $xaction->getNewValue()); } protected function shouldApplyInitialEffects( PhabricatorLiskDAO $object, array $xactions) { return false; } protected function applyInitialEffects( PhabricatorLiskDAO $object, array $xactions) { throw new PhutilMethodNotImplementedException(); } private function applyInternalEffects( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $type = $xaction->getTransactionType(); $xtype = $this->getModularTransactionType($type); if ($xtype) { $xtype = clone $xtype; $xtype->setStorage($xaction); return $xtype->applyInternalEffects($object, $xaction->getNewValue()); } switch ($type) { case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getCustomFieldForTransaction($object, $xaction); return $field->applyApplicationTransactionInternalEffects($xaction); case PhabricatorTransactions::TYPE_CREATE: case PhabricatorTransactions::TYPE_HISTORY: case PhabricatorTransactions::TYPE_SUBTYPE: case PhabricatorTransactions::TYPE_TOKEN: case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: case PhabricatorTransactions::TYPE_SUBSCRIBERS: case PhabricatorTransactions::TYPE_INLINESTATE: case PhabricatorTransactions::TYPE_EDGE: case PhabricatorTransactions::TYPE_SPACE: case PhabricatorTransactions::TYPE_COMMENT: return $this->applyBuiltinInternalTransaction($object, $xaction); } return $this->applyCustomInternalTransaction($object, $xaction); } private function applyExternalEffects( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $type = $xaction->getTransactionType(); $xtype = $this->getModularTransactionType($type); if ($xtype) { $xtype = clone $xtype; $xtype->setStorage($xaction); return $xtype->applyExternalEffects($object, $xaction->getNewValue()); } switch ($type) { case PhabricatorTransactions::TYPE_SUBSCRIBERS: $subeditor = id(new PhabricatorSubscriptionsEditor()) ->setObject($object) ->setActor($this->requireActor()); $old_map = array_fuse($xaction->getOldValue()); $new_map = array_fuse($xaction->getNewValue()); $subeditor->unsubscribe( array_keys( array_diff_key($old_map, $new_map))); $subeditor->subscribeExplicit( array_keys( array_diff_key($new_map, $old_map))); $subeditor->save(); // for the rest of these edits, subscribers should include those just // added as well as those just removed. $subscribers = array_unique(array_merge( $this->subscribers, $xaction->getOldValue(), $xaction->getNewValue())); $this->subscribers = $subscribers; return $this->applyBuiltinExternalTransaction($object, $xaction); case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getCustomFieldForTransaction($object, $xaction); return $field->applyApplicationTransactionExternalEffects($xaction); case PhabricatorTransactions::TYPE_CREATE: case PhabricatorTransactions::TYPE_HISTORY: case PhabricatorTransactions::TYPE_SUBTYPE: case PhabricatorTransactions::TYPE_EDGE: case PhabricatorTransactions::TYPE_TOKEN: case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: case PhabricatorTransactions::TYPE_INLINESTATE: case PhabricatorTransactions::TYPE_SPACE: case PhabricatorTransactions::TYPE_COMMENT: return $this->applyBuiltinExternalTransaction($object, $xaction); } return $this->applyCustomExternalTransaction($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $type = $xaction->getTransactionType(); throw new Exception( pht( "Transaction type '%s' is missing an internal apply implementation!", $type)); } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $type = $xaction->getTransactionType(); throw new Exception( pht( "Transaction type '%s' is missing an external apply implementation!", $type)); } /** * @{class:PhabricatorTransactions} provides many built-in transactions * which should not require much - if any - code in specific applications. * * This method is a hook for the exceedingly-rare cases where you may need * to do **additional** work for built-in transactions. Developers should * extend this method, making sure to return the parent implementation * regardless of handling any transactions. * * See also @{method:applyBuiltinExternalTransaction}. */ protected function applyBuiltinInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_VIEW_POLICY: $object->setViewPolicy($xaction->getNewValue()); break; case PhabricatorTransactions::TYPE_EDIT_POLICY: $object->setEditPolicy($xaction->getNewValue()); break; case PhabricatorTransactions::TYPE_JOIN_POLICY: $object->setJoinPolicy($xaction->getNewValue()); break; case PhabricatorTransactions::TYPE_SPACE: $object->setSpacePHID($xaction->getNewValue()); break; case PhabricatorTransactions::TYPE_SUBTYPE: $object->setEditEngineSubtype($xaction->getNewValue()); break; } } /** * See @{method::applyBuiltinInternalTransaction}. */ protected function applyBuiltinExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_EDGE: if ($this->getIsInverseEdgeEditor()) { // If we're writing an inverse edge transaction, don't actually // do anything. The initiating editor on the other side of the // transaction will take care of the edge writes. break; } $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $src = $object->getPHID(); $const = $xaction->getMetadataValue('edge:type'); $type = PhabricatorEdgeType::getByConstant($const); if ($type->shouldWriteInverseTransactions()) { $this->applyInverseEdgeTransactions( $object, $xaction, $type->getInverseEdgeConstant()); } foreach ($new as $dst_phid => $edge) { $new[$dst_phid]['src'] = $src; } $editor = new PhabricatorEdgeEditor(); foreach ($old as $dst_phid => $edge) { if (!empty($new[$dst_phid])) { if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) { continue; } } $editor->removeEdge($src, $const, $dst_phid); } foreach ($new as $dst_phid => $edge) { if (!empty($old[$dst_phid])) { if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) { continue; } } $data = array( 'data' => $edge['data'], ); $editor->addEdge($src, $const, $dst_phid, $data); } $editor->save(); $this->updateWorkboardColumns($object, $const, $old, $new); break; case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_SPACE: $this->scrambleFileSecrets($object); break; case PhabricatorTransactions::TYPE_HISTORY: $this->sendHistory = true; break; } } /** * Fill in a transaction's common values, like author and content source. */ protected function populateTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $actor = $this->getActor(); // TODO: This needs to be more sophisticated once we have meta-policies. $xaction->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC); if ($actor->isOmnipotent()) { $xaction->setEditPolicy(PhabricatorPolicies::POLICY_NOONE); } else { $xaction->setEditPolicy($this->getActingAsPHID()); } // If the transaction already has an explicit author PHID, allow it to // stand. This is used by applications like Owners that hook into the // post-apply change pipeline. if (!$xaction->getAuthorPHID()) { $xaction->setAuthorPHID($this->getActingAsPHID()); } $xaction->setContentSource($this->getContentSource()); $xaction->attachViewer($actor); $xaction->attachObject($object); if ($object->getPHID()) { $xaction->setObjectPHID($object->getPHID()); } if ($this->getIsSilent()) { $xaction->setIsSilentTransaction(true); } if ($actor->hasHighSecuritySession()) { $xaction->setIsMFATransaction(true); } return $xaction; } protected function didApplyInternalEffects( PhabricatorLiskDAO $object, array $xactions) { return $xactions; } protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { return $xactions; } + final protected function didCommitTransactions( + PhabricatorLiskDAO $object, + array $xactions) { + + foreach ($xactions as $xaction) { + $type = $xaction->getTransactionType(); + + $xtype = $this->getModularTransactionType($type); + if (!$xtype) { + continue; + } + + $xtype = clone $xtype; + $xtype->setStorage($xaction); + $xtype->didCommitTransaction($object, $xaction->getNewValue()); + } + } + public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source; return $this; } public function setContentSourceFromRequest(AphrontRequest $request) { return $this->setContentSource( PhabricatorContentSource::newFromRequest($request)); } public function getContentSource() { return $this->contentSource; } final public function applyTransactions( PhabricatorLiskDAO $object, array $xactions) { $this->object = $object; $this->xactions = $xactions; $this->isNewObject = ($object->getPHID() === null); $this->validateEditParameters($object, $xactions); $actor = $this->requireActor(); // NOTE: Some transaction expansion requires that the edited object be // attached. foreach ($xactions as $xaction) { $xaction->attachObject($object); $xaction->attachViewer($actor); } $xactions = $this->expandTransactions($object, $xactions); $xactions = $this->expandSupportTransactions($object, $xactions); $xactions = $this->combineTransactions($xactions); foreach ($xactions as $xaction) { $xaction = $this->populateTransaction($object, $xaction); } $is_preview = $this->getIsPreview(); $read_locking = false; $transaction_open = false; if (!$is_preview) { $errors = array(); $type_map = mgroup($xactions, 'getTransactionType'); foreach ($this->getTransactionTypes() as $type) { $type_xactions = idx($type_map, $type, array()); $errors[] = $this->validateTransaction($object, $type, $type_xactions); } $errors[] = $this->validateAllTransactions($object, $xactions); $errors = array_mergev($errors); $continue_on_missing = $this->getContinueOnMissingFields(); foreach ($errors as $key => $error) { if ($continue_on_missing && $error->getIsMissingFieldError()) { unset($errors[$key]); } } if ($errors) { throw new PhabricatorApplicationTransactionValidationException($errors); } if ($this->raiseWarnings) { $warnings = array(); foreach ($xactions as $xaction) { if ($this->hasWarnings($object, $xaction)) { $warnings[] = $xaction; } } if ($warnings) { throw new PhabricatorApplicationTransactionWarningException( $warnings); } } $this->willApplyTransactions($object, $xactions); if ($object->getID()) { $this->buildOldRecipientLists($object, $xactions); $object->openTransaction(); $transaction_open = true; $object->beginReadLocking(); $read_locking = true; $object->reload(); } if ($this->shouldApplyInitialEffects($object, $xactions)) { if (!$transaction_open) { $object->openTransaction(); $transaction_open = true; } } } try { if ($this->shouldApplyInitialEffects($object, $xactions)) { $this->applyInitialEffects($object, $xactions); } foreach ($xactions as $xaction) { $this->adjustTransactionValues($object, $xaction); } // Now that we've merged and combined transactions, check for required // capabilities. Note that we're doing this before filtering // transactions: if you try to apply an edit which you do not have // permission to apply, we want to give you a permissions error even // if the edit would have no effect. $this->applyCapabilityChecks($object, $xactions); // See T13186. Fatal hard if this object has an older // "requireCapabilities()" method. The code may rely on this method being // called to apply policy checks, so err on the side of safety and fatal. // TODO: Remove this check after some time has passed. if (method_exists($this, 'requireCapabilities')) { throw new Exception( pht( 'Editor (of class "%s") implements obsolete policy method '. 'requireCapabilities(). The implementation for this Editor '. 'MUST be updated. See <%s> for discussion.', get_class($this), 'https://secure.phabricator.com/T13186')); } $xactions = $this->filterTransactions($object, $xactions); // TODO: Once everything is on EditEngine, just use getIsNewObject() to // figure this out instead. $mark_as_create = false; $create_type = PhabricatorTransactions::TYPE_CREATE; foreach ($xactions as $xaction) { if ($xaction->getTransactionType() == $create_type) { $mark_as_create = true; } } if ($mark_as_create) { foreach ($xactions as $xaction) { $xaction->setIsCreateTransaction(true); } } $xactions = $this->sortTransactions($xactions); $file_phids = $this->extractFilePHIDs($object, $xactions); if ($is_preview) { $this->loadHandles($xactions); return $xactions; } $comment_editor = id(new PhabricatorApplicationTransactionCommentEditor()) ->setActor($actor) ->setActingAsPHID($this->getActingAsPHID()) ->setContentSource($this->getContentSource()); if (!$transaction_open) { $object->openTransaction(); $transaction_open = true; } foreach ($xactions as $xaction) { $this->applyInternalEffects($object, $xaction); } $xactions = $this->didApplyInternalEffects($object, $xactions); try { $object->save(); } catch (AphrontDuplicateKeyQueryException $ex) { // This callback has an opportunity to throw a better exception, // so execution may end here. $this->didCatchDuplicateKeyException($object, $xactions, $ex); throw $ex; } foreach ($xactions as $xaction) { $xaction->setObjectPHID($object->getPHID()); if ($xaction->getComment()) { $xaction->setPHID($xaction->generatePHID()); $comment_editor->applyEdit($xaction, $xaction->getComment()); } else { // TODO: This is a transitional hack to let us migrate edge // transactions to a more efficient storage format. For now, we're // going to write a new slim format to the database but keep the old // bulky format on the objects so we don't have to upgrade all the // edit logic to the new format yet. See T13051. $edge_type = PhabricatorTransactions::TYPE_EDGE; if ($xaction->getTransactionType() == $edge_type) { $bulky_old = $xaction->getOldValue(); $bulky_new = $xaction->getNewValue(); $record = PhabricatorEdgeChangeRecord::newFromTransaction($xaction); $slim_old = $record->getModernOldEdgeTransactionData(); $slim_new = $record->getModernNewEdgeTransactionData(); $xaction->setOldValue($slim_old); $xaction->setNewValue($slim_new); $xaction->save(); $xaction->setOldValue($bulky_old); $xaction->setNewValue($bulky_new); } else { $xaction->save(); } } } if ($file_phids) { $this->attachFiles($object, $file_phids); } foreach ($xactions as $xaction) { $this->applyExternalEffects($object, $xaction); } $xactions = $this->applyFinalEffects($object, $xactions); if ($read_locking) { $object->endReadLocking(); $read_locking = false; } if ($transaction_open) { $object->saveTransaction(); $transaction_open = false; } + + $this->didCommitTransactions($object, $xactions); + } catch (Exception $ex) { if ($read_locking) { $object->endReadLocking(); $read_locking = false; } if ($transaction_open) { $object->killTransaction(); $transaction_open = false; } throw $ex; } // If we need to perform cache engine updates, execute them now. id(new PhabricatorCacheEngine()) ->updateObject($object); // Now that we've completely applied the core transaction set, try to apply // Herald rules. Herald rules are allowed to either take direct actions on // the database (like writing flags), or take indirect actions (like saving // some targets for CC when we generate mail a little later), or return // transactions which we'll apply normally using another Editor. // First, check if *this* is a sub-editor which is itself applying Herald // rules: if it is, stop working and return so we don't descend into // madness. // Otherwise, we're not a Herald editor, so process Herald rules (possibly // using a Herald editor to apply resulting transactions) and then send out // mail, notifications, and feed updates about everything. if ($this->getIsHeraldEditor()) { // We are the Herald editor, so stop work here and return the updated // transactions. return $xactions; } else if ($this->getIsInverseEdgeEditor()) { // Do not run Herald if we're just recording that this object was // mentioned elsewhere. This tends to create Herald side effects which // feel arbitrary, and can really slow down edits which mention a large // number of other objects. See T13114. } else if ($this->shouldApplyHeraldRules($object, $xactions)) { // We are not the Herald editor, so try to apply Herald rules. $herald_xactions = $this->applyHeraldRules($object, $xactions); if ($herald_xactions) { $xscript_id = $this->getHeraldTranscript()->getID(); foreach ($herald_xactions as $herald_xaction) { // Don't set a transcript ID if this is a transaction from another // application or source, like Owners. if ($herald_xaction->getAuthorPHID()) { continue; } $herald_xaction->setMetadataValue('herald:transcriptID', $xscript_id); } // NOTE: We're acting as the omnipotent user because rules deal with // their own policy issues. We use a synthetic author PHID (the // Herald application) as the author of record, so that transactions // will render in a reasonable way ("Herald assigned this task ..."). $herald_actor = PhabricatorUser::getOmnipotentUser(); $herald_phid = id(new PhabricatorHeraldApplication())->getPHID(); // TODO: It would be nice to give transactions a more specific source // which points at the rule which generated them. You can figure this // out from transcripts, but it would be cleaner if you didn't have to. $herald_source = PhabricatorContentSource::newForSource( PhabricatorHeraldContentSource::SOURCECONST); $herald_editor = newv(get_class($this), array()) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->setParentMessageID($this->getParentMessageID()) ->setIsHeraldEditor(true) ->setActor($herald_actor) ->setActingAsPHID($herald_phid) ->setContentSource($herald_source); $herald_xactions = $herald_editor->applyTransactions( $object, $herald_xactions); // Merge the new transactions into the transaction list: we want to // send email and publish feed stories about them, too. $xactions = array_merge($xactions, $herald_xactions); } // If Herald did not generate transactions, we may still need to handle // "Send an Email" rules. $adapter = $this->getHeraldAdapter(); $this->heraldEmailPHIDs = $adapter->getEmailPHIDs(); $this->heraldForcedEmailPHIDs = $adapter->getForcedEmailPHIDs(); $this->webhookMap = $adapter->getWebhookMap(); } $xactions = $this->didApplyTransactions($object, $xactions); if ($object instanceof PhabricatorCustomFieldInterface) { // Maybe this makes more sense to move into the search index itself? For // now I'm putting it here since I think we might end up with things that // need it to be up to date once the next page loads, but if we don't go // there we could move it into search once search moves to the daemons. // It now happens in the search indexer as well, but the search indexer is // always daemonized, so the logic above still potentially holds. We could // possibly get rid of this. The major motivation for putting it in the // indexer was to enable reindexing to work. $fields = PhabricatorCustomField::getObjectFields( $object, PhabricatorCustomField::ROLE_APPLICATIONSEARCH); $fields->readFieldsFromStorage($object); $fields->rebuildIndexes($object); } $herald_xscript = $this->getHeraldTranscript(); if ($herald_xscript) { $herald_header = $herald_xscript->getXHeraldRulesHeader(); $herald_header = HeraldTranscript::saveXHeraldRulesHeader( $object->getPHID(), $herald_header); } else { $herald_header = HeraldTranscript::loadXHeraldRulesHeader( $object->getPHID()); } $this->heraldHeader = $herald_header; // We're going to compute some of the data we'll use to publish these // transactions here, before queueing a worker. // // Primarily, this is more correct: we want to publish the object as it // exists right now. The worker may not execute for some time, and we want // to use the current To/CC list, not respect any changes which may occur // between now and when the worker executes. // // As a secondary benefit, this tends to reduce the amount of state that // Editors need to pass into workers. $object = $this->willPublish($object, $xactions); if (!$this->getIsSilent()) { if ($this->shouldSendMail($object, $xactions)) { $this->mailShouldSend = true; $this->mailToPHIDs = $this->getMailTo($object); $this->mailCCPHIDs = $this->getMailCC($object); $this->mailUnexpandablePHIDs = $this->newMailUnexpandablePHIDs($object); // Add any recipients who were previously on the notification list // but were removed by this change. $this->applyOldRecipientLists(); if ($object instanceof PhabricatorSubscribableInterface) { $this->mailMutedPHIDs = PhabricatorEdgeQuery::loadDestinationPHIDs( $object->getPHID(), PhabricatorMutedByEdgeType::EDGECONST); } else { $this->mailMutedPHIDs = array(); } $mail_xactions = $this->getTransactionsForMail($object, $xactions); $stamps = $this->newMailStamps($object, $xactions); foreach ($stamps as $stamp) { $this->mailStamps[] = $stamp->toDictionary(); } } if ($this->shouldPublishFeedStory($object, $xactions)) { $this->feedShouldPublish = true; $this->feedRelatedPHIDs = $this->getFeedRelatedPHIDs( $object, $xactions); $this->feedNotifyPHIDs = $this->getFeedNotifyPHIDs( $object, $xactions); } } PhabricatorWorker::scheduleTask( 'PhabricatorApplicationTransactionPublishWorker', array( 'objectPHID' => $object->getPHID(), 'actorPHID' => $this->getActingAsPHID(), 'xactionPHIDs' => mpull($xactions, 'getPHID'), 'state' => $this->getWorkerState(), ), array( 'objectPHID' => $object->getPHID(), 'priority' => PhabricatorWorker::PRIORITY_ALERTS, )); $this->flushTransactionQueue($object); return $xactions; } protected function didCatchDuplicateKeyException( PhabricatorLiskDAO $object, array $xactions, Exception $ex) { return; } public function publishTransactions( PhabricatorLiskDAO $object, array $xactions) { $this->object = $object; $this->xactions = $xactions; // Hook for edges or other properties that may need (re-)loading $object = $this->willPublish($object, $xactions); // The object might have changed, so reassign it. $this->object = $object; $messages = array(); if ($this->mailShouldSend) { $messages = $this->buildMail($object, $xactions); } if ($this->supportsSearch()) { PhabricatorSearchWorker::queueDocumentForIndexing( $object->getPHID(), array( 'transactionPHIDs' => mpull($xactions, 'getPHID'), )); } if ($this->feedShouldPublish) { $mailed = array(); foreach ($messages as $mail) { foreach ($mail->buildRecipientList() as $phid) { $mailed[$phid] = $phid; } } $this->publishFeedStory($object, $xactions, $mailed); } if ($this->sendHistory) { $history_mail = $this->buildHistoryMail($object); if ($history_mail) { $messages[] = $history_mail; } } // NOTE: This actually sends the mail. We do this last to reduce the chance // that we send some mail, hit an exception, then send the mail again when // retrying. foreach ($messages as $mail) { $mail->save(); } $this->queueWebhooks($object, $xactions); return $xactions; } protected function didApplyTransactions($object, array $xactions) { // Hook for subclasses. return $xactions; } private function loadHandles(array $xactions) { $phids = array(); foreach ($xactions as $key => $xaction) { $phids[$key] = $xaction->getRequiredHandlePHIDs(); } $handles = array(); $merged = array_mergev($phids); if ($merged) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->requireActor()) ->withPHIDs($merged) ->execute(); } foreach ($xactions as $key => $xaction) { $xaction->setHandles(array_select_keys($handles, $phids[$key])); } } private function loadSubscribers(PhabricatorLiskDAO $object) { if ($object->getPHID() && ($object instanceof PhabricatorSubscribableInterface)) { $subs = PhabricatorSubscribersQuery::loadSubscribersForPHID( $object->getPHID()); $this->subscribers = array_fuse($subs); } else { $this->subscribers = array(); } } private function validateEditParameters( PhabricatorLiskDAO $object, array $xactions) { if (!$this->getContentSource()) { throw new PhutilInvalidStateException('setContentSource'); } // Do a bunch of sanity checks that the incoming transactions are fresh. // They should be unsaved and have only "transactionType" and "newValue" // set. $types = array_fill_keys($this->getTransactionTypes(), true); assert_instances_of($xactions, 'PhabricatorApplicationTransaction'); foreach ($xactions as $xaction) { if ($xaction->getPHID() || $xaction->getID()) { throw new PhabricatorApplicationTransactionStructureException( $xaction, pht('You can not apply transactions which already have IDs/PHIDs!')); } if ($xaction->getObjectPHID()) { throw new PhabricatorApplicationTransactionStructureException( $xaction, pht( 'You can not apply transactions which already have %s!', 'objectPHIDs')); } if ($xaction->getCommentPHID()) { throw new PhabricatorApplicationTransactionStructureException( $xaction, pht( 'You can not apply transactions which already have %s!', 'commentPHIDs')); } if ($xaction->getCommentVersion() !== 0) { throw new PhabricatorApplicationTransactionStructureException( $xaction, pht( 'You can not apply transactions which already have '. 'commentVersions!')); } $expect_value = !$xaction->shouldGenerateOldValue(); $has_value = $xaction->hasOldValue(); if ($expect_value && !$has_value) { throw new PhabricatorApplicationTransactionStructureException( $xaction, pht( 'This transaction is supposed to have an %s set, but it does not!', 'oldValue')); } if ($has_value && !$expect_value) { throw new PhabricatorApplicationTransactionStructureException( $xaction, pht( 'This transaction should generate its %s automatically, '. 'but has already had one set!', 'oldValue')); } $type = $xaction->getTransactionType(); if (empty($types[$type])) { throw new PhabricatorApplicationTransactionStructureException( $xaction, pht( 'Transaction has type "%s", but that transaction type is not '. 'supported by this editor (%s).', $type, get_class($this))); } } } private function applyCapabilityChecks( PhabricatorLiskDAO $object, array $xactions) { assert_instances_of($xactions, 'PhabricatorApplicationTransaction'); $can_edit = PhabricatorPolicyCapability::CAN_EDIT; if ($this->getIsNewObject()) { // If we're creating a new object, we don't need any special capabilities // on the object. The actor has already made it through creation checks, // and objects which haven't been created yet often can not be // meaningfully tested for capabilities anyway. $required_capabilities = array(); } else { if (!$xactions && !$this->xactions) { // If we aren't doing anything, require CAN_EDIT to improve consistency. $required_capabilities = array($can_edit); } else { $required_capabilities = array(); foreach ($xactions as $xaction) { $type = $xaction->getTransactionType(); $xtype = $this->getModularTransactionType($type); if (!$xtype) { $capabilities = $this->getLegacyRequiredCapabilities($xaction); } else { $capabilities = $xtype->getRequiredCapabilities($object, $xaction); } // For convenience, we allow flexibility in the return types because // it's very unusual that a transaction actually requires multiple // capability checks. if ($capabilities === null) { $capabilities = array(); } else { $capabilities = (array)$capabilities; } foreach ($capabilities as $capability) { $required_capabilities[$capability] = $capability; } } } } $required_capabilities = array_fuse($required_capabilities); $actor = $this->getActor(); if ($required_capabilities) { id(new PhabricatorPolicyFilter()) ->setViewer($actor) ->requireCapabilities($required_capabilities) ->raisePolicyExceptions(true) ->apply(array($object)); } } private function getLegacyRequiredCapabilities( PhabricatorApplicationTransaction $xaction) { $type = $xaction->getTransactionType(); switch ($type) { case PhabricatorTransactions::TYPE_COMMENT: // TODO: Comments technically require CAN_INTERACT, but this is // currently somewhat special and handled through EditEngine. For now, // don't enforce it here. return null; case PhabricatorTransactions::TYPE_SUBSCRIBERS: // TODO: Removing subscribers other than yourself should probably // require CAN_EDIT permission. You can do this via the API but // generally can not via the web interface. return null; case PhabricatorTransactions::TYPE_TOKEN: // TODO: This technically requires CAN_INTERACT, like comments. return null; case PhabricatorTransactions::TYPE_HISTORY: // This is a special magic transaction which sends you history via // email and is only partially supported in the upstream. You don't // need any capabilities to apply it. return null; case PhabricatorTransactions::TYPE_EDGE: return $this->getLegacyRequiredEdgeCapabilities($xaction); default: // For other older (non-modular) transactions, always require exactly // CAN_EDIT. Transactions which do not need CAN_EDIT or need additional // capabilities must move to ModularTransactions. return PhabricatorPolicyCapability::CAN_EDIT; } } private function getLegacyRequiredEdgeCapabilities( PhabricatorApplicationTransaction $xaction) { // You don't need to have edit permission on an object to mention it or // otherwise add a relationship pointing toward it. if ($this->getIsInverseEdgeEditor()) { return null; } $edge_type = $xaction->getMetadataValue('edge:type'); switch ($edge_type) { case PhabricatorMutedByEdgeType::EDGECONST: // At time of writing, you can only write this edge for yourself, so // you don't need permissions. If you can eventually mute an object // for other users, this would need to be revisited. return null; case PhabricatorObjectMentionsObjectEdgeType::EDGECONST: return null; case PhabricatorProjectProjectHasMemberEdgeType::EDGECONST: $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $add = array_keys(array_diff_key($new, $old)); $rem = array_keys(array_diff_key($old, $new)); $actor_phid = $this->requireActor()->getPHID(); $is_join = (($add === array($actor_phid)) && !$rem); $is_leave = (($rem === array($actor_phid)) && !$add); if ($is_join) { // You need CAN_JOIN to join a project. return PhabricatorPolicyCapability::CAN_JOIN; } if ($is_leave) { $object = $this->object; // You usually don't need any capabilities to leave a project... if ($object->getIsMembershipLocked()) { // ...you must be able to edit to leave locked projects, though. return PhabricatorPolicyCapability::CAN_EDIT; } else { return null; } } // You need CAN_EDIT to change members other than yourself. return PhabricatorPolicyCapability::CAN_EDIT; default: return PhabricatorPolicyCapability::CAN_EDIT; } } private function buildSubscribeTransaction( PhabricatorLiskDAO $object, array $xactions, array $changes) { if (!($object instanceof PhabricatorSubscribableInterface)) { return null; } if ($this->shouldEnableMentions($object, $xactions)) { // Identify newly mentioned users. We ignore users who were previously // mentioned so that we don't re-subscribe users after an edit of text // which mentions them. $old_texts = mpull($changes, 'getOldValue'); $new_texts = mpull($changes, 'getNewValue'); $old_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions( $this->getActor(), $old_texts); $new_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions( $this->getActor(), $new_texts); $phids = array_diff($new_phids, $old_phids); } else { $phids = array(); } $this->mentionedPHIDs = $phids; if ($object->getPHID()) { // Don't try to subscribe already-subscribed mentions: we want to generate // a dialog about an action having no effect if the user explicitly adds // existing CCs, but not if they merely mention existing subscribers. $phids = array_diff($phids, $this->subscribers); } if ($phids) { $users = id(new PhabricatorPeopleQuery()) ->setViewer($this->getActor()) ->withPHIDs($phids) ->execute(); $users = mpull($users, null, 'getPHID'); foreach ($phids as $key => $phid) { // Do not subscribe mentioned users // who do not have VIEW Permissions if ($object instanceof PhabricatorPolicyInterface && !PhabricatorPolicyFilter::hasCapability( $users[$phid], $object, PhabricatorPolicyCapability::CAN_VIEW) ) { unset($phids[$key]); } else { if ($object->isAutomaticallySubscribed($phid)) { unset($phids[$key]); } } } $phids = array_values($phids); } // No else here to properly return null should we unset all subscriber if (!$phids) { return null; } $xaction = newv(get_class(head($xactions)), array()); $xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS); $xaction->setNewValue(array('+' => $phids)); return $xaction; } protected function mergeTransactions( PhabricatorApplicationTransaction $u, PhabricatorApplicationTransaction $v) { $type = $u->getTransactionType(); $xtype = $this->getModularTransactionType($type); if ($xtype) { $object = $this->object; return $xtype->mergeTransactions($object, $u, $v); } switch ($type) { case PhabricatorTransactions::TYPE_SUBSCRIBERS: return $this->mergePHIDOrEdgeTransactions($u, $v); case PhabricatorTransactions::TYPE_EDGE: $u_type = $u->getMetadataValue('edge:type'); $v_type = $v->getMetadataValue('edge:type'); if ($u_type == $v_type) { return $this->mergePHIDOrEdgeTransactions($u, $v); } return null; } // By default, do not merge the transactions. return null; } /** * Optionally expand transactions which imply other effects. For example, * resigning from a revision in Differential implies removing yourself as * a reviewer. */ protected function expandTransactions( PhabricatorLiskDAO $object, array $xactions) { $results = array(); foreach ($xactions as $xaction) { foreach ($this->expandTransaction($object, $xaction) as $expanded) { $results[] = $expanded; } } return $results; } protected function expandTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { return array($xaction); } public function getExpandedSupportTransactions( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $xactions = array($xaction); $xactions = $this->expandSupportTransactions( $object, $xactions); if (count($xactions) == 1) { return array(); } foreach ($xactions as $index => $cxaction) { if ($cxaction === $xaction) { unset($xactions[$index]); break; } } return $xactions; } private function expandSupportTransactions( PhabricatorLiskDAO $object, array $xactions) { $this->loadSubscribers($object); $xactions = $this->applyImplicitCC($object, $xactions); $changes = $this->getRemarkupChanges($xactions); $subscribe_xaction = $this->buildSubscribeTransaction( $object, $xactions, $changes); if ($subscribe_xaction) { $xactions[] = $subscribe_xaction; } // TODO: For now, this is just a placeholder. $engine = PhabricatorMarkupEngine::getEngine('extract'); $engine->setConfig('viewer', $this->requireActor()); $block_xactions = $this->expandRemarkupBlockTransactions( $object, $xactions, $changes, $engine); foreach ($block_xactions as $xaction) { $xactions[] = $xaction; } return $xactions; } private function getRemarkupChanges(array $xactions) { $changes = array(); foreach ($xactions as $key => $xaction) { foreach ($this->getRemarkupChangesFromTransaction($xaction) as $change) { $changes[] = $change; } } return $changes; } private function getRemarkupChangesFromTransaction( PhabricatorApplicationTransaction $transaction) { return $transaction->getRemarkupChanges(); } private function expandRemarkupBlockTransactions( PhabricatorLiskDAO $object, array $xactions, array $changes, PhutilMarkupEngine $engine) { $block_xactions = $this->expandCustomRemarkupBlockTransactions( $object, $xactions, $changes, $engine); $mentioned_phids = array(); if ($this->shouldEnableMentions($object, $xactions)) { foreach ($changes as $change) { // Here, we don't care about processing only new mentions after an edit // because there is no way for an object to ever "unmention" itself on // another object, so we can ignore the old value. $engine->markupText($change->getNewValue()); $mentioned_phids += $engine->getTextMetadata( PhabricatorObjectRemarkupRule::KEY_MENTIONED_OBJECTS, array()); } } if (!$mentioned_phids) { return $block_xactions; } $mentioned_objects = id(new PhabricatorObjectQuery()) ->setViewer($this->getActor()) ->withPHIDs($mentioned_phids) ->execute(); $mentionable_phids = array(); if ($this->shouldEnableMentions($object, $xactions)) { foreach ($mentioned_objects as $mentioned_object) { if ($mentioned_object instanceof PhabricatorMentionableInterface) { $mentioned_phid = $mentioned_object->getPHID(); if (idx($this->getUnmentionablePHIDMap(), $mentioned_phid)) { continue; } // don't let objects mention themselves if ($object->getPHID() && $mentioned_phid == $object->getPHID()) { continue; } $mentionable_phids[$mentioned_phid] = $mentioned_phid; } } } if ($mentionable_phids) { $edge_type = PhabricatorObjectMentionsObjectEdgeType::EDGECONST; $block_xactions[] = newv(get_class(head($xactions)), array()) ->setIgnoreOnNoEffect(true) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $edge_type) ->setNewValue(array('+' => $mentionable_phids)); } return $block_xactions; } protected function expandCustomRemarkupBlockTransactions( PhabricatorLiskDAO $object, array $xactions, array $changes, PhutilMarkupEngine $engine) { return array(); } /** * Attempt to combine similar transactions into a smaller number of total * transactions. For example, two transactions which edit the title of an * object can be merged into a single edit. */ private function combineTransactions(array $xactions) { $stray_comments = array(); $result = array(); $types = array(); foreach ($xactions as $key => $xaction) { $type = $xaction->getTransactionType(); if (isset($types[$type])) { foreach ($types[$type] as $other_key) { $other_xaction = $result[$other_key]; // Don't merge transactions with different authors. For example, // don't merge Herald transactions and owners transactions. if ($other_xaction->getAuthorPHID() != $xaction->getAuthorPHID()) { continue; } $merged = $this->mergeTransactions($result[$other_key], $xaction); if ($merged) { $result[$other_key] = $merged; if ($xaction->getComment() && ($xaction->getComment() !== $merged->getComment())) { $stray_comments[] = $xaction->getComment(); } if ($result[$other_key]->getComment() && ($result[$other_key]->getComment() !== $merged->getComment())) { $stray_comments[] = $result[$other_key]->getComment(); } // Move on to the next transaction. continue 2; } } } $result[$key] = $xaction; $types[$type][] = $key; } // If we merged any comments away, restore them. foreach ($stray_comments as $comment) { $xaction = newv(get_class(head($result)), array()); $xaction->setTransactionType(PhabricatorTransactions::TYPE_COMMENT); $xaction->setComment($comment); $result[] = $xaction; } return array_values($result); } public function mergePHIDOrEdgeTransactions( PhabricatorApplicationTransaction $u, PhabricatorApplicationTransaction $v) { $result = $u->getNewValue(); foreach ($v->getNewValue() as $key => $value) { if ($u->getTransactionType() == PhabricatorTransactions::TYPE_EDGE) { if (empty($result[$key])) { $result[$key] = $value; } else { // We're merging two lists of edge adds, sets, or removes. Merge // them by merging individual PHIDs within them. $merged = $result[$key]; foreach ($value as $dst => $v_spec) { if (empty($merged[$dst])) { $merged[$dst] = $v_spec; } else { // Two transactions are trying to perform the same operation on // the same edge. Normalize the edge data and then merge it. This // allows transactions to specify how data merges execute in a // precise way. $u_spec = $merged[$dst]; if (!is_array($u_spec)) { $u_spec = array('dst' => $u_spec); } if (!is_array($v_spec)) { $v_spec = array('dst' => $v_spec); } $ux_data = idx($u_spec, 'data', array()); $vx_data = idx($v_spec, 'data', array()); $merged_data = $this->mergeEdgeData( $u->getMetadataValue('edge:type'), $ux_data, $vx_data); $u_spec['data'] = $merged_data; $merged[$dst] = $u_spec; } } $result[$key] = $merged; } } else { $result[$key] = array_merge($value, idx($result, $key, array())); } } $u->setNewValue($result); // When combining an "ignore" transaction with a normal transaction, make // sure we don't propagate the "ignore" flag. if (!$v->getIgnoreOnNoEffect()) { $u->setIgnoreOnNoEffect(false); } return $u; } protected function mergeEdgeData($type, array $u, array $v) { return $v + $u; } protected function getPHIDTransactionNewValue( PhabricatorApplicationTransaction $xaction, $old = null) { if ($old !== null) { $old = array_fuse($old); } else { $old = array_fuse($xaction->getOldValue()); } return $this->getPHIDList($old, $xaction->getNewValue()); } public function getPHIDList(array $old, array $new) { $new_add = idx($new, '+', array()); unset($new['+']); $new_rem = idx($new, '-', array()); unset($new['-']); $new_set = idx($new, '=', null); if ($new_set !== null) { $new_set = array_fuse($new_set); } unset($new['=']); if ($new) { throw new Exception( pht( "Invalid '%s' value for PHID transaction. Value should contain only ". "keys '%s' (add PHIDs), '%s' (remove PHIDs) and '%s' (set PHIDS).", 'new', '+', '-', '=')); } $result = array(); foreach ($old as $phid) { if ($new_set !== null && empty($new_set[$phid])) { continue; } $result[$phid] = $phid; } if ($new_set !== null) { foreach ($new_set as $phid) { $result[$phid] = $phid; } } foreach ($new_add as $phid) { $result[$phid] = $phid; } foreach ($new_rem as $phid) { unset($result[$phid]); } return array_values($result); } protected function getEdgeTransactionNewValue( PhabricatorApplicationTransaction $xaction) { $new = $xaction->getNewValue(); $new_add = idx($new, '+', array()); unset($new['+']); $new_rem = idx($new, '-', array()); unset($new['-']); $new_set = idx($new, '=', null); unset($new['=']); if ($new) { throw new Exception( pht( "Invalid '%s' value for Edge transaction. Value should contain only ". "keys '%s' (add edges), '%s' (remove edges) and '%s' (set edges).", 'new', '+', '-', '=')); } $old = $xaction->getOldValue(); $lists = array($new_set, $new_add, $new_rem); foreach ($lists as $list) { $this->checkEdgeList($list, $xaction->getMetadataValue('edge:type')); } $result = array(); foreach ($old as $dst_phid => $edge) { if ($new_set !== null && empty($new_set[$dst_phid])) { continue; } $result[$dst_phid] = $this->normalizeEdgeTransactionValue( $xaction, $edge, $dst_phid); } if ($new_set !== null) { foreach ($new_set as $dst_phid => $edge) { $result[$dst_phid] = $this->normalizeEdgeTransactionValue( $xaction, $edge, $dst_phid); } } foreach ($new_add as $dst_phid => $edge) { $result[$dst_phid] = $this->normalizeEdgeTransactionValue( $xaction, $edge, $dst_phid); } foreach ($new_rem as $dst_phid => $edge) { unset($result[$dst_phid]); } return $result; } private function checkEdgeList($list, $edge_type) { if (!$list) { return; } foreach ($list as $key => $item) { if (phid_get_type($key) === PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) { throw new Exception( pht( 'Edge transactions must have destination PHIDs as in edge '. 'lists (found key "%s" on transaction of type "%s").', $key, $edge_type)); } if (!is_array($item) && $item !== $key) { throw new Exception( pht( 'Edge transactions must have PHIDs or edge specs as values '. '(found value "%s" on transaction of type "%s").', $item, $edge_type)); } } } private function normalizeEdgeTransactionValue( PhabricatorApplicationTransaction $xaction, $edge, $dst_phid) { if (!is_array($edge)) { if ($edge != $dst_phid) { throw new Exception( pht( 'Transaction edge data must either be the edge PHID or an edge '. 'specification dictionary.')); } $edge = array(); } else { foreach ($edge as $key => $value) { switch ($key) { case 'src': case 'dst': case 'type': case 'data': case 'dateCreated': case 'dateModified': case 'seq': case 'dataID': break; default: throw new Exception( pht( 'Transaction edge specification contains unexpected key "%s".', $key)); } } } $edge['dst'] = $dst_phid; $edge_type = $xaction->getMetadataValue('edge:type'); if (empty($edge['type'])) { $edge['type'] = $edge_type; } else { if ($edge['type'] != $edge_type) { $this_type = $edge['type']; throw new Exception( pht( "Edge transaction includes edge of type '%s', but ". "transaction is of type '%s'. Each edge transaction ". "must alter edges of only one type.", $this_type, $edge_type)); } } if (!isset($edge['data'])) { $edge['data'] = array(); } return $edge; } protected function sortTransactions(array $xactions) { $head = array(); $tail = array(); // Move bare comments to the end, so the actions precede them. foreach ($xactions as $xaction) { $type = $xaction->getTransactionType(); if ($type == PhabricatorTransactions::TYPE_COMMENT) { $tail[] = $xaction; } else { $head[] = $xaction; } } return array_values(array_merge($head, $tail)); } protected function filterTransactions( PhabricatorLiskDAO $object, array $xactions) { $type_comment = PhabricatorTransactions::TYPE_COMMENT; $no_effect = array(); $has_comment = false; $any_effect = false; foreach ($xactions as $key => $xaction) { if ($this->transactionHasEffect($object, $xaction)) { if ($xaction->getTransactionType() != $type_comment) { $any_effect = true; } } else if ($xaction->getIgnoreOnNoEffect()) { unset($xactions[$key]); } else { $no_effect[$key] = $xaction; } if ($xaction->hasComment()) { $has_comment = true; } } if (!$no_effect) { return $xactions; } if (!$this->getContinueOnNoEffect() && !$this->getIsPreview()) { throw new PhabricatorApplicationTransactionNoEffectException( $no_effect, $any_effect, $has_comment); } if (!$any_effect && !$has_comment) { // If we only have empty comment transactions, just drop them all. return array(); } foreach ($no_effect as $key => $xaction) { if ($xaction->hasComment()) { $xaction->setTransactionType($type_comment); $xaction->setOldValue(null); $xaction->setNewValue(null); } else { unset($xactions[$key]); } } return $xactions; } /** * Hook for validating transactions. This callback will be invoked for each * available transaction type, even if an edit does not apply any transactions * of that type. This allows you to raise exceptions when required fields are * missing, by detecting that the object has no field value and there is no * transaction which sets one. * * @param PhabricatorLiskDAO Object being edited. * @param string Transaction type to validate. * @param list Transactions of given type, * which may be empty if the edit does not apply any transactions of the * given type. * @return list List of * validation errors. */ protected function validateTransaction( PhabricatorLiskDAO $object, $type, array $xactions) { $errors = array(); $xtype = $this->getModularTransactionType($type); if ($xtype) { $errors[] = $xtype->validateTransactions($object, $xactions); } switch ($type) { case PhabricatorTransactions::TYPE_VIEW_POLICY: $errors[] = $this->validatePolicyTransaction( $object, $xactions, $type, PhabricatorPolicyCapability::CAN_VIEW); break; case PhabricatorTransactions::TYPE_EDIT_POLICY: $errors[] = $this->validatePolicyTransaction( $object, $xactions, $type, PhabricatorPolicyCapability::CAN_EDIT); break; case PhabricatorTransactions::TYPE_SPACE: $errors[] = $this->validateSpaceTransactions( $object, $xactions, $type); break; case PhabricatorTransactions::TYPE_SUBTYPE: $errors[] = $this->validateSubtypeTransactions( $object, $xactions, $type); break; case PhabricatorTransactions::TYPE_CUSTOMFIELD: $groups = array(); foreach ($xactions as $xaction) { $groups[$xaction->getMetadataValue('customfield:key')][] = $xaction; } $field_list = PhabricatorCustomField::getObjectFields( $object, PhabricatorCustomField::ROLE_EDIT); $field_list->setViewer($this->getActor()); $role_xactions = PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS; foreach ($field_list->getFields() as $field) { if (!$field->shouldEnableForRole($role_xactions)) { continue; } $errors[] = $field->validateApplicationTransactions( $this, $type, idx($groups, $field->getFieldKey(), array())); } break; } return array_mergev($errors); } public function validatePolicyTransaction( PhabricatorLiskDAO $object, array $xactions, $transaction_type, $capability) { $actor = $this->requireActor(); $errors = array(); // Note $this->xactions is necessary; $xactions is $this->xactions of // $transaction_type $policy_object = $this->adjustObjectForPolicyChecks( $object, $this->xactions); // Make sure the user isn't editing away their ability to $capability this // object. foreach ($xactions as $xaction) { try { PhabricatorPolicyFilter::requireCapabilityWithForcedPolicy( $actor, $policy_object, $capability, $xaction->getNewValue()); } catch (PhabricatorPolicyException $ex) { $errors[] = new PhabricatorApplicationTransactionValidationError( $transaction_type, pht('Invalid'), pht( 'You can not select this %s policy, because you would no longer '. 'be able to %s the object.', $capability, $capability), $xaction); } } if ($this->getIsNewObject()) { if (!$xactions) { $has_capability = PhabricatorPolicyFilter::hasCapability( $actor, $policy_object, $capability); if (!$has_capability) { $errors[] = new PhabricatorApplicationTransactionValidationError( $transaction_type, pht('Invalid'), pht( 'The selected %s policy excludes you. Choose a %s policy '. 'which allows you to %s the object.', $capability, $capability, $capability)); } } } return $errors; } private function validateSpaceTransactions( PhabricatorLiskDAO $object, array $xactions, $transaction_type) { $errors = array(); $actor = $this->getActor(); $has_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($actor); $actor_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces($actor); $active_spaces = PhabricatorSpacesNamespaceQuery::getViewerActiveSpaces( $actor); foreach ($xactions as $xaction) { $space_phid = $xaction->getNewValue(); if ($space_phid === null) { if (!$has_spaces) { // The install doesn't have any spaces, so this is fine. continue; } // The install has some spaces, so every object needs to be put // in a valid space. $errors[] = new PhabricatorApplicationTransactionValidationError( $transaction_type, pht('Invalid'), pht('You must choose a space for this object.'), $xaction); continue; } // If the PHID isn't `null`, it needs to be a valid space that the // viewer can see. if (empty($actor_spaces[$space_phid])) { $errors[] = new PhabricatorApplicationTransactionValidationError( $transaction_type, pht('Invalid'), pht( 'You can not shift this object in the selected space, because '. 'the space does not exist or you do not have access to it.'), $xaction); } else if (empty($active_spaces[$space_phid])) { // It's OK to edit objects in an archived space, so just move on if // we aren't adjusting the value. $old_space_phid = $this->getTransactionOldValue($object, $xaction); if ($space_phid == $old_space_phid) { continue; } $errors[] = new PhabricatorApplicationTransactionValidationError( $transaction_type, pht('Archived'), pht( 'You can not shift this object into the selected space, because '. 'the space is archived. Objects can not be created inside (or '. 'moved into) archived spaces.'), $xaction); } } return $errors; } private function validateSubtypeTransactions( PhabricatorLiskDAO $object, array $xactions, $transaction_type) { $errors = array(); $map = $object->newEditEngineSubtypeMap(); $old = $object->getEditEngineSubtype(); foreach ($xactions as $xaction) { $new = $xaction->getNewValue(); if ($old == $new) { continue; } if (!isset($map[$new])) { $errors[] = new PhabricatorApplicationTransactionValidationError( $transaction_type, pht('Invalid'), pht( 'The subtype "%s" is not a valid subtype.', $new), $xaction); continue; } } return $errors; } protected function adjustObjectForPolicyChecks( PhabricatorLiskDAO $object, array $xactions) { $copy = clone $object; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_SUBSCRIBERS: $clone_xaction = clone $xaction; $clone_xaction->setOldValue(array_values($this->subscribers)); $clone_xaction->setNewValue( $this->getPHIDTransactionNewValue( $clone_xaction)); PhabricatorPolicyRule::passTransactionHintToRule( $copy, new PhabricatorSubscriptionsSubscribersPolicyRule(), array_fuse($clone_xaction->getNewValue())); break; case PhabricatorTransactions::TYPE_SPACE: $space_phid = $this->getTransactionNewValue($object, $xaction); $copy->setSpacePHID($space_phid); break; } } return $copy; } protected function validateAllTransactions( PhabricatorLiskDAO $object, array $xactions) { return array(); } /** * Check for a missing text field. * * A text field is missing if the object has no value and there are no * transactions which set a value, or if the transactions remove the value. * This method is intended to make implementing @{method:validateTransaction} * more convenient: * * $missing = $this->validateIsEmptyTextField( * $object->getName(), * $xactions); * * This will return `true` if the net effect of the object and transactions * is an empty field. * * @param wild Current field value. * @param list Transactions editing the * field. * @return bool True if the field will be an empty text field after edits. */ protected function validateIsEmptyTextField($field_value, array $xactions) { if (strlen($field_value) && empty($xactions)) { return false; } if ($xactions && strlen(last($xactions)->getNewValue())) { return false; } return true; } /* -( Implicit CCs )------------------------------------------------------- */ /** * When a user interacts with an object, we might want to add them to CC. */ final public function applyImplicitCC( PhabricatorLiskDAO $object, array $xactions) { if (!($object instanceof PhabricatorSubscribableInterface)) { // If the object isn't subscribable, we can't CC them. return $xactions; } $actor_phid = $this->getActingAsPHID(); $type_user = PhabricatorPeopleUserPHIDType::TYPECONST; if (phid_get_type($actor_phid) != $type_user) { // Transactions by application actors like Herald, Harbormaster and // Diffusion should not CC the applications. return $xactions; } if ($object->isAutomaticallySubscribed($actor_phid)) { // If they're auto-subscribed, don't CC them. return $xactions; } $should_cc = false; foreach ($xactions as $xaction) { if ($this->shouldImplyCC($object, $xaction)) { $should_cc = true; break; } } if (!$should_cc) { // Only some types of actions imply a CC (like adding a comment). return $xactions; } if ($object->getPHID()) { if (isset($this->subscribers[$actor_phid])) { // If the user is already subscribed, don't implicitly CC them. return $xactions; } $unsub = PhabricatorEdgeQuery::loadDestinationPHIDs( $object->getPHID(), PhabricatorObjectHasUnsubscriberEdgeType::EDGECONST); $unsub = array_fuse($unsub); if (isset($unsub[$actor_phid])) { // If the user has previously unsubscribed from this object explicitly, // don't implicitly CC them. return $xactions; } } $xaction = newv(get_class(head($xactions)), array()); $xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS); $xaction->setNewValue(array('+' => array($actor_phid))); array_unshift($xactions, $xaction); return $xactions; } protected function shouldImplyCC( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { return $xaction->isCommentTransaction(); } /* -( Sending Mail )------------------------------------------------------- */ /** * @task mail */ protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return false; } /** * @task mail */ private function buildMail( PhabricatorLiskDAO $object, array $xactions) { $email_to = $this->mailToPHIDs; $email_cc = $this->mailCCPHIDs; $email_cc = array_merge($email_cc, $this->heraldEmailPHIDs); $unexpandable = $this->mailUnexpandablePHIDs; if (!is_array($unexpandable)) { $unexpandable = array(); } $messages = $this->buildMailWithRecipients( $object, $xactions, $email_to, $email_cc, $unexpandable); $this->runHeraldMailRules($messages); return $messages; } private function buildMailWithRecipients( PhabricatorLiskDAO $object, array $xactions, array $email_to, array $email_cc, array $unexpandable) { $targets = $this->buildReplyHandler($object) ->setUnexpandablePHIDs($unexpandable) ->getMailTargets($email_to, $email_cc); // Set this explicitly before we start swapping out the effective actor. $this->setActingAsPHID($this->getActingAsPHID()); $messages = array(); foreach ($targets as $target) { $original_actor = $this->getActor(); $viewer = $target->getViewer(); $this->setActor($viewer); $locale = PhabricatorEnv::beginScopedLocale($viewer->getTranslation()); $caught = null; $mail = null; try { // Reload handles for the new viewer. $this->loadHandles($xactions); $mail = $this->buildMailForTarget($object, $xactions, $target); if ($mail) { if ($this->mustEncrypt) { $mail ->setMustEncrypt(true) ->setMustEncryptReasons($this->mustEncrypt); } } } catch (Exception $ex) { $caught = $ex; } $this->setActor($original_actor); unset($locale); if ($caught) { throw $ex; } if ($mail) { $messages[] = $mail; } } return $messages; } protected function getTransactionsForMail( PhabricatorLiskDAO $object, array $xactions) { return $xactions; } private function buildMailForTarget( PhabricatorLiskDAO $object, array $xactions, PhabricatorMailTarget $target) { // Check if any of the transactions are visible for this viewer. If we // don't have any visible transactions, don't send the mail. $any_visible = false; foreach ($xactions as $xaction) { if (!$xaction->shouldHideForMail($xactions)) { $any_visible = true; break; } } if (!$any_visible) { return null; } $mail_xactions = $this->getTransactionsForMail($object, $xactions); $mail = $this->buildMailTemplate($object); $body = $this->buildMailBody($object, $mail_xactions); $mail_tags = $this->getMailTags($object, $mail_xactions); $action = $this->getMailAction($object, $mail_xactions); $stamps = $this->generateMailStamps($object, $this->mailStamps); if (PhabricatorEnv::getEnvConfig('metamta.email-preferences')) { $this->addEmailPreferenceSectionToMailBody( $body, $object, $mail_xactions); } $muted_phids = $this->mailMutedPHIDs; if (!is_array($muted_phids)) { $muted_phids = array(); } $mail ->setSensitiveContent(false) ->setFrom($this->getActingAsPHID()) ->setSubjectPrefix($this->getMailSubjectPrefix()) ->setVarySubjectPrefix('['.$action.']') ->setThreadID($this->getMailThreadID($object), $this->getIsNewObject()) ->setRelatedPHID($object->getPHID()) ->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs()) ->setMutedPHIDs($muted_phids) ->setForceHeraldMailRecipientPHIDs($this->heraldForcedEmailPHIDs) ->setMailTags($mail_tags) ->setIsBulk(true) ->setBody($body->render()) ->setHTMLBody($body->renderHTML()); foreach ($body->getAttachments() as $attachment) { $mail->addAttachment($attachment); } if ($this->heraldHeader) { $mail->addHeader('X-Herald-Rules', $this->heraldHeader); } if ($object instanceof PhabricatorProjectInterface) { $this->addMailProjectMetadata($object, $mail); } if ($this->getParentMessageID()) { $mail->setParentMessageID($this->getParentMessageID()); } // If we have stamps, attach the raw dictionary version (not the actual // objects) to the mail so that debugging tools can see what we used to // render the final list. if ($this->mailStamps) { $mail->setMailStampMetadata($this->mailStamps); } // If we have rendered stamps, attach them to the mail. if ($stamps) { $mail->setMailStamps($stamps); } return $target->willSendMail($mail); } private function addMailProjectMetadata( PhabricatorLiskDAO $object, PhabricatorMetaMTAMail $template) { $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $object->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); if (!$project_phids) { return; } // TODO: This viewer isn't quite right. It would be slightly better to use // the mail recipient, but that's not very easy given the way rendering // works today. $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->requireActor()) ->withPHIDs($project_phids) ->execute(); $project_tags = array(); foreach ($handles as $handle) { if (!$handle->isComplete()) { continue; } $project_tags[] = '<'.$handle->getObjectName().'>'; } if (!$project_tags) { return; } $project_tags = implode(', ', $project_tags); $template->addHeader('X-Phabricator-Projects', $project_tags); } protected function getMailThreadID(PhabricatorLiskDAO $object) { return $object->getPHID(); } /** * @task mail */ protected function getStrongestAction( PhabricatorLiskDAO $object, array $xactions) { return last(msort($xactions, 'getActionStrength')); } /** * @task mail */ protected function buildReplyHandler(PhabricatorLiskDAO $object) { throw new Exception(pht('Capability not supported.')); } /** * @task mail */ protected function getMailSubjectPrefix() { throw new Exception(pht('Capability not supported.')); } /** * @task mail */ protected function getMailTags( PhabricatorLiskDAO $object, array $xactions) { $tags = array(); foreach ($xactions as $xaction) { $tags[] = $xaction->getMailTags(); } return array_mergev($tags); } /** * @task mail */ public function getMailTagsMap() { // TODO: We should move shared mail tags, like "comment", here. return array(); } /** * @task mail */ protected function getMailAction( PhabricatorLiskDAO $object, array $xactions) { return $this->getStrongestAction($object, $xactions)->getActionName(); } /** * @task mail */ protected function buildMailTemplate(PhabricatorLiskDAO $object) { throw new Exception(pht('Capability not supported.')); } /** * @task mail */ protected function getMailTo(PhabricatorLiskDAO $object) { throw new Exception(pht('Capability not supported.')); } protected function newMailUnexpandablePHIDs(PhabricatorLiskDAO $object) { return array(); } /** * @task mail */ protected function getMailCC(PhabricatorLiskDAO $object) { $phids = array(); $has_support = false; if ($object instanceof PhabricatorSubscribableInterface) { $phid = $object->getPHID(); $phids[] = PhabricatorSubscribersQuery::loadSubscribersForPHID($phid); $has_support = true; } if ($object instanceof PhabricatorProjectInterface) { $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $object->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); if ($project_phids) { $projects = id(new PhabricatorProjectQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs($project_phids) ->needWatchers(true) ->execute(); $watcher_phids = array(); foreach ($projects as $project) { foreach ($project->getAllAncestorWatcherPHIDs() as $phid) { $watcher_phids[$phid] = $phid; } } if ($watcher_phids) { // We need to do a visibility check for all the watchers, as // watching a project is not a guarantee that you can see objects // associated with it. $users = id(new PhabricatorPeopleQuery()) ->setViewer($this->requireActor()) ->withPHIDs($watcher_phids) ->execute(); $watchers = array(); foreach ($users as $user) { $can_see = PhabricatorPolicyFilter::hasCapability( $user, $object, PhabricatorPolicyCapability::CAN_VIEW); if ($can_see) { $watchers[] = $user->getPHID(); } } $phids[] = $watchers; } } $has_support = true; } if (!$has_support) { throw new Exception( pht('The object being edited does not implement any standard '. 'interfaces (like PhabricatorSubscribableInterface) which allow '. 'CCs to be generated automatically. Override the "getMailCC()" '. 'method and generate CCs explicitly.')); } return array_mergev($phids); } /** * @task mail */ protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = id(new PhabricatorMetaMTAMailBody()) ->setViewer($this->requireActor()) ->setContextObject($object); $this->addHeadersAndCommentsToMailBody($body, $xactions); $this->addCustomFieldsToMailBody($body, $object, $xactions); return $body; } /** * @task mail */ protected function addEmailPreferenceSectionToMailBody( PhabricatorMetaMTAMailBody $body, PhabricatorLiskDAO $object, array $xactions) { $href = PhabricatorEnv::getProductionURI( '/settings/panel/emailpreferences/'); $body->addLinkSection(pht('EMAIL PREFERENCES'), $href); } /** * @task mail */ protected function addHeadersAndCommentsToMailBody( PhabricatorMetaMTAMailBody $body, array $xactions, $object_label = null, $object_href = null) { // First, remove transactions which shouldn't be rendered in mail. foreach ($xactions as $key => $xaction) { if ($xaction->shouldHideForMail($xactions)) { unset($xactions[$key]); } } $headers = array(); $headers_html = array(); $comments = array(); $details = array(); $seen_comment = false; foreach ($xactions as $xaction) { // Most mail has zero or one comments. In these cases, we render the // "alice added a comment." transaction in the header, like a normal // transaction. // Some mail, like Differential undraft mail or "!history" mail, may // have two or more comments. In these cases, we'll put the first // "alice added a comment." transaction in the header normally, but // move the other transactions down so they provide context above the // actual comment. $comment = $xaction->getBodyForMail(); if ($comment !== null) { $is_comment = true; $comments[] = array( 'xaction' => $xaction, 'comment' => $comment, 'initial' => !$seen_comment, ); } else { $is_comment = false; } if (!$is_comment || !$seen_comment) { $header = $xaction->getTitleForMail(); if ($header !== null) { $headers[] = $header; } $header_html = $xaction->getTitleForHTMLMail(); if ($header_html !== null) { $headers_html[] = $header_html; } } if ($xaction->hasChangeDetailsForMail()) { $details[] = $xaction; } if ($is_comment) { $seen_comment = true; } } $headers_text = implode("\n", $headers); $body->addRawPlaintextSection($headers_text); $headers_html = phutil_implode_html(phutil_tag('br'), $headers_html); $header_button = null; if ($object_label !== null) { $button_style = array( 'text-decoration: none;', 'padding: 4px 8px;', 'margin: 0 8px 8px;', 'float: right;', 'color: #464C5C;', 'font-weight: bold;', 'border-radius: 3px;', 'background-color: #F7F7F9;', 'background-image: linear-gradient(to bottom,#fff,#f1f0f1);', 'display: inline-block;', 'border: 1px solid rgba(71,87,120,.2);', ); $header_button = phutil_tag( 'a', array( 'style' => implode(' ', $button_style), 'href' => $object_href, ), $object_label); } $xactions_style = array(); $header_action = phutil_tag( 'td', array(), $header_button); $header_action = phutil_tag( 'td', array( 'style' => implode(' ', $xactions_style), ), array( $headers_html, // Add an extra newline to prevent the "View Object" button from // running into the transaction text in Mail.app text snippet // previews. "\n", )); $headers_html = phutil_tag( 'table', array(), phutil_tag('tr', array(), array($header_action, $header_button))); $body->addRawHTMLSection($headers_html); foreach ($comments as $spec) { $xaction = $spec['xaction']; $comment = $spec['comment']; $is_initial = $spec['initial']; // If this is not the first comment in the mail, add the header showing // who wrote the comment immediately above the comment. if (!$is_initial) { $header = $xaction->getTitleForMail(); if ($header !== null) { $body->addRawPlaintextSection($header); } $header_html = $xaction->getTitleForHTMLMail(); if ($header_html !== null) { $body->addRawHTMLSection($header_html); } } $body->addRemarkupSection(null, $comment); } foreach ($details as $xaction) { $details = $xaction->renderChangeDetailsForMail($body->getViewer()); if ($details !== null) { $label = $this->getMailDiffSectionHeader($xaction); $body->addHTMLSection($label, $details); } } } private function getMailDiffSectionHeader($xaction) { $type = $xaction->getTransactionType(); $xtype = $this->getModularTransactionType($type); if ($xtype) { return $xtype->getMailDiffSectionHeader(); } return pht('EDIT DETAILS'); } /** * @task mail */ protected function addCustomFieldsToMailBody( PhabricatorMetaMTAMailBody $body, PhabricatorLiskDAO $object, array $xactions) { if ($object instanceof PhabricatorCustomFieldInterface) { $field_list = PhabricatorCustomField::getObjectFields( $object, PhabricatorCustomField::ROLE_TRANSACTIONMAIL); $field_list->setViewer($this->getActor()); $field_list->readFieldsFromStorage($object); foreach ($field_list->getFields() as $field) { $field->updateTransactionMailBody( $body, $this, $xactions); } } } /** * @task mail */ private function runHeraldMailRules(array $messages) { foreach ($messages as $message) { $engine = new HeraldEngine(); $adapter = id(new PhabricatorMailOutboundMailHeraldAdapter()) ->setObject($message); $rules = $engine->loadRulesForAdapter($adapter); $effects = $engine->applyRules($rules, $adapter); $engine->applyEffects($effects, $adapter, $rules); } } /* -( Publishing Feed Stories )-------------------------------------------- */ /** * @task feed */ protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { return false; } /** * @task feed */ protected function getFeedStoryType() { return 'PhabricatorApplicationTransactionFeedStory'; } /** * @task feed */ protected function getFeedRelatedPHIDs( PhabricatorLiskDAO $object, array $xactions) { $phids = array( $object->getPHID(), $this->getActingAsPHID(), ); if ($object instanceof PhabricatorProjectInterface) { $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $object->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); foreach ($project_phids as $project_phid) { $phids[] = $project_phid; } } return $phids; } /** * @task feed */ protected function getFeedNotifyPHIDs( PhabricatorLiskDAO $object, array $xactions) { return array_unique(array_merge( $this->getMailTo($object), $this->getMailCC($object))); } /** * @task feed */ protected function getFeedStoryData( PhabricatorLiskDAO $object, array $xactions) { $xactions = msort($xactions, 'getActionStrength'); $xactions = array_reverse($xactions); return array( 'objectPHID' => $object->getPHID(), 'transactionPHIDs' => mpull($xactions, 'getPHID'), ); } /** * @task feed */ protected function publishFeedStory( PhabricatorLiskDAO $object, array $xactions, array $mailed_phids) { $xactions = mfilter($xactions, 'shouldHideForFeed', true); if (!$xactions) { return; } $related_phids = $this->feedRelatedPHIDs; $subscribed_phids = $this->feedNotifyPHIDs; // Remove muted users from the subscription list so they don't get // notifications, either. $muted_phids = $this->mailMutedPHIDs; if (!is_array($muted_phids)) { $muted_phids = array(); } $subscribed_phids = array_fuse($subscribed_phids); foreach ($muted_phids as $muted_phid) { unset($subscribed_phids[$muted_phid]); } $subscribed_phids = array_values($subscribed_phids); $story_type = $this->getFeedStoryType(); $story_data = $this->getFeedStoryData($object, $xactions); $unexpandable_phids = $this->mailUnexpandablePHIDs; if (!is_array($unexpandable_phids)) { $unexpandable_phids = array(); } id(new PhabricatorFeedStoryPublisher()) ->setStoryType($story_type) ->setStoryData($story_data) ->setStoryTime(time()) ->setStoryAuthorPHID($this->getActingAsPHID()) ->setRelatedPHIDs($related_phids) ->setPrimaryObjectPHID($object->getPHID()) ->setSubscribedPHIDs($subscribed_phids) ->setUnexpandablePHIDs($unexpandable_phids) ->setMailRecipientPHIDs($mailed_phids) ->setMailTags($this->getMailTags($object, $xactions)) ->publish(); } /* -( Search Index )------------------------------------------------------- */ /** * @task search */ protected function supportsSearch() { return false; } /* -( Herald Integration )-------------------------------------------------- */ protected function shouldApplyHeraldRules( PhabricatorLiskDAO $object, array $xactions) { return false; } protected function buildHeraldAdapter( PhabricatorLiskDAO $object, array $xactions) { throw new Exception(pht('No herald adapter specified.')); } private function setHeraldAdapter(HeraldAdapter $adapter) { $this->heraldAdapter = $adapter; return $this; } protected function getHeraldAdapter() { return $this->heraldAdapter; } private function setHeraldTranscript(HeraldTranscript $transcript) { $this->heraldTranscript = $transcript; return $this; } protected function getHeraldTranscript() { return $this->heraldTranscript; } private function applyHeraldRules( PhabricatorLiskDAO $object, array $xactions) { $adapter = $this->buildHeraldAdapter($object, $xactions) ->setContentSource($this->getContentSource()) ->setIsNewObject($this->getIsNewObject()) ->setActingAsPHID($this->getActingAsPHID()) ->setAppliedTransactions($xactions); if ($this->getApplicationEmail()) { $adapter->setApplicationEmail($this->getApplicationEmail()); } // If this editor is operating in silent mode, tell Herald that we aren't // going to send any mail. This allows it to skip "the first time this // rule matches, send me an email" rules which would otherwise match even // though we aren't going to send any mail. if ($this->getIsSilent()) { $adapter->setForbiddenAction( HeraldMailableState::STATECONST, HeraldCoreStateReasons::REASON_SILENT); } $xscript = HeraldEngine::loadAndApplyRules($adapter); $this->setHeraldAdapter($adapter); $this->setHeraldTranscript($xscript); if ($adapter instanceof HarbormasterBuildableAdapterInterface) { $buildable_phid = $adapter->getHarbormasterBuildablePHID(); HarbormasterBuildable::applyBuildPlans( $buildable_phid, $adapter->getHarbormasterContainerPHID(), $adapter->getQueuedHarbormasterBuildRequests()); // Whether we queued any builds or not, any automatic buildable for this // object is now done preparing builds and can transition into a // completed status. $buildables = id(new HarbormasterBuildableQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withManualBuildables(false) ->withBuildablePHIDs(array($buildable_phid)) ->execute(); foreach ($buildables as $buildable) { // If this buildable has already moved beyond preparation, we don't // need to nudge it again. if (!$buildable->isPreparing()) { continue; } $buildable->sendMessage( $this->getActor(), HarbormasterMessageType::BUILDABLE_BUILD, true); } } $this->mustEncrypt = $adapter->getMustEncryptReasons(); return array_merge( $this->didApplyHeraldRules($object, $adapter, $xscript), $adapter->getQueuedTransactions()); } protected function didApplyHeraldRules( PhabricatorLiskDAO $object, HeraldAdapter $adapter, HeraldTranscript $transcript) { return array(); } /* -( Custom Fields )------------------------------------------------------ */ /** * @task customfield */ private function getCustomFieldForTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $field_key = $xaction->getMetadataValue('customfield:key'); if (!$field_key) { throw new Exception( pht( "Custom field transaction has no '%s'!", 'customfield:key')); } $field = PhabricatorCustomField::getObjectField( $object, PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS, $field_key); if (!$field) { throw new Exception( pht( "Custom field transaction has invalid '%s'; field '%s' ". "is disabled or does not exist.", 'customfield:key', $field_key)); } if (!$field->shouldAppearInApplicationTransactions()) { throw new Exception( pht( "Custom field transaction '%s' does not implement ". "integration for %s.", $field_key, 'ApplicationTransactions')); } $field->setViewer($this->getActor()); return $field; } /* -( Files )-------------------------------------------------------------- */ /** * Extract the PHIDs of any files which these transactions attach. * * @task files */ private function extractFilePHIDs( PhabricatorLiskDAO $object, array $xactions) { $changes = $this->getRemarkupChanges($xactions); $blocks = mpull($changes, 'getNewValue'); $phids = array(); if ($blocks) { $phids[] = PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles( $this->getActor(), $blocks); } foreach ($xactions as $xaction) { $type = $xaction->getTransactionType(); $xtype = $this->getModularTransactionType($type); if ($xtype) { $phids[] = $xtype->extractFilePHIDs($object, $xaction->getNewValue()); } else { $phids[] = $this->extractFilePHIDsFromCustomTransaction( $object, $xaction); } } $phids = array_unique(array_filter(array_mergev($phids))); if (!$phids) { return array(); } // Only let a user attach files they can actually see, since this would // otherwise let you access any file by attaching it to an object you have // view permission on. $files = id(new PhabricatorFileQuery()) ->setViewer($this->getActor()) ->withPHIDs($phids) ->execute(); return mpull($files, 'getPHID'); } /** * @task files */ protected function extractFilePHIDsFromCustomTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { return array(); } /** * @task files */ private function attachFiles( PhabricatorLiskDAO $object, array $file_phids) { if (!$file_phids) { return; } $editor = new PhabricatorEdgeEditor(); $src = $object->getPHID(); $type = PhabricatorObjectHasFileEdgeType::EDGECONST; foreach ($file_phids as $dst) { $editor->addEdge($src, $type, $dst); } $editor->save(); } private function applyInverseEdgeTransactions( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction, $inverse_type) { $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $add = array_keys(array_diff_key($new, $old)); $rem = array_keys(array_diff_key($old, $new)); $add = array_fuse($add); $rem = array_fuse($rem); $all = $add + $rem; $nodes = id(new PhabricatorObjectQuery()) ->setViewer($this->requireActor()) ->withPHIDs($all) ->execute(); foreach ($nodes as $node) { if (!($node instanceof PhabricatorApplicationTransactionInterface)) { continue; } if ($node instanceof PhabricatorUser) { // TODO: At least for now, don't record inverse edge transactions // for users (for example, "alincoln joined project X"): Feed fills // this role instead. continue; } $editor = $node->getApplicationTransactionEditor(); $template = $node->getApplicationTransactionTemplate(); $target = $node->getApplicationTransactionObject(); if (isset($add[$node->getPHID()])) { $edge_edit_type = '+'; } else { $edge_edit_type = '-'; } $template ->setTransactionType($xaction->getTransactionType()) ->setMetadataValue('edge:type', $inverse_type) ->setNewValue( array( $edge_edit_type => array($object->getPHID() => $object->getPHID()), )); $editor ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->setParentMessageID($this->getParentMessageID()) ->setIsInverseEdgeEditor(true) ->setIsSilent($this->getIsSilent()) ->setActor($this->requireActor()) ->setActingAsPHID($this->getActingAsPHID()) ->setContentSource($this->getContentSource()); $editor->applyTransactions($target, array($template)); } } /* -( Workers )------------------------------------------------------------ */ /** * Load any object state which is required to publish transactions. * * This hook is invoked in the main process before we compute data related * to publishing transactions (like email "To" and "CC" lists), and again in * the worker before publishing occurs. * * @return object Publishable object. * @task workers */ protected function willPublish(PhabricatorLiskDAO $object, array $xactions) { return $object; } /** * Convert the editor state to a serializable dictionary which can be passed * to a worker. * * This data will be loaded with @{method:loadWorkerState} in the worker. * * @return dict Serializable editor state. * @task workers */ final private function getWorkerState() { $state = array(); foreach ($this->getAutomaticStateProperties() as $property) { $state[$property] = $this->$property; } $custom_state = $this->getCustomWorkerState(); $custom_encoding = $this->getCustomWorkerStateEncoding(); $state += array( 'excludeMailRecipientPHIDs' => $this->getExcludeMailRecipientPHIDs(), 'custom' => $this->encodeStateForStorage($custom_state, $custom_encoding), 'custom.encoding' => $custom_encoding, ); return $state; } /** * Hook; return custom properties which need to be passed to workers. * * @return dict Custom properties. * @task workers */ protected function getCustomWorkerState() { return array(); } /** * Hook; return storage encoding for custom properties which need to be * passed to workers. * * This primarily allows binary data to be passed to workers and survive * JSON encoding. * * @return dict Property encodings. * @task workers */ protected function getCustomWorkerStateEncoding() { return array(); } /** * Load editor state using a dictionary emitted by @{method:getWorkerState}. * * This method is used to load state when running worker operations. * * @param dict Editor state, from @{method:getWorkerState}. * @return this * @task workers */ final public function loadWorkerState(array $state) { foreach ($this->getAutomaticStateProperties() as $property) { $this->$property = idx($state, $property); } $exclude = idx($state, 'excludeMailRecipientPHIDs', array()); $this->setExcludeMailRecipientPHIDs($exclude); $custom_state = idx($state, 'custom', array()); $custom_encodings = idx($state, 'custom.encoding', array()); $custom = $this->decodeStateFromStorage($custom_state, $custom_encodings); $this->loadCustomWorkerState($custom); return $this; } /** * Hook; set custom properties on the editor from data emitted by * @{method:getCustomWorkerState}. * * @param dict Custom state, * from @{method:getCustomWorkerState}. * @return this * @task workers */ protected function loadCustomWorkerState(array $state) { return $this; } /** * Get a list of object properties which should be automatically sent to * workers in the state data. * * These properties will be automatically stored and loaded by the editor in * the worker. * * @return list List of properties. * @task workers */ private function getAutomaticStateProperties() { return array( 'parentMessageID', 'isNewObject', 'heraldEmailPHIDs', 'heraldForcedEmailPHIDs', 'heraldHeader', 'mailToPHIDs', 'mailCCPHIDs', 'feedNotifyPHIDs', 'feedRelatedPHIDs', 'feedShouldPublish', 'mailShouldSend', 'mustEncrypt', 'mailStamps', 'mailUnexpandablePHIDs', 'mailMutedPHIDs', 'webhookMap', 'silent', 'sendHistory', ); } /** * Apply encodings prior to storage. * * See @{method:getCustomWorkerStateEncoding}. * * @param map Map of values to encode. * @param map Map of encodings to apply. * @return map Map of encoded values. * @task workers */ final private function encodeStateForStorage( array $state, array $encodings) { foreach ($state as $key => $value) { $encoding = idx($encodings, $key); switch ($encoding) { case self::STORAGE_ENCODING_BINARY: // The mechanics of this encoding (serialize + base64) are a little // awkward, but it allows us encode arrays and still be JSON-safe // with binary data. $value = @serialize($value); if ($value === false) { throw new Exception( pht( 'Failed to serialize() value for key "%s".', $key)); } $value = base64_encode($value); if ($value === false) { throw new Exception( pht( 'Failed to base64 encode value for key "%s".', $key)); } break; } $state[$key] = $value; } return $state; } /** * Undo storage encoding applied when storing state. * * See @{method:getCustomWorkerStateEncoding}. * * @param map Map of encoded values. * @param map Map of encodings. * @return map Map of decoded values. * @task workers */ final private function decodeStateFromStorage( array $state, array $encodings) { foreach ($state as $key => $value) { $encoding = idx($encodings, $key); switch ($encoding) { case self::STORAGE_ENCODING_BINARY: $value = base64_decode($value); if ($value === false) { throw new Exception( pht( 'Failed to base64_decode() value for key "%s".', $key)); } $value = unserialize($value); break; } $state[$key] = $value; } return $state; } /** * Remove conflicts from a list of projects. * * Objects aren't allowed to be tagged with multiple milestones in the same * group, nor projects such that one tag is the ancestor of any other tag. * If the list of PHIDs include mutually exclusive projects, remove the * conflicting projects. * * @param list List of project PHIDs. * @return list List with conflicts removed. */ private function applyProjectConflictRules(array $phids) { if (!$phids) { return array(); } // Overall, the last project in the list wins in cases of conflict (so when // you add something, the thing you just added sticks and removes older // values). // Beyond that, there are two basic cases: // Milestones: An object can't be in "A > Sprint 3" and "A > Sprint 4". // If multiple projects are milestones of the same parent, we only keep the // last one. // Ancestor: You can't be in "A" and "A > B". If "A > B" comes later // in the list, we remove "A" and keep "A > B". If "A" comes later, we // remove "A > B" and keep "A". // Note that it's OK to be in "A > B" and "A > C". There's only a conflict // if one project is an ancestor of another. It's OK to have something // tagged with multiple projects which share a common ancestor, so long as // they are not mutual ancestors. $viewer = PhabricatorUser::getOmnipotentUser(); $projects = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withPHIDs(array_keys($phids)) ->execute(); $projects = mpull($projects, null, 'getPHID'); // We're going to build a map from each project with milestones to the last // milestone in the list. This last milestone is the milestone we'll keep. $milestone_map = array(); // We're going to build a set of the projects which have no descendants // later in the list. This allows us to apply both ancestor rules. $ancestor_map = array(); foreach ($phids as $phid => $ignored) { $project = idx($projects, $phid); if (!$project) { continue; } // This is the last milestone we've seen, so set it as the selection for // the project's parent. This might be setting a new value or overwriting // an earlier value. if ($project->isMilestone()) { $parent_phid = $project->getParentProjectPHID(); $milestone_map[$parent_phid] = $phid; } // Since this is the last item in the list we've examined so far, add it // to the set of projects with no later descendants. $ancestor_map[$phid] = $phid; // Remove any ancestors from the set, since this is a later descendant. foreach ($project->getAncestorProjects() as $ancestor) { $ancestor_phid = $ancestor->getPHID(); unset($ancestor_map[$ancestor_phid]); } } // Now that we've built the maps, we can throw away all the projects which // have conflicts. foreach ($phids as $phid => $ignored) { $project = idx($projects, $phid); if (!$project) { // If a PHID is invalid, we just leave it as-is. We could clean it up, // but leaving it untouched is less likely to cause collateral damage. continue; } // If this was a milestone, check if it was the last milestone from its // group in the list. If not, remove it from the list. if ($project->isMilestone()) { $parent_phid = $project->getParentProjectPHID(); if ($milestone_map[$parent_phid] !== $phid) { unset($phids[$phid]); continue; } } // If a later project in the list is a subproject of this one, it will // have removed ancestors from the map. If this project does not point // at itself in the ancestor map, it should be discarded in favor of a // subproject that comes later. if (idx($ancestor_map, $phid) !== $phid) { unset($phids[$phid]); continue; } // If a later project in the list is an ancestor of this one, it will // have added itself to the map. If any ancestor of this project points // at itself in the map, this project should be discarded in favor of // that later ancestor. foreach ($project->getAncestorProjects() as $ancestor) { $ancestor_phid = $ancestor->getPHID(); if (isset($ancestor_map[$ancestor_phid])) { unset($phids[$phid]); continue 2; } } } return $phids; } /** * When the view policy for an object is changed, scramble the secret keys * for attached files to invalidate existing URIs. */ private function scrambleFileSecrets($object) { // If this is a newly created object, we don't need to scramble anything // since it couldn't have been previously published. if ($this->getIsNewObject()) { return; } // If the object is a file itself, scramble it. if ($object instanceof PhabricatorFile) { if ($this->shouldScramblePolicy($object->getViewPolicy())) { $object->scrambleSecret(); $object->save(); } } $phid = $object->getPHID(); $attached_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $phid, PhabricatorObjectHasFileEdgeType::EDGECONST); if (!$attached_phids) { return; } $omnipotent_viewer = PhabricatorUser::getOmnipotentUser(); $files = id(new PhabricatorFileQuery()) ->setViewer($omnipotent_viewer) ->withPHIDs($attached_phids) ->execute(); foreach ($files as $file) { $view_policy = $file->getViewPolicy(); if ($this->shouldScramblePolicy($view_policy)) { $file->scrambleSecret(); $file->save(); } } } /** * Check if a policy is strong enough to justify scrambling. Objects which * are set to very open policies don't need to scramble their files, and * files with very open policies don't need to be scrambled when associated * objects change. */ private function shouldScramblePolicy($policy) { switch ($policy) { case PhabricatorPolicies::POLICY_PUBLIC: case PhabricatorPolicies::POLICY_USER: return false; } return true; } private function updateWorkboardColumns($object, $const, $old, $new) { // If an object is removed from a project, remove it from any proxy // columns for that project. This allows a task which is moved up from a // milestone to the parent to move back into the "Backlog" column on the // parent workboard. if ($const != PhabricatorProjectObjectHasProjectEdgeType::EDGECONST) { return; } // TODO: This should likely be some future WorkboardInterface. $appears_on_workboards = ($object instanceof ManiphestTask); if (!$appears_on_workboards) { return; } $removed_phids = array_keys(array_diff_key($old, $new)); if (!$removed_phids) { return; } // Find any proxy columns for the removed projects. $proxy_columns = id(new PhabricatorProjectColumnQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withProxyPHIDs($removed_phids) ->execute(); if (!$proxy_columns) { return array(); } $proxy_phids = mpull($proxy_columns, 'getPHID'); $position_table = new PhabricatorProjectColumnPosition(); $conn_w = $position_table->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE objectPHID = %s AND columnPHID IN (%Ls)', $position_table->getTableName(), $object->getPHID(), $proxy_phids); } private function getModularTransactionTypes() { if ($this->modularTypes === null) { $template = $this->object->getApplicationTransactionTemplate(); if ($template instanceof PhabricatorModularTransaction) { $xtypes = $template->newModularTransactionTypes(); foreach ($xtypes as $key => $xtype) { $xtype = clone $xtype; $xtype->setEditor($this); $xtypes[$key] = $xtype; } } else { $xtypes = array(); } $this->modularTypes = $xtypes; } return $this->modularTypes; } private function getModularTransactionType($type) { $types = $this->getModularTransactionTypes(); return idx($types, $type); } private function willApplyTransactions($object, array $xactions) { foreach ($xactions as $xaction) { $type = $xaction->getTransactionType(); $xtype = $this->getModularTransactionType($type); if (!$xtype) { continue; } $xtype->willApplyTransactions($object, $xactions); } } public function getCreateObjectTitle($author, $object) { return pht('%s created this object.', $author); } public function getCreateObjectTitleForFeed($author, $object) { return pht('%s created an object: %s.', $author, $object); } /* -( Queue )-------------------------------------------------------------- */ protected function queueTransaction( PhabricatorApplicationTransaction $xaction) { $this->transactionQueue[] = $xaction; return $this; } private function flushTransactionQueue($object) { if (!$this->transactionQueue) { return; } $xactions = $this->transactionQueue; $this->transactionQueue = array(); $editor = $this->newQueueEditor(); return $editor->applyTransactions($object, $xactions); } private function newQueueEditor() { $editor = id(newv(get_class($this), array())) ->setActor($this->getActor()) ->setContentSource($this->getContentSource()) ->setContinueOnNoEffect($this->getContinueOnNoEffect()) ->setContinueOnMissingFields($this->getContinueOnMissingFields()) ->setIsSilent($this->getIsSilent()); if ($this->actingAsPHID !== null) { $editor->setActingAsPHID($this->actingAsPHID); } return $editor; } /* -( Stamps )------------------------------------------------------------- */ public function newMailStampTemplates($object) { $actor = $this->getActor(); $templates = array(); $extensions = $this->newMailExtensions($object); foreach ($extensions as $extension) { $stamps = $extension->newMailStampTemplates($object); foreach ($stamps as $stamp) { $key = $stamp->getKey(); if (isset($templates[$key])) { throw new Exception( pht( 'Mail extension ("%s") defines a stamp template with the '. 'same key ("%s") as another template. Each stamp template '. 'must have a unique key.', get_class($extension), $key)); } $stamp->setViewer($actor); $templates[$key] = $stamp; } } return $templates; } final public function getMailStamp($key) { if (!isset($this->stampTemplates)) { throw new PhutilInvalidStateException('newMailStampTemplates'); } if (!isset($this->stampTemplates[$key])) { throw new Exception( pht( 'Editor ("%s") has no mail stamp template with provided key ("%s").', get_class($this), $key)); } return $this->stampTemplates[$key]; } private function newMailStamps($object, array $xactions) { $actor = $this->getActor(); $this->stampTemplates = $this->newMailStampTemplates($object); $extensions = $this->newMailExtensions($object); $stamps = array(); foreach ($extensions as $extension) { $extension->newMailStamps($object, $xactions); } return $this->stampTemplates; } private function newMailExtensions($object) { $actor = $this->getActor(); $all_extensions = PhabricatorMailEngineExtension::getAllExtensions(); $extensions = array(); foreach ($all_extensions as $key => $template) { $extension = id(clone $template) ->setViewer($actor) ->setEditor($this); if ($extension->supportsObject($object)) { $extensions[$key] = $extension; } } return $extensions; } private function generateMailStamps($object, $data) { if (!$data || !is_array($data)) { return null; } $templates = $this->newMailStampTemplates($object); foreach ($data as $spec) { if (!is_array($spec)) { continue; } $key = idx($spec, 'key'); if (!isset($templates[$key])) { continue; } $type = idx($spec, 'type'); if ($templates[$key]->getStampType() !== $type) { continue; } $value = idx($spec, 'value'); $templates[$key]->setValueFromDictionary($value); } $results = array(); foreach ($templates as $template) { $value = $template->getValueForRendering(); $rendered = $template->renderStamps($value); if ($rendered === null) { continue; } $rendered = (array)$rendered; foreach ($rendered as $stamp) { $results[] = $stamp; } } natcasesort($results); return $results; } public function getRemovedRecipientPHIDs() { return $this->mailRemovedPHIDs; } private function buildOldRecipientLists($object, $xactions) { // See T4776. Before we start making any changes, build a list of the old // recipients. If a change removes a user from the recipient list for an // object we still want to notify the user about that change. This allows // them to respond if they didn't want to be removed. if (!$this->shouldSendMail($object, $xactions)) { return; } $this->oldTo = $this->getMailTo($object); $this->oldCC = $this->getMailCC($object); return $this; } private function applyOldRecipientLists() { $actor_phid = $this->getActingAsPHID(); // If you took yourself off the recipient list (for example, by // unsubscribing or resigning) assume that you know what you did and // don't need to be notified. // If you just moved from "To" to "Cc" (or vice versa), you're still a // recipient so we don't need to add you back in. $map = array_fuse($this->mailToPHIDs) + array_fuse($this->mailCCPHIDs); foreach ($this->oldTo as $phid) { if ($phid === $actor_phid) { continue; } if (isset($map[$phid])) { continue; } $this->mailToPHIDs[] = $phid; $this->mailRemovedPHIDs[] = $phid; } foreach ($this->oldCC as $phid) { if ($phid === $actor_phid) { continue; } if (isset($map[$phid])) { continue; } $this->mailCCPHIDs[] = $phid; $this->mailRemovedPHIDs[] = $phid; } return $this; } private function queueWebhooks($object, array $xactions) { $hook_viewer = PhabricatorUser::getOmnipotentUser(); $webhook_map = $this->webhookMap; if (!is_array($webhook_map)) { $webhook_map = array(); } // Add any "Firehose" hooks to the list of hooks we're going to call. $firehose_hooks = id(new HeraldWebhookQuery()) ->setViewer($hook_viewer) ->withStatuses( array( HeraldWebhook::HOOKSTATUS_FIREHOSE, )) ->execute(); foreach ($firehose_hooks as $firehose_hook) { // This is "the hook itself is the reason this hook is being called", // since we're including it because it's configured as a firehose // hook. $hook_phid = $firehose_hook->getPHID(); $webhook_map[$hook_phid][] = $hook_phid; } if (!$webhook_map) { return; } // NOTE: We're going to queue calls to disabled webhooks, they'll just // immediately fail in the worker queue. This makes the behavior more // visible. $call_hooks = id(new HeraldWebhookQuery()) ->setViewer($hook_viewer) ->withPHIDs(array_keys($webhook_map)) ->execute(); foreach ($call_hooks as $call_hook) { $trigger_phids = idx($webhook_map, $call_hook->getPHID()); $request = HeraldWebhookRequest::initializeNewWebhookRequest($call_hook) ->setObjectPHID($object->getPHID()) ->setTransactionPHIDs(mpull($xactions, 'getPHID')) ->setTriggerPHIDs($trigger_phids) ->setRetryMode(HeraldWebhookRequest::RETRY_FOREVER) ->setIsSilentAction((bool)$this->getIsSilent()) ->setIsSecureAction((bool)$this->getMustEncrypt()) ->save(); $request->queueCall(); } } private function hasWarnings($object, $xaction) { // TODO: For the moment, this is a very un-modular hack to support // exactly one type of warning (mentioning users on a draft revision) // that we want to show. See PHI433. if (!($object instanceof DifferentialRevision)) { return false; } if (!$object->isDraft()) { return false; } $type = $xaction->getTransactionType(); if ($type != PhabricatorTransactions::TYPE_SUBSCRIBERS) { return false; } // NOTE: This will currently warn even if you're only removing // subscribers. return true; } private function buildHistoryMail(PhabricatorLiskDAO $object) { $viewer = $this->requireActor(); $recipient_phid = $this->getActingAsPHID(); // Load every transaction so we can build a mail message with a complete // history for the object. $query = PhabricatorApplicationTransactionQuery::newQueryForObject($object); $xactions = $query ->setViewer($viewer) ->withObjectPHIDs(array($object->getPHID())) ->execute(); $xactions = array_reverse($xactions); $mail_messages = $this->buildMailWithRecipients( $object, $xactions, array($recipient_phid), array(), array()); $mail = head($mail_messages); // Since the user explicitly requested "!history", force delivery of this // message regardless of their other mail settings. $mail->setForceDelivery(true); return $mail; } } diff --git a/src/applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php b/src/applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php index c5da130b68..0d20533798 100644 --- a/src/applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php +++ b/src/applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php @@ -1,76 +1,73 @@ getApplicationTransactionTemplate(); try { $comment = $xaction->getApplicationTransactionCommentObject(); } catch (PhutilMethodNotImplementedException $ex) { $comment = null; } return (bool)$comment; } public function newBulkEditGroups(PhabricatorEditEngine $engine) { return array( id(new PhabricatorBulkEditGroup()) ->setKey('comments') ->setLabel(pht('Comments')), ); } public function buildCustomEditFields( PhabricatorEditEngine $engine, PhabricatorApplicationTransactionInterface $object) { $comment_type = PhabricatorTransactions::TYPE_COMMENT; // Comments have a lot of special behavior which doesn't always check // this flag, but we set it for consistency. $is_interact = true; $comment_field = id(new PhabricatorCommentEditField()) ->setKey(self::EDITKEY) ->setLabel(pht('Comments')) ->setBulkEditLabel(pht('Add comment')) ->setBulkEditGroupKey('comments') ->setAliases(array('comments')) - ->setIsHidden(true) - ->setIsReorderable(false) - ->setIsDefaultable(false) - ->setIsLockable(false) + ->setIsFormField(false) ->setCanApplyWithoutEditCapability($is_interact) ->setTransactionType($comment_type) ->setConduitDescription(pht('Make comments.')) ->setConduitTypeDescription( pht('Comment to add, formatted as remarkup.')) ->setValue(null); return array( $comment_field, ); } } diff --git a/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php b/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php index 338702478c..7d32545416 100644 --- a/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php +++ b/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php @@ -1,60 +1,60 @@ supportsSubtypes(); } public function buildCustomEditFields( PhabricatorEditEngine $engine, PhabricatorApplicationTransactionInterface $object) { $subtype_type = PhabricatorTransactions::TYPE_SUBTYPE; $map = $object->newEditEngineSubtypeMap(); $options = mpull($map, 'getName'); $subtype_field = id(new PhabricatorSelectEditField()) ->setKey(self::EDITKEY) ->setLabel(pht('Subtype')) - ->setIsConduitOnly(true) - ->setIsHidden(true) - ->setIsReorderable(false) - ->setIsDefaultable(false) - ->setIsLockable(false) + ->setIsFormField(false) ->setTransactionType($subtype_type) ->setConduitDescription(pht('Change the object subtype.')) ->setConduitTypeDescription(pht('New object subtype key.')) ->setValue($object->getEditEngineSubtype()) ->setOptions($options); - // If subtypes are configured, enable changing them from the bulk editor. + // If subtypes are configured, enable changing them from the bulk editor + // and comment action stack. if (count($map) > 1) { - $subtype_field->setBulkEditLabel(pht('Change subtype to')); + $subtype_field + ->setBulkEditLabel(pht('Change subtype to')) + ->setCommentActionLabel(pht('Change Subtype')) + ->setCommentActionOrder(3000); } return array( $subtype_field, ); } } diff --git a/src/applications/transactions/query/PhabricatorApplicationTransactionCommentQuery.php b/src/applications/transactions/query/PhabricatorApplicationTransactionCommentQuery.php index f79de81ba3..4ca56101fc 100644 --- a/src/applications/transactions/query/PhabricatorApplicationTransactionCommentQuery.php +++ b/src/applications/transactions/query/PhabricatorApplicationTransactionCommentQuery.php @@ -1,125 +1,127 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withTransactionPHIDs(array $transaction_phids) { $this->transactionPHIDs = $transaction_phids; return $this; } public function withAuthorPHIDs(array $phids) { $this->authorPHIDs = $phids; return $this; } public function withIsDeleted($deleted) { $this->isDeleted = $deleted; return $this; } public function withHasTransaction($has_transaction) { $this->hasTransaction = $has_transaction; return $this; } protected function loadPage() { $table = $this->getTemplate(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T xcomment %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { - return $this->formatWhereClause($this->buildWhereClauseComponents($conn_r)); + protected function buildWhereClause(AphrontDatabaseConnection $conn) { + return $this->formatWhereClause( + $conn, + $this->buildWhereClauseComponents($conn)); } protected function buildWhereClauseComponents( - AphrontDatabaseConnection $conn_r) { + AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'xcomment.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'xcomment.phid IN (%Ls)', $this->phids); } if ($this->authorPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'xcomment.authorPHID IN (%Ls)', $this->authorPHIDs); } if ($this->transactionPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'xcomment.transactionPHID IN (%Ls)', $this->transactionPHIDs); } if ($this->isDeleted !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'xcomment.isDeleted = %d', (int)$this->isDeleted); } if ($this->hasTransaction !== null) { if ($this->hasTransaction) { $where[] = qsprintf( - $conn_r, + $conn, 'xcomment.transactionPHID IS NOT NULL'); } else { $where[] = qsprintf( - $conn_r, + $conn, 'xcomment.transactionPHID IS NULL'); } } return $where; } public function getQueryApplicationClass() { // TODO: Figure out the app via the template? return null; } } diff --git a/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php b/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php index ada979c45c..3a1c8ec60b 100644 --- a/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php +++ b/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php @@ -1,351 +1,356 @@ setSubtype(PhabricatorEditEngine::SUBTYPE_DEFAULT) ->setEngineKey($engine->getEngineKey()) ->attachEngine($engine) ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorEditEngineConfigurationPHIDType::TYPECONST); } public function getCreateSortKey() { return $this->getSortKey($this->createOrder); } public function getEditSortKey() { return $this->getSortKey($this->editOrder); } private function getSortKey($order) { // Put objects at the bottom by default if they haven't previously been // reordered. When they're explicitly reordered, the smallest sort key we // assign is 1, so if the object has a value of 0 it means it hasn't been // ordered yet. if ($order != 0) { $group = 'A'; } else { $group = 'B'; } return sprintf( "%s%012d%s\0%012d", $group, $order, $this->getName(), $this->getID()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'engineKey' => 'text64', 'builtinKey' => 'text64?', 'name' => 'text255', 'isDisabled' => 'bool', 'isDefault' => 'bool', 'isEdit' => 'bool', 'createOrder' => 'uint32', 'editOrder' => 'uint32', 'subtype' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( 'key_engine' => array( 'columns' => array('engineKey', 'builtinKey'), 'unique' => true, ), 'key_default' => array( 'columns' => array('engineKey', 'isDefault', 'isDisabled'), ), 'key_edit' => array( 'columns' => array('engineKey', 'isEdit', 'isDisabled'), ), ), ) + parent::getConfiguration(); } public function getProperty($key, $default = null) { return idx($this->properties, $key, $default); } public function setProperty($key, $value) { $this->properties[$key] = $value; return $this; } public function setBuiltinKey($key) { if (strpos($key, '/') !== false) { throw new Exception( pht('EditEngine BuiltinKey contains an invalid key character "/".')); } return parent::setBuiltinKey($key); } public function attachEngine(PhabricatorEditEngine $engine) { $this->engine = $engine; return $this; } public function getEngine() { return $this->assertAttached($this->engine); } public function applyConfigurationToFields( PhabricatorEditEngine $engine, $object, array $fields) { $fields = mpull($fields, null, 'getKey'); $is_new = !$object->getID(); $values = $this->getProperty('defaults', array()); foreach ($fields as $key => $field) { + if (!$field->getIsFormField()) { + continue; + } + if (!$field->getIsDefaultable()) { continue; } + if ($is_new) { if (array_key_exists($key, $values)) { $field->readDefaultValueFromConfiguration($values[$key]); } } } $locks = $this->getFieldLocks(); foreach ($fields as $field) { $key = $field->getKey(); switch (idx($locks, $key)) { case self::LOCK_LOCKED: $field->setIsHidden(false); if ($field->getIsLockable()) { $field->setIsLocked(true); } break; case self::LOCK_HIDDEN: $field->setIsHidden(true); if ($field->getIsLockable()) { $field->setIsLocked(false); } break; case self::LOCK_VISIBLE: $field->setIsHidden(false); if ($field->getIsLockable()) { $field->setIsLocked(false); } break; default: // If we don't have an explicit value, don't make any adjustments. break; } } $fields = $this->reorderFields($fields); $preamble = $this->getPreamble(); if (strlen($preamble)) { $fields = array( 'config.preamble' => id(new PhabricatorInstructionsEditField()) ->setKey('config.preamble') ->setIsReorderable(false) ->setIsDefaultable(false) ->setIsLockable(false) ->setValue($preamble), ) + $fields; } return $fields; } private function reorderFields(array $fields) { // Fields which can not be reordered are fixed in order at the top of the // form. These are used to show instructions or contextual information. $fixed = array(); foreach ($fields as $key => $field) { if (!$field->getIsReorderable()) { $fixed[$key] = $field; } } $keys = $this->getFieldOrder(); $fields = $fixed + array_select_keys($fields, $keys) + $fields; return $fields; } public function getURI() { $engine_key = $this->getEngineKey(); $key = $this->getIdentifier(); return "/transactions/editengine/{$engine_key}/view/{$key}/"; } public function getCreateURI() { $form_key = $this->getIdentifier(); $engine = $this->getEngine(); try { $create_uri = $engine->getEditURI(null, "form/{$form_key}/"); } catch (Exception $ex) { $create_uri = null; } return $create_uri; } public function getIdentifier() { $key = $this->getID(); if (!$key) { $key = $this->getBuiltinKey(); } return $key; } public function getDisplayName() { $name = $this->getName(); if (strlen($name)) { return $name; } $builtin = $this->getBuiltinKey(); if ($builtin !== null) { return pht('Builtin Form "%s"', $builtin); } return pht('Untitled Form'); } public function getPreamble() { return $this->getProperty('preamble'); } public function setPreamble($preamble) { return $this->setProperty('preamble', $preamble); } public function setFieldOrder(array $field_order) { return $this->setProperty('order', $field_order); } public function getFieldOrder() { return $this->getProperty('order', array()); } public function setFieldLocks(array $field_locks) { return $this->setProperty('locks', $field_locks); } public function getFieldLocks() { return $this->getProperty('locks', array()); } public function getFieldDefault($key) { $defaults = $this->getProperty('defaults', array()); return idx($defaults, $key); } public function setFieldDefault($key, $value) { $defaults = $this->getProperty('defaults', array()); $defaults[$key] = $value; return $this->setProperty('defaults', $defaults); } public function getIcon() { return $this->getEngine()->getIcon(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEngine() ->getApplication() ->getPolicy($capability); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicyFilter::hasCapability( $viewer, $this->getEngine()->getApplication(), PhabricatorPolicyCapability::CAN_EDIT); } return false; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorEditEngineConfigurationEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorEditEngineConfigurationTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } } diff --git a/src/applications/transactions/storage/PhabricatorModularTransactionType.php b/src/applications/transactions/storage/PhabricatorModularTransactionType.php index 3d81e02c38..ca82af1c66 100644 --- a/src/applications/transactions/storage/PhabricatorModularTransactionType.php +++ b/src/applications/transactions/storage/PhabricatorModularTransactionType.php @@ -1,395 +1,428 @@ getPhobjectClassConstant('TRANSACTIONTYPE'); } public function generateOldValue($object) { throw new PhutilMethodNotImplementedException(); } public function generateNewValue($object, $value) { return $value; } public function validateTransactions($object, array $xactions) { return array(); } public function willApplyTransactions($object, array $xactions) { return; } public function applyInternalEffects($object, $value) { return; } public function applyExternalEffects($object, $value) { return; } + public function didCommitTransaction($object, $value) { + return; + } + public function getTransactionHasEffect($object, $old, $new) { return ($old !== $new); } public function extractFilePHIDs($object, $value) { return array(); } public function shouldHide() { return false; } public function shouldHideForFeed() { return false; } public function shouldHideForMail() { return false; } public function getIcon() { return null; } public function getTitle() { return null; } public function getTitleForFeed() { return null; } public function getActionName() { return null; } public function getActionStrength() { return null; } public function getColor() { return null; } public function hasChangeDetailView() { return false; } public function newChangeDetailView() { return null; } public function getMailDiffSectionHeader() { return pht('EDIT DETAILS'); } public function newRemarkupChanges() { return array(); } public function mergeTransactions( $object, PhabricatorApplicationTransaction $u, PhabricatorApplicationTransaction $v) { return null; } final public function setStorage( PhabricatorApplicationTransaction $xaction) { $this->storage = $xaction; return $this; } private function getStorage() { return $this->storage; } final public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } final protected function getViewer() { return $this->viewer; } final public function getActor() { return $this->getEditor()->getActor(); } final public function getActingAsPHID() { return $this->getEditor()->getActingAsPHID(); } final public function setEditor( PhabricatorApplicationTransactionEditor $editor) { $this->editor = $editor; return $this; } final protected function getEditor() { if (!$this->editor) { throw new PhutilInvalidStateException('setEditor'); } return $this->editor; } final protected function hasEditor() { return (bool)$this->editor; } final protected function getAuthorPHID() { return $this->getStorage()->getAuthorPHID(); } final protected function getObjectPHID() { return $this->getStorage()->getObjectPHID(); } final protected function getObject() { return $this->getStorage()->getObject(); } final protected function getOldValue() { return $this->getStorage()->getOldValue(); } final protected function getNewValue() { return $this->getStorage()->getNewValue(); } final protected function renderAuthor() { $author_phid = $this->getAuthorPHID(); return $this->getStorage()->renderHandleLink($author_phid); } final protected function renderObject() { $object_phid = $this->getObjectPHID(); return $this->getStorage()->renderHandleLink($object_phid); } final protected function renderHandle($phid) { $viewer = $this->getViewer(); $display = $viewer->renderHandle($phid); if ($this->isTextMode()) { $display->setAsText(true); } return $display; } final protected function renderOldHandle() { return $this->renderHandle($this->getOldValue()); } final protected function renderNewHandle() { return $this->renderHandle($this->getNewValue()); } + final protected function renderOldPolicy() { + return $this->renderPolicy($this->getOldValue(), 'old'); + } + + final protected function renderNewPolicy() { + return $this->renderPolicy($this->getNewValue(), 'new'); + } + + final protected function renderPolicy($phid, $mode) { + $viewer = $this->getViewer(); + $handles = $viewer->loadHandles(array($phid)); + + $policy = PhabricatorPolicy::newFromPolicyAndHandle( + $phid, + $handles[$phid]); + + if ($this->isTextMode()) { + return $this->renderValue($policy->getFullName()); + } + + $storage = $this->getStorage(); + if ($policy->getType() == PhabricatorPolicyType::TYPE_CUSTOM) { + $policy->setHref('/transactions/'.$mode.'/'.$storage->getPHID().'/'); + $policy->setWorkflow(true); + } + + return $this->renderValue($policy->renderDescription()); + } + final protected function renderHandleList(array $phids) { $viewer = $this->getViewer(); $display = $viewer->renderHandleList($phids) ->setAsInline(true); if ($this->isTextMode()) { $display->setAsText(true); } return $display; } final protected function renderValue($value) { if ($this->isTextMode()) { return sprintf('"%s"', $value); } return phutil_tag( 'span', array( 'class' => 'phui-timeline-value', ), $value); } final protected function renderValueList(array $values) { $result = array(); foreach ($values as $value) { $result[] = $this->renderValue($value); } if ($this->isTextMode()) { return implode(', ', $result); } return phutil_implode_html(', ', $result); } final protected function renderOldValue() { return $this->renderValue($this->getOldValue()); } final protected function renderNewValue() { return $this->renderValue($this->getNewValue()); } final protected function renderDate($epoch) { $viewer = $this->getViewer(); // We accept either epoch timestamps or dictionaries describing a // PhutilCalendarDateTime. if (is_array($epoch)) { $datetime = PhutilCalendarAbsoluteDateTime::newFromDictionary($epoch) ->setViewerTimezone($viewer->getTimezoneIdentifier()); $all_day = $datetime->getIsAllDay(); $epoch = $datetime->getEpoch(); } else { $all_day = false; } if ($all_day) { $display = phabricator_date($epoch, $viewer); } else if ($this->isRenderingTargetExternal()) { // When rendering to text, we explicitly render the offset from UTC to // provide context to the date: the mail may be generating with the // server's settings, or the user may later refer back to it after // changing timezones. $display = phabricator_datetimezone($epoch, $viewer); } else { $display = phabricator_datetime($epoch, $viewer); } return $this->renderValue($display); } final protected function renderOldDate() { return $this->renderDate($this->getOldValue()); } final protected function renderNewDate() { return $this->renderDate($this->getNewValue()); } final protected function newError($title, $message, $xaction = null) { return new PhabricatorApplicationTransactionValidationError( $this->getTransactionTypeConstant(), $title, $message, $xaction); } final protected function newRequiredError($message, $xaction = null) { return $this->newError(pht('Required'), $message, $xaction) ->setIsMissingFieldError(true); } final protected function newInvalidError($message, $xaction = null) { return $this->newError(pht('Invalid'), $message, $xaction); } final protected function isNewObject() { return $this->getEditor()->getIsNewObject(); } final protected function isEmptyTextTransaction($value, array $xactions) { foreach ($xactions as $xaction) { $value = $xaction->getNewValue(); } return !strlen($value); } /** * When rendering to external targets (Email/Asana/etc), we need to include * more information that users can't obtain later. */ final protected function isRenderingTargetExternal() { // Right now, this is our best proxy for this: return $this->isTextMode(); // "TARGET_TEXT" means "EMail" and "TARGET_HTML" means "Web". } final protected function isTextMode() { $target = $this->getStorage()->getRenderingTarget(); return ($target == PhabricatorApplicationTransaction::TARGET_TEXT); } final protected function newRemarkupChange() { return id(new PhabricatorTransactionRemarkupChange()) ->setTransaction($this->getStorage()); } final protected function isCreateTransaction() { return $this->getStorage()->getIsCreateTransaction(); } final protected function getPHIDList(array $old, array $new) { $editor = $this->getEditor(); return $editor->getPHIDList($old, $new); } public function getMetadataValue($key, $default = null) { return $this->getStorage()->getMetadataValue($key, $default); } public function loadTransactionTypeConduitData(array $xactions) { return null; } public function getTransactionTypeForConduit($xaction) { return null; } public function getFieldValuesForConduit($xaction, $data) { return array(); } protected function requireApplicationCapability($capability) { $application_class = $this->getEditor()->getEditorApplicationClass(); $application = newv($application_class, array()); PhabricatorPolicyFilter::requireCapability( $this->getActor(), $application, $capability); } /** * Get a list of capabilities the actor must have on the object to apply * a transaction to it. * * Usually, you should use this to reduce capability requirements when a * transaction (like leaving a Conpherence thread) can be applied without * having edit permission on the object. You can override this method to * remove the CAN_EDIT requirement, or to replace it with a different * requirement. * * If you are increasing capability requirements and need to add an * additional capability or policy requirement above and beyond CAN_EDIT, it * is usually better implemented as a validation check. * * @param object Object being edited. * @param PhabricatorApplicationTransaction Transaction being applied. * @return null|const|list A capability constant (or list of * capability constants) which the actor must have on the object. You can * return `null` as a shorthand for "no capabilities are required". */ public function getRequiredCapabilities( $object, PhabricatorApplicationTransaction $xaction) { return PhabricatorPolicyCapability::CAN_EDIT; } } diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php index c0f514c904..227854c79f 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php @@ -1,569 +1,571 @@ objectPHID = $object_phid; return $this; } public function getObjectPHID() { return $this->objectPHID; } public function setShowPreview($show_preview) { $this->showPreview = $show_preview; return $this; } public function getShowPreview() { return $this->showPreview; } public function setRequestURI(PhutilURI $request_uri) { $this->requestURI = $request_uri; return $this; } public function getRequestURI() { return $this->requestURI; } public function setCurrentVersion($current_version) { $this->currentVersion = $current_version; return $this; } public function getCurrentVersion() { return $this->currentVersion; } public function setVersionedDraft( PhabricatorVersionedDraft $versioned_draft) { $this->versionedDraft = $versioned_draft; return $this; } public function getVersionedDraft() { return $this->versionedDraft; } public function setDraft(PhabricatorDraft $draft) { $this->draft = $draft; return $this; } public function getDraft() { return $this->draft; } public function setSubmitButtonName($submit_button_name) { $this->submitButtonName = $submit_button_name; return $this; } public function getSubmitButtonName() { return $this->submitButtonName; } public function setAction($action) { $this->action = $action; return $this; } public function getAction() { return $this->action; } public function setHeaderText($text) { $this->headerText = $text; return $this; } public function setFullWidth($fw) { $this->fullWidth = $fw; return $this; } public function setInfoView(PHUIInfoView $info_view) { $this->infoView = $info_view; return $this; } public function getInfoView() { return $this->infoView; } public function setCommentActions(array $comment_actions) { assert_instances_of($comment_actions, 'PhabricatorEditEngineCommentAction'); $this->commentActions = $comment_actions; return $this; } public function getCommentActions() { return $this->commentActions; } public function setCommentActionGroups(array $groups) { assert_instances_of($groups, 'PhabricatorEditEngineCommentActionGroup'); $this->commentActionGroups = $groups; return $this; } public function getCommentActionGroups() { return $this->commentActionGroups; } public function setNoPermission($no_permission) { $this->noPermission = $no_permission; return $this; } public function getNoPermission() { return $this->noPermission; } public function setEditEngineLock(PhabricatorEditEngineLock $lock) { $this->editEngineLock = $lock; return $this; } public function getEditEngineLock() { return $this->editEngineLock; } public function setTransactionTimeline( PhabricatorApplicationTransactionView $timeline) { $timeline->setQuoteTargetID($this->getCommentID()); if ($this->getNoPermission() || $this->getEditEngineLock()) { $timeline->setShouldTerminate(true); } $this->transactionTimeline = $timeline; return $this; } public function render() { if ($this->getNoPermission()) { return null; } $lock = $this->getEditEngineLock(); if ($lock) { return id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setErrors( array( $lock->getLockedObjectDisplayText(), )); } $user = $this->getUser(); if (!$user->isLoggedIn()) { $uri = id(new PhutilURI('/login/')) ->setQueryParam('next', (string)$this->getRequestURI()); return id(new PHUIObjectBoxView()) ->setFlush(true) ->appendChild( javelin_tag( 'a', array( 'class' => 'login-to-comment button', 'href' => $uri, ), pht('Log In to Comment'))); } $data = array(); $comment = $this->renderCommentPanel(); if ($this->getShowPreview()) { $preview = $this->renderPreviewPanel(); } else { $preview = null; } if (!$this->getCommentActions()) { Javelin::initBehavior( 'phabricator-transaction-comment-form', array( 'formID' => $this->getFormID(), 'timelineID' => $this->getPreviewTimelineID(), 'panelID' => $this->getPreviewPanelID(), 'showPreview' => $this->getShowPreview(), 'actionURI' => $this->getAction(), )); } require_celerity_resource('phui-comment-form-css'); $image_uri = $user->getProfileImageURI(); $image = phutil_tag( 'div', array( 'style' => 'background-image: url('.$image_uri.')', 'class' => 'phui-comment-image visual-only', )); $wedge = phutil_tag( 'div', array( 'class' => 'phui-timeline-wedge', ), ''); $badge_view = $this->renderBadgeView(); $comment_box = id(new PHUIObjectBoxView()) ->setFlush(true) ->addClass('phui-comment-form-view') ->addSigil('phui-comment-form') ->appendChild( phutil_tag( 'h3', array( 'class' => 'aural-only', ), pht('Add Comment'))) ->appendChild($image) ->appendChild($badge_view) ->appendChild($wedge) ->appendChild($comment); return array($comment_box, $preview); } private function renderCommentPanel() { $draft_comment = ''; $draft_key = null; if ($this->getDraft()) { $draft_comment = $this->getDraft()->getDraft(); $draft_key = $this->getDraft()->getDraftKey(); } $versioned_draft = $this->getVersionedDraft(); if ($versioned_draft) { $draft_comment = $versioned_draft->getProperty('comment', ''); } if (!$this->getObjectPHID()) { throw new PhutilInvalidStateException('setObjectPHID', 'render'); } $version_key = PhabricatorVersionedDraft::KEY_VERSION; $version_value = $this->getCurrentVersion(); $form = id(new AphrontFormView()) ->setUser($this->getUser()) ->addSigil('transaction-append') ->setWorkflow(true) ->setFullWidth($this->fullWidth) ->setMetadata( array( 'objectPHID' => $this->getObjectPHID(), )) ->setAction($this->getAction()) ->setID($this->getFormID()) ->addHiddenInput('__draft__', $draft_key) ->addHiddenInput($version_key, $version_value); $comment_actions = $this->getCommentActions(); if ($comment_actions) { $action_map = array(); $type_map = array(); $comment_actions = mpull($comment_actions, null, 'getKey'); $draft_actions = array(); $draft_keys = array(); if ($versioned_draft) { $draft_actions = $versioned_draft->getProperty('actions', array()); if (!is_array($draft_actions)) { $draft_actions = array(); } foreach ($draft_actions as $action) { $type = idx($action, 'type'); $comment_action = idx($comment_actions, $type); if (!$comment_action) { continue; } $value = idx($action, 'value'); $comment_action->setValue($value); $draft_keys[] = $type; } } foreach ($comment_actions as $key => $comment_action) { $key = $comment_action->getKey(); $label = $comment_action->getLabel(); - $action_map[$key] = array( 'key' => $key, 'label' => $label, 'type' => $comment_action->getPHUIXControlType(), 'spec' => $comment_action->getPHUIXControlSpecification(), 'initialValue' => $comment_action->getInitialValue(), 'groupKey' => $comment_action->getGroupKey(), 'conflictKey' => $comment_action->getConflictKey(), 'auralLabel' => pht('Remove Action: %s', $label), + 'buttonText' => $comment_action->getSubmitButtonText(), ); $type_map[$key] = $comment_action; } $options = $this->newCommentActionOptions($action_map); $action_id = celerity_generate_unique_node_id(); $input_id = celerity_generate_unique_node_id(); $place_id = celerity_generate_unique_node_id(); $form->appendChild( phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'editengine.actions', 'id' => $input_id, ))); $invisi_bar = phutil_tag( 'div', array( 'id' => $place_id, 'class' => 'phui-comment-control-stack', )); $action_select = id(new AphrontFormSelectControl()) ->addClass('phui-comment-fullwidth-control') ->addClass('phui-comment-action-control') ->setID($action_id) ->setOptions($options); $action_bar = phutil_tag( 'div', array( 'class' => 'phui-comment-action-bar grouped', ), array( $action_select, )); $form->appendChild($action_bar); $info_view = $this->getInfoView(); if ($info_view) { $form->appendChild($info_view); } $form->appendChild($invisi_bar); $form->addClass('phui-comment-has-actions'); Javelin::initBehavior( 'comment-actions', array( 'actionID' => $action_id, 'inputID' => $input_id, 'formID' => $this->getFormID(), 'placeID' => $place_id, 'panelID' => $this->getPreviewPanelID(), 'timelineID' => $this->getPreviewTimelineID(), 'actions' => $action_map, 'showPreview' => $this->getShowPreview(), 'actionURI' => $this->getAction(), 'drafts' => $draft_keys, + 'defaultButtonText' => $this->getSubmitButtonName(), )); } $submit_button = id(new AphrontFormSubmitControl()) ->addClass('phui-comment-fullwidth-control') ->addClass('phui-comment-submit-control') ->setValue($this->getSubmitButtonName()); $form ->appendChild( id(new PhabricatorRemarkupControl()) ->setID($this->getCommentID()) ->addClass('phui-comment-fullwidth-control') ->addClass('phui-comment-textarea-control') ->setCanPin(true) ->setName('comment') ->setUser($this->getUser()) ->setValue($draft_comment)) ->appendChild( id(new AphrontFormSubmitControl()) ->addClass('phui-comment-fullwidth-control') ->addClass('phui-comment-submit-control') + ->addSigil('submit-transactions') ->setValue($this->getSubmitButtonName())); return $form; } private function renderPreviewPanel() { $preview = id(new PHUITimelineView()) ->setID($this->getPreviewTimelineID()); return phutil_tag( 'div', array( 'id' => $this->getPreviewPanelID(), 'style' => 'display: none', 'class' => 'phui-comment-preview-view', ), $preview); } private function getPreviewPanelID() { if (!$this->previewPanelID) { $this->previewPanelID = celerity_generate_unique_node_id(); } return $this->previewPanelID; } private function getPreviewTimelineID() { if (!$this->previewTimelineID) { $this->previewTimelineID = celerity_generate_unique_node_id(); } return $this->previewTimelineID; } public function setFormID($id) { $this->formID = $id; return $this; } private function getFormID() { if (!$this->formID) { $this->formID = celerity_generate_unique_node_id(); } return $this->formID; } private function getStatusID() { if (!$this->statusID) { $this->statusID = celerity_generate_unique_node_id(); } return $this->statusID; } private function getCommentID() { if (!$this->commentID) { $this->commentID = celerity_generate_unique_node_id(); } return $this->commentID; } private function newCommentActionOptions(array $action_map) { $options = array(); $options['+'] = pht('Add Action...'); // Merge options into groups. $groups = array(); foreach ($action_map as $key => $item) { $group_key = $item['groupKey']; if (!isset($groups[$group_key])) { $groups[$group_key] = array(); } $groups[$group_key][$key] = $item; } $group_specs = $this->getCommentActionGroups(); $group_labels = mpull($group_specs, 'getLabel', 'getKey'); // Reorder groups to put them in the same order as the recognized // group definitions. $groups = array_select_keys($groups, array_keys($group_labels)) + $groups; // Move options with no group to the end. $default_group = idx($groups, ''); if ($default_group) { unset($groups['']); $groups[''] = $default_group; } foreach ($groups as $group_key => $group_items) { if (strlen($group_key)) { $group_label = idx($group_labels, $group_key, $group_key); $options[$group_label] = ipull($group_items, 'label'); } else { foreach ($group_items as $key => $item) { $options[$key] = $item['label']; } } } return $options; } private function renderBadgeView() { $user = $this->getUser(); $can_use_badges = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorBadgesApplication', $user); if (!$can_use_badges) { return null; } // Pull Badges from UserCache $badges = $user->getRecentBadgeAwards(); $badge_view = null; if ($badges) { $badge_list = array(); foreach ($badges as $badge) { $badge_view = id(new PHUIBadgeMiniView()) ->setIcon($badge['icon']) ->setQuality($badge['quality']) ->setHeader($badge['name']) ->setTipDirection('E') ->setHref('/badges/view/'.$badge['id'].'/'); $badge_list[] = $badge_view; } $flex = new PHUIBadgeBoxView(); $flex->addItems($badge_list); $flex->setCollapsed(true); $badge_view = phutil_tag( 'div', array( 'class' => 'phui-timeline-badges', ), $flex); } return $badge_view; } } diff --git a/src/docs/user/userguide/diffusion_managing.diviner b/src/docs/user/userguide/diffusion_managing.diviner index cc03433532..a671b0f09e 100644 --- a/src/docs/user/userguide/diffusion_managing.diviner +++ b/src/docs/user/userguide/diffusion_managing.diviner @@ -1,385 +1,427 @@ @title Diffusion User Guide: Managing Repositories @group userguide Guide to configuring and managing repositories in Diffusion. Overview ======== After you create a new repository in Diffusion or select **Manage Repository** from the main screen if an existing repository, you'll be taken to the repository management interface for that repository. On this interface, you'll find many options which allow you to configure the behavior of a repository. This document walks through the options. Basics ====== The **Basics** section of the management interface allows you to configure the repository name, description, and identifiers. You can also activate or deactivate the repository here, and configure a few other miscellaneous settings. Basics: Name ============ The repository name is a human-readable primary name for the repository. It does not need to be unique Because the name is not unique and does not have any meaningful restrictions, it's fairly ambiguous and isn't very useful as an identifier. The other basic information (primarily callsigns and short names) gives you control over repository identifiers. Basics: Callsigns ================= Each repository can optionally be identified by a "callsign", which is a short uppercase string like "P" (for Phabricator) or "ARC" (for Arcanist). The primary goal of callsigns is to namespace commits to SVN repositories: if you use multiple SVN repositories, each repository has a revision 1, revision 2, etc., so referring to them by number alone is ambiguous. However, even for Git and Mercurial they impart additional information to human readers and allow parsers to detect that something is a commit name with high probability (and allow distinguishing between multiple copies of a repository). Configuring a callsign can make interacting with a commonly-used repository easier, but you may not want to bother assigning one to every repository if you have some similar, templated, or rarely-used repositories. If you choose to assign a callsign to a repository, it must be unique within an install but do not need to be globally unique, so you are free to use the single-letter callsigns for brevity. For example, Facebook uses "E" for the Engineering repository, "O" for the Ops repository, "Y" for a Yum package repository, and so on, while Phabricator uses "P", "ARC", "PHU" for libphutil, and "J" for Javelin. Keeping callsigns brief will make them easier to use, and the use of one-character callsigns is encouraged if they are reasonably evocative. If you configure a callsign like `XYZ`, Phabricator will activate callsign URIs and activate the callsign identifier (like `rXYZ`) for the repository. These more human-readable identifiers can make things a little easier to interact with. Basics: Short Name ================== Each repository can optionally have a unique short name. Short names must be unique and have some minor restrictions to make sure they are unambiguous and appropriate for use as directory names and in URIs. Basics: Description =================== You may optionally provide a brief (or, at your discretion, excruciatingly long) human-readable description of the repository. This description will be shown on the main repository page. You can also create a `README` file at the repository root (or in any subdirectory) to provide information about the repository. These formats are supported: | File Name | Rendered As... |-------------------|--------------- | `README` | Plain Text | `README.txt` | Plain Text | `README.remarkup` | Remarkup | `README.md` | Remarkup | `README.rainbow` | Rainbow Basics: Encoding ================ Before content from the repository can be shown in the web UI or embedded in other contexts like email, it must be converted to UTF-8. Most source code is written in UTF-8 or a subset of UTF-8 (like plain ASCII) already, so everything will work fine. The majority of repositories do not need to adjust this setting. If your repository is primarily written in some other encoding, specify it here so Phabricator can convert from it properly when reading content to embed in a webpage or email. Basics: Dangerous Changes ========================= By default, repositories are protected against dangerous changes. Dangerous changes are operations which rewrite or destroy repository history (for example, by deleting or rewriting branches). Normally, these take the form of `git push --force` or similar. It is normally a good idea to leave this protection enabled because most scalable workflows rarely rewrite repository history and it's easy to make mistakes which are expensive to correct if this protection is disabled. If you do occasionally need to rewrite published history, you can treat this option like a safety: disable it, perform required rewrites, then enable it again. If you fully disable this at the repository level, you can still use Herald to selectively protect certain branches or grant this power to a limited set of users. This option is only available in Git and Mercurial, because it is impossible to make dangerous changes in Subversion. This option has no effect if a repository is not hosted because Phabricator can not prevent dangerous changes in a remote repository it is merely observing. Basics: Deactivate Repository ============================= Repositories can be deactivated. Deactivating a repository has these effects: - the repository will no longer be updated; - users will no longer be able to clone/fetch/checkout the repository; - users will no longer be able to push to the repository; and - the repository will be hidden from view in default queries. When repositories are created for the first time, they are deactivated. This gives you an opportunity to customize settings, like adjusting policies or configuring a URI to observe. You must activate a repository before it will start working normally. Basics: Delete Repository ========================= Repositories can not be deleted from the web UI, so this option is always disabled. Clicking it gives you information about how to delete a repository. Repositories can only be deleted from the command line, with `bin/remove`: ``` $ ./bin/remove destroy ``` WARNING: This command will issue you a dire warning about the severity of the action you are taking. Heed this warning. You are **strongly discouraged** from destroying repositories. Instead, deactivate them. Policies ======== The **Policies** section of the management interface allows you to review and manage repository access policies. You can configure granular access policies for each repository to control who can view, clone, administrate, and push to the repository. Policies: View ============== The view policy for a repository controls who can view the repository from the web UI and clone, fetch, or check it out from Phabricator. Users who can view a repository can also access the "Manage" interface to review information about the repository and examine the edit history, but can not make any changes. Policies: Edit ============== The edit policy for a repository controls who can change repository settings using the "Manage" interface. In essence, this is permission to administrate the repository. You must be able to view a repository to edit it. You do not need this permission to push changes to a repository. Policies: Push ============== The push policy for a repository controls who can push changes to the repository. This policy has no effect if Phabricator is not hosting the repository, because it can not control who is allowed to make changes to a remote repository it is merely observing. You must also be able to view a repository to push to it. You do not need to be able to edit a repository to push to it. Further restrictions on who can push (and what they can push) can be configured for hosted repositories with Herald, which allows you to write more sophisticated rules that evaluate when Phabricator receives a push. To get started with Herald, see @{article:Herald User Guide}. Additionally, Git and Mercurial repositories have a setting which allows you to **Prevent Dangerous Changes**. This setting is enabled by default and will prevent any users from pushing changes which rewrite or destroy history. URIs ==== The **URIs** panel allows you to add and manage URIs which Phabricator will fetch from, serve from, and push to. These options are covered in detail in @{article:Diffusion User Guide: URIs}. -Staging Area -============ +Limits +====== -The **Staging Area** panel configures staging areas, used to make proposed -changes available to build and continuous integration systems. +The **Limits** panel allows you to configure limits and timeouts. -For more details, see @{article:Harbormaster User Guide}. +**Filesize Limit**: Allows you to set a maximum filesize for any file in the +repository. If a commit creates a larger file (or modifies an existing file so +it becomes too large) it will be rejected. This option only applies to hosted +repositories. +This limit is primarily intended to make it more difficult to accidentally push +very large files that shouldn't be version controlled (like logs, binaries, +machine learning data, or media assets). Pushing huge datafiles by mistake can +make the repository unwieldy by dramatically increasing how much data must be +transferred over the network to clone it, and simply reverting the changes +doesn't reduce the impact of this kind of mistake. -Automation -========== - -The **Automation** panel configures support for allowing Phabricator to make -writes directly to the repository, so that it can perform operations like -automatically landing revisions from the web UI. - -For details on repository automation, see -@{article:Drydock User Guide: Repository Automation}. - +**Clone/Fetch Timeout**: Configure the internal timeout for creating copies +of this repository during operations like intracluster synchronization and +Drydock working copy construction. This timeout does not affect external +users. -Symbols -====== +**Touch Limit**: Apply a limit to the maximum number of paths that any commit +may touch. If a commit affects more paths than this limit, it will be rejected. +This option only applies to hosted repositories. Users may work around this +limit by breaking the commit into several smaller commits which each affect +fewer paths. -The **Symbols** panel allows you to customize how symbols (like class and -function names) are linked when viewing code in the repository, and when -viewing revisions which propose code changes to the repository. +This limit is intended to offer a guard rail against users making silly +mistakes that create obviously mistaken changes, like copying an entire +repository into itself and pushing the result. This kind of change can take +some effort to clean up if it becomes part of repository history. -To take advantage of this feature, you need to do additional work to build -symbol indexes. For details on configuring and populating symbol indexes, see -@{article:User Guide: Symbol Indexes}. +Note that if you move a file, both the old and new locations count as touched +paths. You should generally configure this limit to be more than twice the +number of files you anticipate any user ever legitimately wanting to move in +a single commit. For example, a limit of `20000` will let users move up to +10,000 files in a single commit, but will reject users mistakenly trying to +push a copy of another repository or a directory with a million logfiles or +whatever other kind of creative nonsense they manage to dream up. Branches ======== The **Branches** panel allows you to configure how Phabricator interacts with branches. This panel is not available for Subversion repositories, because Subversion does not have formal branches. You can configure **Default Branch**. This controls which branch is shown by default in the UI. If no branch is provided, Phabricator will use `master` in Git and `default` in Mercurial. If you want Diffusion to ignore some branches in the repository, you can configure **Track Only**. Other branches will be ignored. If you do not specify any branches, all branches are tracked. When specifying branches, you should enter one branch name per line. You can use regular expressions to match branches by wrapping an expression in `regexp(...)`. For example: | Example | Effect | |---------|--------| | `master` | Track only `master`. | `regexp(/^release-/)` | Track all branches which start with `release-`. | `regexp(/^(?!temp-)/)` | Do not track branches which start with `temp-`. Actions ====== The **Actions** panel can configure notifications and publishing behavior. Normally, Phabricator publishes notifications when it discovers new commits. You can disable publishing for a repository by turning off **Publish/Notify**. This will disable notifications, feed, and Herald (including audits and build plans) for this repository. When Phabricator discovers a new commit, it can automatically close associated revisions and tasks. If you don't want Phabricator to close objects when it discovers new commits, disable **Autoclose** for the repository. +Staging Area +============ + +The **Staging Area** panel configures staging areas, used to make proposed +changes available to build and continuous integration systems. + +For more details, see @{article:Harbormaster User Guide}. + + +Automation +========== + +The **Automation** panel configures support for allowing Phabricator to make +writes directly to the repository, so that it can perform operations like +automatically landing revisions from the web UI. + +For details on repository automation, see +@{article:Drydock User Guide: Repository Automation}. + + +Symbols +====== + +The **Symbols** panel allows you to customize how symbols (like class and +function names) are linked when viewing code in the repository, and when +viewing revisions which propose code changes to the repository. + +To take advantage of this feature, you need to do additional work to build +symbol indexes. For details on configuring and populating symbol indexes, see +@{article:User Guide: Symbol Indexes}. + + Repository Identifiers and Names ================================ Repositories have several short identifiers which you can use to refer to the repository. For example, if you use command-line administrative tools to interact with a repository, you'll provide one of these identifiers: ``` $ ./bin/repository update ``` The identifiers available for a repository depend on which options are configured. Each repository may have several identifiers: - An **ID** identifier, like `R123`. This is available for all repositories. - A **callsign** identifier, like `rXY`. This is available for repositories with a callsign. - A **short name** identifier, like `xylophone`. This is available for repositories with a short name. All three identifiers can be used to refer to the repository in cases where the intent is unambiguous, but only the first two forms work in ambiguous contexts. For example, if you type `R123` or `rXY` into a comment, Phabricator will recognize them as references to the repository. If you type `xylophone`, it assumes you mean the word "xylophone". Only the `R123` identifier is immutable: the others can be changed later by adjusting the callsign or short name for the repository. Commit Identifiers ================== Diffusion uses repository identifiers and information about the commit itself to generate globally unique identifiers for each commit, like `rE12345`. Each commit may have several identifiers: - A repository **ID** identifier, like `R123:abcdef123...`. - A repository **callsign** identifier, like `rXYZabcdef123...`. This only works if a repository has a callsign. - Any unique prefix of the commit hash. Git and Mercurial use commit hashes to identify commits, and Phabricator will recognize a commit if the hash prefix is unique and sufficiently long. Commit hashes qualified with a repository identifier must be at least 5 characters long; unqualified commit hashes must be at least 7 characters long. In Subversion, commit identifiers are sequential integers and prefixes can not be used to identify them. When rendering the name of a Git or Mercurial commit hash, Phabricator tends to shorten it to 12 characters. This "short length" is relatively long compared to Git itself (which often uses 7 characters). See this post on the LKML for a historical explanation of Git's occasional internal use of 7-character hashes: https://lkml.org/lkml/2010/10/28/287 Because 7-character hashes are likely to collide for even moderately large repositories, Diffusion generally uses either a 12-character prefix (which makes collisions very unlikely) or the full 40-character hash (which makes collisions astronomically unlikely). Next Steps ========== Continue by: - returning to the @{article:Diffusion User Guide}. diff --git a/src/infrastructure/cluster/PhabricatorDatabaseRef.php b/src/infrastructure/cluster/PhabricatorDatabaseRef.php index 7dc55427ca..89435b5869 100644 --- a/src/infrastructure/cluster/PhabricatorDatabaseRef.php +++ b/src/infrastructure/cluster/PhabricatorDatabaseRef.php @@ -1,738 +1,744 @@ host = $host; return $this; } public function getHost() { return $this->host; } public function setPort($port) { $this->port = $port; return $this; } public function getPort() { return $this->port; } public function setUser($user) { $this->user = $user; return $this; } public function getUser() { return $this->user; } public function setPass(PhutilOpaqueEnvelope $pass) { $this->pass = $pass; return $this; } public function getPass() { return $this->pass; } public function setIsMaster($is_master) { $this->isMaster = $is_master; return $this; } public function getIsMaster() { return $this->isMaster; } public function setDisabled($disabled) { $this->disabled = $disabled; return $this; } public function getDisabled() { return $this->disabled; } public function setConnectionLatency($connection_latency) { $this->connectionLatency = $connection_latency; return $this; } public function getConnectionLatency() { return $this->connectionLatency; } public function setConnectionStatus($connection_status) { $this->connectionStatus = $connection_status; return $this; } public function getConnectionStatus() { if ($this->connectionStatus === null) { throw new PhutilInvalidStateException('queryAll'); } return $this->connectionStatus; } public function setConnectionMessage($connection_message) { $this->connectionMessage = $connection_message; return $this; } public function getConnectionMessage() { return $this->connectionMessage; } public function setReplicaStatus($replica_status) { $this->replicaStatus = $replica_status; return $this; } public function getReplicaStatus() { return $this->replicaStatus; } public function setReplicaMessage($replica_message) { $this->replicaMessage = $replica_message; return $this; } public function getReplicaMessage() { return $this->replicaMessage; } public function setReplicaDelay($replica_delay) { $this->replicaDelay = $replica_delay; return $this; } public function getReplicaDelay() { return $this->replicaDelay; } public function setIsIndividual($is_individual) { $this->isIndividual = $is_individual; return $this; } public function getIsIndividual() { return $this->isIndividual; } public function setIsDefaultPartition($is_default_partition) { $this->isDefaultPartition = $is_default_partition; return $this; } public function getIsDefaultPartition() { return $this->isDefaultPartition; } public function setUsePersistentConnections($use_persistent_connections) { $this->usePersistentConnections = $use_persistent_connections; return $this; } public function getUsePersistentConnections() { return $this->usePersistentConnections; } public function setApplicationMap(array $application_map) { $this->applicationMap = $application_map; return $this; } public function getApplicationMap() { return $this->applicationMap; } public function getPartitionStateForCommit() { $state = PhabricatorEnv::getEnvConfig('cluster.databases'); foreach ($state as $key => $value) { // Don't store passwords, since we don't care if they differ and // users may find it surprising. unset($state[$key]['pass']); } return phutil_json_encode($state); } public function setMasterRef(PhabricatorDatabaseRef $master_ref) { $this->masterRef = $master_ref; return $this; } public function getMasterRef() { return $this->masterRef; } public function addReplicaRef(PhabricatorDatabaseRef $replica_ref) { $this->replicaRefs[] = $replica_ref; return $this; } public function getReplicaRefs() { return $this->replicaRefs; } public function getRefKey() { $host = $this->getHost(); $port = $this->getPort(); if (strlen($port)) { return "{$host}:{$port}"; } return $host; } 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'), ), self::STATUS_AUTH => array( 'icon' => 'fa-key', 'color' => 'red', 'label' => pht('Invalid Credentials'), ), self::STATUS_REPLICATION_CLIENT => array( 'icon' => 'fa-eye-slash', 'color' => 'yellow', 'label' => pht('Missing Permission'), ), ); } public static function getReplicaStatusMap() { return array( self::REPLICATION_OKAY => array( 'icon' => 'fa-download', 'color' => 'green', 'label' => pht('Okay'), ), self::REPLICATION_MASTER_REPLICA => array( 'icon' => 'fa-database', 'color' => 'red', 'label' => pht('Replicating Master'), ), self::REPLICATION_REPLICA_NONE => array( 'icon' => 'fa-download', 'color' => 'red', 'label' => pht('Not A Replica'), ), self::REPLICATION_SLOW => array( 'icon' => 'fa-hourglass', 'color' => 'red', 'label' => pht('Slow Replication'), ), self::REPLICATION_NOT_REPLICATING => array( 'icon' => 'fa-exclamation-triangle', 'color' => 'red', 'label' => pht('Not Replicating'), ), ); } public static function getClusterRefs() { $cache = PhabricatorCaches::getRequestCache(); $refs = $cache->getKey(self::KEY_REFS); if (!$refs) { $refs = self::newRefs(); $cache->setKey(self::KEY_REFS, $refs); } return $refs; } public static function getLiveIndividualRef() { $cache = PhabricatorCaches::getRequestCache(); $ref = $cache->getKey(self::KEY_INDIVIDUAL); if (!$ref) { $ref = self::newIndividualRef(); $cache->setKey(self::KEY_INDIVIDUAL, $ref); } return $ref; } public static function newRefs() { $default_port = PhabricatorEnv::getEnvConfig('mysql.port'); $default_port = nonempty($default_port, 3306); $default_user = PhabricatorEnv::getEnvConfig('mysql.user'); $default_pass = PhabricatorEnv::getEnvConfig('mysql.pass'); $default_pass = new PhutilOpaqueEnvelope($default_pass); $config = PhabricatorEnv::getEnvConfig('cluster.databases'); return id(new PhabricatorDatabaseRefParser()) ->setDefaultPort($default_port) ->setDefaultUser($default_user) ->setDefaultPass($default_pass) ->newRefs($config); } public static function queryAll() { $refs = self::getActiveDatabaseRefs(); return self::queryRefs($refs); } private static function queryRefs(array $refs) { foreach ($refs as $ref) { $conn = $ref->newManagementConnection(); $t_start = microtime(true); $replica_status = false; try { $replica_status = queryfx_one($conn, 'SHOW SLAVE STATUS'); $ref->setConnectionStatus(self::STATUS_OKAY); } catch (AphrontAccessDeniedQueryException $ex) { $ref->setConnectionStatus(self::STATUS_REPLICATION_CLIENT); $ref->setConnectionMessage( pht( 'No permission to run "SHOW SLAVE STATUS". Grant this user '. '"REPLICATION CLIENT" permission to allow Phabricator to '. 'monitor replica health.')); } catch (AphrontInvalidCredentialsQueryException $ex) { $ref->setConnectionStatus(self::STATUS_AUTH); $ref->setConnectionMessage($ex->getMessage()); } catch (AphrontQueryException $ex) { $ref->setConnectionStatus(self::STATUS_FAIL); $class = get_class($ex); $message = $ex->getMessage(); $ref->setConnectionMessage( pht( '%s: %s', get_class($ex), $ex->getMessage())); } $t_end = microtime(true); $ref->setConnectionLatency($t_end - $t_start); if ($replica_status !== false) { $is_replica = (bool)$replica_status; if ($ref->getIsMaster() && $is_replica) { $ref->setReplicaStatus(self::REPLICATION_MASTER_REPLICA); $ref->setReplicaMessage( pht( 'This host has a "master" role, but is replicating data from '. 'another host ("%s")!', idx($replica_status, 'Master_Host'))); } else if (!$ref->getIsMaster() && !$is_replica) { $ref->setReplicaStatus(self::REPLICATION_REPLICA_NONE); $ref->setReplicaMessage( pht( 'This host has a "replica" role, but is not replicating data '. 'from a master (no output from "SHOW SLAVE STATUS").')); } else { $ref->setReplicaStatus(self::REPLICATION_OKAY); } if ($is_replica) { $latency = idx($replica_status, 'Seconds_Behind_Master'); if (!strlen($latency)) { $ref->setReplicaStatus(self::REPLICATION_NOT_REPLICATING); } else { $latency = (int)$latency; $ref->setReplicaDelay($latency); if ($latency > 30) { $ref->setReplicaStatus(self::REPLICATION_SLOW); $ref->setReplicaMessage( pht( 'This replica is lagging far behind the master. Data is at '. 'risk!')); } } } } } return $refs; } public function newManagementConnection() { return $this->newConnection( array( 'retries' => 0, 'timeout' => 2, )); } public function newApplicationConnection($database) { return $this->newConnection( array( 'database' => $database, )); } public function isSevered() { // If we only have an individual database, never sever our connection to // it, at least for now. It's possible that using the same severing rules // might eventually make sense to help alleviate load-related failures, // but we should wait for all the cluster stuff to stabilize first. if ($this->getIsIndividual()) { return false; } if ($this->didFailToConnect) { return true; } $record = $this->getHealthRecord(); $is_healthy = $record->getIsHealthy(); if (!$is_healthy) { return true; } return false; } public function isReachable(AphrontDatabaseConnection $connection) { $record = $this->getHealthRecord(); $should_check = $record->getShouldCheck(); if ($this->isSevered() && !$should_check) { return false; } $this->connectionException = null; try { $connection->openConnection(); $reachable = true; } catch (AphrontSchemaQueryException $ex) { // We get one of these if the database we're trying to select does not // exist. In this case, just re-throw the exception. This is expected // during first-time setup, when databases like "config" will not exist // yet. throw $ex; } catch (Exception $ex) { $this->connectionException = $ex; $reachable = false; } if ($should_check) { $record->didHealthCheck($reachable); } if (!$reachable) { $this->didFailToConnect = true; } return $reachable; } public function checkHealth() { $health = $this->getHealthRecord(); $should_check = $health->getShouldCheck(); if ($should_check) { // This does an implicit health update. $connection = $this->newManagementConnection(); $this->isReachable($connection); } 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 PhabricatorClusterServiceHealthRecord( $this->getHealthRecordCacheKey()); } return $this->healthRecord; } public function getConnectionException() { return $this->connectionException; } public static function getActiveDatabaseRefs() { $refs = array(); foreach (self::getMasterDatabaseRefs() as $ref) { $refs[] = $ref; } foreach (self::getReplicaDatabaseRefs() as $ref) { $refs[] = $ref; } return $refs; } public static function getAllMasterDatabaseRefs() { $refs = self::getClusterRefs(); if (!$refs) { return array(self::getLiveIndividualRef()); } $masters = array(); foreach ($refs as $ref) { if ($ref->getIsMaster()) { $masters[] = $ref; } } return $masters; } public static function getMasterDatabaseRefs() { $refs = self::getAllMasterDatabaseRefs(); return self::getEnabledRefs($refs); } public function isApplicationHost($database) { return isset($this->applicationMap[$database]); } public function loadRawMySQLConfigValue($key) { $conn = $this->newManagementConnection(); try { - $value = queryfx_one($conn, 'SELECT @@%Q', $key); - $value = $value['@@'.$key]; + $value = queryfx_one($conn, 'SELECT @@%C', $key); + + // NOTE: Although MySQL allows us to escape configuration values as if + // they are column names, the escaping is included in the column name + // of the return value: if we select "@@`x`", we get back a column named + // "@@`x`", not "@@x" as we might expect. + $value = head($value); + } catch (AphrontQueryException $ex) { $value = null; } return $value; } public static function getMasterDatabaseRefForApplication($application) { $masters = self::getMasterDatabaseRefs(); $application_master = null; $default_master = null; foreach ($masters as $master) { if ($master->isApplicationHost($application)) { $application_master = $master; break; } if ($master->getIsDefaultPartition()) { $default_master = $master; } } if ($application_master) { $masters = array($application_master); } else if ($default_master) { $masters = array($default_master); } else { $masters = array(); } $masters = self::getEnabledRefs($masters); $master = head($masters); return $master; } public static function newIndividualRef() { $default_user = PhabricatorEnv::getEnvConfig('mysql.user'); $default_pass = new PhutilOpaqueEnvelope( PhabricatorEnv::getEnvConfig('mysql.pass')); $default_host = PhabricatorEnv::getEnvConfig('mysql.host'); $default_port = PhabricatorEnv::getEnvConfig('mysql.port'); return id(new self()) ->setUser($default_user) ->setPass($default_pass) ->setHost($default_host) ->setPort($default_port) ->setIsIndividual(true) ->setIsMaster(true) ->setIsDefaultPartition(true) ->setUsePersistentConnections(false); } public static function getAllReplicaDatabaseRefs() { $refs = self::getClusterRefs(); if (!$refs) { return array(); } $replicas = array(); foreach ($refs as $ref) { if ($ref->getIsMaster()) { continue; } $replicas[] = $ref; } return $replicas; } public static function getReplicaDatabaseRefs() { $refs = self::getAllReplicaDatabaseRefs(); return self::getEnabledRefs($refs); } private static function getEnabledRefs(array $refs) { foreach ($refs as $key => $ref) { if ($ref->getDisabled()) { unset($refs[$key]); } } return $refs; } public static function getReplicaDatabaseRefForApplication($application) { $replicas = self::getReplicaDatabaseRefs(); $application_replicas = array(); $default_replicas = array(); foreach ($replicas as $replica) { $master = $replica->getMasterRef(); if ($master->isApplicationHost($application)) { $application_replicas[] = $replica; } if ($master->getIsDefaultPartition()) { $default_replicas[] = $replica; } } if ($application_replicas) { $replicas = $application_replicas; } else { $replicas = $default_replicas; } $replicas = self::getEnabledRefs($replicas); // TODO: We may have multiple replicas to choose from, and could make // more of an effort to pick the "best" one here instead of always // picking the first one. Once we've picked one, we should try to use // the same replica for the rest of the request, though. return head($replicas); } private function newConnection(array $options) { // If we believe the database is unhealthy, don't spend as much time // trying to connect to it, since it's likely to continue to fail and // hammering it can only make the problem worse. $record = $this->getHealthRecord(); if ($record->getIsHealthy()) { $default_retries = 3; $default_timeout = 10; } else { $default_retries = 0; $default_timeout = 2; } $spec = $options + array( 'user' => $this->getUser(), 'pass' => $this->getPass(), 'host' => $this->getHost(), 'port' => $this->getPort(), 'database' => null, 'retries' => $default_retries, 'timeout' => $default_timeout, 'persistent' => $this->getUsePersistentConnections(), ); $is_cli = (php_sapi_name() == 'cli'); $use_persistent = false; if (!empty($spec['persistent']) && !$is_cli) { $use_persistent = true; } unset($spec['persistent']); $connection = self::newRawConnection($spec); // If configured, use persistent connections. See T11672 for details. if ($use_persistent) { $connection->setPersistent($use_persistent); } // Unless this is a script running from the CLI, prevent any query from // running for more than 30 seconds. See T10849 for details. if (!$is_cli) { $connection->setQueryTimeout(30); } return $connection; } public static function newRawConnection(array $options) { if (extension_loaded('mysqli')) { return new AphrontMySQLiDatabaseConnection($options); } else { return new AphrontMySQLDatabaseConnection($options); } } } diff --git a/src/infrastructure/customfield/editor/PhabricatorCustomFieldEditField.php b/src/infrastructure/customfield/editor/PhabricatorCustomFieldEditField.php index 27b8276c85..1a63e040e2 100644 --- a/src/infrastructure/customfield/editor/PhabricatorCustomFieldEditField.php +++ b/src/infrastructure/customfield/editor/PhabricatorCustomFieldEditField.php @@ -1,185 +1,185 @@ customField = $custom_field; return $this; } public function getCustomField() { return $this->customField; } public function setCustomFieldHTTPParameterType( AphrontHTTPParameterType $type) { $this->httpParameterType = $type; return $this; } public function getCustomFieldHTTPParameterType() { return $this->httpParameterType; } public function setCustomFieldConduitParameterType( ConduitParameterType $type) { $this->conduitParameterType = $type; return $this; } public function getCustomFieldConduitParameterType() { return $this->conduitParameterType; } public function setCustomFieldBulkParameterType( BulkParameterType $type) { $this->bulkParameterType = $type; return $this; } public function getCustomFieldBulkParameterType() { return $this->bulkParameterType; } public function setCustomFieldCommentAction( PhabricatorEditEngineCommentAction $comment_action) { $this->commentAction = $comment_action; return $this; } public function getCustomFieldCommentAction() { return $this->commentAction; } protected function buildControl() { - if ($this->getIsConduitOnly()) { + if (!$this->getIsFormField()) { return null; } $field = $this->getCustomField(); $clone = clone $field; $value = $this->getValue(); $clone->setValueFromApplicationTransactions($value); return $clone->renderEditControl(array()); } protected function newEditType() { return id(new PhabricatorCustomFieldEditType()) ->setCustomField($this->getCustomField()); } public function getValueForTransaction() { $value = $this->getValue(); $field = $this->getCustomField(); // Avoid changing the value of the field itself, since later calls would // incorrectly reflect the new value. $clone = clone $field; $clone->setValueFromApplicationTransactions($value); return $clone->getNewValueForApplicationTransactions(); } protected function getValueForCommentAction($value) { $field = $this->getCustomField(); $clone = clone $field; $clone->setValueFromApplicationTransactions($value); // TODO: This is somewhat bogus because only StandardCustomFields // implement a getFieldValue() method -- not all CustomFields. Today, // only StandardCustomFields can ever actually generate a comment action // so we never reach this method with other field types. return $clone->getFieldValue(); } protected function getValueExistsInSubmit(AphrontRequest $request, $key) { return true; } protected function getValueFromSubmit(AphrontRequest $request, $key) { $field = $this->getCustomField(); $clone = clone $field; $clone->readValueFromRequest($request); return $clone->getNewValueForApplicationTransactions(); } protected function newConduitEditTypes() { $field = $this->getCustomField(); if (!$field->shouldAppearInConduitTransactions()) { return array(); } return parent::newConduitEditTypes(); } protected function newHTTPParameterType() { $type = $this->getCustomFieldHTTPParameterType(); if ($type) { return clone $type; } return null; } protected function newCommentAction() { $action = $this->getCustomFieldCommentAction(); if ($action) { return clone $action; } return null; } protected function newConduitParameterType() { $type = $this->getCustomFieldConduitParameterType(); if ($type) { return clone $type; } return null; } protected function newBulkParameterType() { $type = $this->getCustomFieldBulkParameterType(); if ($type) { return clone $type; } return null; } public function getAllReadValueFromRequestKeys() { $keys = array(); // NOTE: This piece of complexity is so we can expose a reasonable key in // the UI ("custom.x") instead of a crufty internal key ("std:app:x"). // Perhaps we can simplify this some day. // In the parent, this is just getKey(), but that returns a cumbersome // key in EditFields. Use the simpler edit type key instead. $keys[] = $this->getEditTypeKey(); foreach ($this->getAliases() as $alias) { $keys[] = $alias; } return $keys; } } diff --git a/src/infrastructure/customfield/field/PhabricatorCustomField.php b/src/infrastructure/customfield/field/PhabricatorCustomField.php index 36db8f239b..d7df3c5b78 100644 --- a/src/infrastructure/customfield/field/PhabricatorCustomField.php +++ b/src/infrastructure/customfield/field/PhabricatorCustomField.php @@ -1,1625 +1,1625 @@ getCustomFields(); } catch (PhabricatorDataNotAttachedException $ex) { $attachment = new PhabricatorCustomFieldAttachment(); $object->attachCustomFields($attachment); } try { $field_list = $attachment->getCustomFieldList($role); } catch (PhabricatorCustomFieldNotAttachedException $ex) { $base_class = $object->getCustomFieldBaseClass(); $spec = $object->getCustomFieldSpecificationForRole($role); if (!is_array($spec)) { throw new Exception( pht( "Expected an array from %s for object of class '%s'.", 'getCustomFieldSpecificationForRole()', get_class($object))); } $fields = self::buildFieldList( $base_class, $spec, $object); foreach ($fields as $key => $field) { if (!$field->shouldEnableForRole($role)) { unset($fields[$key]); } } foreach ($fields as $field) { $field->setObject($object); } $field_list = new PhabricatorCustomFieldList($fields); $attachment->addCustomFieldList($role, $field_list); } return $field_list; } /** * @task apps */ public static function getObjectField( PhabricatorCustomFieldInterface $object, $role, $field_key) { $fields = self::getObjectFields($object, $role)->getFields(); return idx($fields, $field_key); } /** * @task apps */ public static function buildFieldList( $base_class, array $spec, $object, array $options = array()) { $field_objects = id(new PhutilClassMapQuery()) ->setAncestorClass($base_class) ->execute(); $fields = array(); foreach ($field_objects as $field_object) { $field_object = clone $field_object; foreach ($field_object->createFields($object) as $field) { $key = $field->getFieldKey(); if (isset($fields[$key])) { throw new Exception( pht( "Both '%s' and '%s' define a custom field with ". "field key '%s'. Field keys must be unique.", get_class($fields[$key]), get_class($field), $key)); } $fields[$key] = $field; } } foreach ($fields as $key => $field) { if (!$field->isFieldEnabled()) { unset($fields[$key]); } } $fields = array_select_keys($fields, array_keys($spec)) + $fields; if (empty($options['withDisabled'])) { foreach ($fields as $key => $field) { if (isset($spec[$key]['disabled'])) { $is_disabled = $spec[$key]['disabled']; } else { $is_disabled = $field->shouldDisableByDefault(); } if ($is_disabled) { if ($field->canDisableField()) { unset($fields[$key]); } } } } return $fields; } /* -( Core Properties and Field Identity )--------------------------------- */ /** * Return a key which uniquely identifies this field, like * "mycompany:dinosaur:count". Normally you should provide some level of * namespacing to prevent collisions. * * @return string String which uniquely identifies this field. * @task core */ public function getFieldKey() { if ($this->proxy) { return $this->proxy->getFieldKey(); } throw new PhabricatorCustomFieldImplementationIncompleteException( $this, $field_key_is_incomplete = true); } public function getModernFieldKey() { if ($this->proxy) { return $this->proxy->getModernFieldKey(); } return $this->getFieldKey(); } /** * Return a human-readable field name. * * @return string Human readable field name. * @task core */ public function getFieldName() { if ($this->proxy) { return $this->proxy->getFieldName(); } return $this->getModernFieldKey(); } /** * Return a short, human-readable description of the field's behavior. This * provides more context to administrators when they are customizing fields. * * @return string|null Optional human-readable description. * @task core */ public function getFieldDescription() { if ($this->proxy) { return $this->proxy->getFieldDescription(); } return null; } /** * Most field implementations are unique, in that one class corresponds to * one field. However, some field implementations are general and a single * implementation may drive several fields. * * For general implementations, the general field implementation can return * multiple field instances here. * * @param object The object to create fields for. * @return list List of fields. * @task core */ public function createFields($object) { return array($this); } /** * You can return `false` here if the field should not be enabled for any * role. For example, it might depend on something (like an application or * library) which isn't installed, or might have some global configuration * which allows it to be disabled. * * @return bool False to completely disable this field for all roles. * @task core */ public function isFieldEnabled() { if ($this->proxy) { return $this->proxy->isFieldEnabled(); } return true; } /** * Low level selector for field availability. Fields can appear in different * roles (like an edit view, a list view, etc.), but not every field needs * to appear everywhere. Fields that are disabled in a role won't appear in * that context within applications. * * Normally, you do not need to override this method. Instead, override the * methods specific to roles you want to enable. For example, implement * @{method:shouldUseStorage()} to activate the `'storage'` role. * * @return bool True to enable the field for the given role. * @task core */ public function shouldEnableForRole($role) { // NOTE: All of these calls proxy individually, so we don't need to // proxy this call as a whole. switch ($role) { case self::ROLE_APPLICATIONTRANSACTIONS: return $this->shouldAppearInApplicationTransactions(); case self::ROLE_APPLICATIONSEARCH: return $this->shouldAppearInApplicationSearch(); case self::ROLE_STORAGE: return $this->shouldUseStorage(); case self::ROLE_EDIT: return $this->shouldAppearInEditView(); case self::ROLE_VIEW: return $this->shouldAppearInPropertyView(); case self::ROLE_LIST: return $this->shouldAppearInListView(); case self::ROLE_GLOBALSEARCH: return $this->shouldAppearInGlobalSearch(); case self::ROLE_CONDUIT: return $this->shouldAppearInConduitDictionary(); case self::ROLE_TRANSACTIONMAIL: return $this->shouldAppearInTransactionMail(); case self::ROLE_HERALD: return $this->shouldAppearInHerald(); case self::ROLE_HERALDACTION: return $this->shouldAppearInHeraldActions(); case self::ROLE_EDITENGINE: return $this->shouldAppearInEditView() || $this->shouldAppearInEditEngine(); case self::ROLE_EXPORT: return $this->shouldAppearInDataExport(); case self::ROLE_DEFAULT: return true; default: throw new Exception(pht("Unknown field role '%s'!", $role)); } } /** * Allow administrators to disable this field. Most fields should allow this, * but some are fundamental to the behavior of the application and can be * locked down to avoid chaos, disorder, and the decline of civilization. * * @return bool False to prevent this field from being disabled through * configuration. * @task core */ public function canDisableField() { return true; } public function shouldDisableByDefault() { return false; } /** * Return an index string which uniquely identifies this field. * * @return string Index string which uniquely identifies this field. * @task core */ final public function getFieldIndex() { return PhabricatorHash::digestForIndex($this->getFieldKey()); } /* -( Field Proxies )------------------------------------------------------ */ /** * Proxies allow a field to use some other field's implementation for most * of their behavior while still subclassing an application field. When a * proxy is set for a field with @{method:setProxy}, all of its methods will * call through to the proxy by default. * * This is most commonly used to implement configuration-driven custom fields * using @{class:PhabricatorStandardCustomField}. * * This method must be overridden to return `true` before a field can accept * proxies. * * @return bool True if you can @{method:setProxy} this field. * @task proxy */ public function canSetProxy() { if ($this instanceof PhabricatorStandardCustomFieldInterface) { return true; } return false; } /** * Set the proxy implementation for this field. See @{method:canSetProxy} for * discussion of field proxies. * * @param PhabricatorCustomField Field implementation. * @return this */ final public function setProxy(PhabricatorCustomField $proxy) { if (!$this->canSetProxy()) { throw new PhabricatorCustomFieldNotProxyException($this); } $this->proxy = $proxy; return $this; } /** * Get the field's proxy implementation, if any. For discussion, see * @{method:canSetProxy}. * * @return PhabricatorCustomField|null Proxy field, if one is set. */ final public function getProxy() { return $this->proxy; } /* -( Contextual Data )---------------------------------------------------- */ /** * Sets the object this field belongs to. * * @param PhabricatorCustomFieldInterface The object this field belongs to. * @return this * @task context */ final public function setObject(PhabricatorCustomFieldInterface $object) { if ($this->proxy) { $this->proxy->setObject($object); return $this; } $this->object = $object; $this->didSetObject($object); return $this; } /** * Read object data into local field storage, if applicable. * * @param PhabricatorCustomFieldInterface The object this field belongs to. * @return this * @task context */ public function readValueFromObject(PhabricatorCustomFieldInterface $object) { if ($this->proxy) { $this->proxy->readValueFromObject($object); } return $this; } /** * Get the object this field belongs to. * * @return PhabricatorCustomFieldInterface The object this field belongs to. * @task context */ final public function getObject() { if ($this->proxy) { return $this->proxy->getObject(); } return $this->object; } /** * This is a hook, primarily for subclasses to load object data. * * @return PhabricatorCustomFieldInterface The object this field belongs to. * @return void */ protected function didSetObject(PhabricatorCustomFieldInterface $object) { return; } /** * @task context */ final public function setViewer(PhabricatorUser $viewer) { if ($this->proxy) { $this->proxy->setViewer($viewer); return $this; } $this->viewer = $viewer; return $this; } /** * @task context */ final public function getViewer() { if ($this->proxy) { return $this->proxy->getViewer(); } return $this->viewer; } /** * @task context */ final protected function requireViewer() { if ($this->proxy) { return $this->proxy->requireViewer(); } if (!$this->viewer) { throw new PhabricatorCustomFieldDataNotAvailableException($this); } return $this->viewer; } /* -( Rendering Utilities )------------------------------------------------ */ /** * @task render */ protected function renderHandleList(array $handles) { if (!$handles) { return null; } $out = array(); foreach ($handles as $handle) { $out[] = $handle->renderHovercardLink(); } return phutil_implode_html(phutil_tag('br'), $out); } /* -( Storage )------------------------------------------------------------ */ /** * Return true to use field storage. * * Fields which can be edited by the user will most commonly use storage, * while some other types of fields (for instance, those which just display * information in some stylized way) may not. Many builtin fields do not use * storage because their data is available on the object itself. * * If you implement this, you must also implement @{method:getValueForStorage} * and @{method:setValueFromStorage}. * * @return bool True to use storage. * @task storage */ public function shouldUseStorage() { if ($this->proxy) { return $this->proxy->shouldUseStorage(); } return false; } /** * Return a new, empty storage object. This should be a subclass of * @{class:PhabricatorCustomFieldStorage} which is bound to the application's * database. * * @return PhabricatorCustomFieldStorage New empty storage object. * @task storage */ public function newStorageObject() { // NOTE: This intentionally isn't proxied, to avoid call cycles. throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * Return a serialized representation of the field value, appropriate for * storing in auxiliary field storage. You must implement this method if * you implement @{method:shouldUseStorage}. * * If the field value is a scalar, it can be returned unmodiifed. If not, * it should be serialized (for example, using JSON). * * @return string Serialized field value. * @task storage */ public function getValueForStorage() { if ($this->proxy) { return $this->proxy->getValueForStorage(); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * Set the field's value given a serialized storage value. This is called * when the field is loaded; if no data is available, the value will be * null. You must implement this method if you implement * @{method:shouldUseStorage}. * * Usually, the value can be loaded directly. If it isn't a scalar, you'll * need to undo whatever serialization you applied in * @{method:getValueForStorage}. * * @param string|null Serialized field representation (from * @{method:getValueForStorage}) or null if no value has * ever been stored. * @return this * @task storage */ public function setValueFromStorage($value) { if ($this->proxy) { return $this->proxy->setValueFromStorage($value); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } public function didSetValueFromStorage() { if ($this->proxy) { return $this->proxy->didSetValueFromStorage(); } return $this; } /* -( ApplicationSearch )-------------------------------------------------- */ /** * Appearing in ApplicationSearch allows a field to be indexed and searched * for. * * @return bool True to appear in ApplicationSearch. * @task appsearch */ public function shouldAppearInApplicationSearch() { if ($this->proxy) { return $this->proxy->shouldAppearInApplicationSearch(); } return false; } /** * Return one or more indexes which this field can meaningfully query against * to implement ApplicationSearch. * * Normally, you should build these using @{method:newStringIndex} and * @{method:newNumericIndex}. For example, if a field holds a numeric value * it might return a single numeric index: * * return array($this->newNumericIndex($this->getValue())); * * If a field holds a more complex value (like a list of users), it might * return several string indexes: * * $indexes = array(); * foreach ($this->getValue() as $phid) { * $indexes[] = $this->newStringIndex($phid); * } * return $indexes; * * @return list List of indexes. * @task appsearch */ public function buildFieldIndexes() { if ($this->proxy) { return $this->proxy->buildFieldIndexes(); } return array(); } /** * Return an index against which this field can be meaningfully ordered * against to implement ApplicationSearch. * * This should be a single index, normally built using * @{method:newStringIndex} and @{method:newNumericIndex}. * * The value of the index is not used. * * Return null from this method if the field can not be ordered. * * @return PhabricatorCustomFieldIndexStorage A single index to order by. * @task appsearch */ public function buildOrderIndex() { if ($this->proxy) { return $this->proxy->buildOrderIndex(); } return null; } /** * Build a new empty storage object for storing string indexes. Normally, * this should be a concrete subclass of * @{class:PhabricatorCustomFieldStringIndexStorage}. * * @return PhabricatorCustomFieldStringIndexStorage Storage object. * @task appsearch */ protected function newStringIndexStorage() { // NOTE: This intentionally isn't proxied, to avoid call cycles. throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * Build a new empty storage object for storing string indexes. Normally, * this should be a concrete subclass of * @{class:PhabricatorCustomFieldStringIndexStorage}. * * @return PhabricatorCustomFieldStringIndexStorage Storage object. * @task appsearch */ protected function newNumericIndexStorage() { // NOTE: This intentionally isn't proxied, to avoid call cycles. throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * Build and populate storage for a string index. * * @param string String to index. * @return PhabricatorCustomFieldStringIndexStorage Populated storage. * @task appsearch */ protected function newStringIndex($value) { if ($this->proxy) { return $this->proxy->newStringIndex(); } $key = $this->getFieldIndex(); return $this->newStringIndexStorage() ->setIndexKey($key) ->setIndexValue($value); } /** * Build and populate storage for a numeric index. * * @param string Numeric value to index. * @return PhabricatorCustomFieldNumericIndexStorage Populated storage. * @task appsearch */ protected function newNumericIndex($value) { if ($this->proxy) { return $this->proxy->newNumericIndex(); } $key = $this->getFieldIndex(); return $this->newNumericIndexStorage() ->setIndexKey($key) ->setIndexValue($value); } /** * Read a query value from a request, for storage in a saved query. Normally, * this method should, e.g., read a string out of the request. * * @param PhabricatorApplicationSearchEngine Engine building the query. * @param AphrontRequest Request to read from. * @return wild * @task appsearch */ public function readApplicationSearchValueFromRequest( PhabricatorApplicationSearchEngine $engine, AphrontRequest $request) { if ($this->proxy) { return $this->proxy->readApplicationSearchValueFromRequest( $engine, $request); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * Constrain a query, given a field value. Generally, this method should * use `with...()` methods to apply filters or other constraints to the * query. * * @param PhabricatorApplicationSearchEngine Engine executing the query. * @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain. * @param wild Constraint provided by the user. * @return void * @task appsearch */ public function applyApplicationSearchConstraintToQuery( PhabricatorApplicationSearchEngine $engine, PhabricatorCursorPagedPolicyAwareQuery $query, $value) { if ($this->proxy) { return $this->proxy->applyApplicationSearchConstraintToQuery( $engine, $query, $value); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * Append search controls to the interface. * * @param PhabricatorApplicationSearchEngine Engine constructing the form. * @param AphrontFormView The form to update. * @param wild Value from the saved query. * @return void * @task appsearch */ public function appendToApplicationSearchForm( PhabricatorApplicationSearchEngine $engine, AphrontFormView $form, $value) { if ($this->proxy) { return $this->proxy->appendToApplicationSearchForm( $engine, $form, $value); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /* -( ApplicationTransactions )-------------------------------------------- */ /** * Appearing in ApplicationTrasactions allows a field to be edited using * standard workflows. * * @return bool True to appear in ApplicationTransactions. * @task appxaction */ public function shouldAppearInApplicationTransactions() { if ($this->proxy) { return $this->proxy->shouldAppearInApplicationTransactions(); } return false; } /** * @task appxaction */ public function getApplicationTransactionType() { if ($this->proxy) { return $this->proxy->getApplicationTransactionType(); } return PhabricatorTransactions::TYPE_CUSTOMFIELD; } /** * @task appxaction */ public function getApplicationTransactionMetadata() { if ($this->proxy) { return $this->proxy->getApplicationTransactionMetadata(); } return array(); } /** * @task appxaction */ public function getOldValueForApplicationTransactions() { if ($this->proxy) { return $this->proxy->getOldValueForApplicationTransactions(); } return $this->getValueForStorage(); } /** * @task appxaction */ public function getNewValueForApplicationTransactions() { if ($this->proxy) { return $this->proxy->getNewValueForApplicationTransactions(); } return $this->getValueForStorage(); } /** * @task appxaction */ public function setValueFromApplicationTransactions($value) { if ($this->proxy) { return $this->proxy->setValueFromApplicationTransactions($value); } return $this->setValueFromStorage($value); } /** * @task appxaction */ public function getNewValueFromApplicationTransactions( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->getNewValueFromApplicationTransactions($xaction); } return $xaction->getNewValue(); } /** * @task appxaction */ public function getApplicationTransactionHasEffect( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->getApplicationTransactionHasEffect($xaction); } return ($xaction->getOldValue() !== $xaction->getNewValue()); } /** * @task appxaction */ public function applyApplicationTransactionInternalEffects( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->applyApplicationTransactionInternalEffects($xaction); } return; } /** * @task appxaction */ public function getApplicationTransactionRemarkupBlocks( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->getApplicationTransactionRemarkupBlocks($xaction); } return array(); } /** * @task appxaction */ public function applyApplicationTransactionExternalEffects( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->applyApplicationTransactionExternalEffects($xaction); } if (!$this->shouldEnableForRole(self::ROLE_STORAGE)) { return; } $this->setValueFromApplicationTransactions($xaction->getNewValue()); $value = $this->getValueForStorage(); $table = $this->newStorageObject(); $conn_w = $table->establishConnection('w'); if ($value === null) { queryfx( $conn_w, 'DELETE FROM %T WHERE objectPHID = %s AND fieldIndex = %s', $table->getTableName(), $this->getObject()->getPHID(), $this->getFieldIndex()); } else { queryfx( $conn_w, 'INSERT INTO %T (objectPHID, fieldIndex, fieldValue) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE fieldValue = VALUES(fieldValue)', $table->getTableName(), $this->getObject()->getPHID(), $this->getFieldIndex(), $value); } return; } /** * Validate transactions for an object. This allows you to raise an error * when a transaction would set a field to an invalid value, or when a field * is required but no transactions provide value. * * @param PhabricatorLiskDAO Editor applying the transactions. * @param string Transaction type. This type is always * `PhabricatorTransactions::TYPE_CUSTOMFIELD`, it is provided for * convenience when constructing exceptions. * @param list Transactions being applied, * which may be empty if this field is not being edited. * @return list Validation * errors. * * @task appxaction */ public function validateApplicationTransactions( PhabricatorApplicationTransactionEditor $editor, $type, array $xactions) { if ($this->proxy) { return $this->proxy->validateApplicationTransactions( $editor, $type, $xactions); } return array(); } public function getApplicationTransactionTitle( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->getApplicationTransactionTitle( $xaction); } $author_phid = $xaction->getAuthorPHID(); return pht( '%s updated this object.', $xaction->renderHandleLink($author_phid)); } public function getApplicationTransactionTitleForFeed( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->getApplicationTransactionTitleForFeed( $xaction); } $author_phid = $xaction->getAuthorPHID(); $object_phid = $xaction->getObjectPHID(); return pht( '%s updated %s.', $xaction->renderHandleLink($author_phid), $xaction->renderHandleLink($object_phid)); } public function getApplicationTransactionHasChangeDetails( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->getApplicationTransactionHasChangeDetails( $xaction); } return false; } public function getApplicationTransactionChangeDetails( PhabricatorApplicationTransaction $xaction, PhabricatorUser $viewer) { if ($this->proxy) { return $this->proxy->getApplicationTransactionChangeDetails( $xaction, $viewer); } return null; } public function getApplicationTransactionRequiredHandlePHIDs( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->getApplicationTransactionRequiredHandlePHIDs( $xaction); } return array(); } public function shouldHideInApplicationTransactions( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->shouldHideInApplicationTransactions($xaction); } return false; } /* -( Transaction Mail )--------------------------------------------------- */ /** * @task xactionmail */ public function shouldAppearInTransactionMail() { if ($this->proxy) { return $this->proxy->shouldAppearInTransactionMail(); } return false; } /** * @task xactionmail */ public function updateTransactionMailBody( PhabricatorMetaMTAMailBody $body, PhabricatorApplicationTransactionEditor $editor, array $xactions) { if ($this->proxy) { return $this->proxy->updateTransactionMailBody($body, $editor, $xactions); } return; } /* -( Edit View )---------------------------------------------------------- */ public function getEditEngineFields(PhabricatorEditEngine $engine) { $field = $this->newStandardEditField(); return array( $field, ); } protected function newEditField() { $field = id(new PhabricatorCustomFieldEditField()) ->setCustomField($this); $http_type = $this->getHTTPParameterType(); if ($http_type) { $field->setCustomFieldHTTPParameterType($http_type); } $conduit_type = $this->getConduitEditParameterType(); if ($conduit_type) { $field->setCustomFieldConduitParameterType($conduit_type); } $bulk_type = $this->getBulkParameterType(); if ($bulk_type) { $field->setCustomFieldBulkParameterType($bulk_type); } $comment_action = $this->getCommentAction(); if ($comment_action) { $field ->setCustomFieldCommentAction($comment_action) ->setCommentActionLabel( pht( 'Change %s', $this->getFieldName())); } return $field; } protected function newStandardEditField() { if ($this->proxy) { return $this->proxy->newStandardEditField(); } - if (!$this->shouldAppearInEditView()) { - $conduit_only = true; + if ($this->shouldAppearInEditView()) { + $form_field = true; } else { - $conduit_only = false; + $form_field = false; } $bulk_label = $this->getBulkEditLabel(); return $this->newEditField() ->setKey($this->getFieldKey()) ->setEditTypeKey($this->getModernFieldKey()) ->setLabel($this->getFieldName()) ->setBulkEditLabel($bulk_label) ->setDescription($this->getFieldDescription()) ->setTransactionType($this->getApplicationTransactionType()) - ->setIsConduitOnly($conduit_only) + ->setIsFormField($form_field) ->setValue($this->getNewValueForApplicationTransactions()); } protected function getBulkEditLabel() { if ($this->proxy) { return $this->proxy->getBulkEditLabel(); } return pht('Set "%s" to', $this->getFieldName()); } public function getBulkParameterType() { return $this->newBulkParameterType(); } protected function newBulkParameterType() { if ($this->proxy) { return $this->proxy->newBulkParameterType(); } return null; } protected function getHTTPParameterType() { if ($this->proxy) { return $this->proxy->getHTTPParameterType(); } return null; } /** * @task edit */ public function shouldAppearInEditView() { if ($this->proxy) { return $this->proxy->shouldAppearInEditView(); } return false; } /** * @task edit */ public function shouldAppearInEditEngine() { if ($this->proxy) { return $this->proxy->shouldAppearInEditEngine(); } return false; } /** * @task edit */ public function readValueFromRequest(AphrontRequest $request) { if ($this->proxy) { return $this->proxy->readValueFromRequest($request); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * @task edit */ public function getRequiredHandlePHIDsForEdit() { if ($this->proxy) { return $this->proxy->getRequiredHandlePHIDsForEdit(); } return array(); } /** * @task edit */ public function getInstructionsForEdit() { if ($this->proxy) { return $this->proxy->getInstructionsForEdit(); } return null; } /** * @task edit */ public function renderEditControl(array $handles) { if ($this->proxy) { return $this->proxy->renderEditControl($handles); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /* -( Property View )------------------------------------------------------ */ /** * @task view */ public function shouldAppearInPropertyView() { if ($this->proxy) { return $this->proxy->shouldAppearInPropertyView(); } return false; } /** * @task view */ public function renderPropertyViewLabel() { if ($this->proxy) { return $this->proxy->renderPropertyViewLabel(); } return $this->getFieldName(); } /** * @task view */ public function renderPropertyViewValue(array $handles) { if ($this->proxy) { return $this->proxy->renderPropertyViewValue($handles); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * @task view */ public function getStyleForPropertyView() { if ($this->proxy) { return $this->proxy->getStyleForPropertyView(); } return 'property'; } /** * @task view */ public function getIconForPropertyView() { if ($this->proxy) { return $this->proxy->getIconForPropertyView(); } return null; } /** * @task view */ public function getRequiredHandlePHIDsForPropertyView() { if ($this->proxy) { return $this->proxy->getRequiredHandlePHIDsForPropertyView(); } return array(); } /* -( List View )---------------------------------------------------------- */ /** * @task list */ public function shouldAppearInListView() { if ($this->proxy) { return $this->proxy->shouldAppearInListView(); } return false; } /** * @task list */ public function renderOnListItem(PHUIObjectItemView $view) { if ($this->proxy) { return $this->proxy->renderOnListItem($view); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /* -( Global Search )------------------------------------------------------ */ /** * @task globalsearch */ public function shouldAppearInGlobalSearch() { if ($this->proxy) { return $this->proxy->shouldAppearInGlobalSearch(); } return false; } /** * @task globalsearch */ public function updateAbstractDocument( PhabricatorSearchAbstractDocument $document) { if ($this->proxy) { return $this->proxy->updateAbstractDocument($document); } return $document; } /* -( Data Export )-------------------------------------------------------- */ public function shouldAppearInDataExport() { if ($this->proxy) { return $this->proxy->shouldAppearInDataExport(); } try { $this->newExportFieldType(); return true; } catch (PhabricatorCustomFieldImplementationIncompleteException $ex) { return false; } } public function newExportField() { if ($this->proxy) { return $this->proxy->newExportField(); } return $this->newExportFieldType() ->setLabel($this->getFieldName()); } public function newExportData() { if ($this->proxy) { return $this->proxy->newExportData(); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } protected function newExportFieldType() { if ($this->proxy) { return $this->proxy->newExportFieldType(); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /* -( Conduit )------------------------------------------------------------ */ /** * @task conduit */ public function shouldAppearInConduitDictionary() { if ($this->proxy) { return $this->proxy->shouldAppearInConduitDictionary(); } return false; } /** * @task conduit */ public function getConduitDictionaryValue() { if ($this->proxy) { return $this->proxy->getConduitDictionaryValue(); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } public function shouldAppearInConduitTransactions() { if ($this->proxy) { return $this->proxy->shouldAppearInConduitDictionary(); } return false; } public function getConduitSearchParameterType() { return $this->newConduitSearchParameterType(); } protected function newConduitSearchParameterType() { if ($this->proxy) { return $this->proxy->newConduitSearchParameterType(); } return null; } public function getConduitEditParameterType() { return $this->newConduitEditParameterType(); } protected function newConduitEditParameterType() { if ($this->proxy) { return $this->proxy->newConduitEditParameterType(); } return null; } public function getCommentAction() { return $this->newCommentAction(); } protected function newCommentAction() { if ($this->proxy) { return $this->proxy->newCommentAction(); } return null; } /* -( Herald )------------------------------------------------------------- */ /** * Return `true` to make this field available in Herald. * * @return bool True to expose the field in Herald. * @task herald */ public function shouldAppearInHerald() { if ($this->proxy) { return $this->proxy->shouldAppearInHerald(); } return false; } /** * Get the name of the field in Herald. By default, this uses the * normal field name. * * @return string Herald field name. * @task herald */ public function getHeraldFieldName() { if ($this->proxy) { return $this->proxy->getHeraldFieldName(); } return $this->getFieldName(); } /** * Get the field value for evaluation by Herald. * * @return wild Field value. * @task herald */ public function getHeraldFieldValue() { if ($this->proxy) { return $this->proxy->getHeraldFieldValue(); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * Get the available conditions for this field in Herald. * * @return list List of Herald condition constants. * @task herald */ public function getHeraldFieldConditions() { if ($this->proxy) { return $this->proxy->getHeraldFieldConditions(); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * Get the Herald value type for the given condition. * * @param const Herald condition constant. * @return const|null Herald value type, or null to use the default. * @task herald */ public function getHeraldFieldValueType($condition) { if ($this->proxy) { return $this->proxy->getHeraldFieldValueType($condition); } return null; } public function getHeraldFieldStandardType() { if ($this->proxy) { return $this->proxy->getHeraldFieldStandardType(); } return null; } public function getHeraldDatasource() { if ($this->proxy) { return $this->proxy->getHeraldDatasource(); } return null; } public function shouldAppearInHeraldActions() { if ($this->proxy) { return $this->proxy->shouldAppearInHeraldActions(); } return false; } public function getHeraldActionName() { if ($this->proxy) { return $this->proxy->getHeraldActionName(); } return null; } public function getHeraldActionStandardType() { if ($this->proxy) { return $this->proxy->getHeraldActionStandardType(); } return null; } public function getHeraldActionDescription($value) { if ($this->proxy) { return $this->proxy->getHeraldActionDescription($value); } return null; } public function getHeraldActionEffectDescription($value) { if ($this->proxy) { return $this->proxy->getHeraldActionEffectDescription($value); } return null; } public function getHeraldActionDatasource() { if ($this->proxy) { return $this->proxy->getHeraldActionDatasource(); } return null; } } diff --git a/src/infrastructure/customfield/field/PhabricatorCustomFieldList.php b/src/infrastructure/customfield/field/PhabricatorCustomFieldList.php index efe12e68b8..8ae5529f95 100644 --- a/src/infrastructure/customfield/field/PhabricatorCustomFieldList.php +++ b/src/infrastructure/customfield/field/PhabricatorCustomFieldList.php @@ -1,328 +1,328 @@ fields = $fields; } public function getFields() { return $this->fields; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; foreach ($this->getFields() as $field) { $field->setViewer($viewer); } return $this; } public function readFieldsFromObject( PhabricatorCustomFieldInterface $object) { $fields = $this->getFields(); foreach ($fields as $field) { $field ->setObject($object) ->readValueFromObject($object); } return $this; } /** * Read stored values for all fields which support storage. * * @param PhabricatorCustomFieldInterface Object to read field values for. * @return void */ public function readFieldsFromStorage( PhabricatorCustomFieldInterface $object) { $this->readFieldsFromObject($object); $fields = $this->getFields(); id(new PhabricatorCustomFieldStorageQuery()) ->addFields($fields) ->execute(); return $this; } public function appendFieldsToForm(AphrontFormView $form) { $enabled = array(); foreach ($this->fields as $field) { if ($field->shouldEnableForRole(PhabricatorCustomField::ROLE_EDIT)) { $enabled[] = $field; } } $phids = array(); foreach ($enabled as $field_key => $field) { $phids[$field_key] = $field->getRequiredHandlePHIDsForEdit(); } $all_phids = array_mergev($phids); if ($all_phids) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->viewer) ->withPHIDs($all_phids) ->execute(); } else { $handles = array(); } foreach ($enabled as $field_key => $field) { $field_handles = array_select_keys($handles, $phids[$field_key]); $instructions = $field->getInstructionsForEdit(); if (strlen($instructions)) { $form->appendRemarkupInstructions($instructions); } $form->appendChild($field->renderEditControl($field_handles)); } } public function appendFieldsToPropertyList( PhabricatorCustomFieldInterface $object, PhabricatorUser $viewer, PHUIPropertyListView $view) { $this->readFieldsFromStorage($object); $fields = $this->fields; foreach ($fields as $field) { $field->setViewer($viewer); } // Move all the blocks to the end, regardless of their configuration order, // because it always looks silly to render a block in the middle of a list // of properties. $head = array(); $tail = array(); foreach ($fields as $key => $field) { $style = $field->getStyleForPropertyView(); switch ($style) { case 'property': case 'header': $head[$key] = $field; break; case 'block': $tail[$key] = $field; break; default: throw new Exception( pht( "Unknown field property view style '%s'; valid styles are ". "'%s' and '%s'.", $style, 'block', 'property')); } } $fields = $head + $tail; $add_header = null; $phids = array(); foreach ($fields as $key => $field) { $phids[$key] = $field->getRequiredHandlePHIDsForPropertyView(); } $all_phids = array_mergev($phids); if ($all_phids) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs($all_phids) ->execute(); } else { $handles = array(); } foreach ($fields as $key => $field) { $field_handles = array_select_keys($handles, $phids[$key]); $label = $field->renderPropertyViewLabel(); $value = $field->renderPropertyViewValue($field_handles); if ($value !== null) { switch ($field->getStyleForPropertyView()) { case 'header': // We want to hide headers if the fields they're associated with // don't actually produce any visible properties. For example, in a // list like this: // // Header A // Prop A: Value A // Header B // Prop B: Value B // // ...if the "Prop A" field returns `null` when rendering its // property value and we rendered naively, we'd get this: // // Header A // Header B // Prop B: Value B // // This is silly. Instead, we hide "Header A". $add_header = $value; break; case 'property': if ($add_header !== null) { // Add the most recently seen header. $view->addSectionHeader($add_header); $add_header = null; } $view->addProperty($label, $value); break; case 'block': $icon = $field->getIconForPropertyView(); $view->invokeWillRenderEvent(); if ($label !== null) { $view->addSectionHeader($label, $icon); } $view->addTextContent($value); break; } } } } public function buildFieldTransactionsFromRequest( PhabricatorApplicationTransaction $template, AphrontRequest $request) { $xactions = array(); $role = PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS; foreach ($this->fields as $field) { if (!$field->shouldEnableForRole($role)) { continue; } $transaction_type = $field->getApplicationTransactionType(); $xaction = id(clone $template) ->setTransactionType($transaction_type); if ($transaction_type == PhabricatorTransactions::TYPE_CUSTOMFIELD) { // For TYPE_CUSTOMFIELD transactions only, we provide the old value // as an input. $old_value = $field->getOldValueForApplicationTransactions(); $xaction->setOldValue($old_value); } $field->readValueFromRequest($request); $xaction ->setNewValue($field->getNewValueForApplicationTransactions()); if ($transaction_type == PhabricatorTransactions::TYPE_CUSTOMFIELD) { // For TYPE_CUSTOMFIELD transactions, add the field key in metadata. $xaction->setMetadataValue('customfield:key', $field->getFieldKey()); } $metadata = $field->getApplicationTransactionMetadata(); foreach ($metadata as $key => $value) { $xaction->setMetadataValue($key, $value); } $xactions[] = $xaction; } return $xactions; } /** * Publish field indexes into index tables, so ApplicationSearch can search * them. * * @return void */ public function rebuildIndexes(PhabricatorCustomFieldInterface $object) { $indexes = array(); $index_keys = array(); $phid = $object->getPHID(); $role = PhabricatorCustomField::ROLE_APPLICATIONSEARCH; foreach ($this->fields as $field) { if (!$field->shouldEnableForRole($role)) { continue; } $index_keys[$field->getFieldIndex()] = true; foreach ($field->buildFieldIndexes() as $index) { $index->setObjectPHID($phid); $indexes[$index->getTableName()][] = $index; } } if (!$indexes) { return; } $any_index = head(head($indexes)); $conn_w = $any_index->establishConnection('w'); foreach ($indexes as $table => $index_list) { $sql = array(); foreach ($index_list as $index) { $sql[] = $index->formatForInsert($conn_w); } $indexes[$table] = $sql; } $any_index->openTransaction(); foreach ($indexes as $table => $sql_list) { queryfx( $conn_w, 'DELETE FROM %T WHERE objectPHID = %s AND indexKey IN (%Ls)', $table, $phid, array_keys($index_keys)); if (!$sql_list) { continue; } foreach (PhabricatorLiskDAO::chunkSQL($sql_list) as $chunk) { queryfx( $conn_w, - 'INSERT INTO %T (objectPHID, indexKey, indexValue) VALUES %Q', + 'INSERT INTO %T (objectPHID, indexKey, indexValue) VALUES %LQ', $table, $chunk); } } $any_index->saveTransaction(); } public function updateAbstractDocument( PhabricatorSearchAbstractDocument $document) { $role = PhabricatorCustomField::ROLE_GLOBALSEARCH; foreach ($this->getFields() as $field) { if (!$field->shouldEnableForRole($role)) { continue; } $field->updateAbstractDocument($document); } } } diff --git a/src/infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php b/src/infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php index bb70f03d88..0163143ae7 100644 --- a/src/infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php +++ b/src/infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php @@ -1,337 +1,345 @@ skipLease = $skip; return $this; } public function withIDs(array $ids) { $this->ids = $ids; return $this; } public function withObjectPHIDs(array $phids) { $this->objectPHIDs = $phids; return $this; } /** * Select only leased tasks, only unleased tasks, or both types of task. * * By default, queries select only unleased tasks (equivalent to passing * `false` to this method). You can pass `true` to select only leased tasks, * or `null` to ignore the lease status of tasks. * * If your result set potentially includes leased tasks, you must disable * leasing using @{method:setSkipLease}. These options are intended for use * when displaying task status information. * * @param mixed `true` to select only leased tasks, `false` to select only * unleased tasks (default), or `null` to select both. * @return this */ public function withLeasedTasks($leased) { $this->leased = $leased; return $this; } public function setLimit($limit) { $this->limit = $limit; return $this; } public function execute() { if (!$this->limit) { throw new Exception( pht('You must %s when leasing tasks.', 'setLimit()')); } if ($this->leased !== false) { if (!$this->skipLease) { throw new Exception( pht( 'If you potentially select leased tasks using %s, '. 'you MUST disable lease acquisition by calling %s.', 'withLeasedTasks()', 'setSkipLease()')); } } $task_table = new PhabricatorWorkerActiveTask(); $taskdata_table = new PhabricatorWorkerTaskData(); $lease_ownership_name = $this->getLeaseOwnershipName(); $conn_w = $task_table->establishConnection('w'); // Try to satisfy the request from new, unleased tasks first. If we don't // find enough tasks, try tasks with expired leases (i.e., tasks which have // previously failed). // If we're selecting leased tasks, look for them first. $phases = array(); if ($this->leased !== false) { $phases[] = self::PHASE_LEASED; } if ($this->leased !== true) { $phases[] = self::PHASE_UNLEASED; $phases[] = self::PHASE_EXPIRED; } $limit = $this->limit; $leased = 0; $task_ids = array(); foreach ($phases as $phase) { // NOTE: If we issue `UPDATE ... WHERE ... ORDER BY id ASC`, the query // goes very, very slowly. The `ORDER BY` triggers this, although we get // the same apparent results without it. Without the ORDER BY, binary // read slaves complain that the query isn't repeatable. To avoid both // problems, do a SELECT and then an UPDATE. $rows = queryfx_all( $conn_w, 'SELECT id, leaseOwner FROM %T %Q %Q %Q', $task_table->getTableName(), $this->buildCustomWhereClause($conn_w, $phase), $this->buildOrderClause($conn_w, $phase), $this->buildLimitClause($conn_w, $limit - $leased)); // NOTE: Sometimes, we'll race with another worker and they'll grab // this task before we do. We could reduce how often this happens by // selecting more tasks than we need, then shuffling them and trying // to lock only the number we're actually after. However, the amount // of time workers spend here should be very small relative to their // total runtime, so keep it simple for the moment. if ($rows) { if ($this->skipLease) { $leased += count($rows); $task_ids += array_fuse(ipull($rows, 'id')); } else { queryfx( $conn_w, 'UPDATE %T task SET leaseOwner = %s, leaseExpires = UNIX_TIMESTAMP() + %d %Q', $task_table->getTableName(), $lease_ownership_name, self::getDefaultLeaseDuration(), $this->buildUpdateWhereClause($conn_w, $phase, $rows)); $leased += $conn_w->getAffectedRows(); } if ($leased == $limit) { break; } } } if (!$leased) { return array(); } if ($this->skipLease) { $selection_condition = qsprintf( $conn_w, 'task.id IN (%Ld)', $task_ids); } else { $selection_condition = qsprintf( $conn_w, 'task.leaseOwner = %s AND leaseExpires > UNIX_TIMESTAMP()', $lease_ownership_name); } $data = queryfx_all( $conn_w, 'SELECT task.*, taskdata.data _taskData, UNIX_TIMESTAMP() _serverTime FROM %T task LEFT JOIN %T taskdata ON taskdata.id = task.dataID WHERE %Q %Q %Q', $task_table->getTableName(), $taskdata_table->getTableName(), $selection_condition, $this->buildOrderClause($conn_w, $phase), $this->buildLimitClause($conn_w, $limit)); $tasks = $task_table->loadAllFromArray($data); $tasks = mpull($tasks, null, 'getID'); foreach ($data as $row) { $tasks[$row['id']]->setServerTime($row['_serverTime']); if ($row['_taskData']) { $task_data = json_decode($row['_taskData'], true); } else { $task_data = null; } $tasks[$row['id']]->setData($task_data); } if ($this->skipLease) { // Reorder rows into the original phase order if this is a status query. $tasks = array_select_keys($tasks, $task_ids); } return $tasks; } protected function buildCustomWhereClause( - AphrontDatabaseConnection $conn_w, + AphrontDatabaseConnection $conn, $phase) { $where = array(); switch ($phase) { case self::PHASE_LEASED: - $where[] = 'leaseOwner IS NOT NULL'; - $where[] = 'leaseExpires >= UNIX_TIMESTAMP()'; + $where[] = qsprintf( + $conn, + 'leaseOwner IS NOT NULL'); + $where[] = qsprintf( + $conn, + 'leaseExpires >= UNIX_TIMESTAMP()'); break; case self::PHASE_UNLEASED: - $where[] = 'leaseOwner IS NULL'; + $where[] = qsprintf( + $conn, + 'leaseOwner IS NULL'); break; case self::PHASE_EXPIRED: - $where[] = 'leaseExpires < UNIX_TIMESTAMP()'; + $where[] = qsprintf( + $conn, + 'leaseExpires < UNIX_TIMESTAMP()'); break; default: throw new Exception(pht("Unknown phase '%s'!", $phase)); } if ($this->ids !== null) { - $where[] = qsprintf($conn_w, 'id IN (%Ld)', $this->ids); + $where[] = qsprintf($conn, 'id IN (%Ld)', $this->ids); } if ($this->objectPHIDs !== null) { - $where[] = qsprintf($conn_w, 'objectPHID IN (%Ls)', $this->objectPHIDs); + $where[] = qsprintf($conn, 'objectPHID IN (%Ls)', $this->objectPHIDs); } - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } private function buildUpdateWhereClause( - AphrontDatabaseConnection $conn_w, + AphrontDatabaseConnection $conn, $phase, array $rows) { $where = array(); // NOTE: This is basically working around the MySQL behavior that // `IN (NULL)` doesn't match NULL. switch ($phase) { case self::PHASE_LEASED: throw new Exception( pht( 'Trying to lease tasks selected in the leased phase! This is '. 'intended to be impossible.')); case self::PHASE_UNLEASED: - $where[] = qsprintf($conn_w, 'leaseOwner IS NULL'); - $where[] = qsprintf($conn_w, 'id IN (%Ld)', ipull($rows, 'id')); + $where[] = qsprintf($conn, 'leaseOwner IS NULL'); + $where[] = qsprintf($conn, 'id IN (%Ld)', ipull($rows, 'id')); break; case self::PHASE_EXPIRED: $in = array(); foreach ($rows as $row) { $in[] = qsprintf( - $conn_w, + $conn, '(id = %d AND leaseOwner = %s)', $row['id'], $row['leaseOwner']); } - $where[] = qsprintf($conn_w, '(%Q)', implode(' OR ', $in)); + $where[] = qsprintf($conn, '%LO', $in); break; default: throw new Exception(pht('Unknown phase "%s"!', $phase)); } - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } private function buildOrderClause(AphrontDatabaseConnection $conn_w, $phase) { switch ($phase) { case self::PHASE_LEASED: // Ideally we'd probably order these by lease acquisition time, but // we don't have that handy and this is a good approximation. return qsprintf($conn_w, 'ORDER BY priority ASC, id ASC'); case self::PHASE_UNLEASED: // When selecting new tasks, we want to consume them in order of // increasing priority (and then FIFO). return qsprintf($conn_w, 'ORDER BY priority ASC, id ASC'); case self::PHASE_EXPIRED: // When selecting failed tasks, we want to consume them in roughly // FIFO order of their failures, which is not necessarily their original // queue order. // Particularly, this is important for tasks which use soft failures to // indicate that they are waiting on other tasks to complete: we need to // push them to the end of the queue after they fail, at least on // average, so we don't deadlock retrying the same blocked task over // and over again. return qsprintf($conn_w, 'ORDER BY leaseExpires ASC'); default: throw new Exception(pht('Unknown phase "%s"!', $phase)); } } private function buildLimitClause(AphrontDatabaseConnection $conn_w, $limit) { return qsprintf($conn_w, 'LIMIT %d', $limit); } private function getLeaseOwnershipName() { static $sequence = 0; // TODO: If the host name is very long, this can overflow the 64-character // column, so we pick just the first part of the host name. It might be // useful to just use a random hash as the identifier instead and put the // pid / time / host (which are somewhat useful diagnostically) elsewhere. // Likely, we could store a daemon ID instead and use that to identify // when and where code executed. See T6742. $host = php_uname('n'); $host = id(new PhutilUTF8StringTruncator()) ->setMaximumBytes(32) ->setTerminator('...') ->truncateString($host); $parts = array( getmypid(), time(), $host, ++$sequence, ); return implode(':', $parts); } } diff --git a/src/infrastructure/daemon/workers/query/PhabricatorWorkerTaskQuery.php b/src/infrastructure/daemon/workers/query/PhabricatorWorkerTaskQuery.php index ae6e2cc442..3797de50f9 100644 --- a/src/infrastructure/daemon/workers/query/PhabricatorWorkerTaskQuery.php +++ b/src/infrastructure/daemon/workers/query/PhabricatorWorkerTaskQuery.php @@ -1,128 +1,128 @@ ids = $ids; return $this; } public function withDateModifiedSince($timestamp) { $this->dateModifiedSince = $timestamp; return $this; } public function withDateCreatedBefore($timestamp) { $this->dateCreatedBefore = $timestamp; return $this; } public function withObjectPHIDs(array $phids) { $this->objectPHIDs = $phids; return $this; } public function withClassNames(array $names) { $this->classNames = $names; return $this; } public function withFailureCountBetween($min, $max) { $this->minFailureCount = $min; $this->maxFailureCount = $max; return $this; } public function setLimit($limit) { $this->limit = $limit; return $this; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id in (%Ld)', $this->ids); } if ($this->objectPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'objectPHID IN (%Ls)', $this->objectPHIDs); } if ($this->dateModifiedSince !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'dateModified > %d', $this->dateModifiedSince); } if ($this->dateCreatedBefore !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'dateCreated < %d', $this->dateCreatedBefore); } if ($this->classNames !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'taskClass IN (%Ls)', $this->classNames); } if ($this->minFailureCount !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'failureCount >= %d', $this->minFailureCount); } if ($this->maxFailureCount !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'failureCount <= %d', $this->maxFailureCount); } - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } - protected function buildOrderClause(AphrontDatabaseConnection $conn_r) { + protected function buildOrderClause(AphrontDatabaseConnection $conn) { // NOTE: The garbage collector executes this query with a date constraint, // and the query is inefficient if we don't use the same key for ordering. // See T9808 for discussion. if ($this->dateCreatedBefore) { - return qsprintf($conn_r, 'ORDER BY dateCreated DESC, id DESC'); + return qsprintf($conn, 'ORDER BY dateCreated DESC, id DESC'); } else if ($this->dateModifiedSince) { - return qsprintf($conn_r, 'ORDER BY dateModified DESC, id DESC'); + return qsprintf($conn, 'ORDER BY dateModified DESC, id DESC'); } else { - return qsprintf($conn_r, 'ORDER BY id DESC'); + return qsprintf($conn, 'ORDER BY id DESC'); } } - protected function buildLimitClause(AphrontDatabaseConnection $conn_r) { - $clause = ''; + protected function buildLimitClause(AphrontDatabaseConnection $conn) { if ($this->limit) { - $clause = qsprintf($conn_r, 'LIMIT %d', $this->limit); + return qsprintf($conn, 'LIMIT %d', $this->limit); + } else { + return qsprintf($conn, ''); } - return $clause; } } diff --git a/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php b/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php index a8dc5061e7..87c16a48c5 100644 --- a/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php +++ b/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php @@ -1,233 +1,237 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withVersionBetween($min, $max) { $this->versionMin = $min; $this->versionMax = $max; return $this; } public function withNextEventBetween($min, $max) { $this->nextEpochMin = $min; $this->nextEpochMax = $max; return $this; } public function needEvents($need_events) { $this->needEvents = $need_events; return $this; } /** * Set the result order. * * Note that using `ORDER_EXECUTION` will also filter results to include only * triggers which have been scheduled to execute. You should not use this * ordering when querying for specific triggers, e.g. by ID or PHID. * * @param const Result order. * @return this */ public function setOrder($order) { $this->order = $order; return $this; } protected function nextPage(array $page) { // NOTE: We don't implement paging because we don't currently ever need // it and paging ORDER_EXECUTION is a hassle. throw new PhutilMethodNotImplementedException(); } protected function loadPage() { $task_table = new PhabricatorWorkerTrigger(); $conn_r = $task_table->establishConnection('r'); $rows = queryfx_all( $conn_r, 'SELECT t.* FROM %T t %Q %Q %Q %Q', $task_table->getTableName(), $this->buildJoinClause($conn_r), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); $triggers = $task_table->loadAllFromArray($rows); if ($triggers) { if ($this->needEvents) { $ids = mpull($triggers, 'getID'); $events = id(new PhabricatorWorkerTriggerEvent())->loadAllWhere( 'triggerID IN (%Ld)', $ids); $events = mpull($events, null, 'getTriggerID'); foreach ($triggers as $key => $trigger) { $event = idx($events, $trigger->getID()); $trigger->attachEvent($event); } } foreach ($triggers as $key => $trigger) { $clock_class = $trigger->getClockClass(); if (!is_subclass_of($clock_class, 'PhabricatorTriggerClock')) { unset($triggers[$key]); continue; } try { $argv = array($trigger->getClockProperties()); $clock = newv($clock_class, $argv); } catch (Exception $ex) { unset($triggers[$key]); continue; } $trigger->attachClock($clock); } foreach ($triggers as $key => $trigger) { $action_class = $trigger->getActionClass(); if (!is_subclass_of($action_class, 'PhabricatorTriggerAction')) { unset($triggers[$key]); continue; } try { $argv = array($trigger->getActionProperties()); $action = newv($action_class, $argv); } catch (Exception $ex) { unset($triggers[$key]); continue; } $trigger->attachAction($action); } } return $triggers; } - protected function buildJoinClause(AphrontDatabaseConnection $conn_r) { + protected function buildJoinClause(AphrontDatabaseConnection $conn) { $joins = array(); if (($this->nextEpochMin !== null) || ($this->nextEpochMax !== null) || ($this->order == self::ORDER_EXECUTION)) { $joins[] = qsprintf( - $conn_r, + $conn, 'JOIN %T e ON e.triggerID = t.id', id(new PhabricatorWorkerTriggerEvent())->getTableName()); } - return implode(' ', $joins); + if ($joins) { + return qsprintf($conn, '%LJ', $joins); + } else { + return qsprintf($conn, ''); + } } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 't.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 't.phid IN (%Ls)', $this->phids); } if ($this->versionMin !== null) { $where[] = qsprintf( - $conn_r, + $conn, 't.triggerVersion >= %d', $this->versionMin); } if ($this->versionMax !== null) { $where[] = qsprintf( - $conn_r, + $conn, 't.triggerVersion <= %d', $this->versionMax); } if ($this->nextEpochMin !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'e.nextEventEpoch >= %d', $this->nextEpochMin); } if ($this->nextEpochMax !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'e.nextEventEpoch <= %d', $this->nextEpochMax); } - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } private function buildOrderClause(AphrontDatabaseConnection $conn_r) { switch ($this->order) { case self::ORDER_ID: return qsprintf( $conn_r, 'ORDER BY id DESC'); case self::ORDER_EXECUTION: return qsprintf( $conn_r, 'ORDER BY e.nextEventEpoch ASC, e.id ASC'); case self::ORDER_VERSION: return qsprintf( $conn_r, 'ORDER BY t.triggerVersion ASC'); default: throw new Exception( pht( 'Unsupported order "%s".', $this->order)); } } } diff --git a/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php b/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php index 700a11ea36..588b5267b4 100644 --- a/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php +++ b/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php @@ -1,406 +1,406 @@ addEdge($src, $type, $dst) * ->save(); * * @task edit Editing Edges * @task cycles Cycle Prevention * @task internal Internals */ final class PhabricatorEdgeEditor extends Phobject { private $addEdges = array(); private $remEdges = array(); private $openTransactions = array(); /* -( Editing Edges )------------------------------------------------------ */ /** * Add a new edge (possibly also adding its inverse). Changes take effect when * you call @{method:save}. If the edge already exists, it will not be * overwritten, but if data is attached to the edge it will be updated. * Removals queued with @{method:removeEdge} are executed before * adds, so the effect of removing and adding the same edge is to overwrite * any existing edge. * * The `$options` parameter accepts these values: * * - `data` Optional, data to write onto the edge. * - `inverse_data` Optional, data to write on the inverse edge. If not * provided, `data` will be written. * * @param phid Source object PHID. * @param const Edge type constant. * @param phid Destination object PHID. * @param map Options map (see documentation). * @return this * * @task edit */ public function addEdge($src, $type, $dst, array $options = array()) { foreach ($this->buildEdgeSpecs($src, $type, $dst, $options) as $spec) { $this->addEdges[] = $spec; } return $this; } /** * Remove an edge (possibly also removing its inverse). Changes take effect * when you call @{method:save}. If an edge does not exist, the removal * will be ignored. Edges are added after edges are removed, so the effect of * a remove plus an add is to overwrite. * * @param phid Source object PHID. * @param const Edge type constant. * @param phid Destination object PHID. * @return this * * @task edit */ public function removeEdge($src, $type, $dst) { foreach ($this->buildEdgeSpecs($src, $type, $dst) as $spec) { $this->remEdges[] = $spec; } return $this; } /** * Apply edge additions and removals queued by @{method:addEdge} and * @{method:removeEdge}. Note that transactions are opened, all additions and * removals are executed, and then transactions are saved. Thus, in some cases * it may be slightly more efficient to perform multiple edit operations * (e.g., adds followed by removals) if their outcomes are not dependent, * since transactions will not be held open as long. * * @return this * @task edit */ public function save() { $cycle_types = $this->getPreventCyclesEdgeTypes(); $locks = array(); $caught = null; try { // NOTE: We write edge data first, before doing any transactions, since // it's OK if we just leave it hanging out in space unattached to // anything. $this->writeEdgeData(); // If we're going to perform cycle detection, lock the edge type before // doing edits. if ($cycle_types) { $src_phids = ipull($this->addEdges, 'src'); foreach ($cycle_types as $cycle_type) { $key = 'edge.cycle:'.$cycle_type; $locks[] = PhabricatorGlobalLock::newLock($key)->lock(15); } } static $id = 0; $id++; // NOTE: Removes first, then adds, so that "remove + add" is a useful // operation meaning "overwrite". $this->executeRemoves(); $this->executeAdds(); foreach ($cycle_types as $cycle_type) { $this->detectCycles($src_phids, $cycle_type); } $this->saveTransactions(); } catch (Exception $ex) { $caught = $ex; } if ($caught) { $this->killTransactions(); } foreach ($locks as $lock) { $lock->unlock(); } if ($caught) { throw $caught; } } /* -( Internals )---------------------------------------------------------- */ /** * Build the specification for an edge operation, and possibly build its * inverse as well. * * @task internal */ private function buildEdgeSpecs($src, $type, $dst, array $options = array()) { $data = array(); if (!empty($options['data'])) { $data['data'] = $options['data']; } $src_type = phid_get_type($src); $dst_type = phid_get_type($dst); $specs = array(); $specs[] = array( 'src' => $src, 'src_type' => $src_type, 'dst' => $dst, 'dst_type' => $dst_type, 'type' => $type, 'data' => $data, ); $type_obj = PhabricatorEdgeType::getByConstant($type); $inverse = $type_obj->getInverseEdgeConstant(); if ($inverse !== null) { // If `inverse_data` is set, overwrite the edge data. Normally, just // write the same data to the inverse edge. if (array_key_exists('inverse_data', $options)) { $data['data'] = $options['inverse_data']; } $specs[] = array( 'src' => $dst, 'src_type' => $dst_type, 'dst' => $src, 'dst_type' => $src_type, 'type' => $inverse, 'data' => $data, ); } return $specs; } /** * Write edge data. * * @task internal */ private function writeEdgeData() { $adds = $this->addEdges; $writes = array(); foreach ($adds as $key => $edge) { if ($edge['data']) { $writes[] = array($key, $edge['src_type'], json_encode($edge['data'])); } } foreach ($writes as $write) { list($key, $src_type, $data) = $write; $conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w'); queryfx( $conn_w, 'INSERT INTO %T (data) VALUES (%s)', PhabricatorEdgeConfig::TABLE_NAME_EDGEDATA, $data); $this->addEdges[$key]['data_id'] = $conn_w->getInsertID(); } } /** * Add queued edges. * * @task internal */ private function executeAdds() { $adds = $this->addEdges; $adds = igroup($adds, 'src_type'); // Assign stable sequence numbers to each edge, so we have a consistent // ordering across edges by source and type. foreach ($adds as $src_type => $edges) { $edges_by_src = igroup($edges, 'src'); foreach ($edges_by_src as $src => $src_edges) { $seq = 0; foreach ($src_edges as $key => $edge) { $src_edges[$key]['seq'] = $seq++; $src_edges[$key]['dateCreated'] = time(); } $edges_by_src[$src] = $src_edges; } $adds[$src_type] = array_mergev($edges_by_src); } $inserts = array(); foreach ($adds as $src_type => $edges) { $conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w'); $sql = array(); foreach ($edges as $edge) { $sql[] = qsprintf( $conn_w, '(%s, %d, %s, %d, %d, %nd)', $edge['src'], $edge['type'], $edge['dst'], $edge['dateCreated'], $edge['seq'], idx($edge, 'data_id')); } $inserts[] = array($conn_w, $sql); } foreach ($inserts as $insert) { list($conn_w, $sql) = $insert; $conn_w->openTransaction(); $this->openTransactions[] = $conn_w; - foreach (array_chunk($sql, 256) as $chunk) { + foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) { queryfx( $conn_w, 'INSERT INTO %T (src, type, dst, dateCreated, seq, dataID) - VALUES %Q ON DUPLICATE KEY UPDATE dataID = VALUES(dataID)', + VALUES %LQ ON DUPLICATE KEY UPDATE dataID = VALUES(dataID)', PhabricatorEdgeConfig::TABLE_NAME_EDGE, - implode(', ', $chunk)); + $chunk); } } } /** * Remove queued edges. * * @task internal */ private function executeRemoves() { $rems = $this->remEdges; $rems = igroup($rems, 'src_type'); $deletes = array(); foreach ($rems as $src_type => $edges) { $conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w'); $sql = array(); foreach ($edges as $edge) { $sql[] = qsprintf( $conn_w, '(src = %s AND type = %d AND dst = %s)', $edge['src'], $edge['type'], $edge['dst']); } $deletes[] = array($conn_w, $sql); } foreach ($deletes as $delete) { list($conn_w, $sql) = $delete; $conn_w->openTransaction(); $this->openTransactions[] = $conn_w; foreach (array_chunk($sql, 256) as $chunk) { queryfx( $conn_w, - 'DELETE FROM %T WHERE (%Q)', + 'DELETE FROM %T WHERE %LO', PhabricatorEdgeConfig::TABLE_NAME_EDGE, - implode(' OR ', $chunk)); + $chunk); } } } /** * Save open transactions. * * @task internal */ private function saveTransactions() { foreach ($this->openTransactions as $key => $conn_w) { $conn_w->saveTransaction(); unset($this->openTransactions[$key]); } } private function killTransactions() { foreach ($this->openTransactions as $key => $conn_w) { $conn_w->killTransaction(); unset($this->openTransactions[$key]); } } /* -( Cycle Prevention )--------------------------------------------------- */ /** * Get a list of all edge types which are being added, and which we should * prevent cycles on. * * @return list List of edge types which should have cycles prevented. * @task cycle */ private function getPreventCyclesEdgeTypes() { $edge_types = array(); foreach ($this->addEdges as $edge) { $edge_types[$edge['type']] = true; } foreach ($edge_types as $type => $ignored) { $type_obj = PhabricatorEdgeType::getByConstant($type); if (!$type_obj->shouldPreventCycles()) { unset($edge_types[$type]); } } return array_keys($edge_types); } /** * Detect graph cycles of a given edge type. If the edit introduces a cycle, * a @{class:PhabricatorEdgeCycleException} is thrown with details. * * @return void * @task cycle */ private function detectCycles(array $phids, $edge_type) { // For simplicity, we just seed the graph with the affected nodes rather // than seeding it with their edges. To do this, we just add synthetic // edges from an imaginary '' node to the known edges. $graph = id(new PhabricatorEdgeGraph()) ->setEdgeType($edge_type) ->addNodes( array( '' => $phids, )) ->loadGraph(); foreach ($phids as $phid) { $cycle = $graph->detectCycles($phid); if ($cycle) { throw new PhabricatorEdgeCycleException($edge_type, $cycle); } } } } diff --git a/src/infrastructure/edges/query/PhabricatorEdgeQuery.php b/src/infrastructure/edges/query/PhabricatorEdgeQuery.php index 2dfceb7fbc..6519c47724 100644 --- a/src/infrastructure/edges/query/PhabricatorEdgeQuery.php +++ b/src/infrastructure/edges/query/PhabricatorEdgeQuery.php @@ -1,333 +1,333 @@ withSourcePHIDs(array($src)) * ->withEdgeTypes(array($type)) * ->execute(); * * For more information on edges, see @{article:Using Edges}. * * @task config Configuring the Query * @task exec Executing the Query * @task internal Internal */ final class PhabricatorEdgeQuery extends PhabricatorQuery { private $sourcePHIDs; private $destPHIDs; private $edgeTypes; private $resultSet; const ORDER_OLDEST_FIRST = 'order:oldest'; const ORDER_NEWEST_FIRST = 'order:newest'; private $order = self::ORDER_NEWEST_FIRST; private $needEdgeData; /* -( Configuring the Query )---------------------------------------------- */ /** * Find edges originating at one or more source PHIDs. You MUST provide this * to execute an edge query. * * @param list List of source PHIDs. * @return this * * @task config */ public function withSourcePHIDs(array $source_phids) { $this->sourcePHIDs = $source_phids; return $this; } /** * Find edges terminating at one or more destination PHIDs. * * @param list List of destination PHIDs. * @return this * */ public function withDestinationPHIDs(array $dest_phids) { $this->destPHIDs = $dest_phids; return $this; } /** * Find edges of specific types. * * @param list List of PhabricatorEdgeConfig type constants. * @return this * * @task config */ public function withEdgeTypes(array $types) { $this->edgeTypes = $types; return $this; } /** * Configure the order edge results are returned in. * * @param const Order constant. * @return this * * @task config */ public function setOrder($order) { $this->order = $order; return $this; } /** * When loading edges, also load edge data. * * @param bool True to load edge data. * @return this * * @task config */ public function needEdgeData($need) { $this->needEdgeData = $need; return $this; } /* -( Executing the Query )------------------------------------------------ */ /** * Convenience method for loading destination PHIDs with one source and one * edge type. Equivalent to building a full query, but simplifies a common * use case. * * @param phid Source PHID. * @param const Edge type. * @return list List of destination PHIDs. */ public static function loadDestinationPHIDs($src_phid, $edge_type) { $edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($src_phid)) ->withEdgeTypes(array($edge_type)) ->execute(); return array_keys($edges[$src_phid][$edge_type]); } /** * Convenience method for loading a single edge's metadata for * a given source, destination, and edge type. Returns null * if the edge does not exist or does not have metadata. Builds * and immediately executes a full query. * * @param phid Source PHID. * @param const Edge type. * @param phid Destination PHID. * @return wild Edge annotation (or null). */ public static function loadSingleEdgeData($src_phid, $edge_type, $dest_phid) { $edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($src_phid)) ->withEdgeTypes(array($edge_type)) ->withDestinationPHIDs(array($dest_phid)) ->needEdgeData(true) ->execute(); if (isset($edges[$src_phid][$edge_type][$dest_phid]['data'])) { return $edges[$src_phid][$edge_type][$dest_phid]['data']; } return null; } /** * Load specified edges. * * @task exec */ public function execute() { if (!$this->sourcePHIDs) { throw new Exception( pht( 'You must use %s to query edges.', 'withSourcePHIDs()')); } $sources = phid_group_by_type($this->sourcePHIDs); $result = array(); // When a query specifies types, make sure we return data for all queried // types. if ($this->edgeTypes) { foreach ($this->sourcePHIDs as $phid) { foreach ($this->edgeTypes as $type) { $result[$phid][$type] = array(); } } } foreach ($sources as $type => $phids) { $conn_r = PhabricatorEdgeConfig::establishConnection($type, 'r'); $where = $this->buildWhereClause($conn_r); $order = $this->buildOrderClause($conn_r); $edges = queryfx_all( $conn_r, 'SELECT edge.* FROM %T edge %Q %Q', PhabricatorEdgeConfig::TABLE_NAME_EDGE, $where, $order); if ($this->needEdgeData) { $data_ids = array_filter(ipull($edges, 'dataID')); $data_map = array(); if ($data_ids) { $data_rows = queryfx_all( $conn_r, 'SELECT edgedata.* FROM %T edgedata WHERE id IN (%Ld)', PhabricatorEdgeConfig::TABLE_NAME_EDGEDATA, $data_ids); foreach ($data_rows as $row) { $data_map[$row['id']] = idx( phutil_json_decode($row['data']), 'data'); } } foreach ($edges as $key => $edge) { $edges[$key]['data'] = idx($data_map, $edge['dataID'], array()); } } foreach ($edges as $edge) { $result[$edge['src']][$edge['type']][$edge['dst']] = $edge; } } $this->resultSet = $result; return $result; } /** * Convenience function for selecting edge destination PHIDs after calling * execute(). * * Returns a flat list of PHIDs matching the provided source PHID and type * filters. By default, the filters are empty so all PHIDs will be returned. * For example, if you're doing a batch query from several sources, you might * write code like this: * * $query = new PhabricatorEdgeQuery(); * $query->setViewer($viewer); * $query->withSourcePHIDs(mpull($objects, 'getPHID')); * $query->withEdgeTypes(array($some_type)); * $query->execute(); * * // Gets all of the destinations. * $all_phids = $query->getDestinationPHIDs(); * $handles = id(new PhabricatorHandleQuery()) * ->setViewer($viewer) * ->withPHIDs($all_phids) * ->execute(); * * foreach ($objects as $object) { * // Get all of the destinations for the given object. * $dst_phids = $query->getDestinationPHIDs(array($object->getPHID())); * $object->attachHandles(array_select_keys($handles, $dst_phids)); * } * * @param list? List of PHIDs to select, or empty to select all. * @param list? List of edge types to select, or empty to select all. * @return list List of matching destination PHIDs. */ public function getDestinationPHIDs( array $src_phids = array(), array $types = array()) { if ($this->resultSet === null) { throw new PhutilInvalidStateException('execute'); } $result_phids = array(); $set = $this->resultSet; if ($src_phids) { $set = array_select_keys($set, $src_phids); } foreach ($set as $src => $edges_by_type) { if ($types) { $edges_by_type = array_select_keys($edges_by_type, $types); } foreach ($edges_by_type as $edges) { foreach ($edges as $edge_phid => $edge) { $result_phids[$edge_phid] = true; } } } return array_keys($result_phids); } /* -( Internals )---------------------------------------------------------- */ /** * @task internal */ - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->sourcePHIDs) { $where[] = qsprintf( - $conn_r, + $conn, 'edge.src IN (%Ls)', $this->sourcePHIDs); } if ($this->edgeTypes) { $where[] = qsprintf( - $conn_r, + $conn, 'edge.type IN (%Ls)', $this->edgeTypes); } if ($this->destPHIDs) { // potentially complain if $this->edgeType was not set $where[] = qsprintf( - $conn_r, + $conn, 'edge.dst IN (%Ls)', $this->destPHIDs); } - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } /** * @task internal */ - private function buildOrderClause($conn_r) { + private function buildOrderClause(AphrontDatabaseConnection $conn) { if ($this->order == self::ORDER_NEWEST_FIRST) { - return 'ORDER BY edge.dateCreated DESC, edge.seq DESC'; + return qsprintf($conn, 'ORDER BY edge.dateCreated DESC, edge.seq DESC'); } else { - return 'ORDER BY edge.dateCreated ASC, edge.seq ASC'; + return qsprintf($conn, 'ORDER BY edge.dateCreated ASC, edge.seq ASC'); } } } diff --git a/src/infrastructure/query/PhabricatorOffsetPagedQuery.php b/src/infrastructure/query/PhabricatorOffsetPagedQuery.php index ef97a4ebe4..fd9ea18e3f 100644 --- a/src/infrastructure/query/PhabricatorOffsetPagedQuery.php +++ b/src/infrastructure/query/PhabricatorOffsetPagedQuery.php @@ -1,51 +1,51 @@ offset = $offset; return $this; } final public function setLimit($limit) { $this->limit = $limit; return $this; } final public function getOffset() { return $this->offset; } final public function getLimit() { return $this->limit; } - protected function buildLimitClause(AphrontDatabaseConnection $conn_r) { + protected function buildLimitClause(AphrontDatabaseConnection $conn) { if ($this->limit && $this->offset) { - return qsprintf($conn_r, 'LIMIT %d, %d', $this->offset, $this->limit); + return qsprintf($conn, 'LIMIT %d, %d', $this->offset, $this->limit); } else if ($this->limit) { - return qsprintf($conn_r, 'LIMIT %d', $this->limit); + return qsprintf($conn, 'LIMIT %d', $this->limit); } else if ($this->offset) { - return qsprintf($conn_r, 'LIMIT %d, %d', $this->offset, PHP_INT_MAX); + return qsprintf($conn, 'LIMIT %d, %d', $this->offset, PHP_INT_MAX); } else { - return ''; + return qsprintf($conn, ''); } } final public function executeWithOffsetPager(PHUIPagerView $pager) { $this->setLimit($pager->getPageSize() + 1); $this->setOffset($pager->getOffset()); $results = $this->execute(); return $pager->sliceResults($results); } } diff --git a/src/infrastructure/query/PhabricatorQuery.php b/src/infrastructure/query/PhabricatorQuery.php index 5893297dbc..4315ef79ae 100644 --- a/src/infrastructure/query/PhabricatorQuery.php +++ b/src/infrastructure/query/PhabricatorQuery.php @@ -1,122 +1,97 @@ flattenSubclause($parts); - if (!$parts) { - return ''; - } + protected function formatWhereClause( + AphrontDatabaseConnection $conn, + array $parts) { - return 'WHERE '.$this->formatWhereSubclause($parts); - } - - - /** - * @task format - */ - protected function formatWhereSubclause(array $parts) { $parts = $this->flattenSubclause($parts); if (!$parts) { - return null; + return qsprintf($conn, ''); } - return '('.implode(') AND (', $parts).')'; + return qsprintf($conn, 'WHERE %LA', $parts); } - /** - * @task format - */ - protected function formatSelectClause(array $parts) { - $parts = $this->flattenSubclause($parts); - if (!$parts) { - throw new Exception(pht('Can not build empty select clause!')); - } - - return 'SELECT '.$this->formatSelectSubclause($parts); - } - /** * @task format */ - protected function formatSelectSubclause(array $parts) { - $parts = $this->flattenSubclause($parts); - if (!$parts) { - return null; - } - return implode(', ', $parts); - } - + protected function formatSelectClause( + AphrontDatabaseConnection $conn, + array $parts) { - /** - * @task format - */ - protected function formatJoinClause(array $parts) { $parts = $this->flattenSubclause($parts); if (!$parts) { - return ''; + throw new Exception(pht('Can not build empty SELECT clause!')); } - return implode(' ', $parts); + return qsprintf($conn, 'SELECT %LQ', $parts); } /** * @task format */ - protected function formatHavingClause(array $parts) { + protected function formatJoinClause( + AphrontDatabaseConnection $conn, + array $parts) { + $parts = $this->flattenSubclause($parts); if (!$parts) { - return ''; + return qsprintf($conn, ''); } - return 'HAVING '.$this->formatHavingSubclause($parts); + return qsprintf($conn, '%LJ', $parts); } /** * @task format */ - protected function formatHavingSubclause(array $parts) { + protected function formatHavingClause( + AphrontDatabaseConnection $conn, + array $parts) { + $parts = $this->flattenSubclause($parts); if (!$parts) { - return null; + return qsprintf($conn, ''); } - return '('.implode(') AND (', $parts).')'; + return qsprintf($conn, 'HAVING %LA', $parts); } /** * @task format */ private function flattenSubclause(array $parts) { $result = array(); foreach ($parts as $part) { if (is_array($part)) { foreach ($this->flattenSubclause($part) as $subpart) { $result[] = $subpart; } } else if (strlen($part)) { $result[] = $part; } } return $result; } } diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php index 1784a8ce17..931840e8fb 100644 --- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php @@ -1,2940 +1,2955 @@ getResultCursor(head($page)), $this->getResultCursor(last($page)), ); } protected function getResultCursor($object) { if (!is_object($object)) { throw new Exception( pht( 'Expected object, got "%s".', gettype($object))); } return $object->getID(); } protected function nextPage(array $page) { // See getPagingViewer() for a description of this flag. $this->internalPaging = true; if ($this->beforeID !== null) { $page = array_reverse($page, $preserve_keys = true); list($before, $after) = $this->getPageCursors($page); $this->beforeID = $before; } else { list($before, $after) = $this->getPageCursors($page); $this->afterID = $after; } } final public function setAfterID($object_id) { $this->afterID = $object_id; return $this; } final protected function getAfterID() { return $this->afterID; } final public function setBeforeID($object_id) { $this->beforeID = $object_id; return $this; } final protected function getBeforeID() { return $this->beforeID; } final public function getFerretMetadata() { if (!$this->supportsFerretEngine()) { throw new Exception( pht( 'Unable to retrieve Ferret engine metadata, this class ("%s") does '. 'not support the Ferret engine.', get_class($this))); } return $this->ferretMetadata; } protected function loadStandardPage(PhabricatorLiskDAO $table) { $rows = $this->loadStandardPageRows($table); return $table->loadAllFromArray($rows); } protected function loadStandardPageRows(PhabricatorLiskDAO $table) { $conn = $table->establishConnection('r'); return $this->loadStandardPageRowsWithConnection( $conn, $table->getTableName()); } protected function loadStandardPageRowsWithConnection( AphrontDatabaseConnection $conn, $table_name) { $query = $this->buildStandardPageQuery($conn, $table_name); $rows = queryfx_all($conn, '%Q', $query); $rows = $this->didLoadRawRows($rows); return $rows; } protected function buildStandardPageQuery( AphrontDatabaseConnection $conn, $table_name) { + $table_alias = $this->getPrimaryTableAlias(); + if ($table_alias === null) { + $table_alias = qsprintf($conn, ''); + } else { + $table_alias = qsprintf($conn, '%T', $table_alias); + } + return qsprintf( $conn, '%Q FROM %T %Q %Q %Q %Q %Q %Q %Q', $this->buildSelectClause($conn), $table_name, - (string)$this->getPrimaryTableAlias(), + $table_alias, $this->buildJoinClause($conn), $this->buildWhereClause($conn), $this->buildGroupClause($conn), $this->buildHavingClause($conn), $this->buildOrderClause($conn), $this->buildLimitClause($conn)); } protected function didLoadRawRows(array $rows) { if ($this->ferretEngine) { foreach ($rows as $row) { $phid = $row['phid']; $metadata = id(new PhabricatorFerretMetadata()) ->setPHID($phid) ->setEngine($this->ferretEngine) ->setRelevance(idx($row, '_ft_rank')); $this->ferretMetadata[$phid] = $metadata; unset($row['_ft_rank']); } } return $rows; } /** * Get the viewer for making cursor paging queries. * * NOTE: You should ONLY use this viewer to load cursor objects while * building paging queries. * * Cursor paging can happen in two ways. First, the user can request a page * like `/stuff/?after=33`, which explicitly causes paging. Otherwise, we * can fall back to implicit paging if we filter some results out of a * result list because the user can't see them and need to go fetch some more * results to generate a large enough result list. * * In the first case, want to use the viewer's policies to load the object. * This prevents an attacker from figuring out information about an object * they can't see by executing queries like `/stuff/?after=33&order=name`, * which would otherwise give them a hint about the name of the object. * Generally, if a user can't see an object, they can't use it to page. * * In the second case, we need to load the object whether the user can see * it or not, because we need to examine new results. For example, if a user * loads `/stuff/` and we run a query for the first 100 items that they can * see, but the first 100 rows in the database aren't visible, we need to * be able to issue a query for the next 100 results. If we can't load the * cursor object, we'll fail or issue the same query over and over again. * So, generally, internal paging must bypass policy controls. * * This method returns the appropriate viewer, based on the context in which * the paging is occurring. * * @return PhabricatorUser Viewer for executing paging queries. */ final protected function getPagingViewer() { if ($this->internalPaging) { return PhabricatorUser::getOmnipotentUser(); } else { return $this->getViewer(); } } - final protected function buildLimitClause(AphrontDatabaseConnection $conn_r) { + final protected function buildLimitClause(AphrontDatabaseConnection $conn) { if ($this->shouldLimitResults()) { $limit = $this->getRawResultLimit(); if ($limit) { - return qsprintf($conn_r, 'LIMIT %d', $limit); + return qsprintf($conn, 'LIMIT %d', $limit); } } - return ''; + return qsprintf($conn, ''); } protected function shouldLimitResults() { return true; } final protected function didLoadResults(array $results) { if ($this->beforeID) { $results = array_reverse($results, $preserve_keys = true); } return $results; } final public function executeWithCursorPager(AphrontCursorPagerView $pager) { $limit = $pager->getPageSize(); $this->setLimit($limit + 1); if ($pager->getAfterID()) { $this->setAfterID($pager->getAfterID()); } else if ($pager->getBeforeID()) { $this->setBeforeID($pager->getBeforeID()); } $results = $this->execute(); $count = count($results); $sliced_results = $pager->sliceResults($results); if ($sliced_results) { list($before, $after) = $this->getPageCursors($sliced_results); if ($pager->getBeforeID() || ($count > $limit)) { $pager->setNextPageID($after); } if ($pager->getAfterID() || ($pager->getBeforeID() && ($count > $limit))) { $pager->setPrevPageID($before); } } return $sliced_results; } /** * Return the alias this query uses to identify the primary table. * * Some automatic query constructions may need to be qualified with a table * alias if the query performs joins which make column names ambiguous. If * this is the case, return the alias for the primary table the query * uses; generally the object table which has `id` and `phid` columns. * * @return string Alias for the primary table. */ protected function getPrimaryTableAlias() { return null; } public function newResultObject() { return null; } /* -( Building Query Clauses )--------------------------------------------- */ /** * @task clauses */ protected function buildSelectClause(AphrontDatabaseConnection $conn) { $parts = $this->buildSelectClauseParts($conn); - return $this->formatSelectClause($parts); + return $this->formatSelectClause($conn, $parts); } /** * @task clauses */ protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) { $select = array(); $alias = $this->getPrimaryTableAlias(); if ($alias) { $select[] = qsprintf($conn, '%T.*', $alias); } else { - $select[] = '*'; + $select[] = qsprintf($conn, '*'); } $select[] = $this->buildEdgeLogicSelectClause($conn); $select[] = $this->buildFerretSelectClause($conn); return $select; } /** * @task clauses */ protected function buildJoinClause(AphrontDatabaseConnection $conn) { $joins = $this->buildJoinClauseParts($conn); - return $this->formatJoinClause($joins); + return $this->formatJoinClause($conn, $joins); } /** * @task clauses */ protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { $joins = array(); $joins[] = $this->buildEdgeLogicJoinClause($conn); $joins[] = $this->buildApplicationSearchJoinClause($conn); $joins[] = $this->buildNgramsJoinClause($conn); $joins[] = $this->buildFerretJoinClause($conn); return $joins; } /** * @task clauses */ protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = $this->buildWhereClauseParts($conn); - return $this->formatWhereClause($where); + return $this->formatWhereClause($conn, $where); } /** * @task clauses */ protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = array(); $where[] = $this->buildPagingClause($conn); $where[] = $this->buildEdgeLogicWhereClause($conn); $where[] = $this->buildSpacesWhereClause($conn); $where[] = $this->buildNgramsWhereClause($conn); $where[] = $this->buildFerretWhereClause($conn); $where[] = $this->buildApplicationSearchWhereClause($conn); return $where; } /** * @task clauses */ protected function buildHavingClause(AphrontDatabaseConnection $conn) { $having = $this->buildHavingClauseParts($conn); - return $this->formatHavingClause($having); + return $this->formatHavingClause($conn, $having); } /** * @task clauses */ protected function buildHavingClauseParts(AphrontDatabaseConnection $conn) { $having = array(); $having[] = $this->buildEdgeLogicHavingClause($conn); return $having; } /** * @task clauses */ protected function buildGroupClause(AphrontDatabaseConnection $conn) { if (!$this->shouldGroupQueryResultRows()) { - return ''; + return qsprintf($conn, ''); } return qsprintf( $conn, 'GROUP BY %Q', - $this->getApplicationSearchObjectPHIDColumn()); + $this->getApplicationSearchObjectPHIDColumn($conn)); } /** * @task clauses */ protected function shouldGroupQueryResultRows() { if ($this->shouldGroupEdgeLogicResultRows()) { return true; } if ($this->getApplicationSearchMayJoinMultipleRows()) { return true; } if ($this->shouldGroupNgramResultRows()) { return true; } if ($this->shouldGroupFerretResultRows()) { return true; } return false; } /* -( Paging )------------------------------------------------------------- */ /** * @task paging */ protected function buildPagingClause(AphrontDatabaseConnection $conn) { $orderable = $this->getOrderableColumns(); $vector = $this->getOrderVector(); if ($this->beforeID !== null) { $cursor = $this->beforeID; $reversed = true; } else if ($this->afterID !== null) { $cursor = $this->afterID; $reversed = false; } else { // No paging is being applied to this query so we do not need to // construct a paging clause. - return ''; + return qsprintf($conn, ''); } $keys = array(); foreach ($vector as $order) { $keys[] = $order->getOrderKey(); } $value_map = $this->getPagingValueMap($cursor, $keys); $columns = array(); foreach ($vector as $order) { $key = $order->getOrderKey(); if (!array_key_exists($key, $value_map)) { throw new Exception( pht( 'Query "%s" failed to return a value from getPagingValueMap() '. 'for column "%s".', get_class($this), $key)); } $column = $orderable[$key]; $column['value'] = $value_map[$key]; // If the vector component is reversed, we need to reverse whatever the // order of the column is. if ($order->getIsReversed()) { $column['reverse'] = !idx($column, 'reverse', false); } $columns[] = $column; } return $this->buildPagingClauseFromMultipleColumns( $conn, $columns, array( 'reversed' => $reversed, )); } /** * @task paging */ protected function getPagingValueMap($cursor, array $keys) { return array( 'id' => $cursor, ); } /** * @task paging */ protected function loadCursorObject($cursor) { $query = newv(get_class($this), array()) ->setViewer($this->getPagingViewer()) ->withIDs(array((int)$cursor)); $this->willExecuteCursorQuery($query); $object = $query->executeOne(); if (!$object) { throw new Exception( pht( 'Cursor "%s" does not identify a valid object in query "%s".', $cursor, get_class($this))); } return $object; } /** * @task paging */ protected function willExecuteCursorQuery( PhabricatorCursorPagedPolicyAwareQuery $query) { return; } /** * Simplifies the task of constructing a paging clause across multiple * columns. In the general case, this looks like: * * A > a OR (A = a AND B > b) OR (A = a AND B = b AND C > c) * * To build a clause, specify the name, type, and value of each column * to include: * * $this->buildPagingClauseFromMultipleColumns( * $conn_r, * array( * array( * 'table' => 't', * 'column' => 'title', * 'type' => 'string', * 'value' => $cursor->getTitle(), * 'reverse' => true, * ), * array( * 'table' => 't', * 'column' => 'id', * 'type' => 'int', * 'value' => $cursor->getID(), * ), * ), * array( * 'reversed' => $is_reversed, * )); * * This method will then return a composable clause for inclusion in WHERE. * * @param AphrontDatabaseConnection Connection query will execute on. * @param list Column description dictionaries. * @param map Additional construction options. * @return string Query clause. * @task paging */ final protected function buildPagingClauseFromMultipleColumns( AphrontDatabaseConnection $conn, array $columns, array $options) { foreach ($columns as $column) { PhutilTypeSpec::checkMap( $column, array( 'table' => 'optional string|null', 'column' => 'string', 'value' => 'wild', 'type' => 'string', 'reverse' => 'optional bool', 'unique' => 'optional bool', 'null' => 'optional string|null', )); } PhutilTypeSpec::checkMap( $options, array( 'reversed' => 'optional bool', )); $is_query_reversed = idx($options, 'reversed', false); $clauses = array(); $accumulated = array(); $last_key = last_key($columns); foreach ($columns as $key => $column) { $type = $column['type']; $null = idx($column, 'null'); if ($column['value'] === null) { if ($null) { $value = null; } else { throw new Exception( pht( 'Column "%s" has null value, but does not specify a null '. 'behavior.', $key)); } } else { switch ($type) { case 'int': $value = qsprintf($conn, '%d', $column['value']); break; case 'float': $value = qsprintf($conn, '%f', $column['value']); break; case 'string': $value = qsprintf($conn, '%s', $column['value']); break; default: throw new Exception( pht( 'Column "%s" has unknown column type "%s".', $column['column'], $type)); } } $is_column_reversed = idx($column, 'reverse', false); $reverse = ($is_query_reversed xor $is_column_reversed); $clause = $accumulated; $table_name = idx($column, 'table'); $column_name = $column['column']; if ($table_name !== null) { $field = qsprintf($conn, '%T.%T', $table_name, $column_name); } else { $field = qsprintf($conn, '%T', $column_name); } $parts = array(); if ($null) { $can_page_if_null = ($null === 'head'); $can_page_if_nonnull = ($null === 'tail'); if ($reverse) { $can_page_if_null = !$can_page_if_null; $can_page_if_nonnull = !$can_page_if_nonnull; } $subclause = null; if ($can_page_if_null && $value === null) { $parts[] = qsprintf( $conn, '(%Q IS NOT NULL)', $field); } else if ($can_page_if_nonnull && $value !== null) { $parts[] = qsprintf( $conn, '(%Q IS NULL)', $field); } } if ($value !== null) { $parts[] = qsprintf( $conn, '%Q %Q %Q', $field, - $reverse ? '>' : '<', + $reverse ? qsprintf($conn, '>') : qsprintf($conn, '<'), $value); } if ($parts) { - if (count($parts) > 1) { - $clause[] = '('.implode(') OR (', $parts).')'; - } else { - $clause[] = head($parts); - } + $clause[] = qsprintf($conn, '%LO', $parts); } if ($clause) { - if (count($clause) > 1) { - $clauses[] = '('.implode(') AND (', $clause).')'; - } else { - $clauses[] = head($clause); - } + $clauses[] = qsprintf($conn, '%LA', $clause); } if ($value === null) { $accumulated[] = qsprintf( $conn, '%Q IS NULL', $field); } else { $accumulated[] = qsprintf( $conn, '%Q = %Q', $field, $value); } } - return '('.implode(') OR (', $clauses).')'; + if ($clauses) { + return qsprintf($conn, '%LO', $clauses); + } + + return qsprintf($conn, ''); } /* -( Result Ordering )---------------------------------------------------- */ /** * Select a result ordering. * * This is a high-level method which selects an ordering from a predefined * list of builtin orders, as provided by @{method:getBuiltinOrders}. These * options are user-facing and not exhaustive, but are generally convenient * and meaningful. * * You can also use @{method:setOrderVector} to specify a low-level ordering * across individual orderable columns. This offers greater control but is * also more involved. * * @param string Key of a builtin order supported by this query. * @return this * @task order */ public function setOrder($order) { $aliases = $this->getBuiltinOrderAliasMap(); if (empty($aliases[$order])) { throw new Exception( pht( 'Query "%s" does not support a builtin order "%s". Supported orders '. 'are: %s.', get_class($this), $order, implode(', ', array_keys($aliases)))); } $this->builtinOrder = $aliases[$order]; $this->orderVector = null; return $this; } /** * Set a grouping order to apply before primary result ordering. * * This allows you to preface the query order vector with additional orders, * so you can effect "group by" queries while still respecting "order by". * * This is a high-level method which works alongside @{method:setOrder}. For * lower-level control over order vectors, use @{method:setOrderVector}. * * @param PhabricatorQueryOrderVector|list List of order keys. * @return this * @task order */ public function setGroupVector($vector) { $this->groupVector = $vector; $this->orderVector = null; return $this; } /** * Get builtin orders for this class. * * In application UIs, we want to be able to present users with a small * selection of meaningful order options (like "Order by Title") rather than * an exhaustive set of column ordering options. * * Meaningful user-facing orders are often really orders across multiple * columns: for example, a "title" ordering is usually implemented as a * "title, id" ordering under the hood. * * Builtin orders provide a mapping from convenient, understandable * user-facing orders to implementations. * * A builtin order should provide these keys: * * - `vector` (`list`): The actual order vector to use. * - `name` (`string`): Human-readable order name. * * @return map Map from builtin order keys to specification. * @task order */ public function getBuiltinOrders() { $orders = array( 'newest' => array( 'vector' => array('id'), 'name' => pht('Creation (Newest First)'), 'aliases' => array('created'), ), 'oldest' => array( 'vector' => array('-id'), 'name' => pht('Creation (Oldest First)'), ), ); $object = $this->newResultObject(); if ($object instanceof PhabricatorCustomFieldInterface) { $list = PhabricatorCustomField::getObjectFields( $object, PhabricatorCustomField::ROLE_APPLICATIONSEARCH); foreach ($list->getFields() as $field) { $index = $field->buildOrderIndex(); if (!$index) { continue; } $legacy_key = 'custom:'.$field->getFieldKey(); $modern_key = $field->getModernFieldKey(); $orders[$modern_key] = array( 'vector' => array($modern_key, 'id'), 'name' => $field->getFieldName(), 'aliases' => array($legacy_key), ); $orders['-'.$modern_key] = array( 'vector' => array('-'.$modern_key, '-id'), 'name' => pht('%s (Reversed)', $field->getFieldName()), ); } } if ($this->supportsFerretEngine()) { $orders['relevance'] = array( 'vector' => array('rank', 'fulltext-modified', 'id'), 'name' => pht('Relevance'), ); } return $orders; } public function getBuiltinOrderAliasMap() { $orders = $this->getBuiltinOrders(); $map = array(); foreach ($orders as $key => $order) { $keys = array(); $keys[] = $key; foreach (idx($order, 'aliases', array()) as $alias) { $keys[] = $alias; } foreach ($keys as $alias) { if (isset($map[$alias])) { throw new Exception( pht( 'Two builtin orders ("%s" and "%s") define the same key or '. 'alias ("%s"). Each order alias and key must be unique and '. 'identify a single order.', $key, $map[$alias], $alias)); } $map[$alias] = $key; } } return $map; } /** * Set a low-level column ordering. * * This is a low-level method which offers granular control over column * ordering. In most cases, applications can more easily use * @{method:setOrder} to choose a high-level builtin order. * * To set an order vector, specify a list of order keys as provided by * @{method:getOrderableColumns}. * * @param PhabricatorQueryOrderVector|list List of order keys. * @return this * @task order */ public function setOrderVector($vector) { $vector = PhabricatorQueryOrderVector::newFromVector($vector); $orderable = $this->getOrderableColumns(); // Make sure that all the components identify valid columns. $unique = array(); foreach ($vector as $order) { $key = $order->getOrderKey(); if (empty($orderable[$key])) { $valid = implode(', ', array_keys($orderable)); throw new Exception( pht( 'This query ("%s") does not support sorting by order key "%s". '. 'Supported orders are: %s.', get_class($this), $key, $valid)); } $unique[$key] = idx($orderable[$key], 'unique', false); } // Make sure that the last column is unique so that this is a strong // ordering which can be used for paging. $last = last($unique); if ($last !== true) { throw new Exception( pht( 'Order vector "%s" is invalid: the last column in an order must '. 'be a column with unique values, but "%s" is not unique.', $vector->getAsString(), last_key($unique))); } // Make sure that other columns are not unique; an ordering like "id, name" // does not make sense because only "id" can ever have an effect. array_pop($unique); foreach ($unique as $key => $is_unique) { if ($is_unique) { throw new Exception( pht( 'Order vector "%s" is invalid: only the last column in an order '. 'may be unique, but "%s" is a unique column and not the last '. 'column in the order.', $vector->getAsString(), $key)); } } $this->orderVector = $vector; return $this; } /** * Get the effective order vector. * * @return PhabricatorQueryOrderVector Effective vector. * @task order */ protected function getOrderVector() { if (!$this->orderVector) { if ($this->builtinOrder !== null) { $builtin_order = idx($this->getBuiltinOrders(), $this->builtinOrder); $vector = $builtin_order['vector']; } else { $vector = $this->getDefaultOrderVector(); } if ($this->groupVector) { $group = PhabricatorQueryOrderVector::newFromVector($this->groupVector); $group->appendVector($vector); $vector = $group; } $vector = PhabricatorQueryOrderVector::newFromVector($vector); // We call setOrderVector() here to apply checks to the default vector. // This catches any errors in the implementation. $this->setOrderVector($vector); } return $this->orderVector; } /** * @task order */ protected function getDefaultOrderVector() { return array('id'); } /** * @task order */ public function getOrderableColumns() { $cache = PhabricatorCaches::getRequestCache(); $class = get_class($this); $cache_key = 'query.orderablecolumns.'.$class; $columns = $cache->getKey($cache_key); if ($columns !== null) { return $columns; } $columns = array( 'id' => array( 'table' => $this->getPrimaryTableAlias(), 'column' => 'id', 'reverse' => false, 'type' => 'int', 'unique' => true, ), ); $object = $this->newResultObject(); if ($object instanceof PhabricatorCustomFieldInterface) { $list = PhabricatorCustomField::getObjectFields( $object, PhabricatorCustomField::ROLE_APPLICATIONSEARCH); foreach ($list->getFields() as $field) { $index = $field->buildOrderIndex(); if (!$index) { continue; } $digest = $field->getFieldIndex(); $key = $field->getModernFieldKey(); $columns[$key] = array( 'table' => 'appsearch_order_'.$digest, 'column' => 'indexValue', 'type' => $index->getIndexValueType(), 'null' => 'tail', 'customfield' => true, 'customfield.index.table' => $index->getTableName(), 'customfield.index.key' => $digest, ); } } if ($this->supportsFerretEngine()) { $columns['rank'] = array( 'table' => null, 'column' => '_ft_rank', 'type' => 'int', ); $columns['fulltext-created'] = array( 'table' => 'ft_doc', 'column' => 'epochCreated', 'type' => 'int', ); $columns['fulltext-modified'] = array( 'table' => 'ft_doc', 'column' => 'epochModified', 'type' => 'int', ); } $cache->setKey($cache_key, $columns); return $columns; } /** * @task order */ final protected function buildOrderClause( AphrontDatabaseConnection $conn, $for_union = false) { $orderable = $this->getOrderableColumns(); $vector = $this->getOrderVector(); $parts = array(); foreach ($vector as $order) { $part = $orderable[$order->getOrderKey()]; if ($order->getIsReversed()) { $part['reverse'] = !idx($part, 'reverse', false); } $parts[] = $part; } return $this->formatOrderClause($conn, $parts, $for_union); } /** * @task order */ protected function formatOrderClause( AphrontDatabaseConnection $conn, array $parts, $for_union = false) { $is_query_reversed = false; if ($this->getBeforeID()) { $is_query_reversed = !$is_query_reversed; } $sql = array(); foreach ($parts as $key => $part) { $is_column_reversed = !empty($part['reverse']); $descending = true; if ($is_query_reversed) { $descending = !$descending; } if ($is_column_reversed) { $descending = !$descending; } $table = idx($part, 'table'); // When we're building an ORDER BY clause for a sequence of UNION // statements, we can't refer to tables from the subqueries. if ($for_union) { $table = null; } $column = $part['column']; if ($table !== null) { $field = qsprintf($conn, '%T.%T', $table, $column); } else { $field = qsprintf($conn, '%T', $column); } $null = idx($part, 'null'); if ($null) { switch ($null) { case 'head': $null_field = qsprintf($conn, '(%Q IS NULL)', $field); break; case 'tail': $null_field = qsprintf($conn, '(%Q IS NOT NULL)', $field); break; default: throw new Exception( pht( 'NULL value "%s" is invalid. Valid values are "head" and '. '"tail".', $null)); } if ($descending) { $sql[] = qsprintf($conn, '%Q DESC', $null_field); } else { $sql[] = qsprintf($conn, '%Q ASC', $null_field); } } if ($descending) { $sql[] = qsprintf($conn, '%Q DESC', $field); } else { $sql[] = qsprintf($conn, '%Q ASC', $field); } } - return qsprintf($conn, 'ORDER BY %Q', implode(', ', $sql)); + return qsprintf($conn, 'ORDER BY %LQ', $sql); } /* -( Application Search )------------------------------------------------- */ /** * Constrain the query with an ApplicationSearch index, requiring field values * contain at least one of the values in a set. * * This constraint can build the most common types of queries, like: * * - Find users with shirt sizes "X" or "XL". * - Find shoes with size "13". * * @param PhabricatorCustomFieldIndexStorage Table where the index is stored. * @param string|list One or more values to filter by. * @return this * @task appsearch */ public function withApplicationSearchContainsConstraint( PhabricatorCustomFieldIndexStorage $index, $value) { $values = (array)$value; $data_values = array(); $constraint_values = array(); foreach ($values as $value) { if ($value instanceof PhabricatorQueryConstraint) { $constraint_values[] = $value; } else { $data_values[] = $value; } } $alias = 'appsearch_'.count($this->applicationSearchConstraints); $this->applicationSearchConstraints[] = array( 'type' => $index->getIndexValueType(), 'cond' => '=', 'table' => $index->getTableName(), 'index' => $index->getIndexKey(), 'alias' => $alias, 'value' => $values, 'data' => $data_values, 'constraints' => $constraint_values, ); return $this; } /** * Constrain the query with an ApplicationSearch index, requiring values * exist in a given range. * * This constraint is useful for expressing date ranges: * * - Find events between July 1st and July 7th. * * The ends of the range are inclusive, so a `$min` of `3` and a `$max` of * `5` will match fields with values `3`, `4`, or `5`. Providing `null` for * either end of the range will leave that end of the constraint open. * * @param PhabricatorCustomFieldIndexStorage Table where the index is stored. * @param int|null Minimum permissible value, inclusive. * @param int|null Maximum permissible value, inclusive. * @return this * @task appsearch */ public function withApplicationSearchRangeConstraint( PhabricatorCustomFieldIndexStorage $index, $min, $max) { $index_type = $index->getIndexValueType(); if ($index_type != 'int') { throw new Exception( pht( 'Attempting to apply a range constraint to a field with index type '. '"%s", expected type "%s".', $index_type, 'int')); } $alias = 'appsearch_'.count($this->applicationSearchConstraints); $this->applicationSearchConstraints[] = array( 'type' => $index->getIndexValueType(), 'cond' => 'range', 'table' => $index->getTableName(), 'index' => $index->getIndexKey(), 'alias' => $alias, 'value' => array($min, $max), ); return $this; } /** * Get the name of the query's primary object PHID column, for constructing * JOIN clauses. Normally (and by default) this is just `"phid"`, but it may * be something more exotic. * * See @{method:getPrimaryTableAlias} if the column needs to be qualified with * a table alias. * - * @return string Column name. + * @param AphrontDatabaseConnection Connection executing queries. + * @return PhutilQueryString Column name. * @task appsearch */ - protected function getApplicationSearchObjectPHIDColumn() { + protected function getApplicationSearchObjectPHIDColumn( + AphrontDatabaseConnection $conn) { + if ($this->getPrimaryTableAlias()) { - $prefix = $this->getPrimaryTableAlias().'.'; + return qsprintf($conn, '%T.phid', $this->getPrimaryTableAlias()); } else { - $prefix = ''; + return qsprintf($conn, 'phid'); } - - return $prefix.'phid'; } /** * Determine if the JOINs built by ApplicationSearch might cause each primary * object to return multiple result rows. Generally, this means the query * needs an extra GROUP BY clause. * * @return bool True if the query may return multiple rows for each object. * @task appsearch */ protected function getApplicationSearchMayJoinMultipleRows() { foreach ($this->applicationSearchConstraints as $constraint) { $type = $constraint['type']; $value = $constraint['value']; $cond = $constraint['cond']; switch ($cond) { case '=': switch ($type) { case 'string': case 'int': if (count($value) > 1) { return true; } break; default: throw new Exception(pht('Unknown index type "%s"!', $type)); } break; case 'range': // NOTE: It's possible to write a custom field where multiple rows // match a range constraint, but we don't currently ship any in the // upstream and I can't immediately come up with cases where this // would make sense. break; default: throw new Exception(pht('Unknown constraint condition "%s"!', $cond)); } } return false; } /** * Construct a GROUP BY clause appropriate for ApplicationSearch constraints. * * @param AphrontDatabaseConnection Connection executing the query. * @return string Group clause. * @task appsearch */ protected function buildApplicationSearchGroupClause( - AphrontDatabaseConnection $conn_r) { + AphrontDatabaseConnection $conn) { if ($this->getApplicationSearchMayJoinMultipleRows()) { return qsprintf( - $conn_r, + $conn, 'GROUP BY %Q', - $this->getApplicationSearchObjectPHIDColumn()); + $this->getApplicationSearchObjectPHIDColumn($conn)); } else { - return ''; + return qsprintf($conn, ''); } } /** * Construct a JOIN clause appropriate for applying ApplicationSearch * constraints. * * @param AphrontDatabaseConnection Connection executing the query. * @return string Join clause. * @task appsearch */ protected function buildApplicationSearchJoinClause( AphrontDatabaseConnection $conn) { $joins = array(); foreach ($this->applicationSearchConstraints as $key => $constraint) { $table = $constraint['table']; $alias = $constraint['alias']; $index = $constraint['index']; $cond = $constraint['cond']; - $phid_column = $this->getApplicationSearchObjectPHIDColumn(); + $phid_column = $this->getApplicationSearchObjectPHIDColumn($conn); switch ($cond) { case '=': // Figure out whether we need to do a LEFT JOIN or not. We need to // LEFT JOIN if we're going to select "IS NULL" rows. - $join_type = 'JOIN'; + $join_type = qsprintf($conn, 'JOIN'); foreach ($constraint['constraints'] as $query_constraint) { $op = $query_constraint->getOperator(); if ($op === PhabricatorQueryConstraint::OPERATOR_NULL) { - $join_type = 'LEFT JOIN'; + $join_type = qsprintf($conn, 'LEFT JOIN'); break; } } $joins[] = qsprintf( $conn, '%Q %T %T ON %T.objectPHID = %Q AND %T.indexKey = %s', $join_type, $table, $alias, $alias, $phid_column, $alias, $index); break; case 'range': list($min, $max) = $constraint['value']; if (($min === null) && ($max === null)) { // If there's no actual range constraint, just move on. break; } if ($min === null) { $constraint_clause = qsprintf( $conn, '%T.indexValue <= %d', $alias, $max); } else if ($max === null) { $constraint_clause = qsprintf( $conn, '%T.indexValue >= %d', $alias, $min); } else { $constraint_clause = qsprintf( $conn, '%T.indexValue BETWEEN %d AND %d', $alias, $min, $max); } $joins[] = qsprintf( $conn, 'JOIN %T %T ON %T.objectPHID = %Q AND %T.indexKey = %s AND (%Q)', $table, $alias, $alias, $phid_column, $alias, $index, $constraint_clause); break; default: throw new Exception(pht('Unknown constraint condition "%s"!', $cond)); } } - $phid_column = $this->getApplicationSearchObjectPHIDColumn(); + $phid_column = $this->getApplicationSearchObjectPHIDColumn($conn); $orderable = $this->getOrderableColumns(); $vector = $this->getOrderVector(); foreach ($vector as $order) { $spec = $orderable[$order->getOrderKey()]; if (empty($spec['customfield'])) { continue; } $table = $spec['customfield.index.table']; $alias = $spec['table']; $key = $spec['customfield.index.key']; $joins[] = qsprintf( $conn, 'LEFT JOIN %T %T ON %T.objectPHID = %Q AND %T.indexKey = %s', $table, $alias, $alias, $phid_column, $alias, $key); } - return implode(' ', $joins); + if ($joins) { + return qsprintf($conn, '%LJ', $joins); + } else { + return qsprintf($conn, ''); + } } /** * Construct a WHERE clause appropriate for applying ApplicationSearch * constraints. * * @param AphrontDatabaseConnection Connection executing the query. * @return list Where clause parts. * @task appsearch */ protected function buildApplicationSearchWhereClause( AphrontDatabaseConnection $conn) { $where = array(); foreach ($this->applicationSearchConstraints as $key => $constraint) { $alias = $constraint['alias']; $cond = $constraint['cond']; $type = $constraint['type']; $data_values = $constraint['data']; $constraint_values = $constraint['constraints']; $constraint_parts = array(); switch ($cond) { case '=': if ($data_values) { switch ($type) { case 'string': $constraint_parts[] = qsprintf( $conn, '%T.indexValue IN (%Ls)', $alias, $data_values); break; case 'int': $constraint_parts[] = qsprintf( $conn, '%T.indexValue IN (%Ld)', $alias, $data_values); break; default: throw new Exception(pht('Unknown index type "%s"!', $type)); } } if ($constraint_values) { foreach ($constraint_values as $value) { $op = $value->getOperator(); switch ($op) { case PhabricatorQueryConstraint::OPERATOR_NULL: $constraint_parts[] = qsprintf( $conn, '%T.indexValue IS NULL', $alias); break; case PhabricatorQueryConstraint::OPERATOR_ANY: $constraint_parts[] = qsprintf( $conn, '%T.indexValue IS NOT NULL', $alias); break; default: throw new Exception( pht( 'No support for applying operator "%s" against '. 'index of type "%s".', $op, $type)); } } } if ($constraint_parts) { - $where[] = '('.implode(') OR (', $constraint_parts).')'; + $where[] = qsprintf($conn, '%LO', $constraint_parts); } break; } } return $where; } /* -( Integration with CustomField )--------------------------------------- */ /** * @task customfield */ protected function getPagingValueMapForCustomFields( PhabricatorCustomFieldInterface $object) { // We have to get the current field values on the cursor object. $fields = PhabricatorCustomField::getObjectFields( $object, PhabricatorCustomField::ROLE_APPLICATIONSEARCH); $fields->setViewer($this->getViewer()); $fields->readFieldsFromStorage($object); $map = array(); foreach ($fields->getFields() as $field) { $map['custom:'.$field->getFieldKey()] = $field->getValueForStorage(); } return $map; } /** * @task customfield */ protected function isCustomFieldOrderKey($key) { $prefix = 'custom:'; return !strncmp($key, $prefix, strlen($prefix)); } /* -( Ferret )------------------------------------------------------------- */ public function supportsFerretEngine() { $object = $this->newResultObject(); return ($object instanceof PhabricatorFerretInterface); } public function withFerretQuery( PhabricatorFerretEngine $engine, PhabricatorSavedQuery $query) { if (!$this->supportsFerretEngine()) { throw new Exception( pht( 'Query ("%s") does not support the Ferret fulltext engine.', get_class($this))); } $this->ferretEngine = $engine; $this->ferretQuery = $query; return $this; } public function getFerretTokens() { if (!$this->supportsFerretEngine()) { throw new Exception( pht( 'Query ("%s") does not support the Ferret fulltext engine.', get_class($this))); } return $this->ferretTokens; } public function withFerretConstraint( PhabricatorFerretEngine $engine, array $fulltext_tokens) { if (!$this->supportsFerretEngine()) { throw new Exception( pht( 'Query ("%s") does not support the Ferret fulltext engine.', get_class($this))); } if ($this->ferretEngine) { throw new Exception( pht( 'Query may not have multiple fulltext constraints.')); } if (!$fulltext_tokens) { return $this; } $this->ferretEngine = $engine; $this->ferretTokens = $fulltext_tokens; $current_function = $engine->getDefaultFunctionKey(); $table_map = array(); $idx = 1; foreach ($this->ferretTokens as $fulltext_token) { $raw_token = $fulltext_token->getToken(); $function = $raw_token->getFunction(); if ($function === null) { $function = $current_function; } $raw_field = $engine->getFieldForFunction($function); if (!isset($table_map[$function])) { $alias = 'ftfield_'.$idx++; $table_map[$function] = array( 'alias' => $alias, 'key' => $raw_field, ); } $current_function = $function; } // Join the title field separately so we can rank results. $table_map['rank'] = array( 'alias' => 'ft_rank', 'key' => PhabricatorSearchDocumentFieldType::FIELD_TITLE, ); $this->ferretTables = $table_map; return $this; } protected function buildFerretSelectClause(AphrontDatabaseConnection $conn) { $select = array(); if (!$this->supportsFerretEngine()) { return $select; } $vector = $this->getOrderVector(); if (!$vector->containsKey('rank')) { // We only need to SELECT the virtual "_ft_rank" column if we're // actually sorting the results by rank. return $select; } if (!$this->ferretEngine) { - $select[] = '0 _ft_rank'; + $select[] = qsprintf($conn, '0 _ft_rank'); return $select; } $engine = $this->ferretEngine; $stemmer = $engine->newStemmer(); $op_sub = PhutilSearchQueryCompiler::OPERATOR_SUBSTRING; $op_not = PhutilSearchQueryCompiler::OPERATOR_NOT; $table_alias = 'ft_rank'; $parts = array(); foreach ($this->ferretTokens as $fulltext_token) { $raw_token = $fulltext_token->getToken(); $value = $raw_token->getValue(); if ($raw_token->getOperator() == $op_not) { // Ignore "not" terms when ranking, since they aren't useful. continue; } if ($raw_token->getOperator() == $op_sub) { $is_substring = true; } else { $is_substring = false; } if ($is_substring) { $parts[] = qsprintf( $conn, 'IF(%T.rawCorpus LIKE %~, 2, 0)', $table_alias, $value); continue; } if ($raw_token->isQuoted()) { $is_quoted = true; $is_stemmed = false; } else { $is_quoted = false; $is_stemmed = true; } $term_constraints = array(); $term_value = $engine->newTermsCorpus($value); $parts[] = qsprintf( $conn, 'IF(%T.termCorpus LIKE %~, 2, 0)', $table_alias, $term_value); if ($is_stemmed) { $stem_value = $stemmer->stemToken($value); $stem_value = $engine->newTermsCorpus($stem_value); $parts[] = qsprintf( $conn, 'IF(%T.normalCorpus LIKE %~, 1, 0)', $table_alias, $stem_value); } } - $parts[] = '0'; + $parts[] = qsprintf($conn, '%d', 0); + + $sum = array_shift($parts); + foreach ($parts as $part) { + $sum = qsprintf( + $conn, + '%Q + %Q', + $sum, + $part); + } $select[] = qsprintf( $conn, '%Q _ft_rank', - implode(' + ', $parts)); + $sum); return $select; } protected function buildFerretJoinClause(AphrontDatabaseConnection $conn) { if (!$this->ferretEngine) { return array(); } $op_sub = PhutilSearchQueryCompiler::OPERATOR_SUBSTRING; $op_not = PhutilSearchQueryCompiler::OPERATOR_NOT; $engine = $this->ferretEngine; $stemmer = $engine->newStemmer(); $ngram_table = $engine->getNgramsTableName(); $flat = array(); foreach ($this->ferretTokens as $fulltext_token) { $raw_token = $fulltext_token->getToken(); // If this is a negated term like "-pomegranate", don't join the ngram // table since we aren't looking for documents with this term. (We could // LEFT JOIN the table and require a NULL row, but this is probably more // trouble than it's worth.) if ($raw_token->getOperator() == $op_not) { continue; } $value = $raw_token->getValue(); $length = count(phutil_utf8v($value)); if ($raw_token->getOperator() == $op_sub) { $is_substring = true; } else { $is_substring = false; } // If the user specified a substring query for a substring which is // shorter than the ngram length, we can't use the ngram index, so // don't do a join. We'll fall back to just doing LIKE on the full // corpus. if ($is_substring) { if ($length < 3) { continue; } } if ($raw_token->isQuoted()) { $is_stemmed = false; } else { $is_stemmed = true; } if ($is_substring) { $ngrams = $engine->getSubstringNgramsFromString($value); } else { $terms_value = $engine->newTermsCorpus($value); $ngrams = $engine->getTermNgramsFromString($terms_value); // If this is a stemmed term, only look for ngrams present in both the // unstemmed and stemmed variations. if ($is_stemmed) { // Trim the boundary space characters so the stemmer recognizes this // is (or, at least, may be) a normal word and activates. $terms_value = trim($terms_value, ' '); $stem_value = $stemmer->stemToken($terms_value); $stem_ngrams = $engine->getTermNgramsFromString($stem_value); $ngrams = array_intersect($ngrams, $stem_ngrams); } } foreach ($ngrams as $ngram) { $flat[] = array( 'table' => $ngram_table, 'ngram' => $ngram, ); } } // Remove common ngrams, like "the", which occur too frequently in // documents to be useful in constraining the query. The best ngrams // are obscure sequences which occur in very few documents. if ($flat) { $common_ngrams = queryfx_all( $conn, 'SELECT ngram FROM %T WHERE ngram IN (%Ls)', $engine->getCommonNgramsTableName(), ipull($flat, 'ngram')); $common_ngrams = ipull($common_ngrams, 'ngram', 'ngram'); foreach ($flat as $key => $spec) { $ngram = $spec['ngram']; if (isset($common_ngrams[$ngram])) { unset($flat[$key]); continue; } // NOTE: MySQL discards trailing whitespace in CHAR(X) columns. $trim_ngram = rtrim($ngram, ' '); if (isset($common_ngrams[$trim_ngram])) { unset($flat[$key]); continue; } } } // MySQL only allows us to join a maximum of 61 tables per query. Each // ngram is going to cost us a join toward that limit, so if the user // specified a very long query string, just pick 16 of the ngrams // at random. if (count($flat) > 16) { shuffle($flat); $flat = array_slice($flat, 0, 16); } $alias = $this->getPrimaryTableAlias(); if ($alias) { $phid_column = qsprintf($conn, '%T.%T', $alias, 'phid'); } else { $phid_column = qsprintf($conn, '%T', 'phid'); } $document_table = $engine->getDocumentTableName(); $field_table = $engine->getFieldTableName(); $joins = array(); $joins[] = qsprintf( $conn, 'JOIN %T ft_doc ON ft_doc.objectPHID = %Q', $document_table, $phid_column); $idx = 1; foreach ($flat as $spec) { $table = $spec['table']; $ngram = $spec['ngram']; $alias = 'ftngram_'.$idx++; $joins[] = qsprintf( $conn, 'JOIN %T %T ON %T.documentID = ft_doc.id AND %T.ngram = %s', $table, $alias, $alias, $alias, $ngram); } foreach ($this->ferretTables as $table) { $alias = $table['alias']; $joins[] = qsprintf( $conn, 'JOIN %T %T ON ft_doc.id = %T.documentID AND %T.fieldKey = %s', $field_table, $alias, $alias, $alias, $table['key']); } return $joins; } protected function buildFerretWhereClause(AphrontDatabaseConnection $conn) { if (!$this->ferretEngine) { return array(); } $engine = $this->ferretEngine; $stemmer = $engine->newStemmer(); $table_map = $this->ferretTables; $op_sub = PhutilSearchQueryCompiler::OPERATOR_SUBSTRING; $op_not = PhutilSearchQueryCompiler::OPERATOR_NOT; $op_exact = PhutilSearchQueryCompiler::OPERATOR_EXACT; $where = array(); $current_function = 'all'; foreach ($this->ferretTokens as $fulltext_token) { $raw_token = $fulltext_token->getToken(); $value = $raw_token->getValue(); $function = $raw_token->getFunction(); if ($function === null) { $function = $current_function; } $current_function = $function; $table_alias = $table_map[$function]['alias']; $is_not = ($raw_token->getOperator() == $op_not); if ($raw_token->getOperator() == $op_sub) { $is_substring = true; } else { $is_substring = false; } // If we're doing exact search, just test the raw corpus. $is_exact = ($raw_token->getOperator() == $op_exact); if ($is_exact) { if ($is_not) { $where[] = qsprintf( $conn, '(%T.rawCorpus != %s)', $table_alias, $value); } else { $where[] = qsprintf( $conn, '(%T.rawCorpus = %s)', $table_alias, $value); } continue; } // If we're doing substring search, we just match against the raw corpus // and we're done. if ($is_substring) { if ($is_not) { $where[] = qsprintf( $conn, '(%T.rawCorpus NOT LIKE %~)', $table_alias, $value); } else { $where[] = qsprintf( $conn, '(%T.rawCorpus LIKE %~)', $table_alias, $value); } continue; } // Otherwise, we need to match against the term corpus and the normal // corpus, so that searching for "raw" does not find "strawberry". if ($raw_token->isQuoted()) { $is_quoted = true; $is_stemmed = false; } else { $is_quoted = false; $is_stemmed = true; } // Never stem negated queries, since this can exclude results users // did not mean to exclude and generally confuse things. if ($is_not) { $is_stemmed = false; } $term_constraints = array(); $term_value = $engine->newTermsCorpus($value); if ($is_not) { $term_constraints[] = qsprintf( $conn, '(%T.termCorpus NOT LIKE %~)', $table_alias, $term_value); } else { $term_constraints[] = qsprintf( $conn, '(%T.termCorpus LIKE %~)', $table_alias, $term_value); } if ($is_stemmed) { $stem_value = $stemmer->stemToken($value); $stem_value = $engine->newTermsCorpus($stem_value); $term_constraints[] = qsprintf( $conn, '(%T.normalCorpus LIKE %~)', $table_alias, $stem_value); } if ($is_not) { $where[] = qsprintf( $conn, - '(%Q)', - implode(' AND ', $term_constraints)); + '%LA', + $term_constraints); } else if ($is_quoted) { $where[] = qsprintf( $conn, - '(%T.rawCorpus LIKE %~ AND (%Q))', + '(%T.rawCorpus LIKE %~ AND %LO)', $table_alias, $value, - implode(' OR ', $term_constraints)); + $term_constraints); } else { $where[] = qsprintf( $conn, - '(%Q)', - implode(' OR ', $term_constraints)); + '%LO', + $term_constraints); } } if ($this->ferretQuery) { $query = $this->ferretQuery; $author_phids = $query->getParameter('authorPHIDs'); if ($author_phids) { $where[] = qsprintf( $conn, 'ft_doc.authorPHID IN (%Ls)', $author_phids); } $with_unowned = $query->getParameter('withUnowned'); $with_any = $query->getParameter('withAnyOwner'); if ($with_any && $with_unowned) { throw new PhabricatorEmptyQueryException( pht( 'This query matches only unowned documents owned by anyone, '. 'which is impossible.')); } $owner_phids = $query->getParameter('ownerPHIDs'); if ($owner_phids && !$with_any) { if ($with_unowned) { $where[] = qsprintf( $conn, 'ft_doc.ownerPHID IN (%Ls) OR ft_doc.ownerPHID IS NULL', $owner_phids); } else { $where[] = qsprintf( $conn, 'ft_doc.ownerPHID IN (%Ls)', $owner_phids); } } else if ($with_unowned) { $where[] = qsprintf( $conn, 'ft_doc.ownerPHID IS NULL'); } if ($with_any) { $where[] = qsprintf( $conn, 'ft_doc.ownerPHID IS NOT NULL'); } $rel_open = PhabricatorSearchRelationship::RELATIONSHIP_OPEN; $statuses = $query->getParameter('statuses'); $is_closed = null; if ($statuses) { $statuses = array_fuse($statuses); if (count($statuses) == 1) { if (isset($statuses[$rel_open])) { $is_closed = 0; } else { $is_closed = 1; } } } if ($is_closed !== null) { $where[] = qsprintf( $conn, 'ft_doc.isClosed = %d', $is_closed); } } return $where; } protected function shouldGroupFerretResultRows() { return (bool)$this->ferretTokens; } /* -( Ngrams )------------------------------------------------------------- */ protected function withNgramsConstraint( PhabricatorSearchNgrams $index, $value) { if (strlen($value)) { $this->ngrams[] = array( 'index' => $index, 'value' => $value, 'length' => count(phutil_utf8v($value)), ); } return $this; } protected function buildNgramsJoinClause(AphrontDatabaseConnection $conn) { $flat = array(); foreach ($this->ngrams as $spec) { $index = $spec['index']; $value = $spec['value']; $length = $spec['length']; if ($length >= 3) { $ngrams = $index->getNgramsFromString($value, 'query'); $prefix = false; } else if ($length == 2) { $ngrams = $index->getNgramsFromString($value, 'prefix'); $prefix = false; } else { $ngrams = array(' '.$value); $prefix = true; } foreach ($ngrams as $ngram) { $flat[] = array( 'table' => $index->getTableName(), 'ngram' => $ngram, 'prefix' => $prefix, ); } } // MySQL only allows us to join a maximum of 61 tables per query. Each // ngram is going to cost us a join toward that limit, so if the user // specified a very long query string, just pick 16 of the ngrams // at random. if (count($flat) > 16) { shuffle($flat); $flat = array_slice($flat, 0, 16); } $alias = $this->getPrimaryTableAlias(); if ($alias) { $id_column = qsprintf($conn, '%T.%T', $alias, 'id'); } else { $id_column = qsprintf($conn, '%T', 'id'); } $idx = 1; $joins = array(); foreach ($flat as $spec) { $table = $spec['table']; $ngram = $spec['ngram']; $prefix = $spec['prefix']; $alias = 'ngm'.$idx++; if ($prefix) { $joins[] = qsprintf( $conn, 'JOIN %T %T ON %T.objectID = %Q AND %T.ngram LIKE %>', $table, $alias, $alias, $id_column, $alias, $ngram); } else { $joins[] = qsprintf( $conn, 'JOIN %T %T ON %T.objectID = %Q AND %T.ngram = %s', $table, $alias, $alias, $id_column, $alias, $ngram); } } return $joins; } protected function buildNgramsWhereClause(AphrontDatabaseConnection $conn) { $where = array(); foreach ($this->ngrams as $ngram) { $index = $ngram['index']; $value = $ngram['value']; $column = $index->getColumnName(); $alias = $this->getPrimaryTableAlias(); if ($alias) { $column = qsprintf($conn, '%T.%T', $alias, $column); } else { $column = qsprintf($conn, '%T', $column); } $tokens = $index->tokenizeString($value); foreach ($tokens as $token) { $where[] = qsprintf( $conn, '%Q LIKE %~', $column, $token); } } return $where; } protected function shouldGroupNgramResultRows() { return (bool)$this->ngrams; } /* -( Edge Logic )--------------------------------------------------------- */ /** * Convenience method for specifying edge logic constraints with a list of * PHIDs. * * @param const Edge constant. * @param const Constraint operator. * @param list List of PHIDs. * @return this * @task edgelogic */ public function withEdgeLogicPHIDs($edge_type, $operator, array $phids) { $constraints = array(); foreach ($phids as $phid) { $constraints[] = new PhabricatorQueryConstraint($operator, $phid); } return $this->withEdgeLogicConstraints($edge_type, $constraints); } /** * @return this * @task edgelogic */ public function withEdgeLogicConstraints($edge_type, array $constraints) { assert_instances_of($constraints, 'PhabricatorQueryConstraint'); $constraints = mgroup($constraints, 'getOperator'); foreach ($constraints as $operator => $list) { foreach ($list as $item) { $this->edgeLogicConstraints[$edge_type][$operator][] = $item; } } $this->edgeLogicConstraintsAreValid = false; return $this; } /** * @task edgelogic */ public function buildEdgeLogicSelectClause(AphrontDatabaseConnection $conn) { $select = array(); $this->validateEdgeLogicConstraints(); foreach ($this->edgeLogicConstraints as $type => $constraints) { foreach ($constraints as $operator => $list) { $alias = $this->getEdgeLogicTableAlias($operator, $type); switch ($operator) { case PhabricatorQueryConstraint::OPERATOR_AND: if (count($list) > 1) { $select[] = qsprintf( $conn, 'COUNT(DISTINCT(%T.dst)) %T', $alias, $this->buildEdgeLogicTableAliasCount($alias)); } break; case PhabricatorQueryConstraint::OPERATOR_ANCESTOR: // This is tricky. We have a query which specifies multiple // projects, each of which may have an arbitrarily large number // of descendants. // Suppose the projects are "Engineering" and "Operations", and // "Engineering" has subprojects X, Y and Z. // We first use `FIELD(dst, X, Y, Z)` to produce a 0 if a row // is not part of Engineering at all, or some number other than // 0 if it is. // Then we use `IF(..., idx, NULL)` to convert the 0 to a NULL and // any other value to an index (say, 1) for the ancestor. // We build these up for every ancestor, then use `COALESCE(...)` // to select the non-null one, giving us an ancestor which this // row is a member of. // From there, we use `COUNT(DISTINCT(...))` to make sure that // each result row is a member of all ancestors. if (count($list) > 1) { $idx = 1; $parts = array(); foreach ($list as $constraint) { $parts[] = qsprintf( $conn, 'IF(FIELD(%T.dst, %Ls) != 0, %d, NULL)', $alias, (array)$constraint->getValue(), $idx++); } - $parts = implode(', ', $parts); + $parts = qsprintf($conn, '%LQ', $parts); $select[] = qsprintf( $conn, 'COUNT(DISTINCT(COALESCE(%Q))) %T', $parts, $this->buildEdgeLogicTableAliasAncestor($alias)); } break; default: break; } } } return $select; } /** * @task edgelogic */ public function buildEdgeLogicJoinClause(AphrontDatabaseConnection $conn) { $edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE; - $phid_column = $this->getApplicationSearchObjectPHIDColumn(); + $phid_column = $this->getApplicationSearchObjectPHIDColumn($conn); $joins = array(); foreach ($this->edgeLogicConstraints as $type => $constraints) { $op_null = PhabricatorQueryConstraint::OPERATOR_NULL; $has_null = isset($constraints[$op_null]); // If we're going to process an only() operator, build a list of the // acceptable set of PHIDs first. We'll only match results which have // no edges to any other PHIDs. $all_phids = array(); if (isset($constraints[PhabricatorQueryConstraint::OPERATOR_ONLY])) { foreach ($constraints as $operator => $list) { switch ($operator) { case PhabricatorQueryConstraint::OPERATOR_ANCESTOR: case PhabricatorQueryConstraint::OPERATOR_AND: case PhabricatorQueryConstraint::OPERATOR_OR: foreach ($list as $constraint) { $value = (array)$constraint->getValue(); foreach ($value as $v) { $all_phids[$v] = $v; } } break; } } } foreach ($constraints as $operator => $list) { $alias = $this->getEdgeLogicTableAlias($operator, $type); $phids = array(); foreach ($list as $constraint) { $value = (array)$constraint->getValue(); foreach ($value as $v) { $phids[$v] = $v; } } $phids = array_keys($phids); switch ($operator) { case PhabricatorQueryConstraint::OPERATOR_NOT: $joins[] = qsprintf( $conn, 'LEFT JOIN %T %T ON %Q = %T.src AND %T.type = %d AND %T.dst IN (%Ls)', $edge_table, $alias, $phid_column, $alias, $alias, $type, $alias, $phids); break; case PhabricatorQueryConstraint::OPERATOR_ANCESTOR: case PhabricatorQueryConstraint::OPERATOR_AND: case PhabricatorQueryConstraint::OPERATOR_OR: // If we're including results with no matches, we have to degrade // this to a LEFT join. We'll use WHERE to select matching rows // later. if ($has_null) { - $join_type = 'LEFT'; + $join_type = qsprintf($conn, 'LEFT'); } else { - $join_type = ''; + $join_type = qsprintf($conn, ''); } $joins[] = qsprintf( $conn, '%Q JOIN %T %T ON %Q = %T.src AND %T.type = %d AND %T.dst IN (%Ls)', $join_type, $edge_table, $alias, $phid_column, $alias, $alias, $type, $alias, $phids); break; case PhabricatorQueryConstraint::OPERATOR_NULL: $joins[] = qsprintf( $conn, 'LEFT JOIN %T %T ON %Q = %T.src AND %T.type = %d', $edge_table, $alias, $phid_column, $alias, $alias, $type); break; case PhabricatorQueryConstraint::OPERATOR_ONLY: $joins[] = qsprintf( $conn, 'LEFT JOIN %T %T ON %Q = %T.src AND %T.type = %d AND %T.dst NOT IN (%Ls)', $edge_table, $alias, $phid_column, $alias, $alias, $type, $alias, $all_phids); break; } } } return $joins; } /** * @task edgelogic */ public function buildEdgeLogicWhereClause(AphrontDatabaseConnection $conn) { $where = array(); foreach ($this->edgeLogicConstraints as $type => $constraints) { $full = array(); $null = array(); $op_null = PhabricatorQueryConstraint::OPERATOR_NULL; $has_null = isset($constraints[$op_null]); foreach ($constraints as $operator => $list) { $alias = $this->getEdgeLogicTableAlias($operator, $type); switch ($operator) { case PhabricatorQueryConstraint::OPERATOR_NOT: case PhabricatorQueryConstraint::OPERATOR_ONLY: $full[] = qsprintf( $conn, '%T.dst IS NULL', $alias); break; case PhabricatorQueryConstraint::OPERATOR_AND: case PhabricatorQueryConstraint::OPERATOR_OR: if ($has_null) { $full[] = qsprintf( $conn, '%T.dst IS NOT NULL', $alias); } break; case PhabricatorQueryConstraint::OPERATOR_NULL: $null[] = qsprintf( $conn, '%T.dst IS NULL', $alias); break; } } if ($full && $null) { - $full = $this->formatWhereSubclause($full); - $null = $this->formatWhereSubclause($null); - $where[] = qsprintf($conn, '(%Q OR %Q)', $full, $null); + $where[] = qsprintf($conn, '(%LA OR %LA)', $full, $null); } else if ($full) { foreach ($full as $condition) { $where[] = $condition; } } else if ($null) { foreach ($null as $condition) { $where[] = $condition; } } } return $where; } /** * @task edgelogic */ public function buildEdgeLogicHavingClause(AphrontDatabaseConnection $conn) { $having = array(); foreach ($this->edgeLogicConstraints as $type => $constraints) { foreach ($constraints as $operator => $list) { $alias = $this->getEdgeLogicTableAlias($operator, $type); switch ($operator) { case PhabricatorQueryConstraint::OPERATOR_AND: if (count($list) > 1) { $having[] = qsprintf( $conn, '%T = %d', $this->buildEdgeLogicTableAliasCount($alias), count($list)); } break; case PhabricatorQueryConstraint::OPERATOR_ANCESTOR: if (count($list) > 1) { $having[] = qsprintf( $conn, '%T = %d', $this->buildEdgeLogicTableAliasAncestor($alias), count($list)); } break; } } } return $having; } /** * @task edgelogic */ public function shouldGroupEdgeLogicResultRows() { foreach ($this->edgeLogicConstraints as $type => $constraints) { foreach ($constraints as $operator => $list) { switch ($operator) { case PhabricatorQueryConstraint::OPERATOR_NOT: case PhabricatorQueryConstraint::OPERATOR_AND: case PhabricatorQueryConstraint::OPERATOR_OR: if (count($list) > 1) { return true; } break; case PhabricatorQueryConstraint::OPERATOR_ANCESTOR: // NOTE: We must always group query results rows when using an // "ANCESTOR" operator because a single task may be related to // two different descendants of a particular ancestor. For // discussion, see T12753. return true; case PhabricatorQueryConstraint::OPERATOR_NULL: case PhabricatorQueryConstraint::OPERATOR_ONLY: return true; } } } return false; } /** * @task edgelogic */ private function getEdgeLogicTableAlias($operator, $type) { return 'edgelogic_'.$operator.'_'.$type; } /** * @task edgelogic */ private function buildEdgeLogicTableAliasCount($alias) { return $alias.'_count'; } /** * @task edgelogic */ private function buildEdgeLogicTableAliasAncestor($alias) { return $alias.'_ancestor'; } /** * Select certain edge logic constraint values. * * @task edgelogic */ protected function getEdgeLogicValues( array $edge_types, array $operators) { $values = array(); $constraint_lists = $this->edgeLogicConstraints; if ($edge_types) { $constraint_lists = array_select_keys($constraint_lists, $edge_types); } foreach ($constraint_lists as $type => $constraints) { if ($operators) { $constraints = array_select_keys($constraints, $operators); } foreach ($constraints as $operator => $list) { foreach ($list as $constraint) { $value = (array)$constraint->getValue(); foreach ($value as $v) { $values[] = $v; } } } } return $values; } /** * Validate edge logic constraints for the query. * * @return this * @task edgelogic */ private function validateEdgeLogicConstraints() { if ($this->edgeLogicConstraintsAreValid) { return $this; } foreach ($this->edgeLogicConstraints as $type => $constraints) { foreach ($constraints as $operator => $list) { switch ($operator) { case PhabricatorQueryConstraint::OPERATOR_EMPTY: throw new PhabricatorEmptyQueryException( pht('This query specifies an empty constraint.')); } } } // This should probably be more modular, eventually, but we only do // project-based edge logic today. $project_phids = $this->getEdgeLogicValues( array( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, ), array( PhabricatorQueryConstraint::OPERATOR_AND, PhabricatorQueryConstraint::OPERATOR_OR, PhabricatorQueryConstraint::OPERATOR_NOT, PhabricatorQueryConstraint::OPERATOR_ANCESTOR, )); if ($project_phids) { $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withPHIDs($project_phids) ->execute(); $projects = mpull($projects, null, 'getPHID'); foreach ($project_phids as $phid) { if (empty($projects[$phid])) { throw new PhabricatorEmptyQueryException( pht( 'This query is constrained by a project you do not have '. 'permission to see.')); } } } $op_and = PhabricatorQueryConstraint::OPERATOR_AND; $op_or = PhabricatorQueryConstraint::OPERATOR_OR; $op_ancestor = PhabricatorQueryConstraint::OPERATOR_ANCESTOR; foreach ($this->edgeLogicConstraints as $type => $constraints) { foreach ($constraints as $operator => $list) { switch ($operator) { case PhabricatorQueryConstraint::OPERATOR_ONLY: if (count($list) > 1) { throw new PhabricatorEmptyQueryException( pht( 'This query specifies only() more than once.')); } $have_and = idx($constraints, $op_and); $have_or = idx($constraints, $op_or); $have_ancestor = idx($constraints, $op_ancestor); if (!$have_and && !$have_or && !$have_ancestor) { throw new PhabricatorEmptyQueryException( pht( 'This query specifies only(), but no other constraints '. 'which it can apply to.')); } break; } } } $this->edgeLogicConstraintsAreValid = true; return $this; } /* -( Spaces )------------------------------------------------------------- */ /** * Constrain the query to return results from only specific Spaces. * * Pass a list of Space PHIDs, or `null` to represent the default space. Only * results in those Spaces will be returned. * * Queries are always constrained to include only results from spaces the * viewer has access to. * * @param list * @task spaces */ public function withSpacePHIDs(array $space_phids) { $object = $this->newResultObject(); if (!$object) { throw new Exception( pht( 'This query (of class "%s") does not implement newResultObject(), '. 'but must implement this method to enable support for Spaces.', get_class($this))); } if (!($object instanceof PhabricatorSpacesInterface)) { throw new Exception( pht( 'This query (of class "%s") returned an object of class "%s" from '. 'getNewResultObject(), but it does not implement the required '. 'interface ("%s"). Objects must implement this interface to enable '. 'Spaces support.', get_class($this), get_class($object), 'PhabricatorSpacesInterface')); } $this->spacePHIDs = $space_phids; return $this; } public function withSpaceIsArchived($archived) { $this->spaceIsArchived = $archived; return $this; } /** * Constrain the query to include only results in valid Spaces. * * This method builds part of a WHERE clause which considers the spaces the * viewer has access to see with any explicit constraint on spaces added by * @{method:withSpacePHIDs}. * * @param AphrontDatabaseConnection Database connection. * @return string Part of a WHERE clause. * @task spaces */ private function buildSpacesWhereClause(AphrontDatabaseConnection $conn) { $object = $this->newResultObject(); if (!$object) { return null; } if (!($object instanceof PhabricatorSpacesInterface)) { return null; } $viewer = $this->getViewer(); // If we have an omnipotent viewer and no formal space constraints, don't // emit a clause. This primarily enables older migrations to run cleanly, // without fataling because they try to match a `spacePHID` column which // does not exist yet. See T8743, T8746. if ($viewer->isOmnipotent()) { if ($this->spaceIsArchived === null && $this->spacePHIDs === null) { return null; } } $space_phids = array(); $include_null = false; $all = PhabricatorSpacesNamespaceQuery::getAllSpaces(); if (!$all) { // If there are no spaces at all, implicitly give the viewer access to // the default space. $include_null = true; } else { // Otherwise, give them access to the spaces they have permission to // see. $viewer_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces( $viewer); foreach ($viewer_spaces as $viewer_space) { if ($this->spaceIsArchived !== null) { if ($viewer_space->getIsArchived() != $this->spaceIsArchived) { continue; } } $phid = $viewer_space->getPHID(); $space_phids[$phid] = $phid; if ($viewer_space->getIsDefaultNamespace()) { $include_null = true; } } } // If we have additional explicit constraints, evaluate them now. if ($this->spacePHIDs !== null) { $explicit = array(); $explicit_null = false; foreach ($this->spacePHIDs as $phid) { if ($phid === null) { $space = PhabricatorSpacesNamespaceQuery::getDefaultSpace(); } else { $space = idx($all, $phid); } if ($space) { $phid = $space->getPHID(); $explicit[$phid] = $phid; if ($space->getIsDefaultNamespace()) { $explicit_null = true; } } } // If the viewer can see the default space but it isn't on the explicit // list of spaces to query, don't match it. if ($include_null && !$explicit_null) { $include_null = false; } // Include only the spaces common to the viewer and the constraints. $space_phids = array_intersect_key($space_phids, $explicit); } if (!$space_phids && !$include_null) { if ($this->spacePHIDs === null) { throw new PhabricatorEmptyQueryException( pht('You do not have access to any spaces.')); } else { throw new PhabricatorEmptyQueryException( pht( 'You do not have access to any of the spaces this query '. 'is constrained to.')); } } $alias = $this->getPrimaryTableAlias(); if ($alias) { $col = qsprintf($conn, '%T.spacePHID', $alias); } else { - $col = 'spacePHID'; + $col = qsprintf($conn, 'spacePHID'); } if ($space_phids && $include_null) { return qsprintf( $conn, '(%Q IN (%Ls) OR %Q IS NULL)', $col, $space_phids, $col); } else if ($space_phids) { return qsprintf( $conn, '%Q IN (%Ls)', $col, $space_phids); } else { return qsprintf( $conn, '%Q IS NULL', $col); } } } diff --git a/src/infrastructure/storage/__tests__/QueryFormattingTestCase.php b/src/infrastructure/storage/__tests__/QueryFormattingTestCase.php index 53d16a7948..cff7390949 100644 --- a/src/infrastructure/storage/__tests__/QueryFormattingTestCase.php +++ b/src/infrastructure/storage/__tests__/QueryFormattingTestCase.php @@ -1,69 +1,69 @@ establishConnection('r'); $this->assertEqual( 'NULL', - qsprintf($conn, '%nd', null)); + (string)qsprintf($conn, '%nd', null)); $this->assertEqual( '0', - qsprintf($conn, '%nd', 0)); + (string)qsprintf($conn, '%nd', 0)); $this->assertEqual( '0', - qsprintf($conn, '%d', 0)); + (string)qsprintf($conn, '%d', 0)); $raised = null; try { qsprintf($conn, '%d', 'derp'); } catch (Exception $ex) { $raised = $ex; } $this->assertTrue( (bool)$raised, pht('%s should raise exception for invalid %%d conversion.', 'qsprintf')); $this->assertEqual( "''", - qsprintf($conn, '%s', null)); + (string)qsprintf($conn, '%s', null)); $this->assertEqual( 'NULL', - qsprintf($conn, '%ns', null)); + (string)qsprintf($conn, '%ns', null)); $this->assertEqual( "'', ''", - qsprintf($conn, '%Ls', array('x', 'y'))); + (string)qsprintf($conn, '%Ls', array('x', 'y'))); $this->assertEqual( "''", - qsprintf($conn, '%B', null)); + (string)qsprintf($conn, '%B', null)); $this->assertEqual( 'NULL', - qsprintf($conn, '%nB', null)); + (string)qsprintf($conn, '%nB', null)); $this->assertEqual( "'', ''", - qsprintf($conn, '%LB', array('x', 'y'))); + (string)qsprintf($conn, '%LB', array('x', 'y'))); $this->assertEqual( '', - qsprintf($conn, '%T', 'x')); + (string)qsprintf($conn, '%T', 'x')); $this->assertEqual( '', - qsprintf($conn, '%C', 'y')); + (string)qsprintf($conn, '%C', 'y')); $this->assertEqual( '.', - qsprintf($conn, '%R', new AphrontDatabaseTableRef('x', 'y'))); + (string)qsprintf($conn, '%R', new AphrontDatabaseTableRef('x', 'y'))); } } diff --git a/src/infrastructure/storage/lisk/LiskDAO.php b/src/infrastructure/storage/lisk/LiskDAO.php index 583d8226f6..aa12f8e614 100644 --- a/src/infrastructure/storage/lisk/LiskDAO.php +++ b/src/infrastructure/storage/lisk/LiskDAO.php @@ -1,2039 +1,2067 @@ setName('Sawyer') * ->setBreed('Pug') * ->save(); * * Note that **Lisk automatically builds getters and setters for all of your * object's protected properties** via @{method:__call}. If you want to add * custom behavior to your getters or setters, you can do so by overriding the * @{method:readField} and @{method:writeField} methods. * * Calling @{method:save} will persist the object to the database. After calling * @{method:save}, you can call @{method:getID} to retrieve the object's ID. * * To load objects by ID, use the @{method:load} method: * * $dog = id(new Dog())->load($id); * * This will load the Dog record with ID $id into $dog, or `null` if no such * record exists (@{method:load} is an instance method rather than a static * method because PHP does not support late static binding, at least until PHP * 5.3). * * To update an object, change its properties and save it: * * $dog->setBreed('Lab')->save(); * * To delete an object, call @{method:delete}: * * $dog->delete(); * * That's Lisk CRUD in a nutshell. * * = Queries = * * Often, you want to load a bunch of objects, or execute a more specialized * query. Use @{method:loadAllWhere} or @{method:loadOneWhere} to do this: * * $pugs = $dog->loadAllWhere('breed = %s', 'Pug'); * $sawyer = $dog->loadOneWhere('name = %s', 'Sawyer'); * * These methods work like @{function@libphutil:queryfx}, but only take half of * a query (the part after the WHERE keyword). Lisk will handle the connection, * columns, and object construction; you are responsible for the rest of it. * @{method:loadAllWhere} returns a list of objects, while * @{method:loadOneWhere} returns a single object (or `null`). * * There's also a @{method:loadRelatives} method which helps to prevent the 1+N * queries problem. * * = Managing Transactions = * * Lisk uses a transaction stack, so code does not generally need to be aware * of the transactional state of objects to implement correct transaction * semantics: * * $obj->openTransaction(); * $obj->save(); * $other->save(); * // ... * $other->openTransaction(); * $other->save(); * $another->save(); * if ($some_condition) { * $other->saveTransaction(); * } else { * $other->killTransaction(); * } * // ... * $obj->saveTransaction(); * * Assuming ##$obj##, ##$other## and ##$another## live on the same database, * this code will work correctly by establishing savepoints. * * Selects whose data are used later in the transaction should be included in * @{method:beginReadLocking} or @{method:beginWriteLocking} block. * * @task conn Managing Connections * @task config Configuring Lisk * @task load Loading Objects * @task info Examining Objects * @task save Writing Objects * @task hook Hooks and Callbacks * @task util Utilities * @task xaction Managing Transactions * @task isolate Isolation for Unit Testing */ abstract class LiskDAO extends Phobject implements AphrontDatabaseTableRefInterface { const CONFIG_IDS = 'id-mechanism'; const CONFIG_TIMESTAMPS = 'timestamps'; const CONFIG_AUX_PHID = 'auxiliary-phid'; const CONFIG_SERIALIZATION = 'col-serialization'; const CONFIG_BINARY = 'binary'; const CONFIG_COLUMN_SCHEMA = 'col-schema'; const CONFIG_KEY_SCHEMA = 'key-schema'; const CONFIG_NO_TABLE = 'no-table'; const CONFIG_NO_MUTATE = 'no-mutate'; const SERIALIZATION_NONE = 'id'; const SERIALIZATION_JSON = 'json'; const SERIALIZATION_PHP = 'php'; const IDS_AUTOINCREMENT = 'ids-auto'; const IDS_COUNTER = 'ids-counter'; const IDS_MANUAL = 'ids-manual'; const COUNTER_TABLE_NAME = 'lisk_counter'; private static $processIsolationLevel = 0; private static $transactionIsolationLevel = 0; private $ephemeral = false; private $forcedConnection; private static $connections = array(); private $inSet = null; protected $id; protected $phid; protected $dateCreated; protected $dateModified; /** * Build an empty object. * * @return obj Empty object. */ public function __construct() { $id_key = $this->getIDKey(); if ($id_key) { $this->$id_key = null; } } /* -( Managing Connections )----------------------------------------------- */ /** * Establish a live connection to a database service. This method should * return a new connection. Lisk handles connection caching and management; * do not perform caching deeper in the stack. * * @param string Mode, either 'r' (reading) or 'w' (reading and writing). * @return AphrontDatabaseConnection New database connection. * @task conn */ abstract protected function establishLiveConnection($mode); /** * Return a namespace for this object's connections in the connection cache. * Generally, the database name is appropriate. Two connections are considered * equivalent if they have the same connection namespace and mode. * * @return string Connection namespace for cache * @task conn */ protected function getConnectionNamespace() { return $this->getDatabaseName(); } abstract protected function getDatabaseName(); /** * Get an existing, cached connection for this object. * * @param mode Connection mode. * @return AphrontDatabaseConnection|null Connection, if it exists in cache. * @task conn */ protected function getEstablishedConnection($mode) { $key = $this->getConnectionNamespace().':'.$mode; if (isset(self::$connections[$key])) { return self::$connections[$key]; } return null; } /** * Store a connection in the connection cache. * * @param mode Connection mode. * @param AphrontDatabaseConnection Connection to cache. * @return this * @task conn */ protected function setEstablishedConnection( $mode, AphrontDatabaseConnection $connection, $force_unique = false) { $key = $this->getConnectionNamespace().':'.$mode; if ($force_unique) { $key .= ':unique'; while (isset(self::$connections[$key])) { $key .= '!'; } } self::$connections[$key] = $connection; return $this; } /** * Force an object to use a specific connection. * * This overrides all connection management and forces the object to use * a specific connection when interacting with the database. * * @param AphrontDatabaseConnection Connection to force this object to use. * @task conn */ public function setForcedConnection(AphrontDatabaseConnection $connection) { $this->forcedConnection = $connection; return $this; } /* -( Configuring Lisk )--------------------------------------------------- */ /** * Change Lisk behaviors, like ID configuration and timestamps. If you want * to change these behaviors, you should override this method in your child * class and change the options you're interested in. For example: * * protected function getConfiguration() { * return array( * Lisk_DataAccessObject::CONFIG_EXAMPLE => true, * ) + parent::getConfiguration(); * } * * The available options are: * * CONFIG_IDS * Lisk objects need to have a unique identifying ID. The three mechanisms * available for generating this ID are IDS_AUTOINCREMENT (default, assumes * the ID column is an autoincrement primary key), IDS_MANUAL (you are taking * full responsibility for ID management), or IDS_COUNTER (see below). * * InnoDB does not persist the value of `auto_increment` across restarts, * and instead initializes it to `MAX(id) + 1` during startup. This means it * may reissue the same autoincrement ID more than once, if the row is deleted * and then the database is restarted. To avoid this, you can set an object to * use a counter table with IDS_COUNTER. This will generally behave like * IDS_AUTOINCREMENT, except that the counter value will persist across * restarts and inserts will be slightly slower. If a database stores any * DAOs which use this mechanism, you must create a table there with this * schema: * * CREATE TABLE lisk_counter ( * counterName VARCHAR(64) COLLATE utf8_bin PRIMARY KEY, * counterValue BIGINT UNSIGNED NOT NULL * ) ENGINE=InnoDB DEFAULT CHARSET=utf8; * * CONFIG_TIMESTAMPS * Lisk can automatically handle keeping track of a `dateCreated' and * `dateModified' column, which it will update when it creates or modifies * an object. If you don't want to do this, you may disable this option. * By default, this option is ON. * * CONFIG_AUX_PHID * This option can be enabled by being set to some truthy value. The meaning * of this value is defined by your PHID generation mechanism. If this option * is enabled, a `phid' property will be populated with a unique PHID when an * object is created (or if it is saved and does not currently have one). You * need to override generatePHID() and hook it into your PHID generation * mechanism for this to work. By default, this option is OFF. * * CONFIG_SERIALIZATION * You can optionally provide a column serialization map that will be applied * to values when they are written to the database. For example: * * self::CONFIG_SERIALIZATION => array( * 'complex' => self::SERIALIZATION_JSON, * ) * * This will cause Lisk to JSON-serialize the 'complex' field before it is * written, and unserialize it when it is read. * * CONFIG_BINARY * You can optionally provide a map of columns to a flag indicating that * they store binary data. These columns will not raise an error when * handling binary writes. * * CONFIG_COLUMN_SCHEMA * Provide a map of columns to schema column types. * * CONFIG_KEY_SCHEMA * Provide a map of key names to key specifications. * * CONFIG_NO_TABLE * Allows you to specify that this object does not actually have a table in * the database. * * CONFIG_NO_MUTATE * Provide a map of columns which should not be included in UPDATE statements. * If you have some columns which are always written to explicitly and should * never be overwritten by a save(), you can specify them here. This is an * advanced, specialized feature and there are usually better approaches for * most locking/contention problems. * * @return dictionary Map of configuration options to values. * * @task config */ protected function getConfiguration() { return array( self::CONFIG_IDS => self::IDS_AUTOINCREMENT, self::CONFIG_TIMESTAMPS => true, ); } /** * Determine the setting of a configuration option for this class of objects. * * @param const Option name, one of the CONFIG_* constants. * @return mixed Option value, if configured (null if unavailable). * * @task config */ public function getConfigOption($option_name) { static $options = null; if (!isset($options)) { $options = $this->getConfiguration(); } return idx($options, $option_name); } /* -( Loading Objects )---------------------------------------------------- */ /** * Load an object by ID. You need to invoke this as an instance method, not * a class method, because PHP doesn't have late static binding (until * PHP 5.3.0). For example: * * $dog = id(new Dog())->load($dog_id); * * @param int Numeric ID identifying the object to load. * @return obj|null Identified object, or null if it does not exist. * * @task load */ public function load($id) { if (is_object($id)) { $id = (string)$id; } if (!$id || (!is_int($id) && !ctype_digit($id))) { return null; } return $this->loadOneWhere( '%C = %d', $this->getIDKeyForUse(), $id); } /** * Loads all of the objects, unconditionally. * * @return dict Dictionary of all persisted objects of this type, keyed * on object ID. * * @task load */ public function loadAll() { return $this->loadAllWhere('1 = 1'); } /** * Load all objects which match a WHERE clause. You provide everything after * the 'WHERE'; Lisk handles everything up to it. For example: * * $old_dogs = id(new Dog())->loadAllWhere('age > %d', 7); * * The pattern and arguments are as per queryfx(). * * @param string queryfx()-style SQL WHERE clause. * @param ... Zero or more conversions. * @return dict Dictionary of matching objects, keyed on ID. * * @task load */ public function loadAllWhere($pattern /* , $arg, $arg, $arg ... */) { $args = func_get_args(); $data = call_user_func_array( array($this, 'loadRawDataWhere'), $args); return $this->loadAllFromArray($data); } /** * Load a single object identified by a 'WHERE' clause. You provide * everything after the 'WHERE', and Lisk builds the first half of the * query. See loadAllWhere(). This method is similar, but returns a single * result instead of a list. * * @param string queryfx()-style SQL WHERE clause. * @param ... Zero or more conversions. * @return obj|null Matching object, or null if no object matches. * * @task load */ public function loadOneWhere($pattern /* , $arg, $arg, $arg ... */) { $args = func_get_args(); $data = call_user_func_array( array($this, 'loadRawDataWhere'), $args); if (count($data) > 1) { throw new AphrontCountQueryException( pht( 'More than one result from %s!', __FUNCTION__.'()')); } $data = reset($data); if (!$data) { return null; } return $this->loadFromArray($data); } protected function loadRawDataWhere($pattern /* , $args... */) { - $connection = $this->establishConnection('r'); + $conn = $this->establishConnection('r'); - $lock_clause = ''; - if ($connection->isReadLocking()) { - $lock_clause = 'FOR UPDATE'; - } else if ($connection->isWriteLocking()) { - $lock_clause = 'LOCK IN SHARE MODE'; + if ($conn->isReadLocking()) { + $lock_clause = qsprintf($conn, 'FOR UPDATE'); + } else if ($conn->isWriteLocking()) { + $lock_clause = qsprintf($conn, 'LOCK IN SHARE MODE'); + } else { + $lock_clause = qsprintf($conn, ''); } $args = func_get_args(); $args = array_slice($args, 1); $pattern = 'SELECT * FROM %R WHERE '.$pattern.' %Q'; array_unshift($args, $this); array_push($args, $lock_clause); array_unshift($args, $pattern); - return call_user_func_array( - array($connection, 'queryData'), - $args); + return call_user_func_array(array($conn, 'queryData'), $args); } /** * Reload an object from the database, discarding any changes to persistent * properties. This is primarily useful after entering a transaction but * before applying changes to an object. * * @return this * * @task load */ public function reload() { if (!$this->getID()) { throw new Exception( pht("Unable to reload object that hasn't been loaded!")); } $result = $this->loadOneWhere( '%C = %d', $this->getIDKeyForUse(), $this->getID()); if (!$result) { throw new AphrontObjectMissingQueryException(); } return $this; } /** * Initialize this object's properties from a dictionary. Generally, you * load single objects with loadOneWhere(), but sometimes it may be more * convenient to pull data from elsewhere directly (e.g., a complicated * join via @{method:queryData}) and then load from an array representation. * * @param dict Dictionary of properties, which should be equivalent to * selecting a row from the table or calling * @{method:getProperties}. * @return this * * @task load */ public function loadFromArray(array $row) { static $valid_properties = array(); $map = array(); foreach ($row as $k => $v) { // We permit (but ignore) extra properties in the array because a // common approach to building the array is to issue a raw SELECT query // which may include extra explicit columns or joins. // This pathway is very hot on some pages, so we're inlining a cache // and doing some microoptimization to avoid a strtolower() call for each // assignment. The common path (assigning a valid property which we've // already seen) always incurs only one empty(). The second most common // path (assigning an invalid property which we've already seen) costs // an empty() plus an isset(). if (empty($valid_properties[$k])) { if (isset($valid_properties[$k])) { // The value is set but empty, which means it's false, so we've // already determined it's not valid. We don't need to check again. continue; } $valid_properties[$k] = $this->hasProperty($k); if (!$valid_properties[$k]) { continue; } } $map[$k] = $v; } $this->willReadData($map); foreach ($map as $prop => $value) { $this->$prop = $value; } $this->didReadData(); return $this; } /** * Initialize a list of objects from a list of dictionaries. Usually you * load lists of objects with @{method:loadAllWhere}, but sometimes that * isn't flexible enough. One case is if you need to do joins to select the * right objects: * * function loadAllWithOwner($owner) { * $data = $this->queryData( * 'SELECT d.* * FROM owner o * JOIN owner_has_dog od ON o.id = od.ownerID * JOIN dog d ON od.dogID = d.id * WHERE o.id = %d', * $owner); * return $this->loadAllFromArray($data); * } * * This is a lot messier than @{method:loadAllWhere}, but more flexible. * * @param list List of property dictionaries. * @return dict List of constructed objects, keyed on ID. * * @task load */ public function loadAllFromArray(array $rows) { $result = array(); $id_key = $this->getIDKey(); foreach ($rows as $row) { $obj = clone $this; if ($id_key && isset($row[$id_key])) { $result[$row[$id_key]] = $obj->loadFromArray($row); } else { $result[] = $obj->loadFromArray($row); } if ($this->inSet) { $this->inSet->addToSet($obj); } } return $result; } /** * This method helps to prevent the 1+N queries problem. It happens when you * execute a query for each row in a result set. Like in this code: * * COUNTEREXAMPLE, name=Easy to write but expensive to execute * $diffs = id(new DifferentialDiff())->loadAllWhere( * 'revisionID = %d', * $revision->getID()); * foreach ($diffs as $diff) { * $changesets = id(new DifferentialChangeset())->loadAllWhere( * 'diffID = %d', * $diff->getID()); * // Do something with $changesets. * } * * One can solve this problem by reading all the dependent objects at once and * assigning them later: * * COUNTEREXAMPLE, name=Cheaper to execute but harder to write and maintain * $diffs = id(new DifferentialDiff())->loadAllWhere( * 'revisionID = %d', * $revision->getID()); * $all_changesets = id(new DifferentialChangeset())->loadAllWhere( * 'diffID IN (%Ld)', * mpull($diffs, 'getID')); * $all_changesets = mgroup($all_changesets, 'getDiffID'); * foreach ($diffs as $diff) { * $changesets = idx($all_changesets, $diff->getID(), array()); * // Do something with $changesets. * } * * The method @{method:loadRelatives} abstracts this approach which allows * writing a code which is simple and efficient at the same time: * * name=Easy to write and cheap to execute * $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID'); * foreach ($diffs as $diff) { * $changesets = $diff->loadRelatives( * new DifferentialChangeset(), * 'diffID'); * // Do something with $changesets. * } * * This will load dependent objects for all diffs in the first call of * @{method:loadRelatives} and use this result for all following calls. * * The method supports working with set of sets, like in this code: * * $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID'); * foreach ($diffs as $diff) { * $changesets = $diff->loadRelatives( * new DifferentialChangeset(), * 'diffID'); * foreach ($changesets as $changeset) { * $hunks = $changeset->loadRelatives( * new DifferentialHunk(), * 'changesetID'); * // Do something with hunks. * } * } * * This code will execute just three queries - one to load all diffs, one to * load all their related changesets and one to load all their related hunks. * You can try to write an equivalent code without using this method as * a homework. * * The method also supports retrieving referenced objects, for example authors * of all diffs (using shortcut @{method:loadOneRelative}): * * foreach ($diffs as $diff) { * $author = $diff->loadOneRelative( * new PhabricatorUser(), * 'phid', * 'getAuthorPHID'); * // Do something with author. * } * * It is also possible to specify additional conditions for the `WHERE` * clause. Similarly to @{method:loadAllWhere}, you can specify everything * after `WHERE` (except `LIMIT`). Contrary to @{method:loadAllWhere}, it is * allowed to pass only a constant string (`%` doesn't have a special * meaning). This is intentional to avoid mistakes with using data from one * row in retrieving other rows. Example of a correct usage: * * $status = $author->loadOneRelative( * new PhabricatorCalendarEvent(), * 'userPHID', * 'getPHID', * '(UNIX_TIMESTAMP() BETWEEN dateFrom AND dateTo)'); * * @param LiskDAO Type of objects to load. * @param string Name of the column in target table. * @param string Method name in this table. * @param string Additional constraints on returned rows. It supports no * placeholders and requires putting the WHERE part into * parentheses. It's not possible to use LIMIT. * @return list Objects of type $object. * * @task load */ public function loadRelatives( LiskDAO $object, $foreign_column, $key_method = 'getID', $where = '') { if (!$this->inSet) { id(new LiskDAOSet())->addToSet($this); } $relatives = $this->inSet->loadRelatives( $object, $foreign_column, $key_method, $where); return idx($relatives, $this->$key_method(), array()); } /** * Load referenced row. See @{method:loadRelatives} for details. * * @param LiskDAO Type of objects to load. * @param string Name of the column in target table. * @param string Method name in this table. * @param string Additional constraints on returned rows. It supports no * placeholders and requires putting the WHERE part into * parentheses. It's not possible to use LIMIT. * @return LiskDAO Object of type $object or null if there's no such object. * * @task load */ final public function loadOneRelative( LiskDAO $object, $foreign_column, $key_method = 'getID', $where = '') { $relatives = $this->loadRelatives( $object, $foreign_column, $key_method, $where); if (!$relatives) { return null; } if (count($relatives) > 1) { throw new AphrontCountQueryException( pht( 'More than one result from %s!', __FUNCTION__.'()')); } return reset($relatives); } final public function putInSet(LiskDAOSet $set) { $this->inSet = $set; return $this; } final protected function getInSet() { return $this->inSet; } /* -( Examining Objects )-------------------------------------------------- */ /** * Set unique ID identifying this object. You normally don't need to call this * method unless with `IDS_MANUAL`. * * @param mixed Unique ID. * @return this * @task save */ public function setID($id) { static $id_key = null; if ($id_key === null) { $id_key = $this->getIDKeyForUse(); } $this->$id_key = $id; return $this; } /** * Retrieve the unique ID identifying this object. This value will be null if * the object hasn't been persisted and you didn't set it manually. * * @return mixed Unique ID. * * @task info */ public function getID() { static $id_key = null; if ($id_key === null) { $id_key = $this->getIDKeyForUse(); } return $this->$id_key; } public function getPHID() { return $this->phid; } /** * Test if a property exists. * * @param string Property name. * @return bool True if the property exists. * @task info */ public function hasProperty($property) { return (bool)$this->checkProperty($property); } /** * Retrieve a list of all object properties. This list only includes * properties that are declared as protected, and it is expected that * all properties returned by this function should be persisted to the * database. * Properties that should not be persisted must be declared as private. * * @return dict Dictionary of normalized (lowercase) to canonical (original * case) property names. * * @task info */ protected function getAllLiskProperties() { static $properties = null; if (!isset($properties)) { $class = new ReflectionClass(get_class($this)); $properties = array(); foreach ($class->getProperties(ReflectionProperty::IS_PROTECTED) as $p) { $properties[strtolower($p->getName())] = $p->getName(); } $id_key = $this->getIDKey(); if ($id_key != 'id') { unset($properties['id']); } if (!$this->getConfigOption(self::CONFIG_TIMESTAMPS)) { unset($properties['datecreated']); unset($properties['datemodified']); } if ($id_key != 'phid' && !$this->getConfigOption(self::CONFIG_AUX_PHID)) { unset($properties['phid']); } } return $properties; } /** * Check if a property exists on this object. * * @return string|null Canonical property name, or null if the property * does not exist. * * @task info */ protected function checkProperty($property) { static $properties = null; if ($properties === null) { $properties = $this->getAllLiskProperties(); } $property = strtolower($property); if (empty($properties[$property])) { return null; } return $properties[$property]; } /** * Get or build the database connection for this object. * * @param string 'r' for read, 'w' for read/write. * @param bool True to force a new connection. The connection will not * be retrieved from or saved into the connection cache. * @return AphrontDatabaseConnection Lisk connection object. * * @task info */ public function establishConnection($mode, $force_new = false) { if ($mode != 'r' && $mode != 'w') { throw new Exception( pht( "Unknown mode '%s', should be 'r' or 'w'.", $mode)); } if ($this->forcedConnection) { return $this->forcedConnection; } if (self::shouldIsolateAllLiskEffectsToCurrentProcess()) { $mode = 'isolate-'.$mode; $connection = $this->getEstablishedConnection($mode); if (!$connection) { $connection = $this->establishIsolatedConnection($mode); $this->setEstablishedConnection($mode, $connection); } return $connection; } if (self::shouldIsolateAllLiskEffectsToTransactions()) { // If we're doing fixture transaction isolation, force the mode to 'w' // so we always get the same connection for reads and writes, and thus // can see the writes inside the transaction. $mode = 'w'; } // TODO: There is currently no protection on 'r' queries against writing. $connection = null; if (!$force_new) { if ($mode == 'r') { // If we're requesting a read connection but already have a write // connection, reuse the write connection so that reads can take place // inside transactions. $connection = $this->getEstablishedConnection('w'); } if (!$connection) { $connection = $this->getEstablishedConnection($mode); } } if (!$connection) { $connection = $this->establishLiveConnection($mode); if (self::shouldIsolateAllLiskEffectsToTransactions()) { $connection->openTransaction(); } $this->setEstablishedConnection( $mode, $connection, $force_unique = $force_new); } return $connection; } /** * Convert this object into a property dictionary. This dictionary can be * restored into an object by using @{method:loadFromArray} (unless you're * using legacy features with CONFIG_CONVERT_CAMELCASE, but in that case you * should just go ahead and die in a fire). * * @return dict Dictionary of object properties. * * @task info */ protected function getAllLiskPropertyValues() { $map = array(); foreach ($this->getAllLiskProperties() as $p) { // We may receive a warning here for properties we've implicitly added // through configuration; squelch it. $map[$p] = @$this->$p; } return $map; } /* -( Writing Objects )---------------------------------------------------- */ /** * Make an object read-only. * * Making an object ephemeral indicates that you will be changing state in * such a way that you would never ever want it to be written back to the * storage. */ public function makeEphemeral() { $this->ephemeral = true; return $this; } private function isEphemeralCheck() { if ($this->ephemeral) { throw new LiskEphemeralObjectException(); } } /** * Persist this object to the database. In most cases, this is the only * method you need to call to do writes. If the object has not yet been * inserted this will do an insert; if it has, it will do an update. * * @return this * * @task save */ public function save() { if ($this->shouldInsertWhenSaved()) { return $this->insert(); } else { return $this->update(); } } /** * Save this object, forcing the query to use REPLACE regardless of object * state. * * @return this * * @task save */ public function replace() { $this->isEphemeralCheck(); return $this->insertRecordIntoDatabase('REPLACE'); } /** * Save this object, forcing the query to use INSERT regardless of object * state. * * @return this * * @task save */ public function insert() { $this->isEphemeralCheck(); return $this->insertRecordIntoDatabase('INSERT'); } /** * Save this object, forcing the query to use UPDATE regardless of object * state. * * @return this * * @task save */ public function update() { $this->isEphemeralCheck(); $this->willSaveObject(); $data = $this->getAllLiskPropertyValues(); // Remove columns flagged as nonmutable from the update statement. $no_mutate = $this->getConfigOption(self::CONFIG_NO_MUTATE); if ($no_mutate) { foreach ($no_mutate as $column) { unset($data[$column]); } } $this->willWriteData($data); $map = array(); foreach ($data as $k => $v) { $map[$k] = $v; } $conn = $this->establishConnection('w'); $binary = $this->getBinaryColumns(); foreach ($map as $key => $value) { if (!empty($binary[$key])) { $map[$key] = qsprintf($conn, '%C = %nB', $key, $value); } else { $map[$key] = qsprintf($conn, '%C = %ns', $key, $value); } } - $map = implode(', ', $map); $id = $this->getID(); $conn->query( - 'UPDATE %R SET %Q WHERE %C = '.(is_int($id) ? '%d' : '%s'), + 'UPDATE %R SET %LQ WHERE %C = '.(is_int($id) ? '%d' : '%s'), $this, $map, $this->getIDKeyForUse(), $id); // We can't detect a missing object because updating an object without // changing any values doesn't affect rows. We could jiggle timestamps // to catch this for objects which track them if we wanted. $this->didWriteData(); return $this; } /** * Delete this object, permanently. * * @return this * * @task save */ public function delete() { $this->isEphemeralCheck(); $this->willDelete(); $conn = $this->establishConnection('w'); $conn->query( 'DELETE FROM %R WHERE %C = %d', $this, $this->getIDKeyForUse(), $this->getID()); $this->didDelete(); return $this; } /** * Internal implementation of INSERT and REPLACE. * * @param const Either "INSERT" or "REPLACE", to force the desired mode. * @return this * * @task save */ protected function insertRecordIntoDatabase($mode) { $this->willSaveObject(); $data = $this->getAllLiskPropertyValues(); $conn = $this->establishConnection('w'); $id_mechanism = $this->getConfigOption(self::CONFIG_IDS); switch ($id_mechanism) { case self::IDS_AUTOINCREMENT: // If we are using autoincrement IDs, let MySQL assign the value for the // ID column, if it is empty. If the caller has explicitly provided a // value, use it. $id_key = $this->getIDKeyForUse(); if (empty($data[$id_key])) { unset($data[$id_key]); } break; case self::IDS_COUNTER: // If we are using counter IDs, assign a new ID if we don't already have // one. $id_key = $this->getIDKeyForUse(); if (empty($data[$id_key])) { $counter_name = $this->getTableName(); $id = self::loadNextCounterValue($conn, $counter_name); $this->setID($id); $data[$id_key] = $id; } break; case self::IDS_MANUAL: break; default: throw new Exception(pht('Unknown %s mechanism!', 'CONFIG_IDs')); } $this->willWriteData($data); $columns = array_keys($data); $binary = $this->getBinaryColumns(); foreach ($data as $key => $value) { try { if (!empty($binary[$key])) { $data[$key] = qsprintf($conn, '%nB', $value); } else { $data[$key] = qsprintf($conn, '%ns', $value); } } catch (AphrontParameterQueryException $parameter_exception) { throw new PhutilProxyException( pht( "Unable to insert or update object of class %s, field '%s' ". "has a non-scalar value.", get_class($this), $key), $parameter_exception); } } - $data = implode(', ', $data); + + switch ($mode) { + case 'INSERT': + $verb = qsprintf($conn, 'INSERT'); + break; + case 'REPLACE': + $verb = qsprintf($conn, 'REPLACE'); + break; + default: + throw new Exception( + pht( + 'Insert mode verb "%s" is not recognized, use INSERT or REPLACE.', + $mode)); + } $conn->query( - '%Q INTO %R (%LC) VALUES (%Q)', - $mode, + '%Q INTO %R (%LC) VALUES (%LQ)', + $verb, $this, $columns, $data); // Only use the insert id if this table is using auto-increment ids if ($id_mechanism === self::IDS_AUTOINCREMENT) { $this->setID($conn->getInsertID()); } $this->didWriteData(); return $this; } /** * Method used to determine whether to insert or update when saving. * * @return bool true if the record should be inserted */ protected function shouldInsertWhenSaved() { $key_type = $this->getConfigOption(self::CONFIG_IDS); if ($key_type == self::IDS_MANUAL) { throw new Exception( pht( 'You are using manual IDs. You must override the %s method '. 'to properly detect when to insert a new record.', __FUNCTION__.'()')); } else { return !$this->getID(); } } /* -( Hooks and Callbacks )------------------------------------------------ */ /** * Retrieve the database table name. By default, this is the class name. * * @return string Table name for object storage. * * @task hook */ public function getTableName() { return get_class($this); } /** * Retrieve the primary key column, "id" by default. If you can not * reasonably name your ID column "id", override this method. * * @return string Name of the ID column. * * @task hook */ public function getIDKey() { return 'id'; } protected function getIDKeyForUse() { $id_key = $this->getIDKey(); if (!$id_key) { throw new Exception( pht( 'This DAO does not have a single-part primary key. The method you '. 'called requires a single-part primary key.')); } return $id_key; } /** * Generate a new PHID, used by CONFIG_AUX_PHID. * * @return phid Unique, newly allocated PHID. * * @task hook */ public function generatePHID() { $type = $this->getPHIDType(); return PhabricatorPHID::generateNewPHID($type); } public function getPHIDType() { throw new PhutilMethodNotImplementedException(); } /** * Hook to apply serialization or validation to data before it is written to * the database. See also @{method:willReadData}. * * @task hook */ protected function willWriteData(array &$data) { $this->applyLiskDataSerialization($data, false); } /** * Hook to perform actions after data has been written to the database. * * @task hook */ protected function didWriteData() {} /** * Hook to make internal object state changes prior to INSERT, REPLACE or * UPDATE. * * @task hook */ protected function willSaveObject() { $use_timestamps = $this->getConfigOption(self::CONFIG_TIMESTAMPS); if ($use_timestamps) { if (!$this->getDateCreated()) { $this->setDateCreated(time()); } $this->setDateModified(time()); } if ($this->getConfigOption(self::CONFIG_AUX_PHID) && !$this->getPHID()) { $this->setPHID($this->generatePHID()); } } /** * Hook to apply serialization or validation to data as it is read from the * database. See also @{method:willWriteData}. * * @task hook */ protected function willReadData(array &$data) { $this->applyLiskDataSerialization($data, $deserialize = true); } /** * Hook to perform an action on data after it is read from the database. * * @task hook */ protected function didReadData() {} /** * Hook to perform an action before the deletion of an object. * * @task hook */ protected function willDelete() {} /** * Hook to perform an action after the deletion of an object. * * @task hook */ protected function didDelete() {} /** * Reads the value from a field. Override this method for custom behavior * of @{method:getField} instead of overriding getField directly. * * @param string Canonical field name * @return mixed Value of the field * * @task hook */ protected function readField($field) { if (isset($this->$field)) { return $this->$field; } return null; } /** * Writes a value to a field. Override this method for custom behavior of * setField($value) instead of overriding setField directly. * * @param string Canonical field name * @param mixed Value to write * * @task hook */ protected function writeField($field, $value) { $this->$field = $value; } /* -( Manging Transactions )----------------------------------------------- */ /** * Increase transaction stack depth. * * @return this */ public function openTransaction() { $this->establishConnection('w')->openTransaction(); return $this; } /** * Decrease transaction stack depth, saving work. * * @return this */ public function saveTransaction() { $this->establishConnection('w')->saveTransaction(); return $this; } /** * Decrease transaction stack depth, discarding work. * * @return this */ public function killTransaction() { $this->establishConnection('w')->killTransaction(); return $this; } /** * Begins read-locking selected rows with SELECT ... FOR UPDATE, so that * other connections can not read them (this is an enormous oversimplification * of FOR UPDATE semantics; consult the MySQL documentation for details). To * end read locking, call @{method:endReadLocking}. For example: * * $beach->openTransaction(); * $beach->beginReadLocking(); * * $beach->reload(); * $beach->setGrainsOfSand($beach->getGrainsOfSand() + 1); * $beach->save(); * * $beach->endReadLocking(); * $beach->saveTransaction(); * * @return this * @task xaction */ public function beginReadLocking() { $this->establishConnection('w')->beginReadLocking(); return $this; } /** * Ends read-locking that began at an earlier @{method:beginReadLocking} call. * * @return this * @task xaction */ public function endReadLocking() { $this->establishConnection('w')->endReadLocking(); return $this; } /** * Begins write-locking selected rows with SELECT ... LOCK IN SHARE MODE, so * that other connections can not update or delete them (this is an * oversimplification of LOCK IN SHARE MODE semantics; consult the * MySQL documentation for details). To end write locking, call * @{method:endWriteLocking}. * * @return this * @task xaction */ public function beginWriteLocking() { $this->establishConnection('w')->beginWriteLocking(); return $this; } /** * Ends write-locking that began at an earlier @{method:beginWriteLocking} * call. * * @return this * @task xaction */ public function endWriteLocking() { $this->establishConnection('w')->endWriteLocking(); return $this; } /* -( Isolation )---------------------------------------------------------- */ /** * @task isolate */ public static function beginIsolateAllLiskEffectsToCurrentProcess() { self::$processIsolationLevel++; } /** * @task isolate */ public static function endIsolateAllLiskEffectsToCurrentProcess() { self::$processIsolationLevel--; if (self::$processIsolationLevel < 0) { throw new Exception( pht('Lisk process isolation level was reduced below 0.')); } } /** * @task isolate */ public static function shouldIsolateAllLiskEffectsToCurrentProcess() { return (bool)self::$processIsolationLevel; } /** * @task isolate */ private function establishIsolatedConnection($mode) { $config = array(); return new AphrontIsolatedDatabaseConnection($config); } /** * @task isolate */ public static function beginIsolateAllLiskEffectsToTransactions() { if (self::$transactionIsolationLevel === 0) { self::closeAllConnections(); } self::$transactionIsolationLevel++; } /** * @task isolate */ public static function endIsolateAllLiskEffectsToTransactions() { self::$transactionIsolationLevel--; if (self::$transactionIsolationLevel < 0) { throw new Exception( pht('Lisk transaction isolation level was reduced below 0.')); } else if (self::$transactionIsolationLevel == 0) { foreach (self::$connections as $key => $conn) { if ($conn) { $conn->killTransaction(); } } self::closeAllConnections(); } } /** * @task isolate */ public static function shouldIsolateAllLiskEffectsToTransactions() { return (bool)self::$transactionIsolationLevel; } /** * Close any connections with no recent activity. * * Long-running processes can use this method to clean up connections which * have not been used recently. * * @param int Close connections with no activity for this many seconds. * @return void */ public static function closeInactiveConnections($idle_window) { $connections = self::$connections; $now = PhabricatorTime::getNow(); foreach ($connections as $key => $connection) { + // If the connection is not idle, never consider it inactive. + if (!$connection->isIdle()) { + continue; + } + $last_active = $connection->getLastActiveEpoch(); $idle_duration = ($now - $last_active); if ($idle_duration <= $idle_window) { continue; } self::closeConnection($key); } } public static function closeAllConnections() { $connections = self::$connections; foreach ($connections as $key => $connection) { self::closeConnection($key); } } + public static function closeIdleConnections() { + $connections = self::$connections; + + foreach ($connections as $key => $connection) { + if (!$connection->isIdle()) { + continue; + } + + self::closeConnection($key); + } + } + private static function closeConnection($key) { if (empty(self::$connections[$key])) { throw new Exception( pht( 'No database connection with connection key "%s" exists!', $key)); } $connection = self::$connections[$key]; unset(self::$connections[$key]); $connection->close(); } /* -( Utilities )---------------------------------------------------------- */ /** * Applies configured serialization to a dictionary of values. * * @task util */ protected function applyLiskDataSerialization(array &$data, $deserialize) { $serialization = $this->getConfigOption(self::CONFIG_SERIALIZATION); if ($serialization) { foreach (array_intersect_key($serialization, $data) as $col => $format) { switch ($format) { case self::SERIALIZATION_NONE: break; case self::SERIALIZATION_PHP: if ($deserialize) { $data[$col] = unserialize($data[$col]); } else { $data[$col] = serialize($data[$col]); } break; case self::SERIALIZATION_JSON: if ($deserialize) { $data[$col] = json_decode($data[$col], true); } else { $data[$col] = phutil_json_encode($data[$col]); } break; default: throw new Exception( pht("Unknown serialization format '%s'.", $format)); } } } } /** * Black magic. Builds implied get*() and set*() for all properties. * * @param string Method name. * @param list Argument vector. * @return mixed get*() methods return the property value. set*() methods * return $this. * @task util */ public function __call($method, $args) { // NOTE: PHP has a bug that static variables defined in __call() are shared // across all children classes. Call a different method to work around this // bug. return $this->call($method, $args); } /** * @task util */ final protected function call($method, $args) { // NOTE: This method is very performance-sensitive (many thousands of calls // per page on some pages), and thus has some silliness in the name of // optimizations. static $dispatch_map = array(); if ($method[0] === 'g') { if (isset($dispatch_map[$method])) { $property = $dispatch_map[$method]; } else { if (substr($method, 0, 3) !== 'get') { throw new Exception(pht("Unable to resolve method '%s'!", $method)); } $property = substr($method, 3); if (!($property = $this->checkProperty($property))) { throw new Exception(pht('Bad getter call: %s', $method)); } $dispatch_map[$method] = $property; } return $this->readField($property); } if ($method[0] === 's') { if (isset($dispatch_map[$method])) { $property = $dispatch_map[$method]; } else { if (substr($method, 0, 3) !== 'set') { throw new Exception(pht("Unable to resolve method '%s'!", $method)); } $property = substr($method, 3); $property = $this->checkProperty($property); if (!$property) { throw new Exception(pht('Bad setter call: %s', $method)); } $dispatch_map[$method] = $property; } $this->writeField($property, $args[0]); return $this; } throw new Exception(pht("Unable to resolve method '%s'.", $method)); } /** * Warns against writing to undeclared property. * * @task util */ public function __set($name, $value) { // Hack for policy system hints, see PhabricatorPolicyRule for notes. if ($name != '_hashKey') { phlog( pht( 'Wrote to undeclared property %s.', get_class($this).'::$'.$name)); } $this->$name = $value; } /** * Increments a named counter and returns the next value. * * @param AphrontDatabaseConnection Database where the counter resides. * @param string Counter name to create or increment. * @return int Next counter value. * * @task util */ public static function loadNextCounterValue( AphrontDatabaseConnection $conn_w, $counter_name) { // NOTE: If an insert does not touch an autoincrement row or call // LAST_INSERT_ID(), MySQL normally does not change the value of // LAST_INSERT_ID(). This can cause a counter's value to leak to a // new counter if the second counter is created after the first one is // updated. To avoid this, we insert LAST_INSERT_ID(1), to ensure the // LAST_INSERT_ID() is always updated and always set correctly after the // query completes. queryfx( $conn_w, 'INSERT INTO %T (counterName, counterValue) VALUES (%s, LAST_INSERT_ID(1)) ON DUPLICATE KEY UPDATE counterValue = LAST_INSERT_ID(counterValue + 1)', self::COUNTER_TABLE_NAME, $counter_name); return $conn_w->getInsertID(); } /** * Returns the current value of a named counter. * * @param AphrontDatabaseConnection Database where the counter resides. * @param string Counter name to read. * @return int|null Current value, or `null` if the counter does not exist. * * @task util */ public static function loadCurrentCounterValue( AphrontDatabaseConnection $conn_r, $counter_name) { $row = queryfx_one( $conn_r, 'SELECT counterValue FROM %T WHERE counterName = %s', self::COUNTER_TABLE_NAME, $counter_name); if (!$row) { return null; } return (int)$row['counterValue']; } /** * Overwrite a named counter, forcing it to a specific value. * * If the counter does not exist, it is created. * * @param AphrontDatabaseConnection Database where the counter resides. * @param string Counter name to create or overwrite. * @return void * * @task util */ public static function overwriteCounterValue( AphrontDatabaseConnection $conn_w, $counter_name, $counter_value) { queryfx( $conn_w, 'INSERT INTO %T (counterName, counterValue) VALUES (%s, %d) ON DUPLICATE KEY UPDATE counterValue = VALUES(counterValue)', self::COUNTER_TABLE_NAME, $counter_name, $counter_value); } private function getBinaryColumns() { return $this->getConfigOption(self::CONFIG_BINARY); } public function getSchemaColumns() { $custom_map = $this->getConfigOption(self::CONFIG_COLUMN_SCHEMA); if (!$custom_map) { $custom_map = array(); } $serialization = $this->getConfigOption(self::CONFIG_SERIALIZATION); if (!$serialization) { $serialization = array(); } $serialization_map = array( self::SERIALIZATION_JSON => 'text', self::SERIALIZATION_PHP => 'bytes', ); $binary_map = $this->getBinaryColumns(); $id_mechanism = $this->getConfigOption(self::CONFIG_IDS); if ($id_mechanism == self::IDS_AUTOINCREMENT) { $id_type = 'auto'; } else { $id_type = 'id'; } $builtin = array( 'id' => $id_type, 'phid' => 'phid', 'viewPolicy' => 'policy', 'editPolicy' => 'policy', 'epoch' => 'epoch', 'dateCreated' => 'epoch', 'dateModified' => 'epoch', ); $map = array(); foreach ($this->getAllLiskProperties() as $property) { // First, use types specified explicitly in the table configuration. if (array_key_exists($property, $custom_map)) { $map[$property] = $custom_map[$property]; continue; } // If we don't have an explicit type, try a builtin type for the // column. $type = idx($builtin, $property); if ($type) { $map[$property] = $type; continue; } // If the column has serialization, we can infer the column type. if (isset($serialization[$property])) { $type = idx($serialization_map, $serialization[$property]); if ($type) { $map[$property] = $type; continue; } } if (isset($binary_map[$property])) { $map[$property] = 'bytes'; continue; } if ($property === 'spacePHID') { $map[$property] = 'phid?'; continue; } // If the column is named `somethingPHID`, infer it is a PHID. if (preg_match('/[a-z]PHID$/', $property)) { $map[$property] = 'phid'; continue; } // If the column is named `somethingID`, infer it is an ID. if (preg_match('/[a-z]ID$/', $property)) { $map[$property] = 'id'; continue; } // We don't know the type of this column. $map[$property] = PhabricatorConfigSchemaSpec::DATATYPE_UNKNOWN; } return $map; } public function getSchemaKeys() { $custom_map = $this->getConfigOption(self::CONFIG_KEY_SCHEMA); if (!$custom_map) { $custom_map = array(); } $default_map = array(); foreach ($this->getAllLiskProperties() as $property) { switch ($property) { case 'id': $default_map['PRIMARY'] = array( 'columns' => array('id'), 'unique' => true, ); break; case 'phid': $default_map['key_phid'] = array( 'columns' => array('phid'), 'unique' => true, ); break; case 'spacePHID': $default_map['key_space'] = array( 'columns' => array('spacePHID'), ); break; } } return $custom_map + $default_map; } public function getColumnMaximumByteLength($column) { $map = $this->getSchemaColumns(); if (!isset($map[$column])) { throw new Exception( pht( 'Object (of class "%s") does not have a column "%s".', get_class($this), $column)); } $data_type = $map[$column]; return id(new PhabricatorStorageSchemaSpec()) ->getMaximumByteLengthForDataType($data_type); } /* -( AphrontDatabaseTableRefInterface )----------------------------------- */ public function getAphrontRefDatabaseName() { return $this->getDatabaseName(); } public function getAphrontRefTableName() { return $this->getTableName(); } } diff --git a/src/infrastructure/storage/lisk/LiskDAOSet.php b/src/infrastructure/storage/lisk/LiskDAOSet.php index e1bc1e50d1..90eba708ea 100644 --- a/src/infrastructure/storage/lisk/LiskDAOSet.php +++ b/src/infrastructure/storage/lisk/LiskDAOSet.php @@ -1,92 +1,101 @@ addToSet($author); * foreach ($reviewers as $reviewer) { * $users->addToSet($reviewer); * } * foreach ($ccs as $cc) { * $users->addToSet($cc); * } * // Preload e-mails of all involved users and return e-mails of author. * $author_emails = $author->loadRelatives( * new PhabricatorUserEmail(), * 'userPHID', * 'getPHID'); */ final class LiskDAOSet extends Phobject { private $daos = array(); private $relatives = array(); private $subsets = array(); public function addToSet(LiskDAO $dao) { if ($this->relatives) { throw new Exception( pht( "Don't call %s after loading data!", __FUNCTION__.'()')); } $this->daos[] = $dao; $dao->putInSet($this); return $this; } /** * The main purpose of this method is to break cyclic dependency. * It removes all objects from this set and all subsets created by it. */ public function clearSet() { $this->daos = array(); $this->relatives = array(); foreach ($this->subsets as $set) { $set->clearSet(); } $this->subsets = array(); return $this; } /** * See @{method:LiskDAO::loadRelatives}. */ public function loadRelatives( LiskDAO $object, $foreign_column, $key_method = 'getID', $where = '') { $relatives = &$this->relatives[ get_class($object)."-{$foreign_column}-{$key_method}-{$where}"]; if ($relatives === null) { $ids = array(); foreach ($this->daos as $dao) { $id = $dao->$key_method(); if ($id !== null) { $ids[$id] = $id; } } if (!$ids) { $relatives = array(); } else { $set = new LiskDAOSet(); $this->subsets[] = $set; + + $conn = $object->establishConnection('r'); + + if (strlen($where)) { + $where_clause = qsprintf($conn, 'AND %Q', $where); + } else { + $where_clause = qsprintf($conn, ''); + } + $relatives = $object->putInSet($set)->loadAllWhere( '%C IN (%Ls) %Q', $foreign_column, $ids, - ($where != '' ? 'AND '.$where : '')); + $where_clause); $relatives = mgroup($relatives, 'get'.$foreign_column); } } return $relatives; } } diff --git a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php index b300efbf4b..e47d701df9 100644 --- a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php +++ b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php @@ -1,331 +1,328 @@ '; const CONFIG_APPLICATION_SERIALIZERS = 'phabricator/serializers'; /* -( Configuring Storage )------------------------------------------------ */ /** * @task config */ public static function pushStorageNamespace($namespace) { self::$namespaceStack[] = $namespace; } /** * @task config */ public static function popStorageNamespace() { array_pop(self::$namespaceStack); } /** * @task config */ public static function getDefaultStorageNamespace() { return PhabricatorEnv::getEnvConfig('storage.default-namespace'); } /** * @task config */ public static function getStorageNamespace() { $namespace = end(self::$namespaceStack); if (!strlen($namespace)) { $namespace = self::getDefaultStorageNamespace(); } if (!strlen($namespace)) { throw new Exception(pht('No storage namespace configured!')); } return $namespace; } /** * @task config */ protected function establishLiveConnection($mode) { $namespace = self::getStorageNamespace(); $database = $namespace.'_'.$this->getApplicationName(); $is_readonly = PhabricatorEnv::isReadOnly(); if ($is_readonly && ($mode != 'r')) { $this->raiseImproperWrite($database); } $connection = $this->newClusterConnection( $this->getApplicationName(), $database, $mode); // TODO: This should be testing if the mode is "r", but that would probably // break a lot of things. Perform a more narrow test for readonly mode // until we have greater certainty that this works correctly most of the // time. if ($is_readonly) { $connection->setReadOnly(true); } return $connection; } private function newClusterConnection($application, $database, $mode) { $master = PhabricatorDatabaseRef::getMasterDatabaseRefForApplication( $application); $master_exception = null; if ($master && !$master->isSevered()) { $connection = $master->newApplicationConnection($database); if ($master->isReachable($connection)) { return $connection; } else { if ($mode == 'w') { $this->raiseImpossibleWrite($database); } PhabricatorEnv::setReadOnly( true, PhabricatorEnv::READONLY_UNREACHABLE); $master_exception = $master->getConnectionException(); } } $replica = PhabricatorDatabaseRef::getReplicaDatabaseRefForApplication( $application); if ($replica) { $connection = $replica->newApplicationConnection($database); $connection->setReadOnly(true); if ($replica->isReachable($connection)) { return $connection; } } if (!$master && !$replica) { $this->raiseUnconfigured($database); } $this->raiseUnreachable($database, $master_exception); } private function raiseImproperWrite($database) { throw new PhabricatorClusterImproperWriteException( pht( 'Unable to establish a write-mode connection (to application '. 'database "%s") because Phabricator is in read-only mode. Whatever '. 'you are trying to do does not function correctly in read-only mode.', $database)); } private function raiseImpossibleWrite($database) { throw new PhabricatorClusterImpossibleWriteException( pht( 'Unable to connect to master database ("%s"). This is a severe '. 'failure; your request did not complete.', $database)); } private function raiseUnconfigured($database) { throw new Exception( pht( 'Unable to establish a connection to any database host '. '(while trying "%s"). No masters or replicas are configured.', $database)); } private function raiseUnreachable($database, Exception $proxy = null) { $message = pht( 'Unable to establish a connection to any database host '. '(while trying "%s"). All masters and replicas are completely '. 'unreachable.', $database); if ($proxy) { $proxy_message = pht( '%s: %s', get_class($proxy), $proxy->getMessage()); $message = $message."\n\n".$proxy_message; } throw new PhabricatorClusterStrandedException($message); } /** * @task config */ public function getTableName() { $str = 'phabricator'; $len = strlen($str); $class = strtolower(get_class($this)); if (!strncmp($class, $str, $len)) { $class = substr($class, $len); } $app = $this->getApplicationName(); if (!strncmp($class, $app, strlen($app))) { $class = substr($class, strlen($app)); } if (strlen($class)) { return $app.'_'.$class; } else { return $app; } } /** * @task config */ abstract public function getApplicationName(); protected function getDatabaseName() { return self::getStorageNamespace().'_'.$this->getApplicationName(); } /** * Break a list of escaped SQL statement fragments (e.g., VALUES lists for * INSERT, previously built with @{function:qsprintf}) into chunks which will * fit under the MySQL 'max_allowed_packet' limit. * - * Chunks are glued together with `$glue`, by default ", ". - * * If a statement is too large to fit within the limit, it is broken into * its own chunk (but might fail when the query executes). */ public static function chunkSQL( array $fragments, - $glue = ', ', $limit = null) { if ($limit === null) { // NOTE: Hard-code this at 1MB for now, minus a 10% safety buffer. // Eventually we could query MySQL or let the user configure it. $limit = (int)((1024 * 1024) * 0.90); } $result = array(); $chunk = array(); $len = 0; - $glue_len = strlen($glue); + $glue_len = strlen(', '); foreach ($fragments as $fragment) { - $this_len = strlen($fragment); + if ($fragment instanceof PhutilQueryString) { + $this_len = strlen($fragment->getUnmaskedString()); + } else { + $this_len = strlen($fragment); + } if ($chunk) { // Chunks after the first also imply glue. $this_len += $glue_len; } if ($len + $this_len <= $limit) { $len += $this_len; $chunk[] = $fragment; } else { if ($chunk) { $result[] = $chunk; } - $len = strlen($fragment); + $len = ($this_len - $glue_len); $chunk = array($fragment); } } if ($chunk) { $result[] = $chunk; } - foreach ($result as $key => $fragment_list) { - $result[$key] = implode($glue, $fragment_list); - } - return $result; } protected function assertAttached($property) { if ($property === self::ATTACHABLE) { throw new PhabricatorDataNotAttachedException($this); } return $property; } protected function assertAttachedKey($value, $key) { $this->assertAttached($value); if (!array_key_exists($key, $value)) { throw new PhabricatorDataNotAttachedException($this); } return $value[$key]; } protected function detectEncodingForStorage($string) { return phutil_is_utf8($string) ? 'utf8' : null; } protected function getUTF8StringFromStorage($string, $encoding) { if ($encoding == 'utf8') { return $string; } if (function_exists('mb_detect_encoding')) { if (strlen($encoding)) { $try_encodings = array( $encoding, ); } else { // TODO: This is pretty much a guess, and probably needs to be // configurable in the long run. $try_encodings = array( 'JIS', 'EUC-JP', 'SJIS', 'ISO-8859-1', ); } $guess = mb_detect_encoding($string, $try_encodings); if ($guess) { return mb_convert_encoding($string, 'UTF-8', $guess); } } return phutil_utf8ize($string); } protected function willReadData(array &$data) { parent::willReadData($data); static $custom; if ($custom === null) { $custom = $this->getConfigOption(self::CONFIG_APPLICATION_SERIALIZERS); } if ($custom) { foreach ($custom as $key => $serializer) { $data[$key] = $serializer->willReadValue($data[$key]); } } } protected function willWriteData(array &$data) { static $custom; if ($custom === null) { $custom = $this->getConfigOption(self::CONFIG_APPLICATION_SERIALIZERS); } if ($custom) { foreach ($custom as $key => $serializer) { $data[$key] = $serializer->willWriteValue($data[$key]); } } parent::willWriteData($data); } } diff --git a/src/infrastructure/storage/lisk/__tests__/LiskChunkTestCase.php b/src/infrastructure/storage/lisk/__tests__/LiskChunkTestCase.php index 8ce7608a8b..66ffdb17bc 100644 --- a/src/infrastructure/storage/lisk/__tests__/LiskChunkTestCase.php +++ b/src/infrastructure/storage/lisk/__tests__/LiskChunkTestCase.php @@ -1,66 +1,68 @@ assertEqual( array( - 'aa', - 'bb', - 'ccc', - 'dd', - 'e', + array('a'), + array('a'), + array('b'), + array('b'), + array('ccc'), + array('dd'), + array('e'), ), - PhabricatorLiskDAO::chunkSQL($fragments, '', 2)); + PhabricatorLiskDAO::chunkSQL($fragments, 2)); $fragments = array( 'a', 'a', 'a', 'XX', 'a', 'a', 'a', 'a', ); $this->assertEqual( array( - 'a, a, a', - 'XX, a, a', - 'a, a', + array('a', 'a', 'a'), + array('XX', 'a', 'a'), + array('a', 'a'), ), - PhabricatorLiskDAO::chunkSQL($fragments, ', ', 8)); + PhabricatorLiskDAO::chunkSQL($fragments, 8)); $fragments = array( 'xxxxxxxxxx', 'yyyyyyyyyy', 'a', 'b', 'c', 'zzzzzzzzzz', ); $this->assertEqual( array( - 'xxxxxxxxxx', - 'yyyyyyyyyy', - 'a, b, c', - 'zzzzzzzzzz', + array('xxxxxxxxxx'), + array('yyyyyyyyyy'), + array('a', 'b', 'c'), + array('zzzzzzzzzz'), ), - PhabricatorLiskDAO::chunkSQL($fragments, ', ', 8)); + PhabricatorLiskDAO::chunkSQL($fragments, 8)); } } diff --git a/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php b/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php index 19d7a98d42..e66ba784f7 100644 --- a/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php +++ b/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php @@ -1,363 +1,365 @@ disableUTF8MB4 = $disable_utf8_mb4; return $this; } public function getDisableUTF8MB4() { return $this->disableUTF8MB4; } public function setNamespace($namespace) { $this->namespace = $namespace; PhabricatorLiskDAO::pushStorageNamespace($namespace); return $this; } public function getNamespace() { return $this->namespace; } public function setUser($user) { $this->user = $user; return $this; } public function getUser() { return $this->user; } public function setPassword($password) { $this->password = $password; return $this; } public function getPassword() { return $this->password; } public function setHost($host) { $this->host = $host; return $this; } public function getHost() { return $this->host; } public function setPort($port) { $this->port = $port; return $this; } public function getPort() { return $this->port; } public function setRef(PhabricatorDatabaseRef $ref) { $this->ref = $ref; return $this; } public function getRef() { return $this->ref; } public function getDatabaseName($fragment) { return $this->namespace.'_'.$fragment; } public function getDatabaseList(array $patches, $only_living = false) { assert_instances_of($patches, 'PhabricatorStoragePatch'); $list = array(); foreach ($patches as $patch) { if ($patch->getType() == 'db') { if ($only_living && $patch->isDead()) { continue; } $list[] = $this->getDatabaseName($patch->getName()); } } return $list; } public function getConn($fragment) { $database = $this->getDatabaseName($fragment); $return = &$this->conns[$this->host][$this->user][$database]; if (!$return) { $return = PhabricatorDatabaseRef::newRawConnection( array( 'user' => $this->user, 'pass' => $this->password, 'host' => $this->host, 'port' => $this->port, 'database' => $fragment ? $database : null, )); } return $return; } public function getAppliedPatches() { try { $applied = queryfx_all( $this->getConn('meta_data'), 'SELECT patch FROM %T', self::TABLE_STATUS); return ipull($applied, 'patch'); } catch (AphrontAccessDeniedQueryException $ex) { throw new PhutilProxyException( pht( 'Failed while trying to read schema status: the database "%s" '. 'exists, but the current user ("%s") does not have permission to '. 'access it. GRANT the current user more permissions, or use a '. 'different user.', $this->getDatabaseName('meta_data'), $this->getUser()), $ex); } catch (AphrontQueryException $ex) { return null; } } public function getPatchDurations() { try { $rows = queryfx_all( $this->getConn('meta_data'), 'SELECT patch, duration FROM %T WHERE duration IS NOT NULL', self::TABLE_STATUS); return ipull($rows, 'duration', 'patch'); } catch (AphrontQueryException $ex) { return array(); } } public function createDatabase($fragment) { $info = $this->getCharsetInfo(); queryfx( $this->getConn(null), 'CREATE DATABASE IF NOT EXISTS %T COLLATE %T', $this->getDatabaseName($fragment), $info[self::COLLATE_TEXT]); } public function createTable($fragment, $table, array $cols) { queryfx( $this->getConn($fragment), 'CREATE TABLE IF NOT EXISTS %T.%T (%Q) '. 'ENGINE=InnoDB, COLLATE utf8_general_ci', $this->getDatabaseName($fragment), $table, implode(', ', $cols)); } public function getLegacyPatches(array $patches) { assert_instances_of($patches, 'PhabricatorStoragePatch'); try { $row = queryfx_one( $this->getConn('meta_data'), 'SELECT version FROM %T', 'schema_version'); $version = $row['version']; } catch (AphrontQueryException $ex) { return array(); } $legacy = array(); foreach ($patches as $key => $patch) { if ($patch->getLegacy() !== false && $patch->getLegacy() <= $version) { $legacy[] = $key; } } return $legacy; } public function markPatchApplied($patch, $duration = null) { $conn = $this->getConn('meta_data'); queryfx( $conn, 'INSERT INTO %T (patch, applied) VALUES (%s, %d)', self::TABLE_STATUS, $patch, time()); // We didn't add this column for a long time, so it may not exist yet. if ($duration !== null) { try { queryfx( $conn, 'UPDATE %T SET duration = %d WHERE patch = %s', self::TABLE_STATUS, (int)floor($duration * 1000000), $patch); } catch (AphrontQueryException $ex) { // Just ignore this, as it almost certainly indicates that we just // don't have the column yet. } } } public function applyPatch(PhabricatorStoragePatch $patch) { $type = $patch->getType(); $name = $patch->getName(); switch ($type) { case 'db': $this->createDatabase($name); break; case 'sql': $this->applyPatchSQL($name); break; case 'php': $this->applyPatchPHP($name); break; default: throw new Exception(pht("Unable to apply patch of type '%s'.", $type)); } } public function applyPatchSQL($sql) { $sql = Filesystem::readFile($sql); $queries = preg_split('/;\s+/', $sql); $queries = array_filter($queries); $conn = $this->getConn(null); $charset_info = $this->getCharsetInfo(); foreach ($charset_info as $key => $value) { $charset_info[$key] = qsprintf($conn, '%T', $value); } foreach ($queries as $query) { $query = str_replace('{$NAMESPACE}', $this->namespace, $query); foreach ($charset_info as $key => $value) { $query = str_replace('{$'.$key.'}', $value, $query); } try { - queryfx($conn, '%Q', $query); + // NOTE: We're using the unsafe "%Z" conversion here. There's no + // avoiding it since we're executing raw text files full of SQL. + queryfx($conn, '%Z', $query); } catch (AphrontAccessDeniedQueryException $ex) { throw new PhutilProxyException( pht( 'Unable to access a required database or table. This almost '. 'always means that the user you are connecting with ("%s") does '. 'not have sufficient permissions granted in MySQL. You can '. 'use `bin/storage databases` to get a list of all databases '. 'permission is required on.', $this->getUser()), $ex); } } } public function applyPatchPHP($script) { $schema_conn = $this->getConn(null); require_once $script; } public function isCharacterSetAvailable($character_set) { if ($character_set == 'utf8mb4') { if ($this->getDisableUTF8MB4()) { return false; } } $conn = $this->getConn(null); return self::isCharacterSetAvailableOnConnection($character_set, $conn); } public static function isCharacterSetAvailableOnConnection( $character_set, AphrontDatabaseConnection $conn) { $result = queryfx_one( $conn, 'SELECT CHARACTER_SET_NAME FROM INFORMATION_SCHEMA.CHARACTER_SETS WHERE CHARACTER_SET_NAME = %s', $character_set); return (bool)$result; } public function getCharsetInfo() { if ($this->isCharacterSetAvailable('utf8mb4')) { // If utf8mb4 is available, we use it with the utf8mb4_unicode_ci // collation. This is most correct, and will sort properly. $charset = 'utf8mb4'; $charset_sort = 'utf8mb4'; $charset_full = 'utf8mb4'; $collate_text = 'utf8mb4_bin'; $collate_sort = 'utf8mb4_unicode_ci'; $collate_full = 'utf8mb4_unicode_ci'; } else { // If utf8mb4 is not available, we use binary for most data. This allows // us to store 4-byte unicode characters. // // It's possible that strings will be truncated in the middle of a // character on insert. We encourage users to set STRICT_ALL_TABLES // to prevent this. // // For "fulltext" and "sort" columns, we don't use binary. // // With "fulltext", we can not use binary because MySQL won't let us. // We use 3-byte utf8 instead and accept being unable to index 4-byte // characters. // // With "sort", if we use binary we lose case insensitivity (for // example, "ALincoln@logcabin.com" and "alincoln@logcabin.com" would no // longer be identified as the same email address). This can be very // confusing and is far worse overall than not supporting 4-byte unicode // characters, so we use 3-byte utf8 and accept limited 4-byte support as // a tradeoff to get sensible collation behavior. Many columns where // collation is important rarely contain 4-byte characters anyway, so we // are not giving up too much. $charset = 'binary'; $charset_sort = 'utf8'; $charset_full = 'utf8'; $collate_text = 'binary'; $collate_sort = 'utf8_general_ci'; $collate_full = 'utf8_general_ci'; } return array( self::CHARSET_DEFAULT => $charset, self::CHARSET_SORT => $charset_sort, self::CHARSET_FULLTEXT => $charset_full, self::COLLATE_TEXT => $collate_text, self::COLLATE_SORT => $collate_sort, self::COLLATE_FULLTEXT => $collate_full, ); } } diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php index 5b9459cfb8..5bc83972dd 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php @@ -1,1229 +1,1283 @@ apis = $apis; return $this; } final public function getAnyAPI() { return head($this->getAPIs()); } final public function getMasterAPIs() { $apis = $this->getAPIs(); $results = array(); foreach ($apis as $api) { if ($api->getRef()->getIsMaster()) { $results[] = $api; } } if (!$results) { throw new PhutilArgumentUsageException( pht( 'This command only operates on database masters, but the selected '. 'database hosts do not include any masters.')); } return $results; } final public function getSingleAPI() { $apis = $this->getAPIs(); if (count($apis) == 1) { return head($apis); } throw new PhutilArgumentUsageException( pht( 'Phabricator is configured in cluster mode, with multiple database '. 'hosts. Use "--host" to specify which host you want to operate on.')); } final public function getAPIs() { return $this->apis; } final protected function isDryRun() { return $this->dryRun; } final protected function setDryRun($dry_run) { $this->dryRun = $dry_run; return $this; } final protected function isForce() { return $this->force; } final protected function setForce($force) { $this->force = $force; return $this; } public function getPatches() { return $this->patches; } public function setPatches(array $patches) { assert_instances_of($patches, 'PhabricatorStoragePatch'); $this->patches = $patches; return $this; } protected function isReadOnlyWorkflow() { return false; } public function execute(PhutilArgumentParser $args) { $this->setDryRun($args->getArg('dryrun')); $this->setForce($args->getArg('force')); if (!$this->isReadOnlyWorkflow()) { if (PhabricatorEnv::isReadOnly()) { if ($this->isForce()) { PhabricatorEnv::setReadOnly(false, null); } else { throw new PhutilArgumentUsageException( pht( 'Phabricator is currently in read-only mode. Use --force to '. 'override this mode.')); } } } return $this->didExecute($args); } public function didExecute(PhutilArgumentParser $args) {} private function loadSchemata(PhabricatorStorageManagementAPI $api) { $query = id(new PhabricatorConfigSchemaQuery()); $ref = $api->getRef(); $ref_key = $ref->getRefKey(); $query->setAPIs(array($api)); $query->setRefs(array($ref)); $actual = $query->loadActualSchemata(); $expect = $query->loadExpectedSchemata(); $comp = $query->buildComparisonSchemata($expect, $actual); return array( $comp[$ref_key], $expect[$ref_key], $actual[$ref_key], ); } final protected function adjustSchemata( PhabricatorStorageManagementAPI $api, $unsafe) { $lock = $this->lock($api); try { $err = $this->doAdjustSchemata($api, $unsafe); // Analyze tables if we're not doing a dry run and adjustments are either // all clear or have minor errors like surplus tables. if (!$this->dryRun) { $should_analyze = (($err == 0) || ($err == 2)); if ($should_analyze) { $this->analyzeTables($api); } } } catch (Exception $ex) { $lock->unlock(); throw $ex; } $lock->unlock(); return $err; } final private function doAdjustSchemata( PhabricatorStorageManagementAPI $api, $unsafe) { $console = PhutilConsole::getConsole(); $console->writeOut( "%s\n", pht( 'Verifying database schemata on "%s"...', $api->getRef()->getRefKey())); list($adjustments, $errors) = $this->findAdjustments($api); if (!$adjustments) { $console->writeOut( "%s\n", pht('Found no adjustments for schemata.')); return $this->printErrors($errors, 0); } if (!$this->force && !$api->isCharacterSetAvailable('utf8mb4')) { $message = pht( "You have an old version of MySQL (older than 5.5) which does not ". "support the utf8mb4 character set. We strongly recommend upgrading ". "to 5.5 or newer.\n\n". "If you apply adjustments now and later update MySQL to 5.5 or newer, ". "you'll need to apply adjustments again (and they will take a long ". "time).\n\n". "You can exit this workflow, update MySQL now, and then run this ". "workflow again. This is recommended, but may cause a lot of downtime ". "right now.\n\n". "You can exit this workflow, continue using Phabricator without ". "applying adjustments, update MySQL at a later date, and then run ". "this workflow again. This is also a good approach, and will let you ". "delay downtime until later.\n\n". "You can proceed with this workflow, and then optionally update ". "MySQL at a later date. After you do, you'll need to apply ". "adjustments again.\n\n". "For more information, see \"Managing Storage Adjustments\" in ". "the documentation."); $console->writeOut( "\n** %s **\n\n%s\n", pht('OLD MySQL VERSION'), phutil_console_wrap($message)); $prompt = pht('Continue with old MySQL version?'); if (!phutil_console_confirm($prompt, $default_no = true)) { return; } } $table = id(new PhutilConsoleTable()) ->addColumn('database', array('title' => pht('Database'))) ->addColumn('table', array('title' => pht('Table'))) ->addColumn('name', array('title' => pht('Name'))) ->addColumn('info', array('title' => pht('Issues'))); foreach ($adjustments as $adjust) { $info = array(); foreach ($adjust['issues'] as $issue) { $info[] = PhabricatorConfigStorageSchema::getIssueName($issue); } $table->addRow(array( 'database' => $adjust['database'], 'table' => idx($adjust, 'table'), 'name' => idx($adjust, 'name'), 'info' => implode(', ', $info), )); } $console->writeOut("\n\n"); $table->draw(); if ($this->dryRun) { $console->writeOut( "%s\n", pht('DRYRUN: Would apply adjustments.')); return 0; } else if ($this->didInitialize) { // If we just initialized the database, continue without prompting. This // is nicer for first-time setup and there's no reasonable reason any // user would ever answer "no" to the prompt against an empty schema. } else if (!$this->force) { $console->writeOut( "\n%s\n", pht( "Found %s adjustment(s) to apply, detailed above.\n\n". "You can review adjustments in more detail from the web interface, ". "in Config > Database Status. To better understand the adjustment ". "workflow, see \"Managing Storage Adjustments\" in the ". "documentation.\n\n". "MySQL needs to copy table data to make some adjustments, so these ". "migrations may take some time.", phutil_count($adjustments))); $prompt = pht('Apply these schema adjustments?'); if (!phutil_console_confirm($prompt, $default_no = true)) { return 1; } } $console->writeOut( "%s\n", pht('Applying schema adjustments...')); $conn = $api->getConn(null); if ($unsafe) { queryfx($conn, 'SET SESSION sql_mode = %s', ''); } else { queryfx($conn, 'SET SESSION sql_mode = %s', 'STRICT_ALL_TABLES'); } $failed = array(); // We make changes in several phases. $phases = array( // Drop surplus autoincrements. This allows us to drop primary keys on // autoincrement columns. 'drop_auto', // Drop all keys we're going to adjust. This prevents them from // interfering with column changes. 'drop_keys', // Apply all database, table, and column changes. 'main', // Restore adjusted keys. 'add_keys', // Add missing autoincrements. 'add_auto', ); $bar = id(new PhutilConsoleProgressBar()) ->setTotal(count($adjustments) * count($phases)); foreach ($phases as $phase) { foreach ($adjustments as $adjust) { try { switch ($adjust['kind']) { case 'database': if ($phase == 'main') { queryfx( $conn, 'ALTER DATABASE %T CHARACTER SET = %s COLLATE = %s', $adjust['database'], $adjust['charset'], $adjust['collation']); } break; case 'table': if ($phase == 'main') { queryfx( $conn, 'ALTER TABLE %T.%T COLLATE = %s, ENGINE = %s', $adjust['database'], $adjust['table'], $adjust['collation'], $adjust['engine']); } break; case 'column': $apply = false; $auto = false; $new_auto = idx($adjust, 'auto'); if ($phase == 'drop_auto') { if ($new_auto === false) { $apply = true; $auto = false; } } else if ($phase == 'main') { $apply = true; if ($new_auto === false) { $auto = false; } else { $auto = $adjust['is_auto']; } } else if ($phase == 'add_auto') { if ($new_auto === true) { $apply = true; $auto = true; } } if ($apply) { $parts = array(); if ($auto) { $parts[] = qsprintf( $conn, 'AUTO_INCREMENT'); } if ($adjust['charset']) { + switch ($adjust['charset']) { + case 'binary': + $charset_value = qsprintf($conn, 'binary'); + break; + case 'utf8': + $charset_value = qsprintf($conn, 'utf8'); + break; + case 'utf8mb4': + $charset_value = qsprintf($conn, 'utf8mb4'); + break; + default: + throw new Exception( + pht( + 'Unsupported character set "%s".', + $adjust['charset'])); + } + + switch ($adjust['collation']) { + case 'binary': + $collation_value = qsprintf($conn, 'binary'); + break; + case 'utf8_general_ci': + $collation_value = qsprintf($conn, 'utf8_general_ci'); + break; + case 'utf8mb4_bin': + $collation_value = qsprintf($conn, 'utf8mb4_bin'); + break; + case 'utf8mb4_unicode_ci': + $collation_value = qsprintf($conn, 'utf8mb4_unicode_ci'); + break; + default: + throw new Exception( + pht( + 'Unsupported collation set "%s".', + $adjust['collation'])); + } + $parts[] = qsprintf( $conn, 'CHARACTER SET %Q COLLATE %Q', - $adjust['charset'], - $adjust['collation']); + $charset_value, + $collation_value); + } + + if ($parts) { + $parts = qsprintf($conn, '%LJ', $parts); + } else { + $parts = qsprintf($conn, ''); + } + + if ($adjust['nullable']) { + $nullable = qsprintf($conn, 'NULL'); + } else { + $nullable = qsprintf($conn, 'NOT NULL'); } + // TODO: We're using "%Z" here for the column type, which is + // technically unsafe. It would be nice to be able to use "%Q" + // instead, but this requires a fair amount of legwork to + // enumerate all column types. + queryfx( $conn, - 'ALTER TABLE %T.%T MODIFY %T %Q %Q %Q', + 'ALTER TABLE %T.%T MODIFY %T %Z %Q %Q', $adjust['database'], $adjust['table'], $adjust['name'], $adjust['type'], - implode(' ', $parts), - $adjust['nullable'] ? 'NULL' : 'NOT NULL'); + $parts, + $nullable); } break; case 'key': if (($phase == 'drop_keys') && $adjust['exists']) { if ($adjust['name'] == 'PRIMARY') { $key_name = 'PRIMARY KEY'; } else { $key_name = qsprintf($conn, 'KEY %T', $adjust['name']); } queryfx( $conn, 'ALTER TABLE %T.%T DROP %Q', $adjust['database'], $adjust['table'], $key_name); } if (($phase == 'add_keys') && $adjust['keep']) { // Different keys need different creation syntax. Notable // special cases are primary keys and fulltext keys. if ($adjust['name'] == 'PRIMARY') { - $key_name = 'PRIMARY KEY'; + $key_name = qsprintf($conn, 'PRIMARY KEY'); } else if ($adjust['indexType'] == 'FULLTEXT') { $key_name = qsprintf($conn, 'FULLTEXT %T', $adjust['name']); } else { if ($adjust['unique']) { $key_name = qsprintf( $conn, 'UNIQUE KEY %T', $adjust['name']); } else { $key_name = qsprintf( $conn, '/* NONUNIQUE */ KEY %T', $adjust['name']); } } queryfx( $conn, - 'ALTER TABLE %T.%T ADD %Q (%Q)', + 'ALTER TABLE %T.%T ADD %Q (%LK)', $adjust['database'], $adjust['table'], $key_name, - implode(', ', $adjust['columns'])); + $adjust['columns']); } break; default: throw new Exception( pht('Unknown schema adjustment kind "%s"!', $adjust['kind'])); } } catch (AphrontQueryException $ex) { $failed[] = array($adjust, $ex); } $bar->update(1); } } $bar->done(); if (!$failed) { $console->writeOut( "%s\n", pht('Completed applying all schema adjustments.')); $err = 0; } else { $table = id(new PhutilConsoleTable()) ->addColumn('target', array('title' => pht('Target'))) ->addColumn('error', array('title' => pht('Error'))); foreach ($failed as $failure) { list($adjust, $ex) = $failure; $pieces = array_select_keys( $adjust, array('database', 'table', 'name')); $pieces = array_filter($pieces); $target = implode('.', $pieces); $table->addRow( array( 'target' => $target, 'error' => $ex->getMessage(), )); } $console->writeOut("\n"); $table->draw(); $console->writeOut( "\n%s\n", pht('Failed to make some schema adjustments, detailed above.')); $console->writeOut( "%s\n", pht( 'For help troubleshooting adjustments, see "Managing Storage '. 'Adjustments" in the documentation.')); $err = 1; } return $this->printErrors($errors, $err); } private function findAdjustments( PhabricatorStorageManagementAPI $api) { list($comp, $expect, $actual) = $this->loadSchemata($api); $issue_charset = PhabricatorConfigStorageSchema::ISSUE_CHARSET; $issue_collation = PhabricatorConfigStorageSchema::ISSUE_COLLATION; $issue_columntype = PhabricatorConfigStorageSchema::ISSUE_COLUMNTYPE; $issue_surpluskey = PhabricatorConfigStorageSchema::ISSUE_SURPLUSKEY; $issue_missingkey = PhabricatorConfigStorageSchema::ISSUE_MISSINGKEY; $issue_columns = PhabricatorConfigStorageSchema::ISSUE_KEYCOLUMNS; $issue_unique = PhabricatorConfigStorageSchema::ISSUE_UNIQUE; $issue_longkey = PhabricatorConfigStorageSchema::ISSUE_LONGKEY; $issue_auto = PhabricatorConfigStorageSchema::ISSUE_AUTOINCREMENT; $issue_engine = PhabricatorConfigStorageSchema::ISSUE_ENGINE; $adjustments = array(); $errors = array(); foreach ($comp->getDatabases() as $database_name => $database) { foreach ($this->findErrors($database) as $issue) { $errors[] = array( 'database' => $database_name, 'issue' => $issue, ); } $expect_database = $expect->getDatabase($database_name); $actual_database = $actual->getDatabase($database_name); if (!$expect_database || !$actual_database) { // If there's a real issue here, skip this stuff. continue; } if ($actual_database->getAccessDenied()) { // If we can't access the database, we can't access the tables either. continue; } $issues = array(); if ($database->hasIssue($issue_charset)) { $issues[] = $issue_charset; } if ($database->hasIssue($issue_collation)) { $issues[] = $issue_collation; } if ($issues) { $adjustments[] = array( 'kind' => 'database', 'database' => $database_name, 'issues' => $issues, 'charset' => $expect_database->getCharacterSet(), 'collation' => $expect_database->getCollation(), ); } foreach ($database->getTables() as $table_name => $table) { foreach ($this->findErrors($table) as $issue) { $errors[] = array( 'database' => $database_name, 'table' => $table_name, 'issue' => $issue, ); } $expect_table = $expect_database->getTable($table_name); $actual_table = $actual_database->getTable($table_name); if (!$expect_table || !$actual_table) { continue; } $issues = array(); if ($table->hasIssue($issue_collation)) { $issues[] = $issue_collation; } if ($table->hasIssue($issue_engine)) { $issues[] = $issue_engine; } if ($issues) { $adjustments[] = array( 'kind' => 'table', 'database' => $database_name, 'table' => $table_name, 'issues' => $issues, 'collation' => $expect_table->getCollation(), 'engine' => $expect_table->getEngine(), ); } foreach ($table->getColumns() as $column_name => $column) { foreach ($this->findErrors($column) as $issue) { $errors[] = array( 'database' => $database_name, 'table' => $table_name, 'name' => $column_name, 'issue' => $issue, ); } $expect_column = $expect_table->getColumn($column_name); $actual_column = $actual_table->getColumn($column_name); if (!$expect_column || !$actual_column) { continue; } $issues = array(); if ($column->hasIssue($issue_collation)) { $issues[] = $issue_collation; } if ($column->hasIssue($issue_charset)) { $issues[] = $issue_charset; } if ($column->hasIssue($issue_columntype)) { $issues[] = $issue_columntype; } if ($column->hasIssue($issue_auto)) { $issues[] = $issue_auto; } if ($issues) { if ($expect_column->getCharacterSet() === null) { // For non-text columns, we won't be specifying a collation or // character set. $charset = null; $collation = null; } else { $charset = $expect_column->getCharacterSet(); $collation = $expect_column->getCollation(); } $adjustment = array( 'kind' => 'column', 'database' => $database_name, 'table' => $table_name, 'name' => $column_name, 'issues' => $issues, 'collation' => $collation, 'charset' => $charset, 'type' => $expect_column->getColumnType(), // NOTE: We don't adjust column nullability because it is // dangerous, so always use the current nullability. 'nullable' => $actual_column->getNullable(), // NOTE: This always stores the current value, because we have // to make these updates separately. 'is_auto' => $actual_column->getAutoIncrement(), ); if ($column->hasIssue($issue_auto)) { $adjustment['auto'] = $expect_column->getAutoIncrement(); } $adjustments[] = $adjustment; } } foreach ($table->getKeys() as $key_name => $key) { foreach ($this->findErrors($key) as $issue) { $errors[] = array( 'database' => $database_name, 'table' => $table_name, 'name' => $key_name, 'issue' => $issue, ); } $expect_key = $expect_table->getKey($key_name); $actual_key = $actual_table->getKey($key_name); $issues = array(); $keep_key = true; if ($key->hasIssue($issue_surpluskey)) { $issues[] = $issue_surpluskey; $keep_key = false; } if ($key->hasIssue($issue_missingkey)) { $issues[] = $issue_missingkey; } if ($key->hasIssue($issue_columns)) { $issues[] = $issue_columns; } if ($key->hasIssue($issue_unique)) { $issues[] = $issue_unique; } // NOTE: We can't really fix this, per se, but we may need to remove // the key to change the column type. In the best case, the new // column type won't be overlong and recreating the key really will // fix the issue. In the worst case, we get the right column type and // lose the key, which is still better than retaining the key having // the wrong column type. if ($key->hasIssue($issue_longkey)) { $issues[] = $issue_longkey; } if ($issues) { $adjustment = array( 'kind' => 'key', 'database' => $database_name, 'table' => $table_name, 'name' => $key_name, 'issues' => $issues, 'exists' => (bool)$actual_key, 'keep' => $keep_key, ); if ($keep_key) { $adjustment += array( 'columns' => $expect_key->getColumnNames(), 'unique' => $expect_key->getUnique(), 'indexType' => $expect_key->getIndexType(), ); } $adjustments[] = $adjustment; } } } } return array($adjustments, $errors); } private function findErrors(PhabricatorConfigStorageSchema $schema) { $result = array(); foreach ($schema->getLocalIssues() as $issue) { $status = PhabricatorConfigStorageSchema::getIssueStatus($issue); if ($status == PhabricatorConfigStorageSchema::STATUS_FAIL) { $result[] = $issue; } } return $result; } private function printErrors(array $errors, $default_return) { if (!$errors) { return $default_return; } $console = PhutilConsole::getConsole(); $table = id(new PhutilConsoleTable()) ->addColumn('target', array('title' => pht('Target'))) ->addColumn('error', array('title' => pht('Error'))); $any_surplus = false; $all_surplus = true; $any_access = false; $all_access = true; foreach ($errors as $error) { $pieces = array_select_keys( $error, array('database', 'table', 'name')); $pieces = array_filter($pieces); $target = implode('.', $pieces); $name = PhabricatorConfigStorageSchema::getIssueName($error['issue']); $issue = $error['issue']; if ($issue === PhabricatorConfigStorageSchema::ISSUE_SURPLUS) { $any_surplus = true; } else { $all_surplus = false; } if ($issue === PhabricatorConfigStorageSchema::ISSUE_ACCESSDENIED) { $any_access = true; } else { $all_access = false; } $table->addRow( array( 'target' => $target, 'error' => $name, )); } $console->writeOut("\n"); $table->draw(); $console->writeOut("\n"); $message = array(); if ($all_surplus) { $message[] = pht( 'You have surplus schemata (extra tables or columns which Phabricator '. 'does not expect). For information on resolving these '. 'issues, see the "Surplus Schemata" section in the "Managing Storage '. 'Adjustments" article in the documentation.'); } else if ($all_access) { $message[] = pht( 'The user you are connecting to MySQL with does not have the correct '. 'permissions, and can not access some databases or tables that it '. 'needs to be able to access. GRANT the user additional permissions.'); } else { $message[] = pht( 'The schemata have errors (detailed above) which the adjustment '. 'workflow can not fix.'); if ($any_access) { $message[] = pht( 'Some of these errors are caused by access control problems. '. 'The user you are connecting with does not have permission to see '. 'all of the database or tables that Phabricator uses. You need to '. 'GRANT the user more permission, or use a different user.'); } if ($any_surplus) { $message[] = pht( 'Some of these errors are caused by surplus schemata (extra '. 'tables or columns which Phabricator does not expect). These are '. 'not serious. For information on resolving these issues, see the '. '"Surplus Schemata" section in the "Managing Storage Adjustments" '. 'article in the documentation.'); } $message[] = pht( 'If you are not developing Phabricator itself, report this issue to '. 'the upstream.'); $message[] = pht( 'If you are developing Phabricator, these errors usually indicate '. 'that your schema specifications do not agree with the schemata your '. 'code actually builds.'); } $message = implode("\n\n", $message); if ($all_surplus) { $console->writeOut( "** %s **\n\n%s\n", pht('SURPLUS SCHEMATA'), phutil_console_wrap($message)); } else if ($all_access) { $console->writeOut( "** %s **\n\n%s\n", pht('ACCESS DENIED'), phutil_console_wrap($message)); } else { $console->writeOut( "** %s **\n\n%s\n", pht('SCHEMATA ERRORS'), phutil_console_wrap($message)); } return 2; } final protected function upgradeSchemata( array $apis, $apply_only = null, $no_quickstart = false, $init_only = false) { $locks = array(); foreach ($apis as $api) { $locks[] = $this->lock($api); } try { $this->doUpgradeSchemata($apis, $apply_only, $no_quickstart, $init_only); } catch (Exception $ex) { foreach ($locks as $lock) { $lock->unlock(); } throw $ex; } foreach ($locks as $lock) { $lock->unlock(); } } final private function doUpgradeSchemata( array $apis, $apply_only, $no_quickstart, $init_only) { $patches = $this->patches; $is_dryrun = $this->dryRun; $api_map = array(); foreach ($apis as $api) { $api_map[$api->getRef()->getRefKey()] = $api; } foreach ($api_map as $ref_key => $api) { $applied = $api->getAppliedPatches(); $needs_init = ($applied === null); if (!$needs_init) { continue; } if ($is_dryrun) { echo tsprintf( "%s\n", pht( 'DRYRUN: Storage on host "%s" does not exist yet, so it '. 'would be created.', $ref_key)); continue; } if ($apply_only) { throw new PhutilArgumentUsageException( pht( 'Storage on host "%s" has not been initialized yet. You must '. 'initialize storage before selectively applying patches.', $ref_key)); } // If we're initializing storage for the first time on any host, track // it so that we can give the user a nicer experience during the // subsequent adjustment phase. $this->didInitialize = true; $legacy = $api->getLegacyPatches($patches); if ($legacy || $no_quickstart || $init_only) { // If we have legacy patches, we can't quickstart. $api->createDatabase('meta_data'); $api->createTable( 'meta_data', 'patch_status', array( 'patch VARCHAR(255) NOT NULL PRIMARY KEY COLLATE utf8_general_ci', 'applied INT UNSIGNED NOT NULL', )); foreach ($legacy as $patch) { $api->markPatchApplied($patch); } } else { echo tsprintf( "%s\n", pht( 'Loading quickstart template onto "%s"...', $ref_key)); $root = dirname(phutil_get_library_root('phabricator')); $sql = $root.'/resources/sql/quickstart.sql'; $api->applyPatchSQL($sql); } } if ($init_only) { echo pht('Storage initialized.')."\n"; return 0; } $applied_map = array(); $state_map = array(); foreach ($api_map as $ref_key => $api) { $applied = $api->getAppliedPatches(); // If we still have nothing applied, this is a dry run and we didn't // actually initialize storage. Here, just do nothing. if ($applied === null) { if ($is_dryrun) { continue; } else { throw new Exception( pht( 'Database initialization on host "%s" applied no patches!', $ref_key)); } } $applied = array_fuse($applied); $state_map[$ref_key] = $applied; if ($apply_only) { if (isset($applied[$apply_only])) { if (!$this->force && !$is_dryrun) { echo phutil_console_wrap( pht( 'Patch "%s" has already been applied on host "%s". Are you '. 'sure you want to apply it again? This may put your storage '. 'in a state that the upgrade scripts can not automatically '. 'manage.', $apply_only, $ref_key)); if (!phutil_console_confirm(pht('Apply patch again?'))) { echo pht('Cancelled.')."\n"; return 1; } } // Mark this patch as not yet applied on this host. unset($applied[$apply_only]); } } $applied_map[$ref_key] = $applied; } // If we're applying only a specific patch, select just that patch. if ($apply_only) { $patches = array_select_keys($patches, array($apply_only)); } // Apply each patch to each database. We apply patches patch-by-patch, // not database-by-database: for each patch we apply it to every database, // then move to the next patch. // We must do this because ".php" patches may depend on ".sql" patches // being up to date on all masters, and that will work fine if we put each // patch on every host before moving on. If we try to bring database hosts // up to date one at a time we can end up in a big mess. $duration_map = array(); // First, find any global patches which have been applied to ANY database. // We are just going to mark these as applied without actually running // them. Otherwise, adding new empty masters to an existing cluster will // try to apply them against invalid states. foreach ($patches as $key => $patch) { if ($patch->getIsGlobalPatch()) { foreach ($applied_map as $ref_key => $applied) { if (isset($applied[$key])) { $duration_map[$key] = 1; } } } } while (true) { $applied_something = false; foreach ($patches as $key => $patch) { // First, check if any databases need this patch. We can just skip it // if it has already been applied everywhere. $need_patch = array(); foreach ($applied_map as $ref_key => $applied) { if (isset($applied[$key])) { continue; } $need_patch[] = $ref_key; } if (!$need_patch) { unset($patches[$key]); continue; } // Check if we can apply this patch yet. Before we can apply a patch, // all of the dependencies for the patch must have been applied on all // databases. Requiring that all databases stay in sync prevents one // database from racing ahead if it happens to get a patch that nothing // else has yet. $missing_patch = null; foreach ($patch->getAfter() as $after) { foreach ($applied_map as $ref_key => $applied) { if (isset($applied[$after])) { // This database already has the patch. We can apply it to // other databases but don't need to apply it here. continue; } $missing_patch = $after; break 2; } } if ($missing_patch) { if ($apply_only) { echo tsprintf( "%s\n", pht( 'Unable to apply patch "%s" because it depends on patch '. '"%s", which has not been applied on some hosts: %s.', $apply_only, $missing_patch, implode(', ', $need_patch))); return 1; } else { // Some databases are missing the dependencies, so keep trying // other patches instead. If everything goes right, we'll apply the // dependencies and then come back and apply this patch later. continue; } } $is_global = $patch->getIsGlobalPatch(); $patch_apis = array_select_keys($api_map, $need_patch); foreach ($patch_apis as $ref_key => $api) { if ($is_global) { // If this is a global patch which we previously applied, just // read the duration from the map without actually applying // the patch. $duration = idx($duration_map, $key); } else { $duration = null; } if ($duration === null) { if ($is_dryrun) { echo tsprintf( "%s\n", pht( 'DRYRUN: Would apply patch "%s" to host "%s".', $key, $ref_key)); } else { echo tsprintf( "%s\n", pht( 'Applying patch "%s" to host "%s"...', $key, $ref_key)); } $t_begin = microtime(true); if (!$is_dryrun) { $api->applyPatch($patch); } $t_end = microtime(true); $duration = ($t_end - $t_begin); $duration_map[$key] = $duration; } // If we're explicitly reapplying this patch, we don't need to // mark it as applied. if (!isset($state_map[$ref_key][$key])) { if (!$is_dryrun) { $api->markPatchApplied($key, ($t_end - $t_begin)); } $applied_map[$ref_key][$key] = true; } } // We applied this everywhere, so we're done with the patch. unset($patches[$key]); $applied_something = true; } if (!$applied_something) { if ($patches) { throw new Exception( pht( 'Some patches could not be applied: %s', implode(', ', array_keys($patches)))); } else if (!$is_dryrun && !$apply_only) { echo pht( 'Storage is up to date. Use "%s" for details.', 'storage status')."\n"; } break; } } } final protected function getBareHostAndPort($host) { // Split out port information, since the command-line client requires a // separate flag for the port. $uri = new PhutilURI('mysql://'.$host); if ($uri->getPort()) { $port = $uri->getPort(); $bare_hostname = $uri->getDomain(); } else { $port = null; $bare_hostname = $host; } return array($bare_hostname, $port); } /** * Acquires a @{class:PhabricatorGlobalLock}. * * @return PhabricatorGlobalLock */ final protected function lock(PhabricatorStorageManagementAPI $api) { // Although we're holding this lock on different databases so it could // have the same name on each as far as the database is concerned, the // locks would be the same within this process. $parameters = array( 'refKey' => $api->getRef()->getRefKey(), ); // We disable logging for this lock because we may not have created the // log table yet, or may need to adjust it. return PhabricatorGlobalLock::newLock('adjust', $parameters) ->useSpecificConnection($api->getConn(null)) ->setDisableLogging(true) ->lock(); } final protected function analyzeTables( PhabricatorStorageManagementAPI $api) { // Analyzing tables can sometimes have a significant effect on query // performance, particularly for the fulltext ngrams tables. See T12819 // for some specific examples. $conn = $api->getConn(null); $patches = $this->getPatches(); $databases = $api->getDatabaseList($patches, true); $this->logInfo( pht('ANALYZE'), pht('Analyzing tables...')); $targets = array(); foreach ($databases as $database) { queryfx($conn, 'USE %C', $database); $tables = queryfx_all($conn, 'SHOW TABLE STATUS'); foreach ($tables as $table) { $table_name = $table['Name']; $targets[] = array( 'database' => $database, 'table' => $table_name, ); } } $bar = id(new PhutilConsoleProgressBar()) ->setTotal(count($targets)); foreach ($targets as $target) { queryfx( $conn, 'ANALYZE TABLE %T.%T', $target['database'], $target['table']); $bar->update(1); } $bar->done(); $this->logOkay( pht('ANALYZED'), pht( 'Analyzed %d table(s).', count($targets))); } } diff --git a/src/view/form/control/AphrontFormSubmitControl.php b/src/view/form/control/AphrontFormSubmitControl.php index c2bf716848..7e948bd135 100644 --- a/src/view/form/control/AphrontFormSubmitControl.php +++ b/src/view/form/control/AphrontFormSubmitControl.php @@ -1,48 +1,62 @@ setTag('a') ->setHref($href) ->setText($label) ->setColor(PHUIButtonView::GREY); $this->addButton($button); return $this; } public function addButton(PHUIButtonView $button) { $this->buttons[] = $button; return $this; } + public function addSigil($sigil) { + $this->sigils[] = $sigil; + return $this; + } + protected function getCustomControlClass() { return 'aphront-form-control-submit'; } protected function renderInput() { $submit_button = null; if ($this->getValue()) { - $submit_button = phutil_tag( + + if ($this->sigils) { + $sigils = $this->sigils; + } else { + $sigils = null; + } + + $submit_button = javelin_tag( 'button', array( 'type' => 'submit', 'name' => '__submit__', + 'sigil' => $sigils, 'disabled' => $this->getDisabled() ? 'disabled' : null, ), $this->getValue()); } return array( $submit_button, $this->buttons, ); } } diff --git a/webroot/rsrc/css/aphront/tooltip.css b/webroot/rsrc/css/aphront/tooltip.css index e6ff46cbed..754e647d0a 100644 --- a/webroot/rsrc/css/aphront/tooltip.css +++ b/webroot/rsrc/css/aphront/tooltip.css @@ -1,102 +1,108 @@ /** * @provides aphront-tooltip-css */ .jx-tooltip-container { position: absolute; padding: 5px; + + /* In Chrome, moving the cursor into the empty space next to the "caret" on + the caret side of the tooltip can cause the tooltip to flicker rapidly + because the cursor hits the container. To stop this, prevent cursor + events on the container. See T8440. */ + pointer-events: none; } .jx-tooltip-appear { animation: 0.5s tooltip-appear; /* Without this, there's a nasty pop-in effect at the end of the animation when Safari changes font smoothing. The text becomes visibly more bold after the last frame of animation. */ -webkit-font-smoothing: subpixel-antialiased; } @keyframes tooltip-appear { 0% { opacity: 0; } 100% { opacity: 1; } } .jx-tooltip-hidden { opacity: 0; } .jx-tooltip-inner { position: relative; background: #000; border-radius: 3px; } .jx-tooltip { color: #fff; font-size: {$normalfontsize}; -webkit-font-smoothing: antialiased; font-weight: bold; padding: 6px 8px; overflow: hidden; white-space: pre-wrap; } .jx-tooltip:after { border: solid transparent; content: " "; height: 0; width: 0; position: absolute; pointer-events: none; border-color: rgba({$alphablack}, 0); border-width: 5px; } .jx-tooltip-align-E { margin-left: 5px; } .jx-tooltip-align-E .jx-tooltip:after { margin-top: -5px; border-right-color: #000; right: 100%; top: 50%; } .jx-tooltip-align-E { margin-right: 5px; } .jx-tooltip-align-W .jx-tooltip:after { margin-top: -5px; border-left-color: #000; left: 100%; top: 50%; } .jx-tooltip-align-N { margin-bottom: 5px; } .jx-tooltip-align-N .jx-tooltip:after { margin-left: -5px; border-top-color: #000; top: 100%; left: 50%; } .jx-tooltip-align-N { margin-top: 5px; } .jx-tooltip-align-S .jx-tooltip:after { margin-left: -5px; border-bottom-color: #000; bottom: 100%; left: 50%; } diff --git a/webroot/rsrc/css/phui/phui-hovercard.css b/webroot/rsrc/css/phui/phui-hovercard.css index 1f01362b6f..876b0b6e53 100644 --- a/webroot/rsrc/css/phui/phui-hovercard.css +++ b/webroot/rsrc/css/phui/phui-hovercard.css @@ -1,115 +1,117 @@ /** * @provides phui-hovercard-view-css */ .jx-hovercard-container { position: absolute; } .phui-hovercard-wrapper { width: 400px; } .device-phone .phui-hovercard-wrapper { width: 300px; } .phui-hovercard-container { width: 100%; box-shadow: {$dropshadow}; border: 1px solid {$lightblueborder}; border-radius: 3px; background-color: {$page.content}; } .phui-hovercard-head .phui-header-shell { padding: 6px 8px 6px 12px; background-color: {$bluebackground}; border-top-left-radius: 3px; border-top-right-radius: 3px; } .phui-hovercard-head .phui-header-header { font-size: {$biggerfontsize}; } .phui-hovercard-head .phui-tag-type-state { color: {$darkbluetext}; text-shadow: none; font-weight: normal; } .phui-hovercard-tags { float: right; white-space: normal; } .phui-hovercard-body { padding: 12px; color: {$darkgreytext}; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; position: relative; } .phui-hovercard-body-item { margin: 4px 0 0 0; + overflow: hidden; + text-overflow: ellipsis; } .phui-hovercard-body-header { font-size: 14px; padding-bottom: 4px; color: {$darkgreytext}; line-height: 18px; } .phui-hovercard-body .phui-hovercard-body-image { width: 58px; } .phui-hovercard-body .phui-hovercard-body-details { margin-left: 58px; } .phui-hovercard-body .profile-header-picture-frame { float: left; width: 50px; height: 50px; background-position: center; background-repeat: no-repeat; background-size: 100%; } .hovercard-badges { margin: 6px 0 0 0; padding: 4px; background: {$page.background}; border-bottom-left-radius: 3px; border-bottom-right-radius: 3px; } .hovercard-badges .phui-badge-flex-item { float: left; } .phui-hovercard-tail { width: 396px; float: left; padding: 2px; border-top: 1px solid {$thinblueborder}; border-bottom-left-radius: 3px; border-bottom-right-radius: 3px; } .phui-hovercard-tail button, .phui-hovercard-tail a.button { margin: 3px; } .phui-hovercard-wrapper .hovercard-task-view { box-shadow: 0px 4px 16px rgba(0,0,0,.2); } .hovercard-task-view .phui-oi-disabled.phui-workcard { background-color: #fff; } diff --git a/webroot/rsrc/externals/javelin/core/Event.js b/webroot/rsrc/externals/javelin/core/Event.js index d4e04be7e2..787a453c4d 100644 --- a/webroot/rsrc/externals/javelin/core/Event.js +++ b/webroot/rsrc/externals/javelin/core/Event.js @@ -1,361 +1,375 @@ /** * @requires javelin-install * @provides javelin-event * @javelin */ /** * A generic event, routed by @{class:JX.Stratcom}. All events within Javelin * are represented by a {@class:JX.Event}, regardless of whether they originate * from a native DOM event (like a mouse click) or are custom application * events. * * See @{article:Concepts: Event Delegation} for an introduction to Javelin's * event delegation model. * * Events have a propagation model similar to native Javascript events, in that * they can be stopped with stop() (which stops them from continuing to * propagate to other handlers) or prevented with prevent() (which prevents them * from taking their default action, like following a link). You can do both at * once with kill(). * * @task stop Stopping Event Behaviors * @task info Getting Event Information */ JX.install('Event', { members : { /** * Stop an event from continuing to propagate. No other handler will * receive this event, but its default behavior will still occur. See * ""Using Events"" for more information on the distinction between * 'stopping' and 'preventing' an event. See also prevent() (which prevents * an event but does not stop it) and kill() (which stops and prevents an * event). * * @return this * @task stop */ stop : function() { var r = this.getRawEvent(); if (r) { r.cancelBubble = true; r.stopPropagation && r.stopPropagation(); } this.setStopped(true); return this; }, /** * Prevent an event's default action. This depends on the event type, but * the common default actions are following links, submitting forms, * and typing text. Event prevention is generally used when you have a link * or form which work properly without Javascript but have a specialized * Javascript behavior. When you intercept the event and make the behavior * occur, you prevent it to keep the browser from following the link. * * Preventing an event does not stop it from propagating, so other handlers * will still receive it. See ""Using Events"" for more information on the * distinction between 'stopping' and 'preventing' an event. See also * stop() (which stops an event but does not prevent it) and kill() * (which stops and prevents an event). * * @return this * @task stop */ prevent : function() { var r = this.getRawEvent(); if (r) { r.returnValue = false; r.preventDefault && r.preventDefault(); } this.setPrevented(true); return this; }, /** * Stop and prevent an event, which stops it from propagating and prevents * its defualt behavior. This is a convenience function, see stop() and * prevent() for information on what it means to stop or prevent an event. * * @return this * @task stop */ kill : function() { this.prevent(); this.stop(); return this; }, /** * Get the special key (like tab or return), if any, associated with this * event. Browsers report special keys differently; this method allows you * to identify a keypress in a browser-agnostic way. Note that this detects * only some special keys: delete, tab, return escape, left, up, right, * down. * * For example, if you want to react to the escape key being pressed, you * could install a listener like this: * * JX.Stratcom.listen('keydown', 'example', function(e) { * if (e.getSpecialKey() == 'esc') { * JX.log("You pressed 'Escape'! Well done! Bravo!"); * } * }); * * @return string|null ##null## if there is no associated special key, * or one of the strings 'delete', 'tab', 'return', * 'esc', 'left', 'up', 'right', or 'down'. * @task info */ getSpecialKey : function() { var r = this.getRawEvent(); if (!r) { return null; } return JX.Event._keymap[r.keyCode] || null; }, /** * Get whether the mouse button associated with the mouse event is the * right-side button in a browser-agnostic way. * * @return bool * @task info */ isRightButton : function() { var r = this.getRawEvent(); return r.which == 3 || r.button == 2; }, + + /** + * Get whether the mouse button associated with the mouse event is the + * left-side button in a browser-agnostic way. + * + * @return bool + * @task info + */ + isLeftButton: function() { + var r = this.getRawEvent(); + return (r.which == 1 || r.button == 0); + }, + + /** * Determine if a mouse event is a normal event (left mouse button, no * modifier keys). * * @return bool * @task info */ isNormalMouseEvent : function() { var supportedEvents = {'click': 1, 'mouseup': 1, 'mousedown': 1}; if (!(this.getType() in supportedEvents)) { return false; } var r = this.getRawEvent(); if (r.metaKey || r.altKey || r.ctrlKey || r.shiftKey) { return false; } if (('which' in r) && (r.which != 1)) { return false; } if (('button' in r) && r.button) { if ('which' in r) { return false; // IE won't have which and has left click == 1 here } else if (r.button != 1) { return false; } } return true; }, /** * Determine if a click event is a normal click (left mouse button, no * modifier keys). * * @return bool * @task info */ isNormalClick : function() { if (this.getType() != 'click') { return false; } return this.isNormalMouseEvent(); }, /** * Get the node corresponding to the specified key in this event's node map. * This is a simple helper method that makes the API for accessing nodes * less ugly. * * JX.Stratcom.listen('click', 'tag:a', function(e) { * var a = e.getNode('tag:a'); * // do something with the link that was clicked * }); * * @param string sigil or stratcom node key * @return node|null Node mapped to the specified key, or null if it the * key does not exist. The available keys include: * - 'tag:'+tag - first node of each type * - 'id:'+id - all nodes with an id * - sigil - first node of each sigil * @task info */ getNode : function(key) { return this.getNodes()[key] || null; }, /** * Get the metadata associated with the node that corresponds to the key * in this event's node map. This is a simple helper method that makes * the API for accessing metadata associated with specific nodes less ugly. * * JX.Stratcom.listen('click', 'tag:a', function(event) { * var anchorData = event.getNodeData('tag:a'); * // do something with the metadata of the link that was clicked * }); * * @param string sigil or stratcom node key * @return dict dictionary of the node's metadata * @task info */ getNodeData : function(key) { // Evade static analysis - JX.Stratcom return JX['Stratcom'].getData(this.getNode(key)); } }, statics : { _keymap : { 8 : 'delete', 9 : 'tab', // On Windows and Linux, Chrome sends '10' for return. On Mac OS X, it // sends 13. Other browsers evidence varying degrees of diversity in their // behavior. Treat '10' and '13' identically. 10 : 'return', 13 : 'return', 27 : 'esc', 37 : 'left', 38 : 'up', 39 : 'right', 40 : 'down', 63232 : 'up', 63233 : 'down', 62234 : 'left', 62235 : 'right' } }, properties : { /** * Native Javascript event which generated this @{class:JX.Event}. Not every * event is generated by a native event, so there may be ##null## in * this field. * * @type Event|null * @task info */ rawEvent : null, /** * String describing the event type, like 'click' or 'mousedown'. This * may also be an application or object event. * * @type string * @task info */ type : null, /** * If available, the DOM node where this event occurred. For example, if * this event is a click on a button, the target will be the button which * was clicked. Application events will not have a target, so this property * will return the value ##null##. * * @type DOMNode|null * @task info */ target : null, /** * Metadata attached to nodes associated with this event. * * For native events, the DOM is walked from the event target to the root * element. Each sigil which is encountered while walking up the tree is * added to the map as a key. If the node has associated metainformation, * it is set as the value; otherwise, the value is null. * * @type dict * @task info */ data : null, /** * Sigil path this event was activated from. TODO: explain this * * @type list * @task info */ path : [], /** * True if propagation of the event has been stopped. See stop(). * * @type bool * @task stop */ stopped : false, /** * True if default behavior of the event has been prevented. See prevent(). * * @type bool * @task stop */ prevented : false, /** * @task info */ nodes : {}, /** * @task info */ nodeDistances : {}, /** * True if this is a cursor event that was caused by a touch interaction * rather than a mouse device interaction. * * @type bool * @taks info */ isTouchEvent: false }, /** * @{class:JX.Event} installs a toString() method in ##__DEV__## which allows * you to log or print events and get a reasonable representation of them: * * Event<'click', ['path', 'stuff'], [object HTMLDivElement]> */ initialize : function() { if (__DEV__) { JX.Event.prototype.toString = function() { var path = '['+this.getPath().join(', ')+']'; var type = this.getType(); if (this.getIsTouchEvent()) { type = type + '/touch'; } return 'Event<'+type+', '+path+', '+this.getTarget()+'>'; }; } } }); diff --git a/webroot/rsrc/externals/javelin/core/init.js b/webroot/rsrc/externals/javelin/core/init.js index fb10b0f4c3..61c9f13d6d 100644 --- a/webroot/rsrc/externals/javelin/core/init.js +++ b/webroot/rsrc/externals/javelin/core/init.js @@ -1,262 +1,267 @@ /** * Javelin core; installs Javelin and Stratcom event delegation. * * @provides javelin-magical-init * * @javelin-installs JX.__rawEventQueue * @javelin-installs JX.__simulate * @javelin-installs JX.__allowedEvents * @javelin-installs JX.enableDispatch * @javelin-installs JX.onload * @javelin-installs JX.flushHoldingQueue * * @javelin */ (function() { if (window.JX) { return; } window.JX = {}; // The holding queues hold calls to functions (JX.install() and JX.behavior()) // before they load, so if you're async-loading them later in the document // the page will execute correctly regardless of the order resources arrive // in. var holding_queues = {}; function makeHoldingQueue(name) { if (JX[name]) { return; } holding_queues[name] = []; JX[name] = function() { holding_queues[name].push(arguments); }; } JX.flushHoldingQueue = function(name, fn) { for (var ii = 0; ii < holding_queues[name].length; ii++) { fn.apply(null, holding_queues[name][ii]); } holding_queues[name] = {}; }; makeHoldingQueue('install'); makeHoldingQueue('behavior'); makeHoldingQueue('install-init'); var loaded = false; var onload = []; var master_event_queue = []; var root = document.documentElement; var has_add_event_listener = !!root.addEventListener; window.__DEV__ = !!root.getAttribute('data-developer-mode'); JX.__rawEventQueue = function(what) { master_event_queue.push(what); var ii; var Stratcom = JX['Stratcom']; if (!loaded && what.type == 'domready') { var initializers = []; var tags = JX.DOM.scry(document.body, 'data'); for (ii = 0; ii < tags.length; ii++) { // Ignore tags which are not immediate children of the document // body. If an attacker somehow injects arbitrary tags into the // content of the document, that should not give them access to // modify initialization behaviors. if (tags[ii].parentNode !== document.body) { continue; } var tag_kind = tags[ii].getAttribute('data-javelin-init-kind'); var tag_data = tags[ii].getAttribute('data-javelin-init-data'); tag_data = JX.JSON.parse(tag_data); initializers.push({kind: tag_kind, data: tag_data}); } Stratcom.initialize(initializers); loaded = true; } if (loaded) { // Empty the queue now so that exceptions don't cause us to repeatedly // try to handle events. var local_queue = master_event_queue; master_event_queue = []; for (ii = 0; ii < local_queue.length; ++ii) { var evt = local_queue[ii]; // Sometimes IE gives us events which throw when ".type" is accessed; // just ignore them since we can't meaningfully dispatch them. TODO: // figure out where these are coming from. try { var test = evt.type; } catch (x) { continue; } if (evt.type == 'domready') { // NOTE: Firefox interprets "document.body.id = null" as the string // literal "null". document.body && (document.body.id = ''); for (var jj = 0; jj < onload.length; jj++) { onload[jj](); } } Stratcom.dispatch(evt); } } else { var target = what.srcElement || what.target; if (target && (what.type in {click: 1, submit: 1}) && target.getAttribute && target.getAttribute('data-mustcapture') === '1') { what.returnValue = false; what.preventDefault && what.preventDefault(); document.body.id = 'event_capture'; // For versions of IE that use attachEvent, the event object is somehow // stored globally by reference, and all the references we push to the // master_event_queue will always refer to the most recent event. We // work around this by popping the useless global event off the queue, // and pushing a clone of the event that was just fired using the IE's // proprietary createEventObject function. // see: http://msdn.microsoft.com/en-us/library/ms536390(v=vs.85).aspx if (!add_event_listener && document.createEventObject) { master_event_queue.pop(); master_event_queue.push(document.createEventObject(what)); } return false; } } }; JX.enableDispatch = function(target, type) { if (__DEV__) { JX.__allowedEvents[type] = true; } if (target.addEventListener) { target.addEventListener(type, JX.__rawEventQueue, true); } else if (target.attachEvent) { target.attachEvent('on' + type, JX.__rawEventQueue); } }; var document_events = [ 'click', 'dblclick', 'change', 'submit', 'keypress', 'mousedown', 'mouseover', 'mouseout', 'keyup', 'keydown', 'input', 'drop', 'dragenter', 'dragleave', 'dragover', 'paste', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'load' ]; // Simulate focus and blur in old versions of IE using focusin and focusout // TODO: Document the gigantic IE mess here with focus/blur. // TODO: beforeactivate/beforedeactivate? // http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html if (!has_add_event_listener) { document_events.push('focusin', 'focusout'); } // Opera is multilol: it propagates focus / blur oddly if (window.opera) { document_events.push('focus', 'blur'); } if (__DEV__) { JX.__allowedEvents = {}; if ('onpagehide' in window) { JX.__allowedEvents.unload = true; } } var ii; for (ii = 0; ii < document_events.length; ++ii) { JX.enableDispatch(root, document_events[ii]); } // In particular, we're interested in capturing window focus/blur here so // long polls can abort when the window is not focused. var window_events = [ ('onpagehide' in window) ? 'pagehide' : 'unload', 'resize', 'scroll', 'focus', 'blur', 'popstate', 'hashchange', // In Firefox, if the user clicks in the window then drags the cursor // outside of the window and releases the mouse button, we don't get this // event unless we listen for it as a window event. 'mouseup' ]; - if (window.localStorage) { - window_events.push('storage'); + try { + if (window.localStorage) { + window_events.push('storage'); + } + } catch (storage_exception) { + // See PHI985. In Firefox, accessing "window.localStorage" may throw an + // exception if cookies are disabled. } for (ii = 0; ii < window_events.length; ++ii) { JX.enableDispatch(window, window_events[ii]); } JX.__simulate = function(node, event) { if (!has_add_event_listener) { var e = {target: node, type: event}; JX.__rawEventQueue(e); if (e.returnValue === false) { return false; } } }; if (has_add_event_listener) { document.addEventListener('DOMContentLoaded', function() { JX.__rawEventQueue({type: 'domready'}); }, true); } else { var ready = 'if (this.readyState == "complete") {' + 'JX.__rawEventQueue({type: "domready"});' + '}'; // NOTE: Don't write a 'src' attribute, because "javascript:void(0)" causes // a mixed content warning in IE8 if the page is served over SSL. document.write( '<\/sc' + 'ript' + '>'); } JX.onload = function(func) { if (loaded) { func(); } else { onload.push(func); } }; })(); diff --git a/webroot/rsrc/js/application/diff/DiffChangesetList.js b/webroot/rsrc/js/application/diff/DiffChangesetList.js index a95267c0a3..5ba43a7e6d 100644 --- a/webroot/rsrc/js/application/diff/DiffChangesetList.js +++ b/webroot/rsrc/js/application/diff/DiffChangesetList.js @@ -1,1885 +1,1888 @@ /** * @provides phabricator-diff-changeset-list * @requires javelin-install * phuix-button-view * @javelin */ JX.install('DiffChangesetList', { construct: function() { this._changesets = []; var onload = JX.bind(this, this._ifawake, this._onload); JX.Stratcom.listen('click', 'differential-load', onload); var onmore = JX.bind(this, this._ifawake, this._onmore); JX.Stratcom.listen('click', 'show-more', onmore); var onmenu = JX.bind(this, this._ifawake, this._onmenu); JX.Stratcom.listen('click', 'differential-view-options', onmenu); var oncollapse = JX.bind(this, this._ifawake, this._oncollapse, true); JX.Stratcom.listen('click', 'hide-inline', oncollapse); var onexpand = JX.bind(this, this._ifawake, this._oncollapse, false); JX.Stratcom.listen('click', 'reveal-inline', onexpand); var onedit = JX.bind(this, this._ifawake, this._onaction, 'edit'); JX.Stratcom.listen( 'click', ['differential-inline-comment', 'differential-inline-edit'], onedit); var ondone = JX.bind(this, this._ifawake, this._onaction, 'done'); JX.Stratcom.listen( 'click', ['differential-inline-comment', 'differential-inline-done'], ondone); var ondelete = JX.bind(this, this._ifawake, this._onaction, 'delete'); JX.Stratcom.listen( 'click', ['differential-inline-comment', 'differential-inline-delete'], ondelete); var onreply = JX.bind(this, this._ifawake, this._onaction, 'reply'); JX.Stratcom.listen( 'click', ['differential-inline-comment', 'differential-inline-reply'], onreply); var onresize = JX.bind(this, this._ifawake, this._onresize); JX.Stratcom.listen('resize', null, onresize); var onscroll = JX.bind(this, this._ifawake, this._onscroll); JX.Stratcom.listen('scroll', null, onscroll); var onselect = JX.bind(this, this._ifawake, this._onselect); JX.Stratcom.listen( 'mousedown', ['differential-inline-comment', 'differential-inline-header'], onselect); var onhover = JX.bind(this, this._ifawake, this._onhover); JX.Stratcom.listen( ['mouseover', 'mouseout'], 'differential-inline-comment', onhover); var onrangedown = JX.bind(this, this._ifawake, this._onrangedown); JX.Stratcom.listen( 'mousedown', ['differential-changeset', 'tag:th'], onrangedown); var onrangemove = JX.bind(this, this._ifawake, this._onrangemove); JX.Stratcom.listen( ['mouseover', 'mouseout'], ['differential-changeset', 'tag:th'], onrangemove); var onrangeup = JX.bind(this, this._ifawake, this._onrangeup); JX.Stratcom.listen( 'mouseup', null, onrangeup); }, properties: { translations: null, inlineURI: null, inlineListURI: null, isStandalone: false }, members: { _initialized: false, _asleep: true, _changesets: null, _cursorItem: null, _focusNode: null, _focusStart: null, _focusEnd: null, _hoverNode: null, _hoverInline: null, _hoverOrigin: null, _hoverTarget: null, _rangeActive: false, _rangeOrigin: null, _rangeTarget: null, _bannerNode: null, _unsavedButton: null, _unsubmittedButton: null, _doneButton: null, _doneMode: null, _dropdownMenu: null, _menuButton: null, _menuItems: null, sleep: function() { this._asleep = true; this._redrawFocus(); this._redrawSelection(); this.resetHover(); this._bannerChangeset = null; this._redrawBanner(); }, wake: function() { this._asleep = false; this._redrawFocus(); this._redrawSelection(); this._bannerChangeset = null; this._redrawBanner(); if (this._initialized) { return; } this._initialized = true; var pht = this.getTranslations(); // We may be viewing the normal "/D123" view (with all the changesets) // or the standalone view (with just one changeset). In the standalone // view, some options (like jumping to next or previous file) do not // make sense and do not function. var standalone = this.getIsStandalone(); var label; label = pht('Jump to next change.'); this._installJumpKey('j', label, 1); label = pht('Jump to previous change.'); this._installJumpKey('k', label, -1); if (!standalone) { label = pht('Jump to next file.'); this._installJumpKey('J', label, 1, 'file'); label = pht('Jump to previous file.'); this._installJumpKey('K', label, -1, 'file'); } label = pht('Jump to next inline comment.'); this._installJumpKey('n', label, 1, 'comment'); label = pht('Jump to previous inline comment.'); this._installJumpKey('p', label, -1, 'comment'); label = pht('Jump to next inline comment, including collapsed comments.'); this._installJumpKey('N', label, 1, 'comment', true); label = pht( 'Jump to previous inline comment, including collapsed comments.'); this._installJumpKey('P', label, -1, 'comment', true); if (!standalone) { label = pht('Hide or show the current file.'); this._installKey('h', label, this._onkeytogglefile); label = pht('Jump to the table of contents.'); this._installKey('t', label, this._ontoc); } label = pht('Reply to selected inline comment or change.'); this._installKey('r', label, JX.bind(this, this._onkeyreply, false)); label = pht('Reply and quote selected inline comment.'); this._installKey('R', label, JX.bind(this, this._onkeyreply, true)); label = pht('Edit selected inline comment.'); this._installKey('e', label, this._onkeyedit); label = pht('Mark or unmark selected inline comment as done.'); this._installKey('w', label, this._onkeydone); label = pht('Collapse or expand inline comment.'); this._installKey('q', label, this._onkeycollapse); label = pht('Hide or show all inline comments.'); this._installKey('A', label, this._onkeyhideall); }, isAsleep: function() { return this._asleep; }, newChangesetForNode: function(node) { var changeset = JX.DiffChangeset.getForNode(node); this._changesets.push(changeset); changeset.setChangesetList(this); return changeset; }, getChangesetForNode: function(node) { return JX.DiffChangeset.getForNode(node); }, getInlineByID: function(id) { var inline = null; for (var ii = 0; ii < this._changesets.length; ii++) { inline = this._changesets[ii].getInlineByID(id); if (inline) { break; } } return inline; }, _ifawake: function(f) { // This function takes another function and only calls it if the // changeset list is awake, so we basically just ignore events when we // are asleep. This may move up the stack at some point as we do more // with Quicksand/Sheets. if (this.isAsleep()) { return; } return f.apply(this, [].slice.call(arguments, 1)); }, _onload: function(e) { var data = e.getNodeData('differential-load'); // NOTE: We can trigger a load from either an explicit "Load" link on // the changeset, or by clicking a link in the table of contents. If // the event was a table of contents link, we let the anchor behavior // run normally. if (data.kill) { e.kill(); } var node = JX.$(data.id); var changeset = this.getChangesetForNode(node); changeset.load(); // TODO: Move this into Changeset. var routable = changeset.getRoutable(); if (routable) { routable.setPriority(2000); } }, _installKey: function(key, label, handler) { handler = JX.bind(this, this._ifawake, handler); return new JX.KeyboardShortcut(key, label) .setHandler(handler) .register(); }, _installJumpKey: function(key, label, delta, filter, show_collapsed) { filter = filter || null; var options = { filter: filter, collapsed: show_collapsed }; var handler = JX.bind(this, this._onjumpkey, delta, options); return this._installKey(key, label, handler); }, _ontoc: function(manager) { var toc = JX.$('toc'); manager.scrollTo(toc); }, getSelectedInline: function() { var cursor = this._cursorItem; if (cursor) { if (cursor.type == 'comment') { return cursor.target; } } return null; }, _onkeyreply: function(is_quote) { var cursor = this._cursorItem; if (cursor) { if (cursor.type == 'comment') { var inline = cursor.target; if (inline.canReply()) { this.setFocus(null); var text; if (is_quote) { text = inline.getRawText(); text = '> ' + text.replace(/\n/g, '\n> ') + '\n\n'; } else { text = ''; } inline.reply(text); return; } } // If the keyboard cursor is selecting a range of lines, we may have // a mixture of old and new changes on the selected rows. It is not // entirely unambiguous what the user means when they say they want // to reply to this, but we use this logic: reply on the new file if // there are any new lines. Otherwise (if there are only removed // lines) reply on the old file. if (cursor.type == 'change') { var origin = cursor.nodes.begin; var target = cursor.nodes.end; // The "origin" and "target" are entire rows, but we need to find // a range of "" nodes to actually create an inline, so go // fishing. var old_list = []; var new_list = []; var row = origin; while (row) { var header = row.firstChild; while (header) { if (JX.DOM.isType(header, 'th')) { if (header.className.indexOf('old') !== -1) { old_list.push(header); } else if (header.className.indexOf('new') !== -1) { new_list.push(header); } } header = header.nextSibling; } if (row == target) { break; } row = row.nextSibling; } var use_list; if (new_list.length) { use_list = new_list; } else { use_list = old_list; } var src = use_list[0]; var dst = use_list[use_list.length - 1]; cursor.changeset.newInlineForRange(src, dst); this.setFocus(null); return; } } var pht = this.getTranslations(); this._warnUser(pht('You must select a comment or change to reply to.')); }, _onkeyedit: function() { var cursor = this._cursorItem; if (cursor) { if (cursor.type == 'comment') { var inline = cursor.target; if (inline.canEdit()) { this.setFocus(null); inline.edit(); return; } } } var pht = this.getTranslations(); this._warnUser(pht('You must select a comment to edit.')); }, _onkeydone: function() { var cursor = this._cursorItem; if (cursor) { if (cursor.type == 'comment') { var inline = cursor.target; if (inline.canDone()) { this.setFocus(null); inline.toggleDone(); return; } } } var pht = this.getTranslations(); this._warnUser(pht('You must select a comment to mark done.')); }, _onkeytogglefile: function() { var cursor = this._cursorItem; if (cursor) { if (cursor.type == 'file') { cursor.changeset.toggleVisibility(); return; } } var pht = this.getTranslations(); this._warnUser(pht('You must select a file to hide or show.')); }, _onkeycollapse: function() { var cursor = this._cursorItem; if (cursor) { if (cursor.type == 'comment') { var inline = cursor.target; if (inline.canCollapse()) { this.setFocus(null); inline.setCollapsed(!inline.isCollapsed()); return; } } } var pht = this.getTranslations(); this._warnUser(pht('You must select a comment to hide.')); }, _onkeyhideall: function() { var inlines = this._getInlinesByType(); if (inlines.visible.length) { this._toggleInlines('all'); } else { this._toggleInlines('show'); } }, _warnUser: function(message) { new JX.Notification() .setContent(message) .alterClassName('jx-notification-alert', true) .setDuration(3000) .show(); }, _onjumpkey: function(delta, options) { var state = this._getSelectionState(); var filter = options.filter || null; var collapsed = options.collapsed || false; var wrap = options.wrap || false; var attribute = options.attribute || null; var show = options.show || false; var cursor = state.cursor; var items = state.items; // If there's currently no selection and the user tries to go back, // don't do anything. if ((cursor === null) && (delta < 0)) { return; } var did_wrap = false; while (true) { if (cursor === null) { cursor = 0; } else { cursor = cursor + delta; } // If we've gone backward past the first change, bail out. if (cursor < 0) { return; } // If we've gone forward off the end of the list, figure out where we // should end up. if (cursor >= items.length) { if (!wrap) { // If we aren't wrapping around, we're done. return; } if (did_wrap) { // If we're already wrapped around, we're done. return; } // Otherwise, wrap the cursor back to the top. cursor = 0; did_wrap = true; } // If we're selecting things of a particular type (like only files) // and the next item isn't of that type, move past it. if (filter !== null) { if (items[cursor].type !== filter) { continue; } } // If the item is collapsed, don't select it when iterating with jump // keys. It can still potentially be selected in other ways. if (!collapsed) { if (items[cursor].collapsed) { continue; } } // If the item has been deleted, don't select it when iterating. The // cursor may remain on it until it is removed. if (items[cursor].deleted) { continue; } // If we're selecting things with a particular attribute, like // "unsaved", skip items without the attribute. if (attribute !== null) { if (!(items[cursor].attributes || {})[attribute]) { continue; } } // If this item is a hidden inline but we're clicking a button which // selects inlines of a particular type, make it visible again. if (items[cursor].hidden) { if (!show) { continue; } items[cursor].target.setHidden(false); } // Otherwise, we've found a valid item to select. break; } this._setSelectionState(items[cursor], true); }, _getSelectionState: function() { var items = this._getSelectableItems(); var cursor = null; if (this._cursorItem !== null) { for (var ii = 0; ii < items.length; ii++) { var item = items[ii]; if (this._cursorItem.target === item.target) { cursor = ii; break; } } } return { cursor: cursor, items: items }; }, _setSelectionState: function(item, scroll) { this._cursorItem = item; this._redrawSelection(scroll); return this; }, _redrawSelection: function(scroll) { var cursor = this._cursorItem; if (!cursor) { this.setFocus(null); return; } // If this item has been removed from the document (for example: create // a new empty comment, then use the "Unsaved" button to select it, then // cancel it), we can still keep the cursor here but do not want to show // a selection reticle over an invisible node. if (cursor.deleted) { this.setFocus(null); return; } this.setFocus(cursor.nodes.begin, cursor.nodes.end); if (scroll) { var pos = JX.$V(cursor.nodes.begin); JX.DOM.scrollToPosition(0, pos.y - 60); } return this; }, redrawCursor: function() { // NOTE: This is setting the cursor to the current cursor. Usually, this // would have no effect. // However, if the old cursor pointed at an inline and the inline has // been edited so the rows have changed, this updates the cursor to point // at the new inline with the proper rows for the current state, and // redraws the reticle correctly. var state = this._getSelectionState(); if (state.cursor !== null) { this._setSelectionState(state.items[state.cursor], false); } }, _getSelectableItems: function() { var result = []; for (var ii = 0; ii < this._changesets.length; ii++) { var items = this._changesets[ii].getSelectableItems(); for (var jj = 0; jj < items.length; jj++) { result.push(items[jj]); } } return result; }, _onhover: function(e) { if (e.getIsTouchEvent()) { return; } var inline; if (e.getType() == 'mouseout') { inline = null; } else { inline = this._getInlineForEvent(e); } this._setHoverInline(inline); }, _onmore: function(e) { e.kill(); var node = e.getNode('differential-changeset'); var changeset = this.getChangesetForNode(node); var data = e.getNodeData('show-more'); var target = e.getNode('context-target'); changeset.loadContext(data.range, target); }, _onmenu: function(e) { var button = e.getNode('differential-view-options'); var data = JX.Stratcom.getData(button); if (data.menu) { // We've already built this menu, so we can let the menu itself handle // the event. return; } e.prevent(); var pht = this.getTranslations(); var node = JX.DOM.findAbove( button, 'div', 'differential-changeset'); var changeset_list = this; var changeset = this.getChangesetForNode(node); var menu = new JX.PHUIXDropdownMenu(button); var list = new JX.PHUIXActionListView(); var add_link = function(icon, name, href, local) { if (!href) { return; } var link = new JX.PHUIXActionView() .setIcon(icon) .setName(name) .setHref(href) .setHandler(function(e) { if (local) { window.location.assign(href); } else { window.open(href); } menu.close(); e.prevent(); }); list.addItem(link); return link; }; var reveal_item = new JX.PHUIXActionView() .setIcon('fa-eye'); list.addItem(reveal_item); var visible_item = new JX.PHUIXActionView() .setHandler(function(e) { e.prevent(); menu.close(); changeset.toggleVisibility(); }); list.addItem(visible_item); add_link('fa-file-text', pht('Browse in Diffusion'), data.diffusionURI); add_link('fa-file-o', pht('View Standalone'), data.standaloneURI); var up_item = new JX.PHUIXActionView() .setHandler(function(e) { if (changeset.isLoaded()) { // Don't let the user swap display modes if a comment is being // edited, since they might lose their work. See PHI180. var inlines = changeset.getInlines(); for (var ii = 0; ii < inlines.length; ii++) { if (inlines[ii].isEditing()) { changeset_list._warnUser( pht( 'Finish editing inline comments before changing display ' + 'modes.')); e.prevent(); menu.close(); return; } } var renderer = changeset.getRenderer(); if (renderer == '1up') { renderer = '2up'; } else { renderer = '1up'; } changeset.setRenderer(renderer); } changeset.reload(); e.prevent(); menu.close(); }); list.addItem(up_item); var encoding_item = new JX.PHUIXActionView() .setIcon('fa-font') .setName(pht('Change Text Encoding...')) .setHandler(function(e) { var params = { encoding: changeset.getEncoding() }; new JX.Workflow('/services/encoding/', params) .setHandler(function(r) { changeset.setEncoding(r.encoding); changeset.reload(); }) .start(); e.prevent(); menu.close(); }); list.addItem(encoding_item); var highlight_item = new JX.PHUIXActionView() .setIcon('fa-sun-o') .setName(pht('Highlight As...')) .setHandler(function(e) { var params = { highlight: changeset.getHighlight() }; new JX.Workflow('/services/highlight/', params) .setHandler(function(r) { changeset.setHighlight(r.highlight); changeset.reload(); }) .start(); e.prevent(); menu.close(); }); list.addItem(highlight_item); add_link('fa-arrow-left', pht('Show Raw File (Left)'), data.leftURI); add_link('fa-arrow-right', pht('Show Raw File (Right)'), data.rightURI); add_link('fa-pencil', pht('Open in Editor'), data.editor, true); add_link('fa-wrench', pht('Configure Editor'), data.editorConfigure); menu.setContent(list.getNode()); menu.listen('open', function() { // When the user opens the menu, check if there are any "Show More" // links in the changeset body. If there aren't, disable the "Show // Entire File" menu item since it won't change anything. var nodes = JX.DOM.scry(JX.$(data.containerID), 'a', 'show-more'); if (nodes.length) { reveal_item .setDisabled(false) .setName(pht('Show All Context')) .setIcon('fa-file-o') .setHandler(function(e) { changeset.loadAllContext(); e.prevent(); menu.close(); }); } else { reveal_item .setDisabled(true) .setIcon('fa-file') .setName(pht('All Context Shown')) .setHandler(function(e) { e.prevent(); }); } encoding_item.setDisabled(!changeset.isLoaded()); highlight_item.setDisabled(!changeset.isLoaded()); if (changeset.isLoaded()) { if (changeset.getRenderer() == '2up') { up_item .setIcon('fa-list-alt') .setName(pht('View Unified')); } else { up_item .setIcon('fa-files-o') .setName(pht('View Side-by-Side')); } } else { up_item .setIcon('fa-refresh') .setName(pht('Load Changes')); } visible_item .setDisabled(true) .setIcon('fa-expand') .setName(pht('Can\'t Toggle Unloaded File')); var diffs = JX.DOM.scry( JX.$(data.containerID), 'table', 'differential-diff'); if (diffs.length > 1) { JX.$E( 'More than one node with sigil "differential-diff" was found in "'+ data.containerID+'."'); } else if (diffs.length == 1) { var diff = diffs[0]; visible_item.setDisabled(false); if (!changeset.isVisible()) { visible_item .setName(pht('Expand File')) .setIcon('fa-expand'); } else { visible_item .setName(pht('Collapse File')) .setIcon('fa-compress'); } } else { // Do nothing when there is no diff shown in the table. For example, // the file is binary. } }); data.menu = menu; menu.open(); }, _oncollapse: function(is_collapse, e) { e.kill(); var inline = this._getInlineForEvent(e); inline.setCollapsed(is_collapse); }, _onresize: function() { this._redrawFocus(); this._redrawSelection(); this._redrawHover(); // Force a banner redraw after a resize event. Particularly, this makes // sure the inline state updates immediately after an inline edit // operation, even if the changeset itself has not changed. this._bannerChangeset = null; this._redrawBanner(); var changesets = this._changesets; for (var ii = 0; ii < changesets.length; ii++) { changesets[ii].redrawFileTree(); } }, _onscroll: function() { this._redrawBanner(); }, _onselect: function(e) { // If the user clicked some element inside the header, like an action // icon, ignore the event. They have to click the header element itself. if (e.getTarget() !== e.getNode('differential-inline-header')) { return; } var inline = this._getInlineForEvent(e); if (!inline) { return; } // The user definitely clicked an inline, so we're going to handle the // event. e.kill(); this.selectInline(inline); }, selectInline: function(inline) { var selection = this._getSelectionState(); var item; // If the comment the user clicked is currently selected, deselect it. // This makes it easy to undo things if you clicked by mistake. if (selection.cursor !== null) { item = selection.items[selection.cursor]; if (item.target === inline) { this._setSelectionState(null, false); return; } } // Otherwise, select the item that the user clicked. This makes it // easier to resume keyboard operations after using the mouse to do // something else. var items = selection.items; for (var ii = 0; ii < items.length; ii++) { item = items[ii]; if (item.target === inline) { this._setSelectionState(item, false); } } }, _onaction: function(action, e) { e.kill(); var inline = this._getInlineForEvent(e); var is_ref = false; // If we don't have a natural inline object, the user may have clicked // an action (like "Delete") inside a preview element at the bottom of // the page. // If they did, try to find an associated normal inline to act on, and // pretend they clicked that instead. This makes the overall state of // the page more consistent. // However, there may be no normal inline (for example, because it is // on a version of the diff which is not visible). In this case, we // act by reference. if (inline === null) { var data = e.getNodeData('differential-inline-comment'); inline = this.getInlineByID(data.id); if (inline) { is_ref = true; } else { switch (action) { case 'delete': this._deleteInlineByID(data.id); return; } } } // TODO: For normal operations, highlight the inline range here. switch (action) { case 'edit': inline.edit(); break; case 'done': inline.toggleDone(); break; case 'delete': inline.delete(is_ref); break; case 'reply': inline.reply(); break; } }, redrawPreview: function() { // TODO: This isn't the cleanest way to find the preview form, but // rendering no longer has direct access to it. var forms = JX.DOM.scry(document.body, 'form', 'transaction-append'); if (forms.length) { JX.DOM.invoke(forms[0], 'shouldRefresh'); } // Clear the mouse hover reticle after a substantive edit: we don't get // a "mouseout" event if the row vanished because of row being removed // after an edit. this.resetHover(); }, setFocus: function(node, extended_node) { this._focusStart = node; this._focusEnd = extended_node; this._redrawFocus(); }, _redrawFocus: function() { var node = this._focusStart; var extended_node = this._focusEnd || node; var reticle = this._getFocusNode(); if (!node || this.isAsleep()) { JX.DOM.remove(reticle); return; } // Outset the reticle some pixels away from the element, so there's some // space between the focused element and the outline. var p = JX.Vector.getPos(node); var s = JX.Vector.getAggregateScrollForNode(node); p.add(s).add(-4, -4).setPos(reticle); // Compute the size we need to extend to the full extent of the focused // nodes. JX.Vector.getPos(extended_node) .add(-p.x, -p.y) .add(JX.Vector.getDim(extended_node)) .add(8, 8) .setDim(reticle); JX.DOM.getContentFrame().appendChild(reticle); }, _getFocusNode: function() { if (!this._focusNode) { var node = JX.$N('div', {className : 'keyboard-focus-focus-reticle'}); this._focusNode = node; } return this._focusNode; }, _setHoverInline: function(inline) { this._hoverInline = inline; if (inline) { var changeset = inline.getChangeset(); var changeset_id; var side = inline.getDisplaySide(); if (side == 'right') { changeset_id = changeset.getRightChangesetID(); } else { changeset_id = changeset.getLeftChangesetID(); } var new_part; if (inline.isNewFile()) { new_part = 'N'; } else { new_part = 'O'; } var prefix = 'C' + changeset_id + new_part + 'L'; var number = inline.getLineNumber(); var length = inline.getLineLength(); try { var origin = JX.$(prefix + number); var target = JX.$(prefix + (number + length)); this._hoverOrigin = origin; this._hoverTarget = target; } catch (error) { // There may not be any nodes present in the document. A case where // this occurs is when you reply to a ghost inline which was made // on lines near the bottom of "long.txt" in an earlier diff, and // the file was later shortened so those lines no longer exist. For // more details, see T11662. this._hoverOrigin = null; this._hoverTarget = null; } } else { this._hoverOrigin = null; this._hoverTarget = null; } this._redrawHover(); }, _setHoverRange: function(origin, target) { this._hoverOrigin = origin; this._hoverTarget = target; this._redrawHover(); }, resetHover: function() { this._setHoverInline(null); this._hoverOrigin = null; this._hoverTarget = null; }, _redrawHover: function() { var reticle = this._getHoverNode(); if (!this._hoverOrigin || this.isAsleep()) { JX.DOM.remove(reticle); return; } JX.DOM.getContentFrame().appendChild(reticle); var top = this._hoverOrigin; var bot = this._hoverTarget; if (JX.$V(top).y > JX.$V(bot).y) { var tmp = top; top = bot; bot = tmp; } // Find the leftmost cell that we're going to highlight: this is the next // in the row. In 2up views, it should be directly adjacent. In // 1up views, we may have to skip over the other line number column. var l = top; while (JX.DOM.isType(l, 'th')) { l = l.nextSibling; } // Find the rightmost cell that we're going to highlight: this is the // farthest consecutive, adjacent in the row. Sometimes the left // and right nodes are the same (left side of 2up view); sometimes we're // going to highlight several nodes (copy + code + coverage). var r = l; while (r.nextSibling && JX.DOM.isType(r.nextSibling, 'td')) { r = r.nextSibling; } var pos = JX.$V(l) .add(JX.Vector.getAggregateScrollForNode(l)); var dim = JX.$V(r) .add(JX.Vector.getAggregateScrollForNode(r)) .add(-pos.x, -pos.y) .add(JX.Vector.getDim(r)); var bpos = JX.$V(bot) .add(JX.Vector.getAggregateScrollForNode(bot)); dim.y = (bpos.y - pos.y) + JX.Vector.getDim(bot).y; pos.setPos(reticle); dim.setDim(reticle); JX.DOM.show(reticle); }, _getHoverNode: function() { if (!this._hoverNode) { var attributes = { className: 'differential-reticle' }; this._hoverNode = JX.$N('div', attributes); } return this._hoverNode; }, _deleteInlineByID: function(id) { var uri = this.getInlineURI(); var data = { op: 'refdelete', id: id }; var handler = JX.bind(this, this.redrawPreview); new JX.Workflow(uri, data) .setHandler(handler) .start(); }, _getInlineForEvent: function(e) { var node = e.getNode('differential-changeset'); if (!node) { return null; } var changeset = this.getChangesetForNode(node); var inline_row = e.getNode('inline-row'); return changeset.getInlineForRow(inline_row); }, getLineNumberFromHeader: function(th) { try { return parseInt(th.id.match(/^C\d+[ON]L(\d+)$/)[1], 10); } catch (x) { return null; } }, getDisplaySideFromHeader: function(th) { return (th.parentNode.firstChild != th) ? 'right' : 'left'; }, _onrangedown: function(e) { // NOTE: We're allowing "mousedown" from a touch event through so users // can leave inlines on a single line. - if (e.isRightButton()) { + + // See PHI985. We want to exclude both right-mouse and middle-mouse + // clicks from continuing. + if (!e.isLeftButton()) { return; } if (this._rangeActive) { return; } var target = e.getTarget(); var number = this.getLineNumberFromHeader(target); if (!number) { return; } e.kill(); this._rangeActive = true; this._rangeOrigin = target; this._rangeTarget = target; this._setHoverRange(this._rangeOrigin, this._rangeTarget); }, _onrangemove: function(e) { if (e.getIsTouchEvent()) { return; } var is_out = (e.getType() == 'mouseout'); var target = e.getTarget(); this._updateRange(target, is_out); }, _updateRange: function(target, is_out) { // Don't update the range if this "" doesn't correspond to a line // number. For instance, this may be a dead line number, like the empty // line numbers on the left hand side of a newly added file. var number = this.getLineNumberFromHeader(target); if (!number) { return; } if (this._rangeActive) { var origin = this._hoverOrigin; // Don't update the reticle if we're selecting a line range and the // "" under the cursor is on the wrong side of the file. You can // only leave inline comments on the left or right side of a file, not // across lines on both sides. var origin_side = this.getDisplaySideFromHeader(origin); var target_side = this.getDisplaySideFromHeader(target); if (origin_side != target_side) { return; } // Don't update the reticle if we're selecting a line range and the // "" under the cursor corresponds to a different file. You can // only leave inline comments on lines in a single file, not across // multiple files. var origin_table = JX.DOM.findAbove(origin, 'table'); var target_table = JX.DOM.findAbove(target, 'table'); if (origin_table != target_table) { return; } } if (is_out) { if (this._rangeActive) { // If we're dragging a range, just leave the state as it is. This // allows you to drag over something invalid while selecting a // range without the range flickering or getting lost. } else { // Otherwise, clear the current range. this.resetHover(); } return; } if (this._rangeActive) { this._rangeTarget = target; } else { this._rangeOrigin = target; this._rangeTarget = target; } this._setHoverRange(this._rangeOrigin, this._rangeTarget); }, _onrangeup: function(e) { if (!this._rangeActive) { return; } e.kill(); var origin = this._rangeOrigin; var target = this._rangeTarget; // If the user dragged a range from the bottom to the top, swap the node // order around. if (JX.$V(origin).y > JX.$V(target).y) { var tmp = target; target = origin; origin = tmp; } var node = JX.DOM.findAbove(origin, null, 'differential-changeset'); var changeset = this.getChangesetForNode(node); changeset.newInlineForRange(origin, target); this._rangeActive = false; this._rangeOrigin = null; this._rangeTarget = null; this.resetHover(); }, _redrawBanner: function() { // If the inline comment menu is open and we've done a redraw, close it. // In particular, this makes it close when you scroll the document: // otherwise, it stays open but the banner moves underneath it. if (this._dropdownMenu) { this._dropdownMenu.close(); } var node = this._getBannerNode(); var changeset = this._getVisibleChangeset(); if (!changeset) { this._bannerChangeset = null; JX.DOM.remove(node); return; } // Don't do anything if nothing has changed. This seems to avoid some // flickering issues in Safari, at least. if (this._bannerChangeset === changeset) { return; } this._bannerChangeset = changeset; var inlines = this._getInlinesByType(); var unsaved = inlines.unsaved; var unsubmitted = inlines.unsubmitted; var undone = inlines.undone; var done = inlines.done; var draft_done = inlines.draftDone; JX.DOM.alterClass( node, 'diff-banner-has-unsaved', !!unsaved.length); JX.DOM.alterClass( node, 'diff-banner-has-unsubmitted', !!unsubmitted.length); JX.DOM.alterClass( node, 'diff-banner-has-draft-done', !!draft_done.length); var pht = this.getTranslations(); var unsaved_button = this._getUnsavedButton(); var unsubmitted_button = this._getUnsubmittedButton(); var done_button = this._getDoneButton(); var menu_button = this._getMenuButton(); if (unsaved.length) { unsaved_button.setText(unsaved.length + ' ' + pht('Unsaved')); JX.DOM.show(unsaved_button.getNode()); } else { JX.DOM.hide(unsaved_button.getNode()); } if (unsubmitted.length || draft_done.length) { var any_draft_count = unsubmitted.length + draft_done.length; unsubmitted_button.setText(any_draft_count + ' ' + pht('Unsubmitted')); JX.DOM.show(unsubmitted_button.getNode()); } else { JX.DOM.hide(unsubmitted_button.getNode()); } if (done.length || undone.length) { // If you haven't marked any comments as "Done", we just show text // like "3 Comments". If you've marked at least one done, we show // "1 / 3 Comments". var done_text; if (done.length) { done_text = [ done.length, ' / ', (done.length + undone.length), ' ', pht('Comments') ]; } else { done_text = [ undone.length, ' ', pht('Comments') ]; } done_button.setText(done_text); JX.DOM.show(done_button.getNode()); // If any comments are not marked "Done", this cycles through the // missing comments. Otherwise, it cycles through all the saved // comments. if (undone.length) { this._doneMode = 'undone'; } else { this._doneMode = 'done'; } } else { JX.DOM.hide(done_button.getNode()); } var path_view = [icon, ' ', changeset.getDisplayPath()]; var buttons_attrs = { className: 'diff-banner-buttons' }; var buttons_list = [ unsaved_button.getNode(), unsubmitted_button.getNode(), done_button.getNode(), menu_button.getNode() ]; var buttons_view = JX.$N('div', buttons_attrs, buttons_list); var icon = new JX.PHUIXIconView() .setIcon(changeset.getIcon()) .getNode(); JX.DOM.setContent(node, [buttons_view, path_view]); document.body.appendChild(node); }, _getInlinesByType: function() { var changesets = this._changesets; var unsaved = []; var unsubmitted = []; var undone = []; var done = []; var draft_done = []; var visible_done = []; var visible_collapsed = []; var visible_ghosts = []; var visible = []; var hidden = []; for (var ii = 0; ii < changesets.length; ii++) { var inlines = changesets[ii].getInlines(); var inline; var jj; for (jj = 0; jj < inlines.length; jj++) { inline = inlines[jj]; if (inline.isDeleted()) { continue; } if (inline.isSynthetic()) { continue; } if (inline.isEditing()) { unsaved.push(inline); } else if (!inline.getID()) { // These are new comments which have been cancelled, and do not // count as anything. continue; } else if (inline.isDraft()) { unsubmitted.push(inline); } else { // NOTE: Unlike other states, an inline may be marked with a // draft checkmark and still be a "done" or "undone" comment. if (inline.isDraftDone()) { draft_done.push(inline); } if (!inline.isDone()) { undone.push(inline); } else { done.push(inline); } } } for (jj = 0; jj < inlines.length; jj++) { inline = inlines[jj]; if (inline.isDeleted()) { continue; } if (inline.isEditing()) { continue; } if (inline.isHidden()) { hidden.push(inline); continue; } visible.push(inline); if (inline.isDone()) { visible_done.push(inline); } if (inline.isCollapsed()) { visible_collapsed.push(inline); } if (inline.isGhost()) { visible_ghosts.push(inline); } } } return { unsaved: unsaved, unsubmitted: unsubmitted, undone: undone, done: done, draftDone: draft_done, visibleDone: visible_done, visibleGhosts: visible_ghosts, visibleCollapsed: visible_collapsed, visible: visible, hidden: hidden }; }, _getUnsavedButton: function() { if (!this._unsavedButton) { var button = new JX.PHUIXButtonView() .setIcon('fa-commenting-o') .setButtonType(JX.PHUIXButtonView.BUTTONTYPE_SIMPLE); var node = button.getNode(); var onunsaved = JX.bind(this, this._onunsavedclick); JX.DOM.listen(node, 'click', null, onunsaved); this._unsavedButton = button; } return this._unsavedButton; }, _getUnsubmittedButton: function() { if (!this._unsubmittedButton) { var button = new JX.PHUIXButtonView() .setIcon('fa-comment-o') .setButtonType(JX.PHUIXButtonView.BUTTONTYPE_SIMPLE); var node = button.getNode(); var onunsubmitted = JX.bind(this, this._onunsubmittedclick); JX.DOM.listen(node, 'click', null, onunsubmitted); this._unsubmittedButton = button; } return this._unsubmittedButton; }, _getDoneButton: function() { if (!this._doneButton) { var button = new JX.PHUIXButtonView() .setIcon('fa-comment') .setButtonType(JX.PHUIXButtonView.BUTTONTYPE_SIMPLE); var node = button.getNode(); var ondone = JX.bind(this, this._ondoneclick); JX.DOM.listen(node, 'click', null, ondone); this._doneButton = button; } return this._doneButton; }, _getMenuButton: function() { if (!this._menuButton) { var pht = this.getTranslations(); var button = new JX.PHUIXButtonView() .setIcon('fa-bars') .setButtonType(JX.PHUIXButtonView.BUTTONTYPE_SIMPLE) .setAuralLabel(pht('Display Options')); var dropdown = new JX.PHUIXDropdownMenu(button.getNode()); this._menuItems = {}; var list = new JX.PHUIXActionListView(); dropdown.setContent(list.getNode()); var map = { hideDone: { type: 'done' }, hideCollapsed: { type: 'collapsed' }, hideGhosts: { type: 'ghosts' }, hideAll: { type: 'all' }, showAll: { type: 'show' } }; for (var k in map) { var spec = map[k]; var handler = JX.bind(this, this._onhideinlines, spec.type); var item = new JX.PHUIXActionView() .setHandler(handler); list.addItem(item); this._menuItems[k] = item; } dropdown.listen('open', JX.bind(this, this._ondropdown)); if (this.getInlineListURI()) { list.addItem( new JX.PHUIXActionView() .setDivider(true)); list.addItem( new JX.PHUIXActionView() .setIcon('fa-external-link') .setName(pht('List Inline Comments')) .setHref(this.getInlineListURI())); } this._menuButton = button; this._dropdownMenu = dropdown; } return this._menuButton; }, _ondropdown: function() { var inlines = this._getInlinesByType(); var items = this._menuItems; var pht = this.getTranslations(); items.hideDone .setName(pht('Hide "Done" Inlines')) .setDisabled(!inlines.visibleDone.length); items.hideCollapsed .setName(pht('Hide Collapsed Inlines')) .setDisabled(!inlines.visibleCollapsed.length); items.hideGhosts .setName(pht('Hide Older Inlines')) .setDisabled(!inlines.visibleGhosts.length); items.hideAll .setName(pht('Hide All Inlines')) .setDisabled(!inlines.visible.length); items.showAll .setName(pht('Show All Inlines')) .setDisabled(!inlines.hidden.length); }, _onhideinlines: function(type, e) { this._dropdownMenu.close(); e.prevent(); this._toggleInlines(type); }, _toggleInlines: function(type) { var inlines = this._getInlinesByType(); // Clear the selection state since we end up in a weird place if the // user hides the selected inline. this._setSelectionState(null); var targets; var mode = true; switch (type) { case 'done': targets = inlines.visibleDone; break; case 'collapsed': targets = inlines.visibleCollapsed; break; case 'ghosts': targets = inlines.visibleGhosts; break; case 'all': targets = inlines.visible; break; case 'show': targets = inlines.hidden; mode = false; break; } for (var ii = 0; ii < targets.length; ii++) { targets[ii].setHidden(mode); } }, _onunsavedclick: function(e) { e.kill(); var options = { filter: 'comment', wrap: true, show: true, attribute: 'unsaved' }; this._onjumpkey(1, options); }, _onunsubmittedclick: function(e) { e.kill(); var options = { filter: 'comment', wrap: true, show: true, attribute: 'anyDraft' }; this._onjumpkey(1, options); }, _ondoneclick: function(e) { e.kill(); var options = { filter: 'comment', wrap: true, show: true, attribute: this._doneMode }; this._onjumpkey(1, options); }, _getBannerNode: function() { if (!this._bannerNode) { var attributes = { className: 'diff-banner', id: 'diff-banner' }; this._bannerNode = JX.$N('div', attributes); } return this._bannerNode; }, _getVisibleChangeset: function() { if (this.isAsleep()) { return null; } if (JX.Device.getDevice() != 'desktop') { return null; } // Never show the banner if we're very near the top of the page. var margin = 480; var s = JX.Vector.getScroll(); if (s.y < margin) { return null; } // We're going to find the changeset which spans an invisible line a // little underneath the bottom of the banner. This makes the header // tick over from "A.txt" to "B.txt" just as "A.txt" scrolls completely // offscreen. var detect_height = 64; for (var ii = 0; ii < this._changesets.length; ii++) { var changeset = this._changesets[ii]; var c = changeset.getVectors(); // If the changeset starts above the line... if (c.pos.y <= (s.y + detect_height)) { // ...and ends below the line, this is the current visible changeset. if ((c.pos.y + c.dim.y) >= (s.y + detect_height)) { return changeset; } } } return null; } } }); diff --git a/webroot/rsrc/js/application/transactions/behavior-comment-actions.js b/webroot/rsrc/js/application/transactions/behavior-comment-actions.js index 9cec374f6b..ab962592c7 100644 --- a/webroot/rsrc/js/application/transactions/behavior-comment-actions.js +++ b/webroot/rsrc/js/application/transactions/behavior-comment-actions.js @@ -1,265 +1,287 @@ /** * @provides javelin-behavior-comment-actions * @requires javelin-behavior * javelin-stratcom * javelin-workflow * javelin-dom * phuix-form-control-view * phuix-icon-view * javelin-behavior-phabricator-gesture */ JX.behavior('comment-actions', function(config) { var action_map = config.actions; var action_node = JX.$(config.actionID); var form_node = JX.$(config.formID); var input_node = JX.$(config.inputID); var place_node = JX.$(config.placeID); var rows = {}; JX.DOM.listen(action_node, 'change', null, function() { var option = find_option(action_node.value); action_node.value = '+'; if (option) { add_row(option); } }); function find_option(key) { var options = action_node.options; var option; for (var ii = 0; ii < options.length; ii++) { option = options[ii]; if (option.value == key) { return option; } } return null; } + function redraw() { + // If any of the stacked actions specify that they change the label for + // the "Submit" button, update the button text. Otherwise, return it to + // the default text. + var button_text = config.defaultButtonText; + for (var k in rows) { + var action = action_map[k]; + if (action.buttonText) { + button_text = action.buttonText; + } + } + + var button_node = JX.DOM.find(form_node, 'button', 'submit-transactions'); + JX.DOM.setContent(button_node, button_text); + } + function remove_action(key) { var row = rows[key]; if (row) { JX.DOM.remove(row.node); row.option.disabled = false; delete rows[key]; } + + redraw(); } function serialize_actions() { var data = []; for (var k in rows) { data.push({ type: k, value: rows[k].control.getValue(), initialValue: action_map[k].initialValue || null }); } return JX.JSON.stringify(data); } function get_data() { var data = JX.DOM.convertFormToDictionary(form_node); data.__preview__ = 1; data[input_node.name] = serialize_actions(); return data; } function restore_draft_actions(drafts) { var draft; var option; var control; for (var ii = 0; ii < drafts.length; ii++) { draft = drafts[ii]; option = find_option(draft); if (!option) { continue; } control = add_row(option); } + + redraw(); } function onresponse(response) { if (JX.Device.getDevice() != 'desktop') { return; } var panel = JX.$(config.panelID); if (!response.xactions.length) { JX.DOM.hide(panel); } else { var preview_root = JX.$(config.timelineID); JX.DOM.setContent( preview_root, [ JX.$H(response.header), JX.$H(response.xactions.join('')), JX.$H(response.previewContent) ]); JX.DOM.show(panel); // NOTE: Resonses are currently processed before associated behaviors are // registered. We need to defer invoking this event so that any behaviors // accompanying the response are registered. var invoke_preview = function() { JX.Stratcom.invoke( 'EditEngine.didCommentPreview', null, { rootNode: preview_root }); }; setTimeout(invoke_preview, 0); } } function force_preview() { if (!config.showPreview) { return; } new JX.Request(config.actionURI, onresponse) .setData(get_data()) .send(); } function add_row(option) { var action = action_map[option.value]; if (!action) { return; } // Remove any conflicting actions. For example, "Accept Revision" conflicts // with "Reject Revision". var conflict_key = action.conflictKey || null; if (conflict_key !== null) { for (var k in action_map) { if (k === action.key) { continue; } if (action_map[k].conflictKey !== conflict_key) { continue; } if (!(k in rows)) { continue; } remove_action(k); } } option.disabled = true; var aural = JX.$N('span', {className: 'aural-only'}, action.auralLabel); var icon = new JX.PHUIXIconView() .setIcon('fa-times-circle'); var remove = JX.$N('a', {href: '#'}, [aural, icon.getNode()]); var control = new JX.PHUIXFormControl() .setLabel(action.label) .setError(remove) .setControl(action.type, action.spec) .setClass('phui-comment-action'); var node = control.getNode(); JX.Stratcom.addSigil(node, 'touchable'); JX.DOM.listen(node, 'gesture.swipe.end', null, function(e) { var data = e.getData(); if (data.direction != 'left') { // Didn't swipe left. return; } if (data.length <= (JX.Vector.getDim(node).x / 2)) { // Didn't swipe far enough. return; } remove_action(action.key); }); rows[action.key] = { control: control, node: node, option: option }; JX.DOM.listen(remove, 'click', null, function(e) { e.kill(); remove_action(action.key); }); place_node.parentNode.insertBefore(node, place_node); + redraw(); + force_preview(); return control; } JX.DOM.listen(form_node, ['submit', 'didSyntheticSubmit'], null, function() { input_node.value = serialize_actions(); }); if (config.showPreview) { var request = new JX.PhabricatorShapedRequest( config.actionURI, onresponse, get_data); var trigger = JX.bind(request, request.trigger); JX.DOM.listen(form_node, 'keydown', null, trigger); JX.DOM.listen(form_node, 'shouldRefresh', null, force_preview); request.start(); var old_device = JX.Device.getDevice(); var ondevicechange = function() { var new_device = JX.Device.getDevice(); var panel = JX.$(config.panelID); if (new_device == 'desktop') { request.setRateLimit(500); // Force an immediate refresh if we switched from another device type // to desktop. if (old_device != new_device) { force_preview(); } } else { // On mobile, don't show live previews and only save drafts every // 10 seconds. request.setRateLimit(10000); JX.DOM.hide(panel); } old_device = new_device; }; ondevicechange(); JX.Stratcom.listen('phabricator-device-change', null, ondevicechange); } restore_draft_actions(config.drafts || []); });