diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 0619fc34f8..6a3b8778e6 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -1,2393 +1,2395 @@ array( 'conpherence.pkg.css' => 'ff161f2d', 'conpherence.pkg.js' => 'b5b51108', 'core.pkg.css' => '37dd219b', 'core.pkg.js' => '5d80e0db', 'darkconsole.pkg.js' => '1f9a31bc', 'differential.pkg.css' => '4ec4a37a', 'differential.pkg.js' => 'd4ab0e81', 'diffusion.pkg.css' => 'b93d9b8c', 'diffusion.pkg.js' => '6134c5a1', 'favicon.ico' => '30672e08', 'maniphest.pkg.css' => '4845691a', 'maniphest.pkg.js' => '5ab2753f', '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' => 'f7b071f1', 'rsrc/css/aphront/dialog-view.css' => '685c7e2d', 'rsrc/css/aphront/list-filter-view.css' => '5d6f0526', 'rsrc/css/aphront/multi-column.css' => '84cc6640', 'rsrc/css/aphront/notification.css' => '3f6c89c9', 'rsrc/css/aphront/panel-view.css' => '8427b78d', 'rsrc/css/aphront/phabricator-nav-view.css' => 'faf6a6fc', 'rsrc/css/aphront/table-view.css' => 'a3aa6910', 'rsrc/css/aphront/tokenizer.css' => '15d5ff71', 'rsrc/css/aphront/tooltip.css' => '173b9431', 'rsrc/css/aphront/typeahead-browse.css' => '4f82e510', 'rsrc/css/aphront/typeahead.css' => '8a84cc7d', 'rsrc/css/application/almanac/almanac.css' => 'dbb9b3af', 'rsrc/css/application/auth/auth.css' => '0877ed6e', 'rsrc/css/application/base/main-menu-view.css' => '16053029', 'rsrc/css/application/base/notification-menu.css' => '6a697e43', 'rsrc/css/application/base/phui-theme.css' => '9f261c6b', 'rsrc/css/application/base/standard-page-view.css' => 'eb5b80c5', 'rsrc/css/application/chatlog/chatlog.css' => 'd295b020', 'rsrc/css/application/conduit/conduit-api.css' => '7bc725c4', 'rsrc/css/application/config/config-options.css' => '0ede4c9b', 'rsrc/css/application/config/config-page.css' => 'c1d5121b', 'rsrc/css/application/config/config-template.css' => '8f18fa41', 'rsrc/css/application/config/setup-issue.css' => 'f794cfc3', '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' => '6953e7ec', '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' => '51efda3a', 'rsrc/css/application/differential/add-comment.css' => 'c47f8c40', 'rsrc/css/application/differential/changeset-view.css' => 'b5e6be7f', 'rsrc/css/application/differential/core.css' => '5b7b8ff4', 'rsrc/css/application/differential/phui-inline-comment.css' => 'ffd1a542', '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-history.css' => '4540f568', 'rsrc/css/application/diffusion/diffusion-icons.css' => 'a6a1e2ba', 'rsrc/css/application/diffusion/diffusion-readme.css' => '419dd5b6', + 'rsrc/css/application/diffusion/diffusion-repository.css' => 'ee6f20ec', 'rsrc/css/application/diffusion/diffusion-source.css' => '750add59', 'rsrc/css/application/feed/feed.css' => 'ecd4ec57', 'rsrc/css/application/files/global-drag-and-drop.css' => '5c1b47c2', 'rsrc/css/application/flag/flag.css' => 'bba8f811', 'rsrc/css/application/harbormaster/harbormaster.css' => 'f491c9f4', 'rsrc/css/application/herald/herald-test.css' => 'a52e323e', 'rsrc/css/application/herald/herald.css' => 'dc31f6e9', 'rsrc/css/application/maniphest/batch-editor.css' => 'b0f0b6d5', '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' => '2f00933b', 'rsrc/css/application/paste/paste.css' => '1898e534', 'rsrc/css/application/people/people-picture-menu-item.css' => 'a06f7f34', 'rsrc/css/application/people/people-profile.css' => '4df76faf', 'rsrc/css/application/phame/phame.css' => 'b3a0b3a3', 'rsrc/css/application/pholio/pholio-edit.css' => '07676f51', 'rsrc/css/application/pholio/pholio-inline-comments.css' => '8e545e49', 'rsrc/css/application/pholio/pholio.css' => 'ca89d380', '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' => '3d3c1f91', 'rsrc/css/application/project/project-view.css' => '792c9057', 'rsrc/css/application/releeph/releeph-core.css' => '9b3c5733', 'rsrc/css/application/releeph/releeph-preview-branch.css' => 'b7a6f4a5', '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' => '66ee5d46', 'rsrc/css/application/search/search-results.css' => '8f8e08ed', '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' => '23beb330', 'rsrc/css/core/remarkup.css' => 'eb37bd0d', 'rsrc/css/core/syntax.css' => 'cae95e89', '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' => 'fccf9f82', 'rsrc/css/layout/phabricator-source-code-view.css' => '4383192f', 'rsrc/css/phui/button/phui-button-bar.css' => '39fe680c', 'rsrc/css/phui/button/phui-button-simple.css' => '8e1baf68', 'rsrc/css/phui/button/phui-button.css' => '022581b4', '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' => '8e10e92c', 'rsrc/css/phui/calendar/phui-calendar.css' => '477acfaa', 'rsrc/css/phui/object-item/phui-oi-big-ui.css' => '19f9369b', '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' => 'bf094950', 'rsrc/css/phui/object-item/phui-oi-simple-ui.css' => 'a8beebea', 'rsrc/css/phui/phui-action-list.css' => 'c01858f4', 'rsrc/css/phui/phui-action-panel.css' => '91c7b835', 'rsrc/css/phui/phui-badge.css' => '22c0cf4f', 'rsrc/css/phui/phui-basic-nav-view.css' => 'a0705f53', 'rsrc/css/phui/phui-big-info-view.css' => 'bd903741', 'rsrc/css/phui/phui-box.css' => '269cbc99', 'rsrc/css/phui/phui-chart.css' => '6bf6f78e', 'rsrc/css/phui/phui-cms.css' => '504b4b23', 'rsrc/css/phui/phui-comment-form.css' => '57af2e14', 'rsrc/css/phui/phui-comment-panel.css' => 'f50152ad', 'rsrc/css/phui/phui-crumbs-view.css' => '6ece3bbb', 'rsrc/css/phui/phui-curtain-view.css' => '55dd0e59', 'rsrc/css/phui/phui-document-pro.css' => '8af7ea27', 'rsrc/css/phui/phui-document-summary.css' => '9ca48bdf', 'rsrc/css/phui/phui-document.css' => 'c32e8dec', 'rsrc/css/phui/phui-feed-story.css' => '44a9c8e9', 'rsrc/css/phui/phui-fontkit.css' => '1320ed01', 'rsrc/css/phui/phui-form-view.css' => '6175808d', 'rsrc/css/phui/phui-form.css' => 'a5570f70', 'rsrc/css/phui/phui-head-thing.css' => 'fd311e5f', 'rsrc/css/phui/phui-header-view.css' => 'e7de7ee2', 'rsrc/css/phui/phui-hovercard.css' => 'f0592bcf', 'rsrc/css/phui/phui-icon-set-selector.css' => '87db8fee', 'rsrc/css/phui/phui-icon.css' => '4c46b6ba', 'rsrc/css/phui/phui-image-mask.css' => 'a8498f9c', 'rsrc/css/phui/phui-info-panel.css' => '27ea50a1', 'rsrc/css/phui/phui-info-view.css' => '6e217679', 'rsrc/css/phui/phui-invisible-character-view.css' => '6993d9f0', 'rsrc/css/phui/phui-lightbox.css' => '0a035e40', 'rsrc/css/phui/phui-list.css' => 'dcafb463', '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' => '2dc7993f', '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' => '93b084cf', 'rsrc/css/phui/phui-timeline-view.css' => '313c7f22', 'rsrc/css/phui/phui-two-column-view.css' => '5b8cd553', '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' => '587d92d7', '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/Stratcom.js' => '6ad39b6f', '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' => '3010e992', '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' => '1e911d0f', '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' => '8d3bc1b2', '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' => '0fcf201c', 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js' => '6c0e62fa', 'rsrc/favicons/apple-touch-icon-114x114.png' => '12a24178', 'rsrc/favicons/apple-touch-icon-120x120.png' => '0d1543c7', 'rsrc/favicons/apple-touch-icon-144x144.png' => '8043b5a5', 'rsrc/favicons/apple-touch-icon-152x152.png' => '65905ecd', 'rsrc/favicons/apple-touch-icon-57x57.png' => '2bfc7b0a', 'rsrc/favicons/apple-touch-icon-60x60.png' => '8ff52925', 'rsrc/favicons/apple-touch-icon-72x72.png' => 'a2bb65d6', 'rsrc/favicons/apple-touch-icon-76x76.png' => '2d061a11', 'rsrc/favicons/favicon-128.png' => '72f7e812', 'rsrc/favicons/favicon-16x16.png' => 'fc6275ba', 'rsrc/favicons/favicon-196x196.png' => '95db275e', 'rsrc/favicons/favicon-32x32.png' => '5bd18b6c', 'rsrc/favicons/favicon-96x96.png' => '7242c8e9', 'rsrc/favicons/favicon-mention.ico' => '1fdd0fa4', 'rsrc/favicons/favicon-message.ico' => '115bc010', 'rsrc/favicons/favicon.ico' => 'cdb11121', 'rsrc/favicons/mask-icon.svg' => 'e132a80f', 'rsrc/favicons/mstile-144x144.png' => '310c2ee5', 'rsrc/favicons/mstile-150x150.png' => '74bf5133', 'rsrc/favicons/mstile-310x150.png' => '4a49d3ee', 'rsrc/favicons/mstile-310x310.png' => 'a52ab264', 'rsrc/favicons/mstile-70x70.png' => '5edce7b8', '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' => '4abee916', 'rsrc/image/sprite-login.png' => '2b9663fd', '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' => '3c547a81', 'rsrc/js/application/aphlict/behavior-aphlict-status.js' => '5e2634b9', 'rsrc/js/application/aphlict/behavior-desktop-notifications-control.js' => 'd5a2d665', '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' => 'c9b99b77', 'rsrc/js/application/conpherence/behavior-participant-pane.js' => '8604caa8', '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' => '99abf4cd', 'rsrc/js/application/diff/DiffChangesetList.js' => '79de07c6', 'rsrc/js/application/diff/DiffInline.js' => '1bfa31c7', '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' => '419998ab', 'rsrc/js/application/differential/behavior-user-select.js' => 'a8d8459d', 'rsrc/js/application/diffusion/DiffusionLocateFileSource.js' => 'c93358e3', '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-diffusion-browse-file.js' => '054a0f0b', 'rsrc/js/application/diffusion/behavior-jump-to.js' => '73d09eef', 'rsrc/js/application/diffusion/behavior-load-blame.js' => '42126667', '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' => 'e5822781', 'rsrc/js/application/drydock/drydock-live-operation-status.js' => '901935ef', 'rsrc/js/application/files/behavior-icon-composer.js' => '8499b6ab', 'rsrc/js/application/files/behavior-launch-icon-composer.js' => '48086888', 'rsrc/js/application/herald/HeraldRuleEditor.js' => 'd6a7e717', 'rsrc/js/application/herald/PathTypeahead.js' => 'f7fc67ec', 'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3', 'rsrc/js/application/maniphest/behavior-batch-editor.js' => '782ab6e7', 'rsrc/js/application/maniphest/behavior-batch-selector.js' => '0825c27a', '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' => 'aa1733d0', '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' => 'fbe497e7', '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' => 'e5339c43', 'rsrc/js/application/search/behavior-reorder-profile-menu-items.js' => 'e2e0a072', 'rsrc/js/application/search/behavior-reorder-queries.js' => 'e9581f08', 'rsrc/js/application/slowvote/behavior-slowvote-embed.js' => '887ad43f', 'rsrc/js/application/transactions/behavior-comment-actions.js' => '9a6dd75c', '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' => 'ae95d984', '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' => 'ccf1cbf8', 'rsrc/js/core/Prefab.js' => 'c5af80a2', '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-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' => 'bb1dd507', 'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '484a6e22', 'rsrc/js/core/behavior-error-log.js' => '6882e80a', 'rsrc/js/core/behavior-fancy-datepicker.js' => 'a9210d03', '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' => '560f41da', 'rsrc/js/core/behavior-line-linker.js' => '1499a8cb', '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' => '947753e0', 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => 'acd29eee', 'rsrc/js/core/behavior-read-only-warning.js' => 'ba158207', 'rsrc/js/core/behavior-refresh-csrf.js' => 'ab2f381b', 'rsrc/js/core/behavior-remarkup-preview.js' => '4b700e9e', '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' => 'd0a99ab4', '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' => '17bb8539', '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-submenu.js' => 'a6f7a73b', 'rsrc/js/phui/behavior-phui-tab-group.js' => '0a0b10e9', 'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8', 'rsrc/js/phuix/PHUIXActionView.js' => '442efd08', 'rsrc/js/phuix/PHUIXAutocomplete.js' => 'f6699267', 'rsrc/js/phuix/PHUIXButtonView.js' => 'a37126bd', 'rsrc/js/phuix/PHUIXDropdownMenu.js' => '8018ee50', 'rsrc/js/phuix/PHUIXExample.js' => '68af71ca', 'rsrc/js/phuix/PHUIXFormControl.js' => '83e03671', 'rsrc/js/phuix/PHUIXIconView.js' => 'bff6884b', ), 'symbols' => array( 'almanac-css' => 'dbb9b3af', 'aphront-bars' => '231ac33c', 'aphront-dark-console-css' => 'f7b071f1', 'aphront-dialog-view-css' => '685c7e2d', 'aphront-list-filter-view-css' => '5d6f0526', 'aphront-multi-column-view-css' => '84cc6640', 'aphront-panel-view-css' => '8427b78d', 'aphront-table-view-css' => 'a3aa6910', 'aphront-tokenizer-control-css' => '15d5ff71', 'aphront-tooltip-css' => '173b9431', 'aphront-typeahead-control-css' => '8a84cc7d', 'application-search-view-css' => '66ee5d46', 'auth-css' => '0877ed6e', 'bulk-job-css' => 'df9c1d4a', 'conduit-api-css' => '7bc725c4', 'config-options-css' => '0ede4c9b', 'config-page-css' => 'c1d5121b', 'conpherence-color-css' => 'abb4c358', 'conpherence-durable-column-view' => '89ea6bef', 'conpherence-header-pane-css' => 'cb6f4e19', 'conpherence-menu-css' => '6953e7ec', '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' => 'b5e6be7f', '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-history-css' => '4540f568', 'diffusion-icons-css' => 'a6a1e2ba', 'diffusion-readme-css' => '419dd5b6', + 'diffusion-repository-css' => 'ee6f20ec', 'diffusion-source-css' => '750add59', 'diviner-shared-css' => '896f1d43', 'font-fontawesome' => 'e838e088', 'font-lato' => 'c7ccd872', 'global-drag-and-drop-css' => '5c1b47c2', 'harbormaster-css' => 'f491c9f4', 'herald-css' => 'dc31f6e9', 'herald-rule-editor' => 'd6a7e717', 'herald-test-css' => 'a52e323e', 'inline-comment-summary-css' => '51efda3a', 'javelin-aphlict' => 'e1d4b11a', 'javelin-behavior' => '61cbc29a', 'javelin-behavior-aphlict-dropdown' => 'caade6f2', 'javelin-behavior-aphlict-listen' => '3c547a81', '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-job-reload' => 'edf8a145', 'javelin-behavior-calendar-month-view' => 'fe33e256', 'javelin-behavior-choose-control' => '327a00d1', 'javelin-behavior-comment-actions' => '9a6dd75c', 'javelin-behavior-config-reorder-fields' => 'b6993408', 'javelin-behavior-conpherence-menu' => 'c9b99b77', 'javelin-behavior-conpherence-participant-pane' => '8604caa8', 'javelin-behavior-conpherence-pontificate' => '55616e04', 'javelin-behavior-conpherence-search' => '9bbf3762', 'javelin-behavior-countdown-timer' => 'e4cc26b3', 'javelin-behavior-dark-console' => '17bb8539', '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' => 'd5a2d665', 'javelin-behavior-detect-timezone' => '4c193c96', 'javelin-behavior-device' => 'bb1dd507', 'javelin-behavior-diff-preview-link' => '051c7832', 'javelin-behavior-differential-diff-radios' => 'e1ff79b1', 'javelin-behavior-differential-feedback-preview' => '51c5ad07', 'javelin-behavior-differential-populate' => '419998ab', 'javelin-behavior-differential-user-select' => 'a8d8459d', 'javelin-behavior-diffusion-browse-file' => '054a0f0b', 'javelin-behavior-diffusion-commit-branches' => 'bdaf4d04', 'javelin-behavior-diffusion-commit-graph' => '75b83cbb', 'javelin-behavior-diffusion-jump-to' => '73d09eef', 'javelin-behavior-diffusion-locate-file' => '6d3e1947', 'javelin-behavior-diffusion-pull-lastmodified' => 'f01586dc', 'javelin-behavior-doorkeeper-tag' => 'e5822781', '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-error-log' => '6882e80a', 'javelin-behavior-event-all-day' => 'b41537c9', 'javelin-behavior-fancy-datepicker' => 'a9210d03', 'javelin-behavior-global-drag-and-drop' => '960f6a39', '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' => '560f41da', 'javelin-behavior-line-chart' => 'e4232876', 'javelin-behavior-load-blame' => '42126667', 'javelin-behavior-maniphest-batch-editor' => '782ab6e7', 'javelin-behavior-maniphest-batch-selector' => '0825c27a', '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' => '1499a8cb', 'javelin-behavior-phabricator-nav' => '947753e0', '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' => 'd0a99ab4', 'javelin-behavior-phabricator-show-older-transactions' => 'ae95d984', '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' => 'fbe497e7', 'javelin-behavior-phui-dropdown-menu' => 'b95d6f7d', 'javelin-behavior-phui-file-upload' => 'b003d4fb', 'javelin-behavior-phui-hovercards' => 'bcaccd64', '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-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-preview' => '4b700e9e', 'javelin-behavior-reorder-applications' => '76b9fc3e', 'javelin-behavior-reorder-columns' => 'e1d25dfb', 'javelin-behavior-reorder-profile-menu-items' => 'e2e0a072', 'javelin-behavior-repository-crossreference' => 'e5339c43', '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-slowvote-embed' => '887ad43f', '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' => 'c93358e3', 'javelin-dom' => '4976858c', 'javelin-dynval' => 'f6555212', 'javelin-event' => '2ee659ce', 'javelin-fx' => '54b612ba', 'javelin-history' => 'd4505101', 'javelin-install' => '05270951', 'javelin-json' => '69adf288', 'javelin-leader' => '7f243deb', 'javelin-magical-init' => '3010e992', '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' => '6ad39b6f', 'javelin-tokenizer' => '8d3bc1b2', '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' => '0fcf201c', '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' => '1e911d0f', 'maniphest-batch-editor' => 'b0f0b6d5', 'maniphest-report-css' => '9b9580b7', 'maniphest-task-edit-css' => 'fda62a9b', 'maniphest-task-summary-css' => '11cc5344', 'multirow-row-manager' => 'b5d57730', 'owners-path-editor' => 'aa1733d0', 'owners-path-editor-css' => '2f00933b', 'paste-css' => '1898e534', 'path-typeahead' => 'f7fc67ec', 'people-picture-menu-item-css' => 'a06f7f34', 'people-profile-css' => '4df76faf', 'phabricator-action-list-view-css' => 'c01858f4', 'phabricator-busy' => '59a7976a', 'phabricator-chatlog-css' => 'd295b020', 'phabricator-content-source-view-css' => '4b8b05d4', 'phabricator-core-css' => '23beb330', 'phabricator-countdown-css' => '16c52f5c', 'phabricator-darklog' => 'c8e1ffe3', 'phabricator-darkmessage' => 'c48cccdd', 'phabricator-dashboard-css' => 'fe5b1869', 'phabricator-diff-changeset' => '99abf4cd', 'phabricator-diff-changeset-list' => '79de07c6', 'phabricator-diff-inline' => '1bfa31c7', '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' => 'fccf9f82', 'phabricator-flag-css' => 'bba8f811', 'phabricator-keyboard-shortcut' => '1ae869f2', 'phabricator-keyboard-shortcut-manager' => 'c19dd9b9', 'phabricator-main-menu-view' => '16053029', 'phabricator-nav-view-css' => 'faf6a6fc', 'phabricator-notification' => 'ccf1cbf8', 'phabricator-notification-css' => '3f6c89c9', 'phabricator-notification-menu-css' => '6a697e43', 'phabricator-object-selector-css' => '85ee8ce6', 'phabricator-phtize' => 'd254d646', 'phabricator-prefab' => 'c5af80a2', 'phabricator-remarkup-css' => 'eb37bd0d', 'phabricator-search-results-css' => '8f8e08ed', 'phabricator-shaped-request' => '7cbe244b', 'phabricator-slowvote-css' => 'a94b7230', 'phabricator-source-code-view-css' => '4383192f', 'phabricator-standard-page-view' => 'eb5b80c5', 'phabricator-textareautils' => '320810c8', 'phabricator-title' => '485aaa6c', 'phabricator-tooltip' => '358b8c04', 'phabricator-ui-example-css' => '528b19de', 'phabricator-zindex-css' => '9d8f7c4b', 'phame-css' => 'b3a0b3a3', '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' => '91c7b835', 'phui-badge-view-css' => '22c0cf4f', 'phui-basic-nav-view-css' => 'a0705f53', 'phui-big-info-view-css' => 'bd903741', 'phui-box-css' => '269cbc99', 'phui-button-bar-css' => '39fe680c', 'phui-button-css' => '022581b4', 'phui-button-simple-css' => '8e1baf68', 'phui-calendar-css' => '477acfaa', 'phui-calendar-day-css' => '572b1893', 'phui-calendar-list-css' => '576be600', 'phui-calendar-month-css' => '8e10e92c', 'phui-chart-css' => '6bf6f78e', 'phui-cms-css' => '504b4b23', 'phui-comment-form-css' => '57af2e14', 'phui-comment-panel-css' => 'f50152ad', 'phui-crumbs-view-css' => '6ece3bbb', 'phui-curtain-view-css' => '55dd0e59', 'phui-document-summary-view-css' => '9ca48bdf', 'phui-document-view-css' => 'c32e8dec', 'phui-document-view-pro-css' => '8af7ea27', 'phui-feed-story-css' => '44a9c8e9', 'phui-font-icon-base-css' => '870a7360', 'phui-fontkit-css' => '1320ed01', 'phui-form-css' => 'a5570f70', 'phui-form-view-css' => '6175808d', 'phui-head-thing-view-css' => 'fd311e5f', 'phui-header-view-css' => 'e7de7ee2', 'phui-hovercard' => '1bd28176', 'phui-hovercard-view-css' => 'f0592bcf', 'phui-icon-set-selector-css' => '87db8fee', 'phui-icon-view-css' => '4c46b6ba', 'phui-image-mask-css' => 'a8498f9c', 'phui-info-panel-css' => '27ea50a1', 'phui-info-view-css' => '6e217679', 'phui-inline-comment-view-css' => 'ffd1a542', 'phui-invisible-character-view-css' => '6993d9f0', 'phui-lightbox-css' => '0a035e40', 'phui-list-view-css' => 'dcafb463', 'phui-object-box-css' => '9cff003c', 'phui-oi-big-ui-css' => '19f9369b', 'phui-oi-color-css' => 'cd2b9b77', 'phui-oi-drag-ui-css' => '08f4ccc3', 'phui-oi-flush-ui-css' => '9d9685d6', 'phui-oi-list-view-css' => 'bf094950', 'phui-oi-simple-ui-css' => 'a8beebea', 'phui-pager-css' => 'edcbc226', 'phui-pinboard-view-css' => '2495140e', 'phui-property-list-view-css' => '2dc7993f', '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' => '93b084cf', 'phui-theme-css' => '9f261c6b', 'phui-timeline-view-css' => '313c7f22', 'phui-two-column-view-css' => '5b8cd553', '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' => '442efd08', 'phuix-autocomplete' => 'f6699267', 'phuix-button-view' => 'a37126bd', 'phuix-dropdown-menu' => '8018ee50', 'phuix-form-control-view' => '83e03671', 'phuix-icon-view' => 'bff6884b', 'policy-css' => '957ea14c', 'policy-edit-css' => '815c66f7', 'policy-transaction-detail-css' => '82100a43', 'ponder-view-css' => 'fbd45f96', 'project-card-view-css' => '3d3c1f91', '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' => 'f794cfc3', 'sprite-login-css' => '587d92d7', 'sprite-tokens-css' => '9cdfd599', 'syntax-default-css' => '9923583c', 'syntax-highlighting-css' => 'cae95e89', 'tokens-css' => '3d0f239e', 'typeahead-browse-css' => '4f82e510', 'unhandled-exception-css' => '4c96257a', ), 'requires' => array( '013ffff9' => array( 'javelin-install', 'javelin-util', 'javelin-request', 'javelin-typeahead-source', ), '01fca1f0' => array( 'javelin-behavior', 'javelin-workflow', 'javelin-json', 'javelin-dom', 'phabricator-keyboard-shortcut', ), '051c7832' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), '05270951' => array( 'javelin-util', 'javelin-magical-init', ), '054a0f0b' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'phabricator-tooltip', ), '065227cc' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-workflow', ), '0825c27a' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-util', ), '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', ), '0f764c35' => array( 'javelin-install', 'javelin-util', ), '0fcf201c' => array( 'javelin-install', 'javelin-util', 'javelin-dom', 'javelin-typeahead-normalizer', ), '1499a8cb' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-history', ), '15d5ff71' => array( 'aphront-typeahead-control-css', 'phui-tag-view-css', ), 16053029 => array( 'phui-theme-css', ), '17bb8539' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-util', 'javelin-dom', 'javelin-request', 'phabricator-keyboard-shortcut', 'phabricator-darklog', 'phabricator-darkmessage', ), '185bbd53' => array( 'javelin-install', ), '19f9369b' => array( 'phui-oi-list-view-css', ), '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', ), '1bfa31c7' => array( 'javelin-dom', ), '1e911d0f' => array( 'javelin-stratcom', 'javelin-request', 'javelin-dom', 'javelin-vector', 'javelin-install', 'javelin-util', 'javelin-mask', 'javelin-uri', 'javelin-routable', ), '1f6794f6' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', 'javelin-uri', 'phabricator-textareautils', ), '1fe2510c' => 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', ), '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', ), '358b8c04' => array( 'javelin-install', 'javelin-util', 'javelin-dom', 'javelin-vector', ), '39fe680c' => array( 'phui-button-css', 'phui-button-simple-css', ), '3ab51e2c' => array( 'javelin-behavior', 'javelin-behavior-device', 'javelin-stratcom', 'javelin-vector', 'javelin-dom', 'javelin-magical-init', ), '3c547a81' => 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', ), '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', ), '408bf173' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-stratcom', 'javelin-workflow', 'phabricator-draggable-list', ), '419998ab' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'phabricator-tooltip', 'phabricator-diff-changeset-list', 'phabricator-diff-changeset', ), 42126667 => array( 'javelin-behavior', 'javelin-dom', 'javelin-request', ), '4250a34e' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-vector', 'javelin-stratcom', 'javelin-workflow', 'javelin-workboard-controller', ), '442efd08' => array( 'javelin-install', 'javelin-dom', 'javelin-util', ), '44959b73' => array( 'javelin-util', 'javelin-uri', 'javelin-install', ), '453c5375' => array( 'javelin-behavior', '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', ), '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', ), '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', ), '560f41da' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-mask', 'javelin-util', 'phuix-icon-view', 'phabricator-busy', ), '58dea2fa' => array( 'javelin-install', 'javelin-util', 'javelin-request', 'javelin-dom', 'javelin-uri', 'phabricator-file-upload', ), '59a7976a' => array( 'javelin-install', 'javelin-dom', 'javelin-fx', ), '59b251eb' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-vector', 'javelin-dom', ), '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', ), '62dfea03' => array( 'javelin-install', 'javelin-util', ), '635de1ec' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', ), '680ea2c8' => array( 'javelin-install', 'javelin-dom', 'phabricator-notification', ), '6882e80a' => array( 'javelin-dom', ), '68af71ca' => array( 'javelin-install', 'javelin-dom', 'phuix-button-view', ), '69adf288' => array( 'javelin-install', ), '6ad39b6f' => array( 'javelin-install', 'javelin-event', 'javelin-util', 'javelin-magical-init', ), '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', ), '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', ), '73d09eef' => array( 'javelin-behavior', 'javelin-vector', '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', ), '77c1f0b0' => array( 'javelin-behavior', 'javelin-dom', 'javelin-request', 'javelin-util', ), '782ab6e7' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'phabricator-prefab', 'multirow-row-manager', 'javelin-json', ), '7927a7d3' => array( 'javelin-behavior', 'javelin-quicksand', ), '79de07c6' => array( 'javelin-install', 'phuix-button-view', ), '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', ), '8018ee50' => array( 'javelin-install', 'javelin-util', 'javelin-dom', 'javelin-vector', 'javelin-stratcom', ), '834a1173' => array( 'javelin-behavior', 'javelin-scrollbar', ), '83e03671' => array( 'javelin-install', 'javelin-dom', ), '8499b6ab' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', ), '85ee8ce6' => array( 'aphront-dialog-view-css', ), '8604caa8' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-workflow', 'javelin-util', 'phabricator-notification', 'conpherence-thread-manager', ), '88236f00' => array( 'javelin-behavior', 'phabricator-keyboard-shortcut', 'javelin-stratcom', ), '887ad43f' => array( 'javelin-behavior', 'javelin-request', 'javelin-stratcom', 'javelin-dom', ), '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', ), '8d3bc1b2' => array( 'javelin-dom', 'javelin-util', 'javelin-stratcom', 'javelin-install', ), '8e1baf68' => array( 'phui-button-css', ), '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', ), '947753e0' => array( 'javelin-behavior', 'javelin-behavior-device', 'javelin-stratcom', 'javelin-dom', 'javelin-magical-init', 'javelin-vector', 'javelin-request', 'javelin-util', ), '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', ), '99abf4cd' => array( 'javelin-dom', 'javelin-util', 'javelin-stratcom', 'javelin-install', 'javelin-workflow', 'javelin-router', 'javelin-behavior-device', 'javelin-vector', 'phabricator-diff-inline', ), '9a6dd75c' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', 'phuix-form-control-view', 'phuix-icon-view', 'javelin-behavior-phabricator-gesture', ), '9bbf3762' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-workflow', 'javelin-stratcom', ), '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', ), 'a37126bd' => array( 'javelin-install', 'javelin-dom', ), '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', ), 'a9210d03' => array( 'javelin-behavior', 'javelin-util', 'javelin-dom', 'javelin-stratcom', 'javelin-vector', ), 'a9f88de2' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-workflow', 'javelin-fx', 'javelin-util', ), 'aa1733d0' => array( 'multirow-row-manager', 'javelin-install', 'path-typeahead', 'javelin-dom', 'javelin-util', 'phabricator-prefab', ), 'ab2f381b' => array( 'javelin-request', 'javelin-behavior', 'javelin-dom', 'javelin-router', 'javelin-util', 'phabricator-busy', ), 'acd29eee' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'phabricator-phtize', 'phabricator-textareautils', 'javelin-workflow', 'javelin-vector', 'phuix-autocomplete', 'javelin-mask', ), 'ae95d984' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'phabricator-busy', ), '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', ), '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', ), 'b5e6be7f' => array( 'phui-inline-comment-view-css', ), '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', ), 'bb1dd507' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-vector', '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', ), 'c420b0b9' => array( 'javelin-behavior', 'javelin-behavior-device', 'javelin-stratcom', 'phabricator-tooltip', ), 'c587b80f' => array( 'javelin-install', ), 'c5af80a2' => 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', ), 'c7ccd872' => array( 'phui-fontkit-css', ), 'c90a04fc' => array( 'javelin-dom', 'javelin-dynval', 'javelin-reactor', 'javelin-reactornode', 'javelin-install', 'javelin-util', ), 'c93358e3' => array( 'javelin-install', 'javelin-dom', 'javelin-typeahead-preloaded-source', 'javelin-util', ), 'c989ade3' => array( 'javelin-install', 'javelin-util', 'javelin-stratcom', ), 'c9b99b77' => 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', ), 'caade6f2' => array( 'javelin-behavior', 'javelin-request', 'javelin-stratcom', 'javelin-vector', 'javelin-dom', 'javelin-uri', 'javelin-behavior-device', 'phabricator-title', 'phabricator-favicon', ), 'cae95e89' => array( 'syntax-default-css', ), 'ccf1cbf8' => array( 'javelin-install', 'javelin-dom', 'javelin-stratcom', 'javelin-util', 'phabricator-notification-css', ), 'cd2b9b77' => array( 'phui-oi-list-view-css', ), 'd0a99ab4' => array( 'javelin-behavior', 'javelin-typeahead-ondemand-source', 'javelin-typeahead', 'javelin-dom', 'javelin-uri', 'javelin-util', 'javelin-stratcom', 'phabricator-prefab', 'phuix-icon-view', ), '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', ), 'd5a2d665' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-uri', 'phabricator-notification', ), 'd6a7e717' => array( 'multirow-row-manager', 'javelin-install', 'javelin-util', 'javelin-dom', 'javelin-stratcom', 'javelin-json', 'phabricator-prefab', ), 'd7a74243' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', 'phabricator-draggable-list', ), 'd835b03a' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'phabricator-shaped-request', ), 'de2e896f' => array( 'javelin-behavior', 'javelin-dom', 'javelin-typeahead', 'javelin-typeahead-ondemand-source', 'javelin-dom', ), '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', ), 'e5339c43' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-uri', ), 'e5822781' => array( 'javelin-behavior', 'javelin-dom', 'javelin-json', 'javelin-workflow', 'javelin-magical-init', ), 'e9581f08' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', 'phabricator-draggable-list', ), 'edf8a145' => array( 'javelin-behavior', 'javelin-uri', ), 'efe49472' => array( 'javelin-install', 'javelin-util', ), 'f01586dc' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-workflow', 'javelin-json', ), 'f50152ad' => array( 'phui-timeline-view-css', ), 'f6555212' => array( 'javelin-install', 'javelin-reactornode', 'javelin-util', 'javelin-reactor', ), 'f6699267' => array( 'javelin-install', 'javelin-dom', 'phuix-icon-view', 'phabricator-prefab', ), 'f7fc67ec' => array( 'javelin-install', 'javelin-typeahead', 'javelin-dom', 'javelin-request', 'javelin-typeahead-ondemand-source', 'javelin-util', ), 'f829edb3' => array( 'javelin-view', 'javelin-install', 'javelin-dom', ), 'fbe497e7' => 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', ), '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', ), 'darkconsole.pkg.js' => array( 'javelin-behavior-dark-console', 'javelin-behavior-error-log', ), '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-load-blame', '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/src/applications/diffusion/controller/DiffusionBranchTableController.php b/src/applications/diffusion/controller/DiffusionBranchTableController.php index 7f8ae11f57..04d00f0306 100644 --- a/src/applications/diffusion/controller/DiffusionBranchTableController.php +++ b/src/applications/diffusion/controller/DiffusionBranchTableController.php @@ -1,90 +1,93 @@ loadDiffusionContext(); if ($response) { return $response; } $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $pager = id(new PHUIPagerView()) ->readFromRequest($request); $params = array( 'offset' => $pager->getOffset(), 'limit' => $pager->getPageSize() + 1, ); $contains = $drequest->getSymbolicCommit(); if (strlen($contains)) { $params['contains'] = $contains; } $branches = $this->callConduitWithDiffusionRequest( 'diffusion.branchquery', $params); $branches = $pager->sliceResults($branches); $branches = DiffusionRepositoryRef::loadAllFromDictionaries($branches); $content = null; if (!$branches) { $content = $this->renderStatusMessage( pht('No Branches'), pht('This repository has no branches.')); } else { $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withIdentifiers(mpull($branches, 'getCommitIdentifier')) ->withRepository($repository) ->execute(); $list = id(new DiffusionBranchListView()) ->setUser($viewer) ->setBranches($branches) ->setCommits($commits) ->setDiffusionRequest($drequest); $content = id(new PHUIObjectBoxView()) ->setHeaderText($repository->getName()) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($list) ->setPager($pager); } $crumbs = $this->buildCrumbs( array( 'branches' => true, )); $crumbs->setBorder(true); $header = id(new PHUIHeaderView()) ->setHeader(pht('Branches')) ->setHeaderIcon('fa-code-fork'); + $tabs = $this->buildTabsView('branch'); + $view = id(new PHUITwoColumnView()) ->setHeader($header) + ->setTabs($tabs) ->setFooter(array( $content, )); return $this->newPage() ->setTitle( array( pht('Branches'), $repository->getDisplayName(), )) ->setCrumbs($crumbs) ->appendChild($view); } } diff --git a/src/applications/diffusion/controller/DiffusionBrowseController.php b/src/applications/diffusion/controller/DiffusionBrowseController.php index 4eb6144ede..3fa9e62948 100644 --- a/src/applications/diffusion/controller/DiffusionBrowseController.php +++ b/src/applications/diffusion/controller/DiffusionBrowseController.php @@ -1,2018 +1,2019 @@ loadDiffusionContext(); if ($response) { return $response; } $drequest = $this->getDiffusionRequest(); // Figure out if we're browsing a directory, a file, or a search result // list. $grep = $request->getStr('grep'); $find = $request->getStr('find'); if (strlen($grep) || strlen($find)) { return $this->browseSearch(); } $pager = id(new PHUIPagerView()) ->readFromRequest($request); $results = DiffusionBrowseResultSet::newFromConduit( $this->callConduitWithDiffusionRequest( 'diffusion.browsequery', array( 'path' => $drequest->getPath(), 'commit' => $drequest->getStableCommit(), 'offset' => $pager->getOffset(), 'limit' => $pager->getPageSize() + 1, ))); $reason = $results->getReasonForEmptyResultSet(); $is_file = ($reason == DiffusionBrowseResultSet::REASON_IS_FILE); if ($is_file) { return $this->browseFile(); } else { $paths = $results->getPaths(); $paths = $pager->sliceResults($paths); $results->setPaths($paths); return $this->browseDirectory($results, $pager); } } private function browseSearch() { $drequest = $this->getDiffusionRequest(); $header = $this->buildHeaderView($drequest); $search_form = $this->renderSearchForm(); $search_results = $this->renderSearchResults(); $search_form = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Search')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($search_form); $crumbs = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'browse', )); $crumbs->setBorder(true); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter( array( $search_form, $search_results, )); return $this->newPage() ->setTitle( array( nonempty(basename($drequest->getPath()), '/'), $drequest->getRepository()->getDisplayName(), )) ->setCrumbs($crumbs) ->appendChild($view); } private function browseFile() { $viewer = $this->getViewer(); $request = $this->getRequest(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $before = $request->getStr('before'); if ($before) { return $this->buildBeforeResponse($before); } $path = $drequest->getPath(); $blame_key = PhabricatorDiffusionBlameSetting::SETTINGKEY; - $show_blame = $request->getBool( 'blame', $viewer->getUserSetting($blame_key)); $view = $request->getStr('view'); if ($request->isFormPost() && $view != 'raw' && $viewer->isLoggedIn()) { $preferences = PhabricatorUserPreferences::loadUserPreferences($viewer); $editor = id(new PhabricatorUserPreferencesEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); $xactions = array(); $xactions[] = $preferences->newTransaction($blame_key, $show_blame); $editor->applyTransactions($preferences, $xactions); $uri = $request->getRequestURI() ->alter('blame', null); return id(new AphrontRedirectResponse())->setURI($uri); } // We need the blame information if blame is on and this is an Ajax request. // If blame is on and this is a colorized request, we don't show blame at // first (we ajax it in afterward) so we don't need to query for it. $needs_blame = ($show_blame && $request->isAjax()); $params = array( 'commit' => $drequest->getCommit(), 'path' => $drequest->getPath(), ); $byte_limit = null; if ($view !== 'raw') { $byte_limit = PhabricatorFileStorageEngine::getChunkThreshold(); $time_limit = 10; $params += array( 'timeout' => $time_limit, 'byteLimit' => $byte_limit, ); } $response = $this->callConduitWithDiffusionRequest( 'diffusion.filecontentquery', $params); $hit_byte_limit = $response['tooHuge']; $hit_time_limit = $response['tooSlow']; $file_phid = $response['filePHID']; + $show_editor = false; if ($hit_byte_limit) { $corpus = $this->buildErrorCorpus( pht( 'This file is larger than %s byte(s), and too large to display '. 'in the web UI.', phutil_format_bytes($byte_limit))); } else if ($hit_time_limit) { $corpus = $this->buildErrorCorpus( pht( 'This file took too long to load from the repository (more than '. '%s second(s)).', new PhutilNumber($time_limit))); } else { $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($file_phid)) ->executeOne(); if (!$file) { throw new Exception(pht('Failed to load content file!')); } if ($view === 'raw') { return $file->getRedirectResponse(); } $data = $file->loadFileData(); $lfs_ref = $this->getGitLFSRef($repository, $data); if ($lfs_ref) { if ($view == 'git-lfs') { $file = $this->loadGitLFSFile($lfs_ref); // Rename the file locally so we generate a better vanity URI for // it. In storage, it just has a name like "lfs-13f9a94c0923...", // since we don't get any hints about possible human-readable names // at upload time. $basename = basename($drequest->getPath()); $file->makeEphemeral(); $file->setName($basename); return $file->getRedirectResponse(); } else { $corpus = $this->buildGitLFSCorpus($lfs_ref); } } else if (ArcanistDiffUtils::isHeuristicBinaryFile($data)) { $file_uri = $file->getBestURI(); if ($file->isViewableImage()) { $corpus = $this->buildImageCorpus($file_uri); } else { $corpus = $this->buildBinaryCorpus($file_uri, $data); } } else { $this->loadLintMessages(); $this->coverage = $drequest->loadCoverage(); + $show_editor = true; // Build the content of the file. $corpus = $this->buildCorpus( $show_blame, $data, $needs_blame, $drequest, $path, $data); } } if ($request->isAjax()) { return id(new AphrontAjaxResponse())->setContent($corpus); } require_celerity_resource('diffusion-source-css'); // Render the page. - $view = $this->buildCurtain($drequest); - $curtain = $this->enrichCurtain( - $view, - $drequest, - $show_blame); - + $curtain = $this->buildCurtain($drequest, $show_blame, $show_editor); $properties = $this->buildPropertyView($drequest); $header = $this->buildHeaderView($drequest); $header->setHeaderIcon('fa-file-code-o'); $content = array(); $follow = $request->getStr('follow'); if ($follow) { $notice = new PHUIInfoView(); $notice->setSeverity(PHUIInfoView::SEVERITY_WARNING); $notice->setTitle(pht('Unable to Continue')); switch ($follow) { case 'first': $notice->appendChild( pht( 'Unable to continue tracing the history of this file because '. 'this commit is the first commit in the repository.')); break; case 'created': $notice->appendChild( pht( 'Unable to continue tracing the history of this file because '. 'this commit created the file.')); break; } $content[] = $notice; } $renamed = $request->getStr('renamed'); if ($renamed) { $notice = new PHUIInfoView(); $notice->setSeverity(PHUIInfoView::SEVERITY_NOTICE); $notice->setTitle(pht('File Renamed')); $notice->appendChild( pht( 'File history passes through a rename from "%s" to "%s".', $drequest->getPath(), $renamed)); $content[] = $notice; } $content[] = $corpus; $content[] = $this->buildOpenRevisions(); $crumbs = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'browse', )); $crumbs->setBorder(true); $basename = basename($this->getDiffusionRequest()->getPath()); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) ->setMainColumn(array( $content, )); if ($properties) { $view->addPropertySection(pht('Details'), $properties); } $title = array($basename, $repository->getDisplayName()); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild( array( $view, )); } public function browseDirectory( DiffusionBrowseResultSet $results, PHUIPagerView $pager) { $request = $this->getRequest(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $reason = $results->getReasonForEmptyResultSet(); - $curtain = $this->buildCurtain($drequest); + $actions = $this->getActions($drequest); $details = $this->buildPropertyView($drequest); $header = $this->buildHeaderView($drequest); $header->setHeaderIcon('fa-folder-open'); $search_form = $this->renderSearchForm(); $empty_result = null; $browse_panel = null; $branch_panel = null; if (!$results->isValidResults()) { $empty_result = new DiffusionEmptyResultView(); $empty_result->setDiffusionRequest($drequest); $empty_result->setDiffusionBrowseResultSet($results); $empty_result->setView($request->getStr('view')); } else { $phids = array(); foreach ($results->getPaths() as $result) { $data = $result->getLastCommitData(); if ($data) { if ($data->getCommitDetail('authorPHID')) { $phids[$data->getCommitDetail('authorPHID')] = true; } } } $phids = array_keys($phids); $handles = $this->loadViewerHandles($phids); $browse_table = id(new DiffusionBrowseTableView()) ->setDiffusionRequest($drequest) ->setHandles($handles) ->setPaths($results->getPaths()) ->setUser($request->getUser()); - $browse_header = id(new PHUIHeaderView()) - ->setHeader(nonempty(basename($drequest->getPath()), '/')) - ->setHeaderIcon('fa-folder-open'); + $title = nonempty(basename($drequest->getPath()), '/'); + $icon = 'fa-folder-open'; + $browse_header = $this->buildPanelHeaderView($title, $icon, $actions); $browse_panel = id(new PHUIObjectBoxView()) ->setHeader($browse_header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($browse_table) ->setPager($pager); $browse_panel->setShowHide( array(pht('Show Search')), pht('Hide Search'), $search_form, '#'); $path = $drequest->getPath(); $is_branch = (!strlen($path) && $repository->supportsBranchComparison()); if ($is_branch) { $branch_panel = $this->buildBranchTable(); } } $open_revisions = $this->buildOpenRevisions(); $readme = $this->renderDirectoryReadme($results); $crumbs = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'browse', )); $crumbs->setBorder(true); $view = id(new PHUITwoColumnView()) ->setHeader($header) - ->setCurtain($curtain) - ->setMainColumn( + ->setFooter( array( $branch_panel, $empty_result, $browse_panel, - )) - ->setFooter( - array( $open_revisions, $readme, )); if ($details) { $view->addPropertySection(pht('Details'), $details); } return $this->newPage() ->setTitle(array( nonempty(basename($drequest->getPath()), '/'), $repository->getDisplayName(), )) ->setCrumbs($crumbs) ->appendChild( array( $view, )); } private function renderSearchResults() { $request = $this->getRequest(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $results = array(); $pager = id(new PHUIPagerView()) ->readFromRequest($request); $search_mode = null; switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $results = array(); break; default: if (strlen($this->getRequest()->getStr('grep'))) { $search_mode = 'grep'; $query_string = $request->getStr('grep'); $results = $this->callConduitWithDiffusionRequest( 'diffusion.searchquery', array( 'grep' => $query_string, 'commit' => $drequest->getStableCommit(), 'path' => $drequest->getPath(), 'limit' => $pager->getPageSize() + 1, 'offset' => $pager->getOffset(), )); } else { // Filename search. $search_mode = 'find'; $query_string = $request->getStr('find'); $results = $this->callConduitWithDiffusionRequest( 'diffusion.querypaths', array( 'pattern' => $query_string, 'commit' => $drequest->getStableCommit(), 'path' => $drequest->getPath(), 'limit' => $pager->getPageSize() + 1, 'offset' => $pager->getOffset(), )); } break; } $results = $pager->sliceResults($results); if ($search_mode == 'grep') { $table = $this->renderGrepResults($results, $query_string); $header = pht( 'File content matching "%s" under "%s"', $query_string, nonempty($drequest->getPath(), '/')); } else { $table = $this->renderFindResults($results); $header = pht( 'Paths matching "%s" under "%s"', $query_string, nonempty($drequest->getPath(), '/')); } return id(new PHUIObjectBoxView()) ->setHeaderText($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($table) ->setPager($pager); } private function renderGrepResults(array $results, $pattern) { $drequest = $this->getDiffusionRequest(); require_celerity_resource('phabricator-search-results-css'); $rows = array(); foreach ($results as $result) { list($path, $line, $string) = $result; $href = $drequest->generateURI(array( 'action' => 'browse', 'path' => $path, 'line' => $line, )); $matches = null; $count = @preg_match_all( '('.$pattern.')u', $string, $matches, PREG_OFFSET_CAPTURE); if (!$count) { $output = ltrim($string); } else { $output = array(); $cursor = 0; $length = strlen($string); foreach ($matches[0] as $match) { $offset = $match[1]; if ($cursor != $offset) { $output[] = array( 'text' => substr($string, $cursor, $offset), 'highlight' => false, ); } $output[] = array( 'text' => $match[0], 'highlight' => true, ); $cursor = $offset + strlen($match[0]); } if ($cursor != $length) { $output[] = array( 'text' => substr($string, $cursor), 'highlight' => false, ); } if ($output) { $output[0]['text'] = ltrim($output[0]['text']); } foreach ($output as $key => $segment) { if ($segment['highlight']) { $output[$key] = phutil_tag('strong', array(), $segment['text']); } else { $output[$key] = $segment['text']; } } } $string = phutil_tag( 'pre', array('class' => 'PhabricatorMonospaced phui-source-fragment'), $output); $path = Filesystem::readablePath($path, $drequest->getPath()); $rows[] = array( phutil_tag('a', array('href' => $href), $path), $line, $string, ); } $table = id(new AphrontTableView($rows)) ->setClassName('remarkup-code') ->setHeaders(array(pht('Path'), pht('Line'), pht('String'))) ->setColumnClasses(array('', 'n', 'wide')) ->setNoDataString( pht( 'The pattern you searched for was not found in the content of any '. 'files.')); return $table; } private function renderFindResults(array $results) { $drequest = $this->getDiffusionRequest(); $rows = array(); foreach ($results as $result) { $href = $drequest->generateURI(array( 'action' => 'browse', 'path' => $result, )); $readable = Filesystem::readablePath($result, $drequest->getPath()); $rows[] = array( phutil_tag('a', array('href' => $href), $readable), ); } $table = id(new AphrontTableView($rows)) ->setHeaders(array(pht('Path'))) ->setColumnClasses(array('wide')) ->setNoDataString( pht( 'The pattern you searched for did not match the names of any '. 'files.')); return $table; } private function loadLintMessages() { $drequest = $this->getDiffusionRequest(); $branch = $drequest->loadBranch(); if (!$branch || !$branch->getLintCommit()) { return; } $this->lintCommit = $branch->getLintCommit(); $conn = id(new PhabricatorRepository())->establishConnection('r'); $where = ''; if ($drequest->getLint()) { $where = qsprintf( $conn, 'AND code = %s', $drequest->getLint()); } $this->lintMessages = queryfx_all( $conn, 'SELECT * FROM %T WHERE branchID = %d %Q AND path = %s', PhabricatorRepository::TABLE_LINTMESSAGE, $branch->getID(), $where, '/'.$drequest->getPath()); } private function buildCorpus( $show_blame, $file_corpus, $needs_blame, DiffusionRequest $drequest, $path, $data) { $viewer = $this->getViewer(); $blame_timeout = 15; $blame_failed = false; $highlight_limit = DifferentialChangesetParser::HIGHLIGHT_BYTE_LIMIT; $blame_limit = DifferentialChangesetParser::HIGHLIGHT_BYTE_LIMIT; $can_highlight = (strlen($file_corpus) <= $highlight_limit); $can_blame = (strlen($file_corpus) <= $blame_limit); if ($needs_blame && $can_blame) { $blame = $this->loadBlame($path, $drequest->getCommit(), $blame_timeout); list($blame_list, $blame_commits) = $blame; if ($blame_list === null) { $blame_failed = true; $blame_list = array(); } } else { $blame_list = array(); $blame_commits = array(); } require_celerity_resource('syntax-highlighting-css'); if ($can_highlight) { $highlighted = PhabricatorSyntaxHighlighter::highlightWithFilename( $path, $file_corpus); } else { // Highlight as plain text to escape the content properly. $highlighted = PhabricatorSyntaxHighlighter::highlightWithLanguage( 'txt', $file_corpus); } $lines = phutil_split_lines($highlighted); $rows = $this->buildDisplayRows( $lines, $blame_list, $blame_commits, $show_blame); $corpus_table = javelin_tag( 'table', array( 'class' => 'diffusion-source remarkup-code PhabricatorMonospaced', 'sigil' => 'phabricator-source', ), $rows); if ($this->getRequest()->isAjax()) { return $corpus_table; } $id = celerity_generate_unique_node_id(); $repo = $drequest->getRepository(); $symbol_repos = nonempty($repo->getSymbolSources(), array()); $symbol_repos[] = $repo->getPHID(); $lang = last(explode('.', $drequest->getPath())); $repo_languages = $repo->getSymbolLanguages(); $repo_languages = nonempty($repo_languages, array()); $repo_languages = array_fill_keys($repo_languages, true); $needs_symbols = true; if ($repo_languages && $symbol_repos) { $have_symbols = id(new DiffusionSymbolQuery()) ->existsSymbolsInRepository($repo->getPHID()); if (!$have_symbols) { $needs_symbols = false; } } if ($needs_symbols && $repo_languages) { $needs_symbols = isset($repo_languages[$lang]); } if ($needs_symbols) { Javelin::initBehavior( 'repository-crossreference', array( 'container' => $id, 'lang' => $lang, 'repositories' => $symbol_repos, )); } $corpus = phutil_tag( 'div', array( 'id' => $id, ), $corpus_table); Javelin::initBehavior('load-blame', array('id' => $id)); - - $edit = $this->renderEditButton(); $file = $this->renderFileButton(); - $header = id(new PHUIHeaderView()) - ->setHeader(basename($this->getDiffusionRequest()->getPath())) - ->setHeaderIcon('fa-file-code-o') - ->addActionLink($edit) - ->addActionLink($file); + $title = basename($this->getDiffusionRequest()->getPath()); + $icon = 'fa-file-code-o'; + $drequest = $this->getDiffusionRequest(); + $actions = $this->getActions($drequest); + + $header = $this->buildPanelHeaderView($title, $icon, $actions); + $header->addActionLink($file); $corpus = id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($corpus) ->setCollapsed(true); $messages = array(); if (!$can_highlight) { $messages[] = pht( 'This file is larger than %s, so syntax highlighting is disabled '. 'by default.', phutil_format_bytes($highlight_limit)); } if ($show_blame && !$can_blame) { $messages[] = pht( 'This file is larger than %s, so blame is disabled.', phutil_format_bytes($blame_limit)); } if ($blame_failed) { $messages[] = pht( 'Failed to load blame information for this file in %s second(s).', new PhutilNumber($blame_timeout)); } if ($messages) { $corpus->setInfoView( id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setErrors($messages)); } return $corpus; } - private function enrichCurtain( - PHUICurtainView $curtain, + private function buildCurtain( DiffusionRequest $drequest, - $show_blame) { + $show_blame, + $show_editor) { + $curtain = $this->newCurtainView($drequest); $viewer = $this->getViewer(); $base_uri = $this->getRequest()->getRequestURI(); + $user = $this->getRequest()->getUser(); + $repository = $drequest->getRepository(); + $path = $drequest->getPath(); + $line = nonempty((int)$drequest->getLine(), 1); + + $editor_link = $user->loadEditorLink($path, $line, $repository); + $template = $user->loadEditorLink($path, '%l', $repository); + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Show Last Change')) ->setHref( $drequest->generateURI( array( 'action' => 'change', ))) ->setIcon('fa-backward')); if ($show_blame) { $blame_text = pht('Disable Blame'); $blame_icon = 'fa-exclamation-circle lightgreytext'; $blame_value = 0; } else { $blame_text = pht('Enable Blame'); $blame_icon = 'fa-exclamation-circle'; $blame_value = 1; } $curtain->addAction( id(new PhabricatorActionView()) ->setName($blame_text) ->setHref($base_uri->alter('blame', $blame_value)) ->setIcon($blame_icon) ->setUser($viewer) ->setRenderAsForm($viewer->isLoggedIn())); + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Open in Editor')) + ->setHref($editor_link) + ->setIcon('fa-pencil') + ->setID('editor_link') + ->setMetadata(array('link_template' => $template)) + ->setDisabled(!$editor_link)); + $href = null; if ($this->getRequest()->getStr('lint') !== null) { $lint_text = pht('Hide %d Lint Message(s)', count($this->lintMessages)); $href = $base_uri->alter('lint', null); } else if ($this->lintCommit === null) { $lint_text = pht('Lint not Available'); } else { $lint_text = pht( 'Show %d Lint Message(s)', count($this->lintMessages)); $href = $this->getDiffusionRequest()->generateURI(array( 'action' => 'browse', 'commit' => $this->lintCommit, ))->alter('lint', ''); } $curtain->addAction( id(new PhabricatorActionView()) ->setName($lint_text) ->setHref($href) ->setIcon('fa-exclamation-triangle') ->setDisabled(!$href)); $repository = $drequest->getRepository(); $owners = 'PhabricatorOwnersApplication'; if (PhabricatorApplication::isClassInstalled($owners)) { $package_query = id(new PhabricatorOwnersPackageQuery()) ->setViewer($viewer) ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE)) ->withControl( $repository->getPHID(), array( $drequest->getPath(), )); $package_query->execute(); $packages = $package_query->getControllingPackagesForPath( $repository->getPHID(), $drequest->getPath()); if ($packages) { $ownership = id(new PHUIStatusListView()) ->setUser($viewer); foreach ($packages as $package) { $icon = 'fa-list-alt'; $color = 'grey'; $item = id(new PHUIStatusItemView()) ->setIcon($icon, $color) ->setTarget($viewer->renderHandle($package->getPHID())); $ownership->addItem($item); } } else { $ownership = phutil_tag('em', array(), pht('None')); } $curtain->newPanel() ->setHeaderText(pht('Owners')) ->appendChild($ownership); } return $curtain; } - private function renderEditButton() { - $request = $this->getRequest(); - $user = $request->getUser(); - - $drequest = $this->getDiffusionRequest(); - - $repository = $drequest->getRepository(); - $path = $drequest->getPath(); - $line = nonempty((int)$drequest->getLine(), 1); - - $editor_link = $user->loadEditorLink($path, $line, $repository); - $template = $user->loadEditorLink($path, '%l', $repository); - - $button = id(new PHUIButtonView()) - ->setTag('a') - ->setText(pht('Open in Editor')) - ->setHref($editor_link) - ->setIcon('fa-pencil') - ->setID('editor_link') - ->setMetadata(array('link_template' => $template)) - ->setDisabled(!$editor_link); - - return $button; - } - private function renderFileButton($file_uri = null, $label = null) { $base_uri = $this->getRequest()->getRequestURI(); if ($file_uri) { - $text = pht('Download Raw File'); + $text = pht('Download Raw'); $href = $file_uri; $icon = 'fa-download'; } else { - $text = pht('View Raw File'); + $text = pht('View Raw'); $href = $base_uri->alter('view', 'raw'); $icon = 'fa-file-text'; } if ($label !== null) { $text = $label; } $button = id(new PHUIButtonView()) ->setTag('a') ->setText($text) ->setHref($href) ->setIcon($icon); return $button; } private function renderGitLFSButton() { $viewer = $this->getViewer(); $uri = $this->getRequest()->getRequestURI(); $href = $uri->alter('view', 'git-lfs'); $text = pht('Download from Git LFS'); $icon = 'fa-download'; return id(new PHUIButtonView()) ->setTag('a') ->setText($text) ->setHref($href) ->setIcon($icon); } private function buildDisplayRows( array $lines, array $blame_list, array $blame_commits, $show_blame) { $request = $this->getRequest(); $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $revision_map = array(); $revisions = array(); if ($blame_commits) { $commit_map = mpull($blame_commits, 'getCommitIdentifier', 'getPHID'); $revision_ids = id(new DifferentialRevision()) ->loadIDsByCommitPHIDs(array_keys($commit_map)); if ($revision_ids) { $revisions = id(new DifferentialRevisionQuery()) ->setViewer($viewer) ->withIDs($revision_ids) ->execute(); $revisions = mpull($revisions, null, 'getID'); } foreach ($revision_ids as $commit_phid => $revision_id) { $revision_map[$commit_map[$commit_phid]] = $revision_id; } } $phids = array(); foreach ($blame_commits as $commit) { $author_phid = $commit->getAuthorPHID(); if ($author_phid === null) { continue; } $phids[$author_phid] = $author_phid; } foreach ($revisions as $revision) { $author_phid = $revision->getAuthorPHID(); if ($author_phid === null) { continue; } $phids[$author_phid] = $author_phid; } $handles = $viewer->loadHandles($phids); $colors = array(); if ($blame_commits) { $epochs = array(); foreach ($blame_commits as $identifier => $commit) { $epochs[$identifier] = $commit->getEpoch(); } $epoch_list = array_filter($epochs); $epoch_list = array_unique($epoch_list); $epoch_list = array_values($epoch_list); $epoch_min = min($epoch_list); $epoch_max = max($epoch_list); $epoch_range = ($epoch_max - $epoch_min) + 1; foreach ($blame_commits as $identifier => $commit) { $epoch = $epochs[$identifier]; if (!$epoch) { $color = '#ffffdd'; // Warning color, missing data. } else { $color_ratio = ($epoch - $epoch_min) / $epoch_range; $color_value = 0xE6 * (1.0 - $color_ratio); $color = sprintf( '#%02x%02x%02x', $color_value, 0xF6, $color_value); } $colors[$identifier] = $color; } } $display = array(); $last_identifier = null; $last_color = null; foreach ($lines as $line_index => $line) { $color = '#f6f6f6'; $duplicate = false; if (isset($blame_list[$line_index])) { $identifier = $blame_list[$line_index]; if (isset($colors[$identifier])) { $color = $colors[$identifier]; } if ($identifier === $last_identifier) { $duplicate = true; } else { $last_identifier = $identifier; } } $display[$line_index] = array( 'data' => $line, 'target' => false, 'highlighted' => false, 'color' => $color, 'duplicate' => $duplicate, ); } $line_arr = array(); $line_str = $drequest->getLine(); $ranges = explode(',', $line_str); foreach ($ranges as $range) { if (strpos($range, '-') !== false) { list($min, $max) = explode('-', $range, 2); $line_arr[] = array( 'min' => min($min, $max), 'max' => max($min, $max), ); } else if (strlen($range)) { $line_arr[] = array( 'min' => $range, 'max' => $range, ); } } // Mark the first highlighted line as the target line. if ($line_arr) { $target_line = $line_arr[0]['min']; if (isset($display[$target_line - 1])) { $display[$target_line - 1]['target'] = true; } } // Mark all other highlighted lines as highlighted. foreach ($line_arr as $range) { for ($ii = $range['min']; $ii <= $range['max']; $ii++) { if (isset($display[$ii - 1])) { $display[$ii - 1]['highlighted'] = true; } } } $engine = null; $inlines = array(); if ($this->getRequest()->getStr('lint') !== null && $this->lintMessages) { $engine = new PhabricatorMarkupEngine(); $engine->setViewer($viewer); foreach ($this->lintMessages as $message) { $inline = id(new PhabricatorAuditInlineComment()) ->setSyntheticAuthor( ArcanistLintSeverity::getStringForSeverity($message['severity']). ' '.$message['code'].' ('.$message['name'].')') ->setLineNumber($message['line']) ->setContent($message['description']); $inlines[$message['line']][] = $inline; $engine->addObject( $inline, PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY); } $engine->process(); require_celerity_resource('differential-changeset-view-css'); } $rows = $this->renderInlines( idx($inlines, 0, array()), $show_blame, (bool)$this->coverage, $engine); // NOTE: We're doing this manually because rendering is otherwise // dominated by URI generation for very large files. $line_base = (string)$drequest->generateURI( array( 'action' => 'browse', 'stable' => true, )); require_celerity_resource('aphront-tooltip-css'); Javelin::initBehavior('phabricator-oncopy'); Javelin::initBehavior('phabricator-tooltips'); Javelin::initBehavior('phabricator-line-linker'); // Render these once, since they tend to get repeated many times in large // blame outputs. $commit_links = $this->renderCommitLinks($blame_commits, $handles); $revision_links = $this->renderRevisionLinks($revisions, $handles); if ($this->coverage) { require_celerity_resource('differential-changeset-view-css'); Javelin::initBehavior( 'diffusion-browse-file', array( 'labels' => array( 'cov-C' => pht('Covered'), 'cov-N' => pht('Not Covered'), 'cov-U' => pht('Not Executable'), ), )); } $skip_text = pht('Skip Past This Commit'); foreach ($display as $line_index => $line) { $row = array(); $line_number = $line_index + 1; $line_href = $line_base.'$'.$line_number; if (isset($blame_list[$line_index])) { $identifier = $blame_list[$line_index]; } else { $identifier = null; } $revision_link = null; $commit_link = null; $before_link = null; $style = 'background: '.$line['color'].';'; if ($identifier && !$line['duplicate']) { if (isset($commit_links[$identifier])) { $commit_link = $commit_links[$identifier]; } if (isset($revision_map[$identifier])) { $revision_id = $revision_map[$identifier]; if (isset($revision_links[$revision_id])) { $revision_link = $revision_links[$revision_id]; } } $skip_href = $line_href.'?before='.$identifier.'&view=blame'; $before_link = javelin_tag( 'a', array( 'href' => $skip_href, 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $skip_text, 'align' => 'E', 'size' => 300, ), ), "\xC2\xAB"); } if ($show_blame) { $row[] = phutil_tag( 'th', array( 'class' => 'diffusion-blame-link', ), $before_link); $object_links = array(); $object_links[] = $commit_link; if ($revision_link) { $object_links[] = phutil_tag('span', array(), '/'); $object_links[] = $revision_link; } $row[] = phutil_tag( 'th', array( 'class' => 'diffusion-rev-link', ), $object_links); } $line_link = phutil_tag( 'a', array( 'href' => $line_href, 'style' => $style, ), $line_number); $row[] = javelin_tag( 'th', array( 'class' => 'diffusion-line-link', 'sigil' => 'phabricator-source-line', 'style' => $style, ), $line_link); if ($line['target']) { Javelin::initBehavior( 'diffusion-jump-to', array( 'target' => 'scroll_target', )); $anchor_text = phutil_tag( 'a', array( 'id' => 'scroll_target', ), ''); } else { $anchor_text = null; } $row[] = phutil_tag( 'td', array( ), array( $anchor_text, // NOTE: See phabricator-oncopy behavior. "\xE2\x80\x8B", // TODO: [HTML] Not ideal. phutil_safe_html(str_replace("\t", ' ', $line['data'])), )); if ($this->coverage) { $cov_index = $line_index; if (isset($this->coverage[$cov_index])) { $cov_class = $this->coverage[$cov_index]; } else { $cov_class = 'N'; } $row[] = phutil_tag( 'td', array( 'class' => 'cov cov-'.$cov_class, ), ''); } $rows[] = phutil_tag( 'tr', array( 'class' => ($line['highlighted'] ? 'phabricator-source-highlight' : null), ), $row); $cur_inlines = $this->renderInlines( idx($inlines, $line_number, array()), $show_blame, $this->coverage, $engine); foreach ($cur_inlines as $cur_inline) { $rows[] = $cur_inline; } } return $rows; } private function renderInlines( array $inlines, $show_blame, $has_coverage, $engine) { $rows = array(); foreach ($inlines as $inline) { // TODO: This should use modern scaffolding code. $inline_view = id(new PHUIDiffInlineCommentDetailView()) ->setUser($this->getViewer()) ->setMarkupEngine($engine) ->setInlineComment($inline) ->render(); $row = array_fill(0, ($show_blame ? 3 : 1), phutil_tag('th')); $row[] = phutil_tag('td', array(), $inline_view); if ($has_coverage) { $row[] = phutil_tag( 'td', array( 'class' => 'cov cov-I', )); } $rows[] = phutil_tag('tr', array('class' => 'inline'), $row); } return $rows; } private function buildImageCorpus($file_uri) { $properties = new PHUIPropertyListView(); $properties->addImageContent( phutil_tag( 'img', array( 'src' => $file_uri, ))); $file = $this->renderFileButton($file_uri); - $header = id(new PHUIHeaderView()) - ->setHeader(basename($this->getDiffusionRequest()->getPath())) - ->addActionLink($file) - ->setHeaderIcon('fa-file-image-o'); + $title = basename($this->getDiffusionRequest()->getPath()); + $icon = 'fa-file-image-o'; + $drequest = $this->getDiffusionRequest(); + $actions = $this->getActions($drequest); + $header = $this->buildPanelHeaderView($title, $icon, $actions); + $header->addActionLink($file); return id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->addPropertyList($properties); } private function buildBinaryCorpus($file_uri, $data) { $size = new PhutilNumber(strlen($data)); $text = pht('This is a binary file. It is %s byte(s) in length.', $size); $text = id(new PHUIBoxView()) ->addPadding(PHUI::PADDING_LARGE) ->appendChild($text); $file = $this->renderFileButton($file_uri); - $header = id(new PHUIHeaderView()) - ->setHeader(pht('Details')) - ->addActionLink($file); + $title = basename($this->getDiffusionRequest()->getPath()); + $icon = 'fa-file'; + $drequest = $this->getDiffusionRequest(); + $actions = $this->getActions($drequest); + $header = $this->buildPanelHeaderView($title, $icon, $actions); + $header->addActionLink($file); $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($text); return $box; } private function buildErrorCorpus($message) { $text = id(new PHUIBoxView()) ->addPadding(PHUI::PADDING_LARGE) ->appendChild($message); $header = id(new PHUIHeaderView()) ->setHeader(pht('Details')); $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($text); return $box; } private function buildBeforeResponse($before) { $request = $this->getRequest(); $drequest = $this->getDiffusionRequest(); // NOTE: We need to get the grandparent so we can capture filename changes // in the parent. $parent = $this->loadParentCommitOf($before); $old_filename = null; $was_created = false; if ($parent) { $grandparent = $this->loadParentCommitOf($parent); if ($grandparent) { $rename_query = new DiffusionRenameHistoryQuery(); $rename_query->setRequest($drequest); $rename_query->setOldCommit($grandparent); $rename_query->setViewer($request->getUser()); $old_filename = $rename_query->loadOldFilename(); $was_created = $rename_query->getWasCreated(); } } $follow = null; if ($was_created) { // If the file was created in history, that means older commits won't // have it. Since we know it existed at 'before', it must have been // created then; jump there. $target_commit = $before; $follow = 'created'; } else if ($parent) { // If we found a parent, jump to it. This is the normal case. $target_commit = $parent; } else { // If there's no parent, this was probably created in the initial commit? // And the "was_created" check will fail because we can't identify the // grandparent. Keep the user at 'before'. $target_commit = $before; $follow = 'first'; } $path = $drequest->getPath(); $renamed = null; if ($old_filename !== null && $old_filename !== '/'.$path) { $renamed = $path; $path = $old_filename; } $line = null; // If there's a follow error, drop the line so the user sees the message. if (!$follow) { $line = $this->getBeforeLineNumber($target_commit); } $before_uri = $drequest->generateURI( array( 'action' => 'browse', 'commit' => $target_commit, 'line' => $line, 'path' => $path, )); $before_uri->setQueryParams($request->getRequestURI()->getQueryParams()); $before_uri = $before_uri->alter('before', null); $before_uri = $before_uri->alter('renamed', $renamed); $before_uri = $before_uri->alter('follow', $follow); return id(new AphrontRedirectResponse())->setURI($before_uri); } private function getBeforeLineNumber($target_commit) { $drequest = $this->getDiffusionRequest(); $viewer = $this->getViewer(); $line = $drequest->getLine(); if (!$line) { return null; } $diff_info = $this->callConduitWithDiffusionRequest( 'diffusion.rawdiffquery', array( 'commit' => $drequest->getCommit(), 'path' => $drequest->getPath(), 'againstCommit' => $target_commit, )); $file_phid = $diff_info['filePHID']; $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($file_phid)) ->executeOne(); if (!$file) { throw new Exception( pht( 'Failed to load file ("%s") returned by "%s".', $file_phid, 'diffusion.rawdiffquery.')); } $raw_diff = $file->loadFileData(); $old_line = 0; $new_line = 0; foreach (explode("\n", $raw_diff) as $text) { if ($text[0] == '-' || $text[0] == ' ') { $old_line++; } if ($text[0] == '+' || $text[0] == ' ') { $new_line++; } if ($new_line == $line) { return $old_line; } } // We didn't find the target line. return $line; } private function loadParentCommitOf($commit) { $drequest = $this->getDiffusionRequest(); $user = $this->getRequest()->getUser(); $before_req = DiffusionRequest::newFromDictionary( array( 'user' => $user, 'repository' => $drequest->getRepository(), 'commit' => $commit, )); $parents = DiffusionQuery::callConduitWithDiffusionRequest( $user, $before_req, 'diffusion.commitparentsquery', array( 'commit' => $commit, )); return head($parents); } private function renderRevisionTooltip( DifferentialRevision $revision, $handles) { $viewer = $this->getRequest()->getUser(); $date = phabricator_date($revision->getDateModified(), $viewer); $id = $revision->getID(); $title = $revision->getTitle(); $header = "D{$id} {$title}"; $author = $handles[$revision->getAuthorPHID()]->getName(); return "{$header}\n{$date} \xC2\xB7 {$author}"; } private function renderCommitTooltip( PhabricatorRepositoryCommit $commit, $author) { $viewer = $this->getRequest()->getUser(); $date = phabricator_date($commit->getEpoch(), $viewer); $summary = trim($commit->getSummary()); return "{$summary}\n{$date} \xC2\xB7 {$author}"; } protected function renderSearchForm() { $drequest = $this->getDiffusionRequest(); $forms = array(); $form = id(new AphrontFormView()) ->setUser($this->getViewer()) ->setMethod('GET'); switch ($drequest->getRepository()->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $forms[] = id(clone $form) ->appendChild(pht('Search is not available in Subversion.')); break; default: $forms[] = id(clone $form) ->appendChild( id(new AphrontFormTextWithSubmitControl()) ->setLabel(pht('File Name')) ->setSubmitLabel(pht('Search File Names')) ->setName('find') ->setValue($this->getRequest()->getStr('find'))); $forms[] = id(clone $form) ->appendChild( id(new AphrontFormTextWithSubmitControl()) ->setLabel(pht('Pattern')) ->setSubmitLabel(pht('Grep File Content')) ->setName('grep') ->setValue($this->getRequest()->getStr('grep'))); break; } require_celerity_resource('diffusion-icons-css'); $form_box = phutil_tag_div('diffusion-search-boxen', $forms); return $form_box; } protected function markupText($text) { $engine = PhabricatorMarkupEngine::newDiffusionMarkupEngine(); $engine->setConfig('viewer', $this->getRequest()->getUser()); $text = $engine->markupText($text); $text = phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $text); return $text; } protected function buildHeaderView(DiffusionRequest $drequest) { $viewer = $this->getViewer(); $tag = $this->renderCommitHashTag($drequest); $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($this->renderPathLinks($drequest, $mode = 'browse')) ->addTag($tag); return $header; } - protected function buildCurtain(DiffusionRequest $drequest) { - $viewer = $this->getViewer(); - $repository = $drequest->getRepository(); + protected function buildPanelHeaderView($title, $icon, array $actions) { - $curtain = $this->newCurtainView($drequest); + $header = id(new PHUIHeaderView()) + ->setHeader($title) + ->setHeaderIcon($icon) + ->addClass('diffusion-panel-header-view'); - $history_uri = $drequest->generateURI( - array( - 'action' => 'history', - )); + foreach ($actions as $action_link) { + if ($action_link) { + $header->addActionLink($action_link); + } + } + return $header; - $curtain->addAction( - id(new PhabricatorActionView()) - ->setName(pht('View History')) - ->setHref($history_uri) - ->setIcon('fa-list')); + } + protected function getActions(DiffusionRequest $drequest) { + $viewer = $this->getViewer(); + $repository = $drequest->getRepository(); + $history_uri = $drequest->generateURI(array('action' => 'history')); $behind_head = $drequest->getSymbolicCommit(); - - if ($repository->supportsBranchComparison()) { - $compare_uri = $drequest->generateURI( - array( - 'action' => 'compare', - )); - - $curtain->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Compare Against...')) - ->setIcon('fa-code-fork') - ->setWorkflow(true) - ->setHref($compare_uri)); - } - $head_uri = $drequest->generateURI( array( 'commit' => '', 'action' => 'browse', )); - $curtain->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Jump to HEAD')) + + if ($repository->supportsBranchComparison()) { + $compare_uri = $drequest->generateURI(array('action' => 'compare')); + $compare = id(new PHUIButtonView()) + ->setText(pht('Compare')) + ->setIcon('fa-code-fork') + ->setWorkflow(true) + ->setTag('a') + ->setHref($compare_uri) + ->setColor(PHUIButtonView::GREY); + } + + $head = null; + if ($behind_head) { + $head = id(new PHUIButtonView()) + ->setText(pht('Back to HEAD')) ->setHref($head_uri) ->setIcon('fa-home') - ->setDisabled(!$behind_head)); + ->setColor(PHUIButtonView::GREY); + } - return $curtain; + $history = id(new PHUIButtonView()) + ->setText(pht('History')) + ->setHref($history_uri) + ->setTag('a') + ->setIcon('fa-history') + ->setColor(PHUIButtonView::GREY); + + return array($history, $compare, $head); } protected function buildPropertyView( DiffusionRequest $drequest) { $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setUser($viewer); if ($drequest->getSymbolicType() == 'tag') { $symbolic = $drequest->getSymbolicCommit(); $view->addProperty(pht('Tag'), $symbolic); $tags = $this->callConduitWithDiffusionRequest( 'diffusion.tagsquery', array( 'names' => array($symbolic), 'needMessages' => true, )); $tags = DiffusionRepositoryTag::newFromConduit($tags); $tags = mpull($tags, null, 'getName'); $tag = idx($tags, $symbolic); if ($tag && strlen($tag->getMessage())) { $view->addSectionHeader( pht('Tag Content'), 'fa-tag'); $view->addTextContent($this->markupText($tag->getMessage())); } } if ($view->hasAnyProperties()) { return $view; } return null; } private function buildOpenRevisions() { $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $path = $drequest->getPath(); $path_map = id(new DiffusionPathIDQuery(array($path)))->loadPathIDs(); $path_id = idx($path_map, $path); if (!$path_id) { return null; } $recent = (PhabricatorTime::getNow() - phutil_units('30 days in seconds')); $revisions = id(new DifferentialRevisionQuery()) ->setViewer($viewer) ->withPath($repository->getID(), $path_id) ->withStatus(DifferentialRevisionQuery::STATUS_OPEN) ->withUpdatedEpochBetween($recent, null) ->setOrder(DifferentialRevisionQuery::ORDER_MODIFIED) ->setLimit(10) ->needReviewers(true) ->needFlags(true) ->needDrafts(true) ->execute(); if (!$revisions) { return null; } $header = id(new PHUIHeaderView()) ->setHeader(pht('Recently Open Revisions')) ->setHeaderIcon('fa-gear'); $view = id(new DifferentialRevisionListView()) ->setHeader($header) ->setRevisions($revisions) ->setUser($viewer); $phids = $view->getRequiredHandlePHIDs(); $handles = $this->loadViewerHandles($phids); $view->setHandles($handles); return $view; } private function loadBlame($path, $commit, $timeout) { $blame = $this->callConduitWithDiffusionRequest( 'diffusion.blame', array( 'commit' => $commit, 'paths' => array($path), 'timeout' => $timeout, )); $identifiers = idx($blame, $path, null); if ($identifiers) { $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withRepository($repository) ->withIdentifiers($identifiers) // TODO: We only fetch this to improve author display behavior, but // shouldn't really need to? ->needCommitData(true) ->execute(); $commits = mpull($commits, null, 'getCommitIdentifier'); } else { $commits = array(); } return array($identifiers, $commits); } private function renderCommitLinks(array $commits, $handles) { $links = array(); foreach ($commits as $identifier => $commit) { $tooltip = $this->renderCommitTooltip( $commit, $commit->renderAuthorShortName($handles)); $commit_link = javelin_tag( 'a', array( 'href' => $commit->getURI(), 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $tooltip, 'align' => 'E', 'size' => 600, ), ), $commit->getLocalName()); $links[$identifier] = $commit_link; } return $links; } private function renderRevisionLinks(array $revisions, $handles) { $links = array(); foreach ($revisions as $revision) { $revision_id = $revision->getID(); $tooltip = $this->renderRevisionTooltip($revision, $handles); $revision_link = javelin_tag( 'a', array( 'href' => '/'.$revision->getMonogram(), 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $tooltip, 'align' => 'E', 'size' => 600, ), ), $revision->getMonogram()); $links[$revision_id] = $revision_link; } return $links; } private function getGitLFSRef(PhabricatorRepository $repository, $data) { if (!$repository->canUseGitLFS()) { return null; } $lfs_pattern = '(^version https://git-lfs\\.github\\.com/spec/v1[\r\n])'; if (!preg_match($lfs_pattern, $data)) { return null; } $matches = null; if (!preg_match('(^oid sha256:(.*)$)m', $data, $matches)) { return null; } $hash = $matches[1]; $hash = trim($hash); return id(new PhabricatorRepositoryGitLFSRefQuery()) ->setViewer($this->getViewer()) ->withRepositoryPHIDs(array($repository->getPHID())) ->withObjectHashes(array($hash)) ->executeOne(); } private function buildGitLFSCorpus(PhabricatorRepositoryGitLFSRef $ref) { // TODO: We should probably test if we can load the file PHID here and // show the user an error if we can't, rather than making them click // through to hit an error. - $header = id(new PHUIHeaderView()) - ->setHeader(basename($this->getDiffusionRequest()->getPath())) - ->setHeaderIcon('fa-archive'); + $title = basename($this->getDiffusionRequest()->getPath()); + $icon = 'fa-archive'; + $drequest = $this->getDiffusionRequest(); + $actions = $this->getActions($drequest); + $header = $this->buildPanelHeaderView($title, $icon, $actions); $severity = PHUIInfoView::SEVERITY_NOTICE; $messages = array(); $messages[] = pht( 'This %s file is stored in Git Large File Storage.', phutil_format_bytes($ref->getByteSize())); try { $file = $this->loadGitLFSFile($ref); $data = $this->renderGitLFSButton(); $header->addActionLink($data); } catch (Exception $ex) { $severity = PHUIInfoView::SEVERITY_ERROR; $messages[] = pht('The data for this file could not be loaded.'); } $raw = $this->renderFileButton(null, pht('View Raw LFS Pointer')); $header->addActionLink($raw); $corpus = id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setCollapsed(true); if ($messages) { $corpus->setInfoView( id(new PHUIInfoView()) ->setSeverity($severity) ->setErrors($messages)); } return $corpus; } private function loadGitLFSFile(PhabricatorRepositoryGitLFSRef $ref) { $viewer = $this->getViewer(); $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($ref->getFilePHID())) ->executeOne(); if (!$file) { throw new Exception( pht( 'Failed to load file object for Git LFS ref "%s"!', $ref->getObjectHash())); } return $file; } private function buildBranchTable() { $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $branch = $drequest->getBranch(); $default_branch = $repository->getDefaultBranch(); if ($branch === $default_branch) { return null; } $pager = id(new PHUIPagerView()) ->setPageSize(10); try { $results = $this->callConduitWithDiffusionRequest( 'diffusion.historyquery', array( 'commit' => $branch, 'against' => $default_branch, 'path' => $drequest->getPath(), 'offset' => $pager->getOffset(), 'limit' => $pager->getPageSize() + 1, )); } catch (Exception $ex) { return null; } $history = DiffusionPathChange::newFromConduit($results['pathChanges']); $history = $pager->sliceResults($history); if (!$history) { return null; } $history_table = id(new DiffusionHistoryTableView()) ->setViewer($viewer) ->setDiffusionRequest($drequest) ->setHistory($history); $history_table->loadRevisions(); $history_table ->setParents($results['parents']) ->setFilterParents(true) ->setIsHead(true) ->setIsTail(!$pager->getHasMorePages()); $header = id(new PHUIHeaderView()) ->setHeader(pht('%s vs %s', $branch, $default_branch)); return id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($history_table); } } diff --git a/src/applications/diffusion/controller/DiffusionController.php b/src/applications/diffusion/controller/DiffusionController.php index abebbace90..8de17c03a2 100644 --- a/src/applications/diffusion/controller/DiffusionController.php +++ b/src/applications/diffusion/controller/DiffusionController.php @@ -1,412 +1,483 @@ diffusionRequest) { throw new PhutilInvalidStateException('loadDiffusionContext'); } return $this->diffusionRequest; } protected function hasDiffusionRequest() { return (bool)$this->diffusionRequest; } public function willBeginExecution() { $request = $this->getRequest(); // Check if this is a VCS request, e.g. from "git clone", "hg clone", or // "svn checkout". If it is, we jump off into repository serving code to // process the request. $serve_controller = new DiffusionServeController(); if ($serve_controller->isVCSRequest($request)) { return $this->delegateToController($serve_controller); } return parent::willBeginExecution(); } protected function loadDiffusionContextForEdit() { return $this->loadContext( array( 'edit' => true, )); } protected function loadDiffusionContext() { return $this->loadContext(array()); } private function loadContext(array $options) { $request = $this->getRequest(); $viewer = $this->getViewer(); + require_celerity_resource('diffusion-repository-css'); $identifier = $this->getRepositoryIdentifierFromRequest($request); $params = $options + array( 'repository' => $identifier, 'user' => $viewer, 'blob' => $this->getDiffusionBlobFromRequest($request), 'commit' => $request->getURIData('commit'), 'path' => $request->getURIData('path'), 'line' => $request->getURIData('line'), 'branch' => $request->getURIData('branch'), 'lint' => $request->getStr('lint'), ); $drequest = DiffusionRequest::newFromDictionary($params); if (!$drequest) { return new Aphront404Response(); } // If the client is making a request like "/diffusion/1/...", but the // repository has a different canonical path like "/diffusion/XYZ/...", // redirect them to the canonical path. $request_path = $request->getPath(); $repository = $drequest->getRepository(); $canonical_path = $repository->getCanonicalPath($request_path); if ($canonical_path !== null) { if ($canonical_path != $request_path) { return id(new AphrontRedirectResponse())->setURI($canonical_path); } } $this->diffusionRequest = $drequest; return null; } protected function getDiffusionBlobFromRequest(AphrontRequest $request) { return $request->getURIData('dblob'); } protected function getRepositoryIdentifierFromRequest( AphrontRequest $request) { $short_name = $request->getURIData('repositoryShortName'); if (strlen($short_name)) { // If the short name ends in ".git", ignore it. $short_name = preg_replace('/\\.git\z/', '', $short_name); return $short_name; } $identifier = $request->getURIData('repositoryCallsign'); if (strlen($identifier)) { return $identifier; } $id = $request->getURIData('repositoryID'); if (strlen($id)) { return (int)$id; } return null; } public function buildCrumbs(array $spec = array()) { $crumbs = $this->buildApplicationCrumbs(); $crumb_list = $this->buildCrumbList($spec); foreach ($crumb_list as $crumb) { $crumbs->addCrumb($crumb); } return $crumbs; } private function buildCrumbList(array $spec = array()) { $spec = $spec + array( - 'commit' => null, - 'tags' => null, - 'branches' => null, - 'view' => null, + 'commit' => null, + 'tags' => null, + 'branches' => null, + 'view' => null, ); $crumb_list = array(); // On the home page, we don't have a DiffusionRequest. if ($this->hasDiffusionRequest()) { $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); } else { $drequest = null; $repository = null; } if (!$repository) { return $crumb_list; } $repository_name = $repository->getName(); if (!$spec['commit'] && !$spec['tags'] && !$spec['branches']) { $branch_name = $drequest->getBranch(); if ($branch_name) { $repository_name .= ' ('.$branch_name.')'; } } $crumb = id(new PHUICrumbView()) ->setName($repository_name); if (!$spec['view'] && !$spec['commit'] && !$spec['tags'] && !$spec['branches']) { $crumb_list[] = $crumb; return $crumb_list; } $crumb->setHref( $drequest->generateURI( array( 'action' => 'branch', 'path' => '/', ))); $crumb_list[] = $crumb; $stable_commit = $drequest->getStableCommit(); $commit_name = $repository->formatCommitName($stable_commit, $local = true); $commit_uri = $repository->getCommitURI($stable_commit); if ($spec['tags']) { $crumb = new PHUICrumbView(); if ($spec['commit']) { $crumb->setName(pht('Tags for %s', $commit_name)); $crumb->setHref($commit_uri); } else { $crumb->setName(pht('Tags')); } $crumb_list[] = $crumb; return $crumb_list; } if ($spec['branches']) { $crumb = id(new PHUICrumbView()) ->setName(pht('Branches')); $crumb_list[] = $crumb; return $crumb_list; } if ($spec['commit']) { $crumb = id(new PHUICrumbView()) ->setName($commit_name); $crumb_list[] = $crumb; return $crumb_list; } $crumb = new PHUICrumbView(); $view = $spec['view']; switch ($view) { case 'history': $view_name = pht('History'); break; case 'graph': $view_name = pht('Graph'); break; case 'browse': $view_name = pht('Browse'); break; case 'lint': $view_name = pht('Lint'); break; case 'change': $view_name = pht('Change'); break; case 'compare': $view_name = pht('Compare'); break; } $crumb = id(new PHUICrumbView()) ->setName($view_name); $crumb_list[] = $crumb; return $crumb_list; } protected function callConduitWithDiffusionRequest( $method, array $params = array()) { $user = $this->getRequest()->getUser(); $drequest = $this->getDiffusionRequest(); return DiffusionQuery::callConduitWithDiffusionRequest( $user, $drequest, $method, $params); } protected function callConduitMethod($method, array $params = array()) { $user = $this->getViewer(); $drequest = $this->getDiffusionRequest(); return DiffusionQuery::callConduitWithDiffusionRequest( $user, $drequest, $method, $params, true); } protected function getRepositoryControllerURI( PhabricatorRepository $repository, $path) { return $repository->getPathURI($path); } protected function renderPathLinks(DiffusionRequest $drequest, $action) { $path = $drequest->getPath(); $path_parts = array_filter(explode('/', trim($path, '/'))); $divider = phutil_tag( 'span', array( 'class' => 'phui-header-divider', ), '/'); $links = array(); if ($path_parts) { $links[] = phutil_tag( 'a', array( 'href' => $drequest->generateURI( array( 'action' => $action, 'path' => '', )), ), $drequest->getRepository()->getDisplayName()); $links[] = $divider; $accum = ''; $last_key = last_key($path_parts); foreach ($path_parts as $key => $part) { $accum .= '/'.$part; if ($key === $last_key) { $links[] = $part; } else { $links[] = phutil_tag( 'a', array( 'href' => $drequest->generateURI( array( 'action' => $action, 'path' => $accum.'/', )), ), $part); $links[] = $divider; } } } else { $links[] = $drequest->getRepository()->getDisplayName(); $links[] = $divider; } return $links; } protected function renderStatusMessage($title, $body) { return id(new PHUIInfoView()) - ->setSeverity(PHUIInfoView::SEVERITY_WARNING) + ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->setTitle($title) ->setFlush(true) ->appendChild($body); } protected function renderCommitHashTag(DiffusionRequest $drequest) { $stable_commit = $drequest->getStableCommit(); $commit = phutil_tag( 'a', array( 'href' => $drequest->generateURI( array( 'action' => 'commit', 'commit' => $stable_commit, )), ), $drequest->getRepository()->formatCommitName($stable_commit, true)); $tag = id(new PHUITagView()) ->setName($commit) ->setColor(PHUITagView::COLOR_INDIGO) ->setBorder(PHUITagView::BORDER_NONE) ->setType(PHUITagView::TYPE_SHADE); return $tag; } protected function renderDirectoryReadme(DiffusionBrowseResultSet $browse) { $readme_path = $browse->getReadmePath(); if ($readme_path === null) { return null; } $drequest = $this->getDiffusionRequest(); $viewer = $this->getViewer(); $repository = $drequest->getRepository(); $repository_phid = $repository->getPHID(); $stable_commit = $drequest->getStableCommit(); $stable_commit_hash = PhabricatorHash::digestForIndex($stable_commit); $readme_path_hash = PhabricatorHash::digestForIndex($readme_path); $cache = PhabricatorCaches::getMutableStructureCache(); $cache_key = "diffusion". ".repository({$repository_phid})". ".commit({$stable_commit_hash})". ".readme({$readme_path_hash})"; $readme_cache = $cache->getKey($cache_key); if (!$readme_cache) { try { $result = $this->callConduitWithDiffusionRequest( 'diffusion.filecontentquery', array( 'path' => $readme_path, 'commit' => $drequest->getStableCommit(), )); } catch (Exception $ex) { return null; } $file_phid = $result['filePHID']; if (!$file_phid) { return null; } $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($file_phid)) ->executeOne(); if (!$file) { return null; } $corpus = $file->loadFileData(); $readme_cache = array( 'corpus' => $corpus, ); $cache->setKey($cache_key, $readme_cache); } $readme_corpus = $readme_cache['corpus']; if (!strlen($readme_corpus)) { return null; } return id(new DiffusionReadmeView()) ->setUser($this->getViewer()) ->setPath($readme_path) ->setContent($readme_corpus); } + protected function buildTabsView($key) { + $drequest = $this->getDiffusionRequest(); + $repository = $drequest->getRepository(); + + $view = new PHUIListView(); + + $view->addMenuItem( + id(new PHUIListItemView()) + ->setKey('home') + ->setName(pht('Home')) + ->setIcon('fa-home') + ->setHref($drequest->generateURI( + array( + 'action' => 'branch', + 'path' => '/', + ))) + ->setSelected($key == 'home')); + + if (!$repository->isSVN()) { + $view->addMenuItem( + id(new PHUIListItemView()) + ->setKey('branch') + ->setName(pht('Branches')) + ->setIcon('fa-code-fork') + ->setHref($drequest->generateURI( + array( + 'action' => 'branches', + ))) + ->setSelected($key == 'branch')); + } + + if (!$repository->isSVN()) { + $view->addMenuItem( + id(new PHUIListItemView()) + ->setKey('tags') + ->setName(pht('Tags')) + ->setIcon('fa-tags') + ->setHref($drequest->generateURI( + array( + 'action' => 'tags', + ))) + ->setSelected($key == 'tags')); + } + + $view->addMenuItem( + id(new PHUIListItemView()) + ->setKey('history') + ->setName(pht('History')) + ->setIcon('fa-history') + ->setHref($drequest->generateURI( + array( + 'action' => 'history', + ))) + ->setSelected($key == 'history')); + + $view->addMenuItem( + id(new PHUIListItemView()) + ->setKey('graph') + ->setName(pht('Graph')) + ->setIcon('fa-code-fork') + ->setHref($drequest->generateURI( + array( + 'action' => 'graph', + ))) + ->setSelected($key == 'graph')); + + return $view; + + } + } diff --git a/src/applications/diffusion/controller/DiffusionGraphController.php b/src/applications/diffusion/controller/DiffusionGraphController.php index 8428a8152d..8ec909139a 100644 --- a/src/applications/diffusion/controller/DiffusionGraphController.php +++ b/src/applications/diffusion/controller/DiffusionGraphController.php @@ -1,108 +1,103 @@ loadDiffusionContext(); if ($response) { return $response; } $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $pager = id(new PHUIPagerView()) ->readFromRequest($request); $params = array( 'commit' => $drequest->getCommit(), 'path' => $drequest->getPath(), 'offset' => $pager->getOffset(), 'limit' => $pager->getPageSize() + 1, ); $history_results = $this->callConduitWithDiffusionRequest( 'diffusion.historyquery', $params); $history = DiffusionPathChange::newFromConduit( $history_results['pathChanges']); $history = $pager->sliceResults($history); $graph = id(new DiffusionHistoryTableView()) ->setViewer($viewer) ->setDiffusionRequest($drequest) ->setHistory($history); $graph->loadRevisions(); $show_graph = !strlen($drequest->getPath()); if ($show_graph) { $graph->setParents($history_results['parents']); $graph->setIsHead(!$pager->getOffset()); $graph->setIsTail(!$pager->getHasMorePages()); } $header = $this->buildHeader($drequest); $crumbs = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'graph', )); $crumbs->setBorder(true); $title = array( pht('Graph'), $repository->getDisplayName(), ); $graph_view = id(new PHUIObjectBoxView()) ->setHeaderText(pht('History Graph')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($graph) ->setPager($pager); + $tabs = $this->buildTabsView('graph'); + $view = id(new PHUITwoColumnView()) ->setHeader($header) + ->setTabs($tabs) ->setFooter($graph_view); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild($view); } private function buildHeader(DiffusionRequest $drequest) { $viewer = $this->getViewer(); - $tag = $this->renderCommitHashTag($drequest); - $history_uri = $drequest->generateURI( - array( - 'action' => 'history', - )); - - $history_button = id(new PHUIButtonView()) - ->setTag('a') - ->setText(pht('History')) - ->setHref($history_uri) - ->setIcon('fa-history'); + $no_path = !strlen($drequest->getPath()); + if ($no_path) { + $header_text = pht('Graph'); + } else { + $header_text = $this->renderPathLinks($drequest, $mode = 'history'); + } $header = id(new PHUIHeaderView()) ->setUser($viewer) - ->setPolicyObject($drequest->getRepository()) - ->addTag($tag) - ->setHeader($this->renderPathLinks($drequest, $mode = 'history')) - ->setHeaderIcon('fa-code-fork') - ->addActionLink($history_button); + ->setHeader($header_text) + ->setHeaderIcon('fa-code-fork'); return $header; } } diff --git a/src/applications/diffusion/controller/DiffusionHistoryController.php b/src/applications/diffusion/controller/DiffusionHistoryController.php index c2f718f11e..fde35133cc 100644 --- a/src/applications/diffusion/controller/DiffusionHistoryController.php +++ b/src/applications/diffusion/controller/DiffusionHistoryController.php @@ -1,107 +1,98 @@ loadDiffusionContext(); if ($response) { return $response; } $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $pager = id(new PHUIPagerView()) ->readFromRequest($request); $params = array( 'commit' => $drequest->getCommit(), 'path' => $drequest->getPath(), 'offset' => $pager->getOffset(), 'limit' => $pager->getPageSize() + 1, ); $history_results = $this->callConduitWithDiffusionRequest( 'diffusion.historyquery', $params); $history = DiffusionPathChange::newFromConduit( $history_results['pathChanges']); $history = $pager->sliceResults($history); $history_list = id(new DiffusionHistoryListView()) ->setViewer($viewer) ->setDiffusionRequest($drequest) ->setHistory($history); $history_list->loadRevisions(); $header = $this->buildHeader($drequest); $crumbs = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'history', )); $crumbs->setBorder(true); $title = array( pht('History'), $repository->getDisplayName(), ); $pager = id(new PHUIBoxView()) ->addClass('mlb') ->appendChild($pager); + $tabs = $this->buildTabsView('history'); + $view = id(new PHUITwoColumnView()) ->setHeader($header) + ->setTabs($tabs) ->setFooter(array( $history_list, $pager, )); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild($view) ->addClass('diffusion-history-view'); } private function buildHeader(DiffusionRequest $drequest) { $viewer = $this->getViewer(); - $tag = $this->renderCommitHashTag($drequest); - $show_graph = !strlen($drequest->getPath()); + $no_path = !strlen($drequest->getPath()); + if ($no_path) { + $header_text = pht('History'); + } else { + $header_text = $this->renderPathLinks($drequest, $mode = 'history'); + } $header = id(new PHUIHeaderView()) ->setUser($viewer) - ->setPolicyObject($drequest->getRepository()) - ->addTag($tag) - ->setHeader($this->renderPathLinks($drequest, $mode = 'history')) + ->setHeader($header_text) ->setHeaderIcon('fa-clock-o'); - if ($show_graph) { - $graph_uri = $drequest->generateURI( - array( - 'action' => 'graph', - )); - - $graph_button = id(new PHUIButtonView()) - ->setTag('a') - ->setText(pht('Graph')) - ->setHref($graph_uri) - ->setIcon('fa-code-fork'); - $header->addActionLink($graph_button); - } - return $header; } } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryController.php b/src/applications/diffusion/controller/DiffusionRepositoryController.php index 362f78bd3f..d8451db2db 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryController.php @@ -1,755 +1,557 @@ loadDiffusionContext(); if ($response) { return $response; } $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $crumbs = $this->buildCrumbs(); $crumbs->setBorder(true); $header = $this->buildHeaderView($repository); $curtain = $this->buildCurtain($repository); $property_table = $this->buildPropertiesTable($repository); $description = $this->buildDescriptionView($repository); $locate_file = $this->buildLocateFile(); // Before we do any work, make sure we're looking at a some content: we're // on a valid branch, and the repository is not empty. $page_has_content = false; $empty_title = null; $empty_message = null; // If this VCS supports branches, check that the selected branch actually // exists. if ($drequest->supportsBranches()) { // NOTE: Mercurial may have multiple branch heads with the same name. $ref_cursors = id(new PhabricatorRepositoryRefCursorQuery()) ->setViewer($viewer) ->withRepositoryPHIDs(array($repository->getPHID())) ->withRefTypes(array(PhabricatorRepositoryRefCursor::TYPE_BRANCH)) ->withRefNames(array($drequest->getBranch())) ->execute(); if ($ref_cursors) { // This is a valid branch, so we necessarily have some content. $page_has_content = true; } else { $empty_title = pht('No Such Branch'); $empty_message = pht( 'There is no branch named "%s" in this repository.', $drequest->getBranch()); } } // If we didn't find any branches, check if there are any commits at all. // This can tailor the message for empty repositories. if (!$page_has_content) { $any_commit = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withRepository($repository) ->setLimit(1) ->execute(); if ($any_commit) { if (!$drequest->supportsBranches()) { $page_has_content = true; } } else { $empty_title = pht('Empty Repository'); $empty_message = pht('This repository does not have any commits yet.'); } } if ($page_has_content) { $content = $this->buildNormalContent($drequest); } else { $content = id(new PHUIInfoView()) ->setTitle($empty_title) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setErrors(array($empty_message)); } + $tabs = $this->buildTabsView('home'); + $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) + ->setTabs($tabs) ->setMainColumn(array( $property_table, $description, $locate_file, )) ->setFooter($content); return $this->newPage() ->setTitle( array( $repository->getName(), $repository->getDisplayName(), )) ->setCrumbs($crumbs) ->appendChild(array( $view, )); } private function buildNormalContent(DiffusionRequest $drequest) { $request = $this->getRequest(); $repository = $drequest->getRepository(); $commit = $drequest->getCommit(); $path = $drequest->getPath(); $this->historyFuture = $this->callConduitMethod( 'diffusion.historyquery', array( 'commit' => $commit, 'path' => $path, 'offset' => 0, 'limit' => 15, )); $browse_pager = id(new PHUIPagerView()) ->readFromRequest($request); $this->browseFuture = $this->callConduitMethod( 'diffusion.browsequery', array( 'commit' => $commit, 'path' => $path, 'limit' => $browse_pager->getPageSize() + 1, )); - if ($this->needTagFuture()) { - $tag_limit = $this->getTagLimit(); - $this->tagFuture = $this->callConduitMethod( - 'diffusion.tagsquery', - array( - // On the home page, we want to find tags on any branch. - 'commit' => null, - 'limit' => $tag_limit + 1, - )); - } - - if ($this->needBranchFuture()) { - $branch_limit = $this->getBranchLimit(); - $this->branchFuture = $this->callConduitMethod( - 'diffusion.branchquery', - array( - 'closed' => false, - 'limit' => $branch_limit + 1, - )); - } - $futures = array( $this->historyFuture, $this->browseFuture, - $this->tagFuture, - $this->branchFuture, ); $futures = array_filter($futures); $futures = new FutureIterator($futures); foreach ($futures as $future) { // Just resolve all the futures before continuing. } $phids = array(); $content = array(); try { $history_results = $this->historyFuture->resolve(); $history = DiffusionPathChange::newFromConduit( $history_results['pathChanges']); foreach ($history as $item) { $data = $item->getCommitData(); if ($data) { if ($data->getCommitDetail('authorPHID')) { $phids[$data->getCommitDetail('authorPHID')] = true; } if ($data->getCommitDetail('committerPHID')) { $phids[$data->getCommitDetail('committerPHID')] = true; } } } $history_exception = null; } catch (Exception $ex) { $history_results = null; $history = null; $history_exception = $ex; } try { $browse_results = $this->browseFuture->resolve(); $browse_results = DiffusionBrowseResultSet::newFromConduit( $browse_results); $browse_paths = $browse_results->getPaths(); $browse_paths = $browse_pager->sliceResults($browse_paths); foreach ($browse_paths as $item) { $data = $item->getLastCommitData(); if ($data) { if ($data->getCommitDetail('authorPHID')) { $phids[$data->getCommitDetail('authorPHID')] = true; } if ($data->getCommitDetail('committerPHID')) { $phids[$data->getCommitDetail('committerPHID')] = true; } } } $browse_exception = null; } catch (Exception $ex) { $browse_results = null; $browse_paths = null; $browse_exception = $ex; } $phids = array_keys($phids); $handles = $this->loadViewerHandles($phids); if ($browse_results) { $readme = $this->renderDirectoryReadme($browse_results); } else { $readme = null; } $content[] = $this->buildBrowseTable( $browse_results, $browse_paths, $browse_exception, $handles, $browse_pager); $content[] = $this->buildHistoryTable( $history_results, $history, $history_exception); - try { - $content[] = $this->buildTagListTable($drequest); - } catch (Exception $ex) { - if (!$repository->isImporting()) { - $content[] = $this->renderStatusMessage( - pht('Unable to Load Tags'), - $ex->getMessage()); - } - } - - try { - $content[] = $this->buildBranchListTable($drequest); - } catch (Exception $ex) { - if (!$repository->isImporting()) { - $content[] = $this->renderStatusMessage( - pht('Unable to Load Branches'), - $ex->getMessage()); - } - } - if ($readme) { $content[] = $readme; } return $content; } private function buildHeaderView(PhabricatorRepository $repository) { $viewer = $this->getViewer(); $header = id(new PHUIHeaderView()) ->setHeader($repository->getName()) ->setUser($viewer) ->setPolicyObject($repository) ->setProfileHeader(true) ->setImage($repository->getProfileImageURI()) ->setImageEditURL('/diffusion/picture/'.$repository->getID().'/'); if (!$repository->isTracked()) { $header->setStatus('fa-ban', 'dark', pht('Inactive')); } else if ($repository->isImporting()) { $ratio = $repository->loadImportProgress(); $percentage = sprintf('%.2f%%', 100 * $ratio); $header->setStatus( 'fa-clock-o', 'indigo', pht('Importing (%s)...', $percentage)); } else { $header->setStatus('fa-check', 'bluegrey', pht('Active')); } return $header; } private function buildCurtain(PhabricatorRepository $repository) { $viewer = $this->getViewer(); $edit_uri = $repository->getPathURI('manage/'); $curtain = $this->newCurtainView($repository); $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Manage Repository')) ->setIcon('fa-cogs') ->setHref($edit_uri)); if ($repository->isHosted()) { $push_uri = $this->getApplicationURI( 'pushlog/?repositories='.$repository->getMonogram()); $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('View Push Logs')) ->setIcon('fa-list-alt') ->setHref($push_uri)); } return $curtain; } private function buildDescriptionView(PhabricatorRepository $repository) { $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setUser($viewer); $description = $repository->getDetail('description'); if (strlen($description)) { $description = new PHUIRemarkupView($viewer, $description); $view->addTextContent($description); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Description')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($view); } return null; } private function buildPropertiesTable(PhabricatorRepository $repository) { $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setUser($viewer); $display_never = PhabricatorRepositoryURI::DISPLAY_NEVER; $uris = $repository->getURIs(); foreach ($uris as $uri) { if ($uri->getIsDisabled()) { continue; } if ($uri->getEffectiveDisplayType() == $display_never) { continue; } if ($repository->isSVN()) { $label = phutil_tag_div('diffusion-clone-label', pht('Checkout')); } else { $label = phutil_tag_div('diffusion-clone-label', pht('Clone')); } $view->addProperty( $label, $this->renderCloneURI($repository, $uri)); } + if (!$view->hasAnyProperties()) { + $view = id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) + ->appendChild(pht('Repository has no URIs set.')); + } + $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Details')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($view); $info = null; $drequest = $this->getDiffusionRequest(); // Try to load alternatives. This may fail for repositories which have not // cloned yet. If it does, just ignore it and continue. try { $alternatives = $drequest->getRefAlternatives(); } catch (ConduitClientException $ex) { $alternatives = array(); } if ($alternatives) { $message = array( pht( 'The ref "%s" is ambiguous in this repository.', $drequest->getBranch()), ' ', phutil_tag( 'a', array( 'href' => $drequest->generateURI( array( 'action' => 'refs', )), ), pht('View Alternatives')), ); $messages = array($message); $info = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setErrors(array($message)); $box->setInfoView($info); } return $box; } - private function buildBranchListTable(DiffusionRequest $drequest) { - $viewer = $this->getViewer(); - - if (!$this->needBranchFuture()) { - return null; - } - - $branches = $this->branchFuture->resolve(); - if (!$branches) { - return null; - } - - $limit = $this->getBranchLimit(); - $more_branches = (count($branches) > $limit); - $branches = array_slice($branches, 0, $limit); - - $branches = DiffusionRepositoryRef::loadAllFromDictionaries($branches); - - $commits = id(new DiffusionCommitQuery()) - ->setViewer($viewer) - ->withIdentifiers(mpull($branches, 'getCommitIdentifier')) - ->withRepository($drequest->getRepository()) - ->execute(); - - $table = id(new DiffusionBranchTableView()) - ->setUser($viewer) - ->setDiffusionRequest($drequest) - ->setBranches($branches) - ->setCommits($commits); - - $panel = id(new PHUIObjectBoxView()) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); - $header = new PHUIHeaderView(); - $header->setHeader(pht('Branches')); - - if ($more_branches) { - $header->setSubheader(pht('Showing %d branches.', $limit)); - } - - $button = id(new PHUIButtonView()) - ->setText(pht('Show All')) - ->setTag('a') - ->setIcon('fa-code-fork') - ->setHref($drequest->generateURI( - array( - 'action' => 'branches', - ))); - - $header->addActionLink($button); - $panel->setHeader($header); - $panel->setTable($table); - - return $panel; - } - - private function buildTagListTable(DiffusionRequest $drequest) { - $viewer = $this->getViewer(); - $repository = $drequest->getRepository(); - - if (!$this->needTagFuture()) { - return null; - } - - $tags = $this->tagFuture->resolve(); - $tags = DiffusionRepositoryTag::newFromConduit($tags); - if (!$tags) { - return null; - } - - $tag_limit = $this->getTagLimit(); - $more_tags = (count($tags) > $tag_limit); - $tags = array_slice($tags, 0, $tag_limit); - - $commits = id(new DiffusionCommitQuery()) - ->setViewer($viewer) - ->withIdentifiers(mpull($tags, 'getCommitIdentifier')) - ->withRepository($repository) - ->needCommitData(true) - ->execute(); - - $view = id(new DiffusionTagTableView()) - ->setUser($viewer) - ->setDiffusionRequest($drequest) - ->setTags($tags) - ->setCommits($commits); - - $phids = $view->getRequiredHandlePHIDs(); - $handles = $this->loadViewerHandles($phids); - $view->setHandles($handles); - - $panel = new PHUIObjectBoxView(); - $header = new PHUIHeaderView(); - $header->setHeader(pht('Tags')); - - if ($more_tags) { - $header->setSubheader( - pht('Showing the %d most recent tags.', $tag_limit)); - } - - $button = id(new PHUIButtonView()) - ->setText(pht('Show All Tags')) - ->setTag('a') - ->setIcon('fa-tag') - ->setHref($drequest->generateURI( - array( - 'action' => 'tags', - ))); - - $header->addActionLink($button); - - $panel->setHeader($header); - $panel->setTable($view); - $panel->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); - - return $panel; - } - private function buildHistoryTable( $history_results, $history, $history_exception) { $request = $this->getRequest(); $viewer = $request->getUser(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); if ($history_exception) { if ($repository->isImporting()) { return $this->renderStatusMessage( pht('Still Importing...'), pht( 'This repository is still importing. History is not yet '. 'available.')); } else { return $this->renderStatusMessage( pht('Unable to Retrieve History'), $history_exception->getMessage()); } } $history_table = id(new DiffusionHistoryTableView()) ->setUser($viewer) ->setDiffusionRequest($drequest) ->setHistory($history); // TODO: Super sketchy. $history_table->loadRevisions(); if ($history_results) { $history_table->setParents($history_results['parents']); } $history_table->setIsHead(true); - $history = id(new PHUIButtonView()) - ->setText(pht('History')) - ->setHref($drequest->generateURI( - array( - 'action' => 'history', - ))) - ->setTag('a') - ->setIcon('fa-history'); - - $graph = id(new PHUIButtonView()) - ->setText(pht('Graph')) - ->setHref($drequest->generateURI( - array( - 'action' => 'graph', - ))) - ->setTag('a') - ->setIcon('fa-code-fork'); - $panel = id(new PHUIObjectBoxView()) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); $header = id(new PHUIHeaderView()) - ->setHeader(pht('Recent Commits')) - ->addActionLink($graph) - ->addActionLink($history); + ->setHeader(pht('Recent Commits')); $panel->setHeader($header); $panel->setTable($history_table); return $panel; } private function buildLocateFile() { $request = $this->getRequest(); $viewer = $request->getUser(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $locate_panel = null; if ($repository->canUsePathTree()) { Javelin::initBehavior( 'diffusion-locate-file', array( 'controlID' => 'locate-control', 'inputID' => 'locate-input', 'browseBaseURI' => (string)$drequest->generateURI( array( 'action' => 'browse', )), 'uri' => (string)$drequest->generateURI( array( 'action' => 'pathtree', )), )); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTypeaheadControl()) ->setHardpointID('locate-control') ->setID('locate-input') ->setLabel(pht('Locate File'))); $form_box = id(new PHUIBoxView()) ->appendChild($form->buildLayoutView()); $locate_panel = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Locate File')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($form_box); } return $locate_panel; } private function buildBrowseTable( $browse_results, $browse_paths, $browse_exception, array $handles, PHUIPagerView $pager) { require_celerity_resource('diffusion-icons-css'); $request = $this->getRequest(); $viewer = $request->getUser(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); if ($browse_exception) { if ($repository->isImporting()) { // The history table renders a useful message. return null; } else { return $this->renderStatusMessage( pht('Unable to Retrieve Paths'), $browse_exception->getMessage()); } } $browse_table = id(new DiffusionBrowseTableView()) ->setUser($viewer) ->setDiffusionRequest($drequest) ->setHandles($handles); if ($browse_paths) { $browse_table->setPaths($browse_paths); } else { $browse_table->setPaths(array()); } $browse_uri = $drequest->generateURI(array('action' => 'browse')); $browse_panel = id(new PHUIObjectBoxView()) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); $header = id(new PHUIHeaderView()) ->setHeader($repository->getName()); $button = id(new PHUIButtonView()) ->setText(pht('Browse')) ->setTag('a') ->setIcon('fa-code') ->setHref($browse_uri); $header->addActionLink($button); $browse_panel->setHeader($header); $browse_panel->setTable($browse_table); $pager->setURI($browse_uri, 'offset'); if ($pager->willShowPagingControls()) { $browse_panel->setPager($pager); } return $browse_panel; } private function renderCloneURI( PhabricatorRepository $repository, PhabricatorRepositoryURI $uri) { if ($repository->isSVN()) { $display = csprintf( 'svn checkout %R %R', (string)$uri->getDisplayURI(), $repository->getCloneName()); } else { $display = csprintf('%R', (string)$uri->getDisplayURI()); } $display = (string)$display; $viewer = $this->getViewer(); return id(new DiffusionCloneURIView()) ->setViewer($viewer) ->setRepository($repository) ->setRepositoryURI($uri) ->setDisplayURI($display); } - private function needTagFuture() { - $drequest = $this->getDiffusionRequest(); - $repository = $drequest->getRepository(); - - switch ($repository->getVersionControlSystem()) { - case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: - // No tags in SVN. - return false; - } - - return true; - } - private function getTagLimit() { return 15; } - private function needBranchFuture() { - $drequest = $this->getDiffusionRequest(); - - if ($drequest->getBranch() === null) { - return false; - } - - return true; - } - - private function getBranchLimit() { - return 15; - } - } diff --git a/src/applications/diffusion/controller/DiffusionTagListController.php b/src/applications/diffusion/controller/DiffusionTagListController.php index 5e765ddcb6..f7cd032e1a 100644 --- a/src/applications/diffusion/controller/DiffusionTagListController.php +++ b/src/applications/diffusion/controller/DiffusionTagListController.php @@ -1,106 +1,109 @@ loadDiffusionContext(); if ($response) { return $response; } $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $pager = id(new PHUIPagerView()) ->readFromRequest($request); $params = array( 'limit' => $pager->getPageSize() + 1, 'offset' => $pager->getOffset(), ); if (strlen($drequest->getSymbolicCommit())) { $is_commit = true; $params['commit'] = $drequest->getSymbolicCommit(); } else { $is_commit = false; } switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $tags = array(); break; default: $conduit_result = $this->callConduitWithDiffusionRequest( 'diffusion.tagsquery', $params); $tags = DiffusionRepositoryTag::newFromConduit($conduit_result); break; } $tags = $pager->sliceResults($tags); $content = null; $header = id(new PHUIHeaderView()) ->setHeader(pht('Tags')) ->setHeaderIcon('fa-tags'); if (!$tags) { $content = $this->renderStatusMessage( pht('No Tags'), $is_commit ? pht('This commit has no tags.') : pht('This repository has no tags.')); } else { $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withRepository($repository) ->withIdentifiers(mpull($tags, 'getCommitIdentifier')) ->needCommitData(true) ->execute(); $tag_list = id(new DiffusionTagListView()) ->setTags($tags) ->setUser($viewer) ->setCommits($commits) ->setDiffusionRequest($drequest); $phids = $tag_list->getRequiredHandlePHIDs(); $handles = $this->loadViewerHandles($phids); $tag_list->setHandles($handles); $content = id(new PHUIObjectBoxView()) ->setHeaderText($repository->getDisplayName()) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($tag_list) ->setPager($pager); } $crumbs = $this->buildCrumbs( array( 'tags' => true, 'commit' => $drequest->getSymbolicCommit(), )); $crumbs->setBorder(true); + $tabs = $this->buildTabsView('tags'); + $view = id(new PHUITwoColumnView()) ->setHeader($header) + ->setTabs($tabs) ->setFooter($content); return $this->newPage() ->setTitle( array( pht('Tags'), $repository->getDisplayName(), )) ->setCrumbs($crumbs) ->appendChild($view) ->addClass('diffusion-history-view'); } } diff --git a/src/applications/diffusion/typeahead/DiffusionRepositoryDatasource.php b/src/applications/diffusion/typeahead/DiffusionRepositoryDatasource.php index 0b3f191563..e0662b1e1a 100644 --- a/src/applications/diffusion/typeahead/DiffusionRepositoryDatasource.php +++ b/src/applications/diffusion/typeahead/DiffusionRepositoryDatasource.php @@ -1,83 +1,83 @@ getViewer(); $raw_query = $this->getRawQuery(); $query = id(new PhabricatorRepositoryQuery()) ->setOrder('name') ->withDatasourceQuery($raw_query); $repos = $this->executeQuery($query); $type_icon = id(new PhabricatorRepositoryRepositoryPHIDType()) ->getTypeIcon(); $image_sprite = "phabricator-search-icon phui-font-fa phui-icon-view {$type_icon}"; $results = array(); foreach ($repos as $repository) { $monogram = $repository->getMonogram(); $name = $repository->getName(); $display_name = "{$monogram} {$name}"; $parts = array(); $parts[] = $name; $slug = $repository->getRepositorySlug(); if (strlen($slug)) { $parts[] = $slug; } $callsign = $repository->getCallsign(); if ($callsign) { $parts[] = $callsign; } foreach ($repository->getAllMonograms() as $monogram) { $parts[] = $monogram; } - $name = implode(' ', $parts); + $name = implode("\n", $parts); $vcs = $repository->getVersionControlSystem(); $vcs_type = PhabricatorRepositoryType::getNameForRepositoryType($vcs); $result = id(new PhabricatorTypeaheadResult()) ->setName($name) ->setDisplayName($display_name) ->setURI($repository->getURI()) ->setPHID($repository->getPHID()) ->setPriorityString($repository->getMonogram()) ->setPriorityType('repo') ->setImageSprite($image_sprite) ->setDisplayType(pht('Repository')) ->addAttribute($vcs_type); if (!$repository->isTracked()) { $result->setClosed(pht('Inactive')); } $results[] = $result; } return $results; } } diff --git a/src/applications/diffusion/view/DiffusionReadmeView.php b/src/applications/diffusion/view/DiffusionReadmeView.php index b475fa88ac..88e2e84115 100644 --- a/src/applications/diffusion/view/DiffusionReadmeView.php +++ b/src/applications/diffusion/view/DiffusionReadmeView.php @@ -1,108 +1,112 @@ path = $path; return $this; } public function getPath() { return $this->path; } public function setContent($content) { $this->content = $content; return $this; } public function getContent() { return $this->content; } /** * Get the markup language a README should be interpreted as. * * @param string Local README path, like "README.txt". * @return string Best markup interpreter (like "remarkup") for this file. */ private function getReadmeLanguage($path) { $path = phutil_utf8_strtolower($path); if ($path == 'readme') { return 'remarkup'; } $ext = last(explode('.', $path)); switch ($ext) { case 'remarkup': case 'md': return 'remarkup'; case 'rainbow': return 'rainbow'; case 'txt': default: return 'text'; } } public function render() { $readme_path = $this->getPath(); $readme_name = basename($readme_path); $interpreter = $this->getReadmeLanguage($readme_name); require_celerity_resource('diffusion-readme-css'); $content = $this->getContent(); $class = null; switch ($interpreter) { case 'remarkup': // TODO: This is sketchy, but make sure we hit the markup cache. $markup_object = id(new PhabricatorMarkupOneOff()) ->setEngineRuleset('diffusion-readme') ->setContent($content); $markup_field = 'default'; $content = id(new PhabricatorMarkupEngine()) ->setViewer($this->getUser()) ->addObject($markup_object, $markup_field) ->process() ->getOutput($markup_object, $markup_field); $engine = $markup_object->newMarkupEngine($markup_field); $readme_content = $content; $class = null; break; case 'rainbow': $content = id(new PhutilRainbowSyntaxHighlighter()) ->getHighlightFuture($content) ->resolve(); $readme_content = phutil_escape_html_newlines($content); require_celerity_resource('syntax-highlighting-css'); $class = 'remarkup-code ml'; break; default: case 'text': $readme_content = phutil_escape_html_newlines($content); $class = 'ml'; break; } $readme_content = phutil_tag_div($class, $readme_content); $document = id(new PHUIDocumentViewPro()) ->setFluid(true) ->appendChild($readme_content); + $header = id(new PHUIHeaderView()) + ->setHeader($readme_name) + ->addClass('diffusion-panel-header-view'); + return id(new PHUIObjectBoxView()) - ->setHeaderText($readme_name) + ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($document) ->addClass('diffusion-readme-view'); } } diff --git a/src/applications/phame/controller/post/PhamePostArchiveController.php b/src/applications/phame/controller/post/PhamePostArchiveController.php index b8647121ef..093e7019bf 100644 --- a/src/applications/phame/controller/post/PhamePostArchiveController.php +++ b/src/applications/phame/controller/post/PhamePostArchiveController.php @@ -1,56 +1,57 @@ getViewer(); $id = $request->getURIData('id'); $post = id(new PhamePostQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$post) { return new Aphront404Response(); } $cancel_uri = $post->getViewURI(); if ($request->isFormPost()) { $xactions = array(); $new_value = PhameConstants::VISIBILITY_ARCHIVED; $xactions[] = id(new PhamePostTransaction()) ->setTransactionType(PhamePostVisibilityTransaction::TRANSACTIONTYPE) ->setNewValue($new_value); id(new PhamePostEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($post, $xactions); return id(new AphrontRedirectResponse()) ->setURI($cancel_uri); } $title = pht('Archive Post'); $body = pht( - 'This post will revert to archived status and no longer be visible '. - 'to other users or members of this blog.'); + 'If you archive this post, it will only be visible to users who can '. + 'edit %s.', + $viewer->renderHandle($post->getBlogPHID())); $button = pht('Archive Post'); return $this->newDialog() ->setTitle($title) ->appendParagraph($body) ->addSubmitButton($button) ->addCancelButton($cancel_uri); } } diff --git a/src/applications/phame/controller/post/PhamePostViewController.php b/src/applications/phame/controller/post/PhamePostViewController.php index a73876a197..63adedb7ae 100644 --- a/src/applications/phame/controller/post/PhamePostViewController.php +++ b/src/applications/phame/controller/post/PhamePostViewController.php @@ -1,349 +1,353 @@ setupLiveEnvironment(); if ($response) { return $response; } $viewer = $request->getViewer(); $moved = $request->getStr('moved'); $post = $this->getPost(); $blog = $this->getBlog(); $is_live = $this->getIsLive(); $is_external = $this->getIsExternal(); $header = id(new PHUIHeaderView()) ->addClass('phame-header-bar') ->setUser($viewer); $hero = $this->buildPhamePostHeader($post); if (!$is_external) { $actions = $this->renderActions($post); $header->setPolicyObject($post); $header->setActionList($actions); } $document = id(new PHUIDocumentViewPro()) ->setHeader($header); if ($moved) { $document->appendChild( id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->appendChild(pht('Post moved successfully.'))); } if ($post->isDraft()) { $document->appendChild( id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->setTitle(pht('Draft Post')) ->appendChild( - pht('Only you can see this draft until you publish it. '. - 'Use "Publish" to publish this post.'))); + pht( + 'This is a draft, and is only visible to you and other users '. + 'who can edit %s. Use "Publish" to publish this post.', + $viewer->renderHandle($post->getBlogPHID())))); } if ($post->isArchived()) { $document->appendChild( id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_ERROR) ->setTitle(pht('Archived Post')) ->appendChild( - pht('Only you can see this archived post until you publish it. '. - 'Use "Publish" to publish this post.'))); + pht( + 'This post has been archived, and is only visible to you and '. + 'other users who can edit %s.', + $viewer->renderHandle($post->getBlogPHID())))); } if (!$post->getBlog()) { $document->appendChild( id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setTitle(pht('Not On A Blog')) ->appendChild( pht('This post is not associated with a blog (the blog may have '. 'been deleted). Use "Move Post" to move it to a new blog.'))); } $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer) ->addObject($post, PhamePost::MARKUP_FIELD_BODY) ->process(); $document->appendChild( phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $engine->getOutput($post, PhamePost::MARKUP_FIELD_BODY))); $blogger = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withPHIDs(array($post->getBloggerPHID())) ->needProfileImage(true) ->executeOne(); $blogger_profile = $blogger->loadUserProfile(); $author_uri = '/p/'.$blogger->getUsername().'/'; $author_uri = PhabricatorEnv::getURI($author_uri); $author = phutil_tag( 'a', array( 'href' => $author_uri, ), $blogger->getUsername()); $date = phabricator_datetime($post->getDatePublished(), $viewer); if ($post->isDraft()) { $subtitle = pht('Unpublished draft by %s.', $author); } else if ($post->isArchived()) { $subtitle = pht('Archived post by %s.', $author); } else { $subtitle = pht('Written by %s on %s.', $author, $date); } $user_icon = $blogger_profile->getIcon(); $user_icon = PhabricatorPeopleIconSet::getIconIcon($user_icon); $user_icon = id(new PHUIIconView())->setIcon($user_icon); $about = id(new PhameDescriptionView()) ->setTitle($subtitle) ->setDescription( array( $user_icon, ' ', $blogger_profile->getDisplayTitle(), )) ->setImage($blogger->getProfileImageURI()) ->setImageHref($author_uri); $monogram = $post->getMonogram(); $timeline = $this->buildTransactionTimeline( $post, id(new PhamePostTransactionQuery()) ->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT))); $timeline->setQuoteRef($monogram); if ($is_external) { $add_comment = null; } else { $add_comment = $this->buildCommentForm($post, $timeline); $add_comment = phutil_tag_div('mlb mlt phame-comment-view', $add_comment); } $timeline = phutil_tag_div('phui-document-view-pro-box', $timeline); list($prev, $next) = $this->loadAdjacentPosts($post); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($post); $is_live = $this->getIsLive(); $is_external = $this->getIsExternal(); $next_view = new PhameNextPostView(); if ($next) { $next_view->setNext($next->getTitle(), $next->getBestURI($is_live, $is_external)); } if ($prev) { $next_view->setPrevious($prev->getTitle(), $prev->getBestURI($is_live, $is_external)); } $document->setFoot($next_view); $crumbs = $this->buildApplicationCrumbs(); $properties = phutil_tag_div('phui-document-view-pro-box', $properties); $page = $this->newPage() ->setTitle($post->getTitle()) ->setPageObjectPHIDs(array($post->getPHID())) ->setCrumbs($crumbs) ->appendChild( array( $hero, $document, $about, $properties, $timeline, $add_comment, )); if ($is_live) { $page ->setShowChrome(false) ->setShowFooter(false); } return $page; } private function renderActions(PhamePost $post) { $viewer = $this->getViewer(); $actions = id(new PhabricatorActionListView()) ->setObject($post) ->setUser($viewer); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $post, PhabricatorPolicyCapability::CAN_EDIT); $id = $post->getID(); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI('post/edit/'.$id.'/')) ->setName(pht('Edit Post')) ->setDisabled(!$can_edit)); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-camera-retro') ->setHref($this->getApplicationURI('post/header/'.$id.'/')) ->setName(pht('Edit Header Image')) ->setDisabled(!$can_edit)); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-arrows') ->setHref($this->getApplicationURI('post/move/'.$id.'/')) ->setName(pht('Move Post')) ->setDisabled(!$can_edit) ->setWorkflow(true)); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-history') ->setHref($this->getApplicationURI('post/history/'.$id.'/')) ->setName(pht('View History'))); if ($post->isDraft()) { $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-eye') ->setHref($this->getApplicationURI('post/publish/'.$id.'/')) ->setName(pht('Publish')) ->setDisabled(!$can_edit) ->setWorkflow(true)); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-ban') ->setHref($this->getApplicationURI('post/archive/'.$id.'/')) ->setName(pht('Archive')) ->setDisabled(!$can_edit) ->setWorkflow(true)); } else if ($post->isArchived()) { $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-eye') ->setHref($this->getApplicationURI('post/publish/'.$id.'/')) ->setName(pht('Publish')) ->setDisabled(!$can_edit) ->setWorkflow(true)); } else { $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-eye-slash') ->setHref($this->getApplicationURI('post/unpublish/'.$id.'/')) ->setName(pht('Unpublish')) ->setDisabled(!$can_edit) ->setWorkflow(true)); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-ban') ->setHref($this->getApplicationURI('post/archive/'.$id.'/')) ->setName(pht('Archive')) ->setDisabled(!$can_edit) ->setWorkflow(true)); } if ($post->isDraft()) { $live_name = pht('Preview'); } else { $live_name = pht('View Live'); } if (!$post->isArchived()) { $actions->addAction( id(new PhabricatorActionView()) ->setUser($viewer) ->setIcon('fa-globe') ->setHref($post->getLiveURI()) ->setName($live_name)); } return $actions; } private function buildCommentForm(PhamePost $post, $timeline) { $viewer = $this->getViewer(); $box = id(new PhamePostEditEngine()) ->setViewer($viewer) ->buildEditEngineCommentView($post) ->setTransactionTimeline($timeline); return phutil_tag_div('phui-document-view-pro-box', $box); } private function loadAdjacentPosts(PhamePost $post) { $viewer = $this->getViewer(); $query = id(new PhamePostQuery()) ->setViewer($viewer) ->withVisibility(array(PhameConstants::VISIBILITY_PUBLISHED)) ->withBlogPHIDs(array($post->getBlog()->getPHID())) ->setLimit(1); $prev = id(clone $query) ->setAfterID($post->getID()) ->execute(); $next = id(clone $query) ->setBeforeID($post->getID()) ->execute(); return array(head($prev), head($next)); } private function buildPhamePostHeader( PhamePost $post) { $image = null; if ($post->getHeaderImagePHID()) { $image = phutil_tag( 'div', array( 'class' => 'phame-header-hero', ), phutil_tag( 'img', array( 'src' => $post->getHeaderImageURI(), 'class' => 'phame-header-image', ))); } $title = phutil_tag_div('phame-header-title', $post->getTitle()); $subtitle = null; if ($post->getSubtitle()) { $subtitle = phutil_tag_div('phame-header-subtitle', $post->getSubtitle()); } return phutil_tag_div( 'phame-mega-header', array($image, $title, $subtitle)); } } diff --git a/src/applications/phame/storage/PhamePost.php b/src/applications/phame/storage/PhamePost.php index f87a37e7a4..a9525e0be7 100644 --- a/src/applications/phame/storage/PhamePost.php +++ b/src/applications/phame/storage/PhamePost.php @@ -1,389 +1,390 @@ setBloggerPHID($blogger->getPHID()) ->setBlogPHID($blog->getPHID()) ->attachBlog($blog) ->setDatePublished(PhabricatorTime::getNow()) ->setVisibility(PhameConstants::VISIBILITY_PUBLISHED); return $post; } public function attachBlog(PhameBlog $blog) { $this->blog = $blog; return $this; } public function getBlog() { return $this->assertAttached($this->blog); } public function getMonogram() { return 'J'.$this->getID(); } public function getLiveURI() { $blog = $this->getBlog(); $is_draft = $this->isDraft(); $is_archived = $this->isArchived(); if (strlen($blog->getDomain()) && !$is_draft && !$is_archived) { return $this->getExternalLiveURI(); } else { return $this->getInternalLiveURI(); } } public function getExternalLiveURI() { $id = $this->getID(); $slug = $this->getSlug(); $path = "/post/{$id}/{$slug}/"; $domain = $this->getBlog()->getDomain(); return (string)id(new PhutilURI('http://'.$domain)) ->setPath($path); } public function getInternalLiveURI() { $id = $this->getID(); $slug = $this->getSlug(); $blog_id = $this->getBlog()->getID(); return "/phame/live/{$blog_id}/post/{$id}/{$slug}/"; } public function getViewURI() { $id = $this->getID(); $slug = $this->getSlug(); return "/phame/post/view/{$id}/{$slug}/"; } public function getBestURI($is_live, $is_external) { if ($is_live) { if ($is_external) { return $this->getExternalLiveURI(); } else { return $this->getInternalLiveURI(); } } else { return $this->getViewURI(); } } public function getEditURI() { return '/phame/post/edit/'.$this->getID().'/'; } public function isDraft() { return ($this->getVisibility() == PhameConstants::VISIBILITY_DRAFT); } public function isArchived() { return ($this->getVisibility() == PhameConstants::VISIBILITY_ARCHIVED); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'configData' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255', 'subtitle' => 'text64', 'phameTitle' => 'sort64?', 'visibility' => 'uint32', 'mailKey' => 'bytes20', 'headerImagePHID' => 'phid?', // T6203/NULLABILITY // These seem like they should always be non-null? 'blogPHID' => 'phid?', 'body' => 'text?', 'configData' => 'text?', // T6203/NULLABILITY // This one probably should be nullable? 'datePublished' => 'epoch', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'bloggerPosts' => array( 'columns' => array( 'bloggerPHID', 'visibility', 'datePublished', 'id', ), ), ), ) + parent::getConfiguration(); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPhamePostPHIDType::TYPECONST); } public function getSlug() { return PhabricatorSlug::normalizeProjectSlug($this->getTitle()); } public function getHeaderImageURI() { return $this->getHeaderImageFile()->getBestURI(); } public function attachHeaderImageFile(PhabricatorFile $file) { $this->headerImageFile = $file; return $this; } public function getHeaderImageFile() { return $this->assertAttached($this->headerImageFile); } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { - // Draft posts are visible only to the author. Published posts are visible - // to whoever the blog is visible to. + // Draft and archived posts are visible only to the author and other + // users who can edit the blog. Published posts are visible to whoever + // the blog is visible to. switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if (!$this->isDraft() && !$this->isArchived() && $this->getBlog()) { return $this->getBlog()->getViewPolicy(); } else if ($this->getBlog()) { return $this->getBlog()->getEditPolicy(); } else { return PhabricatorPolicies::POLICY_NOONE; } break; case PhabricatorPolicyCapability::CAN_EDIT: if ($this->getBlog()) { return $this->getBlog()->getEditPolicy(); } else { return PhabricatorPolicies::POLICY_NOONE; } } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { // A blog post's author can always view it. switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: case PhabricatorPolicyCapability::CAN_EDIT: return ($user->getPHID() == $this->getBloggerPHID()); } } public function describeAutomaticCapability($capability) { return pht('The author of a blog post can always view and edit it.'); } /* -( PhabricatorMarkupInterface Implementation )-------------------------- */ public function getMarkupFieldKey($field) { $content = $this->getMarkupText($field); return PhabricatorMarkupEngine::digestRemarkupContent($this, $content); } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newPhameMarkupEngine(); } public function getMarkupText($field) { switch ($field) { case self::MARKUP_FIELD_BODY: return $this->getBody(); case self::MARKUP_FIELD_SUMMARY: return PhabricatorMarkupEngine::summarize($this->getBody()); } } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return (bool)$this->getPHID(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhamePostEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhamePostTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getBloggerPHID(), ); } /* -( PhabricatorSubscribableInterface Implementation )-------------------- */ public function isAutomaticallySubscribed($phid) { return ($this->bloggerPHID == $phid); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('title') ->setType('string') ->setDescription(pht('Title of the post.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('slug') ->setType('string') ->setDescription(pht('Slug for the post.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('blogPHID') ->setType('phid') ->setDescription(pht('PHID of the blog that the post belongs to.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('authorPHID') ->setType('phid') ->setDescription(pht('PHID of the author of the post.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('body') ->setType('string') ->setDescription(pht('Body of the post.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('datePublished') ->setType('epoch?') ->setDescription(pht('Publish date, if the post has been published.')), ); } public function getFieldValuesForConduit() { if ($this->isDraft()) { $date_published = null; } else if ($this->isArchived()) { $date_published = null; } else { $date_published = (int)$this->getDatePublished(); } return array( 'title' => $this->getTitle(), 'slug' => $this->getSlug(), 'blogPHID' => $this->getBlogPHID(), 'authorPHID' => $this->getBloggerPHID(), 'body' => $this->getBody(), 'datePublished' => $date_published, ); } public function getConduitSearchAttachments() { return array(); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PhamePostFulltextEngine(); } } diff --git a/src/applications/phortune/product/PhortuneSubscriptionProduct.php b/src/applications/phortune/product/PhortuneSubscriptionProduct.php index b63cc7c21c..85eb2999c7 100644 --- a/src/applications/phortune/product/PhortuneSubscriptionProduct.php +++ b/src/applications/phortune/product/PhortuneSubscriptionProduct.php @@ -1,107 +1,108 @@ subscriptionPHID = $subscription_phid; return $this; } public function getSubscriptionPHID() { return $this->subscriptionPHID; } public function setSubscription(PhortuneSubscription $subscription) { $this->subscription = $subscription; return $this; } public function getSubscription() { return $this->subscription; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function getRef() { return $this->getSubscriptionPHID(); } public function getName(PhortuneProduct $product) { return $this->getSubscription()->getSubscriptionName(); } public function getPriceAsCurrency(PhortuneProduct $product) { // Prices are calculated by the SubscriptionImplementation. return PhortuneCurrency::newEmptyCurrency(); } public function didPurchaseProduct( PhortuneProduct $product, PhortunePurchase $purchase) { - // TODO: Callback the subscription. - return; + return $this->getSubscription()->didPurchaseProduct( + $product, + $purchase); } public function didRefundProduct( PhortuneProduct $product, PhortunePurchase $purchase, PhortuneCurrency $amount) { // TODO: Callback the subscription. return; } public function getPurchaseName( PhortuneProduct $product, PhortunePurchase $purchase) { return $this->getSubscription()->getPurchaseName( $product, $purchase); } public function getPurchaseURI( PhortuneProduct $product, PhortunePurchase $purchase) { return $this->getSubscription()->getPurchaseURI( $product, $purchase); } public function loadImplementationsForRefs( PhabricatorUser $viewer, array $refs) { $subscriptions = id(new PhortuneSubscriptionQuery()) ->setViewer($viewer) ->withPHIDs($refs) ->execute(); $subscriptions = mpull($subscriptions, null, 'getPHID'); $objects = array(); foreach ($refs as $ref) { $subscription = idx($subscriptions, $ref); if (!$subscription) { continue; } $objects[] = id(new PhortuneSubscriptionProduct()) ->setViewer($viewer) ->setSubscriptionPHID($ref) ->setSubscription($subscription); } return $objects; } } diff --git a/src/applications/phortune/storage/PhortuneSubscription.php b/src/applications/phortune/storage/PhortuneSubscription.php index cc839adbea..a996dbf5d2 100644 --- a/src/applications/phortune/storage/PhortuneSubscription.php +++ b/src/applications/phortune/storage/PhortuneSubscription.php @@ -1,282 +1,291 @@ true, self::CONFIG_SERIALIZATION => array( 'metadata' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'defaultPaymentMethodPHID' => 'phid?', 'subscriptionClassKey' => 'bytes12', 'subscriptionClass' => 'text128', 'subscriptionRefKey' => 'bytes12', 'subscriptionRef' => 'text128', 'status' => 'text32', ), self::CONFIG_KEY_SCHEMA => array( 'key_subscription' => array( 'columns' => array('subscriptionClassKey', 'subscriptionRefKey'), 'unique' => true, ), 'key_account' => array( 'columns' => array('accountPHID'), ), 'key_merchant' => array( 'columns' => array('merchantPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhortuneSubscriptionPHIDType::TYPECONST); } public static function initializeNewSubscription( PhortuneAccount $account, PhortuneMerchant $merchant, PhabricatorUser $author, PhortuneSubscriptionImplementation $implementation, PhabricatorTriggerClock $clock) { $trigger = id(new PhabricatorWorkerTrigger()) ->setClock($clock); return id(new PhortuneSubscription()) ->setStatus(self::STATUS_ACTIVE) ->setAccountPHID($account->getPHID()) ->attachAccount($account) ->setMerchantPHID($merchant->getPHID()) ->attachMerchant($merchant) ->setAuthorPHID($author->getPHID()) ->setSubscriptionClass(get_class($implementation)) ->setSubscriptionRef($implementation->getRef()) ->attachImplementation($implementation) ->attachTrigger($trigger); } public function attachImplementation( PhortuneSubscriptionImplementation $impl) { $this->implementation = $impl; return $this; } public function getImplementation() { return $this->assertAttached($this->implementation); } public function attachAccount(PhortuneAccount $account) { $this->account = $account; return $this; } public function getAccount() { return $this->assertAttached($this->account); } public function attachMerchant(PhortuneMerchant $merchant) { $this->merchant = $merchant; return $this; } public function getMerchant() { return $this->assertAttached($this->merchant); } public function attachTrigger(PhabricatorWorkerTrigger $trigger) { $this->trigger = $trigger; return $this; } public function getTrigger() { return $this->assertAttached($this->trigger); } public function save() { $this->subscriptionClassKey = PhabricatorHash::digestForIndex( $this->subscriptionClass); $this->subscriptionRefKey = PhabricatorHash::digestForIndex( $this->subscriptionRef); $is_new = (!$this->getID()); $this->openTransaction(); // If we're saving this subscription for the first time, we're also // going to set up the trigger for it. if ($is_new) { $trigger_phid = PhabricatorPHID::generateNewPHID( PhabricatorWorkerTriggerPHIDType::TYPECONST); $this->setTriggerPHID($trigger_phid); } $result = parent::save(); if ($is_new) { $trigger_action = new PhabricatorScheduleTaskTriggerAction( array( 'class' => 'PhortuneSubscriptionWorker', 'data' => array( 'subscriptionPHID' => $this->getPHID(), ), 'options' => array( 'objectPHID' => $this->getPHID(), 'priority' => PhabricatorWorker::PRIORITY_BULK, ), )); $trigger = $this->getTrigger(); $trigger->setPHID($trigger_phid); $trigger->setAction($trigger_action); $trigger->save(); } $this->saveTransaction(); return $result; } public function getSubscriptionName() { return $this->getImplementation()->getName($this); } public function getSubscriptionFullName() { return $this->getImplementation()->getFullName($this); } public function getSubscriptionCrumbName() { return $this->getImplementation()->getCrumbName($this); } public function getCartName(PhortuneCart $cart) { return $this->getImplementation()->getCartName($this, $cart); } public function getURI() { $account_id = $this->getAccount()->getID(); $id = $this->getID(); return "/phortune/{$account_id}/subscription/view/{$id}/"; } public function getEditURI() { $account_id = $this->getAccount()->getID(); $id = $this->getID(); return "/phortune/{$account_id}/subscription/edit/{$id}/"; } public function getMerchantURI() { $merchant_id = $this->getMerchant()->getID(); $id = $this->getID(); return "/phortune/merchant/{$merchant_id}/subscription/view/{$id}/"; } public function getCostForBillingPeriodAsCurrency($start_epoch, $end_epoch) { return $this->getImplementation()->getCostForBillingPeriodAsCurrency( $this, $start_epoch, $end_epoch); } public function shouldInvoiceForBillingPeriod($start_epoch, $end_epoch) { return $this->getImplementation()->shouldInvoiceForBillingPeriod( $this, $start_epoch, $end_epoch); } public function getPurchaseName( PhortuneProduct $product, PhortunePurchase $purchase) { return $this->getImplementation()->getPurchaseName( $this, $product, $purchase); } public function getPurchaseURI( PhortuneProduct $product, PhortunePurchase $purchase) { return $this->getImplementation()->getPurchaseURI( $this, $product, $purchase); } + public function didPurchaseProduct( + PhortuneProduct $product, + PhortunePurchase $purchase) { + return $this->getImplementation()->didPurchaseProduct( + $this, + $product, + $purchase); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { // NOTE: Both view and edit use the account's edit policy. We punch a hole // through this for merchants, below. return $this ->getAccount() ->getPolicy(PhabricatorPolicyCapability::CAN_EDIT); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if ($this->getAccount()->hasAutomaticCapability($capability, $viewer)) { return true; } // If the viewer controls the merchant this subscription bills to, they can // view the subscription. if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { $can_admin = PhabricatorPolicyFilter::hasCapability( $viewer, $this->getMerchant(), PhabricatorPolicyCapability::CAN_EDIT); if ($can_admin) { return true; } } return false; } public function describeAutomaticCapability($capability) { return array( pht('Subscriptions inherit the policies of the associated account.'), pht( 'The merchant you are subscribed with can review and manage the '. 'subscription.'), ); } } diff --git a/src/applications/phortune/subscription/PhortuneSubscriptionImplementation.php b/src/applications/phortune/subscription/PhortuneSubscriptionImplementation.php index f8cbc21155..a337551c8d 100644 --- a/src/applications/phortune/subscription/PhortuneSubscriptionImplementation.php +++ b/src/applications/phortune/subscription/PhortuneSubscriptionImplementation.php @@ -1,51 +1,59 @@ getName($subscription); } public function getCrumbName(PhortuneSubscription $subscription) { return $this->getName($subscription); } abstract public function getCostForBillingPeriodAsCurrency( PhortuneSubscription $subscription, $start_epoch, $end_epoch); public function shouldInvoiceForBillingPeriod( PhortuneSubscription $subscription, $start_epoch, $end_epoch) { return true; } public function getCartName( PhortuneSubscription $subscription, PhortuneCart $cart) { return pht('Subscription'); } public function getPurchaseName( PhortuneSubscription $subscription, PhortuneProduct $product, PhortunePurchase $purchase) { return $product->getProductName(); } public function getPurchaseURI( PhortuneSubscription $subscription, PhortuneProduct $product, PhortunePurchase $purchase) { return null; } + + public function didPurchaseProduct( + PhortuneSubscription $subscription, + PhortuneProduct $product, + PhortunePurchase $purchase) { + return null; + } + } diff --git a/src/applications/repository/conduit/RepositoryQueryConduitAPIMethod.php b/src/applications/repository/conduit/RepositoryQueryConduitAPIMethod.php index 61435e2a1b..8b5005d8ec 100644 --- a/src/applications/repository/conduit/RepositoryQueryConduitAPIMethod.php +++ b/src/applications/repository/conduit/RepositoryQueryConduitAPIMethod.php @@ -1,87 +1,87 @@ 'optional list', 'phids' => 'optional list', 'callsigns' => 'optional list', 'vcsTypes' => 'optional list', 'remoteURIs' => 'optional list', 'uuids' => 'optional list', ); } protected function defineReturnType() { return 'list'; } protected function execute(ConduitAPIRequest $request) { $query = $this->newQueryForRequest($request); $ids = $request->getValue('ids', array()); if ($ids) { $query->withIDs($ids); } $phids = $request->getValue('phids', array()); if ($phids) { $query->withPHIDs($phids); } $callsigns = $request->getValue('callsigns', array()); if ($callsigns) { $query->withCallsigns($callsigns); } $vcs_types = $request->getValue('vcsTypes', array()); if ($vcs_types) { $query->withTypes($vcs_types); } $remote_uris = $request->getValue('remoteURIs', array()); if ($remote_uris) { $query->withURIs($remote_uris); } $uuids = $request->getValue('uuids', array()); if ($uuids) { $query->withUUIDs($uuids); } $pager = $this->newPager($request); $repositories = $query->executeWithCursorPager($pager); $results = array(); foreach ($repositories as $repository) { $results[] = $repository->toDictionary(); } return $results; } } diff --git a/src/docs/user/configuration/configuring_backups.diviner b/src/docs/user/configuration/configuring_backups.diviner index 1f13e0e17f..9776448216 100644 --- a/src/docs/user/configuration/configuring_backups.diviner +++ b/src/docs/user/configuration/configuring_backups.diviner @@ -1,147 +1,153 @@ @title Configuring Backups and Performing Migrations @group config Advice for backing up Phabricator, or migrating from one machine to another. Overview ======== Phabricator does not currently have a comprehensive backup system, but creating backups is not particularly difficult and Phabricator does have a few basic tools which can help you set up a reasonable process. In particular, the things which needs to be backed up are: - the MySQL databases; - hosted repositories; - uploaded files; and - your Phabricator configuration files. This document discusses approaches for backing up this data. If you are migrating from one machine to another, you can generally follow the same steps you would if you were creating a backup and then restoring it, you will just backup the old machine and then restore the data onto the new machine. +WARNING: You need to restart Phabricator after restoring data. + +Restarting Phabricator after performing a restore makes sure that caches are +flushed properly. For complete instructions, see +@{article:Restarting Phabricator}. + Backup: MySQL Databases ======================= Most of Phabricator's data is stored in MySQL, and it's the most important thing to back up. You can run `bin/storage dump` to get a dump of all the MySQL databases. This is a convenience script which just runs a normal `mysqldump`, but will only dump databases Phabricator owns. Since most of this data is compressible, it may be helpful to run it through gzip prior to storage. For example: phabricator/ $ ./bin/storage dump | gzip > backup.sql.gz Then store the backup somewhere safe, like in a box buried under an old tree stump. No one will ever think to look for it there. Restore: MySQL ============== To restore a MySQL dump, just pipe it to `mysql` on a clean host. (You may need to uncompress it first, if you compressed it prior to storage.) $ gunzip -c backup.sql.gz | mysql Backup: Hosted Repositories =========================== If you host repositories in Phabricator, you should back them up. You can use `bin/repository list-paths` to show the local paths on disk for each repository. To back them up, copy them elsewhere. You can also just clone them and keep the clones up to date, or use {nav Add Mirror} to have the mirror somewhere automatically. Restore: Hosted Repositories ============================ To restore hosted repositories, copy them back into the correct locations as shown by `bin/repository list-paths`. Backup: Uploaded Files ====================== Uploaded files may be stored in several different locations. The backup procedure depends on where files are stored: **Default / MySQL**: Under the default configuration, uploaded files are stored in MySQL, so the MySQL backup will include all files. In this case, you don't need to do any additional work. **Amazon S3**: If you use Amazon S3, redundancy and backups are built in to the service. This is probably sufficient for most installs. If you trust Amazon with your data //except not really//, you can backup your S3 bucket outside of Phabricator. **Local Disk**: If you use the local disk storage engine, you'll need to back up files manually. You can do this by creating a copy of the root directory where you told Phabricator to put files (the `storage.local-disk.path` configuration setting). For more information about configuring how files are stored, see @{article:Configuring File Storage}. Restore: Uploaded Files ======================= To restore a backup of local disk storage, just copy the backup into place. Backup: Configuration Files =========================== You should also backup your configuration files, and any scripts you use to deploy or administrate Phabricator (like a customized upgrade script). The best way to do this is to check them into a private repository somewhere and just use whatever backup process you already have in place for repositories. Just copying them somewhere will work fine too, of course. In particular, you should backup this configuration file which Phabricator creates: phabricator/conf/local/local.json This file contains all of the configuration settings that have been adjusted by using `bin/config set `. Restore: Configuration Files ============================ To restore configuration files, just copy them into the right locations. Copy your backup of `local.json` to `phabricator/conf/local/local.json`. Security ======== MySQL dumps have no builtin encryption and most data in Phabricator is stored in a raw, accessible form, so giving a user access to backups is a lot like giving them shell access to the machine Phabricator runs on. In particular, a user who has the backups can: - read data that policies do not permit them to see; - read email addresses and object secret keys; and - read other users' session and conduit tokens and impersonate them. Some of this information is durable, so disclosure of even a very old backup may present a risk. If you restrict access to the Phabricator host or database, you should also restrict access to the backups. Next Steps ========== Continue by: - returning to the @{article:Configuration Guide}. diff --git a/src/view/layout/PhabricatorActionView.php b/src/view/layout/PhabricatorActionView.php index 85b9d3abfe..f6de8eca5b 100644 --- a/src/view/layout/PhabricatorActionView.php +++ b/src/view/layout/PhabricatorActionView.php @@ -1,366 +1,371 @@ selected = $selected; return $this; } public function getSelected() { return $this->selected; } public function setMetadata($metadata) { $this->metadata = $metadata; return $this; } public function getMetadata() { return $this->metadata; } public function setDownload($download) { $this->download = $download; return $this; } public function getDownload() { return $this->download; } public function setHref($href) { $this->href = $href; return $this; } public function setColor($color) { $this->color = $color; return $this; } public function addSigil($sigil) { $this->sigils[] = $sigil; return $this; } public function getHref() { return $this->href; } public function setIcon($icon) { $this->icon = $icon; return $this; } public function setName($name) { $this->name = $name; return $this; } public function getName() { return $this->name; } public function setLabel($label) { $this->label = $label; return $this; } public function setDisabled($disabled) { $this->disabled = $disabled; return $this; } public function getDisabled() { return $this->disabled; } public function setWorkflow($workflow) { $this->workflow = $workflow; return $this; } public function setRenderAsForm($form) { $this->renderAsForm = $form; return $this; } public function setOpenInNewWindow($open_in_new_window) { $this->openInNewWindow = $open_in_new_window; return $this; } public function getOpenInNewWindow() { return $this->openInNewWindow; } + public function setID($id) { + $this->id = $id; + return $this; + } + public function getID() { if (!$this->id) { $this->id = celerity_generate_unique_node_id(); } return $this->id; } public function setOrder($order) { $this->order = $order; return $this; } public function getOrder() { return $this->order; } public function setType($type) { $this->type = $type; return $this; } public function getType() { return $this->type; } public function setSubmenu(array $submenu) { $this->submenu = $submenu; if (!$this->getHref()) { $this->setHref('#'); } return $this; } public function getItems($depth = 0) { $items = array(); $items[] = $this; foreach ($this->submenu as $action) { foreach ($action->getItems($depth + 1) as $item) { $item ->setHidden(true) ->setDepth($depth + 1); $items[] = $item; } } return $items; } public function setHidden($hidden) { $this->hidden = $hidden; return $this; } public function getHidden() { return $this->hidden; } public function setDepth($depth) { $this->depth = $depth; return $this; } public function getDepth() { return $this->depth; } public function render() { $caret_id = celerity_generate_unique_node_id(); $icon = null; if ($this->icon) { $color = ''; if ($this->disabled) { $color = ' grey'; } $icon = id(new PHUIIconView()) ->addClass('phabricator-action-view-icon') ->setIcon($this->icon.$color); } $sigils = array(); if ($this->workflow) { $sigils[] = 'workflow'; } if ($this->download) { $sigils[] = 'download'; } if ($this->submenu) { $sigils[] = 'keep-open'; } if ($this->sigils) { $sigils = array_merge($sigils, $this->sigils); } $sigils = $sigils ? implode(' ', $sigils) : null; if ($this->href) { if ($this->renderAsForm) { if (!$this->hasViewer()) { throw new Exception( pht( 'Call %s when rendering an action as a form.', 'setViewer()')); } $item = javelin_tag( 'button', array( 'class' => 'phabricator-action-view-item', ), array($icon, $this->name)); $item = phabricator_form( $this->getViewer(), array( 'action' => $this->getHref(), 'method' => 'POST', 'sigil' => $sigils, 'meta' => $this->metadata, ), $item); } else { if ($this->getOpenInNewWindow()) { $target = '_blank'; } else { $target = null; } if ($this->submenu) { $caret = javelin_tag( 'span', array( 'class' => 'caret-right', 'id' => $caret_id, ), ''); } else { $caret = null; } $item = javelin_tag( 'a', array( 'href' => $this->getHref(), 'class' => 'phabricator-action-view-item', 'target' => $target, 'sigil' => $sigils, 'meta' => $this->metadata, ), array($icon, $this->name, $caret)); } } else { $item = javelin_tag( 'span', array( 'class' => 'phabricator-action-view-item', 'sigil' => $sigils, ), array($icon, $this->name, $this->renderChildren())); } $classes = array(); $classes[] = 'phabricator-action-view'; if ($this->disabled) { $classes[] = 'phabricator-action-view-disabled'; } if ($this->label) { $classes[] = 'phabricator-action-view-label'; } if ($this->selected) { $classes[] = 'phabricator-action-view-selected'; } if ($this->submenu) { $classes[] = 'phabricator-action-view-submenu'; } if ($this->getHref()) { $classes[] = 'phabricator-action-view-href'; } if ($this->icon) { $classes[] = 'action-has-icon'; } if ($this->color) { $classes[] = $this->color; } if ($this->type) { $classes[] = 'phabricator-action-view-'.$this->type; } $style = array(); if ($this->hidden) { $style[] = 'display: none;'; } if ($this->depth) { $indent = ($this->depth * 16); $style[] = "margin-left: {$indent}px;"; } $sigil = null; $meta = null; if ($this->submenu) { Javelin::initBehavior('phui-submenu'); $sigil = 'phui-submenu'; $item_ids = array(); foreach ($this->submenu as $subitem) { $item_ids[] = $subitem->getID(); } $meta = array( 'itemIDs' => $item_ids, 'caretID' => $caret_id, ); } return javelin_tag( 'li', array( 'id' => $this->getID(), 'class' => implode(' ', $classes), 'style' => implode(' ', $style), 'sigil' => $sigil, 'meta' => $meta, ), $item); } } diff --git a/webroot/rsrc/css/application/diffusion/diffusion-repository.css b/webroot/rsrc/css/application/diffusion/diffusion-repository.css new file mode 100644 index 0000000000..99ddd72940 --- /dev/null +++ b/webroot/rsrc/css/application/diffusion/diffusion-repository.css @@ -0,0 +1,13 @@ +/** + * @provides diffusion-repository-css + */ + +.diffusion-page-header-view a.phui-header-action-link { + display: block; + float: none; +} + +.phui-box.phui-object-box.phui-box-blue-property + .diffusion-panel-header-view.phui-header-shell { + padding: 8px 4px 8px 16px; +}