diff --git a/resources/builtin/image-100x100.png b/resources/builtin/image-100x100.png
new file mode 100644
index 0000000000..c56a9aa083
Binary files /dev/null and b/resources/builtin/image-100x100.png differ
diff --git a/resources/builtin/image-220x220.png b/resources/builtin/image-220x220.png
new file mode 100644
index 0000000000..928b5b05eb
Binary files /dev/null and b/resources/builtin/image-220x220.png differ
diff --git a/resources/builtin/image-280x210.png b/resources/builtin/image-280x210.png
new file mode 100644
index 0000000000..48237b045e
Binary files /dev/null and b/resources/builtin/image-280x210.png differ
diff --git a/resources/celerity/map.php b/resources/celerity/map.php
index 0a6ce7d67e..2fdb459dcc 100644
--- a/resources/celerity/map.php
+++ b/resources/celerity/map.php
@@ -1,2231 +1,2220 @@
 <?php
 
 /**
  * This file is automatically generated. Use 'bin/celerity map' to rebuild it.
  *
  * @generated
  */
 return array(
   'names' => array(
-    'core.pkg.css' => '8aed144e',
-    'core.pkg.js' => '60924527',
+    'core.pkg.css' => '50250d4f',
+    'core.pkg.js' => 'f3e08b38',
     'darkconsole.pkg.js' => 'e7393ebb',
     'differential.pkg.css' => 'bb338e4b',
-    'differential.pkg.js' => '3cfa26f9',
+    'differential.pkg.js' => '895b8d62',
     'diffusion.pkg.css' => '591664fa',
     'diffusion.pkg.js' => '0115b37c',
     'maniphest.pkg.css' => '68d4dd3d',
     'maniphest.pkg.js' => 'df4aa49f',
     'rsrc/css/aphront/aphront-bars.css' => '231ac33c',
     'rsrc/css/aphront/dark-console.css' => '6378ef3d',
     'rsrc/css/aphront/dialog-view.css' => '725f2f23',
     'rsrc/css/aphront/lightbox-attachment.css' => '7acac05d',
     'rsrc/css/aphront/list-filter-view.css' => '1fb5cab3',
     'rsrc/css/aphront/multi-column.css' => 'fd18389d',
     'rsrc/css/aphront/notification.css' => '9c279160',
     'rsrc/css/aphront/pager-view.css' => '2e3539af',
     'rsrc/css/aphront/panel-view.css' => '8427b78d',
     'rsrc/css/aphront/phabricator-nav-view.css' => '949b43d9',
     'rsrc/css/aphront/table-view.css' => '59e2c0f8',
     'rsrc/css/aphront/tokenizer.css' => '86a13f7f',
     'rsrc/css/aphront/tooltip.css' => '7672b60f',
     'rsrc/css/aphront/two-column.css' => '16ab3ad2',
     'rsrc/css/aphront/typeahead-browse.css' => 'd8581d2c',
     'rsrc/css/aphront/typeahead.css' => '0e403212',
     'rsrc/css/application/almanac/almanac.css' => 'dbb9b3af',
-    'rsrc/css/application/auth/auth.css' => '1e655982',
+    'rsrc/css/application/auth/auth.css' => '44975d4b',
     'rsrc/css/application/base/main-menu-view.css' => '1766b04d',
     'rsrc/css/application/base/notification-menu.css' => '3c9d8aa1',
     'rsrc/css/application/base/phabricator-application-launch-view.css' => '132f9d14',
-    'rsrc/css/application/base/standard-page-view.css' => 'dc14c671',
+    'rsrc/css/application/base/standard-page-view.css' => '062f0f54',
     'rsrc/css/application/chatlog/chatlog.css' => '852140ff',
+    'rsrc/css/application/conduit/conduit-api.css' => '7bc725c4',
     'rsrc/css/application/config/config-options.css' => '7fedf08b',
     'rsrc/css/application/config/config-template.css' => '8e6c6fcd',
     'rsrc/css/application/config/config-welcome.css' => '6abd79be',
     'rsrc/css/application/config/setup-issue.css' => '22270af2',
     'rsrc/css/application/config/unhandled-exception.css' => '37d4f9a2',
-    'rsrc/css/application/conpherence/durable-column.css' => '2e68a92f',
+    'rsrc/css/application/conpherence/durable-column.css' => '8c43d6ac',
     'rsrc/css/application/conpherence/menu.css' => 'f9f1d143',
-    'rsrc/css/application/conpherence/message-pane.css' => '73631823',
-    'rsrc/css/application/conpherence/notification.css' => 'd208f806',
-    'rsrc/css/application/conpherence/transaction.css' => '25138b7f',
+    'rsrc/css/application/conpherence/message-pane.css' => '7cbf4cbb',
+    'rsrc/css/application/conpherence/notification.css' => '919974b6',
+    'rsrc/css/application/conpherence/transaction.css' => '42a457f6',
     'rsrc/css/application/conpherence/update.css' => '1099a660',
     'rsrc/css/application/conpherence/widget-pane.css' => '2af42ebe',
     'rsrc/css/application/contentsource/content-source-view.css' => '4b8b05d4',
     'rsrc/css/application/countdown/timer.css' => '86b7b0a0',
     'rsrc/css/application/dashboard/dashboard.css' => 'db1d30b0',
     'rsrc/css/application/diff/inline-comment-summary.css' => 'eb5f8e8c',
     'rsrc/css/application/differential/add-comment.css' => 'c47f8c40',
     'rsrc/css/application/differential/changeset-view.css' => 'e19cfd6e',
     'rsrc/css/application/differential/core.css' => '7ac3cabc',
     'rsrc/css/application/differential/phui-inline-comment.css' => '2174771a',
     'rsrc/css/application/differential/results-table.css' => '181aa9d9',
     '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' => '63f3ef4a',
     'rsrc/css/application/diffusion/diffusion-icons.css' => '9c5828da',
     'rsrc/css/application/diffusion/diffusion-readme.css' => '2106ea08',
     'rsrc/css/application/diffusion/diffusion-source.css' => '66fdf661',
     'rsrc/css/application/feed/feed.css' => 'b513b5f4',
     'rsrc/css/application/files/global-drag-and-drop.css' => '697324ad',
     'rsrc/css/application/flag/flag.css' => '5337623f',
     'rsrc/css/application/harbormaster/harbormaster.css' => '49d64eb4',
     'rsrc/css/application/herald/herald-test.css' => '778b008e',
     'rsrc/css/application/herald/herald.css' => '826075fa',
     'rsrc/css/application/maniphest/batch-editor.css' => '8f380ebc',
     'rsrc/css/application/maniphest/report.css' => 'f6931fdf',
     'rsrc/css/application/maniphest/task-edit.css' => '8e23031b',
     'rsrc/css/application/maniphest/task-summary.css' => 'ab2fc691',
     'rsrc/css/application/objectselector/object-selector.css' => '029a133d',
     'rsrc/css/application/owners/owners-path-editor.css' => '2f00933b',
     'rsrc/css/application/paste/paste.css' => 'eb997ddd',
     'rsrc/css/application/people/people-profile.css' => '25970776',
     'rsrc/css/application/phame/phame.css' => '88bd4705',
     'rsrc/css/application/pholio/pholio-edit.css' => '3ad9d1ee',
     'rsrc/css/application/pholio/pholio-inline-comments.css' => '8e545e49',
     'rsrc/css/application/pholio/pholio.css' => '95174bdd',
     'rsrc/css/application/phortune/phortune-credit-card-form.css' => '8391eb02',
     'rsrc/css/application/phortune/phortune.css' => '9149f103',
     'rsrc/css/application/phrequent/phrequent.css' => 'ffc185ad',
     'rsrc/css/application/phriction/phriction-document-css.css' => '0d16bc9a',
     '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/comments.css' => '6cdccea7',
     'rsrc/css/application/ponder/feed.css' => 'e62615b6',
     'rsrc/css/application/ponder/post.css' => '9d415218',
     'rsrc/css/application/ponder/vote.css' => '8ed6ed8b',
     'rsrc/css/application/profile/profile-view.css' => '1a20dcbf',
     'rsrc/css/application/projects/project-icon.css' => 'c2ecb7f1',
     '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/search-results.css' => '15c71110',
     'rsrc/css/application/slowvote/slowvote.css' => '266df6a1',
     'rsrc/css/application/tokens/tokens.css' => '3d0f239e',
     'rsrc/css/application/uiexample/example.css' => '528b19de',
-    'rsrc/css/core/core.css' => 'cee2aadb',
+    'rsrc/css/core/core.css' => '6230ff55',
     'rsrc/css/core/remarkup.css' => '0037bdbf',
     'rsrc/css/core/syntax.css' => '6b7b24d9',
-    'rsrc/css/core/z-index.css' => 'ef044fae',
+    'rsrc/css/core/z-index.css' => '8c8c40aa',
     'rsrc/css/diviner/diviner-shared.css' => '38813222',
     'rsrc/css/font/font-awesome.css' => 'e2e712fe',
     'rsrc/css/font/font-source-sans-pro.css' => '8906c07b',
     'rsrc/css/font/phui-font-icon-base.css' => '3dad2ae3',
     'rsrc/css/layout/phabricator-filetree-view.css' => 'fccf9f82',
     'rsrc/css/layout/phabricator-hovercard-view.css' => '44394670',
     'rsrc/css/layout/phabricator-side-menu-view.css' => 'a440478a',
     'rsrc/css/layout/phabricator-source-code-view.css' => '2ceee894',
-    'rsrc/css/phui/calendar/phui-calendar-day.css' => '75b8cc4a',
+    'rsrc/css/phui/calendar/phui-calendar-day.css' => '38891735',
     'rsrc/css/phui/calendar/phui-calendar-list.css' => 'c1d0ca59',
-    'rsrc/css/phui/calendar/phui-calendar-month.css' => 'a92e47d2',
+    'rsrc/css/phui/calendar/phui-calendar-month.css' => '75e6a2ee',
     'rsrc/css/phui/calendar/phui-calendar.css' => '8675968e',
     'rsrc/css/phui/phui-action-header-view.css' => 'e4471f43',
     'rsrc/css/phui/phui-action-list.css' => '4f4d09f2',
     'rsrc/css/phui/phui-action-panel.css' => '3ee9afd5',
     'rsrc/css/phui/phui-box.css' => 'a5bb366d',
     'rsrc/css/phui/phui-button.css' => 'de610129',
     'rsrc/css/phui/phui-crumbs-view.css' => 'aeff7a21',
     'rsrc/css/phui/phui-document.css' => '7b564cf6',
     'rsrc/css/phui/phui-feed-story.css' => 'c9f3a0b5',
     'rsrc/css/phui/phui-fontkit.css' => '1e71371a',
-    'rsrc/css/phui/phui-form-view.css' => 'ddec8479',
+    'rsrc/css/phui/phui-form-view.css' => 'e1abbe8e',
     'rsrc/css/phui/phui-form.css' => 'f535f938',
-    'rsrc/css/phui/phui-header-view.css' => 'a1d2905a',
+    'rsrc/css/phui/phui-header-view.css' => '5c0c1c39',
     'rsrc/css/phui/phui-icon.css' => 'bc766998',
     'rsrc/css/phui/phui-image-mask.css' => '5a8b09c8',
     'rsrc/css/phui/phui-info-panel.css' => '27ea50a1',
     'rsrc/css/phui/phui-info-view.css' => '33595731',
     'rsrc/css/phui/phui-list.css' => '2e25ebfb',
     'rsrc/css/phui/phui-object-box.css' => '3a601bc5',
-    'rsrc/css/phui/phui-object-item-list-view.css' => '172ea456',
+    'rsrc/css/phui/phui-object-item-list-view.css' => 'c259c94f',
     'rsrc/css/phui/phui-pinboard-view.css' => 'eaab2b1b',
     'rsrc/css/phui/phui-property-list-view.css' => 'd2d143ea',
     'rsrc/css/phui/phui-remarkup-preview.css' => '19ad512b',
     'rsrc/css/phui/phui-spacing.css' => '042804d6',
     'rsrc/css/phui/phui-status.css' => '888cedb8',
     'rsrc/css/phui/phui-tag-view.css' => '402691cc',
     'rsrc/css/phui/phui-text.css' => 'cf019f54',
-    'rsrc/css/phui/phui-timeline-view.css' => 'b0fbc4d7',
+    'rsrc/css/phui/phui-timeline-view.css' => 'a85542c8',
     'rsrc/css/phui/phui-workboard-view.css' => '3279cbbf',
     'rsrc/css/phui/phui-workpanel-view.css' => 'e495a5cc',
     'rsrc/css/sprite-gradient.css' => '4bdb98a7',
     'rsrc/css/sprite-login.css' => 'a3526809',
     'rsrc/css/sprite-main-header.css' => '28d01b0b',
     'rsrc/css/sprite-menu.css' => '3f052fdc',
     'rsrc/css/sprite-projects.css' => 'b0d9e24f',
     'rsrc/css/sprite-tokens.css' => '1706b943',
     'rsrc/externals/font/fontawesome/fontawesome-webfont.eot' => '5fb6fb0e',
     'rsrc/externals/font/fontawesome/fontawesome-webfont.ttf' => 'a653cb11',
     'rsrc/externals/font/fontawesome/fontawesome-webfont.woff' => '80526fc8',
     'rsrc/externals/font/fontawesome/fontawesome-webfont.woff2' => '4924d54d',
     'rsrc/externals/font/sourcesans/SourceSansPro-It.woff' => '3f21af52',
     'rsrc/externals/font/sourcesans/SourceSansPro-It.woff2' => '30a7cf60',
     'rsrc/externals/font/sourcesans/SourceSansPro-Regular.woff2' => 'e89b04b1',
     'rsrc/externals/font/sourcesans/SourceSansPro-Semibold.woff' => '67cce9ea',
     'rsrc/externals/font/sourcesans/SourceSansPro-Semibold.woff2' => 'ec2ed916',
     'rsrc/externals/font/sourcesans/SourceSansPro-SemiboldIt.woff' => 'bd1ce81d',
     'rsrc/externals/font/sourcesans/SourceSansPro-SemiboldIt.woff2' => 'd9928416',
     'rsrc/externals/font/sourcesans/SourceSansPro.woff' => '3614608c',
     'rsrc/externals/javelin/core/Event.js' => '85ea0626',
     'rsrc/externals/javelin/core/Stratcom.js' => '6c53634d',
     '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' => '6f7962d5',
     'rsrc/externals/javelin/lib/History.js' => 'd4505101',
     'rsrc/externals/javelin/lib/JSON.js' => '69adf288',
     'rsrc/externals/javelin/lib/Leader.js' => '331b1611',
     'rsrc/externals/javelin/lib/Mask.js' => '8a41885b',
     'rsrc/externals/javelin/lib/Quicksand.js' => '4cebc641',
     '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' => 'eaa5b321',
+    'rsrc/externals/javelin/lib/Scrollbar.js' => '087e919c',
     'rsrc/externals/javelin/lib/Sound.js' => '949c0fe5',
     'rsrc/externals/javelin/lib/URI.js' => '6eff08aa',
     'rsrc/externals/javelin/lib/Vector.js' => '2caa8fb8',
     'rsrc/externals/javelin/lib/WebSocket.js' => 'e292eaf4',
     'rsrc/externals/javelin/lib/Workflow.js' => '5b2e3e2b',
     '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' => 'ab5f468d',
     'rsrc/externals/javelin/lib/control/typeahead/Typeahead.js' => '70baed2f',
     'rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js' => 'e6e25838',
     'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadCompositeSource.js' => '503e17fd',
     'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js' => '8b3fd187',
     'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js' => '54f314a0',
     'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => '2818f5ce',
     'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js' => '316b8fa1',
     'rsrc/externals/raphael/g.raphael.js' => '40dde778',
     'rsrc/externals/raphael/g.raphael.line.js' => '40da039e',
     'rsrc/externals/raphael/raphael.js' => '51ee6b43',
     'rsrc/favicons/apple-touch-icon-120x120.png' => '43742962',
     'rsrc/favicons/apple-touch-icon-152x152.png' => '669eaec3',
     'rsrc/favicons/apple-touch-icon-76x76.png' => 'ecdef672',
     'rsrc/favicons/favicon-128.png' => '47cdff03',
     'rsrc/favicons/favicon-16x16.png' => 'ee2523ac',
     'rsrc/favicons/favicon-32x32.png' => 'b6a8150e',
     'rsrc/favicons/favicon-96x96.png' => '8f7ea177',
     'rsrc/image/BFCFDA.png' => 'd5ec91f4',
     'rsrc/image/actions/edit.png' => '2fc41442',
     'rsrc/image/avatar.png' => '3eb28cd9',
     'rsrc/image/checker_dark.png' => 'd8e65881',
     'rsrc/image/checker_light.png' => 'a0155918',
     'rsrc/image/checker_lighter.png' => 'd5da91b6',
     '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/bullet_black.png' => 'ff190031',
     'rsrc/image/icon/fatcow/bullet_orange.png' => 'e273e5bb',
     'rsrc/image/icon/fatcow/bullet_red.png' => 'c0b75434',
     '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/folder.png' => '95a435af',
     'rsrc/image/icon/fatcow/folder_go.png' => '001cbc94',
     '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_link.png' => 'a90023c7',
     'rsrc/image/icon/fatcow/page_white_put.png' => '08c95a0c',
     'rsrc/image/icon/fatcow/page_white_text.png' => '1e1f79c3',
     '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/fatcow/thumbnails/default.p100.png' => '7d490b01',
-    'rsrc/image/icon/fatcow/thumbnails/default160x120.png' => 'f2e8a2eb',
-    'rsrc/image/icon/fatcow/thumbnails/default280x210.png' => '43e8926a',
-    'rsrc/image/icon/fatcow/thumbnails/default60x45.png' => '0118abed',
-    'rsrc/image/icon/fatcow/thumbnails/image.p100.png' => 'da23cf97',
-    'rsrc/image/icon/fatcow/thumbnails/image160x120.png' => '79bb556a',
-    'rsrc/image/icon/fatcow/thumbnails/image280x210.png' => '91ae054a',
-    'rsrc/image/icon/fatcow/thumbnails/image60x45.png' => 'c5e1685e',
-    'rsrc/image/icon/fatcow/thumbnails/pdf.p100.png' => '87d5e065',
-    'rsrc/image/icon/fatcow/thumbnails/pdf160x120.png' => 'ac9edbf5',
-    'rsrc/image/icon/fatcow/thumbnails/pdf280x210.png' => '1c585653',
-    'rsrc/image/icon/fatcow/thumbnails/pdf60x45.png' => 'c0db4143',
-    'rsrc/image/icon/fatcow/thumbnails/zip.p100.png' => '6ea5aae4',
-    'rsrc/image/icon/fatcow/thumbnails/zip160x120.png' => '75f9cd0f',
-    'rsrc/image/icon/fatcow/thumbnails/zip280x210.png' => 'dfda5b8e',
-    'rsrc/image/icon/fatcow/thumbnails/zip60x45.png' => 'af11bf3e',
     'rsrc/image/icon/lightbox/close-2.png' => 'cc40e7c8',
     'rsrc/image/icon/lightbox/close-hover-2.png' => 'fb5d6d9e',
     'rsrc/image/icon/lightbox/left-arrow-2.png' => '8426133b',
     'rsrc/image/icon/lightbox/left-arrow-hover-2.png' => '701e5ee3',
     'rsrc/image/icon/lightbox/right-arrow-2.png' => '6d5519a0',
     'rsrc/image/icon/lightbox/right-arrow-hover-2.png' => '3a04aa21',
     '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/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/washington.png' => '40dd301c',
     'rsrc/image/phrequent_active.png' => 'a466a8ed',
     'rsrc/image/phrequent_inactive.png' => 'bfc15a69',
     'rsrc/image/sprite-gradient.png' => 'ec15a417',
     'rsrc/image/sprite-login-X2.png' => 'a15918f0',
     'rsrc/image/sprite-login.png' => '8cee4f6e',
     'rsrc/image/sprite-main-header.png' => '39419fa6',
     'rsrc/image/sprite-menu-X2.png' => '829263b7',
     'rsrc/image/sprite-menu.png' => '055fbcc9',
     'rsrc/image/sprite-projects-X2.png' => '8c91c839',
     'rsrc/image/sprite-projects.png' => 'ef9dc9b5',
     'rsrc/image/sprite-tokens-X2.png' => 'b4776580',
     'rsrc/image/sprite-tokens.png' => '25b75533',
     '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' => '30a6303c',
-    'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => '6e1f0cba',
+    'rsrc/js/application/aphlict/Aphlict.js' => '5359e785',
+    'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => 'e09f6208',
     'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => 'b1a59974',
     'rsrc/js/application/aphlict/behavior-aphlict-status.js' => 'ea681761',
     'rsrc/js/application/auth/behavior-persona-login.js' => '9414ff18',
+    'rsrc/js/application/calendar/event-all-day.js' => 'ca5fa62a',
     'rsrc/js/application/config/behavior-reorder-fields.js' => '14a827de',
-    'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => '6709c934',
+    'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => 'b7342ddb',
     'rsrc/js/application/conpherence/behavior-drag-and-drop-photo.js' => 'cf86d16a',
-    'rsrc/js/application/conpherence/behavior-durable-column.js' => '657c2b50',
-    'rsrc/js/application/conpherence/behavior-menu.js' => '804b0773',
+    'rsrc/js/application/conpherence/behavior-durable-column.js' => '16c695bf',
+    'rsrc/js/application/conpherence/behavior-menu.js' => '4351c4a0',
     'rsrc/js/application/conpherence/behavior-pontificate.js' => '21ba5861',
     'rsrc/js/application/conpherence/behavior-quicksand-blacklist.js' => '7927a7d3',
     'rsrc/js/application/conpherence/behavior-widget-pane.js' => '93568464',
     'rsrc/js/application/countdown/timer.js' => 'e4cc26b3',
     'rsrc/js/application/dashboard/behavior-dashboard-async-panel.js' => '469c0d9e',
     'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => '82439934',
     'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => '453c5375',
     'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => 'd4eecc63',
     'rsrc/js/application/differential/ChangesetViewManager.js' => '58562350',
-    'rsrc/js/application/differential/DifferentialInlineCommentEditor.js' => '2529c82d',
+    'rsrc/js/application/differential/DifferentialInlineCommentEditor.js' => 'd4c87bf4',
     'rsrc/js/application/differential/behavior-add-reviewers-and-ccs.js' => 'e10f8e18',
     'rsrc/js/application/differential/behavior-comment-jump.js' => '4fdb476d',
     'rsrc/js/application/differential/behavior-comment-preview.js' => '8e1389b5',
     'rsrc/js/application/differential/behavior-diff-radios.js' => 'e1ff79b1',
     'rsrc/js/application/differential/behavior-dropdown-menus.js' => '2035b9cb',
     'rsrc/js/application/differential/behavior-edit-inline-comments.js' => 'e723c323',
     'rsrc/js/application/differential/behavior-keyboard-nav.js' => '2c426492',
     'rsrc/js/application/differential/behavior-populate.js' => '8694b1df',
     'rsrc/js/application/differential/behavior-show-field-details.js' => 'bba9eedf',
     'rsrc/js/application/differential/behavior-toggle-files.js' => 'ca3f91eb',
     'rsrc/js/application/differential/behavior-user-select.js' => 'a8d8459d',
     'rsrc/js/application/diffusion/DiffusionLocateFileSource.js' => 'b42eddc7',
     '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' => '9007c197',
     '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' => '2b228192',
     'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => 'e5822781',
     'rsrc/js/application/files/behavior-icon-composer.js' => '8ef9ab58',
     'rsrc/js/application/files/behavior-launch-icon-composer.js' => '48086888',
     'rsrc/js/application/herald/HeraldRuleEditor.js' => '9229e764',
     'rsrc/js/application/herald/PathTypeahead.js' => 'f7fc67ec',
     'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3',
     'rsrc/js/application/maniphest/behavior-batch-editor.js' => 'f5d1233b',
     'rsrc/js/application/maniphest/behavior-batch-selector.js' => '7b98d7c5',
     'rsrc/js/application/maniphest/behavior-line-chart.js' => '88f0c5b3',
     'rsrc/js/application/maniphest/behavior-list-edit.js' => 'a9f88de2',
     'rsrc/js/application/maniphest/behavior-subpriorityeditor.js' => '84845b5b',
     'rsrc/js/application/maniphest/behavior-transaction-controls.js' => '44168bad',
     'rsrc/js/application/maniphest/behavior-transaction-expand.js' => '5fefb143',
     'rsrc/js/application/maniphest/behavior-transaction-preview.js' => 'f8248bc5',
     '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/phame/phame-post-preview.js' => 'be807912',
     'rsrc/js/application/pholio/behavior-pholio-mock-edit.js' => '246dc085',
     'rsrc/js/application/pholio/behavior-pholio-mock-view.js' => 'fbe497e7',
     'rsrc/js/application/phortune/behavior-stripe-payment-form.js' => '3f5d6dbf',
     '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' => '9a340b3d',
     'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c',
     'rsrc/js/application/ponder/behavior-votebox.js' => '4e9b766b',
-    'rsrc/js/application/projects/behavior-project-boards.js' => '60292820',
+    'rsrc/js/application/projects/behavior-project-boards.js' => 'ba4fa35c',
     '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' => 'f9539603',
     'rsrc/js/application/search/behavior-reorder-queries.js' => 'e9581f08',
     'rsrc/js/application/slowvote/behavior-slowvote-embed.js' => '887ad43f',
     'rsrc/js/application/transactions/behavior-show-older-transactions.js' => 'dbbf48b6',
     'rsrc/js/application/transactions/behavior-transaction-comment-form.js' => '9f7309fb',
     'rsrc/js/application/transactions/behavior-transaction-list.js' => '13c739ea',
     'rsrc/js/application/typeahead/behavior-typeahead-browse.js' => '635de1ec',
     'rsrc/js/application/typeahead/behavior-typeahead-search.js' => '93d0c9e3',
     'rsrc/js/application/uiexample/JavelinViewExample.js' => 'd4a14807',
     'rsrc/js/application/uiexample/ReactorButtonExample.js' => 'd19198c8',
     'rsrc/js/application/uiexample/ReactorCheckboxExample.js' => '519705ea',
     'rsrc/js/application/uiexample/ReactorFocusExample.js' => '40a6a403',
     'rsrc/js/application/uiexample/ReactorInputExample.js' => '886fd850',
     'rsrc/js/application/uiexample/ReactorMouseoverExample.js' => '47c794d8',
     'rsrc/js/application/uiexample/ReactorRadioExample.js' => '988040b4',
     'rsrc/js/application/uiexample/ReactorSelectExample.js' => 'a155550f',
     'rsrc/js/application/uiexample/ReactorSendClassExample.js' => '1def2711',
     'rsrc/js/application/uiexample/ReactorSendPropertiesExample.js' => 'b1f0ccee',
     'rsrc/js/application/uiexample/busy-example.js' => '60479091',
     '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' => '07de8873',
     'rsrc/js/core/DraggableList.js' => 'a16ec1c6',
     'rsrc/js/core/FileUpload.js' => '477359c8',
-    'rsrc/js/core/Hovercard.js' => '7e8468ae',
+    'rsrc/js/core/Hovercard.js' => '14ac66f5',
     'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2',
     'rsrc/js/core/KeyboardShortcutManager.js' => 'c1700f6f',
     'rsrc/js/core/MultirowRowManager.js' => 'b5d57730',
     'rsrc/js/core/Notification.js' => '0c6946e7',
     'rsrc/js/core/Prefab.js' => '6920d200',
     'rsrc/js/core/ShapedRequest.js' => '7cbe244b',
     'rsrc/js/core/TextAreaUtils.js' => '5c93c52c',
     'rsrc/js/core/Title.js' => 'df5e11d2',
     'rsrc/js/core/ToolTip.js' => '1d298e3a',
     '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-choose-control.js' => '6153c708',
     'rsrc/js/core/behavior-crop.js' => 'fa0f4fc2',
     'rsrc/js/core/behavior-dark-console.js' => 'f411b6ae',
     'rsrc/js/core/behavior-device.js' => 'a205cf28',
     'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '6d49590e',
     'rsrc/js/core/behavior-error-log.js' => '6882e80a',
     'rsrc/js/core/behavior-fancy-datepicker.js' => '5c0f680f',
     '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' => 'c8e57404',
     'rsrc/js/core/behavior-high-security-warning.js' => 'a464fe03',
     'rsrc/js/core/behavior-history-install.js' => '7ee2b591',
     'rsrc/js/core/behavior-hovercard.js' => 'f36e01af',
     'rsrc/js/core/behavior-keyboard-pager.js' => 'a8da01f0',
     'rsrc/js/core/behavior-keyboard-shortcuts.js' => 'd75709e6',
     'rsrc/js/core/behavior-lightbox-attachments.js' => 'f8ba29d7',
     'rsrc/js/core/behavior-line-linker.js' => '1499a8cb',
     'rsrc/js/core/behavior-more.js' => 'a80d0378',
     'rsrc/js/core/behavior-object-selector.js' => '49b73b36',
     'rsrc/js/core/behavior-oncopy.js' => '2926fff2',
     'rsrc/js/core/behavior-phabricator-nav.js' => '14d7a8b8',
     'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => 'e32d14ab',
     'rsrc/js/core/behavior-refresh-csrf.js' => '7814b593',
     'rsrc/js/core/behavior-remarkup-preview.js' => 'f7379f45',
     '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' => '048330fa',
     'rsrc/js/core/behavior-select-on-click.js' => '4e3e79a6',
     'rsrc/js/core/behavior-toggle-class.js' => '5d7c9f33',
     'rsrc/js/core/behavior-tokenizer.js' => 'b3a4b884',
     'rsrc/js/core/behavior-tooltip.js' => '3ee3408b',
     'rsrc/js/core/behavior-watch-anchor.js' => '9f36c42d',
     'rsrc/js/core/behavior-workflow.js' => '0a3f3021',
     'rsrc/js/core/phtize.js' => 'd254d646',
     'rsrc/js/phui/behavior-phui-object-box-tabs.js' => '2bfa2836',
     'rsrc/js/phui/behavior-phui-timeline-dropdown-menu.js' => '4d94d9c3',
     'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8',
     'rsrc/js/phuix/PHUIXActionView.js' => '8cf6d262',
     'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'bd4c8dca',
   ),
   'symbols' => array(
     'almanac-css' => 'dbb9b3af',
     'aphront-bars' => '231ac33c',
     'aphront-dark-console-css' => '6378ef3d',
     'aphront-dialog-view-css' => '725f2f23',
     'aphront-list-filter-view-css' => '1fb5cab3',
     'aphront-multi-column-view-css' => 'fd18389d',
     'aphront-pager-view-css' => '2e3539af',
     'aphront-panel-view-css' => '8427b78d',
     'aphront-table-view-css' => '59e2c0f8',
     'aphront-tokenizer-control-css' => '86a13f7f',
     'aphront-tooltip-css' => '7672b60f',
     'aphront-two-column-view-css' => '16ab3ad2',
     'aphront-typeahead-control-css' => '0e403212',
-    'auth-css' => '1e655982',
+    'auth-css' => '44975d4b',
     'changeset-view-manager' => '58562350',
+    'conduit-api-css' => '7bc725c4',
     'config-options-css' => '7fedf08b',
     'config-welcome-css' => '6abd79be',
-    'conpherence-durable-column-view' => '2e68a92f',
+    'conpherence-durable-column-view' => '8c43d6ac',
     'conpherence-menu-css' => 'f9f1d143',
-    'conpherence-message-pane-css' => '73631823',
-    'conpherence-notification-css' => 'd208f806',
-    'conpherence-thread-manager' => '6709c934',
-    'conpherence-transaction-css' => '25138b7f',
+    'conpherence-message-pane-css' => '7cbf4cbb',
+    'conpherence-notification-css' => '919974b6',
+    'conpherence-thread-manager' => 'b7342ddb',
+    'conpherence-transaction-css' => '42a457f6',
     'conpherence-update-css' => '1099a660',
     'conpherence-widget-pane-css' => '2af42ebe',
     'differential-changeset-view-css' => 'e19cfd6e',
     'differential-core-view-css' => '7ac3cabc',
-    'differential-inline-comment-editor' => '2529c82d',
+    'differential-inline-comment-editor' => 'd4c87bf4',
     'differential-results-table-css' => '181aa9d9',
     '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' => '63f3ef4a',
     'diffusion-icons-css' => '9c5828da',
     'diffusion-readme-css' => '2106ea08',
     'diffusion-source-css' => '66fdf661',
     'diviner-shared-css' => '38813222',
     'font-fontawesome' => 'e2e712fe',
     'font-source-sans-pro' => '8906c07b',
     'global-drag-and-drop-css' => '697324ad',
     'harbormaster-css' => '49d64eb4',
     'herald-css' => '826075fa',
     'herald-rule-editor' => '9229e764',
     'herald-test-css' => '778b008e',
     'inline-comment-summary-css' => 'eb5f8e8c',
-    'javelin-aphlict' => '30a6303c',
+    'javelin-aphlict' => '5359e785',
     'javelin-behavior' => '61cbc29a',
-    'javelin-behavior-aphlict-dropdown' => '6e1f0cba',
+    'javelin-behavior-aphlict-dropdown' => 'e09f6208',
     'javelin-behavior-aphlict-listen' => 'b1a59974',
     'javelin-behavior-aphlict-status' => 'ea681761',
     'javelin-behavior-aphront-basic-tokenizer' => 'b3a4b884',
     'javelin-behavior-aphront-crop' => 'fa0f4fc2',
     'javelin-behavior-aphront-drag-and-drop-textarea' => '6d49590e',
     '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-choose-control' => '6153c708',
     'javelin-behavior-config-reorder-fields' => '14a827de',
     'javelin-behavior-conpherence-drag-and-drop-photo' => 'cf86d16a',
-    'javelin-behavior-conpherence-menu' => '804b0773',
+    'javelin-behavior-conpherence-menu' => '4351c4a0',
     'javelin-behavior-conpherence-pontificate' => '21ba5861',
     'javelin-behavior-conpherence-widget-pane' => '93568464',
     'javelin-behavior-countdown-timer' => 'e4cc26b3',
     'javelin-behavior-dark-console' => 'f411b6ae',
     'javelin-behavior-dashboard-async-panel' => '469c0d9e',
     'javelin-behavior-dashboard-move-panels' => '82439934',
     'javelin-behavior-dashboard-query-panel-select' => '453c5375',
     'javelin-behavior-dashboard-tab-panel' => 'd4eecc63',
     'javelin-behavior-device' => 'a205cf28',
     'javelin-behavior-differential-add-reviewers-and-ccs' => 'e10f8e18',
     'javelin-behavior-differential-comment-jump' => '4fdb476d',
     'javelin-behavior-differential-diff-radios' => 'e1ff79b1',
     'javelin-behavior-differential-dropdown-menus' => '2035b9cb',
     'javelin-behavior-differential-edit-inline-comments' => 'e723c323',
     'javelin-behavior-differential-feedback-preview' => '8e1389b5',
     'javelin-behavior-differential-keyboard-navigation' => '2c426492',
     'javelin-behavior-differential-populate' => '8694b1df',
     'javelin-behavior-differential-show-field-details' => 'bba9eedf',
     'javelin-behavior-differential-toggle-files' => 'ca3f91eb',
     'javelin-behavior-differential-user-select' => 'a8d8459d',
     'javelin-behavior-diffusion-commit-branches' => 'bdaf4d04',
     'javelin-behavior-diffusion-commit-graph' => '9007c197',
     'javelin-behavior-diffusion-jump-to' => '73d09eef',
     'javelin-behavior-diffusion-locate-file' => '6d3e1947',
     'javelin-behavior-diffusion-pull-lastmodified' => '2b228192',
     'javelin-behavior-doorkeeper-tag' => 'e5822781',
-    'javelin-behavior-durable-column' => '657c2b50',
+    'javelin-behavior-durable-column' => '16c695bf',
     'javelin-behavior-error-log' => '6882e80a',
+    'javelin-behavior-event-all-day' => 'ca5fa62a',
     'javelin-behavior-fancy-datepicker' => '5c0f680f',
     'javelin-behavior-global-drag-and-drop' => 'c8e57404',
     'javelin-behavior-herald-rule-editor' => '7ebaeed3',
     'javelin-behavior-high-security-warning' => 'a464fe03',
     'javelin-behavior-history-install' => '7ee2b591',
     'javelin-behavior-icon-composer' => '8ef9ab58',
     'javelin-behavior-launch-icon-composer' => '48086888',
     'javelin-behavior-lightbox-attachments' => 'f8ba29d7',
     'javelin-behavior-line-chart' => '88f0c5b3',
     'javelin-behavior-load-blame' => '42126667',
     'javelin-behavior-maniphest-batch-editor' => 'f5d1233b',
     'javelin-behavior-maniphest-batch-selector' => '7b98d7c5',
     'javelin-behavior-maniphest-list-editor' => 'a9f88de2',
     'javelin-behavior-maniphest-subpriority-editor' => '84845b5b',
     'javelin-behavior-maniphest-transaction-controls' => '44168bad',
     'javelin-behavior-maniphest-transaction-expand' => '5fefb143',
     'javelin-behavior-maniphest-transaction-preview' => 'f8248bc5',
     'javelin-behavior-owners-path-editor' => '7a68dda3',
     'javelin-behavior-passphrase-credential-control' => '3cb0b2fc',
     'javelin-behavior-persona-login' => '9414ff18',
     'javelin-behavior-phabricator-active-nav' => 'e379b58e',
     'javelin-behavior-phabricator-autofocus' => '7319e029',
     'javelin-behavior-phabricator-busy-example' => '60479091',
     'javelin-behavior-phabricator-file-tree' => '88236f00',
     'javelin-behavior-phabricator-gesture' => '3ab51e2c',
     'javelin-behavior-phabricator-gesture-example' => '558829c2',
     'javelin-behavior-phabricator-hovercards' => 'f36e01af',
     'javelin-behavior-phabricator-keyboard-pager' => 'a8da01f0',
     'javelin-behavior-phabricator-keyboard-shortcuts' => 'd75709e6',
     'javelin-behavior-phabricator-line-linker' => '1499a8cb',
     'javelin-behavior-phabricator-nav' => '14d7a8b8',
     'javelin-behavior-phabricator-notification-example' => '8ce821c5',
     'javelin-behavior-phabricator-object-selector' => '49b73b36',
     'javelin-behavior-phabricator-oncopy' => '2926fff2',
     'javelin-behavior-phabricator-remarkup-assist' => 'e32d14ab',
     'javelin-behavior-phabricator-reveal-content' => '60821bc7',
     'javelin-behavior-phabricator-search-typeahead' => '048330fa',
     'javelin-behavior-phabricator-show-older-transactions' => 'dbbf48b6',
     'javelin-behavior-phabricator-tooltips' => '3ee3408b',
     'javelin-behavior-phabricator-transaction-comment-form' => '9f7309fb',
     'javelin-behavior-phabricator-transaction-list' => '13c739ea',
     'javelin-behavior-phabricator-watch-anchor' => '9f36c42d',
     'javelin-behavior-phame-post-preview' => 'be807912',
     'javelin-behavior-pholio-mock-edit' => '246dc085',
     'javelin-behavior-pholio-mock-view' => 'fbe497e7',
     'javelin-behavior-phui-object-box-tabs' => '2bfa2836',
     'javelin-behavior-phui-timeline-dropdown-menu' => '4d94d9c3',
     'javelin-behavior-policy-control' => '9a340b3d',
     'javelin-behavior-policy-rule-editor' => '5e9f347c',
     'javelin-behavior-ponder-votebox' => '4e9b766b',
-    'javelin-behavior-project-boards' => '60292820',
+    'javelin-behavior-project-boards' => 'ba4fa35c',
     'javelin-behavior-project-create' => '065227cc',
     'javelin-behavior-quicksand-blacklist' => '7927a7d3',
     'javelin-behavior-refresh-csrf' => '7814b593',
     'javelin-behavior-releeph-preview-branch' => 'b2b4fbaf',
     'javelin-behavior-releeph-request-state-change' => 'a0b57eb8',
     'javelin-behavior-releeph-request-typeahead' => 'de2e896f',
     'javelin-behavior-remarkup-preview' => 'f7379f45',
     'javelin-behavior-reorder-applications' => '76b9fc3e',
     'javelin-behavior-reorder-columns' => 'e1d25dfb',
     'javelin-behavior-repository-crossreference' => 'f9539603',
     'javelin-behavior-scrollbar' => '834a1173',
     'javelin-behavior-search-reorder-queries' => 'e9581f08',
     'javelin-behavior-select-on-click' => '4e3e79a6',
     'javelin-behavior-slowvote-embed' => '887ad43f',
     'javelin-behavior-stripe-payment-form' => '3f5d6dbf',
     'javelin-behavior-test-payment-form' => 'fc91ab6c',
     'javelin-behavior-toggle-class' => '5d7c9f33',
     'javelin-behavior-typeahead-browse' => '635de1ec',
     'javelin-behavior-typeahead-search' => '93d0c9e3',
     'javelin-behavior-view-placeholder' => '47830651',
     'javelin-behavior-workflow' => '0a3f3021',
     'javelin-color' => '7e41274a',
     'javelin-cookie' => '62dfea03',
     'javelin-diffusion-locate-file-source' => 'b42eddc7',
     'javelin-dom' => '6f7962d5',
     'javelin-dynval' => 'f6555212',
     'javelin-event' => '85ea0626',
     'javelin-fx' => '54b612ba',
     'javelin-history' => 'd4505101',
     'javelin-install' => '05270951',
     'javelin-json' => '69adf288',
     'javelin-leader' => '331b1611',
     'javelin-magical-init' => '3010e992',
     'javelin-mask' => '8a41885b',
     'javelin-quicksand' => '4cebc641',
     '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' => 'eaa5b321',
+    'javelin-scrollbar' => '087e919c',
     'javelin-sound' => '949c0fe5',
     'javelin-stratcom' => '6c53634d',
     'javelin-tokenizer' => 'ab5f468d',
     'javelin-typeahead' => '70baed2f',
     'javelin-typeahead-composite-source' => '503e17fd',
     'javelin-typeahead-normalizer' => 'e6e25838',
     'javelin-typeahead-ondemand-source' => '8b3fd187',
     'javelin-typeahead-preloaded-source' => '54f314a0',
     'javelin-typeahead-source' => '2818f5ce',
     'javelin-typeahead-static-source' => '316b8fa1',
     'javelin-uri' => '6eff08aa',
     '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' => 'e292eaf4',
     'javelin-workflow' => '5b2e3e2b',
     'lightbox-attachment-css' => '7acac05d',
     'maniphest-batch-editor' => '8f380ebc',
     'maniphest-report-css' => 'f6931fdf',
     'maniphest-task-edit-css' => '8e23031b',
     'maniphest-task-summary-css' => 'ab2fc691',
     'multirow-row-manager' => 'b5d57730',
     'owners-path-editor' => 'aa1733d0',
     'owners-path-editor-css' => '2f00933b',
     'paste-css' => 'eb997ddd',
     'path-typeahead' => 'f7fc67ec',
     'people-profile-css' => '25970776',
     'phabricator-action-list-view-css' => '4f4d09f2',
     'phabricator-application-launch-view-css' => '132f9d14',
     'phabricator-busy' => '59a7976a',
     'phabricator-chatlog-css' => '852140ff',
     'phabricator-content-source-view-css' => '4b8b05d4',
-    'phabricator-core-css' => 'cee2aadb',
+    'phabricator-core-css' => '6230ff55',
     'phabricator-countdown-css' => '86b7b0a0',
     'phabricator-dashboard-css' => 'db1d30b0',
     'phabricator-drag-and-drop-file-upload' => '07de8873',
     'phabricator-draggable-list' => 'a16ec1c6',
     'phabricator-fatal-config-template-css' => '8e6c6fcd',
     'phabricator-feed-css' => 'b513b5f4',
     'phabricator-file-upload' => '477359c8',
     'phabricator-filetree-view-css' => 'fccf9f82',
     'phabricator-flag-css' => '5337623f',
-    'phabricator-hovercard' => '7e8468ae',
+    'phabricator-hovercard' => '14ac66f5',
     'phabricator-hovercard-view-css' => '44394670',
     'phabricator-keyboard-shortcut' => '1ae869f2',
     'phabricator-keyboard-shortcut-manager' => 'c1700f6f',
     'phabricator-main-menu-view' => '1766b04d',
     'phabricator-nav-view-css' => '949b43d9',
     'phabricator-notification' => '0c6946e7',
     'phabricator-notification-css' => '9c279160',
     'phabricator-notification-menu-css' => '3c9d8aa1',
     'phabricator-object-selector-css' => '029a133d',
     'phabricator-phtize' => 'd254d646',
     'phabricator-prefab' => '6920d200',
     'phabricator-profile-css' => '1a20dcbf',
     'phabricator-remarkup-css' => '0037bdbf',
     'phabricator-search-results-css' => '15c71110',
     'phabricator-shaped-request' => '7cbe244b',
     'phabricator-side-menu-view-css' => 'a440478a',
     'phabricator-slowvote-css' => '266df6a1',
     'phabricator-source-code-view-css' => '2ceee894',
-    'phabricator-standard-page-view' => 'dc14c671',
+    'phabricator-standard-page-view' => '062f0f54',
     'phabricator-textareautils' => '5c93c52c',
     'phabricator-title' => 'df5e11d2',
     'phabricator-tooltip' => '1d298e3a',
     'phabricator-ui-example-css' => '528b19de',
     'phabricator-uiexample-javelin-view' => 'd4a14807',
     'phabricator-uiexample-reactor-button' => 'd19198c8',
     'phabricator-uiexample-reactor-checkbox' => '519705ea',
     'phabricator-uiexample-reactor-focus' => '40a6a403',
     'phabricator-uiexample-reactor-input' => '886fd850',
     'phabricator-uiexample-reactor-mouseover' => '47c794d8',
     'phabricator-uiexample-reactor-radio' => '988040b4',
     'phabricator-uiexample-reactor-select' => 'a155550f',
     'phabricator-uiexample-reactor-sendclass' => '1def2711',
     'phabricator-uiexample-reactor-sendproperties' => 'b1f0ccee',
-    'phabricator-zindex-css' => 'ef044fae',
+    'phabricator-zindex-css' => '8c8c40aa',
     'phame-css' => '88bd4705',
     'pholio-css' => '95174bdd',
     'pholio-edit-css' => '3ad9d1ee',
     'pholio-inline-comments-css' => '8e545e49',
     'phortune-credit-card-form' => '2290aeef',
     'phortune-credit-card-form-css' => '8391eb02',
     'phortune-css' => '9149f103',
     'phrequent-css' => 'ffc185ad',
     'phriction-document-css' => '0d16bc9a',
     'phui-action-header-view-css' => 'e4471f43',
     'phui-action-panel-css' => '3ee9afd5',
     'phui-box-css' => 'a5bb366d',
     'phui-button-css' => 'de610129',
     'phui-calendar-css' => '8675968e',
-    'phui-calendar-day-css' => '75b8cc4a',
+    'phui-calendar-day-css' => '38891735',
     'phui-calendar-list-css' => 'c1d0ca59',
-    'phui-calendar-month-css' => 'a92e47d2',
+    'phui-calendar-month-css' => '75e6a2ee',
     'phui-crumbs-view-css' => 'aeff7a21',
     'phui-document-view-css' => '7b564cf6',
     'phui-feed-story-css' => 'c9f3a0b5',
     'phui-font-icon-base-css' => '3dad2ae3',
     'phui-fontkit-css' => '1e71371a',
     'phui-form-css' => 'f535f938',
-    'phui-form-view-css' => 'ddec8479',
-    'phui-header-view-css' => 'a1d2905a',
+    'phui-form-view-css' => 'e1abbe8e',
+    'phui-header-view-css' => '5c0c1c39',
     'phui-icon-view-css' => 'bc766998',
     'phui-image-mask-css' => '5a8b09c8',
     'phui-info-panel-css' => '27ea50a1',
     'phui-info-view-css' => '33595731',
     'phui-inline-comment-view-css' => '2174771a',
     'phui-list-view-css' => '2e25ebfb',
     'phui-object-box-css' => '3a601bc5',
-    'phui-object-item-list-view-css' => '172ea456',
+    'phui-object-item-list-view-css' => 'c259c94f',
     'phui-pinboard-view-css' => 'eaab2b1b',
     'phui-property-list-view-css' => 'd2d143ea',
     'phui-remarkup-preview-css' => '19ad512b',
     'phui-spacing-css' => '042804d6',
     'phui-status-list-view-css' => '888cedb8',
     'phui-tag-view-css' => '402691cc',
     'phui-text-css' => 'cf019f54',
-    'phui-timeline-view-css' => 'b0fbc4d7',
+    'phui-timeline-view-css' => 'a85542c8',
     'phui-workboard-view-css' => '3279cbbf',
     'phui-workpanel-view-css' => 'e495a5cc',
     'phuix-action-list-view' => 'b5c256b8',
     'phuix-action-view' => '8cf6d262',
     'phuix-dropdown-menu' => 'bd4c8dca',
     'policy-css' => '957ea14c',
     'policy-edit-css' => '815c66f7',
     'policy-transaction-detail-css' => '82100a43',
     'ponder-comment-table-css' => '6cdccea7',
     'ponder-feed-view-css' => 'e62615b6',
     'ponder-post-css' => '9d415218',
     'ponder-vote-css' => '8ed6ed8b',
     'project-icon-css' => 'c2ecb7f1',
     'raphael-core' => '51ee6b43',
     'raphael-g' => '40dde778',
     'raphael-g-line' => '40da039e',
     'releeph-core' => '9b3c5733',
     'releeph-preview-branch' => 'b7a6f4a5',
     'releeph-request-differential-create-dialog' => '8d8b92cd',
     'releeph-request-typeahead-css' => '667a48ae',
     'setup-issue-css' => '22270af2',
     'sprite-gradient-css' => '4bdb98a7',
     'sprite-login-css' => 'a3526809',
     'sprite-main-header-css' => '28d01b0b',
     'sprite-menu-css' => '3f052fdc',
     'sprite-projects-css' => 'b0d9e24f',
     'sprite-tokens-css' => '1706b943',
     'syntax-highlighting-css' => '6b7b24d9',
     'tokens-css' => '3d0f239e',
     'typeahead-browse-css' => 'd8581d2c',
     'unhandled-exception-css' => '37d4f9a2',
   ),
   'requires' => array(
     '029a133d' => array(
       'aphront-dialog-view-css',
     ),
     '048330fa' => array(
       'javelin-behavior',
       'javelin-typeahead-ondemand-source',
       'javelin-typeahead',
       'javelin-dom',
       'javelin-uri',
       'javelin-util',
       'javelin-stratcom',
       'phabricator-prefab',
     ),
     '05270951' => array(
       'javelin-util',
       'javelin-magical-init',
     ),
     '065227cc' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-workflow',
     ),
     '07de8873' => array(
       'javelin-install',
       'javelin-util',
       'javelin-request',
       'javelin-dom',
       'javelin-uri',
       'phabricator-file-upload',
     ),
+    '087e919c' => array(
+      'javelin-install',
+      'javelin-dom',
+      'javelin-stratcom',
+      'javelin-vector',
+    ),
     '0a3f3021' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-dom',
       'javelin-router',
     ),
     '0c6946e7' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-util',
       'phabricator-notification-css',
     ),
     '0f764c35' => array(
       'javelin-install',
       'javelin-util',
     ),
     '13c739ea' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-dom',
       'javelin-uri',
       'phabricator-textareautils',
     ),
     '1499a8cb' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-history',
     ),
     '14a827de' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-json',
       'phabricator-draggable-list',
     ),
+    '14ac66f5' => array(
+      'javelin-install',
+      'javelin-dom',
+      'javelin-vector',
+      'javelin-request',
+      'javelin-uri',
+    ),
     '14d7a8b8' => array(
       'javelin-behavior',
       'javelin-behavior-device',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-magical-init',
       'javelin-vector',
       'javelin-request',
       'javelin-util',
     ),
+    '16c695bf' => array(
+      'javelin-behavior',
+      'javelin-dom',
+      'javelin-stratcom',
+      'javelin-behavior-device',
+      'javelin-scrollbar',
+      'javelin-quicksand',
+      'phabricator-keyboard-shortcut',
+      'conpherence-thread-manager',
+    ),
     '1ad0a787' => array(
       'javelin-install',
       'javelin-reactor',
       'javelin-util',
       'javelin-reactor-node-calmer',
     ),
     '1ae869f2' => array(
       'javelin-install',
       'javelin-util',
       'phabricator-keyboard-shortcut-manager',
     ),
     '1d298e3a' => array(
       'javelin-install',
       'javelin-util',
       'javelin-dom',
       'javelin-vector',
     ),
     '1def2711' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-reactor-dom',
     ),
     '2035b9cb' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'javelin-stratcom',
       'javelin-workflow',
       'phuix-dropdown-menu',
       'phuix-action-list-view',
       'phuix-action-view',
       'phabricator-phtize',
       'changeset-view-manager',
     ),
     '21ba5861' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'javelin-workflow',
       'javelin-stratcom',
       'conpherence-thread-manager',
     ),
     '2290aeef' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-json',
       'javelin-workflow',
       'javelin-util',
     ),
     '246dc085' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-workflow',
       'javelin-quicksand',
       'phabricator-phtize',
       'phabricator-drag-and-drop-file-upload',
       'phabricator-draggable-list',
     ),
-    '2529c82d' => array(
-      'javelin-dom',
-      'javelin-util',
-      'javelin-stratcom',
-      'javelin-install',
-      'javelin-request',
-      'javelin-workflow',
-    ),
     '2818f5ce' => array(
       'javelin-install',
       'javelin-util',
       'javelin-dom',
       'javelin-typeahead-normalizer',
     ),
     '2926fff2' => array(
       'javelin-behavior',
       'javelin-dom',
     ),
     '29274e2b' => array(
       'javelin-install',
       'javelin-util',
     ),
     '2b228192' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'javelin-workflow',
       'javelin-json',
     ),
     '2b8de964' => array(
       'javelin-install',
       'javelin-util',
     ),
     '2bfa2836' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
     ),
     '2c426492' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'phabricator-keyboard-shortcut',
     ),
     '2caa8fb8' => array(
       'javelin-install',
       'javelin-event',
     ),
-    '30a6303c' => array(
-      'javelin-install',
-      'javelin-util',
-      'javelin-websocket',
-      'javelin-leader',
-      'javelin-json',
-    ),
     '316b8fa1' => array(
       'javelin-install',
       'javelin-typeahead-source',
     ),
     '331b1611' => array(
       'javelin-install',
     ),
     '3ab51e2c' => array(
       'javelin-behavior',
       'javelin-behavior-device',
       'javelin-stratcom',
       'javelin-vector',
       'javelin-dom',
       'javelin-magical-init',
     ),
     '3cb0b2fc' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-util',
       'javelin-uri',
     ),
     '3ee3408b' => array(
       'javelin-behavior',
       'javelin-behavior-device',
       'javelin-stratcom',
       'phabricator-tooltip',
     ),
     '3f5d6dbf' => array(
       'javelin-behavior',
       'javelin-dom',
       'phortune-credit-card-form',
     ),
     '40a6a403' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-reactor-dom',
     ),
     42126667 => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-request',
     ),
+    '4351c4a0' => 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',
+    ),
     '44168bad' => array(
       'javelin-behavior',
       'javelin-dom',
       'phabricator-prefab',
     ),
     '44959b73' => array(
       'javelin-util',
       'javelin-uri',
       'javelin-install',
     ),
     '453c5375' => array(
       'javelin-behavior',
       'javelin-dom',
     ),
     '469c0d9e' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-workflow',
     ),
     '477359c8' => array(
       'javelin-install',
       'javelin-dom',
       'phabricator-notification',
     ),
     47830651 => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-view-renderer',
       'javelin-install',
     ),
     '47c794d8' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-reactor-dom',
     ),
     48086888 => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-workflow',
     ),
     '49b73b36' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-request',
       'javelin-util',
     ),
     '4cebc641' => array(
       'javelin-install',
     ),
     '4d94d9c3' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'phuix-dropdown-menu',
     ),
     '4e3e79a6' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
     ),
     '4e9b766b' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'javelin-stratcom',
       'javelin-request',
     ),
     '4fdb476d' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
     ),
     '503e17fd' => array(
       'javelin-install',
       'javelin-typeahead-source',
       'javelin-util',
     ),
     '519705ea' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-reactor-dom',
     ),
+    '5359e785' => array(
+      'javelin-install',
+      'javelin-util',
+      'javelin-websocket',
+      'javelin-leader',
+      'javelin-json',
+    ),
     '54b612ba' => array(
       'javelin-color',
       'javelin-install',
       'javelin-util',
     ),
     '54f314a0' => array(
       'javelin-install',
       'javelin-util',
       'javelin-request',
       'javelin-typeahead-source',
     ),
     '558829c2' => array(
       'javelin-stratcom',
       'javelin-behavior',
       'javelin-vector',
       'javelin-dom',
     ),
     58562350 => array(
       'javelin-dom',
       'javelin-util',
       'javelin-stratcom',
       'javelin-install',
       'javelin-workflow',
       'javelin-router',
       'javelin-behavior-device',
       'javelin-vector',
     ),
     '59a7976a' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-fx',
     ),
     '59b251eb' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-vector',
       'javelin-dom',
     ),
     '5b2e3e2b' => array(
       'javelin-stratcom',
       'javelin-request',
       'javelin-dom',
       'javelin-vector',
       'javelin-install',
       'javelin-util',
       'javelin-mask',
       'javelin-uri',
       'javelin-routable',
     ),
     '5c0f680f' => array(
       'javelin-behavior',
       'javelin-util',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-vector',
     ),
     '5c54cbf3' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
     ),
     '5c93c52c' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-vector',
     ),
     '5d7c9f33' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
     ),
     '5e9f347c' => array(
       'javelin-behavior',
       'multirow-row-manager',
       'javelin-dom',
       'javelin-util',
       'phabricator-prefab',
       'javelin-json',
     ),
     '5fefb143' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-workflow',
       'javelin-stratcom',
     ),
-    60292820 => array(
-      'javelin-behavior',
-      'javelin-dom',
-      'javelin-util',
-      'javelin-vector',
-      'javelin-stratcom',
-      'javelin-workflow',
-      'phabricator-draggable-list',
-    ),
     60479091 => array(
       'phabricator-busy',
       'javelin-behavior',
     ),
     '60821bc7' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
     ),
     '6153c708' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-workflow',
     ),
     '61cbc29a' => array(
       'javelin-magical-init',
       'javelin-util',
     ),
     '62dfea03' => array(
       'javelin-install',
       'javelin-util',
     ),
     '635de1ec' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-dom',
     ),
-    '657c2b50' => array(
-      'javelin-behavior',
-      'javelin-dom',
-      'javelin-stratcom',
-      'javelin-behavior-device',
-      'javelin-scrollbar',
-      'javelin-quicksand',
-      'phabricator-keyboard-shortcut',
-      'conpherence-thread-manager',
-    ),
-    '6709c934' => array(
-      'javelin-dom',
-      'javelin-util',
-      'javelin-stratcom',
-      'javelin-install',
-      'javelin-workflow',
-      'javelin-router',
-      'javelin-behavior-device',
-      'javelin-vector',
-    ),
     '6882e80a' => array(
       'javelin-dom',
     ),
     '6920d200' => 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',
     ),
     '69adf288' => array(
       'javelin-install',
     ),
     '6c2b09a2' => array(
       'javelin-install',
       'javelin-util',
     ),
     '6c53634d' => array(
       'javelin-install',
       'javelin-event',
       'javelin-util',
       'javelin-magical-init',
     ),
     '6d3e1947' => array(
       'javelin-behavior',
       'javelin-diffusion-locate-file-source',
       'javelin-dom',
       'javelin-typeahead',
       'javelin-uri',
     ),
     '6d49590e' => array(
       'javelin-behavior',
       'javelin-dom',
       'phabricator-drag-and-drop-file-upload',
       'phabricator-textareautils',
     ),
-    '6e1f0cba' => array(
-      'javelin-behavior',
-      'javelin-request',
-      'javelin-stratcom',
-      'javelin-vector',
-      'javelin-dom',
-      'javelin-uri',
-      'javelin-behavior-device',
-      'phabricator-title',
-    ),
     '6eff08aa' => array(
       'javelin-install',
       'javelin-util',
       'javelin-stratcom',
     ),
     '6f7962d5' => array(
       'javelin-magical-init',
       'javelin-install',
       'javelin-util',
       'javelin-vector',
       'javelin-stratcom',
     ),
     '70baed2f' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-vector',
       'javelin-util',
     ),
     '7319e029' => array(
       'javelin-behavior',
       'javelin-dom',
     ),
     '73d09eef' => array(
       'javelin-behavior',
       'javelin-vector',
       'javelin-dom',
     ),
     '76b9fc3e' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-dom',
       'phabricator-draggable-list',
     ),
     '76f4ebed' => array(
       'javelin-install',
       'javelin-reactor',
       'javelin-util',
     ),
     '7814b593' => array(
       'javelin-request',
       'javelin-behavior',
       'javelin-dom',
       'javelin-router',
       'javelin-util',
       'phabricator-busy',
     ),
     '7927a7d3' => array(
       'javelin-behavior',
       'javelin-quicksand',
     ),
     '7a68dda3' => array(
       'owners-path-editor',
       'javelin-behavior',
     ),
     '7b98d7c5' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-util',
     ),
     '7cbe244b' => array(
       'javelin-install',
       'javelin-util',
       'javelin-request',
       'javelin-router',
     ),
     '7e41274a' => array(
       'javelin-install',
     ),
-    '7e8468ae' => array(
-      'javelin-install',
-      'javelin-dom',
-      'javelin-vector',
-      'javelin-request',
-      'javelin-uri',
-    ),
     '7ebaeed3' => array(
       'herald-rule-editor',
       'javelin-behavior',
     ),
     '7ee2b591' => array(
       'javelin-behavior',
       'javelin-history',
     ),
-    '804b0773' => 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',
-    ),
     82439934 => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'javelin-stratcom',
       'javelin-workflow',
       'phabricator-draggable-list',
     ),
     '834a1173' => array(
       'javelin-behavior',
       'javelin-scrollbar',
     ),
     '84845b5b' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-workflow',
       'phabricator-draggable-list',
     ),
     '85ea0626' => array(
       'javelin-install',
     ),
     '8694b1df' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'phabricator-tooltip',
       'changeset-view-manager',
     ),
     '86a13f7f' => array(
       'aphront-typeahead-control-css',
       'phui-tag-view-css',
     ),
     '88236f00' => array(
       'javelin-behavior',
       'phabricator-keyboard-shortcut',
       'javelin-stratcom',
     ),
     '886fd850' => array(
       'javelin-install',
       'javelin-reactor-dom',
       'javelin-view-html',
       'javelin-view-interpreter',
       'javelin-view-renderer',
     ),
     '887ad43f' => array(
       'javelin-behavior',
       'javelin-request',
       'javelin-stratcom',
       'javelin-dom',
     ),
     '88f0c5b3' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-vector',
     ),
     '8906c07b' => array(
       'phui-fontkit-css',
     ),
     '8a41885b' => array(
       'javelin-install',
       'javelin-dom',
     ),
     '8b3fd187' => array(
       'javelin-install',
       'javelin-util',
       'javelin-request',
       'javelin-typeahead-source',
     ),
     '8ce821c5' => array(
       'phabricator-notification',
       'javelin-stratcom',
       'javelin-behavior',
     ),
     '8cf6d262' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-util',
     ),
     '8e1389b5' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-request',
       'javelin-util',
       'phabricator-shaped-request',
     ),
     '8ef9ab58' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
     ),
     '9007c197' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
     ),
     '9229e764' => array(
       'multirow-row-manager',
       'javelin-install',
       'javelin-util',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-json',
       'phabricator-prefab',
     ),
     93568464 => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-util',
       'phabricator-notification',
       'javelin-behavior-device',
       'phuix-dropdown-menu',
       'phuix-action-list-view',
       'phuix-action-view',
       'conpherence-thread-manager',
     ),
     '93d0c9e3' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-dom',
     ),
     '9414ff18' => array(
       'javelin-behavior',
       'javelin-resource',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-util',
     ),
     '949c0fe5' => array(
       'javelin-install',
     ),
     '94b750d2' => array(
       'javelin-install',
       'javelin-stratcom',
       'javelin-util',
       'javelin-behavior',
       'javelin-json',
       'javelin-dom',
       'javelin-resource',
       'javelin-routable',
     ),
     '988040b4' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-reactor-dom',
     ),
     '9a340b3d' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'phuix-dropdown-menu',
       'phuix-action-list-view',
       'phuix-action-view',
       'javelin-workflow',
     ),
     '9f36c42d' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-vector',
     ),
     '9f7309fb' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'javelin-request',
       'phabricator-shaped-request',
     ),
     'a0b57eb8' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-util',
       'phabricator-keyboard-shortcut',
     ),
     'a155550f' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-reactor-dom',
     ),
     'a16ec1c6' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-util',
       'javelin-vector',
       'javelin-magical-init',
     ),
     'a205cf28' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-vector',
       'javelin-install',
     ),
     'a464fe03' => array(
       'javelin-behavior',
       'javelin-uri',
       'phabricator-notification',
     ),
     'a80d0378' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
     ),
     'a8d8459d' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
     ),
     'a8da01f0' => array(
       'javelin-behavior',
       'javelin-uri',
       'phabricator-keyboard-shortcut',
     ),
     'a9f88de2' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-fx',
       'javelin-util',
     ),
     'aa1733d0' => array(
       'multirow-row-manager',
       'javelin-install',
       'path-typeahead',
       'javelin-dom',
       'javelin-util',
       'phabricator-prefab',
     ),
     'ab5f468d' => array(
       'javelin-dom',
       'javelin-util',
       'javelin-stratcom',
       'javelin-install',
     ),
     'b1a59974' => 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',
     ),
     'b1f0ccee' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-reactor-dom',
     ),
     'b2b4fbaf' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-uri',
       'javelin-request',
     ),
     'b3a4b884' => array(
       'javelin-behavior',
       'phabricator-prefab',
     ),
     'b3e7d692' => array(
       'javelin-install',
     ),
     'b42eddc7' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-typeahead-preloaded-source',
       'javelin-util',
     ),
     'b5c256b8' => array(
       'javelin-install',
       'javelin-dom',
     ),
     'b5d57730' => array(
       'javelin-install',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-util',
     ),
+    'b7342ddb' => array(
+      'javelin-dom',
+      'javelin-util',
+      'javelin-stratcom',
+      'javelin-install',
+      'javelin-aphlict',
+      'javelin-workflow',
+      'javelin-router',
+      'javelin-behavior-device',
+      'javelin-vector',
+    ),
+    'ba4fa35c' => array(
+      'javelin-behavior',
+      'javelin-dom',
+      'javelin-util',
+      'javelin-vector',
+      'javelin-stratcom',
+      'javelin-workflow',
+      'phabricator-draggable-list',
+    ),
     'bba9eedf' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
     ),
     'bd4c8dca' => array(
       'javelin-install',
       'javelin-util',
       'javelin-dom',
       'javelin-vector',
       'javelin-stratcom',
     ),
     'bdaf4d04' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'javelin-request',
     ),
     'be807912' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'phabricator-shaped-request',
     ),
     'c1700f6f' => array(
       'javelin-install',
       'javelin-util',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-vector',
     ),
     'c8e57404' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-uri',
       'javelin-mask',
       'phabricator-drag-and-drop-file-upload',
     ),
     'c90a04fc' => array(
       'javelin-dom',
       'javelin-dynval',
       'javelin-reactor',
       'javelin-reactornode',
       'javelin-install',
       'javelin-util',
     ),
     'ca3f91eb' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'phabricator-phtize',
     ),
     'cf86d16a' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-workflow',
       'phabricator-drag-and-drop-file-upload',
     ),
     'd19198c8' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-util',
       'javelin-dynval',
       'javelin-reactor-dom',
     ),
     'd254d646' => array(
       'javelin-util',
     ),
     'd4505101' => array(
       'javelin-stratcom',
       'javelin-install',
       'javelin-uri',
       'javelin-util',
     ),
     'd4a14807' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-view',
     ),
+    'd4c87bf4' => array(
+      'javelin-dom',
+      'javelin-util',
+      'javelin-stratcom',
+      'javelin-install',
+      'javelin-request',
+      'javelin-workflow',
+    ),
     'd4eecc63' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
     ),
     'd75709e6' => array(
       'javelin-behavior',
       'javelin-workflow',
       'javelin-json',
       'javelin-dom',
       'phabricator-keyboard-shortcut',
     ),
     'd835b03a' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'phabricator-shaped-request',
     ),
     'dbbf48b6' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'phabricator-busy',
     ),
     'de2e896f' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-typeahead',
       'javelin-typeahead-ondemand-source',
       'javelin-dom',
     ),
     'df5e11d2' => array(
       'javelin-install',
     ),
+    'e09f6208' => array(
+      'javelin-behavior',
+      'javelin-request',
+      'javelin-stratcom',
+      'javelin-vector',
+      'javelin-dom',
+      'javelin-uri',
+      'javelin-behavior-device',
+      'phabricator-title',
+    ),
     'e10f8e18' => array(
       'javelin-behavior',
       'javelin-dom',
       'phabricator-prefab',
     ),
     'e19cfd6e' => array(
       'phui-inline-comment-view-css',
     ),
     'e1d25dfb' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-dom',
       'phabricator-draggable-list',
     ),
     'e1ff79b1' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
     ),
     'e292eaf4' => array(
       'javelin-install',
     ),
     'e32d14ab' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'phabricator-phtize',
       'phabricator-textareautils',
       'javelin-workflow',
       'javelin-vector',
     ),
     'e379b58e' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-vector',
       'javelin-dom',
       'javelin-uri',
     ),
     'e4cc26b3' => array(
       'javelin-behavior',
       'javelin-dom',
     ),
     'e5822781' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-json',
       'javelin-workflow',
       'javelin-magical-init',
     ),
     'e6e25838' => array(
       'javelin-install',
     ),
     'e723c323' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-util',
       'javelin-vector',
       'differential-inline-comment-editor',
     ),
     'e9581f08' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-dom',
       'phabricator-draggable-list',
     ),
     'ea681761' => array(
       'javelin-behavior',
       'javelin-aphlict',
       'phabricator-phtize',
       'javelin-dom',
     ),
-    'eaa5b321' => array(
-      'javelin-install',
-      'javelin-dom',
-      'javelin-stratcom',
-      'javelin-vector',
-    ),
     'efe49472' => array(
       'javelin-install',
       'javelin-util',
     ),
     'f36e01af' => array(
       'javelin-behavior',
       'javelin-behavior-device',
       'javelin-stratcom',
       'javelin-vector',
       'phabricator-hovercard',
     ),
     'f411b6ae' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-util',
       'javelin-dom',
       'javelin-request',
       'phabricator-keyboard-shortcut',
     ),
     'f5d1233b' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'phabricator-prefab',
       'multirow-row-manager',
       'javelin-json',
     ),
     'f6555212' => array(
       'javelin-install',
       'javelin-reactornode',
       'javelin-util',
       'javelin-reactor',
     ),
     'f7379f45' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'phabricator-shaped-request',
     ),
     'f7fc67ec' => array(
       'javelin-install',
       'javelin-typeahead',
       'javelin-dom',
       'javelin-request',
       'javelin-typeahead-ondemand-source',
       'javelin-util',
     ),
     'f8248bc5' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'javelin-json',
       'javelin-stratcom',
       'phabricator-shaped-request',
     ),
     'f829edb3' => array(
       'javelin-view',
       'javelin-install',
       'javelin-dom',
     ),
     'f8ba29d7' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-mask',
       'javelin-util',
       'phabricator-busy',
     ),
     'f9539603' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-uri',
     ),
     'fa0f4fc2' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-vector',
       'javelin-magical-init',
     ),
     '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(
     'core.pkg.css' => array(
       'phabricator-core-css',
       'phabricator-zindex-css',
       'phui-button-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',
       'phabricator-remarkup-css',
       'syntax-highlighting-css',
       'aphront-pager-view-css',
       'aphront-tooltip-css',
       'phabricator-flag-css',
       'phui-info-view-css',
       'sprite-gradient-css',
       'sprite-menu-css',
       'phabricator-main-menu-view',
       'phabricator-notification-css',
       'phabricator-notification-menu-css',
       'lightbox-attachment-css',
       'phui-header-view-css',
       'phabricator-filetree-view-css',
       'phabricator-nav-view-css',
       'phabricator-side-menu-view-css',
       'phui-crumbs-view-css',
       'phui-object-item-list-view-css',
       'global-drag-and-drop-css',
       'phui-spacing-css',
       'phui-form-css',
       'phui-icon-view-css',
       'phabricator-application-launch-view-css',
       'phabricator-action-list-view-css',
       'phui-property-list-view-css',
       'phui-tag-view-css',
       'phui-list-view-css',
       'font-fontawesome',
       'phui-font-icon-base-css',
       'sprite-main-header-css',
       'phui-box-css',
       'phui-object-box-css',
       'phui-timeline-view-css',
       'sprite-tokens-css',
       'tokens-css',
       'phui-status-list-view-css',
       'phui-feed-story-css',
       'phabricator-feed-css',
       'phabricator-dashboard-css',
       'aphront-multi-column-view-css',
       'phui-action-header-view-css',
       'conpherence-durable-column-view',
     ),
     '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',
       '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-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',
       'phabricator-hovercard',
       'javelin-behavior-phabricator-hovercards',
       'javelin-color',
       'javelin-fx',
       'phabricator-draggable-list',
       'javelin-behavior-phabricator-transaction-list',
       'javelin-behavior-phabricator-show-older-transactions',
       'javelin-behavior-phui-timeline-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-scrollbar',
       'javelin-behavior-scrollbar',
       'javelin-behavior-durable-column',
       'conpherence-thread-manager',
     ),
     '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-results-table-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',
     ),
     'differential.pkg.js' => array(
       'phabricator-drag-and-drop-file-upload',
       'phabricator-shaped-request',
       'javelin-behavior-differential-feedback-preview',
       'javelin-behavior-differential-edit-inline-comments',
       'javelin-behavior-differential-populate',
       'javelin-behavior-differential-diff-radios',
       'javelin-behavior-differential-comment-jump',
       'javelin-behavior-differential-add-reviewers-and-ccs',
       'javelin-behavior-differential-keyboard-navigation',
       'javelin-behavior-aphront-drag-and-drop-textarea',
       'javelin-behavior-phabricator-object-selector',
       'javelin-behavior-repository-crossreference',
       'javelin-behavior-load-blame',
       'differential-inline-comment-editor',
       'javelin-behavior-differential-dropdown-menus',
       'javelin-behavior-differential-toggle-files',
       'javelin-behavior-differential-user-select',
       'javelin-behavior-aphront-more',
       'changeset-view-manager',
     ),
     '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-transaction-controls',
       'javelin-behavior-maniphest-transaction-preview',
       'javelin-behavior-maniphest-transaction-expand',
       'javelin-behavior-maniphest-subpriority-editor',
       'javelin-behavior-maniphest-list-editor',
     ),
   ),
 );
diff --git a/resources/sql/autopatches/20150507.calendar.1.isallday.sql b/resources/sql/autopatches/20150507.calendar.1.isallday.sql
new file mode 100644
index 0000000000..172015b1be
--- /dev/null
+++ b/resources/sql/autopatches/20150507.calendar.1.isallday.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_calendar.calendar_event
+    ADD isAllDay BOOL NOT NULL;
diff --git a/resources/sql/autopatches/20150513.user.cache.1.sql b/resources/sql/autopatches/20150513.user.cache.1.sql
new file mode 100644
index 0000000000..f6bf6e1e6a
--- /dev/null
+++ b/resources/sql/autopatches/20150513.user.cache.1.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_user.user
+  ADD profileImageCache VARCHAR(255) COLLATE {$COLLATE_TEXT};
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index 8e19b001f3..0fa277df27 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1,6787 +1,6795 @@
 <?php
 
 /**
  * This file is automatically generated. Use 'arc liberate' to rebuild it.
  *
  * @generated
  * @phutil-library-version 2
  */
 phutil_register_library_map(array(
   '__library_version__' => 2,
   'class' => array(
     'AlmanacAddress' => 'applications/almanac/util/AlmanacAddress.php',
     'AlmanacBinding' => 'applications/almanac/storage/AlmanacBinding.php',
     'AlmanacBindingEditController' => 'applications/almanac/controller/AlmanacBindingEditController.php',
     'AlmanacBindingEditor' => 'applications/almanac/editor/AlmanacBindingEditor.php',
     'AlmanacBindingPHIDType' => 'applications/almanac/phid/AlmanacBindingPHIDType.php',
     'AlmanacBindingQuery' => 'applications/almanac/query/AlmanacBindingQuery.php',
     'AlmanacBindingTableView' => 'applications/almanac/view/AlmanacBindingTableView.php',
     'AlmanacBindingTransaction' => 'applications/almanac/storage/AlmanacBindingTransaction.php',
     'AlmanacBindingTransactionQuery' => 'applications/almanac/query/AlmanacBindingTransactionQuery.php',
     'AlmanacBindingViewController' => 'applications/almanac/controller/AlmanacBindingViewController.php',
     'AlmanacClusterDatabaseServiceType' => 'applications/almanac/servicetype/AlmanacClusterDatabaseServiceType.php',
     'AlmanacClusterRepositoryServiceType' => 'applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php',
     'AlmanacClusterServiceType' => 'applications/almanac/servicetype/AlmanacClusterServiceType.php',
     'AlmanacConduitAPIMethod' => 'applications/almanac/conduit/AlmanacConduitAPIMethod.php',
     'AlmanacConsoleController' => 'applications/almanac/controller/AlmanacConsoleController.php',
     'AlmanacController' => 'applications/almanac/controller/AlmanacController.php',
     'AlmanacCoreCustomField' => 'applications/almanac/customfield/AlmanacCoreCustomField.php',
     'AlmanacCreateClusterServicesCapability' => 'applications/almanac/capability/AlmanacCreateClusterServicesCapability.php',
     'AlmanacCreateDevicesCapability' => 'applications/almanac/capability/AlmanacCreateDevicesCapability.php',
     'AlmanacCreateNetworksCapability' => 'applications/almanac/capability/AlmanacCreateNetworksCapability.php',
     'AlmanacCreateServicesCapability' => 'applications/almanac/capability/AlmanacCreateServicesCapability.php',
     'AlmanacCustomField' => 'applications/almanac/customfield/AlmanacCustomField.php',
     'AlmanacCustomServiceType' => 'applications/almanac/servicetype/AlmanacCustomServiceType.php',
     'AlmanacDAO' => 'applications/almanac/storage/AlmanacDAO.php',
     'AlmanacDevice' => 'applications/almanac/storage/AlmanacDevice.php',
     'AlmanacDeviceController' => 'applications/almanac/controller/AlmanacDeviceController.php',
     'AlmanacDeviceEditController' => 'applications/almanac/controller/AlmanacDeviceEditController.php',
     'AlmanacDeviceEditor' => 'applications/almanac/editor/AlmanacDeviceEditor.php',
     'AlmanacDeviceListController' => 'applications/almanac/controller/AlmanacDeviceListController.php',
     'AlmanacDevicePHIDType' => 'applications/almanac/phid/AlmanacDevicePHIDType.php',
     'AlmanacDeviceQuery' => 'applications/almanac/query/AlmanacDeviceQuery.php',
     'AlmanacDeviceSearchEngine' => 'applications/almanac/query/AlmanacDeviceSearchEngine.php',
     'AlmanacDeviceTransaction' => 'applications/almanac/storage/AlmanacDeviceTransaction.php',
     'AlmanacDeviceTransactionQuery' => 'applications/almanac/query/AlmanacDeviceTransactionQuery.php',
     'AlmanacDeviceViewController' => 'applications/almanac/controller/AlmanacDeviceViewController.php',
     'AlmanacInterface' => 'applications/almanac/storage/AlmanacInterface.php',
     'AlmanacInterfaceDatasource' => 'applications/almanac/typeahead/AlmanacInterfaceDatasource.php',
     'AlmanacInterfaceEditController' => 'applications/almanac/controller/AlmanacInterfaceEditController.php',
     'AlmanacInterfacePHIDType' => 'applications/almanac/phid/AlmanacInterfacePHIDType.php',
     'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php',
     'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php',
     'AlmanacKeys' => 'applications/almanac/util/AlmanacKeys.php',
     'AlmanacManagementLockWorkflow' => 'applications/almanac/management/AlmanacManagementLockWorkflow.php',
     'AlmanacManagementRegisterWorkflow' => 'applications/almanac/management/AlmanacManagementRegisterWorkflow.php',
     'AlmanacManagementTrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php',
     'AlmanacManagementUnlockWorkflow' => 'applications/almanac/management/AlmanacManagementUnlockWorkflow.php',
     'AlmanacManagementUntrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php',
     'AlmanacManagementWorkflow' => 'applications/almanac/management/AlmanacManagementWorkflow.php',
     'AlmanacNames' => 'applications/almanac/util/AlmanacNames.php',
     'AlmanacNamesTestCase' => 'applications/almanac/util/__tests__/AlmanacNamesTestCase.php',
     'AlmanacNetwork' => 'applications/almanac/storage/AlmanacNetwork.php',
     'AlmanacNetworkController' => 'applications/almanac/controller/AlmanacNetworkController.php',
     'AlmanacNetworkEditController' => 'applications/almanac/controller/AlmanacNetworkEditController.php',
     'AlmanacNetworkEditor' => 'applications/almanac/editor/AlmanacNetworkEditor.php',
     'AlmanacNetworkListController' => 'applications/almanac/controller/AlmanacNetworkListController.php',
     'AlmanacNetworkPHIDType' => 'applications/almanac/phid/AlmanacNetworkPHIDType.php',
     'AlmanacNetworkQuery' => 'applications/almanac/query/AlmanacNetworkQuery.php',
     'AlmanacNetworkSearchEngine' => 'applications/almanac/query/AlmanacNetworkSearchEngine.php',
     'AlmanacNetworkTransaction' => 'applications/almanac/storage/AlmanacNetworkTransaction.php',
     'AlmanacNetworkTransactionQuery' => 'applications/almanac/query/AlmanacNetworkTransactionQuery.php',
     'AlmanacNetworkViewController' => 'applications/almanac/controller/AlmanacNetworkViewController.php',
     'AlmanacProperty' => 'applications/almanac/storage/AlmanacProperty.php',
     'AlmanacPropertyController' => 'applications/almanac/controller/AlmanacPropertyController.php',
     'AlmanacPropertyDeleteController' => 'applications/almanac/controller/AlmanacPropertyDeleteController.php',
     'AlmanacPropertyEditController' => 'applications/almanac/controller/AlmanacPropertyEditController.php',
     'AlmanacPropertyInterface' => 'applications/almanac/property/AlmanacPropertyInterface.php',
     'AlmanacPropertyQuery' => 'applications/almanac/query/AlmanacPropertyQuery.php',
     'AlmanacQuery' => 'applications/almanac/query/AlmanacQuery.php',
     'AlmanacQueryDevicesConduitAPIMethod' => 'applications/almanac/conduit/AlmanacQueryDevicesConduitAPIMethod.php',
     'AlmanacQueryServicesConduitAPIMethod' => 'applications/almanac/conduit/AlmanacQueryServicesConduitAPIMethod.php',
     'AlmanacSchemaSpec' => 'applications/almanac/storage/AlmanacSchemaSpec.php',
     'AlmanacService' => 'applications/almanac/storage/AlmanacService.php',
     'AlmanacServiceController' => 'applications/almanac/controller/AlmanacServiceController.php',
     'AlmanacServiceDatasource' => 'applications/almanac/typeahead/AlmanacServiceDatasource.php',
     'AlmanacServiceEditController' => 'applications/almanac/controller/AlmanacServiceEditController.php',
     'AlmanacServiceEditor' => 'applications/almanac/editor/AlmanacServiceEditor.php',
     'AlmanacServiceListController' => 'applications/almanac/controller/AlmanacServiceListController.php',
     'AlmanacServicePHIDType' => 'applications/almanac/phid/AlmanacServicePHIDType.php',
     'AlmanacServiceQuery' => 'applications/almanac/query/AlmanacServiceQuery.php',
     'AlmanacServiceSearchEngine' => 'applications/almanac/query/AlmanacServiceSearchEngine.php',
     'AlmanacServiceTransaction' => 'applications/almanac/storage/AlmanacServiceTransaction.php',
     'AlmanacServiceTransactionQuery' => 'applications/almanac/query/AlmanacServiceTransactionQuery.php',
     'AlmanacServiceType' => 'applications/almanac/servicetype/AlmanacServiceType.php',
     'AlmanacServiceViewController' => 'applications/almanac/controller/AlmanacServiceViewController.php',
     'AphlictDropdownDataQuery' => 'applications/aphlict/query/AphlictDropdownDataQuery.php',
     'Aphront304Response' => 'aphront/response/Aphront304Response.php',
     'Aphront400Response' => 'aphront/response/Aphront400Response.php',
     'Aphront403Response' => 'aphront/response/Aphront403Response.php',
     'Aphront404Response' => 'aphront/response/Aphront404Response.php',
     'AphrontAjaxResponse' => 'aphront/response/AphrontAjaxResponse.php',
     'AphrontApplicationConfiguration' => 'aphront/configuration/AphrontApplicationConfiguration.php',
     'AphrontBarView' => 'view/widget/bars/AphrontBarView.php',
     'AphrontCSRFException' => 'aphront/exception/AphrontCSRFException.php',
     'AphrontCalendarEventView' => 'applications/calendar/view/AphrontCalendarEventView.php',
     'AphrontController' => 'aphront/AphrontController.php',
     'AphrontCursorPagerView' => 'view/control/AphrontCursorPagerView.php',
     'AphrontDefaultApplicationConfiguration' => 'aphront/configuration/AphrontDefaultApplicationConfiguration.php',
     'AphrontDialogResponse' => 'aphront/response/AphrontDialogResponse.php',
     'AphrontDialogView' => 'view/AphrontDialogView.php',
     'AphrontException' => 'aphront/exception/AphrontException.php',
     'AphrontFileResponse' => 'aphront/response/AphrontFileResponse.php',
     'AphrontFormCheckboxControl' => 'view/form/control/AphrontFormCheckboxControl.php',
     'AphrontFormChooseButtonControl' => 'view/form/control/AphrontFormChooseButtonControl.php',
     'AphrontFormControl' => 'view/form/control/AphrontFormControl.php',
     'AphrontFormDateControl' => 'view/form/control/AphrontFormDateControl.php',
     'AphrontFormDateControlValue' => 'view/form/control/AphrontFormDateControlValue.php',
     'AphrontFormDividerControl' => 'view/form/control/AphrontFormDividerControl.php',
     'AphrontFormFileControl' => 'view/form/control/AphrontFormFileControl.php',
     'AphrontFormMarkupControl' => 'view/form/control/AphrontFormMarkupControl.php',
     'AphrontFormPasswordControl' => 'view/form/control/AphrontFormPasswordControl.php',
     'AphrontFormPolicyControl' => 'view/form/control/AphrontFormPolicyControl.php',
     'AphrontFormRadioButtonControl' => 'view/form/control/AphrontFormRadioButtonControl.php',
     'AphrontFormRecaptchaControl' => 'view/form/control/AphrontFormRecaptchaControl.php',
     'AphrontFormSelectControl' => 'view/form/control/AphrontFormSelectControl.php',
     'AphrontFormStaticControl' => 'view/form/control/AphrontFormStaticControl.php',
     'AphrontFormSubmitControl' => 'view/form/control/AphrontFormSubmitControl.php',
     'AphrontFormTextAreaControl' => 'view/form/control/AphrontFormTextAreaControl.php',
     'AphrontFormTextControl' => 'view/form/control/AphrontFormTextControl.php',
     'AphrontFormTextWithSubmitControl' => 'view/form/control/AphrontFormTextWithSubmitControl.php',
     'AphrontFormTokenizerControl' => 'view/form/control/AphrontFormTokenizerControl.php',
     'AphrontFormTypeaheadControl' => 'view/form/control/AphrontFormTypeaheadControl.php',
     'AphrontFormView' => 'view/form/AphrontFormView.php',
     'AphrontGlyphBarView' => 'view/widget/bars/AphrontGlyphBarView.php',
     'AphrontHTMLResponse' => 'aphront/response/AphrontHTMLResponse.php',
     'AphrontHTTPProxyResponse' => 'aphront/response/AphrontHTTPProxyResponse.php',
     'AphrontHTTPSink' => 'aphront/sink/AphrontHTTPSink.php',
     'AphrontHTTPSinkTestCase' => 'aphront/sink/__tests__/AphrontHTTPSinkTestCase.php',
     'AphrontIsolatedDatabaseConnectionTestCase' => 'infrastructure/storage/__tests__/AphrontIsolatedDatabaseConnectionTestCase.php',
     'AphrontIsolatedHTTPSink' => 'aphront/sink/AphrontIsolatedHTTPSink.php',
     'AphrontJSONResponse' => 'aphront/response/AphrontJSONResponse.php',
     'AphrontJavelinView' => 'view/AphrontJavelinView.php',
     'AphrontKeyboardShortcutsAvailableView' => 'view/widget/AphrontKeyboardShortcutsAvailableView.php',
     'AphrontListFilterView' => 'view/layout/AphrontListFilterView.php',
     'AphrontMoreView' => 'view/layout/AphrontMoreView.php',
     'AphrontMultiColumnView' => 'view/layout/AphrontMultiColumnView.php',
     'AphrontMySQLDatabaseConnectionTestCase' => 'infrastructure/storage/__tests__/AphrontMySQLDatabaseConnectionTestCase.php',
     'AphrontNullView' => 'view/AphrontNullView.php',
     'AphrontPHPHTTPSink' => 'aphront/sink/AphrontPHPHTTPSink.php',
     'AphrontPageView' => 'view/page/AphrontPageView.php',
     'AphrontPagerView' => 'view/control/AphrontPagerView.php',
     'AphrontPlainTextResponse' => 'aphront/response/AphrontPlainTextResponse.php',
     'AphrontProgressBarView' => 'view/widget/bars/AphrontProgressBarView.php',
     'AphrontProxyResponse' => 'aphront/response/AphrontProxyResponse.php',
     'AphrontRedirectResponse' => 'aphront/response/AphrontRedirectResponse.php',
     'AphrontRedirectResponseTestCase' => 'aphront/response/__tests__/AphrontRedirectResponseTestCase.php',
     'AphrontReloadResponse' => 'aphront/response/AphrontReloadResponse.php',
     'AphrontRequest' => 'aphront/AphrontRequest.php',
     'AphrontRequestTestCase' => 'aphront/__tests__/AphrontRequestTestCase.php',
     'AphrontResponse' => 'aphront/response/AphrontResponse.php',
     'AphrontSideNavFilterView' => 'view/layout/AphrontSideNavFilterView.php',
     'AphrontStackTraceView' => 'view/widget/AphrontStackTraceView.php',
     'AphrontStandaloneHTMLResponse' => 'aphront/response/AphrontStandaloneHTMLResponse.php',
     'AphrontTableView' => 'view/control/AphrontTableView.php',
     'AphrontTagView' => 'view/AphrontTagView.php',
     'AphrontTokenizerTemplateView' => 'view/control/AphrontTokenizerTemplateView.php',
     'AphrontTwoColumnView' => 'view/layout/AphrontTwoColumnView.php',
     'AphrontTypeaheadTemplateView' => 'view/control/AphrontTypeaheadTemplateView.php',
     'AphrontURIMapper' => 'aphront/AphrontURIMapper.php',
     'AphrontUnhandledExceptionResponse' => 'aphront/response/AphrontUnhandledExceptionResponse.php',
     'AphrontUsageException' => 'aphront/exception/AphrontUsageException.php',
     'AphrontView' => 'view/AphrontView.php',
     'AphrontWebpageResponse' => 'aphront/response/AphrontWebpageResponse.php',
     'ArcanistConduitAPIMethod' => 'applications/arcanist/conduit/ArcanistConduitAPIMethod.php',
     'ArcanistProjectInfoConduitAPIMethod' => 'applications/arcanist/conduit/ArcanistProjectInfoConduitAPIMethod.php',
     'AuditConduitAPIMethod' => 'applications/audit/conduit/AuditConduitAPIMethod.php',
     'AuditQueryConduitAPIMethod' => 'applications/audit/conduit/AuditQueryConduitAPIMethod.php',
     'AuthManageProvidersCapability' => 'applications/auth/capability/AuthManageProvidersCapability.php',
     'CalendarColors' => 'applications/calendar/constants/CalendarColors.php',
     'CalendarConstants' => 'applications/calendar/constants/CalendarConstants.php',
     'CalendarTimeUtil' => 'applications/calendar/util/CalendarTimeUtil.php',
     'CalendarTimeUtilTestCase' => 'applications/calendar/__tests__/CalendarTimeUtilTestCase.php',
     'CelerityAPI' => 'applications/celerity/CelerityAPI.php',
     'CelerityManagementMapWorkflow' => 'applications/celerity/management/CelerityManagementMapWorkflow.php',
     'CelerityManagementWorkflow' => 'applications/celerity/management/CelerityManagementWorkflow.php',
     'CelerityPhabricatorResourceController' => 'applications/celerity/controller/CelerityPhabricatorResourceController.php',
     'CelerityPhabricatorResources' => 'applications/celerity/resources/CelerityPhabricatorResources.php',
     'CelerityPhysicalResources' => 'applications/celerity/resources/CelerityPhysicalResources.php',
     'CelerityResourceController' => 'applications/celerity/controller/CelerityResourceController.php',
     'CelerityResourceGraph' => 'applications/celerity/CelerityResourceGraph.php',
     'CelerityResourceMap' => 'applications/celerity/CelerityResourceMap.php',
     'CelerityResourceMapGenerator' => 'applications/celerity/CelerityResourceMapGenerator.php',
     'CelerityResourceTransformer' => 'applications/celerity/CelerityResourceTransformer.php',
     'CelerityResourceTransformerTestCase' => 'applications/celerity/__tests__/CelerityResourceTransformerTestCase.php',
     'CelerityResources' => 'applications/celerity/resources/CelerityResources.php',
     'CelerityResourcesOnDisk' => 'applications/celerity/resources/CelerityResourcesOnDisk.php',
     'CeleritySpriteGenerator' => 'applications/celerity/CeleritySpriteGenerator.php',
     'CelerityStaticResourceResponse' => 'applications/celerity/CelerityStaticResourceResponse.php',
     'ChatLogConduitAPIMethod' => 'applications/chatlog/conduit/ChatLogConduitAPIMethod.php',
     'ChatLogQueryConduitAPIMethod' => 'applications/chatlog/conduit/ChatLogQueryConduitAPIMethod.php',
     'ChatLogRecordConduitAPIMethod' => 'applications/chatlog/conduit/ChatLogRecordConduitAPIMethod.php',
     'ConduitAPIMethod' => 'applications/conduit/method/ConduitAPIMethod.php',
     'ConduitAPIRequest' => 'applications/conduit/protocol/ConduitAPIRequest.php',
     'ConduitAPIResponse' => 'applications/conduit/protocol/ConduitAPIResponse.php',
     'ConduitApplicationNotInstalledException' => 'applications/conduit/protocol/exception/ConduitApplicationNotInstalledException.php',
     'ConduitCall' => 'applications/conduit/call/ConduitCall.php',
     'ConduitCallTestCase' => 'applications/conduit/call/__tests__/ConduitCallTestCase.php',
     'ConduitConnectConduitAPIMethod' => 'applications/conduit/method/ConduitConnectConduitAPIMethod.php',
     'ConduitConnectionGarbageCollector' => 'applications/conduit/garbagecollector/ConduitConnectionGarbageCollector.php',
     'ConduitDeprecatedCallSetupCheck' => 'applications/conduit/check/ConduitDeprecatedCallSetupCheck.php',
     'ConduitException' => 'applications/conduit/protocol/exception/ConduitException.php',
     'ConduitGetCapabilitiesConduitAPIMethod' => 'applications/conduit/method/ConduitGetCapabilitiesConduitAPIMethod.php',
     'ConduitGetCertificateConduitAPIMethod' => 'applications/conduit/method/ConduitGetCertificateConduitAPIMethod.php',
     'ConduitLogGarbageCollector' => 'applications/conduit/garbagecollector/ConduitLogGarbageCollector.php',
     'ConduitMethodDoesNotExistException' => 'applications/conduit/protocol/exception/ConduitMethodDoesNotExistException.php',
     'ConduitMethodNotFoundException' => 'applications/conduit/protocol/exception/ConduitMethodNotFoundException.php',
     'ConduitPingConduitAPIMethod' => 'applications/conduit/method/ConduitPingConduitAPIMethod.php',
     'ConduitQueryConduitAPIMethod' => 'applications/conduit/method/ConduitQueryConduitAPIMethod.php',
     'ConduitSSHWorkflow' => 'applications/conduit/ssh/ConduitSSHWorkflow.php',
     'ConduitTokenGarbageCollector' => 'applications/conduit/garbagecollector/ConduitTokenGarbageCollector.php',
     'ConpherenceColumnViewController' => 'applications/conpherence/controller/ConpherenceColumnViewController.php',
     'ConpherenceConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceConduitAPIMethod.php',
     'ConpherenceConfigOptions' => 'applications/conpherence/config/ConpherenceConfigOptions.php',
     'ConpherenceConstants' => 'applications/conpherence/constants/ConpherenceConstants.php',
     'ConpherenceController' => 'applications/conpherence/controller/ConpherenceController.php',
     'ConpherenceCreateThreadConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceCreateThreadConduitAPIMethod.php',
     'ConpherenceCreateThreadMailReceiver' => 'applications/conpherence/mail/ConpherenceCreateThreadMailReceiver.php',
     'ConpherenceDAO' => 'applications/conpherence/storage/ConpherenceDAO.php',
     'ConpherenceDurableColumnView' => 'applications/conpherence/view/ConpherenceDurableColumnView.php',
     'ConpherenceEditor' => 'applications/conpherence/editor/ConpherenceEditor.php',
     'ConpherenceFileWidgetView' => 'applications/conpherence/view/ConpherenceFileWidgetView.php',
     'ConpherenceFormDragAndDropUploadControl' => 'applications/conpherence/view/ConpherenceFormDragAndDropUploadControl.php',
     'ConpherenceFulltextQuery' => 'applications/conpherence/query/ConpherenceFulltextQuery.php',
     'ConpherenceHovercardEventListener' => 'applications/conpherence/events/ConpherenceHovercardEventListener.php',
     'ConpherenceImageData' => 'applications/conpherence/constants/ConpherenceImageData.php',
     'ConpherenceIndex' => 'applications/conpherence/storage/ConpherenceIndex.php',
     'ConpherenceLayoutView' => 'applications/conpherence/view/ConpherenceLayoutView.php',
     'ConpherenceListController' => 'applications/conpherence/controller/ConpherenceListController.php',
     'ConpherenceMenuItemView' => 'applications/conpherence/view/ConpherenceMenuItemView.php',
     'ConpherenceNewController' => 'applications/conpherence/controller/ConpherenceNewController.php',
     'ConpherenceNewRoomController' => 'applications/conpherence/controller/ConpherenceNewRoomController.php',
     'ConpherenceNotificationPanelController' => 'applications/conpherence/controller/ConpherenceNotificationPanelController.php',
     'ConpherenceParticipant' => 'applications/conpherence/storage/ConpherenceParticipant.php',
     'ConpherenceParticipantCountQuery' => 'applications/conpherence/query/ConpherenceParticipantCountQuery.php',
     'ConpherenceParticipantQuery' => 'applications/conpherence/query/ConpherenceParticipantQuery.php',
     'ConpherenceParticipationStatus' => 'applications/conpherence/constants/ConpherenceParticipationStatus.php',
     'ConpherencePeopleWidgetView' => 'applications/conpherence/view/ConpherencePeopleWidgetView.php',
     'ConpherencePicCropControl' => 'applications/conpherence/view/ConpherencePicCropControl.php',
     'ConpherenceQueryThreadConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceQueryThreadConduitAPIMethod.php',
     'ConpherenceQueryTransactionConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceQueryTransactionConduitAPIMethod.php',
     'ConpherenceReplyHandler' => 'applications/conpherence/mail/ConpherenceReplyHandler.php',
     'ConpherenceRoomListController' => 'applications/conpherence/controller/ConpherenceRoomListController.php',
     'ConpherenceRoomTestCase' => 'applications/conpherence/__tests__/ConpherenceRoomTestCase.php',
     'ConpherenceSchemaSpec' => 'applications/conpherence/storage/ConpherenceSchemaSpec.php',
     'ConpherenceSettings' => 'applications/conpherence/constants/ConpherenceSettings.php',
     'ConpherenceTestCase' => 'applications/conpherence/__tests__/ConpherenceTestCase.php',
     'ConpherenceThread' => 'applications/conpherence/storage/ConpherenceThread.php',
     'ConpherenceThreadIndexer' => 'applications/conpherence/search/ConpherenceThreadIndexer.php',
     'ConpherenceThreadListView' => 'applications/conpherence/view/ConpherenceThreadListView.php',
     'ConpherenceThreadMailReceiver' => 'applications/conpherence/mail/ConpherenceThreadMailReceiver.php',
     'ConpherenceThreadQuery' => 'applications/conpherence/query/ConpherenceThreadQuery.php',
     'ConpherenceThreadRemarkupRule' => 'applications/conpherence/remarkup/ConpherenceThreadRemarkupRule.php',
     'ConpherenceThreadSearchEngine' => 'applications/conpherence/query/ConpherenceThreadSearchEngine.php',
     'ConpherenceThreadTestCase' => 'applications/conpherence/__tests__/ConpherenceThreadTestCase.php',
     'ConpherenceTransaction' => 'applications/conpherence/storage/ConpherenceTransaction.php',
     'ConpherenceTransactionComment' => 'applications/conpherence/storage/ConpherenceTransactionComment.php',
     'ConpherenceTransactionQuery' => 'applications/conpherence/query/ConpherenceTransactionQuery.php',
     'ConpherenceTransactionRenderer' => 'applications/conpherence/ConpherenceTransactionRenderer.php',
     'ConpherenceTransactionType' => 'applications/conpherence/constants/ConpherenceTransactionType.php',
     'ConpherenceTransactionView' => 'applications/conpherence/view/ConpherenceTransactionView.php',
     'ConpherenceUpdateActions' => 'applications/conpherence/constants/ConpherenceUpdateActions.php',
     'ConpherenceUpdateController' => 'applications/conpherence/controller/ConpherenceUpdateController.php',
     'ConpherenceUpdateThreadConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceUpdateThreadConduitAPIMethod.php',
     'ConpherenceViewController' => 'applications/conpherence/controller/ConpherenceViewController.php',
     'ConpherenceWidgetConfigConstants' => 'applications/conpherence/constants/ConpherenceWidgetConfigConstants.php',
     'ConpherenceWidgetController' => 'applications/conpherence/controller/ConpherenceWidgetController.php',
     'ConpherenceWidgetView' => 'applications/conpherence/view/ConpherenceWidgetView.php',
     'DarkConsoleController' => 'applications/console/controller/DarkConsoleController.php',
     'DarkConsoleCore' => 'applications/console/core/DarkConsoleCore.php',
     'DarkConsoleDataController' => 'applications/console/controller/DarkConsoleDataController.php',
     'DarkConsoleErrorLogPlugin' => 'applications/console/plugin/DarkConsoleErrorLogPlugin.php',
     'DarkConsoleErrorLogPluginAPI' => 'applications/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php',
     'DarkConsoleEventPlugin' => 'applications/console/plugin/DarkConsoleEventPlugin.php',
     'DarkConsoleEventPluginAPI' => 'applications/console/plugin/event/DarkConsoleEventPluginAPI.php',
     'DarkConsolePlugin' => 'applications/console/plugin/DarkConsolePlugin.php',
     'DarkConsoleRequestPlugin' => 'applications/console/plugin/DarkConsoleRequestPlugin.php',
     'DarkConsoleServicesPlugin' => 'applications/console/plugin/DarkConsoleServicesPlugin.php',
     'DarkConsoleXHProfPlugin' => 'applications/console/plugin/DarkConsoleXHProfPlugin.php',
     'DarkConsoleXHProfPluginAPI' => 'applications/console/plugin/xhprof/DarkConsoleXHProfPluginAPI.php',
     'DatabaseConfigurationProvider' => 'infrastructure/storage/configuration/DatabaseConfigurationProvider.php',
     'DefaultDatabaseConfigurationProvider' => 'infrastructure/storage/configuration/DefaultDatabaseConfigurationProvider.php',
     'DifferentialAction' => 'applications/differential/constants/DifferentialAction.php',
     'DifferentialActionEmailCommand' => 'applications/differential/command/DifferentialActionEmailCommand.php',
     'DifferentialActionMenuEventListener' => 'applications/differential/event/DifferentialActionMenuEventListener.php',
     'DifferentialAddCommentView' => 'applications/differential/view/DifferentialAddCommentView.php',
+    'DifferentialAdjustmentMapTestCase' => 'applications/differential/storage/__tests__/DifferentialAdjustmentMapTestCase.php',
     'DifferentialAffectedPath' => 'applications/differential/storage/DifferentialAffectedPath.php',
     'DifferentialApplyPatchField' => 'applications/differential/customfield/DifferentialApplyPatchField.php',
     'DifferentialArcanistProjectField' => 'applications/differential/customfield/DifferentialArcanistProjectField.php',
     'DifferentialAsanaRepresentationField' => 'applications/differential/customfield/DifferentialAsanaRepresentationField.php',
     'DifferentialAuditorsField' => 'applications/differential/customfield/DifferentialAuditorsField.php',
     'DifferentialAuthorField' => 'applications/differential/customfield/DifferentialAuthorField.php',
     'DifferentialBlameRevisionField' => 'applications/differential/customfield/DifferentialBlameRevisionField.php',
     'DifferentialBranchField' => 'applications/differential/customfield/DifferentialBranchField.php',
     'DifferentialChangeType' => 'applications/differential/constants/DifferentialChangeType.php',
     'DifferentialChangesSinceLastUpdateField' => 'applications/differential/customfield/DifferentialChangesSinceLastUpdateField.php',
     'DifferentialChangeset' => 'applications/differential/storage/DifferentialChangeset.php',
     'DifferentialChangesetDetailView' => 'applications/differential/view/DifferentialChangesetDetailView.php',
     'DifferentialChangesetFileTreeSideNavBuilder' => 'applications/differential/view/DifferentialChangesetFileTreeSideNavBuilder.php',
     'DifferentialChangesetHTMLRenderer' => 'applications/differential/render/DifferentialChangesetHTMLRenderer.php',
     'DifferentialChangesetListView' => 'applications/differential/view/DifferentialChangesetListView.php',
     'DifferentialChangesetOneUpRenderer' => 'applications/differential/render/DifferentialChangesetOneUpRenderer.php',
     'DifferentialChangesetOneUpTestRenderer' => 'applications/differential/render/DifferentialChangesetOneUpTestRenderer.php',
     'DifferentialChangesetParser' => 'applications/differential/parser/DifferentialChangesetParser.php',
     'DifferentialChangesetParserTestCase' => 'applications/differential/parser/__tests__/DifferentialChangesetParserTestCase.php',
     'DifferentialChangesetQuery' => 'applications/differential/query/DifferentialChangesetQuery.php',
     'DifferentialChangesetRenderer' => 'applications/differential/render/DifferentialChangesetRenderer.php',
     'DifferentialChangesetTestRenderer' => 'applications/differential/render/DifferentialChangesetTestRenderer.php',
     'DifferentialChangesetTwoUpRenderer' => 'applications/differential/render/DifferentialChangesetTwoUpRenderer.php',
     'DifferentialChangesetTwoUpTestRenderer' => 'applications/differential/render/DifferentialChangesetTwoUpTestRenderer.php',
     'DifferentialChangesetViewController' => 'applications/differential/controller/DifferentialChangesetViewController.php',
     'DifferentialCloseConduitAPIMethod' => 'applications/differential/conduit/DifferentialCloseConduitAPIMethod.php',
     'DifferentialCommentPreviewController' => 'applications/differential/controller/DifferentialCommentPreviewController.php',
     'DifferentialCommentSaveController' => 'applications/differential/controller/DifferentialCommentSaveController.php',
     'DifferentialCommitMessageParser' => 'applications/differential/parser/DifferentialCommitMessageParser.php',
     'DifferentialCommitMessageParserTestCase' => 'applications/differential/parser/__tests__/DifferentialCommitMessageParserTestCase.php',
     'DifferentialCommitsField' => 'applications/differential/customfield/DifferentialCommitsField.php',
     'DifferentialConduitAPIMethod' => 'applications/differential/conduit/DifferentialConduitAPIMethod.php',
     'DifferentialConflictsField' => 'applications/differential/customfield/DifferentialConflictsField.php',
     'DifferentialController' => 'applications/differential/controller/DifferentialController.php',
     'DifferentialCoreCustomField' => 'applications/differential/customfield/DifferentialCoreCustomField.php',
     'DifferentialCreateCommentConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateCommentConduitAPIMethod.php',
     'DifferentialCreateDiffConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php',
     'DifferentialCreateInlineConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateInlineConduitAPIMethod.php',
     'DifferentialCreateMailReceiver' => 'applications/differential/mail/DifferentialCreateMailReceiver.php',
     'DifferentialCreateRawDiffConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateRawDiffConduitAPIMethod.php',
     'DifferentialCreateRevisionConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateRevisionConduitAPIMethod.php',
     'DifferentialCustomField' => 'applications/differential/customfield/DifferentialCustomField.php',
     'DifferentialCustomFieldDependsOnParser' => 'applications/differential/parser/DifferentialCustomFieldDependsOnParser.php',
     'DifferentialCustomFieldDependsOnParserTestCase' => 'applications/differential/parser/__tests__/DifferentialCustomFieldDependsOnParserTestCase.php',
     'DifferentialCustomFieldNumericIndex' => 'applications/differential/storage/DifferentialCustomFieldNumericIndex.php',
     'DifferentialCustomFieldRevertsParser' => 'applications/differential/parser/DifferentialCustomFieldRevertsParser.php',
     'DifferentialCustomFieldRevertsParserTestCase' => 'applications/differential/parser/__tests__/DifferentialCustomFieldRevertsParserTestCase.php',
     'DifferentialCustomFieldStorage' => 'applications/differential/storage/DifferentialCustomFieldStorage.php',
     'DifferentialCustomFieldStringIndex' => 'applications/differential/storage/DifferentialCustomFieldStringIndex.php',
     'DifferentialDAO' => 'applications/differential/storage/DifferentialDAO.php',
     'DifferentialDefaultViewCapability' => 'applications/differential/capability/DifferentialDefaultViewCapability.php',
     'DifferentialDependenciesField' => 'applications/differential/customfield/DifferentialDependenciesField.php',
     'DifferentialDependsOnField' => 'applications/differential/customfield/DifferentialDependsOnField.php',
     'DifferentialDiff' => 'applications/differential/storage/DifferentialDiff.php',
     'DifferentialDiffCreateController' => 'applications/differential/controller/DifferentialDiffCreateController.php',
     'DifferentialDiffEditor' => 'applications/differential/editor/DifferentialDiffEditor.php',
     'DifferentialDiffInlineCommentQuery' => 'applications/differential/query/DifferentialDiffInlineCommentQuery.php',
     'DifferentialDiffPHIDType' => 'applications/differential/phid/DifferentialDiffPHIDType.php',
     'DifferentialDiffProperty' => 'applications/differential/storage/DifferentialDiffProperty.php',
     'DifferentialDiffQuery' => 'applications/differential/query/DifferentialDiffQuery.php',
     'DifferentialDiffTableOfContentsView' => 'applications/differential/view/DifferentialDiffTableOfContentsView.php',
     'DifferentialDiffTestCase' => 'applications/differential/storage/__tests__/DifferentialDiffTestCase.php',
     'DifferentialDiffTransaction' => 'applications/differential/storage/DifferentialDiffTransaction.php',
     'DifferentialDiffViewController' => 'applications/differential/controller/DifferentialDiffViewController.php',
     'DifferentialDoorkeeperRevisionFeedStoryPublisher' => 'applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php',
     'DifferentialDraft' => 'applications/differential/storage/DifferentialDraft.php',
     'DifferentialEditPolicyField' => 'applications/differential/customfield/DifferentialEditPolicyField.php',
     'DifferentialFieldParseException' => 'applications/differential/exception/DifferentialFieldParseException.php',
     'DifferentialFieldValidationException' => 'applications/differential/exception/DifferentialFieldValidationException.php',
     'DifferentialFindConduitAPIMethod' => 'applications/differential/conduit/DifferentialFindConduitAPIMethod.php',
     'DifferentialFinishPostponedLintersConduitAPIMethod' => 'applications/differential/conduit/DifferentialFinishPostponedLintersConduitAPIMethod.php',
     'DifferentialGetAllDiffsConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetAllDiffsConduitAPIMethod.php',
     'DifferentialGetCommitMessageConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetCommitMessageConduitAPIMethod.php',
     'DifferentialGetCommitPathsConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetCommitPathsConduitAPIMethod.php',
     'DifferentialGetDiffConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetDiffConduitAPIMethod.php',
     'DifferentialGetRawDiffConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetRawDiffConduitAPIMethod.php',
     'DifferentialGetRevisionCommentsConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetRevisionCommentsConduitAPIMethod.php',
     'DifferentialGetRevisionConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetRevisionConduitAPIMethod.php',
     'DifferentialGetWorkingCopy' => 'applications/differential/DifferentialGetWorkingCopy.php',
     'DifferentialGitHubLandingStrategy' => 'applications/differential/landing/DifferentialGitHubLandingStrategy.php',
     'DifferentialGitSVNIDField' => 'applications/differential/customfield/DifferentialGitSVNIDField.php',
     'DifferentialHostField' => 'applications/differential/customfield/DifferentialHostField.php',
     'DifferentialHostedGitLandingStrategy' => 'applications/differential/landing/DifferentialHostedGitLandingStrategy.php',
     'DifferentialHostedMercurialLandingStrategy' => 'applications/differential/landing/DifferentialHostedMercurialLandingStrategy.php',
     'DifferentialHovercardEventListener' => 'applications/differential/event/DifferentialHovercardEventListener.php',
     'DifferentialHunk' => 'applications/differential/storage/DifferentialHunk.php',
     'DifferentialHunkParser' => 'applications/differential/parser/DifferentialHunkParser.php',
     'DifferentialHunkParserTestCase' => 'applications/differential/parser/__tests__/DifferentialHunkParserTestCase.php',
     'DifferentialHunkQuery' => 'applications/differential/query/DifferentialHunkQuery.php',
     'DifferentialHunkTestCase' => 'applications/differential/storage/__tests__/DifferentialHunkTestCase.php',
     'DifferentialInlineComment' => 'applications/differential/storage/DifferentialInlineComment.php',
     'DifferentialInlineCommentEditController' => 'applications/differential/controller/DifferentialInlineCommentEditController.php',
     'DifferentialInlineCommentPreviewController' => 'applications/differential/controller/DifferentialInlineCommentPreviewController.php',
     'DifferentialInlineCommentQuery' => 'applications/differential/query/DifferentialInlineCommentQuery.php',
     'DifferentialJIRAIssuesField' => 'applications/differential/customfield/DifferentialJIRAIssuesField.php',
     'DifferentialLandingActionMenuEventListener' => 'applications/differential/landing/DifferentialLandingActionMenuEventListener.php',
     'DifferentialLandingStrategy' => 'applications/differential/landing/DifferentialLandingStrategy.php',
     'DifferentialLegacyHunk' => 'applications/differential/storage/DifferentialLegacyHunk.php',
+    'DifferentialLineAdjustmentMap' => 'applications/differential/parser/DifferentialLineAdjustmentMap.php',
     'DifferentialLintField' => 'applications/differential/customfield/DifferentialLintField.php',
     'DifferentialLintStatus' => 'applications/differential/constants/DifferentialLintStatus.php',
     'DifferentialLocalCommitsView' => 'applications/differential/view/DifferentialLocalCommitsView.php',
     'DifferentialManiphestTasksField' => 'applications/differential/customfield/DifferentialManiphestTasksField.php',
     'DifferentialModernHunk' => 'applications/differential/storage/DifferentialModernHunk.php',
     'DifferentialParseCacheGarbageCollector' => 'applications/differential/garbagecollector/DifferentialParseCacheGarbageCollector.php',
     'DifferentialParseCommitMessageConduitAPIMethod' => 'applications/differential/conduit/DifferentialParseCommitMessageConduitAPIMethod.php',
     'DifferentialParseRenderTestCase' => 'applications/differential/__tests__/DifferentialParseRenderTestCase.php',
     'DifferentialPathField' => 'applications/differential/customfield/DifferentialPathField.php',
     'DifferentialPrimaryPaneView' => 'applications/differential/view/DifferentialPrimaryPaneView.php',
     'DifferentialProjectReviewersField' => 'applications/differential/customfield/DifferentialProjectReviewersField.php',
     'DifferentialProjectsField' => 'applications/differential/customfield/DifferentialProjectsField.php',
     'DifferentialQueryConduitAPIMethod' => 'applications/differential/conduit/DifferentialQueryConduitAPIMethod.php',
     'DifferentialQueryDiffsConduitAPIMethod' => 'applications/differential/conduit/DifferentialQueryDiffsConduitAPIMethod.php',
     'DifferentialRawDiffRenderer' => 'applications/differential/render/DifferentialRawDiffRenderer.php',
     'DifferentialReleephRequestFieldSpecification' => 'applications/releeph/differential/DifferentialReleephRequestFieldSpecification.php',
     'DifferentialRemarkupRule' => 'applications/differential/remarkup/DifferentialRemarkupRule.php',
     'DifferentialReplyHandler' => 'applications/differential/mail/DifferentialReplyHandler.php',
     'DifferentialRepositoryField' => 'applications/differential/customfield/DifferentialRepositoryField.php',
     'DifferentialRepositoryLookup' => 'applications/differential/query/DifferentialRepositoryLookup.php',
     'DifferentialRequiredSignaturesField' => 'applications/differential/customfield/DifferentialRequiredSignaturesField.php',
     'DifferentialResultsTableView' => 'applications/differential/view/DifferentialResultsTableView.php',
     'DifferentialRevertPlanField' => 'applications/differential/customfield/DifferentialRevertPlanField.php',
     'DifferentialReviewedByField' => 'applications/differential/customfield/DifferentialReviewedByField.php',
     'DifferentialReviewer' => 'applications/differential/storage/DifferentialReviewer.php',
     'DifferentialReviewerForRevisionEdgeType' => 'applications/differential/edge/DifferentialReviewerForRevisionEdgeType.php',
     'DifferentialReviewerStatus' => 'applications/differential/constants/DifferentialReviewerStatus.php',
     'DifferentialReviewersField' => 'applications/differential/customfield/DifferentialReviewersField.php',
     'DifferentialReviewersView' => 'applications/differential/view/DifferentialReviewersView.php',
     'DifferentialRevision' => 'applications/differential/storage/DifferentialRevision.php',
     'DifferentialRevisionCloseDetailsController' => 'applications/differential/controller/DifferentialRevisionCloseDetailsController.php',
     'DifferentialRevisionControlSystem' => 'applications/differential/constants/DifferentialRevisionControlSystem.php',
     'DifferentialRevisionDependedOnByRevisionEdgeType' => 'applications/differential/edge/DifferentialRevisionDependedOnByRevisionEdgeType.php',
     'DifferentialRevisionDependsOnRevisionEdgeType' => 'applications/differential/edge/DifferentialRevisionDependsOnRevisionEdgeType.php',
     'DifferentialRevisionDetailView' => 'applications/differential/view/DifferentialRevisionDetailView.php',
     'DifferentialRevisionEditController' => 'applications/differential/controller/DifferentialRevisionEditController.php',
     'DifferentialRevisionHasCommitEdgeType' => 'applications/differential/edge/DifferentialRevisionHasCommitEdgeType.php',
     'DifferentialRevisionHasReviewerEdgeType' => 'applications/differential/edge/DifferentialRevisionHasReviewerEdgeType.php',
     'DifferentialRevisionHasTaskEdgeType' => 'applications/differential/edge/DifferentialRevisionHasTaskEdgeType.php',
     'DifferentialRevisionIDField' => 'applications/differential/customfield/DifferentialRevisionIDField.php',
     'DifferentialRevisionLandController' => 'applications/differential/controller/DifferentialRevisionLandController.php',
     'DifferentialRevisionListController' => 'applications/differential/controller/DifferentialRevisionListController.php',
     'DifferentialRevisionListView' => 'applications/differential/view/DifferentialRevisionListView.php',
     'DifferentialRevisionMailReceiver' => 'applications/differential/mail/DifferentialRevisionMailReceiver.php',
     'DifferentialRevisionPHIDType' => 'applications/differential/phid/DifferentialRevisionPHIDType.php',
     'DifferentialRevisionQuery' => 'applications/differential/query/DifferentialRevisionQuery.php',
     'DifferentialRevisionSearchEngine' => 'applications/differential/query/DifferentialRevisionSearchEngine.php',
     'DifferentialRevisionStatus' => 'applications/differential/constants/DifferentialRevisionStatus.php',
     'DifferentialRevisionUpdateHistoryView' => 'applications/differential/view/DifferentialRevisionUpdateHistoryView.php',
     'DifferentialRevisionViewController' => 'applications/differential/controller/DifferentialRevisionViewController.php',
     'DifferentialSchemaSpec' => 'applications/differential/storage/DifferentialSchemaSpec.php',
     'DifferentialSearchIndexer' => 'applications/differential/search/DifferentialSearchIndexer.php',
     'DifferentialSetDiffPropertyConduitAPIMethod' => 'applications/differential/conduit/DifferentialSetDiffPropertyConduitAPIMethod.php',
     'DifferentialStoredCustomField' => 'applications/differential/customfield/DifferentialStoredCustomField.php',
     'DifferentialSubscribersField' => 'applications/differential/customfield/DifferentialSubscribersField.php',
     'DifferentialSummaryField' => 'applications/differential/customfield/DifferentialSummaryField.php',
     'DifferentialTestPlanField' => 'applications/differential/customfield/DifferentialTestPlanField.php',
     'DifferentialTitleField' => 'applications/differential/customfield/DifferentialTitleField.php',
     'DifferentialTransaction' => 'applications/differential/storage/DifferentialTransaction.php',
     'DifferentialTransactionComment' => 'applications/differential/storage/DifferentialTransactionComment.php',
     'DifferentialTransactionEditor' => 'applications/differential/editor/DifferentialTransactionEditor.php',
     'DifferentialTransactionQuery' => 'applications/differential/query/DifferentialTransactionQuery.php',
     'DifferentialTransactionView' => 'applications/differential/view/DifferentialTransactionView.php',
     'DifferentialUnitField' => 'applications/differential/customfield/DifferentialUnitField.php',
     'DifferentialUnitStatus' => 'applications/differential/constants/DifferentialUnitStatus.php',
     'DifferentialUnitTestResult' => 'applications/differential/constants/DifferentialUnitTestResult.php',
     'DifferentialUpdateRevisionConduitAPIMethod' => 'applications/differential/conduit/DifferentialUpdateRevisionConduitAPIMethod.php',
     'DifferentialUpdateUnitResultsConduitAPIMethod' => 'applications/differential/conduit/DifferentialUpdateUnitResultsConduitAPIMethod.php',
     'DifferentialViewPolicyField' => 'applications/differential/customfield/DifferentialViewPolicyField.php',
     'DiffusionArcanistProjectDatasource' => 'applications/diffusion/typeahead/DiffusionArcanistProjectDatasource.php',
     'DiffusionAuditorDatasource' => 'applications/diffusion/typeahead/DiffusionAuditorDatasource.php',
     'DiffusionBranchQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionBranchQueryConduitAPIMethod.php',
     'DiffusionBranchTableController' => 'applications/diffusion/controller/DiffusionBranchTableController.php',
     'DiffusionBranchTableView' => 'applications/diffusion/view/DiffusionBranchTableView.php',
     'DiffusionBrowseController' => 'applications/diffusion/controller/DiffusionBrowseController.php',
     'DiffusionBrowseDirectoryController' => 'applications/diffusion/controller/DiffusionBrowseDirectoryController.php',
     'DiffusionBrowseFileController' => 'applications/diffusion/controller/DiffusionBrowseFileController.php',
     'DiffusionBrowseMainController' => 'applications/diffusion/controller/DiffusionBrowseMainController.php',
     'DiffusionBrowseQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php',
     'DiffusionBrowseResultSet' => 'applications/diffusion/data/DiffusionBrowseResultSet.php',
     'DiffusionBrowseSearchController' => 'applications/diffusion/controller/DiffusionBrowseSearchController.php',
     'DiffusionBrowseTableView' => 'applications/diffusion/view/DiffusionBrowseTableView.php',
     'DiffusionCachedResolveRefsQuery' => 'applications/diffusion/query/DiffusionCachedResolveRefsQuery.php',
     'DiffusionChangeController' => 'applications/diffusion/controller/DiffusionChangeController.php',
     'DiffusionCommitBranchesController' => 'applications/diffusion/controller/DiffusionCommitBranchesController.php',
     'DiffusionCommitChangeTableView' => 'applications/diffusion/view/DiffusionCommitChangeTableView.php',
     'DiffusionCommitController' => 'applications/diffusion/controller/DiffusionCommitController.php',
     'DiffusionCommitEditController' => 'applications/diffusion/controller/DiffusionCommitEditController.php',
     'DiffusionCommitHasRevisionEdgeType' => 'applications/diffusion/edge/DiffusionCommitHasRevisionEdgeType.php',
     'DiffusionCommitHasTaskEdgeType' => 'applications/diffusion/edge/DiffusionCommitHasTaskEdgeType.php',
     'DiffusionCommitHash' => 'applications/diffusion/data/DiffusionCommitHash.php',
     'DiffusionCommitHookEngine' => 'applications/diffusion/engine/DiffusionCommitHookEngine.php',
     'DiffusionCommitHookRejectException' => 'applications/diffusion/exception/DiffusionCommitHookRejectException.php',
     'DiffusionCommitParentsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionCommitParentsQueryConduitAPIMethod.php',
     'DiffusionCommitQuery' => 'applications/diffusion/query/DiffusionCommitQuery.php',
     'DiffusionCommitRef' => 'applications/diffusion/data/DiffusionCommitRef.php',
     'DiffusionCommitRemarkupRule' => 'applications/diffusion/remarkup/DiffusionCommitRemarkupRule.php',
     'DiffusionCommitRemarkupRuleTestCase' => 'applications/diffusion/remarkup/__tests__/DiffusionCommitRemarkupRuleTestCase.php',
     'DiffusionCommitRevertedByCommitEdgeType' => 'applications/diffusion/edge/DiffusionCommitRevertedByCommitEdgeType.php',
     'DiffusionCommitRevertsCommitEdgeType' => 'applications/diffusion/edge/DiffusionCommitRevertsCommitEdgeType.php',
     'DiffusionCommitTagsController' => 'applications/diffusion/controller/DiffusionCommitTagsController.php',
     'DiffusionConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionConduitAPIMethod.php',
     'DiffusionController' => 'applications/diffusion/controller/DiffusionController.php',
     'DiffusionCreateCommentConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionCreateCommentConduitAPIMethod.php',
     'DiffusionCreateRepositoriesCapability' => 'applications/diffusion/capability/DiffusionCreateRepositoriesCapability.php',
     'DiffusionDefaultEditCapability' => 'applications/diffusion/capability/DiffusionDefaultEditCapability.php',
     'DiffusionDefaultPushCapability' => 'applications/diffusion/capability/DiffusionDefaultPushCapability.php',
     'DiffusionDefaultViewCapability' => 'applications/diffusion/capability/DiffusionDefaultViewCapability.php',
     'DiffusionDiffController' => 'applications/diffusion/controller/DiffusionDiffController.php',
     'DiffusionDiffInlineCommentQuery' => 'applications/diffusion/query/DiffusionDiffInlineCommentQuery.php',
     'DiffusionDiffQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionDiffQueryConduitAPIMethod.php',
     'DiffusionDoorkeeperCommitFeedStoryPublisher' => 'applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php',
     'DiffusionEmptyResultView' => 'applications/diffusion/view/DiffusionEmptyResultView.php',
     'DiffusionExistsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionExistsQueryConduitAPIMethod.php',
     'DiffusionExternalController' => 'applications/diffusion/controller/DiffusionExternalController.php',
     'DiffusionFileContent' => 'applications/diffusion/data/DiffusionFileContent.php',
     'DiffusionFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionFileContentQuery.php',
     'DiffusionFileContentQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionFileContentQueryConduitAPIMethod.php',
     'DiffusionFindSymbolsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionFindSymbolsConduitAPIMethod.php',
     'DiffusionGetCommitsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionGetCommitsConduitAPIMethod.php',
     'DiffusionGetLintMessagesConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionGetLintMessagesConduitAPIMethod.php',
     'DiffusionGetRecentCommitsByPathConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionGetRecentCommitsByPathConduitAPIMethod.php',
     'DiffusionGitBranch' => 'applications/diffusion/data/DiffusionGitBranch.php',
     'DiffusionGitBranchTestCase' => 'applications/diffusion/data/__tests__/DiffusionGitBranchTestCase.php',
     'DiffusionGitFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php',
     'DiffusionGitFileContentQueryTestCase' => 'applications/diffusion/query/__tests__/DiffusionGitFileContentQueryTestCase.php',
     'DiffusionGitRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionGitRawDiffQuery.php',
     'DiffusionGitReceivePackSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php',
     'DiffusionGitRequest' => 'applications/diffusion/request/DiffusionGitRequest.php',
     'DiffusionGitResponse' => 'applications/diffusion/response/DiffusionGitResponse.php',
     'DiffusionGitSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitSSHWorkflow.php',
     'DiffusionGitUploadPackSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php',
     'DiffusionHistoryController' => 'applications/diffusion/controller/DiffusionHistoryController.php',
     'DiffusionHistoryQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php',
     'DiffusionHistoryTableView' => 'applications/diffusion/view/DiffusionHistoryTableView.php',
     'DiffusionHovercardEventListener' => 'applications/diffusion/events/DiffusionHovercardEventListener.php',
     'DiffusionInlineCommentController' => 'applications/diffusion/controller/DiffusionInlineCommentController.php',
     'DiffusionInlineCommentPreviewController' => 'applications/diffusion/controller/DiffusionInlineCommentPreviewController.php',
     'DiffusionLastModifiedController' => 'applications/diffusion/controller/DiffusionLastModifiedController.php',
     'DiffusionLastModifiedQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionLastModifiedQueryConduitAPIMethod.php',
     'DiffusionLintController' => 'applications/diffusion/controller/DiffusionLintController.php',
     'DiffusionLintCountQuery' => 'applications/diffusion/query/DiffusionLintCountQuery.php',
     'DiffusionLintDetailsController' => 'applications/diffusion/controller/DiffusionLintDetailsController.php',
     'DiffusionLintSaveRunner' => 'applications/diffusion/DiffusionLintSaveRunner.php',
     'DiffusionLookSoonConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionLookSoonConduitAPIMethod.php',
     'DiffusionLowLevelCommitFieldsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelCommitFieldsQuery.php',
     'DiffusionLowLevelCommitQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelCommitQuery.php',
     'DiffusionLowLevelGitRefQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelGitRefQuery.php',
     'DiffusionLowLevelMercurialBranchesQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelMercurialBranchesQuery.php',
     'DiffusionLowLevelMercurialPathsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelMercurialPathsQuery.php',
     'DiffusionLowLevelParentsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php',
     'DiffusionLowLevelQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelQuery.php',
     'DiffusionLowLevelResolveRefsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php',
     'DiffusionMercurialFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionMercurialFileContentQuery.php',
     'DiffusionMercurialRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionMercurialRawDiffQuery.php',
     'DiffusionMercurialRequest' => 'applications/diffusion/request/DiffusionMercurialRequest.php',
     'DiffusionMercurialResponse' => 'applications/diffusion/response/DiffusionMercurialResponse.php',
     'DiffusionMercurialSSHWorkflow' => 'applications/diffusion/ssh/DiffusionMercurialSSHWorkflow.php',
     'DiffusionMercurialServeSSHWorkflow' => 'applications/diffusion/ssh/DiffusionMercurialServeSSHWorkflow.php',
     'DiffusionMercurialWireClientSSHProtocolChannel' => 'applications/diffusion/ssh/DiffusionMercurialWireClientSSHProtocolChannel.php',
     'DiffusionMercurialWireProtocol' => 'applications/diffusion/protocol/DiffusionMercurialWireProtocol.php',
     'DiffusionMercurialWireSSHTestCase' => 'applications/diffusion/ssh/__tests__/DiffusionMercurialWireSSHTestCase.php',
     'DiffusionMergedCommitsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionMergedCommitsQueryConduitAPIMethod.php',
     'DiffusionMirrorDeleteController' => 'applications/diffusion/controller/DiffusionMirrorDeleteController.php',
     'DiffusionMirrorEditController' => 'applications/diffusion/controller/DiffusionMirrorEditController.php',
     'DiffusionPathChange' => 'applications/diffusion/data/DiffusionPathChange.php',
     'DiffusionPathChangeQuery' => 'applications/diffusion/query/pathchange/DiffusionPathChangeQuery.php',
     'DiffusionPathCompleteController' => 'applications/diffusion/controller/DiffusionPathCompleteController.php',
     'DiffusionPathIDQuery' => 'applications/diffusion/query/pathid/DiffusionPathIDQuery.php',
     'DiffusionPathQuery' => 'applications/diffusion/query/DiffusionPathQuery.php',
     'DiffusionPathQueryTestCase' => 'applications/diffusion/query/pathid/__tests__/DiffusionPathQueryTestCase.php',
     'DiffusionPathTreeController' => 'applications/diffusion/controller/DiffusionPathTreeController.php',
     'DiffusionPathValidateController' => 'applications/diffusion/controller/DiffusionPathValidateController.php',
     'DiffusionPushCapability' => 'applications/diffusion/capability/DiffusionPushCapability.php',
     'DiffusionPushEventViewController' => 'applications/diffusion/controller/DiffusionPushEventViewController.php',
     'DiffusionPushLogController' => 'applications/diffusion/controller/DiffusionPushLogController.php',
     'DiffusionPushLogListController' => 'applications/diffusion/controller/DiffusionPushLogListController.php',
     'DiffusionPushLogListView' => 'applications/diffusion/view/DiffusionPushLogListView.php',
     'DiffusionQuery' => 'applications/diffusion/query/DiffusionQuery.php',
     'DiffusionQueryCommitsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionQueryCommitsConduitAPIMethod.php',
     'DiffusionQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionQueryConduitAPIMethod.php',
     'DiffusionQueryPathsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionQueryPathsConduitAPIMethod.php',
     'DiffusionRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionRawDiffQuery.php',
     'DiffusionRawDiffQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRawDiffQueryConduitAPIMethod.php',
     'DiffusionReadmeView' => 'applications/diffusion/view/DiffusionReadmeView.php',
     'DiffusionRefNotFoundException' => 'applications/diffusion/exception/DiffusionRefNotFoundException.php',
     'DiffusionRefTableController' => 'applications/diffusion/controller/DiffusionRefTableController.php',
     'DiffusionRefsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRefsQueryConduitAPIMethod.php',
     'DiffusionRenameHistoryQuery' => 'applications/diffusion/query/DiffusionRenameHistoryQuery.php',
     'DiffusionRepositoryByIDRemarkupRule' => 'applications/diffusion/remarkup/DiffusionRepositoryByIDRemarkupRule.php',
     'DiffusionRepositoryController' => 'applications/diffusion/controller/DiffusionRepositoryController.php',
     'DiffusionRepositoryCreateController' => 'applications/diffusion/controller/DiffusionRepositoryCreateController.php',
     'DiffusionRepositoryDatasource' => 'applications/diffusion/typeahead/DiffusionRepositoryDatasource.php',
     'DiffusionRepositoryDefaultController' => 'applications/diffusion/controller/DiffusionRepositoryDefaultController.php',
     'DiffusionRepositoryEditActionsController' => 'applications/diffusion/controller/DiffusionRepositoryEditActionsController.php',
     'DiffusionRepositoryEditActivateController' => 'applications/diffusion/controller/DiffusionRepositoryEditActivateController.php',
     'DiffusionRepositoryEditBasicController' => 'applications/diffusion/controller/DiffusionRepositoryEditBasicController.php',
     'DiffusionRepositoryEditBranchesController' => 'applications/diffusion/controller/DiffusionRepositoryEditBranchesController.php',
     'DiffusionRepositoryEditController' => 'applications/diffusion/controller/DiffusionRepositoryEditController.php',
     'DiffusionRepositoryEditDangerousController' => 'applications/diffusion/controller/DiffusionRepositoryEditDangerousController.php',
     'DiffusionRepositoryEditDeleteController' => 'applications/diffusion/controller/DiffusionRepositoryEditDeleteController.php',
     'DiffusionRepositoryEditEncodingController' => 'applications/diffusion/controller/DiffusionRepositoryEditEncodingController.php',
     'DiffusionRepositoryEditHostingController' => 'applications/diffusion/controller/DiffusionRepositoryEditHostingController.php',
     'DiffusionRepositoryEditMainController' => 'applications/diffusion/controller/DiffusionRepositoryEditMainController.php',
     'DiffusionRepositoryEditStorageController' => 'applications/diffusion/controller/DiffusionRepositoryEditStorageController.php',
     'DiffusionRepositoryEditSubversionController' => 'applications/diffusion/controller/DiffusionRepositoryEditSubversionController.php',
     'DiffusionRepositoryEditUpdateController' => 'applications/diffusion/controller/DiffusionRepositoryEditUpdateController.php',
     'DiffusionRepositoryListController' => 'applications/diffusion/controller/DiffusionRepositoryListController.php',
     'DiffusionRepositoryNewController' => 'applications/diffusion/controller/DiffusionRepositoryNewController.php',
     'DiffusionRepositoryPath' => 'applications/diffusion/data/DiffusionRepositoryPath.php',
     'DiffusionRepositoryRef' => 'applications/diffusion/data/DiffusionRepositoryRef.php',
     'DiffusionRepositoryRemarkupRule' => 'applications/diffusion/remarkup/DiffusionRepositoryRemarkupRule.php',
     'DiffusionRepositoryTag' => 'applications/diffusion/data/DiffusionRepositoryTag.php',
     'DiffusionRequest' => 'applications/diffusion/request/DiffusionRequest.php',
     'DiffusionResolveRefsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionResolveRefsConduitAPIMethod.php',
     'DiffusionResolveUserQuery' => 'applications/diffusion/query/DiffusionResolveUserQuery.php',
     'DiffusionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSSHWorkflow.php',
     'DiffusionSearchQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionSearchQueryConduitAPIMethod.php',
     'DiffusionServeController' => 'applications/diffusion/controller/DiffusionServeController.php',
     'DiffusionSetPasswordSettingsPanel' => 'applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php',
     'DiffusionSetupException' => 'applications/diffusion/exception/DiffusionSetupException.php',
     'DiffusionSubversionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSubversionSSHWorkflow.php',
     'DiffusionSubversionServeSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php',
     'DiffusionSubversionWireProtocol' => 'applications/diffusion/protocol/DiffusionSubversionWireProtocol.php',
     'DiffusionSubversionWireProtocolTestCase' => 'applications/diffusion/protocol/__tests__/DiffusionSubversionWireProtocolTestCase.php',
     'DiffusionSvnFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionSvnFileContentQuery.php',
     'DiffusionSvnRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionSvnRawDiffQuery.php',
     'DiffusionSvnRequest' => 'applications/diffusion/request/DiffusionSvnRequest.php',
     'DiffusionSymbolController' => 'applications/diffusion/controller/DiffusionSymbolController.php',
     'DiffusionSymbolDatasource' => 'applications/diffusion/typeahead/DiffusionSymbolDatasource.php',
     'DiffusionSymbolQuery' => 'applications/diffusion/query/DiffusionSymbolQuery.php',
     'DiffusionTagListController' => 'applications/diffusion/controller/DiffusionTagListController.php',
     'DiffusionTagListView' => 'applications/diffusion/view/DiffusionTagListView.php',
     'DiffusionTagsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionTagsQueryConduitAPIMethod.php',
     'DiffusionURITestCase' => 'applications/diffusion/request/__tests__/DiffusionURITestCase.php',
     'DiffusionUpdateCoverageConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionUpdateCoverageConduitAPIMethod.php',
     'DiffusionView' => 'applications/diffusion/view/DiffusionView.php',
     'DivinerArticleAtomizer' => 'applications/diviner/atomizer/DivinerArticleAtomizer.php',
     'DivinerAtom' => 'applications/diviner/atom/DivinerAtom.php',
     'DivinerAtomCache' => 'applications/diviner/cache/DivinerAtomCache.php',
     'DivinerAtomController' => 'applications/diviner/controller/DivinerAtomController.php',
     'DivinerAtomListController' => 'applications/diviner/controller/DivinerAtomListController.php',
     'DivinerAtomPHIDType' => 'applications/diviner/phid/DivinerAtomPHIDType.php',
     'DivinerAtomQuery' => 'applications/diviner/query/DivinerAtomQuery.php',
     'DivinerAtomRef' => 'applications/diviner/atom/DivinerAtomRef.php',
     'DivinerAtomSearchEngine' => 'applications/diviner/query/DivinerAtomSearchEngine.php',
     'DivinerAtomizeWorkflow' => 'applications/diviner/workflow/DivinerAtomizeWorkflow.php',
     'DivinerAtomizer' => 'applications/diviner/atomizer/DivinerAtomizer.php',
     'DivinerBookController' => 'applications/diviner/controller/DivinerBookController.php',
     'DivinerBookItemView' => 'applications/diviner/view/DivinerBookItemView.php',
     'DivinerBookPHIDType' => 'applications/diviner/phid/DivinerBookPHIDType.php',
     'DivinerBookQuery' => 'applications/diviner/query/DivinerBookQuery.php',
     'DivinerController' => 'applications/diviner/controller/DivinerController.php',
     'DivinerDAO' => 'applications/diviner/storage/DivinerDAO.php',
     'DivinerDefaultRenderer' => 'applications/diviner/renderer/DivinerDefaultRenderer.php',
     'DivinerDiskCache' => 'applications/diviner/cache/DivinerDiskCache.php',
     'DivinerFileAtomizer' => 'applications/diviner/atomizer/DivinerFileAtomizer.php',
     'DivinerFindController' => 'applications/diviner/controller/DivinerFindController.php',
     'DivinerGenerateWorkflow' => 'applications/diviner/workflow/DivinerGenerateWorkflow.php',
     'DivinerLiveAtom' => 'applications/diviner/storage/DivinerLiveAtom.php',
     'DivinerLiveBook' => 'applications/diviner/storage/DivinerLiveBook.php',
     'DivinerLivePublisher' => 'applications/diviner/publisher/DivinerLivePublisher.php',
     'DivinerLiveSymbol' => 'applications/diviner/storage/DivinerLiveSymbol.php',
     'DivinerMainController' => 'applications/diviner/controller/DivinerMainController.php',
     'DivinerPHPAtomizer' => 'applications/diviner/atomizer/DivinerPHPAtomizer.php',
     'DivinerParameterTableView' => 'applications/diviner/view/DivinerParameterTableView.php',
     'DivinerPublishCache' => 'applications/diviner/cache/DivinerPublishCache.php',
     'DivinerPublisher' => 'applications/diviner/publisher/DivinerPublisher.php',
     'DivinerRenderer' => 'applications/diviner/renderer/DivinerRenderer.php',
     'DivinerReturnTableView' => 'applications/diviner/view/DivinerReturnTableView.php',
     'DivinerSectionView' => 'applications/diviner/view/DivinerSectionView.php',
     'DivinerStaticPublisher' => 'applications/diviner/publisher/DivinerStaticPublisher.php',
     'DivinerSymbolRemarkupRule' => 'applications/diviner/markup/DivinerSymbolRemarkupRule.php',
     'DivinerWorkflow' => 'applications/diviner/workflow/DivinerWorkflow.php',
     'DoorkeeperAsanaFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperAsanaFeedWorker.php',
     'DoorkeeperBridge' => 'applications/doorkeeper/bridge/DoorkeeperBridge.php',
     'DoorkeeperBridgeAsana' => 'applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php',
     'DoorkeeperBridgeJIRA' => 'applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php',
     'DoorkeeperBridgeJIRATestCase' => 'applications/doorkeeper/bridge/__tests__/DoorkeeperBridgeJIRATestCase.php',
     'DoorkeeperDAO' => 'applications/doorkeeper/storage/DoorkeeperDAO.php',
     'DoorkeeperExternalObject' => 'applications/doorkeeper/storage/DoorkeeperExternalObject.php',
     'DoorkeeperExternalObjectQuery' => 'applications/doorkeeper/query/DoorkeeperExternalObjectQuery.php',
     'DoorkeeperFeedStoryPublisher' => 'applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php',
     'DoorkeeperFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperFeedWorker.php',
     'DoorkeeperImportEngine' => 'applications/doorkeeper/engine/DoorkeeperImportEngine.php',
     'DoorkeeperJIRAFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperJIRAFeedWorker.php',
     'DoorkeeperMissingLinkException' => 'applications/doorkeeper/exception/DoorkeeperMissingLinkException.php',
     'DoorkeeperObjectRef' => 'applications/doorkeeper/engine/DoorkeeperObjectRef.php',
     'DoorkeeperRemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php',
     'DoorkeeperRemarkupRuleAsana' => 'applications/doorkeeper/remarkup/DoorkeeperRemarkupRuleAsana.php',
     'DoorkeeperRemarkupRuleJIRA' => 'applications/doorkeeper/remarkup/DoorkeeperRemarkupRuleJIRA.php',
     'DoorkeeperSchemaSpec' => 'applications/doorkeeper/storage/DoorkeeperSchemaSpec.php',
     'DoorkeeperTagView' => 'applications/doorkeeper/view/DoorkeeperTagView.php',
     'DoorkeeperTagsController' => 'applications/doorkeeper/controller/DoorkeeperTagsController.php',
     'DrydockAllocatorWorker' => 'applications/drydock/worker/DrydockAllocatorWorker.php',
     'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php',
     'DrydockBlueprint' => 'applications/drydock/storage/DrydockBlueprint.php',
     'DrydockBlueprintController' => 'applications/drydock/controller/DrydockBlueprintController.php',
     'DrydockBlueprintCoreCustomField' => 'applications/drydock/customfield/DrydockBlueprintCoreCustomField.php',
     'DrydockBlueprintCreateController' => 'applications/drydock/controller/DrydockBlueprintCreateController.php',
     'DrydockBlueprintCustomField' => 'applications/drydock/customfield/DrydockBlueprintCustomField.php',
     'DrydockBlueprintEditController' => 'applications/drydock/controller/DrydockBlueprintEditController.php',
     'DrydockBlueprintEditor' => 'applications/drydock/editor/DrydockBlueprintEditor.php',
     'DrydockBlueprintImplementation' => 'applications/drydock/blueprint/DrydockBlueprintImplementation.php',
     'DrydockBlueprintListController' => 'applications/drydock/controller/DrydockBlueprintListController.php',
     'DrydockBlueprintPHIDType' => 'applications/drydock/phid/DrydockBlueprintPHIDType.php',
     'DrydockBlueprintQuery' => 'applications/drydock/query/DrydockBlueprintQuery.php',
     'DrydockBlueprintScopeGuard' => 'applications/drydock/util/DrydockBlueprintScopeGuard.php',
     'DrydockBlueprintSearchEngine' => 'applications/drydock/query/DrydockBlueprintSearchEngine.php',
     'DrydockBlueprintTransaction' => 'applications/drydock/storage/DrydockBlueprintTransaction.php',
     'DrydockBlueprintTransactionQuery' => 'applications/drydock/query/DrydockBlueprintTransactionQuery.php',
     'DrydockBlueprintViewController' => 'applications/drydock/controller/DrydockBlueprintViewController.php',
     'DrydockCommandInterface' => 'applications/drydock/interface/command/DrydockCommandInterface.php',
     'DrydockConsoleController' => 'applications/drydock/controller/DrydockConsoleController.php',
     'DrydockConstants' => 'applications/drydock/constants/DrydockConstants.php',
     'DrydockController' => 'applications/drydock/controller/DrydockController.php',
     'DrydockCreateBlueprintsCapability' => 'applications/drydock/capability/DrydockCreateBlueprintsCapability.php',
     'DrydockDAO' => 'applications/drydock/storage/DrydockDAO.php',
     'DrydockDefaultEditCapability' => 'applications/drydock/capability/DrydockDefaultEditCapability.php',
     'DrydockDefaultViewCapability' => 'applications/drydock/capability/DrydockDefaultViewCapability.php',
     'DrydockFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockFilesystemInterface.php',
     'DrydockInterface' => 'applications/drydock/interface/DrydockInterface.php',
     'DrydockLease' => 'applications/drydock/storage/DrydockLease.php',
     'DrydockLeaseController' => 'applications/drydock/controller/DrydockLeaseController.php',
     'DrydockLeaseListController' => 'applications/drydock/controller/DrydockLeaseListController.php',
     'DrydockLeaseListView' => 'applications/drydock/view/DrydockLeaseListView.php',
     'DrydockLeasePHIDType' => 'applications/drydock/phid/DrydockLeasePHIDType.php',
     'DrydockLeaseQuery' => 'applications/drydock/query/DrydockLeaseQuery.php',
     'DrydockLeaseReleaseController' => 'applications/drydock/controller/DrydockLeaseReleaseController.php',
     'DrydockLeaseSearchEngine' => 'applications/drydock/query/DrydockLeaseSearchEngine.php',
     'DrydockLeaseStatus' => 'applications/drydock/constants/DrydockLeaseStatus.php',
     'DrydockLeaseViewController' => 'applications/drydock/controller/DrydockLeaseViewController.php',
     'DrydockLocalCommandInterface' => 'applications/drydock/interface/command/DrydockLocalCommandInterface.php',
     'DrydockLog' => 'applications/drydock/storage/DrydockLog.php',
     'DrydockLogController' => 'applications/drydock/controller/DrydockLogController.php',
     'DrydockLogListController' => 'applications/drydock/controller/DrydockLogListController.php',
     'DrydockLogListView' => 'applications/drydock/view/DrydockLogListView.php',
     'DrydockLogQuery' => 'applications/drydock/query/DrydockLogQuery.php',
     'DrydockLogSearchEngine' => 'applications/drydock/query/DrydockLogSearchEngine.php',
     'DrydockManagementCloseWorkflow' => 'applications/drydock/management/DrydockManagementCloseWorkflow.php',
     'DrydockManagementCreateResourceWorkflow' => 'applications/drydock/management/DrydockManagementCreateResourceWorkflow.php',
     'DrydockManagementLeaseWorkflow' => 'applications/drydock/management/DrydockManagementLeaseWorkflow.php',
     'DrydockManagementReleaseWorkflow' => 'applications/drydock/management/DrydockManagementReleaseWorkflow.php',
     'DrydockManagementWorkflow' => 'applications/drydock/management/DrydockManagementWorkflow.php',
     'DrydockPreallocatedHostBlueprintImplementation' => 'applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php',
     'DrydockQuery' => 'applications/drydock/query/DrydockQuery.php',
     'DrydockResource' => 'applications/drydock/storage/DrydockResource.php',
     'DrydockResourceCloseController' => 'applications/drydock/controller/DrydockResourceCloseController.php',
     'DrydockResourceController' => 'applications/drydock/controller/DrydockResourceController.php',
     'DrydockResourceListController' => 'applications/drydock/controller/DrydockResourceListController.php',
     'DrydockResourceListView' => 'applications/drydock/view/DrydockResourceListView.php',
     'DrydockResourcePHIDType' => 'applications/drydock/phid/DrydockResourcePHIDType.php',
     'DrydockResourceQuery' => 'applications/drydock/query/DrydockResourceQuery.php',
     'DrydockResourceSearchEngine' => 'applications/drydock/query/DrydockResourceSearchEngine.php',
     'DrydockResourceStatus' => 'applications/drydock/constants/DrydockResourceStatus.php',
     'DrydockResourceViewController' => 'applications/drydock/controller/DrydockResourceViewController.php',
     'DrydockSFTPFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockSFTPFilesystemInterface.php',
     'DrydockSSHCommandInterface' => 'applications/drydock/interface/command/DrydockSSHCommandInterface.php',
     'DrydockWebrootInterface' => 'applications/drydock/interface/webroot/DrydockWebrootInterface.php',
     'DrydockWorkingCopyBlueprintImplementation' => 'applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php',
     'FeedConduitAPIMethod' => 'applications/feed/conduit/FeedConduitAPIMethod.php',
     'FeedPublishConduitAPIMethod' => 'applications/feed/conduit/FeedPublishConduitAPIMethod.php',
     'FeedPublisherHTTPWorker' => 'applications/feed/worker/FeedPublisherHTTPWorker.php',
     'FeedPublisherWorker' => 'applications/feed/worker/FeedPublisherWorker.php',
     'FeedPushWorker' => 'applications/feed/worker/FeedPushWorker.php',
     'FeedQueryConduitAPIMethod' => 'applications/feed/conduit/FeedQueryConduitAPIMethod.php',
     'FileAllocateConduitAPIMethod' => 'applications/files/conduit/FileAllocateConduitAPIMethod.php',
     'FileConduitAPIMethod' => 'applications/files/conduit/FileConduitAPIMethod.php',
     'FileCreateMailReceiver' => 'applications/files/mail/FileCreateMailReceiver.php',
     'FileDownloadConduitAPIMethod' => 'applications/files/conduit/FileDownloadConduitAPIMethod.php',
     'FileInfoConduitAPIMethod' => 'applications/files/conduit/FileInfoConduitAPIMethod.php',
     'FileMailReceiver' => 'applications/files/mail/FileMailReceiver.php',
     'FileQueryChunksConduitAPIMethod' => 'applications/files/conduit/FileQueryChunksConduitAPIMethod.php',
     'FileReplyHandler' => 'applications/files/mail/FileReplyHandler.php',
     'FileUploadChunkConduitAPIMethod' => 'applications/files/conduit/FileUploadChunkConduitAPIMethod.php',
     'FileUploadConduitAPIMethod' => 'applications/files/conduit/FileUploadConduitAPIMethod.php',
     'FileUploadHashConduitAPIMethod' => 'applications/files/conduit/FileUploadHashConduitAPIMethod.php',
     'FilesDefaultViewCapability' => 'applications/files/capability/FilesDefaultViewCapability.php',
     'FlagConduitAPIMethod' => 'applications/flag/conduit/FlagConduitAPIMethod.php',
     'FlagDeleteConduitAPIMethod' => 'applications/flag/conduit/FlagDeleteConduitAPIMethod.php',
     'FlagEditConduitAPIMethod' => 'applications/flag/conduit/FlagEditConduitAPIMethod.php',
     'FlagQueryConduitAPIMethod' => 'applications/flag/conduit/FlagQueryConduitAPIMethod.php',
     'FundBacker' => 'applications/fund/storage/FundBacker.php',
     'FundBackerCart' => 'applications/fund/phortune/FundBackerCart.php',
     'FundBackerEditor' => 'applications/fund/editor/FundBackerEditor.php',
     'FundBackerListController' => 'applications/fund/controller/FundBackerListController.php',
     'FundBackerPHIDType' => 'applications/fund/phid/FundBackerPHIDType.php',
     'FundBackerProduct' => 'applications/fund/phortune/FundBackerProduct.php',
     'FundBackerQuery' => 'applications/fund/query/FundBackerQuery.php',
     'FundBackerSearchEngine' => 'applications/fund/query/FundBackerSearchEngine.php',
     'FundBackerTransaction' => 'applications/fund/storage/FundBackerTransaction.php',
     'FundBackerTransactionQuery' => 'applications/fund/query/FundBackerTransactionQuery.php',
     'FundController' => 'applications/fund/controller/FundController.php',
     'FundCreateInitiativesCapability' => 'applications/fund/capability/FundCreateInitiativesCapability.php',
     'FundDAO' => 'applications/fund/storage/FundDAO.php',
     'FundDefaultViewCapability' => 'applications/fund/capability/FundDefaultViewCapability.php',
     'FundInitiative' => 'applications/fund/storage/FundInitiative.php',
     'FundInitiativeBackController' => 'applications/fund/controller/FundInitiativeBackController.php',
     'FundInitiativeCloseController' => 'applications/fund/controller/FundInitiativeCloseController.php',
     'FundInitiativeEditController' => 'applications/fund/controller/FundInitiativeEditController.php',
     'FundInitiativeEditor' => 'applications/fund/editor/FundInitiativeEditor.php',
     'FundInitiativeIndexer' => 'applications/fund/search/FundInitiativeIndexer.php',
     'FundInitiativeListController' => 'applications/fund/controller/FundInitiativeListController.php',
     'FundInitiativePHIDType' => 'applications/fund/phid/FundInitiativePHIDType.php',
     'FundInitiativeQuery' => 'applications/fund/query/FundInitiativeQuery.php',
     'FundInitiativeRemarkupRule' => 'applications/fund/remarkup/FundInitiativeRemarkupRule.php',
     'FundInitiativeReplyHandler' => 'applications/fund/mail/FundInitiativeReplyHandler.php',
     'FundInitiativeSearchEngine' => 'applications/fund/query/FundInitiativeSearchEngine.php',
     'FundInitiativeTransaction' => 'applications/fund/storage/FundInitiativeTransaction.php',
     'FundInitiativeTransactionQuery' => 'applications/fund/query/FundInitiativeTransactionQuery.php',
     'FundInitiativeViewController' => 'applications/fund/controller/FundInitiativeViewController.php',
     'FundSchemaSpec' => 'applications/fund/storage/FundSchemaSpec.php',
     'HarbormasterBuild' => 'applications/harbormaster/storage/build/HarbormasterBuild.php',
     'HarbormasterBuildAbortedException' => 'applications/harbormaster/exception/HarbormasterBuildAbortedException.php',
     'HarbormasterBuildActionController' => 'applications/harbormaster/controller/HarbormasterBuildActionController.php',
     'HarbormasterBuildArtifact' => 'applications/harbormaster/storage/build/HarbormasterBuildArtifact.php',
     'HarbormasterBuildArtifactQuery' => 'applications/harbormaster/query/HarbormasterBuildArtifactQuery.php',
     'HarbormasterBuildCommand' => 'applications/harbormaster/storage/HarbormasterBuildCommand.php',
     'HarbormasterBuildDependencyDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildDependencyDatasource.php',
     'HarbormasterBuildEngine' => 'applications/harbormaster/engine/HarbormasterBuildEngine.php',
     'HarbormasterBuildFailureException' => 'applications/harbormaster/exception/HarbormasterBuildFailureException.php',
     'HarbormasterBuildGraph' => 'applications/harbormaster/engine/HarbormasterBuildGraph.php',
     'HarbormasterBuildItem' => 'applications/harbormaster/storage/build/HarbormasterBuildItem.php',
     'HarbormasterBuildItemPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildItemPHIDType.php',
     'HarbormasterBuildItemQuery' => 'applications/harbormaster/query/HarbormasterBuildItemQuery.php',
     'HarbormasterBuildLog' => 'applications/harbormaster/storage/build/HarbormasterBuildLog.php',
     'HarbormasterBuildLogPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildLogPHIDType.php',
     'HarbormasterBuildLogQuery' => 'applications/harbormaster/query/HarbormasterBuildLogQuery.php',
     'HarbormasterBuildMessage' => 'applications/harbormaster/storage/HarbormasterBuildMessage.php',
     'HarbormasterBuildMessageQuery' => 'applications/harbormaster/query/HarbormasterBuildMessageQuery.php',
     'HarbormasterBuildPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildPHIDType.php',
     'HarbormasterBuildPlan' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php',
     'HarbormasterBuildPlanDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildPlanDatasource.php',
     'HarbormasterBuildPlanEditor' => 'applications/harbormaster/editor/HarbormasterBuildPlanEditor.php',
     'HarbormasterBuildPlanPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildPlanPHIDType.php',
     'HarbormasterBuildPlanQuery' => 'applications/harbormaster/query/HarbormasterBuildPlanQuery.php',
     'HarbormasterBuildPlanSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildPlanSearchEngine.php',
     'HarbormasterBuildPlanTransaction' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlanTransaction.php',
     'HarbormasterBuildPlanTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildPlanTransactionQuery.php',
     'HarbormasterBuildQuery' => 'applications/harbormaster/query/HarbormasterBuildQuery.php',
     'HarbormasterBuildStep' => 'applications/harbormaster/storage/configuration/HarbormasterBuildStep.php',
     'HarbormasterBuildStepCoreCustomField' => 'applications/harbormaster/customfield/HarbormasterBuildStepCoreCustomField.php',
     'HarbormasterBuildStepCustomField' => 'applications/harbormaster/customfield/HarbormasterBuildStepCustomField.php',
     'HarbormasterBuildStepEditor' => 'applications/harbormaster/editor/HarbormasterBuildStepEditor.php',
     'HarbormasterBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterBuildStepImplementation.php',
     'HarbormasterBuildStepPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildStepPHIDType.php',
     'HarbormasterBuildStepQuery' => 'applications/harbormaster/query/HarbormasterBuildStepQuery.php',
     'HarbormasterBuildStepTransaction' => 'applications/harbormaster/storage/configuration/HarbormasterBuildStepTransaction.php',
     'HarbormasterBuildStepTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildStepTransactionQuery.php',
     'HarbormasterBuildTarget' => 'applications/harbormaster/storage/build/HarbormasterBuildTarget.php',
     'HarbormasterBuildTargetPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildTargetPHIDType.php',
     'HarbormasterBuildTargetQuery' => 'applications/harbormaster/query/HarbormasterBuildTargetQuery.php',
     'HarbormasterBuildTransaction' => 'applications/harbormaster/storage/HarbormasterBuildTransaction.php',
     'HarbormasterBuildTransactionEditor' => 'applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php',
     'HarbormasterBuildTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildTransactionQuery.php',
     'HarbormasterBuildViewController' => 'applications/harbormaster/controller/HarbormasterBuildViewController.php',
     'HarbormasterBuildWorker' => 'applications/harbormaster/worker/HarbormasterBuildWorker.php',
     'HarbormasterBuildable' => 'applications/harbormaster/storage/HarbormasterBuildable.php',
     'HarbormasterBuildableActionController' => 'applications/harbormaster/controller/HarbormasterBuildableActionController.php',
     'HarbormasterBuildableInterface' => 'applications/harbormaster/interface/HarbormasterBuildableInterface.php',
     'HarbormasterBuildableListController' => 'applications/harbormaster/controller/HarbormasterBuildableListController.php',
     'HarbormasterBuildablePHIDType' => 'applications/harbormaster/phid/HarbormasterBuildablePHIDType.php',
     'HarbormasterBuildableQuery' => 'applications/harbormaster/query/HarbormasterBuildableQuery.php',
     'HarbormasterBuildableSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildableSearchEngine.php',
     'HarbormasterBuildableTransaction' => 'applications/harbormaster/storage/HarbormasterBuildableTransaction.php',
     'HarbormasterBuildableTransactionEditor' => 'applications/harbormaster/editor/HarbormasterBuildableTransactionEditor.php',
     'HarbormasterBuildableTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildableTransactionQuery.php',
     'HarbormasterBuildableViewController' => 'applications/harbormaster/controller/HarbormasterBuildableViewController.php',
     'HarbormasterCommandBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php',
     'HarbormasterConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php',
     'HarbormasterController' => 'applications/harbormaster/controller/HarbormasterController.php',
     'HarbormasterDAO' => 'applications/harbormaster/storage/HarbormasterDAO.php',
     'HarbormasterHTTPRequestBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php',
     'HarbormasterLeaseHostBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php',
     'HarbormasterManagePlansCapability' => 'applications/harbormaster/capability/HarbormasterManagePlansCapability.php',
     'HarbormasterManagementBuildWorkflow' => 'applications/harbormaster/management/HarbormasterManagementBuildWorkflow.php',
     'HarbormasterManagementUpdateWorkflow' => 'applications/harbormaster/management/HarbormasterManagementUpdateWorkflow.php',
     'HarbormasterManagementWorkflow' => 'applications/harbormaster/management/HarbormasterManagementWorkflow.php',
     'HarbormasterObject' => 'applications/harbormaster/storage/HarbormasterObject.php',
     'HarbormasterPlanController' => 'applications/harbormaster/controller/HarbormasterPlanController.php',
     'HarbormasterPlanDisableController' => 'applications/harbormaster/controller/HarbormasterPlanDisableController.php',
     'HarbormasterPlanEditController' => 'applications/harbormaster/controller/HarbormasterPlanEditController.php',
     'HarbormasterPlanListController' => 'applications/harbormaster/controller/HarbormasterPlanListController.php',
     'HarbormasterPlanRunController' => 'applications/harbormaster/controller/HarbormasterPlanRunController.php',
     'HarbormasterPlanViewController' => 'applications/harbormaster/controller/HarbormasterPlanViewController.php',
     'HarbormasterPublishFragmentBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterPublishFragmentBuildStepImplementation.php',
     'HarbormasterQueryBuildablesConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterQueryBuildablesConduitAPIMethod.php',
     'HarbormasterQueryBuildsConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterQueryBuildsConduitAPIMethod.php',
     'HarbormasterRemarkupRule' => 'applications/harbormaster/remarkup/HarbormasterRemarkupRule.php',
     'HarbormasterSchemaSpec' => 'applications/harbormaster/storage/HarbormasterSchemaSpec.php',
     'HarbormasterScratchTable' => 'applications/harbormaster/storage/HarbormasterScratchTable.php',
     'HarbormasterSendMessageConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterSendMessageConduitAPIMethod.php',
     'HarbormasterSleepBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterSleepBuildStepImplementation.php',
     'HarbormasterStepAddController' => 'applications/harbormaster/controller/HarbormasterStepAddController.php',
     'HarbormasterStepDeleteController' => 'applications/harbormaster/controller/HarbormasterStepDeleteController.php',
     'HarbormasterStepEditController' => 'applications/harbormaster/controller/HarbormasterStepEditController.php',
     'HarbormasterTargetWorker' => 'applications/harbormaster/worker/HarbormasterTargetWorker.php',
     'HarbormasterThrowExceptionBuildStep' => 'applications/harbormaster/step/HarbormasterThrowExceptionBuildStep.php',
     'HarbormasterUIEventListener' => 'applications/harbormaster/event/HarbormasterUIEventListener.php',
     'HarbormasterUploadArtifactBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterUploadArtifactBuildStepImplementation.php',
     'HarbormasterWaitForPreviousBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterWaitForPreviousBuildStepImplementation.php',
     'HarbormasterWorker' => 'applications/harbormaster/worker/HarbormasterWorker.php',
     'HeraldAction' => 'applications/herald/storage/HeraldAction.php',
     'HeraldAdapter' => 'applications/herald/adapter/HeraldAdapter.php',
     'HeraldApplyTranscript' => 'applications/herald/storage/transcript/HeraldApplyTranscript.php',
     'HeraldCommitAdapter' => 'applications/herald/adapter/HeraldCommitAdapter.php',
     'HeraldCondition' => 'applications/herald/storage/HeraldCondition.php',
     'HeraldConditionTranscript' => 'applications/herald/storage/transcript/HeraldConditionTranscript.php',
     'HeraldController' => 'applications/herald/controller/HeraldController.php',
     'HeraldCustomAction' => 'applications/herald/extension/HeraldCustomAction.php',
     'HeraldDAO' => 'applications/herald/storage/HeraldDAO.php',
     'HeraldDifferentialAdapter' => 'applications/herald/adapter/HeraldDifferentialAdapter.php',
     'HeraldDifferentialDiffAdapter' => 'applications/herald/adapter/HeraldDifferentialDiffAdapter.php',
     'HeraldDifferentialRevisionAdapter' => 'applications/herald/adapter/HeraldDifferentialRevisionAdapter.php',
     'HeraldDisableController' => 'applications/herald/controller/HeraldDisableController.php',
     'HeraldEffect' => 'applications/herald/engine/HeraldEffect.php',
     'HeraldEngine' => 'applications/herald/engine/HeraldEngine.php',
     'HeraldInvalidActionException' => 'applications/herald/engine/exception/HeraldInvalidActionException.php',
     'HeraldInvalidConditionException' => 'applications/herald/engine/exception/HeraldInvalidConditionException.php',
     'HeraldManageGlobalRulesCapability' => 'applications/herald/capability/HeraldManageGlobalRulesCapability.php',
     'HeraldManiphestTaskAdapter' => 'applications/herald/adapter/HeraldManiphestTaskAdapter.php',
     'HeraldNewController' => 'applications/herald/controller/HeraldNewController.php',
     'HeraldObjectTranscript' => 'applications/herald/storage/transcript/HeraldObjectTranscript.php',
     'HeraldPholioMockAdapter' => 'applications/herald/adapter/HeraldPholioMockAdapter.php',
     'HeraldPreCommitAdapter' => 'applications/diffusion/herald/HeraldPreCommitAdapter.php',
     'HeraldPreCommitContentAdapter' => 'applications/diffusion/herald/HeraldPreCommitContentAdapter.php',
     'HeraldPreCommitRefAdapter' => 'applications/diffusion/herald/HeraldPreCommitRefAdapter.php',
     'HeraldRecursiveConditionsException' => 'applications/herald/engine/exception/HeraldRecursiveConditionsException.php',
     'HeraldRemarkupRule' => 'applications/herald/remarkup/HeraldRemarkupRule.php',
     'HeraldRepetitionPolicyConfig' => 'applications/herald/config/HeraldRepetitionPolicyConfig.php',
     'HeraldRule' => 'applications/herald/storage/HeraldRule.php',
     'HeraldRuleController' => 'applications/herald/controller/HeraldRuleController.php',
     'HeraldRuleEditor' => 'applications/herald/editor/HeraldRuleEditor.php',
     'HeraldRuleListController' => 'applications/herald/controller/HeraldRuleListController.php',
     'HeraldRulePHIDType' => 'applications/herald/phid/HeraldRulePHIDType.php',
     'HeraldRuleQuery' => 'applications/herald/query/HeraldRuleQuery.php',
     'HeraldRuleSearchEngine' => 'applications/herald/query/HeraldRuleSearchEngine.php',
     'HeraldRuleTestCase' => 'applications/herald/storage/__tests__/HeraldRuleTestCase.php',
     'HeraldRuleTransaction' => 'applications/herald/storage/HeraldRuleTransaction.php',
     'HeraldRuleTransactionComment' => 'applications/herald/storage/HeraldRuleTransactionComment.php',
     'HeraldRuleTranscript' => 'applications/herald/storage/transcript/HeraldRuleTranscript.php',
     'HeraldRuleTypeConfig' => 'applications/herald/config/HeraldRuleTypeConfig.php',
     'HeraldRuleViewController' => 'applications/herald/controller/HeraldRuleViewController.php',
     'HeraldSchemaSpec' => 'applications/herald/storage/HeraldSchemaSpec.php',
     'HeraldTestConsoleController' => 'applications/herald/controller/HeraldTestConsoleController.php',
     'HeraldTransactionQuery' => 'applications/herald/query/HeraldTransactionQuery.php',
     'HeraldTranscript' => 'applications/herald/storage/transcript/HeraldTranscript.php',
     'HeraldTranscriptController' => 'applications/herald/controller/HeraldTranscriptController.php',
     'HeraldTranscriptGarbageCollector' => 'applications/herald/garbagecollector/HeraldTranscriptGarbageCollector.php',
     'HeraldTranscriptListController' => 'applications/herald/controller/HeraldTranscriptListController.php',
     'HeraldTranscriptQuery' => 'applications/herald/query/HeraldTranscriptQuery.php',
     'HeraldTranscriptSearchEngine' => 'applications/herald/query/HeraldTranscriptSearchEngine.php',
     'HeraldTranscriptTestCase' => 'applications/herald/storage/__tests__/HeraldTranscriptTestCase.php',
     'Javelin' => 'infrastructure/javelin/Javelin.php',
     'JavelinReactorUIExample' => 'applications/uiexample/examples/JavelinReactorUIExample.php',
     'JavelinUIExample' => 'applications/uiexample/examples/JavelinUIExample.php',
     'JavelinViewExampleServerView' => 'applications/uiexample/examples/JavelinViewExampleServerView.php',
     'JavelinViewUIExample' => 'applications/uiexample/examples/JavelinViewUIExample.php',
     'LegalpadConstants' => 'applications/legalpad/constants/LegalpadConstants.php',
     'LegalpadController' => 'applications/legalpad/controller/LegalpadController.php',
     'LegalpadCreateDocumentsCapability' => 'applications/legalpad/capability/LegalpadCreateDocumentsCapability.php',
     'LegalpadDAO' => 'applications/legalpad/storage/LegalpadDAO.php',
     'LegalpadDefaultEditCapability' => 'applications/legalpad/capability/LegalpadDefaultEditCapability.php',
     'LegalpadDefaultViewCapability' => 'applications/legalpad/capability/LegalpadDefaultViewCapability.php',
     'LegalpadDocument' => 'applications/legalpad/storage/LegalpadDocument.php',
     'LegalpadDocumentBody' => 'applications/legalpad/storage/LegalpadDocumentBody.php',
     'LegalpadDocumentCommentController' => 'applications/legalpad/controller/LegalpadDocumentCommentController.php',
     'LegalpadDocumentDatasource' => 'applications/legalpad/typeahead/LegalpadDocumentDatasource.php',
     'LegalpadDocumentDoneController' => 'applications/legalpad/controller/LegalpadDocumentDoneController.php',
     'LegalpadDocumentEditController' => 'applications/legalpad/controller/LegalpadDocumentEditController.php',
     'LegalpadDocumentEditor' => 'applications/legalpad/editor/LegalpadDocumentEditor.php',
     'LegalpadDocumentListController' => 'applications/legalpad/controller/LegalpadDocumentListController.php',
     'LegalpadDocumentManageController' => 'applications/legalpad/controller/LegalpadDocumentManageController.php',
     'LegalpadDocumentQuery' => 'applications/legalpad/query/LegalpadDocumentQuery.php',
     'LegalpadDocumentRemarkupRule' => 'applications/legalpad/remarkup/LegalpadDocumentRemarkupRule.php',
     'LegalpadDocumentSearchEngine' => 'applications/legalpad/query/LegalpadDocumentSearchEngine.php',
     'LegalpadDocumentSignController' => 'applications/legalpad/controller/LegalpadDocumentSignController.php',
     'LegalpadDocumentSignature' => 'applications/legalpad/storage/LegalpadDocumentSignature.php',
     'LegalpadDocumentSignatureAddController' => 'applications/legalpad/controller/LegalpadDocumentSignatureAddController.php',
     'LegalpadDocumentSignatureListController' => 'applications/legalpad/controller/LegalpadDocumentSignatureListController.php',
     'LegalpadDocumentSignatureQuery' => 'applications/legalpad/query/LegalpadDocumentSignatureQuery.php',
     'LegalpadDocumentSignatureSearchEngine' => 'applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php',
     'LegalpadDocumentSignatureVerificationController' => 'applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php',
     'LegalpadDocumentSignatureViewController' => 'applications/legalpad/controller/LegalpadDocumentSignatureViewController.php',
     'LegalpadMailReceiver' => 'applications/legalpad/mail/LegalpadMailReceiver.php',
     'LegalpadObjectNeedsSignatureEdgeType' => 'applications/legalpad/edge/LegalpadObjectNeedsSignatureEdgeType.php',
     'LegalpadReplyHandler' => 'applications/legalpad/mail/LegalpadReplyHandler.php',
     'LegalpadSchemaSpec' => 'applications/legalpad/storage/LegalpadSchemaSpec.php',
     'LegalpadSignatureNeededByObjectEdgeType' => 'applications/legalpad/edge/LegalpadSignatureNeededByObjectEdgeType.php',
     'LegalpadTransaction' => 'applications/legalpad/storage/LegalpadTransaction.php',
     'LegalpadTransactionComment' => 'applications/legalpad/storage/LegalpadTransactionComment.php',
     'LegalpadTransactionQuery' => 'applications/legalpad/query/LegalpadTransactionQuery.php',
     'LegalpadTransactionType' => 'applications/legalpad/constants/LegalpadTransactionType.php',
     'LegalpadTransactionView' => 'applications/legalpad/view/LegalpadTransactionView.php',
     'LiskChunkTestCase' => 'infrastructure/storage/lisk/__tests__/LiskChunkTestCase.php',
     'LiskDAO' => 'infrastructure/storage/lisk/LiskDAO.php',
     'LiskDAOSet' => 'infrastructure/storage/lisk/LiskDAOSet.php',
     'LiskDAOTestCase' => 'infrastructure/storage/lisk/__tests__/LiskDAOTestCase.php',
     'LiskEphemeralObjectException' => 'infrastructure/storage/lisk/LiskEphemeralObjectException.php',
     'LiskFixtureTestCase' => 'infrastructure/storage/lisk/__tests__/LiskFixtureTestCase.php',
     'LiskIsolationTestCase' => 'infrastructure/storage/lisk/__tests__/LiskIsolationTestCase.php',
     'LiskIsolationTestDAO' => 'infrastructure/storage/lisk/__tests__/LiskIsolationTestDAO.php',
     'LiskIsolationTestDAOException' => 'infrastructure/storage/lisk/__tests__/LiskIsolationTestDAOException.php',
     'LiskMigrationIterator' => 'infrastructure/storage/lisk/LiskMigrationIterator.php',
     'LiskRawMigrationIterator' => 'infrastructure/storage/lisk/LiskRawMigrationIterator.php',
     'MacroConduitAPIMethod' => 'applications/macro/conduit/MacroConduitAPIMethod.php',
     'MacroCreateMemeConduitAPIMethod' => 'applications/macro/conduit/MacroCreateMemeConduitAPIMethod.php',
     'MacroQueryConduitAPIMethod' => 'applications/macro/conduit/MacroQueryConduitAPIMethod.php',
     'ManiphestAssignEmailCommand' => 'applications/maniphest/command/ManiphestAssignEmailCommand.php',
     'ManiphestAssigneeDatasource' => 'applications/maniphest/typeahead/ManiphestAssigneeDatasource.php',
     'ManiphestBatchEditController' => 'applications/maniphest/controller/ManiphestBatchEditController.php',
     'ManiphestBulkEditCapability' => 'applications/maniphest/capability/ManiphestBulkEditCapability.php',
     'ManiphestClaimEmailCommand' => 'applications/maniphest/command/ManiphestClaimEmailCommand.php',
     'ManiphestCloseEmailCommand' => 'applications/maniphest/command/ManiphestCloseEmailCommand.php',
     'ManiphestConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestConduitAPIMethod.php',
     'ManiphestConfiguredCustomField' => 'applications/maniphest/field/ManiphestConfiguredCustomField.php',
     'ManiphestConstants' => 'applications/maniphest/constants/ManiphestConstants.php',
     'ManiphestController' => 'applications/maniphest/controller/ManiphestController.php',
     'ManiphestCreateMailReceiver' => 'applications/maniphest/mail/ManiphestCreateMailReceiver.php',
     'ManiphestCreateTaskConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestCreateTaskConduitAPIMethod.php',
     'ManiphestCustomField' => 'applications/maniphest/field/ManiphestCustomField.php',
     'ManiphestCustomFieldNumericIndex' => 'applications/maniphest/storage/ManiphestCustomFieldNumericIndex.php',
     'ManiphestCustomFieldStatusParser' => 'applications/maniphest/field/parser/ManiphestCustomFieldStatusParser.php',
     'ManiphestCustomFieldStatusParserTestCase' => 'applications/maniphest/field/parser/__tests__/ManiphestCustomFieldStatusParserTestCase.php',
     'ManiphestCustomFieldStorage' => 'applications/maniphest/storage/ManiphestCustomFieldStorage.php',
     'ManiphestCustomFieldStringIndex' => 'applications/maniphest/storage/ManiphestCustomFieldStringIndex.php',
     'ManiphestDAO' => 'applications/maniphest/storage/ManiphestDAO.php',
     'ManiphestDefaultEditCapability' => 'applications/maniphest/capability/ManiphestDefaultEditCapability.php',
     'ManiphestDefaultViewCapability' => 'applications/maniphest/capability/ManiphestDefaultViewCapability.php',
     'ManiphestEditAssignCapability' => 'applications/maniphest/capability/ManiphestEditAssignCapability.php',
     'ManiphestEditPoliciesCapability' => 'applications/maniphest/capability/ManiphestEditPoliciesCapability.php',
     'ManiphestEditPriorityCapability' => 'applications/maniphest/capability/ManiphestEditPriorityCapability.php',
     'ManiphestEditProjectsCapability' => 'applications/maniphest/capability/ManiphestEditProjectsCapability.php',
     'ManiphestEditStatusCapability' => 'applications/maniphest/capability/ManiphestEditStatusCapability.php',
     'ManiphestEmailCommand' => 'applications/maniphest/command/ManiphestEmailCommand.php',
     'ManiphestExcelDefaultFormat' => 'applications/maniphest/export/ManiphestExcelDefaultFormat.php',
     'ManiphestExcelFormat' => 'applications/maniphest/export/ManiphestExcelFormat.php',
     'ManiphestExportController' => 'applications/maniphest/controller/ManiphestExportController.php',
     'ManiphestGetTaskTransactionsConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php',
     'ManiphestHovercardEventListener' => 'applications/maniphest/event/ManiphestHovercardEventListener.php',
     'ManiphestInfoConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestInfoConduitAPIMethod.php',
     'ManiphestNameIndex' => 'applications/maniphest/storage/ManiphestNameIndex.php',
     'ManiphestNameIndexEventListener' => 'applications/maniphest/event/ManiphestNameIndexEventListener.php',
     'ManiphestPriorityEmailCommand' => 'applications/maniphest/command/ManiphestPriorityEmailCommand.php',
     'ManiphestQueryConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestQueryConduitAPIMethod.php',
     'ManiphestQueryStatusesConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestQueryStatusesConduitAPIMethod.php',
     'ManiphestRemarkupRule' => 'applications/maniphest/remarkup/ManiphestRemarkupRule.php',
     'ManiphestReplyHandler' => 'applications/maniphest/mail/ManiphestReplyHandler.php',
     'ManiphestReportController' => 'applications/maniphest/controller/ManiphestReportController.php',
     'ManiphestSchemaSpec' => 'applications/maniphest/storage/ManiphestSchemaSpec.php',
     'ManiphestSearchIndexer' => 'applications/maniphest/search/ManiphestSearchIndexer.php',
     'ManiphestStatusConfigOptionType' => 'applications/maniphest/config/ManiphestStatusConfigOptionType.php',
     'ManiphestStatusEmailCommand' => 'applications/maniphest/command/ManiphestStatusEmailCommand.php',
     'ManiphestSubpriorityController' => 'applications/maniphest/controller/ManiphestSubpriorityController.php',
     'ManiphestTask' => 'applications/maniphest/storage/ManiphestTask.php',
     'ManiphestTaskClosedStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskClosedStatusDatasource.php',
     'ManiphestTaskDependedOnByTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependedOnByTaskEdgeType.php',
     'ManiphestTaskDependsOnTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependsOnTaskEdgeType.php',
     'ManiphestTaskDetailController' => 'applications/maniphest/controller/ManiphestTaskDetailController.php',
     'ManiphestTaskEditController' => 'applications/maniphest/controller/ManiphestTaskEditController.php',
     'ManiphestTaskHasCommitEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasCommitEdgeType.php',
     'ManiphestTaskHasMockEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasMockEdgeType.php',
     'ManiphestTaskHasRevisionEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasRevisionEdgeType.php',
     'ManiphestTaskListController' => 'applications/maniphest/controller/ManiphestTaskListController.php',
     'ManiphestTaskListView' => 'applications/maniphest/view/ManiphestTaskListView.php',
     'ManiphestTaskMailReceiver' => 'applications/maniphest/mail/ManiphestTaskMailReceiver.php',
     'ManiphestTaskOpenStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskOpenStatusDatasource.php',
     'ManiphestTaskPHIDType' => 'applications/maniphest/phid/ManiphestTaskPHIDType.php',
     'ManiphestTaskPriority' => 'applications/maniphest/constants/ManiphestTaskPriority.php',
     'ManiphestTaskPriorityDatasource' => 'applications/maniphest/typeahead/ManiphestTaskPriorityDatasource.php',
     'ManiphestTaskQuery' => 'applications/maniphest/query/ManiphestTaskQuery.php',
     'ManiphestTaskResultListView' => 'applications/maniphest/view/ManiphestTaskResultListView.php',
     'ManiphestTaskSearchEngine' => 'applications/maniphest/query/ManiphestTaskSearchEngine.php',
     'ManiphestTaskStatus' => 'applications/maniphest/constants/ManiphestTaskStatus.php',
     'ManiphestTaskStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskStatusDatasource.php',
     'ManiphestTaskStatusFunctionDatasource' => 'applications/maniphest/typeahead/ManiphestTaskStatusFunctionDatasource.php',
     'ManiphestTaskStatusTestCase' => 'applications/maniphest/constants/__tests__/ManiphestTaskStatusTestCase.php',
     'ManiphestTaskTestCase' => 'applications/maniphest/__tests__/ManiphestTaskTestCase.php',
     'ManiphestTransaction' => 'applications/maniphest/storage/ManiphestTransaction.php',
     'ManiphestTransactionComment' => 'applications/maniphest/storage/ManiphestTransactionComment.php',
     'ManiphestTransactionEditor' => 'applications/maniphest/editor/ManiphestTransactionEditor.php',
     'ManiphestTransactionPreviewController' => 'applications/maniphest/controller/ManiphestTransactionPreviewController.php',
     'ManiphestTransactionQuery' => 'applications/maniphest/query/ManiphestTransactionQuery.php',
     'ManiphestTransactionSaveController' => 'applications/maniphest/controller/ManiphestTransactionSaveController.php',
     'ManiphestUpdateConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestUpdateConduitAPIMethod.php',
     'ManiphestView' => 'applications/maniphest/view/ManiphestView.php',
     'MetaMTAConstants' => 'applications/metamta/constants/MetaMTAConstants.php',
     'MetaMTAEmailTransactionCommand' => 'applications/metamta/command/MetaMTAEmailTransactionCommand.php',
     'MetaMTAMailReceivedGarbageCollector' => 'applications/metamta/garbagecollector/MetaMTAMailReceivedGarbageCollector.php',
     'MetaMTAMailSentGarbageCollector' => 'applications/metamta/garbagecollector/MetaMTAMailSentGarbageCollector.php',
     'MetaMTANotificationType' => 'applications/metamta/constants/MetaMTANotificationType.php',
     'MetaMTAReceivedMailStatus' => 'applications/metamta/constants/MetaMTAReceivedMailStatus.php',
     'MultimeterContext' => 'applications/multimeter/storage/MultimeterContext.php',
     'MultimeterControl' => 'applications/multimeter/data/MultimeterControl.php',
     'MultimeterController' => 'applications/multimeter/controller/MultimeterController.php',
     'MultimeterDAO' => 'applications/multimeter/storage/MultimeterDAO.php',
     'MultimeterDimension' => 'applications/multimeter/storage/MultimeterDimension.php',
     'MultimeterEvent' => 'applications/multimeter/storage/MultimeterEvent.php',
     'MultimeterEventGarbageCollector' => 'applications/multimeter/garbagecollector/MultimeterEventGarbageCollector.php',
     'MultimeterHost' => 'applications/multimeter/storage/MultimeterHost.php',
     'MultimeterLabel' => 'applications/multimeter/storage/MultimeterLabel.php',
     'MultimeterSampleController' => 'applications/multimeter/controller/MultimeterSampleController.php',
     'MultimeterViewer' => 'applications/multimeter/storage/MultimeterViewer.php',
     'NuanceConduitAPIMethod' => 'applications/nuance/conduit/NuanceConduitAPIMethod.php',
     'NuanceController' => 'applications/nuance/controller/NuanceController.php',
     'NuanceCreateItemConduitAPIMethod' => 'applications/nuance/conduit/NuanceCreateItemConduitAPIMethod.php',
     'NuanceDAO' => 'applications/nuance/storage/NuanceDAO.php',
     'NuanceItem' => 'applications/nuance/storage/NuanceItem.php',
     'NuanceItemEditController' => 'applications/nuance/controller/NuanceItemEditController.php',
     'NuanceItemEditor' => 'applications/nuance/editor/NuanceItemEditor.php',
     'NuanceItemPHIDType' => 'applications/nuance/phid/NuanceItemPHIDType.php',
     'NuanceItemQuery' => 'applications/nuance/query/NuanceItemQuery.php',
     'NuanceItemTransaction' => 'applications/nuance/storage/NuanceItemTransaction.php',
     'NuanceItemTransactionComment' => 'applications/nuance/storage/NuanceItemTransactionComment.php',
     'NuanceItemTransactionQuery' => 'applications/nuance/query/NuanceItemTransactionQuery.php',
     'NuanceItemViewController' => 'applications/nuance/controller/NuanceItemViewController.php',
     'NuancePhabricatorFormSourceDefinition' => 'applications/nuance/source/NuancePhabricatorFormSourceDefinition.php',
     'NuanceQuery' => 'applications/nuance/query/NuanceQuery.php',
     'NuanceQueue' => 'applications/nuance/storage/NuanceQueue.php',
     'NuanceQueueEditController' => 'applications/nuance/controller/NuanceQueueEditController.php',
     'NuanceQueueEditor' => 'applications/nuance/editor/NuanceQueueEditor.php',
     'NuanceQueueItem' => 'applications/nuance/storage/NuanceQueueItem.php',
     'NuanceQueuePHIDType' => 'applications/nuance/phid/NuanceQueuePHIDType.php',
     'NuanceQueueQuery' => 'applications/nuance/query/NuanceQueueQuery.php',
     'NuanceQueueTransaction' => 'applications/nuance/storage/NuanceQueueTransaction.php',
     'NuanceQueueTransactionComment' => 'applications/nuance/storage/NuanceQueueTransactionComment.php',
     'NuanceQueueTransactionQuery' => 'applications/nuance/query/NuanceQueueTransactionQuery.php',
     'NuanceQueueViewController' => 'applications/nuance/controller/NuanceQueueViewController.php',
     'NuanceRequestor' => 'applications/nuance/storage/NuanceRequestor.php',
     'NuanceRequestorEditController' => 'applications/nuance/controller/NuanceRequestorEditController.php',
     'NuanceRequestorEditor' => 'applications/nuance/editor/NuanceRequestorEditor.php',
     'NuanceRequestorPHIDType' => 'applications/nuance/phid/NuanceRequestorPHIDType.php',
     'NuanceRequestorQuery' => 'applications/nuance/query/NuanceRequestorQuery.php',
     'NuanceRequestorSource' => 'applications/nuance/storage/NuanceRequestorSource.php',
     'NuanceRequestorTransaction' => 'applications/nuance/storage/NuanceRequestorTransaction.php',
     'NuanceRequestorTransactionComment' => 'applications/nuance/storage/NuanceRequestorTransactionComment.php',
     'NuanceRequestorTransactionQuery' => 'applications/nuance/query/NuanceRequestorTransactionQuery.php',
     'NuanceRequestorViewController' => 'applications/nuance/controller/NuanceRequestorViewController.php',
     'NuanceSchemaSpec' => 'applications/nuance/storage/NuanceSchemaSpec.php',
     'NuanceSource' => 'applications/nuance/storage/NuanceSource.php',
     'NuanceSourceDefaultEditCapability' => 'applications/nuance/capability/NuanceSourceDefaultEditCapability.php',
     'NuanceSourceDefaultViewCapability' => 'applications/nuance/capability/NuanceSourceDefaultViewCapability.php',
     'NuanceSourceDefinition' => 'applications/nuance/source/NuanceSourceDefinition.php',
     'NuanceSourceEditController' => 'applications/nuance/controller/NuanceSourceEditController.php',
     'NuanceSourceEditor' => 'applications/nuance/editor/NuanceSourceEditor.php',
     'NuanceSourceManageCapability' => 'applications/nuance/capability/NuanceSourceManageCapability.php',
     'NuanceSourcePHIDType' => 'applications/nuance/phid/NuanceSourcePHIDType.php',
     'NuanceSourceQuery' => 'applications/nuance/query/NuanceSourceQuery.php',
     'NuanceSourceTransaction' => 'applications/nuance/storage/NuanceSourceTransaction.php',
     'NuanceSourceTransactionComment' => 'applications/nuance/storage/NuanceSourceTransactionComment.php',
     'NuanceSourceTransactionQuery' => 'applications/nuance/query/NuanceSourceTransactionQuery.php',
     'NuanceSourceViewController' => 'applications/nuance/controller/NuanceSourceViewController.php',
     'NuanceTransaction' => 'applications/nuance/storage/NuanceTransaction.php',
     'OwnersConduitAPIMethod' => 'applications/owners/conduit/OwnersConduitAPIMethod.php',
     'OwnersPackageReplyHandler' => 'applications/owners/mail/OwnersPackageReplyHandler.php',
     'OwnersQueryConduitAPIMethod' => 'applications/owners/conduit/OwnersQueryConduitAPIMethod.php',
     'PHIDConduitAPIMethod' => 'applications/phid/conduit/PHIDConduitAPIMethod.php',
     'PHIDInfoConduitAPIMethod' => 'applications/phid/conduit/PHIDInfoConduitAPIMethod.php',
     'PHIDLookupConduitAPIMethod' => 'applications/phid/conduit/PHIDLookupConduitAPIMethod.php',
     'PHIDQueryConduitAPIMethod' => 'applications/phid/conduit/PHIDQueryConduitAPIMethod.php',
     'PHUI' => 'view/phui/PHUI.php',
     'PHUIActionHeaderExample' => 'applications/uiexample/examples/PHUIActionHeaderExample.php',
     'PHUIActionHeaderView' => 'view/phui/PHUIActionHeaderView.php',
     'PHUIActionPanelExample' => 'applications/uiexample/examples/PHUIActionPanelExample.php',
     'PHUIActionPanelView' => 'view/phui/PHUIActionPanelView.php',
     'PHUIBoxExample' => 'applications/uiexample/examples/PHUIBoxExample.php',
     'PHUIBoxView' => 'view/phui/PHUIBoxView.php',
     'PHUIButtonBarExample' => 'applications/uiexample/examples/PHUIButtonBarExample.php',
     'PHUIButtonBarView' => 'view/phui/PHUIButtonBarView.php',
     'PHUIButtonExample' => 'applications/uiexample/examples/PHUIButtonExample.php',
     'PHUIButtonView' => 'view/phui/PHUIButtonView.php',
     'PHUICalendarDayView' => 'view/phui/calendar/PHUICalendarDayView.php',
     'PHUICalendarListView' => 'view/phui/calendar/PHUICalendarListView.php',
     'PHUICalendarMonthView' => 'view/phui/calendar/PHUICalendarMonthView.php',
     'PHUICalendarWidgetView' => 'view/phui/calendar/PHUICalendarWidgetView.php',
     'PHUIColorPalletteExample' => 'applications/uiexample/examples/PHUIColorPalletteExample.php',
     'PHUICrumbView' => 'view/phui/PHUICrumbView.php',
     'PHUICrumbsView' => 'view/phui/PHUICrumbsView.php',
     'PHUIDiffInlineCommentDetailView' => 'infrastructure/diff/view/PHUIDiffInlineCommentDetailView.php',
     'PHUIDiffInlineCommentEditView' => 'infrastructure/diff/view/PHUIDiffInlineCommentEditView.php',
     'PHUIDiffInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffInlineCommentRowScaffold.php',
     'PHUIDiffInlineCommentTableScaffold' => 'infrastructure/diff/view/PHUIDiffInlineCommentTableScaffold.php',
     'PHUIDiffInlineCommentUndoView' => 'infrastructure/diff/view/PHUIDiffInlineCommentUndoView.php',
     'PHUIDiffInlineCommentView' => 'infrastructure/diff/view/PHUIDiffInlineCommentView.php',
     'PHUIDiffOneUpInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffOneUpInlineCommentRowScaffold.php',
     'PHUIDiffTwoUpInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffTwoUpInlineCommentRowScaffold.php',
     'PHUIDocumentExample' => 'applications/uiexample/examples/PHUIDocumentExample.php',
     'PHUIDocumentView' => 'view/phui/PHUIDocumentView.php',
     'PHUIFeedStoryExample' => 'applications/uiexample/examples/PHUIFeedStoryExample.php',
     'PHUIFeedStoryView' => 'view/phui/PHUIFeedStoryView.php',
     'PHUIFormDividerControl' => 'view/form/control/PHUIFormDividerControl.php',
     'PHUIFormFreeformDateControl' => 'view/form/control/PHUIFormFreeformDateControl.php',
     'PHUIFormInsetView' => 'view/form/PHUIFormInsetView.php',
     'PHUIFormLayoutView' => 'view/form/PHUIFormLayoutView.php',
     'PHUIFormMultiSubmitControl' => 'view/form/control/PHUIFormMultiSubmitControl.php',
     'PHUIFormPageView' => 'view/form/PHUIFormPageView.php',
     'PHUIHandleListView' => 'applications/phid/view/PHUIHandleListView.php',
     'PHUIHandleTagListView' => 'applications/phid/view/PHUIHandleTagListView.php',
     'PHUIHandleView' => 'applications/phid/view/PHUIHandleView.php',
     'PHUIHeaderView' => 'view/phui/PHUIHeaderView.php',
     'PHUIIconExample' => 'applications/uiexample/examples/PHUIIconExample.php',
     'PHUIIconView' => 'view/phui/PHUIIconView.php',
     'PHUIImageMaskExample' => 'applications/uiexample/examples/PHUIImageMaskExample.php',
     'PHUIImageMaskView' => 'view/phui/PHUIImageMaskView.php',
     'PHUIInfoExample' => 'applications/uiexample/examples/PHUIInfoExample.php',
     'PHUIInfoPanelExample' => 'applications/uiexample/examples/PHUIInfoPanelExample.php',
     'PHUIInfoPanelView' => 'view/phui/PHUIInfoPanelView.php',
     'PHUIInfoView' => 'view/form/PHUIInfoView.php',
     'PHUIListExample' => 'applications/uiexample/examples/PHUIListExample.php',
     'PHUIListItemView' => 'view/phui/PHUIListItemView.php',
     'PHUIListView' => 'view/phui/PHUIListView.php',
     'PHUIListViewTestCase' => 'view/layout/__tests__/PHUIListViewTestCase.php',
     'PHUIObjectBoxView' => 'view/phui/PHUIObjectBoxView.php',
     'PHUIObjectItemListExample' => 'applications/uiexample/examples/PHUIObjectItemListExample.php',
     'PHUIObjectItemListView' => 'view/phui/PHUIObjectItemListView.php',
     'PHUIObjectItemView' => 'view/phui/PHUIObjectItemView.php',
     'PHUIPagedFormView' => 'view/form/PHUIPagedFormView.php',
     'PHUIPinboardItemView' => 'view/phui/PHUIPinboardItemView.php',
     'PHUIPinboardView' => 'view/phui/PHUIPinboardView.php',
     'PHUIPropertyGroupView' => 'view/phui/PHUIPropertyGroupView.php',
     'PHUIPropertyListExample' => 'applications/uiexample/examples/PHUIPropertyListExample.php',
     'PHUIPropertyListView' => 'view/phui/PHUIPropertyListView.php',
     'PHUIRemarkupPreviewPanel' => 'view/phui/PHUIRemarkupPreviewPanel.php',
     'PHUIStatusItemView' => 'view/phui/PHUIStatusItemView.php',
     'PHUIStatusListView' => 'view/phui/PHUIStatusListView.php',
     'PHUITagExample' => 'applications/uiexample/examples/PHUITagExample.php',
     'PHUITagView' => 'view/phui/PHUITagView.php',
     'PHUITextExample' => 'applications/uiexample/examples/PHUITextExample.php',
     'PHUITextView' => 'view/phui/PHUITextView.php',
     'PHUITimelineEventView' => 'view/phui/PHUITimelineEventView.php',
     'PHUITimelineExample' => 'applications/uiexample/examples/PHUITimelineExample.php',
     'PHUITimelineView' => 'view/phui/PHUITimelineView.php',
     'PHUITypeaheadExample' => 'applications/uiexample/examples/PHUITypeaheadExample.php',
     'PHUIWorkboardView' => 'view/phui/PHUIWorkboardView.php',
     'PHUIWorkpanelView' => 'view/phui/PHUIWorkpanelView.php',
     'PackageCreateMail' => 'applications/owners/mail/PackageCreateMail.php',
     'PackageDeleteMail' => 'applications/owners/mail/PackageDeleteMail.php',
     'PackageMail' => 'applications/owners/mail/PackageMail.php',
     'PackageModifyMail' => 'applications/owners/mail/PackageModifyMail.php',
     'PassphraseAbstractKey' => 'applications/passphrase/keys/PassphraseAbstractKey.php',
     'PassphraseConduitAPIMethod' => 'applications/passphrase/conduit/PassphraseConduitAPIMethod.php',
     'PassphraseController' => 'applications/passphrase/controller/PassphraseController.php',
     'PassphraseCredential' => 'applications/passphrase/storage/PassphraseCredential.php',
     'PassphraseCredentialConduitController' => 'applications/passphrase/controller/PassphraseCredentialConduitController.php',
     'PassphraseCredentialControl' => 'applications/passphrase/view/PassphraseCredentialControl.php',
     'PassphraseCredentialCreateController' => 'applications/passphrase/controller/PassphraseCredentialCreateController.php',
     'PassphraseCredentialDestroyController' => 'applications/passphrase/controller/PassphraseCredentialDestroyController.php',
     'PassphraseCredentialEditController' => 'applications/passphrase/controller/PassphraseCredentialEditController.php',
     'PassphraseCredentialListController' => 'applications/passphrase/controller/PassphraseCredentialListController.php',
     'PassphraseCredentialLockController' => 'applications/passphrase/controller/PassphraseCredentialLockController.php',
     'PassphraseCredentialPHIDType' => 'applications/passphrase/phid/PassphraseCredentialPHIDType.php',
     'PassphraseCredentialPublicController' => 'applications/passphrase/controller/PassphraseCredentialPublicController.php',
     'PassphraseCredentialQuery' => 'applications/passphrase/query/PassphraseCredentialQuery.php',
     'PassphraseCredentialRevealController' => 'applications/passphrase/controller/PassphraseCredentialRevealController.php',
     'PassphraseCredentialSearchEngine' => 'applications/passphrase/query/PassphraseCredentialSearchEngine.php',
     'PassphraseCredentialTransaction' => 'applications/passphrase/storage/PassphraseCredentialTransaction.php',
     'PassphraseCredentialTransactionEditor' => 'applications/passphrase/editor/PassphraseCredentialTransactionEditor.php',
     'PassphraseCredentialTransactionQuery' => 'applications/passphrase/query/PassphraseCredentialTransactionQuery.php',
     'PassphraseCredentialType' => 'applications/passphrase/credentialtype/PassphraseCredentialType.php',
     'PassphraseCredentialTypePassword' => 'applications/passphrase/credentialtype/PassphraseCredentialTypePassword.php',
     'PassphraseCredentialTypeSSHGeneratedKey' => 'applications/passphrase/credentialtype/PassphraseCredentialTypeSSHGeneratedKey.php',
     'PassphraseCredentialTypeSSHPrivateKey' => 'applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKey.php',
     'PassphraseCredentialTypeSSHPrivateKeyFile' => 'applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyFile.php',
     'PassphraseCredentialTypeSSHPrivateKeyText' => 'applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyText.php',
     'PassphraseCredentialViewController' => 'applications/passphrase/controller/PassphraseCredentialViewController.php',
     'PassphraseDAO' => 'applications/passphrase/storage/PassphraseDAO.php',
     'PassphrasePasswordKey' => 'applications/passphrase/keys/PassphrasePasswordKey.php',
     'PassphraseQueryConduitAPIMethod' => 'applications/passphrase/conduit/PassphraseQueryConduitAPIMethod.php',
     'PassphraseRemarkupRule' => 'applications/passphrase/remarkup/PassphraseRemarkupRule.php',
     'PassphraseSSHKey' => 'applications/passphrase/keys/PassphraseSSHKey.php',
     'PassphraseSchemaSpec' => 'applications/passphrase/storage/PassphraseSchemaSpec.php',
     'PassphraseSearchIndexer' => 'applications/passphrase/search/PassphraseSearchIndexer.php',
     'PassphraseSecret' => 'applications/passphrase/storage/PassphraseSecret.php',
     'PasteConduitAPIMethod' => 'applications/paste/conduit/PasteConduitAPIMethod.php',
     'PasteCreateConduitAPIMethod' => 'applications/paste/conduit/PasteCreateConduitAPIMethod.php',
     'PasteCreateMailReceiver' => 'applications/paste/mail/PasteCreateMailReceiver.php',
     'PasteDefaultEditCapability' => 'applications/paste/capability/PasteDefaultEditCapability.php',
     'PasteDefaultViewCapability' => 'applications/paste/capability/PasteDefaultViewCapability.php',
     'PasteEmbedView' => 'applications/paste/view/PasteEmbedView.php',
     'PasteInfoConduitAPIMethod' => 'applications/paste/conduit/PasteInfoConduitAPIMethod.php',
     'PasteMailReceiver' => 'applications/paste/mail/PasteMailReceiver.php',
     'PasteQueryConduitAPIMethod' => 'applications/paste/conduit/PasteQueryConduitAPIMethod.php',
     'PasteReplyHandler' => 'applications/paste/mail/PasteReplyHandler.php',
     'PeopleBrowseUserDirectoryCapability' => 'applications/people/capability/PeopleBrowseUserDirectoryCapability.php',
     'PeopleCreateUsersCapability' => 'applications/people/capability/PeopleCreateUsersCapability.php',
     'PeopleUserLogGarbageCollector' => 'applications/people/garbagecollector/PeopleUserLogGarbageCollector.php',
     'Phabricator404Controller' => 'applications/base/controller/Phabricator404Controller.php',
     'PhabricatorAWSConfigOptions' => 'applications/config/option/PhabricatorAWSConfigOptions.php',
     'PhabricatorAccessControlTestCase' => 'applications/base/controller/__tests__/PhabricatorAccessControlTestCase.php',
     'PhabricatorAccessLog' => 'infrastructure/log/PhabricatorAccessLog.php',
     'PhabricatorAccessLogConfigOptions' => 'applications/config/option/PhabricatorAccessLogConfigOptions.php',
     'PhabricatorAccountSettingsPanel' => 'applications/settings/panel/PhabricatorAccountSettingsPanel.php',
     'PhabricatorActionListView' => 'view/layout/PhabricatorActionListView.php',
     'PhabricatorActionView' => 'view/layout/PhabricatorActionView.php',
     'PhabricatorActivitySettingsPanel' => 'applications/settings/panel/PhabricatorActivitySettingsPanel.php',
     'PhabricatorAdministratorsPolicyRule' => 'applications/policy/rule/PhabricatorAdministratorsPolicyRule.php',
     'PhabricatorAlmanacApplication' => 'applications/almanac/application/PhabricatorAlmanacApplication.php',
     'PhabricatorAmazonAuthProvider' => 'applications/auth/provider/PhabricatorAmazonAuthProvider.php',
     'PhabricatorAnchorView' => 'view/layout/PhabricatorAnchorView.php',
     'PhabricatorAphlictManagementDebugWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementDebugWorkflow.php',
     'PhabricatorAphlictManagementRestartWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementRestartWorkflow.php',
     'PhabricatorAphlictManagementStartWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementStartWorkflow.php',
     'PhabricatorAphlictManagementStatusWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementStatusWorkflow.php',
     'PhabricatorAphlictManagementStopWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementStopWorkflow.php',
     'PhabricatorAphlictManagementWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php',
     'PhabricatorAphlictSetupCheck' => 'applications/notification/setup/PhabricatorAphlictSetupCheck.php',
     'PhabricatorAphrontBarUIExample' => 'applications/uiexample/examples/PhabricatorAphrontBarUIExample.php',
     'PhabricatorAphrontViewTestCase' => 'view/__tests__/PhabricatorAphrontViewTestCase.php',
     'PhabricatorAppSearchEngine' => 'applications/meta/query/PhabricatorAppSearchEngine.php',
     'PhabricatorApplication' => 'applications/base/PhabricatorApplication.php',
     'PhabricatorApplicationApplicationPHIDType' => 'applications/meta/phid/PhabricatorApplicationApplicationPHIDType.php',
     'PhabricatorApplicationConfigOptions' => 'applications/config/option/PhabricatorApplicationConfigOptions.php',
     'PhabricatorApplicationConfigurationPanel' => 'applications/meta/panel/PhabricatorApplicationConfigurationPanel.php',
     'PhabricatorApplicationDatasource' => 'applications/meta/typeahead/PhabricatorApplicationDatasource.php',
     'PhabricatorApplicationDetailViewController' => 'applications/meta/controller/PhabricatorApplicationDetailViewController.php',
     'PhabricatorApplicationEditController' => 'applications/meta/controller/PhabricatorApplicationEditController.php',
     'PhabricatorApplicationEmailCommandsController' => 'applications/meta/controller/PhabricatorApplicationEmailCommandsController.php',
     'PhabricatorApplicationLaunchView' => 'applications/meta/view/PhabricatorApplicationLaunchView.php',
     'PhabricatorApplicationPanelController' => 'applications/meta/controller/PhabricatorApplicationPanelController.php',
     'PhabricatorApplicationQuery' => 'applications/meta/query/PhabricatorApplicationQuery.php',
     'PhabricatorApplicationSearchController' => 'applications/search/controller/PhabricatorApplicationSearchController.php',
     'PhabricatorApplicationSearchEngine' => 'applications/search/engine/PhabricatorApplicationSearchEngine.php',
     'PhabricatorApplicationSearchResultsControllerInterface' => 'applications/search/interface/PhabricatorApplicationSearchResultsControllerInterface.php',
     'PhabricatorApplicationStatusView' => 'applications/meta/view/PhabricatorApplicationStatusView.php',
     'PhabricatorApplicationTransaction' => 'applications/transactions/storage/PhabricatorApplicationTransaction.php',
     'PhabricatorApplicationTransactionComment' => 'applications/transactions/storage/PhabricatorApplicationTransactionComment.php',
     'PhabricatorApplicationTransactionCommentEditController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php',
     'PhabricatorApplicationTransactionCommentEditor' => 'applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php',
     'PhabricatorApplicationTransactionCommentHistoryController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentHistoryController.php',
     'PhabricatorApplicationTransactionCommentQuery' => 'applications/transactions/query/PhabricatorApplicationTransactionCommentQuery.php',
     'PhabricatorApplicationTransactionCommentQuoteController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentQuoteController.php',
     'PhabricatorApplicationTransactionCommentRawController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentRawController.php',
     'PhabricatorApplicationTransactionCommentRemoveController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php',
     'PhabricatorApplicationTransactionCommentView' => 'applications/transactions/view/PhabricatorApplicationTransactionCommentView.php',
     'PhabricatorApplicationTransactionController' => 'applications/transactions/controller/PhabricatorApplicationTransactionController.php',
     'PhabricatorApplicationTransactionDetailController' => 'applications/transactions/controller/PhabricatorApplicationTransactionDetailController.php',
     'PhabricatorApplicationTransactionEditor' => 'applications/transactions/editor/PhabricatorApplicationTransactionEditor.php',
     'PhabricatorApplicationTransactionFeedStory' => 'applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php',
     'PhabricatorApplicationTransactionInterface' => 'applications/transactions/interface/PhabricatorApplicationTransactionInterface.php',
     'PhabricatorApplicationTransactionNoEffectException' => 'applications/transactions/exception/PhabricatorApplicationTransactionNoEffectException.php',
     'PhabricatorApplicationTransactionNoEffectResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionNoEffectResponse.php',
     'PhabricatorApplicationTransactionQuery' => 'applications/transactions/query/PhabricatorApplicationTransactionQuery.php',
     'PhabricatorApplicationTransactionReplyHandler' => 'applications/transactions/replyhandler/PhabricatorApplicationTransactionReplyHandler.php',
     'PhabricatorApplicationTransactionResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionResponse.php',
     'PhabricatorApplicationTransactionShowOlderController' => 'applications/transactions/controller/PhabricatorApplicationTransactionShowOlderController.php',
     'PhabricatorApplicationTransactionStructureException' => 'applications/transactions/exception/PhabricatorApplicationTransactionStructureException.php',
     'PhabricatorApplicationTransactionTemplatedCommentQuery' => 'applications/transactions/query/PhabricatorApplicationTransactionTemplatedCommentQuery.php',
     'PhabricatorApplicationTransactionTextDiffDetailView' => 'applications/transactions/view/PhabricatorApplicationTransactionTextDiffDetailView.php',
     'PhabricatorApplicationTransactionTransactionPHIDType' => 'applications/transactions/phid/PhabricatorApplicationTransactionTransactionPHIDType.php',
     'PhabricatorApplicationTransactionValidationError' => 'applications/transactions/error/PhabricatorApplicationTransactionValidationError.php',
     'PhabricatorApplicationTransactionValidationException' => 'applications/transactions/exception/PhabricatorApplicationTransactionValidationException.php',
     'PhabricatorApplicationTransactionValidationResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionValidationResponse.php',
     'PhabricatorApplicationTransactionValueController' => 'applications/transactions/controller/PhabricatorApplicationTransactionValueController.php',
     'PhabricatorApplicationTransactionView' => 'applications/transactions/view/PhabricatorApplicationTransactionView.php',
     'PhabricatorApplicationUninstallController' => 'applications/meta/controller/PhabricatorApplicationUninstallController.php',
     'PhabricatorApplicationsApplication' => 'applications/meta/application/PhabricatorApplicationsApplication.php',
     'PhabricatorApplicationsController' => 'applications/meta/controller/PhabricatorApplicationsController.php',
     'PhabricatorApplicationsListController' => 'applications/meta/controller/PhabricatorApplicationsListController.php',
     'PhabricatorAsanaAuthProvider' => 'applications/auth/provider/PhabricatorAsanaAuthProvider.php',
     'PhabricatorAsanaConfigOptions' => 'applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php',
     'PhabricatorAsanaSubtaskHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorAsanaSubtaskHasObjectEdgeType.php',
     'PhabricatorAsanaTaskHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorAsanaTaskHasObjectEdgeType.php',
     'PhabricatorAuditActionConstants' => 'applications/audit/constants/PhabricatorAuditActionConstants.php',
     'PhabricatorAuditAddCommentController' => 'applications/audit/controller/PhabricatorAuditAddCommentController.php',
     'PhabricatorAuditApplication' => 'applications/audit/application/PhabricatorAuditApplication.php',
     'PhabricatorAuditCommentEditor' => 'applications/audit/editor/PhabricatorAuditCommentEditor.php',
     'PhabricatorAuditCommitStatusConstants' => 'applications/audit/constants/PhabricatorAuditCommitStatusConstants.php',
     'PhabricatorAuditController' => 'applications/audit/controller/PhabricatorAuditController.php',
     'PhabricatorAuditEditor' => 'applications/audit/editor/PhabricatorAuditEditor.php',
     'PhabricatorAuditInlineComment' => 'applications/audit/storage/PhabricatorAuditInlineComment.php',
     'PhabricatorAuditListController' => 'applications/audit/controller/PhabricatorAuditListController.php',
     'PhabricatorAuditListView' => 'applications/audit/view/PhabricatorAuditListView.php',
     'PhabricatorAuditMailReceiver' => 'applications/audit/mail/PhabricatorAuditMailReceiver.php',
     'PhabricatorAuditManagementDeleteWorkflow' => 'applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php',
     'PhabricatorAuditManagementWorkflow' => 'applications/audit/management/PhabricatorAuditManagementWorkflow.php',
     'PhabricatorAuditPreviewController' => 'applications/audit/controller/PhabricatorAuditPreviewController.php',
     'PhabricatorAuditReplyHandler' => 'applications/audit/mail/PhabricatorAuditReplyHandler.php',
     'PhabricatorAuditStatusConstants' => 'applications/audit/constants/PhabricatorAuditStatusConstants.php',
     'PhabricatorAuditTransaction' => 'applications/audit/storage/PhabricatorAuditTransaction.php',
     'PhabricatorAuditTransactionComment' => 'applications/audit/storage/PhabricatorAuditTransactionComment.php',
     'PhabricatorAuditTransactionQuery' => 'applications/audit/query/PhabricatorAuditTransactionQuery.php',
     'PhabricatorAuditTransactionView' => 'applications/audit/view/PhabricatorAuditTransactionView.php',
     'PhabricatorAuthAccountView' => 'applications/auth/view/PhabricatorAuthAccountView.php',
     'PhabricatorAuthApplication' => 'applications/auth/application/PhabricatorAuthApplication.php',
     'PhabricatorAuthAuthFactorPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthFactorPHIDType.php',
     'PhabricatorAuthAuthProviderPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthProviderPHIDType.php',
     'PhabricatorAuthConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthConduitAPIMethod.php',
     'PhabricatorAuthConfirmLinkController' => 'applications/auth/controller/PhabricatorAuthConfirmLinkController.php',
     'PhabricatorAuthController' => 'applications/auth/controller/PhabricatorAuthController.php',
     'PhabricatorAuthDAO' => 'applications/auth/storage/PhabricatorAuthDAO.php',
     'PhabricatorAuthDisableController' => 'applications/auth/controller/config/PhabricatorAuthDisableController.php',
     'PhabricatorAuthDowngradeSessionController' => 'applications/auth/controller/PhabricatorAuthDowngradeSessionController.php',
     'PhabricatorAuthEditController' => 'applications/auth/controller/config/PhabricatorAuthEditController.php',
     'PhabricatorAuthFactor' => 'applications/auth/factor/PhabricatorAuthFactor.php',
     'PhabricatorAuthFactorConfig' => 'applications/auth/storage/PhabricatorAuthFactorConfig.php',
     'PhabricatorAuthFinishController' => 'applications/auth/controller/PhabricatorAuthFinishController.php',
     'PhabricatorAuthHighSecurityRequiredException' => 'applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php',
     'PhabricatorAuthHighSecurityToken' => 'applications/auth/data/PhabricatorAuthHighSecurityToken.php',
     'PhabricatorAuthInvite' => 'applications/auth/storage/PhabricatorAuthInvite.php',
     'PhabricatorAuthInviteAccountException' => 'applications/auth/exception/PhabricatorAuthInviteAccountException.php',
     'PhabricatorAuthInviteAction' => 'applications/auth/data/PhabricatorAuthInviteAction.php',
     'PhabricatorAuthInviteActionTableView' => 'applications/auth/view/PhabricatorAuthInviteActionTableView.php',
     'PhabricatorAuthInviteController' => 'applications/auth/controller/PhabricatorAuthInviteController.php',
     'PhabricatorAuthInviteDialogException' => 'applications/auth/exception/PhabricatorAuthInviteDialogException.php',
     'PhabricatorAuthInviteEngine' => 'applications/auth/engine/PhabricatorAuthInviteEngine.php',
     'PhabricatorAuthInviteException' => 'applications/auth/exception/PhabricatorAuthInviteException.php',
     'PhabricatorAuthInviteInvalidException' => 'applications/auth/exception/PhabricatorAuthInviteInvalidException.php',
     'PhabricatorAuthInviteLoginException' => 'applications/auth/exception/PhabricatorAuthInviteLoginException.php',
     'PhabricatorAuthInvitePHIDType' => 'applications/auth/phid/PhabricatorAuthInvitePHIDType.php',
     'PhabricatorAuthInviteQuery' => 'applications/auth/query/PhabricatorAuthInviteQuery.php',
     'PhabricatorAuthInviteRegisteredException' => 'applications/auth/exception/PhabricatorAuthInviteRegisteredException.php',
     'PhabricatorAuthInviteSearchEngine' => 'applications/auth/query/PhabricatorAuthInviteSearchEngine.php',
     'PhabricatorAuthInviteTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthInviteTestCase.php',
     'PhabricatorAuthInviteVerifyException' => 'applications/auth/exception/PhabricatorAuthInviteVerifyException.php',
     'PhabricatorAuthInviteWorker' => 'applications/auth/worker/PhabricatorAuthInviteWorker.php',
     'PhabricatorAuthLinkController' => 'applications/auth/controller/PhabricatorAuthLinkController.php',
     'PhabricatorAuthListController' => 'applications/auth/controller/config/PhabricatorAuthListController.php',
     'PhabricatorAuthLoginController' => 'applications/auth/controller/PhabricatorAuthLoginController.php',
     'PhabricatorAuthManagementCachePKCS8Workflow' => 'applications/auth/management/PhabricatorAuthManagementCachePKCS8Workflow.php',
     'PhabricatorAuthManagementLDAPWorkflow' => 'applications/auth/management/PhabricatorAuthManagementLDAPWorkflow.php',
     'PhabricatorAuthManagementListFactorsWorkflow' => 'applications/auth/management/PhabricatorAuthManagementListFactorsWorkflow.php',
     'PhabricatorAuthManagementRecoverWorkflow' => 'applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php',
     'PhabricatorAuthManagementRefreshWorkflow' => 'applications/auth/management/PhabricatorAuthManagementRefreshWorkflow.php',
     'PhabricatorAuthManagementStripWorkflow' => 'applications/auth/management/PhabricatorAuthManagementStripWorkflow.php',
     'PhabricatorAuthManagementTrustOAuthClientWorkflow' => 'applications/auth/management/PhabricatorAuthManagementTrustOAuthClientWorkflow.php',
     'PhabricatorAuthManagementUntrustOAuthClientWorkflow' => 'applications/auth/management/PhabricatorAuthManagementUntrustOAuthClientWorkflow.php',
     'PhabricatorAuthManagementVerifyWorkflow' => 'applications/auth/management/PhabricatorAuthManagementVerifyWorkflow.php',
     'PhabricatorAuthManagementWorkflow' => 'applications/auth/management/PhabricatorAuthManagementWorkflow.php',
     'PhabricatorAuthNeedsApprovalController' => 'applications/auth/controller/PhabricatorAuthNeedsApprovalController.php',
     'PhabricatorAuthNeedsMultiFactorController' => 'applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php',
     'PhabricatorAuthNewController' => 'applications/auth/controller/config/PhabricatorAuthNewController.php',
     'PhabricatorAuthOldOAuthRedirectController' => 'applications/auth/controller/PhabricatorAuthOldOAuthRedirectController.php',
     'PhabricatorAuthOneTimeLoginController' => 'applications/auth/controller/PhabricatorAuthOneTimeLoginController.php',
     'PhabricatorAuthProvider' => 'applications/auth/provider/PhabricatorAuthProvider.php',
     'PhabricatorAuthProviderConfig' => 'applications/auth/storage/PhabricatorAuthProviderConfig.php',
     'PhabricatorAuthProviderConfigController' => 'applications/auth/controller/config/PhabricatorAuthProviderConfigController.php',
     'PhabricatorAuthProviderConfigEditor' => 'applications/auth/editor/PhabricatorAuthProviderConfigEditor.php',
     'PhabricatorAuthProviderConfigQuery' => 'applications/auth/query/PhabricatorAuthProviderConfigQuery.php',
     'PhabricatorAuthProviderConfigTransaction' => 'applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php',
     'PhabricatorAuthProviderConfigTransactionQuery' => 'applications/auth/query/PhabricatorAuthProviderConfigTransactionQuery.php',
     'PhabricatorAuthQueryPublicKeysConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthQueryPublicKeysConduitAPIMethod.php',
     'PhabricatorAuthRegisterController' => 'applications/auth/controller/PhabricatorAuthRegisterController.php',
     'PhabricatorAuthRevokeTokenController' => 'applications/auth/controller/PhabricatorAuthRevokeTokenController.php',
     'PhabricatorAuthSSHKey' => 'applications/auth/storage/PhabricatorAuthSSHKey.php',
     'PhabricatorAuthSSHKeyController' => 'applications/auth/controller/PhabricatorAuthSSHKeyController.php',
     'PhabricatorAuthSSHKeyDeleteController' => 'applications/auth/controller/PhabricatorAuthSSHKeyDeleteController.php',
     'PhabricatorAuthSSHKeyEditController' => 'applications/auth/controller/PhabricatorAuthSSHKeyEditController.php',
     'PhabricatorAuthSSHKeyGenerateController' => 'applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php',
     'PhabricatorAuthSSHKeyQuery' => 'applications/auth/query/PhabricatorAuthSSHKeyQuery.php',
     'PhabricatorAuthSSHKeyTableView' => 'applications/auth/view/PhabricatorAuthSSHKeyTableView.php',
     'PhabricatorAuthSSHPublicKey' => 'applications/auth/sshkey/PhabricatorAuthSSHPublicKey.php',
     'PhabricatorAuthSession' => 'applications/auth/storage/PhabricatorAuthSession.php',
     'PhabricatorAuthSessionEngine' => 'applications/auth/engine/PhabricatorAuthSessionEngine.php',
     'PhabricatorAuthSessionGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthSessionGarbageCollector.php',
     'PhabricatorAuthSessionQuery' => 'applications/auth/query/PhabricatorAuthSessionQuery.php',
     'PhabricatorAuthSetupCheck' => 'applications/config/check/PhabricatorAuthSetupCheck.php',
     'PhabricatorAuthStartController' => 'applications/auth/controller/PhabricatorAuthStartController.php',
     'PhabricatorAuthTemporaryToken' => 'applications/auth/storage/PhabricatorAuthTemporaryToken.php',
     'PhabricatorAuthTemporaryTokenGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthTemporaryTokenGarbageCollector.php',
     'PhabricatorAuthTemporaryTokenQuery' => 'applications/auth/query/PhabricatorAuthTemporaryTokenQuery.php',
     'PhabricatorAuthTerminateSessionController' => 'applications/auth/controller/PhabricatorAuthTerminateSessionController.php',
     'PhabricatorAuthTryFactorAction' => 'applications/auth/action/PhabricatorAuthTryFactorAction.php',
     'PhabricatorAuthUnlinkController' => 'applications/auth/controller/PhabricatorAuthUnlinkController.php',
     'PhabricatorAuthValidateController' => 'applications/auth/controller/PhabricatorAuthValidateController.php',
     'PhabricatorAuthenticationConfigOptions' => 'applications/config/option/PhabricatorAuthenticationConfigOptions.php',
     'PhabricatorAutoEventListener' => 'infrastructure/events/PhabricatorAutoEventListener.php',
     'PhabricatorBarePageUIExample' => 'applications/uiexample/examples/PhabricatorBarePageUIExample.php',
     'PhabricatorBarePageView' => 'view/page/PhabricatorBarePageView.php',
     'PhabricatorBaseProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorBaseProtocolAdapter.php',
     'PhabricatorBaseURISetupCheck' => 'applications/config/check/PhabricatorBaseURISetupCheck.php',
     'PhabricatorBcryptPasswordHasher' => 'infrastructure/util/password/PhabricatorBcryptPasswordHasher.php',
     'PhabricatorBinariesSetupCheck' => 'applications/config/check/PhabricatorBinariesSetupCheck.php',
     'PhabricatorBitbucketAuthProvider' => 'applications/auth/provider/PhabricatorBitbucketAuthProvider.php',
     'PhabricatorBot' => 'infrastructure/daemon/bot/PhabricatorBot.php',
     'PhabricatorBotBaseStreamingProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorBotBaseStreamingProtocolAdapter.php',
     'PhabricatorBotChannel' => 'infrastructure/daemon/bot/target/PhabricatorBotChannel.php',
     'PhabricatorBotDebugLogHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php',
     'PhabricatorBotFeedNotificationHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotFeedNotificationHandler.php',
     'PhabricatorBotFlowdockProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorBotFlowdockProtocolAdapter.php',
     'PhabricatorBotHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotHandler.php',
     'PhabricatorBotLogHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotLogHandler.php',
     'PhabricatorBotMacroHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotMacroHandler.php',
     'PhabricatorBotMessage' => 'infrastructure/daemon/bot/PhabricatorBotMessage.php',
     'PhabricatorBotObjectNameHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php',
     'PhabricatorBotSymbolHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotSymbolHandler.php',
     'PhabricatorBotTarget' => 'infrastructure/daemon/bot/target/PhabricatorBotTarget.php',
     'PhabricatorBotUser' => 'infrastructure/daemon/bot/target/PhabricatorBotUser.php',
     'PhabricatorBotWhatsNewHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php',
     'PhabricatorBritishEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorBritishEnglishTranslation.php',
     'PhabricatorBuiltinPatchList' => 'infrastructure/storage/patch/PhabricatorBuiltinPatchList.php',
     'PhabricatorBusyUIExample' => 'applications/uiexample/examples/PhabricatorBusyUIExample.php',
     'PhabricatorCacheDAO' => 'applications/cache/storage/PhabricatorCacheDAO.php',
     'PhabricatorCacheGeneralGarbageCollector' => 'applications/cache/garbagecollector/PhabricatorCacheGeneralGarbageCollector.php',
     'PhabricatorCacheManagementPurgeWorkflow' => 'applications/cache/management/PhabricatorCacheManagementPurgeWorkflow.php',
     'PhabricatorCacheManagementWorkflow' => 'applications/cache/management/PhabricatorCacheManagementWorkflow.php',
     'PhabricatorCacheMarkupGarbageCollector' => 'applications/cache/garbagecollector/PhabricatorCacheMarkupGarbageCollector.php',
     'PhabricatorCacheSchemaSpec' => 'applications/cache/storage/PhabricatorCacheSchemaSpec.php',
     'PhabricatorCacheSetupCheck' => 'applications/config/check/PhabricatorCacheSetupCheck.php',
     'PhabricatorCacheSpec' => 'applications/cache/spec/PhabricatorCacheSpec.php',
     'PhabricatorCacheTTLGarbageCollector' => 'applications/cache/garbagecollector/PhabricatorCacheTTLGarbageCollector.php',
     'PhabricatorCaches' => 'applications/cache/PhabricatorCaches.php',
     'PhabricatorCalendarApplication' => 'applications/calendar/application/PhabricatorCalendarApplication.php',
-    'PhabricatorCalendarBrowseController' => 'applications/calendar/controller/PhabricatorCalendarBrowseController.php',
     'PhabricatorCalendarController' => 'applications/calendar/controller/PhabricatorCalendarController.php',
     'PhabricatorCalendarDAO' => 'applications/calendar/storage/PhabricatorCalendarDAO.php',
     'PhabricatorCalendarEvent' => 'applications/calendar/storage/PhabricatorCalendarEvent.php',
     'PhabricatorCalendarEventCancelController' => 'applications/calendar/controller/PhabricatorCalendarEventCancelController.php',
     'PhabricatorCalendarEventCommentController' => 'applications/calendar/controller/PhabricatorCalendarEventCommentController.php',
     'PhabricatorCalendarEventEditController' => 'applications/calendar/controller/PhabricatorCalendarEventEditController.php',
     'PhabricatorCalendarEventEditor' => 'applications/calendar/editor/PhabricatorCalendarEventEditor.php',
     'PhabricatorCalendarEventEmailCommand' => 'applications/calendar/command/PhabricatorCalendarEventEmailCommand.php',
     'PhabricatorCalendarEventInvitee' => 'applications/calendar/storage/PhabricatorCalendarEventInvitee.php',
     'PhabricatorCalendarEventInviteeQuery' => 'applications/calendar/query/PhabricatorCalendarEventInviteeQuery.php',
     'PhabricatorCalendarEventJoinController' => 'applications/calendar/controller/PhabricatorCalendarEventJoinController.php',
     'PhabricatorCalendarEventListController' => 'applications/calendar/controller/PhabricatorCalendarEventListController.php',
     'PhabricatorCalendarEventMailReceiver' => 'applications/calendar/mail/PhabricatorCalendarEventMailReceiver.php',
     'PhabricatorCalendarEventPHIDType' => 'applications/calendar/phid/PhabricatorCalendarEventPHIDType.php',
     'PhabricatorCalendarEventQuery' => 'applications/calendar/query/PhabricatorCalendarEventQuery.php',
     'PhabricatorCalendarEventRSVPEmailCommand' => 'applications/calendar/command/PhabricatorCalendarEventRSVPEmailCommand.php',
     'PhabricatorCalendarEventSearchEngine' => 'applications/calendar/query/PhabricatorCalendarEventSearchEngine.php',
     'PhabricatorCalendarEventSearchIndexer' => 'applications/calendar/search/PhabricatorCalendarEventSearchIndexer.php',
     'PhabricatorCalendarEventTransaction' => 'applications/calendar/storage/PhabricatorCalendarEventTransaction.php',
     'PhabricatorCalendarEventTransactionComment' => 'applications/calendar/storage/PhabricatorCalendarEventTransactionComment.php',
     'PhabricatorCalendarEventTransactionQuery' => 'applications/calendar/query/PhabricatorCalendarEventTransactionQuery.php',
     'PhabricatorCalendarEventViewController' => 'applications/calendar/controller/PhabricatorCalendarEventViewController.php',
     'PhabricatorCalendarHoliday' => 'applications/calendar/storage/PhabricatorCalendarHoliday.php',
     'PhabricatorCalendarHolidayTestCase' => 'applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php',
     'PhabricatorCalendarRemarkupRule' => 'applications/calendar/remarkup/PhabricatorCalendarRemarkupRule.php',
     'PhabricatorCalendarReplyHandler' => 'applications/calendar/mail/PhabricatorCalendarReplyHandler.php',
     'PhabricatorCalendarSchemaSpec' => 'applications/calendar/storage/PhabricatorCalendarSchemaSpec.php',
     'PhabricatorCampfireProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorCampfireProtocolAdapter.php',
     'PhabricatorCelerityApplication' => 'applications/celerity/application/PhabricatorCelerityApplication.php',
     'PhabricatorCelerityTestCase' => '__tests__/PhabricatorCelerityTestCase.php',
     'PhabricatorChangeParserTestCase' => 'applications/repository/worker/__tests__/PhabricatorChangeParserTestCase.php',
     'PhabricatorChangesetResponse' => 'infrastructure/diff/PhabricatorChangesetResponse.php',
     'PhabricatorChatLogApplication' => 'applications/chatlog/application/PhabricatorChatLogApplication.php',
     'PhabricatorChatLogChannel' => 'applications/chatlog/storage/PhabricatorChatLogChannel.php',
     'PhabricatorChatLogChannelListController' => 'applications/chatlog/controller/PhabricatorChatLogChannelListController.php',
     'PhabricatorChatLogChannelLogController' => 'applications/chatlog/controller/PhabricatorChatLogChannelLogController.php',
     'PhabricatorChatLogChannelQuery' => 'applications/chatlog/query/PhabricatorChatLogChannelQuery.php',
     'PhabricatorChatLogController' => 'applications/chatlog/controller/PhabricatorChatLogController.php',
     'PhabricatorChatLogDAO' => 'applications/chatlog/storage/PhabricatorChatLogDAO.php',
     'PhabricatorChatLogEvent' => 'applications/chatlog/storage/PhabricatorChatLogEvent.php',
     'PhabricatorChatLogQuery' => 'applications/chatlog/query/PhabricatorChatLogQuery.php',
     'PhabricatorChunkedFileStorageEngine' => 'applications/files/engine/PhabricatorChunkedFileStorageEngine.php',
     'PhabricatorClusterConfigOptions' => 'applications/config/option/PhabricatorClusterConfigOptions.php',
     'PhabricatorCommitBranchesField' => 'applications/repository/customfield/PhabricatorCommitBranchesField.php',
     'PhabricatorCommitCustomField' => 'applications/repository/customfield/PhabricatorCommitCustomField.php',
     'PhabricatorCommitSearchEngine' => 'applications/audit/query/PhabricatorCommitSearchEngine.php',
     'PhabricatorCommitTagsField' => 'applications/repository/customfield/PhabricatorCommitTagsField.php',
     'PhabricatorCommonPasswords' => 'applications/auth/constants/PhabricatorCommonPasswords.php',
     'PhabricatorConduitAPIController' => 'applications/conduit/controller/PhabricatorConduitAPIController.php',
     'PhabricatorConduitApplication' => 'applications/conduit/application/PhabricatorConduitApplication.php',
     'PhabricatorConduitCertificateSettingsPanel' => 'applications/settings/panel/PhabricatorConduitCertificateSettingsPanel.php',
     'PhabricatorConduitCertificateToken' => 'applications/conduit/storage/PhabricatorConduitCertificateToken.php',
     'PhabricatorConduitConnectionLog' => 'applications/conduit/storage/PhabricatorConduitConnectionLog.php',
     'PhabricatorConduitConsoleController' => 'applications/conduit/controller/PhabricatorConduitConsoleController.php',
     'PhabricatorConduitController' => 'applications/conduit/controller/PhabricatorConduitController.php',
     'PhabricatorConduitDAO' => 'applications/conduit/storage/PhabricatorConduitDAO.php',
     'PhabricatorConduitListController' => 'applications/conduit/controller/PhabricatorConduitListController.php',
     'PhabricatorConduitLogController' => 'applications/conduit/controller/PhabricatorConduitLogController.php',
     'PhabricatorConduitLogQuery' => 'applications/conduit/query/PhabricatorConduitLogQuery.php',
     'PhabricatorConduitMethodCallLog' => 'applications/conduit/storage/PhabricatorConduitMethodCallLog.php',
     'PhabricatorConduitMethodQuery' => 'applications/conduit/query/PhabricatorConduitMethodQuery.php',
     'PhabricatorConduitSearchEngine' => 'applications/conduit/query/PhabricatorConduitSearchEngine.php',
     'PhabricatorConduitTestCase' => '__tests__/PhabricatorConduitTestCase.php',
     'PhabricatorConduitToken' => 'applications/conduit/storage/PhabricatorConduitToken.php',
     'PhabricatorConduitTokenController' => 'applications/conduit/controller/PhabricatorConduitTokenController.php',
     'PhabricatorConduitTokenEditController' => 'applications/conduit/controller/PhabricatorConduitTokenEditController.php',
     'PhabricatorConduitTokenHandshakeController' => 'applications/conduit/controller/PhabricatorConduitTokenHandshakeController.php',
     'PhabricatorConduitTokenQuery' => 'applications/conduit/query/PhabricatorConduitTokenQuery.php',
     'PhabricatorConduitTokenTerminateController' => 'applications/conduit/controller/PhabricatorConduitTokenTerminateController.php',
     'PhabricatorConduitTokensSettingsPanel' => 'applications/conduit/settings/PhabricatorConduitTokensSettingsPanel.php',
     'PhabricatorConfigAllController' => 'applications/config/controller/PhabricatorConfigAllController.php',
     'PhabricatorConfigApplication' => 'applications/config/application/PhabricatorConfigApplication.php',
     'PhabricatorConfigCacheController' => 'applications/config/controller/PhabricatorConfigCacheController.php',
     'PhabricatorConfigColumnSchema' => 'applications/config/schema/PhabricatorConfigColumnSchema.php',
     'PhabricatorConfigConfigPHIDType' => 'applications/config/phid/PhabricatorConfigConfigPHIDType.php',
     'PhabricatorConfigController' => 'applications/config/controller/PhabricatorConfigController.php',
     'PhabricatorConfigCoreSchemaSpec' => 'applications/config/schema/PhabricatorConfigCoreSchemaSpec.php',
     'PhabricatorConfigDatabaseController' => 'applications/config/controller/PhabricatorConfigDatabaseController.php',
     'PhabricatorConfigDatabaseIssueController' => 'applications/config/controller/PhabricatorConfigDatabaseIssueController.php',
     'PhabricatorConfigDatabaseSchema' => 'applications/config/schema/PhabricatorConfigDatabaseSchema.php',
     'PhabricatorConfigDatabaseSource' => 'infrastructure/env/PhabricatorConfigDatabaseSource.php',
     'PhabricatorConfigDatabaseStatusController' => 'applications/config/controller/PhabricatorConfigDatabaseStatusController.php',
     'PhabricatorConfigDefaultSource' => 'infrastructure/env/PhabricatorConfigDefaultSource.php',
     'PhabricatorConfigDictionarySource' => 'infrastructure/env/PhabricatorConfigDictionarySource.php',
     'PhabricatorConfigEditController' => 'applications/config/controller/PhabricatorConfigEditController.php',
     'PhabricatorConfigEditor' => 'applications/config/editor/PhabricatorConfigEditor.php',
     'PhabricatorConfigEntry' => 'applications/config/storage/PhabricatorConfigEntry.php',
     'PhabricatorConfigEntryDAO' => 'applications/config/storage/PhabricatorConfigEntryDAO.php',
     'PhabricatorConfigEntryQuery' => 'applications/config/query/PhabricatorConfigEntryQuery.php',
     'PhabricatorConfigFileSource' => 'infrastructure/env/PhabricatorConfigFileSource.php',
     'PhabricatorConfigGroupController' => 'applications/config/controller/PhabricatorConfigGroupController.php',
     'PhabricatorConfigHistoryController' => 'applications/config/controller/PhabricatorConfigHistoryController.php',
     'PhabricatorConfigIgnoreController' => 'applications/config/controller/PhabricatorConfigIgnoreController.php',
     'PhabricatorConfigIssueListController' => 'applications/config/controller/PhabricatorConfigIssueListController.php',
     'PhabricatorConfigIssueViewController' => 'applications/config/controller/PhabricatorConfigIssueViewController.php',
     'PhabricatorConfigJSON' => 'applications/config/json/PhabricatorConfigJSON.php',
     'PhabricatorConfigJSONOptionType' => 'applications/config/custom/PhabricatorConfigJSONOptionType.php',
     'PhabricatorConfigKeySchema' => 'applications/config/schema/PhabricatorConfigKeySchema.php',
     'PhabricatorConfigListController' => 'applications/config/controller/PhabricatorConfigListController.php',
     'PhabricatorConfigLocalSource' => 'infrastructure/env/PhabricatorConfigLocalSource.php',
     'PhabricatorConfigManagementDeleteWorkflow' => 'applications/config/management/PhabricatorConfigManagementDeleteWorkflow.php',
     'PhabricatorConfigManagementGetWorkflow' => 'applications/config/management/PhabricatorConfigManagementGetWorkflow.php',
     'PhabricatorConfigManagementListWorkflow' => 'applications/config/management/PhabricatorConfigManagementListWorkflow.php',
     'PhabricatorConfigManagementMigrateWorkflow' => 'applications/config/management/PhabricatorConfigManagementMigrateWorkflow.php',
     'PhabricatorConfigManagementSetWorkflow' => 'applications/config/management/PhabricatorConfigManagementSetWorkflow.php',
     'PhabricatorConfigManagementWorkflow' => 'applications/config/management/PhabricatorConfigManagementWorkflow.php',
     'PhabricatorConfigOption' => 'applications/config/option/PhabricatorConfigOption.php',
     'PhabricatorConfigOptionType' => 'applications/config/custom/PhabricatorConfigOptionType.php',
     'PhabricatorConfigProxySource' => 'infrastructure/env/PhabricatorConfigProxySource.php',
     'PhabricatorConfigResponse' => 'applications/config/response/PhabricatorConfigResponse.php',
     'PhabricatorConfigSchemaQuery' => 'applications/config/schema/PhabricatorConfigSchemaQuery.php',
     'PhabricatorConfigSchemaSpec' => 'applications/config/schema/PhabricatorConfigSchemaSpec.php',
     'PhabricatorConfigServerSchema' => 'applications/config/schema/PhabricatorConfigServerSchema.php',
     'PhabricatorConfigSiteSource' => 'infrastructure/env/PhabricatorConfigSiteSource.php',
     'PhabricatorConfigSource' => 'infrastructure/env/PhabricatorConfigSource.php',
     'PhabricatorConfigStackSource' => 'infrastructure/env/PhabricatorConfigStackSource.php',
     'PhabricatorConfigStorageSchema' => 'applications/config/schema/PhabricatorConfigStorageSchema.php',
     'PhabricatorConfigTableSchema' => 'applications/config/schema/PhabricatorConfigTableSchema.php',
     'PhabricatorConfigTransaction' => 'applications/config/storage/PhabricatorConfigTransaction.php',
     'PhabricatorConfigTransactionQuery' => 'applications/config/query/PhabricatorConfigTransactionQuery.php',
     'PhabricatorConfigValidationException' => 'applications/config/exception/PhabricatorConfigValidationException.php',
     'PhabricatorConfigWelcomeController' => 'applications/config/controller/PhabricatorConfigWelcomeController.php',
     'PhabricatorConpherenceApplication' => 'applications/conpherence/application/PhabricatorConpherenceApplication.php',
     'PhabricatorConpherencePreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorConpherencePreferencesSettingsPanel.php',
     'PhabricatorConpherenceThreadPHIDType' => 'applications/conpherence/phid/PhabricatorConpherenceThreadPHIDType.php',
     'PhabricatorConsoleApplication' => 'applications/console/application/PhabricatorConsoleApplication.php',
     'PhabricatorContentSource' => 'applications/metamta/contentsource/PhabricatorContentSource.php',
     'PhabricatorContentSourceView' => 'applications/metamta/contentsource/PhabricatorContentSourceView.php',
     'PhabricatorContributedToObjectEdgeType' => 'applications/transactions/edges/PhabricatorContributedToObjectEdgeType.php',
     'PhabricatorController' => 'applications/base/controller/PhabricatorController.php',
     'PhabricatorCookies' => 'applications/auth/constants/PhabricatorCookies.php',
     'PhabricatorCoreConfigOptions' => 'applications/config/option/PhabricatorCoreConfigOptions.php',
     'PhabricatorCountdown' => 'applications/countdown/storage/PhabricatorCountdown.php',
     'PhabricatorCountdownApplication' => 'applications/countdown/application/PhabricatorCountdownApplication.php',
     'PhabricatorCountdownController' => 'applications/countdown/controller/PhabricatorCountdownController.php',
     'PhabricatorCountdownCountdownPHIDType' => 'applications/countdown/phid/PhabricatorCountdownCountdownPHIDType.php',
     'PhabricatorCountdownDAO' => 'applications/countdown/storage/PhabricatorCountdownDAO.php',
     'PhabricatorCountdownDefaultViewCapability' => 'applications/countdown/capability/PhabricatorCountdownDefaultViewCapability.php',
     'PhabricatorCountdownDeleteController' => 'applications/countdown/controller/PhabricatorCountdownDeleteController.php',
     'PhabricatorCountdownEditController' => 'applications/countdown/controller/PhabricatorCountdownEditController.php',
     'PhabricatorCountdownListController' => 'applications/countdown/controller/PhabricatorCountdownListController.php',
     'PhabricatorCountdownQuery' => 'applications/countdown/query/PhabricatorCountdownQuery.php',
     'PhabricatorCountdownRemarkupRule' => 'applications/countdown/remarkup/PhabricatorCountdownRemarkupRule.php',
     'PhabricatorCountdownSearchEngine' => 'applications/countdown/query/PhabricatorCountdownSearchEngine.php',
     'PhabricatorCountdownView' => 'applications/countdown/view/PhabricatorCountdownView.php',
     'PhabricatorCountdownViewController' => 'applications/countdown/controller/PhabricatorCountdownViewController.php',
     'PhabricatorCredentialsUsedByObjectEdgeType' => 'applications/passphrase/edge/PhabricatorCredentialsUsedByObjectEdgeType.php',
     'PhabricatorCursorPagedPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php',
     'PhabricatorCustomField' => 'infrastructure/customfield/field/PhabricatorCustomField.php',
     'PhabricatorCustomFieldAttachment' => 'infrastructure/customfield/field/PhabricatorCustomFieldAttachment.php',
     'PhabricatorCustomFieldConfigOptionType' => 'infrastructure/customfield/config/PhabricatorCustomFieldConfigOptionType.php',
     'PhabricatorCustomFieldDataNotAvailableException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldDataNotAvailableException.php',
     'PhabricatorCustomFieldImplementationIncompleteException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldImplementationIncompleteException.php',
     'PhabricatorCustomFieldIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldIndexStorage.php',
     'PhabricatorCustomFieldInterface' => 'infrastructure/customfield/interface/PhabricatorCustomFieldInterface.php',
     'PhabricatorCustomFieldList' => 'infrastructure/customfield/field/PhabricatorCustomFieldList.php',
     'PhabricatorCustomFieldMonogramParser' => 'infrastructure/customfield/parser/PhabricatorCustomFieldMonogramParser.php',
     'PhabricatorCustomFieldNotAttachedException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldNotAttachedException.php',
     'PhabricatorCustomFieldNotProxyException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldNotProxyException.php',
     'PhabricatorCustomFieldNumericIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldNumericIndexStorage.php',
     'PhabricatorCustomFieldStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldStorage.php',
     'PhabricatorCustomFieldStringIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldStringIndexStorage.php',
     'PhabricatorCustomHeaderConfigType' => 'applications/config/custom/PhabricatorCustomHeaderConfigType.php',
     'PhabricatorDaemon' => 'infrastructure/daemon/PhabricatorDaemon.php',
     'PhabricatorDaemonConsoleController' => 'applications/daemon/controller/PhabricatorDaemonConsoleController.php',
     'PhabricatorDaemonController' => 'applications/daemon/controller/PhabricatorDaemonController.php',
     'PhabricatorDaemonDAO' => 'applications/daemon/storage/PhabricatorDaemonDAO.php',
     'PhabricatorDaemonEventListener' => 'applications/daemon/event/PhabricatorDaemonEventListener.php',
     'PhabricatorDaemonLog' => 'applications/daemon/storage/PhabricatorDaemonLog.php',
     'PhabricatorDaemonLogEvent' => 'applications/daemon/storage/PhabricatorDaemonLogEvent.php',
     'PhabricatorDaemonLogEventGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonLogEventGarbageCollector.php',
     'PhabricatorDaemonLogEventViewController' => 'applications/daemon/controller/PhabricatorDaemonLogEventViewController.php',
     'PhabricatorDaemonLogEventsView' => 'applications/daemon/view/PhabricatorDaemonLogEventsView.php',
     'PhabricatorDaemonLogGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonLogGarbageCollector.php',
     'PhabricatorDaemonLogListController' => 'applications/daemon/controller/PhabricatorDaemonLogListController.php',
     'PhabricatorDaemonLogListView' => 'applications/daemon/view/PhabricatorDaemonLogListView.php',
     'PhabricatorDaemonLogQuery' => 'applications/daemon/query/PhabricatorDaemonLogQuery.php',
     'PhabricatorDaemonLogViewController' => 'applications/daemon/controller/PhabricatorDaemonLogViewController.php',
     'PhabricatorDaemonManagementDebugWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementDebugWorkflow.php',
     'PhabricatorDaemonManagementLaunchWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementLaunchWorkflow.php',
     'PhabricatorDaemonManagementListWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementListWorkflow.php',
     'PhabricatorDaemonManagementLogWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementLogWorkflow.php',
     'PhabricatorDaemonManagementReloadWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementReloadWorkflow.php',
     'PhabricatorDaemonManagementRestartWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php',
     'PhabricatorDaemonManagementStartWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementStartWorkflow.php',
     'PhabricatorDaemonManagementStatusWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php',
     'PhabricatorDaemonManagementStopWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php',
     'PhabricatorDaemonManagementWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementWorkflow.php',
     'PhabricatorDaemonReference' => 'infrastructure/daemon/control/PhabricatorDaemonReference.php',
     'PhabricatorDaemonTaskGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonTaskGarbageCollector.php',
     'PhabricatorDaemonTasksTableView' => 'applications/daemon/view/PhabricatorDaemonTasksTableView.php',
     'PhabricatorDaemonsApplication' => 'applications/daemon/application/PhabricatorDaemonsApplication.php',
     'PhabricatorDaemonsSetupCheck' => 'applications/config/check/PhabricatorDaemonsSetupCheck.php',
     'PhabricatorDailyRoutineTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorDailyRoutineTriggerClock.php',
     'PhabricatorDashboard' => 'applications/dashboard/storage/PhabricatorDashboard.php',
     'PhabricatorDashboardAddPanelController' => 'applications/dashboard/controller/PhabricatorDashboardAddPanelController.php',
     'PhabricatorDashboardApplication' => 'applications/dashboard/application/PhabricatorDashboardApplication.php',
     'PhabricatorDashboardController' => 'applications/dashboard/controller/PhabricatorDashboardController.php',
     'PhabricatorDashboardCopyController' => 'applications/dashboard/controller/PhabricatorDashboardCopyController.php',
     'PhabricatorDashboardDAO' => 'applications/dashboard/storage/PhabricatorDashboardDAO.php',
     'PhabricatorDashboardDashboardHasPanelEdgeType' => 'applications/dashboard/edge/PhabricatorDashboardDashboardHasPanelEdgeType.php',
     'PhabricatorDashboardDashboardPHIDType' => 'applications/dashboard/phid/PhabricatorDashboardDashboardPHIDType.php',
     'PhabricatorDashboardEditController' => 'applications/dashboard/controller/PhabricatorDashboardEditController.php',
     'PhabricatorDashboardHistoryController' => 'applications/dashboard/controller/PhabricatorDashboardHistoryController.php',
     'PhabricatorDashboardInstall' => 'applications/dashboard/storage/PhabricatorDashboardInstall.php',
     'PhabricatorDashboardInstallController' => 'applications/dashboard/controller/PhabricatorDashboardInstallController.php',
     'PhabricatorDashboardLayoutConfig' => 'applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php',
     'PhabricatorDashboardListController' => 'applications/dashboard/controller/PhabricatorDashboardListController.php',
     'PhabricatorDashboardManageController' => 'applications/dashboard/controller/PhabricatorDashboardManageController.php',
     'PhabricatorDashboardMovePanelController' => 'applications/dashboard/controller/PhabricatorDashboardMovePanelController.php',
     'PhabricatorDashboardPanel' => 'applications/dashboard/storage/PhabricatorDashboardPanel.php',
     'PhabricatorDashboardPanelArchiveController' => 'applications/dashboard/controller/PhabricatorDashboardPanelArchiveController.php',
     'PhabricatorDashboardPanelCoreCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelCoreCustomField.php',
     'PhabricatorDashboardPanelCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelCustomField.php',
     'PhabricatorDashboardPanelEditController' => 'applications/dashboard/controller/PhabricatorDashboardPanelEditController.php',
     'PhabricatorDashboardPanelHasDashboardEdgeType' => 'applications/dashboard/edge/PhabricatorDashboardPanelHasDashboardEdgeType.php',
     'PhabricatorDashboardPanelListController' => 'applications/dashboard/controller/PhabricatorDashboardPanelListController.php',
     'PhabricatorDashboardPanelPHIDType' => 'applications/dashboard/phid/PhabricatorDashboardPanelPHIDType.php',
     'PhabricatorDashboardPanelQuery' => 'applications/dashboard/query/PhabricatorDashboardPanelQuery.php',
     'PhabricatorDashboardPanelRenderController' => 'applications/dashboard/controller/PhabricatorDashboardPanelRenderController.php',
     'PhabricatorDashboardPanelRenderingEngine' => 'applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php',
     'PhabricatorDashboardPanelSearchApplicationCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelSearchApplicationCustomField.php',
     'PhabricatorDashboardPanelSearchEngine' => 'applications/dashboard/query/PhabricatorDashboardPanelSearchEngine.php',
     'PhabricatorDashboardPanelSearchQueryCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelSearchQueryCustomField.php',
     'PhabricatorDashboardPanelTabsCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelTabsCustomField.php',
     'PhabricatorDashboardPanelTransaction' => 'applications/dashboard/storage/PhabricatorDashboardPanelTransaction.php',
     'PhabricatorDashboardPanelTransactionEditor' => 'applications/dashboard/editor/PhabricatorDashboardPanelTransactionEditor.php',
     'PhabricatorDashboardPanelTransactionQuery' => 'applications/dashboard/query/PhabricatorDashboardPanelTransactionQuery.php',
     'PhabricatorDashboardPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardPanelType.php',
     'PhabricatorDashboardPanelViewController' => 'applications/dashboard/controller/PhabricatorDashboardPanelViewController.php',
     'PhabricatorDashboardQuery' => 'applications/dashboard/query/PhabricatorDashboardQuery.php',
     'PhabricatorDashboardQueryPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php',
     'PhabricatorDashboardRemarkupRule' => 'applications/dashboard/remarkup/PhabricatorDashboardRemarkupRule.php',
     'PhabricatorDashboardRemovePanelController' => 'applications/dashboard/controller/PhabricatorDashboardRemovePanelController.php',
     'PhabricatorDashboardRenderingEngine' => 'applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php',
     'PhabricatorDashboardSchemaSpec' => 'applications/dashboard/storage/PhabricatorDashboardSchemaSpec.php',
     'PhabricatorDashboardSearchEngine' => 'applications/dashboard/query/PhabricatorDashboardSearchEngine.php',
     'PhabricatorDashboardTabsPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardTabsPanelType.php',
     'PhabricatorDashboardTextPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardTextPanelType.php',
     'PhabricatorDashboardTransaction' => 'applications/dashboard/storage/PhabricatorDashboardTransaction.php',
     'PhabricatorDashboardTransactionEditor' => 'applications/dashboard/editor/PhabricatorDashboardTransactionEditor.php',
     'PhabricatorDashboardTransactionQuery' => 'applications/dashboard/query/PhabricatorDashboardTransactionQuery.php',
     'PhabricatorDashboardUninstallController' => 'applications/dashboard/controller/PhabricatorDashboardUninstallController.php',
     'PhabricatorDashboardViewController' => 'applications/dashboard/controller/PhabricatorDashboardViewController.php',
     'PhabricatorDataCacheSpec' => 'applications/cache/spec/PhabricatorDataCacheSpec.php',
     'PhabricatorDataNotAttachedException' => 'infrastructure/storage/lisk/PhabricatorDataNotAttachedException.php',
     'PhabricatorDatabaseSetupCheck' => 'applications/config/check/PhabricatorDatabaseSetupCheck.php',
     'PhabricatorDebugController' => 'applications/system/controller/PhabricatorDebugController.php',
     'PhabricatorDefaultSearchEngineSelector' => 'applications/search/selector/PhabricatorDefaultSearchEngineSelector.php',
     'PhabricatorDestructibleInterface' => 'applications/system/interface/PhabricatorDestructibleInterface.php',
     'PhabricatorDestructionEngine' => 'applications/system/engine/PhabricatorDestructionEngine.php',
     'PhabricatorDeveloperConfigOptions' => 'applications/config/option/PhabricatorDeveloperConfigOptions.php',
     'PhabricatorDeveloperPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDeveloperPreferencesSettingsPanel.php',
     'PhabricatorDiffInlineCommentQuery' => 'infrastructure/diff/query/PhabricatorDiffInlineCommentQuery.php',
     'PhabricatorDiffPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDiffPreferencesSettingsPanel.php',
     'PhabricatorDifferenceEngine' => 'infrastructure/diff/PhabricatorDifferenceEngine.php',
     'PhabricatorDifferentialApplication' => 'applications/differential/application/PhabricatorDifferentialApplication.php',
     'PhabricatorDifferentialConfigOptions' => 'applications/differential/config/PhabricatorDifferentialConfigOptions.php',
     'PhabricatorDifferentialRevisionTestDataGenerator' => 'applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php',
     'PhabricatorDiffusionApplication' => 'applications/diffusion/application/PhabricatorDiffusionApplication.php',
     'PhabricatorDiffusionConfigOptions' => 'applications/diffusion/config/PhabricatorDiffusionConfigOptions.php',
     'PhabricatorDisabledUserController' => 'applications/auth/controller/PhabricatorDisabledUserController.php',
     'PhabricatorDisplayPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDisplayPreferencesSettingsPanel.php',
     'PhabricatorDisqusAuthProvider' => 'applications/auth/provider/PhabricatorDisqusAuthProvider.php',
     'PhabricatorDisqusConfigOptions' => 'applications/config/option/PhabricatorDisqusConfigOptions.php',
     'PhabricatorDivinerApplication' => 'applications/diviner/application/PhabricatorDivinerApplication.php',
     'PhabricatorDoorkeeperApplication' => 'applications/doorkeeper/application/PhabricatorDoorkeeperApplication.php',
     'PhabricatorDraft' => 'applications/draft/storage/PhabricatorDraft.php',
     'PhabricatorDraftDAO' => 'applications/draft/storage/PhabricatorDraftDAO.php',
     'PhabricatorDrydockApplication' => 'applications/drydock/application/PhabricatorDrydockApplication.php',
     'PhabricatorEdgeConfig' => 'infrastructure/edges/constants/PhabricatorEdgeConfig.php',
     'PhabricatorEdgeConstants' => 'infrastructure/edges/constants/PhabricatorEdgeConstants.php',
     'PhabricatorEdgeCycleException' => 'infrastructure/edges/exception/PhabricatorEdgeCycleException.php',
     'PhabricatorEdgeEditor' => 'infrastructure/edges/editor/PhabricatorEdgeEditor.php',
     'PhabricatorEdgeGraph' => 'infrastructure/edges/util/PhabricatorEdgeGraph.php',
     'PhabricatorEdgeQuery' => 'infrastructure/edges/query/PhabricatorEdgeQuery.php',
     'PhabricatorEdgeTestCase' => 'infrastructure/edges/__tests__/PhabricatorEdgeTestCase.php',
     'PhabricatorEdgeType' => 'infrastructure/edges/type/PhabricatorEdgeType.php',
     'PhabricatorEditor' => 'infrastructure/PhabricatorEditor.php',
     'PhabricatorElasticSearchEngine' => 'applications/search/engine/PhabricatorElasticSearchEngine.php',
     'PhabricatorElasticSetupCheck' => 'applications/config/check/PhabricatorElasticSetupCheck.php',
     'PhabricatorEmailAddressesSettingsPanel' => 'applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php',
     'PhabricatorEmailFormatSettingsPanel' => 'applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php',
     'PhabricatorEmailLoginController' => 'applications/auth/controller/PhabricatorEmailLoginController.php',
     'PhabricatorEmailPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorEmailPreferencesSettingsPanel.php',
     'PhabricatorEmailVerificationController' => 'applications/auth/controller/PhabricatorEmailVerificationController.php',
     'PhabricatorEmbedFileRemarkupRule' => 'applications/files/markup/PhabricatorEmbedFileRemarkupRule.php',
     'PhabricatorEmojiRemarkupRule' => 'applications/macro/markup/PhabricatorEmojiRemarkupRule.php',
     'PhabricatorEmptyQueryException' => 'infrastructure/query/PhabricatorEmptyQueryException.php',
     'PhabricatorEnv' => 'infrastructure/env/PhabricatorEnv.php',
     'PhabricatorEnvTestCase' => 'infrastructure/env/__tests__/PhabricatorEnvTestCase.php',
     'PhabricatorEvent' => 'infrastructure/events/PhabricatorEvent.php',
     'PhabricatorEventEngine' => 'infrastructure/events/PhabricatorEventEngine.php',
     'PhabricatorEventListener' => 'infrastructure/events/PhabricatorEventListener.php',
     'PhabricatorEventType' => 'infrastructure/events/constant/PhabricatorEventType.php',
     'PhabricatorExampleEventListener' => 'infrastructure/events/PhabricatorExampleEventListener.php',
     'PhabricatorExtendingPhabricatorConfigOptions' => 'applications/config/option/PhabricatorExtendingPhabricatorConfigOptions.php',
     'PhabricatorExtensionsSetupCheck' => 'applications/config/check/PhabricatorExtensionsSetupCheck.php',
     'PhabricatorExternalAccount' => 'applications/people/storage/PhabricatorExternalAccount.php',
     'PhabricatorExternalAccountQuery' => 'applications/auth/query/PhabricatorExternalAccountQuery.php',
     'PhabricatorExternalAccountsSettingsPanel' => 'applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php',
     'PhabricatorExtraConfigSetupCheck' => 'applications/config/check/PhabricatorExtraConfigSetupCheck.php',
     'PhabricatorFacebookAuthProvider' => 'applications/auth/provider/PhabricatorFacebookAuthProvider.php',
     'PhabricatorFactAggregate' => 'applications/fact/storage/PhabricatorFactAggregate.php',
     'PhabricatorFactApplication' => 'applications/fact/application/PhabricatorFactApplication.php',
     'PhabricatorFactChartController' => 'applications/fact/controller/PhabricatorFactChartController.php',
     'PhabricatorFactController' => 'applications/fact/controller/PhabricatorFactController.php',
     'PhabricatorFactCountEngine' => 'applications/fact/engine/PhabricatorFactCountEngine.php',
     'PhabricatorFactCursor' => 'applications/fact/storage/PhabricatorFactCursor.php',
     'PhabricatorFactDAO' => 'applications/fact/storage/PhabricatorFactDAO.php',
     'PhabricatorFactDaemon' => 'applications/fact/daemon/PhabricatorFactDaemon.php',
     'PhabricatorFactEngine' => 'applications/fact/engine/PhabricatorFactEngine.php',
     'PhabricatorFactHomeController' => 'applications/fact/controller/PhabricatorFactHomeController.php',
     'PhabricatorFactLastUpdatedEngine' => 'applications/fact/engine/PhabricatorFactLastUpdatedEngine.php',
     'PhabricatorFactManagementAnalyzeWorkflow' => 'applications/fact/management/PhabricatorFactManagementAnalyzeWorkflow.php',
     'PhabricatorFactManagementCursorsWorkflow' => 'applications/fact/management/PhabricatorFactManagementCursorsWorkflow.php',
     'PhabricatorFactManagementDestroyWorkflow' => 'applications/fact/management/PhabricatorFactManagementDestroyWorkflow.php',
     'PhabricatorFactManagementListWorkflow' => 'applications/fact/management/PhabricatorFactManagementListWorkflow.php',
     'PhabricatorFactManagementStatusWorkflow' => 'applications/fact/management/PhabricatorFactManagementStatusWorkflow.php',
     'PhabricatorFactManagementWorkflow' => 'applications/fact/management/PhabricatorFactManagementWorkflow.php',
     'PhabricatorFactRaw' => 'applications/fact/storage/PhabricatorFactRaw.php',
     'PhabricatorFactSimpleSpec' => 'applications/fact/spec/PhabricatorFactSimpleSpec.php',
     'PhabricatorFactSpec' => 'applications/fact/spec/PhabricatorFactSpec.php',
     'PhabricatorFactUpdateIterator' => 'applications/fact/extract/PhabricatorFactUpdateIterator.php',
     'PhabricatorFeedApplication' => 'applications/feed/application/PhabricatorFeedApplication.php',
     'PhabricatorFeedBuilder' => 'applications/feed/builder/PhabricatorFeedBuilder.php',
     'PhabricatorFeedConfigOptions' => 'applications/feed/config/PhabricatorFeedConfigOptions.php',
     'PhabricatorFeedController' => 'applications/feed/controller/PhabricatorFeedController.php',
     'PhabricatorFeedDAO' => 'applications/feed/storage/PhabricatorFeedDAO.php',
     'PhabricatorFeedDetailController' => 'applications/feed/controller/PhabricatorFeedDetailController.php',
     'PhabricatorFeedListController' => 'applications/feed/controller/PhabricatorFeedListController.php',
     'PhabricatorFeedManagementRepublishWorkflow' => 'applications/feed/management/PhabricatorFeedManagementRepublishWorkflow.php',
     'PhabricatorFeedManagementWorkflow' => 'applications/feed/management/PhabricatorFeedManagementWorkflow.php',
     'PhabricatorFeedPublicStreamController' => 'applications/feed/controller/PhabricatorFeedPublicStreamController.php',
     'PhabricatorFeedQuery' => 'applications/feed/query/PhabricatorFeedQuery.php',
     'PhabricatorFeedSearchEngine' => 'applications/feed/query/PhabricatorFeedSearchEngine.php',
     'PhabricatorFeedStory' => 'applications/feed/story/PhabricatorFeedStory.php',
     'PhabricatorFeedStoryAggregate' => 'applications/feed/story/PhabricatorFeedStoryAggregate.php',
     'PhabricatorFeedStoryAudit' => 'applications/feed/story/PhabricatorFeedStoryAudit.php',
     'PhabricatorFeedStoryCommit' => 'applications/feed/story/PhabricatorFeedStoryCommit.php',
     'PhabricatorFeedStoryData' => 'applications/feed/storage/PhabricatorFeedStoryData.php',
     'PhabricatorFeedStoryDifferential' => 'applications/feed/story/PhabricatorFeedStoryDifferential.php',
     'PhabricatorFeedStoryDifferentialAggregate' => 'applications/feed/story/PhabricatorFeedStoryDifferentialAggregate.php',
     'PhabricatorFeedStoryManiphestAggregate' => 'applications/feed/story/PhabricatorFeedStoryManiphestAggregate.php',
     'PhabricatorFeedStoryNotification' => 'applications/notification/storage/PhabricatorFeedStoryNotification.php',
     'PhabricatorFeedStoryPhriction' => 'applications/feed/story/PhabricatorFeedStoryPhriction.php',
     'PhabricatorFeedStoryPublisher' => 'applications/feed/PhabricatorFeedStoryPublisher.php',
     'PhabricatorFeedStoryReference' => 'applications/feed/storage/PhabricatorFeedStoryReference.php',
     'PhabricatorFile' => 'applications/files/storage/PhabricatorFile.php',
     'PhabricatorFileBundleLoader' => 'applications/files/query/PhabricatorFileBundleLoader.php',
     'PhabricatorFileChunk' => 'applications/files/storage/PhabricatorFileChunk.php',
     'PhabricatorFileChunkIterator' => 'applications/files/engine/PhabricatorFileChunkIterator.php',
     'PhabricatorFileChunkQuery' => 'applications/files/query/PhabricatorFileChunkQuery.php',
     'PhabricatorFileCommentController' => 'applications/files/controller/PhabricatorFileCommentController.php',
     'PhabricatorFileComposeController' => 'applications/files/controller/PhabricatorFileComposeController.php',
     'PhabricatorFileController' => 'applications/files/controller/PhabricatorFileController.php',
     'PhabricatorFileDAO' => 'applications/files/storage/PhabricatorFileDAO.php',
     'PhabricatorFileDataController' => 'applications/files/controller/PhabricatorFileDataController.php',
     'PhabricatorFileDeleteController' => 'applications/files/controller/PhabricatorFileDeleteController.php',
     'PhabricatorFileDropUploadController' => 'applications/files/controller/PhabricatorFileDropUploadController.php',
     'PhabricatorFileEditController' => 'applications/files/controller/PhabricatorFileEditController.php',
     'PhabricatorFileEditor' => 'applications/files/editor/PhabricatorFileEditor.php',
     'PhabricatorFileFilePHIDType' => 'applications/files/phid/PhabricatorFileFilePHIDType.php',
     'PhabricatorFileHasObjectEdgeType' => 'applications/files/edge/PhabricatorFileHasObjectEdgeType.php',
     'PhabricatorFileImageMacro' => 'applications/macro/storage/PhabricatorFileImageMacro.php',
+    'PhabricatorFileImageTransform' => 'applications/files/transform/PhabricatorFileImageTransform.php',
     'PhabricatorFileInfoController' => 'applications/files/controller/PhabricatorFileInfoController.php',
     'PhabricatorFileLinkListView' => 'view/layout/PhabricatorFileLinkListView.php',
     'PhabricatorFileLinkView' => 'view/layout/PhabricatorFileLinkView.php',
     'PhabricatorFileListController' => 'applications/files/controller/PhabricatorFileListController.php',
     'PhabricatorFileQuery' => 'applications/files/query/PhabricatorFileQuery.php',
     'PhabricatorFileSchemaSpec' => 'applications/files/storage/PhabricatorFileSchemaSpec.php',
     'PhabricatorFileSearchEngine' => 'applications/files/query/PhabricatorFileSearchEngine.php',
     'PhabricatorFileStorageBlob' => 'applications/files/storage/PhabricatorFileStorageBlob.php',
     'PhabricatorFileStorageConfigurationException' => 'applications/files/exception/PhabricatorFileStorageConfigurationException.php',
     'PhabricatorFileStorageEngine' => 'applications/files/engine/PhabricatorFileStorageEngine.php',
     'PhabricatorFileTemporaryGarbageCollector' => 'applications/files/garbagecollector/PhabricatorFileTemporaryGarbageCollector.php',
     'PhabricatorFileTestCase' => 'applications/files/storage/__tests__/PhabricatorFileTestCase.php',
     'PhabricatorFileTestDataGenerator' => 'applications/files/lipsum/PhabricatorFileTestDataGenerator.php',
+    'PhabricatorFileThumbnailTransform' => 'applications/files/transform/PhabricatorFileThumbnailTransform.php',
     'PhabricatorFileTransaction' => 'applications/files/storage/PhabricatorFileTransaction.php',
     'PhabricatorFileTransactionComment' => 'applications/files/storage/PhabricatorFileTransactionComment.php',
     'PhabricatorFileTransactionQuery' => 'applications/files/query/PhabricatorFileTransactionQuery.php',
+    'PhabricatorFileTransform' => 'applications/files/transform/PhabricatorFileTransform.php',
     'PhabricatorFileTransformController' => 'applications/files/controller/PhabricatorFileTransformController.php',
+    'PhabricatorFileTransformListController' => 'applications/files/controller/PhabricatorFileTransformListController.php',
     'PhabricatorFileUploadController' => 'applications/files/controller/PhabricatorFileUploadController.php',
     'PhabricatorFileUploadDialogController' => 'applications/files/controller/PhabricatorFileUploadDialogController.php',
     'PhabricatorFileUploadException' => 'applications/files/exception/PhabricatorFileUploadException.php',
     'PhabricatorFileinfoSetupCheck' => 'applications/config/check/PhabricatorFileinfoSetupCheck.php',
     'PhabricatorFilesApplication' => 'applications/files/application/PhabricatorFilesApplication.php',
     'PhabricatorFilesApplicationStorageEnginePanel' => 'applications/files/applicationpanel/PhabricatorFilesApplicationStorageEnginePanel.php',
     'PhabricatorFilesConfigOptions' => 'applications/files/config/PhabricatorFilesConfigOptions.php',
     'PhabricatorFilesManagementCatWorkflow' => 'applications/files/management/PhabricatorFilesManagementCatWorkflow.php',
     'PhabricatorFilesManagementCompactWorkflow' => 'applications/files/management/PhabricatorFilesManagementCompactWorkflow.php',
     'PhabricatorFilesManagementEnginesWorkflow' => 'applications/files/management/PhabricatorFilesManagementEnginesWorkflow.php',
     'PhabricatorFilesManagementMigrateWorkflow' => 'applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php',
     'PhabricatorFilesManagementPurgeWorkflow' => 'applications/files/management/PhabricatorFilesManagementPurgeWorkflow.php',
     'PhabricatorFilesManagementRebuildWorkflow' => 'applications/files/management/PhabricatorFilesManagementRebuildWorkflow.php',
     'PhabricatorFilesManagementWorkflow' => 'applications/files/management/PhabricatorFilesManagementWorkflow.php',
     'PhabricatorFilesOutboundRequestAction' => 'applications/files/action/PhabricatorFilesOutboundRequestAction.php',
     'PhabricatorFlag' => 'applications/flag/storage/PhabricatorFlag.php',
     'PhabricatorFlagColor' => 'applications/flag/constants/PhabricatorFlagColor.php',
     'PhabricatorFlagConstants' => 'applications/flag/constants/PhabricatorFlagConstants.php',
     'PhabricatorFlagController' => 'applications/flag/controller/PhabricatorFlagController.php',
     'PhabricatorFlagDAO' => 'applications/flag/storage/PhabricatorFlagDAO.php',
     'PhabricatorFlagDeleteController' => 'applications/flag/controller/PhabricatorFlagDeleteController.php',
     'PhabricatorFlagEditController' => 'applications/flag/controller/PhabricatorFlagEditController.php',
     'PhabricatorFlagListController' => 'applications/flag/controller/PhabricatorFlagListController.php',
     'PhabricatorFlagQuery' => 'applications/flag/query/PhabricatorFlagQuery.php',
     'PhabricatorFlagSearchEngine' => 'applications/flag/query/PhabricatorFlagSearchEngine.php',
     'PhabricatorFlagSelectControl' => 'applications/flag/view/PhabricatorFlagSelectControl.php',
     'PhabricatorFlaggableInterface' => 'applications/flag/interface/PhabricatorFlaggableInterface.php',
     'PhabricatorFlagsApplication' => 'applications/flag/application/PhabricatorFlagsApplication.php',
     'PhabricatorFlagsUIEventListener' => 'applications/flag/events/PhabricatorFlagsUIEventListener.php',
     'PhabricatorFundApplication' => 'applications/fund/application/PhabricatorFundApplication.php',
     'PhabricatorGDSetupCheck' => 'applications/config/check/PhabricatorGDSetupCheck.php',
     'PhabricatorGarbageCollector' => 'infrastructure/daemon/garbagecollector/PhabricatorGarbageCollector.php',
     'PhabricatorGarbageCollectorConfigOptions' => 'applications/config/option/PhabricatorGarbageCollectorConfigOptions.php',
     'PhabricatorGestureUIExample' => 'applications/uiexample/examples/PhabricatorGestureUIExample.php',
     'PhabricatorGitGraphStream' => 'applications/repository/daemon/PhabricatorGitGraphStream.php',
     'PhabricatorGitHubAuthProvider' => 'applications/auth/provider/PhabricatorGitHubAuthProvider.php',
     'PhabricatorGlobalLock' => 'infrastructure/util/PhabricatorGlobalLock.php',
     'PhabricatorGlobalUploadTargetView' => 'applications/files/view/PhabricatorGlobalUploadTargetView.php',
     'PhabricatorGoogleAuthProvider' => 'applications/auth/provider/PhabricatorGoogleAuthProvider.php',
     'PhabricatorHandleList' => 'applications/phid/handle/pool/PhabricatorHandleList.php',
     'PhabricatorHandleObjectSelectorDataView' => 'applications/phid/handle/view/PhabricatorHandleObjectSelectorDataView.php',
     'PhabricatorHandlePool' => 'applications/phid/handle/pool/PhabricatorHandlePool.php',
     'PhabricatorHandlePoolTestCase' => 'applications/phid/handle/pool/__tests__/PhabricatorHandlePoolTestCase.php',
     'PhabricatorHandleQuery' => 'applications/phid/query/PhabricatorHandleQuery.php',
     'PhabricatorHarbormasterApplication' => 'applications/harbormaster/application/PhabricatorHarbormasterApplication.php',
     'PhabricatorHarbormasterConfigOptions' => 'applications/harbormaster/config/PhabricatorHarbormasterConfigOptions.php',
     'PhabricatorHash' => 'infrastructure/util/PhabricatorHash.php',
     'PhabricatorHashTestCase' => 'infrastructure/util/__tests__/PhabricatorHashTestCase.php',
     'PhabricatorHelpApplication' => 'applications/help/application/PhabricatorHelpApplication.php',
     'PhabricatorHelpController' => 'applications/help/controller/PhabricatorHelpController.php',
     'PhabricatorHelpDocumentationController' => 'applications/help/controller/PhabricatorHelpDocumentationController.php',
     'PhabricatorHelpEditorProtocolController' => 'applications/help/controller/PhabricatorHelpEditorProtocolController.php',
     'PhabricatorHelpKeyboardShortcutController' => 'applications/help/controller/PhabricatorHelpKeyboardShortcutController.php',
     'PhabricatorHeraldApplication' => 'applications/herald/application/PhabricatorHeraldApplication.php',
     'PhabricatorHomeApplication' => 'applications/home/application/PhabricatorHomeApplication.php',
     'PhabricatorHomeController' => 'applications/home/controller/PhabricatorHomeController.php',
     'PhabricatorHomeMainController' => 'applications/home/controller/PhabricatorHomeMainController.php',
     'PhabricatorHomePreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorHomePreferencesSettingsPanel.php',
     'PhabricatorHomeQuickCreateController' => 'applications/home/controller/PhabricatorHomeQuickCreateController.php',
     'PhabricatorHovercardUIExample' => 'applications/uiexample/examples/PhabricatorHovercardUIExample.php',
     'PhabricatorHovercardView' => 'view/widget/hovercard/PhabricatorHovercardView.php',
     'PhabricatorHunksManagementMigrateWorkflow' => 'applications/differential/management/PhabricatorHunksManagementMigrateWorkflow.php',
     'PhabricatorHunksManagementWorkflow' => 'applications/differential/management/PhabricatorHunksManagementWorkflow.php',
     'PhabricatorIRCProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php',
     'PhabricatorIconRemarkupRule' => 'applications/macro/markup/PhabricatorIconRemarkupRule.php',
     'PhabricatorImageMacroRemarkupRule' => 'applications/macro/markup/PhabricatorImageMacroRemarkupRule.php',
     'PhabricatorImageTransformer' => 'applications/files/PhabricatorImageTransformer.php',
     'PhabricatorImagemagickSetupCheck' => 'applications/config/check/PhabricatorImagemagickSetupCheck.php',
     'PhabricatorInfrastructureTestCase' => '__tests__/PhabricatorInfrastructureTestCase.php',
     'PhabricatorInlineCommentController' => 'infrastructure/diff/PhabricatorInlineCommentController.php',
     'PhabricatorInlineCommentInterface' => 'infrastructure/diff/interface/PhabricatorInlineCommentInterface.php',
     'PhabricatorInlineCommentPreviewController' => 'infrastructure/diff/PhabricatorInlineCommentPreviewController.php',
     'PhabricatorInlineSummaryView' => 'infrastructure/diff/view/PhabricatorInlineSummaryView.php',
     'PhabricatorInternationalizationManagementExtractWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementExtractWorkflow.php',
     'PhabricatorInternationalizationManagementWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementWorkflow.php',
     'PhabricatorInvalidConfigSetupCheck' => 'applications/config/check/PhabricatorInvalidConfigSetupCheck.php',
     'PhabricatorIteratedMD5PasswordHasher' => 'infrastructure/util/password/PhabricatorIteratedMD5PasswordHasher.php',
     'PhabricatorJIRAAuthProvider' => 'applications/auth/provider/PhabricatorJIRAAuthProvider.php',
     'PhabricatorJavelinLinter' => 'infrastructure/lint/linter/PhabricatorJavelinLinter.php',
     'PhabricatorJiraIssueHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorJiraIssueHasObjectEdgeType.php',
     'PhabricatorJumpNavHandler' => 'applications/search/engine/PhabricatorJumpNavHandler.php',
     'PhabricatorKeyValueDatabaseCache' => 'applications/cache/PhabricatorKeyValueDatabaseCache.php',
     'PhabricatorLDAPAuthProvider' => 'applications/auth/provider/PhabricatorLDAPAuthProvider.php',
     'PhabricatorLegalpadApplication' => 'applications/legalpad/application/PhabricatorLegalpadApplication.php',
     'PhabricatorLegalpadConfigOptions' => 'applications/legalpad/config/PhabricatorLegalpadConfigOptions.php',
     'PhabricatorLegalpadDocumentPHIDType' => 'applications/legalpad/phid/PhabricatorLegalpadDocumentPHIDType.php',
     'PhabricatorLegalpadSignaturePolicyRule' => 'applications/policy/rule/PhabricatorLegalpadSignaturePolicyRule.php',
     'PhabricatorLibraryTestCase' => '__tests__/PhabricatorLibraryTestCase.php',
     'PhabricatorLipsumArtist' => 'applications/lipsum/image/PhabricatorLipsumArtist.php',
     'PhabricatorLipsumGenerateWorkflow' => 'applications/lipsum/management/PhabricatorLipsumGenerateWorkflow.php',
     'PhabricatorLipsumManagementWorkflow' => 'applications/lipsum/management/PhabricatorLipsumManagementWorkflow.php',
     'PhabricatorLipsumMondrianArtist' => 'applications/lipsum/image/PhabricatorLipsumMondrianArtist.php',
     'PhabricatorLiskDAO' => 'infrastructure/storage/lisk/PhabricatorLiskDAO.php',
     'PhabricatorLiskSerializer' => 'infrastructure/storage/lisk/PhabricatorLiskSerializer.php',
     'PhabricatorListFilterUIExample' => 'applications/uiexample/examples/PhabricatorListFilterUIExample.php',
     'PhabricatorLocalDiskFileStorageEngine' => 'applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php',
     'PhabricatorLocalTimeTestCase' => 'view/__tests__/PhabricatorLocalTimeTestCase.php',
     'PhabricatorLogTriggerAction' => 'infrastructure/daemon/workers/action/PhabricatorLogTriggerAction.php',
     'PhabricatorLogoutController' => 'applications/auth/controller/PhabricatorLogoutController.php',
     'PhabricatorLunarPhasePolicyRule' => 'applications/policy/rule/PhabricatorLunarPhasePolicyRule.php',
     'PhabricatorMacroApplication' => 'applications/macro/application/PhabricatorMacroApplication.php',
     'PhabricatorMacroAudioController' => 'applications/macro/controller/PhabricatorMacroAudioController.php',
     'PhabricatorMacroCommentController' => 'applications/macro/controller/PhabricatorMacroCommentController.php',
     'PhabricatorMacroConfigOptions' => 'applications/macro/config/PhabricatorMacroConfigOptions.php',
     'PhabricatorMacroController' => 'applications/macro/controller/PhabricatorMacroController.php',
     'PhabricatorMacroDatasource' => 'applications/macro/typeahead/PhabricatorMacroDatasource.php',
     'PhabricatorMacroDisableController' => 'applications/macro/controller/PhabricatorMacroDisableController.php',
     'PhabricatorMacroEditController' => 'applications/macro/controller/PhabricatorMacroEditController.php',
     'PhabricatorMacroEditor' => 'applications/macro/editor/PhabricatorMacroEditor.php',
     'PhabricatorMacroListController' => 'applications/macro/controller/PhabricatorMacroListController.php',
     'PhabricatorMacroMacroPHIDType' => 'applications/macro/phid/PhabricatorMacroMacroPHIDType.php',
     'PhabricatorMacroMailReceiver' => 'applications/macro/mail/PhabricatorMacroMailReceiver.php',
     'PhabricatorMacroManageCapability' => 'applications/macro/capability/PhabricatorMacroManageCapability.php',
     'PhabricatorMacroMemeController' => 'applications/macro/controller/PhabricatorMacroMemeController.php',
     'PhabricatorMacroMemeDialogController' => 'applications/macro/controller/PhabricatorMacroMemeDialogController.php',
     'PhabricatorMacroQuery' => 'applications/macro/query/PhabricatorMacroQuery.php',
     'PhabricatorMacroReplyHandler' => 'applications/macro/mail/PhabricatorMacroReplyHandler.php',
     'PhabricatorMacroSearchEngine' => 'applications/macro/query/PhabricatorMacroSearchEngine.php',
     'PhabricatorMacroTransaction' => 'applications/macro/storage/PhabricatorMacroTransaction.php',
     'PhabricatorMacroTransactionComment' => 'applications/macro/storage/PhabricatorMacroTransactionComment.php',
     'PhabricatorMacroTransactionQuery' => 'applications/macro/query/PhabricatorMacroTransactionQuery.php',
     'PhabricatorMacroTransactionType' => 'applications/macro/constants/PhabricatorMacroTransactionType.php',
     'PhabricatorMacroViewController' => 'applications/macro/controller/PhabricatorMacroViewController.php',
     'PhabricatorMail' => 'applications/metamta/PhabricatorMail.php',
     'PhabricatorMailImplementationAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationAdapter.php',
     'PhabricatorMailImplementationAmazonSESAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php',
     'PhabricatorMailImplementationMailgunAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php',
     'PhabricatorMailImplementationPHPMailerAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php',
     'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php',
     'PhabricatorMailImplementationSendGridAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php',
     'PhabricatorMailImplementationTestAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php',
     'PhabricatorMailManagementListInboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementListInboundWorkflow.php',
     'PhabricatorMailManagementListOutboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php',
     'PhabricatorMailManagementReceiveTestWorkflow' => 'applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php',
     'PhabricatorMailManagementResendWorkflow' => 'applications/metamta/management/PhabricatorMailManagementResendWorkflow.php',
     'PhabricatorMailManagementSendTestWorkflow' => 'applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php',
     'PhabricatorMailManagementShowInboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementShowInboundWorkflow.php',
     'PhabricatorMailManagementShowOutboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php',
     'PhabricatorMailManagementWorkflow' => 'applications/metamta/management/PhabricatorMailManagementWorkflow.php',
     'PhabricatorMailReceiver' => 'applications/metamta/receiver/PhabricatorMailReceiver.php',
     'PhabricatorMailReceiverTestCase' => 'applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php',
     'PhabricatorMailReplyHandler' => 'applications/metamta/replyhandler/PhabricatorMailReplyHandler.php',
     'PhabricatorMailSetupCheck' => 'applications/config/check/PhabricatorMailSetupCheck.php',
     'PhabricatorMailgunConfigOptions' => 'applications/config/option/PhabricatorMailgunConfigOptions.php',
     'PhabricatorMailingListDatasource' => 'applications/mailinglists/typeahead/PhabricatorMailingListDatasource.php',
     'PhabricatorMailingListListPHIDType' => 'applications/mailinglists/phid/PhabricatorMailingListListPHIDType.php',
     'PhabricatorMailingListQuery' => 'applications/mailinglists/query/PhabricatorMailingListQuery.php',
     'PhabricatorMailingListSearchEngine' => 'applications/mailinglists/query/PhabricatorMailingListSearchEngine.php',
     'PhabricatorMailingListsApplication' => 'applications/mailinglists/application/PhabricatorMailingListsApplication.php',
     'PhabricatorMailingListsController' => 'applications/mailinglists/controller/PhabricatorMailingListsController.php',
     'PhabricatorMailingListsEditController' => 'applications/mailinglists/controller/PhabricatorMailingListsEditController.php',
     'PhabricatorMailingListsListController' => 'applications/mailinglists/controller/PhabricatorMailingListsListController.php',
     'PhabricatorMailingListsManageCapability' => 'applications/mailinglists/capability/PhabricatorMailingListsManageCapability.php',
     'PhabricatorMainMenuSearchView' => 'view/page/menu/PhabricatorMainMenuSearchView.php',
     'PhabricatorMainMenuView' => 'view/page/menu/PhabricatorMainMenuView.php',
     'PhabricatorManagementWorkflow' => 'infrastructure/management/PhabricatorManagementWorkflow.php',
     'PhabricatorManiphestApplication' => 'applications/maniphest/application/PhabricatorManiphestApplication.php',
     'PhabricatorManiphestConfigOptions' => 'applications/maniphest/config/PhabricatorManiphestConfigOptions.php',
     'PhabricatorManiphestTaskTestDataGenerator' => 'applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php',
     'PhabricatorMarkupCache' => 'applications/cache/storage/PhabricatorMarkupCache.php',
     'PhabricatorMarkupEngine' => 'infrastructure/markup/PhabricatorMarkupEngine.php',
     'PhabricatorMarkupInterface' => 'infrastructure/markup/PhabricatorMarkupInterface.php',
     'PhabricatorMarkupOneOff' => 'infrastructure/markup/PhabricatorMarkupOneOff.php',
     'PhabricatorMarkupPreviewController' => 'infrastructure/markup/PhabricatorMarkupPreviewController.php',
     'PhabricatorMemeRemarkupRule' => 'applications/macro/markup/PhabricatorMemeRemarkupRule.php',
     'PhabricatorMentionRemarkupRule' => 'applications/people/markup/PhabricatorMentionRemarkupRule.php',
     'PhabricatorMentionableInterface' => 'applications/transactions/interface/PhabricatorMentionableInterface.php',
     'PhabricatorMercurialGraphStream' => 'applications/repository/daemon/PhabricatorMercurialGraphStream.php',
     'PhabricatorMetaMTAActor' => 'applications/metamta/query/PhabricatorMetaMTAActor.php',
     'PhabricatorMetaMTAActorQuery' => 'applications/metamta/query/PhabricatorMetaMTAActorQuery.php',
     'PhabricatorMetaMTAApplication' => 'applications/metamta/application/PhabricatorMetaMTAApplication.php',
     'PhabricatorMetaMTAApplicationEmail' => 'applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php',
     'PhabricatorMetaMTAApplicationEmailDatasource' => 'applications/metamta/typeahead/PhabricatorMetaMTAApplicationEmailDatasource.php',
     'PhabricatorMetaMTAApplicationEmailPHIDType' => 'applications/phid/PhabricatorMetaMTAApplicationEmailPHIDType.php',
     'PhabricatorMetaMTAApplicationEmailPanel' => 'applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php',
     'PhabricatorMetaMTAApplicationEmailQuery' => 'applications/metamta/query/PhabricatorMetaMTAApplicationEmailQuery.php',
     'PhabricatorMetaMTAAttachment' => 'applications/metamta/storage/PhabricatorMetaMTAAttachment.php',
     'PhabricatorMetaMTAConfigOptions' => 'applications/config/option/PhabricatorMetaMTAConfigOptions.php',
     'PhabricatorMetaMTAController' => 'applications/metamta/controller/PhabricatorMetaMTAController.php',
     'PhabricatorMetaMTADAO' => 'applications/metamta/storage/PhabricatorMetaMTADAO.php',
     'PhabricatorMetaMTAEmailBodyParser' => 'applications/metamta/parser/PhabricatorMetaMTAEmailBodyParser.php',
     'PhabricatorMetaMTAEmailBodyParserTestCase' => 'applications/metamta/parser/__tests__/PhabricatorMetaMTAEmailBodyParserTestCase.php',
     'PhabricatorMetaMTAErrorMailAction' => 'applications/metamta/action/PhabricatorMetaMTAErrorMailAction.php',
     'PhabricatorMetaMTAMail' => 'applications/metamta/storage/PhabricatorMetaMTAMail.php',
     'PhabricatorMetaMTAMailBody' => 'applications/metamta/view/PhabricatorMetaMTAMailBody.php',
     'PhabricatorMetaMTAMailBodyTestCase' => 'applications/metamta/view/__tests__/PhabricatorMetaMTAMailBodyTestCase.php',
     'PhabricatorMetaMTAMailSection' => 'applications/metamta/view/PhabricatorMetaMTAMailSection.php',
     'PhabricatorMetaMTAMailTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php',
     'PhabricatorMetaMTAMailableDatasource' => 'applications/metamta/typeahead/PhabricatorMetaMTAMailableDatasource.php',
     'PhabricatorMetaMTAMailableFunctionDatasource' => 'applications/metamta/typeahead/PhabricatorMetaMTAMailableFunctionDatasource.php',
     'PhabricatorMetaMTAMailgunReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php',
     'PhabricatorMetaMTAMailingList' => 'applications/mailinglists/storage/PhabricatorMetaMTAMailingList.php',
     'PhabricatorMetaMTAMemberQuery' => 'applications/metamta/query/PhabricatorMetaMTAMemberQuery.php',
     'PhabricatorMetaMTAPermanentFailureException' => 'applications/metamta/exception/PhabricatorMetaMTAPermanentFailureException.php',
     'PhabricatorMetaMTAReceivedMail' => 'applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php',
     'PhabricatorMetaMTAReceivedMailProcessingException' => 'applications/metamta/exception/PhabricatorMetaMTAReceivedMailProcessingException.php',
     'PhabricatorMetaMTAReceivedMailTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMetaMTAReceivedMailTestCase.php',
     'PhabricatorMetaMTASchemaSpec' => 'applications/metamta/storage/PhabricatorMetaMTASchemaSpec.php',
     'PhabricatorMetaMTASendGridReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php',
     'PhabricatorMetaMTAWorker' => 'applications/metamta/PhabricatorMetaMTAWorker.php',
     'PhabricatorMetronomicTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorMetronomicTriggerClock.php',
     'PhabricatorMultiColumnUIExample' => 'applications/uiexample/examples/PhabricatorMultiColumnUIExample.php',
     'PhabricatorMultiFactorSettingsPanel' => 'applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php',
     'PhabricatorMultimeterApplication' => 'applications/multimeter/application/PhabricatorMultimeterApplication.php',
     'PhabricatorMustVerifyEmailController' => 'applications/auth/controller/PhabricatorMustVerifyEmailController.php',
     'PhabricatorMySQLConfigOptions' => 'applications/config/option/PhabricatorMySQLConfigOptions.php',
     'PhabricatorMySQLFileStorageEngine' => 'applications/files/engine/PhabricatorMySQLFileStorageEngine.php',
     'PhabricatorMySQLSearchEngine' => 'applications/search/engine/PhabricatorMySQLSearchEngine.php',
     'PhabricatorMySQLSetupCheck' => 'applications/config/check/PhabricatorMySQLSetupCheck.php',
     'PhabricatorNamedQuery' => 'applications/search/storage/PhabricatorNamedQuery.php',
     'PhabricatorNamedQueryQuery' => 'applications/search/query/PhabricatorNamedQueryQuery.php',
     'PhabricatorNavigationRemarkupRule' => 'infrastructure/markup/rule/PhabricatorNavigationRemarkupRule.php',
     'PhabricatorNeverTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorNeverTriggerClock.php',
     'PhabricatorNotificationAdHocFeedStory' => 'applications/notification/feed/PhabricatorNotificationAdHocFeedStory.php',
     'PhabricatorNotificationBuilder' => 'applications/notification/builder/PhabricatorNotificationBuilder.php',
     'PhabricatorNotificationClearController' => 'applications/notification/controller/PhabricatorNotificationClearController.php',
     'PhabricatorNotificationClient' => 'applications/notification/client/PhabricatorNotificationClient.php',
     'PhabricatorNotificationConfigOptions' => 'applications/config/option/PhabricatorNotificationConfigOptions.php',
     'PhabricatorNotificationController' => 'applications/notification/controller/PhabricatorNotificationController.php',
     'PhabricatorNotificationIndividualController' => 'applications/notification/controller/PhabricatorNotificationIndividualController.php',
     'PhabricatorNotificationListController' => 'applications/notification/controller/PhabricatorNotificationListController.php',
     'PhabricatorNotificationPanelController' => 'applications/notification/controller/PhabricatorNotificationPanelController.php',
     'PhabricatorNotificationQuery' => 'applications/notification/query/PhabricatorNotificationQuery.php',
     'PhabricatorNotificationSearchEngine' => 'applications/notification/query/PhabricatorNotificationSearchEngine.php',
     'PhabricatorNotificationStatusController' => 'applications/notification/controller/PhabricatorNotificationStatusController.php',
     'PhabricatorNotificationStatusView' => 'applications/notification/view/PhabricatorNotificationStatusView.php',
     'PhabricatorNotificationTestController' => 'applications/notification/controller/PhabricatorNotificationTestController.php',
     'PhabricatorNotificationUIExample' => 'applications/uiexample/examples/PhabricatorNotificationUIExample.php',
     'PhabricatorNotificationsApplication' => 'applications/notification/application/PhabricatorNotificationsApplication.php',
     'PhabricatorNuanceApplication' => 'applications/nuance/application/PhabricatorNuanceApplication.php',
     'PhabricatorOAuth1AuthProvider' => 'applications/auth/provider/PhabricatorOAuth1AuthProvider.php',
     'PhabricatorOAuth2AuthProvider' => 'applications/auth/provider/PhabricatorOAuth2AuthProvider.php',
     'PhabricatorOAuthAuthProvider' => 'applications/auth/provider/PhabricatorOAuthAuthProvider.php',
     'PhabricatorOAuthClientAuthorization' => 'applications/oauthserver/storage/PhabricatorOAuthClientAuthorization.php',
     'PhabricatorOAuthClientAuthorizationQuery' => 'applications/oauthserver/query/PhabricatorOAuthClientAuthorizationQuery.php',
     'PhabricatorOAuthClientController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientController.php',
     'PhabricatorOAuthClientDeleteController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientDeleteController.php',
     'PhabricatorOAuthClientEditController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientEditController.php',
     'PhabricatorOAuthClientListController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientListController.php',
     'PhabricatorOAuthClientSecretController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientSecretController.php',
     'PhabricatorOAuthClientViewController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientViewController.php',
     'PhabricatorOAuthResponse' => 'applications/oauthserver/PhabricatorOAuthResponse.php',
     'PhabricatorOAuthServer' => 'applications/oauthserver/PhabricatorOAuthServer.php',
     'PhabricatorOAuthServerAccessToken' => 'applications/oauthserver/storage/PhabricatorOAuthServerAccessToken.php',
     'PhabricatorOAuthServerApplication' => 'applications/oauthserver/application/PhabricatorOAuthServerApplication.php',
     'PhabricatorOAuthServerAuthController' => 'applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php',
     'PhabricatorOAuthServerAuthorizationCode' => 'applications/oauthserver/storage/PhabricatorOAuthServerAuthorizationCode.php',
     'PhabricatorOAuthServerAuthorizationsSettingsPanel' => 'applications/oauthserver/panel/PhabricatorOAuthServerAuthorizationsSettingsPanel.php',
     'PhabricatorOAuthServerClient' => 'applications/oauthserver/storage/PhabricatorOAuthServerClient.php',
     'PhabricatorOAuthServerClientAuthorizationPHIDType' => 'applications/oauthserver/phid/PhabricatorOAuthServerClientAuthorizationPHIDType.php',
     'PhabricatorOAuthServerClientPHIDType' => 'applications/oauthserver/phid/PhabricatorOAuthServerClientPHIDType.php',
     'PhabricatorOAuthServerClientQuery' => 'applications/oauthserver/query/PhabricatorOAuthServerClientQuery.php',
     'PhabricatorOAuthServerClientSearchEngine' => 'applications/oauthserver/query/PhabricatorOAuthServerClientSearchEngine.php',
     'PhabricatorOAuthServerController' => 'applications/oauthserver/controller/PhabricatorOAuthServerController.php',
     'PhabricatorOAuthServerCreateClientsCapability' => 'applications/oauthserver/capability/PhabricatorOAuthServerCreateClientsCapability.php',
     'PhabricatorOAuthServerDAO' => 'applications/oauthserver/storage/PhabricatorOAuthServerDAO.php',
     'PhabricatorOAuthServerScope' => 'applications/oauthserver/PhabricatorOAuthServerScope.php',
     'PhabricatorOAuthServerTestCase' => 'applications/oauthserver/__tests__/PhabricatorOAuthServerTestCase.php',
     'PhabricatorOAuthServerTestController' => 'applications/oauthserver/controller/PhabricatorOAuthServerTestController.php',
     'PhabricatorOAuthServerTokenController' => 'applications/oauthserver/controller/PhabricatorOAuthServerTokenController.php',
     'PhabricatorObjectHandle' => 'applications/phid/PhabricatorObjectHandle.php',
     'PhabricatorObjectHandleConstants' => 'applications/phid/handle/const/PhabricatorObjectHandleConstants.php',
     'PhabricatorObjectHandleStatus' => 'applications/phid/handle/const/PhabricatorObjectHandleStatus.php',
     'PhabricatorObjectHasAsanaSubtaskEdgeType' => 'applications/doorkeeper/edge/PhabricatorObjectHasAsanaSubtaskEdgeType.php',
     'PhabricatorObjectHasAsanaTaskEdgeType' => 'applications/doorkeeper/edge/PhabricatorObjectHasAsanaTaskEdgeType.php',
     'PhabricatorObjectHasContributorEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasContributorEdgeType.php',
     'PhabricatorObjectHasFileEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasFileEdgeType.php',
     'PhabricatorObjectHasJiraIssueEdgeType' => 'applications/doorkeeper/edge/PhabricatorObjectHasJiraIssueEdgeType.php',
     'PhabricatorObjectHasSubscriberEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasSubscriberEdgeType.php',
     'PhabricatorObjectHasUnsubscriberEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasUnsubscriberEdgeType.php',
     'PhabricatorObjectHasWatcherEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasWatcherEdgeType.php',
     'PhabricatorObjectListQuery' => 'applications/phid/query/PhabricatorObjectListQuery.php',
     'PhabricatorObjectListQueryTestCase' => 'applications/phid/query/__tests__/PhabricatorObjectListQueryTestCase.php',
     'PhabricatorObjectMailReceiver' => 'applications/metamta/receiver/PhabricatorObjectMailReceiver.php',
     'PhabricatorObjectMailReceiverTestCase' => 'applications/metamta/receiver/__tests__/PhabricatorObjectMailReceiverTestCase.php',
     'PhabricatorObjectMentionedByObjectEdgeType' => 'applications/transactions/edges/PhabricatorObjectMentionedByObjectEdgeType.php',
     'PhabricatorObjectMentionsObjectEdgeType' => 'applications/transactions/edges/PhabricatorObjectMentionsObjectEdgeType.php',
     'PhabricatorObjectQuery' => 'applications/phid/query/PhabricatorObjectQuery.php',
     'PhabricatorObjectRemarkupRule' => 'infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php',
     'PhabricatorObjectSelectorDialog' => 'view/control/PhabricatorObjectSelectorDialog.php',
     'PhabricatorObjectUsesCredentialsEdgeType' => 'applications/transactions/edges/PhabricatorObjectUsesCredentialsEdgeType.php',
     'PhabricatorOffsetPagedQuery' => 'infrastructure/query/PhabricatorOffsetPagedQuery.php',
     'PhabricatorOneTimeTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorOneTimeTriggerClock.php',
     'PhabricatorOpcodeCacheSpec' => 'applications/cache/spec/PhabricatorOpcodeCacheSpec.php',
     'PhabricatorOwnerPathQuery' => 'applications/owners/query/PhabricatorOwnerPathQuery.php',
     'PhabricatorOwnersApplication' => 'applications/owners/application/PhabricatorOwnersApplication.php',
     'PhabricatorOwnersConfigOptions' => 'applications/owners/config/PhabricatorOwnersConfigOptions.php',
     'PhabricatorOwnersController' => 'applications/owners/controller/PhabricatorOwnersController.php',
     'PhabricatorOwnersDAO' => 'applications/owners/storage/PhabricatorOwnersDAO.php',
     'PhabricatorOwnersDeleteController' => 'applications/owners/controller/PhabricatorOwnersDeleteController.php',
     'PhabricatorOwnersDetailController' => 'applications/owners/controller/PhabricatorOwnersDetailController.php',
     'PhabricatorOwnersEditController' => 'applications/owners/controller/PhabricatorOwnersEditController.php',
     'PhabricatorOwnersListController' => 'applications/owners/controller/PhabricatorOwnersListController.php',
     'PhabricatorOwnersOwner' => 'applications/owners/storage/PhabricatorOwnersOwner.php',
     'PhabricatorOwnersPackage' => 'applications/owners/storage/PhabricatorOwnersPackage.php',
     'PhabricatorOwnersPackageDatasource' => 'applications/owners/typeahead/PhabricatorOwnersPackageDatasource.php',
     'PhabricatorOwnersPackageEditor' => 'applications/owners/editor/PhabricatorOwnersPackageEditor.php',
     'PhabricatorOwnersPackagePHIDType' => 'applications/owners/phid/PhabricatorOwnersPackagePHIDType.php',
     'PhabricatorOwnersPackagePathValidator' => 'applications/repository/worker/commitchangeparser/PhabricatorOwnersPackagePathValidator.php',
     'PhabricatorOwnersPackageQuery' => 'applications/owners/query/PhabricatorOwnersPackageQuery.php',
     'PhabricatorOwnersPackageTestCase' => 'applications/owners/storage/__tests__/PhabricatorOwnersPackageTestCase.php',
     'PhabricatorOwnersPath' => 'applications/owners/storage/PhabricatorOwnersPath.php',
     'PhabricatorPHDConfigOptions' => 'applications/config/option/PhabricatorPHDConfigOptions.php',
     'PhabricatorPHID' => 'applications/phid/storage/PhabricatorPHID.php',
     'PhabricatorPHIDConstants' => 'applications/phid/PhabricatorPHIDConstants.php',
     'PhabricatorPHIDInterface' => 'applications/phid/interface/PhabricatorPHIDInterface.php',
     'PhabricatorPHIDType' => 'applications/phid/type/PhabricatorPHIDType.php',
     'PhabricatorPHPASTApplication' => 'applications/phpast/application/PhabricatorPHPASTApplication.php',
     'PhabricatorPHPConfigSetupCheck' => 'applications/config/check/PhabricatorPHPConfigSetupCheck.php',
     'PhabricatorPHPMailerConfigOptions' => 'applications/config/option/PhabricatorPHPMailerConfigOptions.php',
     'PhabricatorPagedFormUIExample' => 'applications/uiexample/examples/PhabricatorPagedFormUIExample.php',
     'PhabricatorPagerUIExample' => 'applications/uiexample/examples/PhabricatorPagerUIExample.php',
     'PhabricatorPassphraseApplication' => 'applications/passphrase/application/PhabricatorPassphraseApplication.php',
     'PhabricatorPasswordAuthProvider' => 'applications/auth/provider/PhabricatorPasswordAuthProvider.php',
     'PhabricatorPasswordHasher' => 'infrastructure/util/password/PhabricatorPasswordHasher.php',
     'PhabricatorPasswordHasherTestCase' => 'infrastructure/util/password/__tests__/PhabricatorPasswordHasherTestCase.php',
     'PhabricatorPasswordHasherUnavailableException' => 'infrastructure/util/password/PhabricatorPasswordHasherUnavailableException.php',
     'PhabricatorPasswordSettingsPanel' => 'applications/settings/panel/PhabricatorPasswordSettingsPanel.php',
     'PhabricatorPaste' => 'applications/paste/storage/PhabricatorPaste.php',
     'PhabricatorPasteApplication' => 'applications/paste/application/PhabricatorPasteApplication.php',
     'PhabricatorPasteCommentController' => 'applications/paste/controller/PhabricatorPasteCommentController.php',
     'PhabricatorPasteConfigOptions' => 'applications/paste/config/PhabricatorPasteConfigOptions.php',
     'PhabricatorPasteController' => 'applications/paste/controller/PhabricatorPasteController.php',
     'PhabricatorPasteDAO' => 'applications/paste/storage/PhabricatorPasteDAO.php',
     'PhabricatorPasteEditController' => 'applications/paste/controller/PhabricatorPasteEditController.php',
     'PhabricatorPasteEditor' => 'applications/paste/editor/PhabricatorPasteEditor.php',
     'PhabricatorPasteListController' => 'applications/paste/controller/PhabricatorPasteListController.php',
     'PhabricatorPastePastePHIDType' => 'applications/paste/phid/PhabricatorPastePastePHIDType.php',
     'PhabricatorPasteQuery' => 'applications/paste/query/PhabricatorPasteQuery.php',
     'PhabricatorPasteRemarkupRule' => 'applications/paste/remarkup/PhabricatorPasteRemarkupRule.php',
     'PhabricatorPasteSchemaSpec' => 'applications/paste/storage/PhabricatorPasteSchemaSpec.php',
     'PhabricatorPasteSearchEngine' => 'applications/paste/query/PhabricatorPasteSearchEngine.php',
     'PhabricatorPasteTestDataGenerator' => 'applications/paste/lipsum/PhabricatorPasteTestDataGenerator.php',
     'PhabricatorPasteTransaction' => 'applications/paste/storage/PhabricatorPasteTransaction.php',
     'PhabricatorPasteTransactionComment' => 'applications/paste/storage/PhabricatorPasteTransactionComment.php',
     'PhabricatorPasteTransactionQuery' => 'applications/paste/query/PhabricatorPasteTransactionQuery.php',
     'PhabricatorPasteViewController' => 'applications/paste/controller/PhabricatorPasteViewController.php',
     'PhabricatorPathSetupCheck' => 'applications/config/check/PhabricatorPathSetupCheck.php',
     'PhabricatorPeopleAnyOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleAnyOwnerDatasource.php',
     'PhabricatorPeopleApplication' => 'applications/people/application/PhabricatorPeopleApplication.php',
     'PhabricatorPeopleApproveController' => 'applications/people/controller/PhabricatorPeopleApproveController.php',
     'PhabricatorPeopleCalendarController' => 'applications/people/controller/PhabricatorPeopleCalendarController.php',
     'PhabricatorPeopleController' => 'applications/people/controller/PhabricatorPeopleController.php',
     'PhabricatorPeopleCreateController' => 'applications/people/controller/PhabricatorPeopleCreateController.php',
     'PhabricatorPeopleDatasource' => 'applications/people/typeahead/PhabricatorPeopleDatasource.php',
     'PhabricatorPeopleDeleteController' => 'applications/people/controller/PhabricatorPeopleDeleteController.php',
     'PhabricatorPeopleDisableController' => 'applications/people/controller/PhabricatorPeopleDisableController.php',
     'PhabricatorPeopleEmpowerController' => 'applications/people/controller/PhabricatorPeopleEmpowerController.php',
     'PhabricatorPeopleExternalPHIDType' => 'applications/people/phid/PhabricatorPeopleExternalPHIDType.php',
     'PhabricatorPeopleFeedController' => 'applications/people/controller/PhabricatorPeopleFeedController.php',
     'PhabricatorPeopleHovercardEventListener' => 'applications/people/event/PhabricatorPeopleHovercardEventListener.php',
     'PhabricatorPeopleInviteController' => 'applications/people/controller/PhabricatorPeopleInviteController.php',
     'PhabricatorPeopleInviteListController' => 'applications/people/controller/PhabricatorPeopleInviteListController.php',
     'PhabricatorPeopleInviteSendController' => 'applications/people/controller/PhabricatorPeopleInviteSendController.php',
     'PhabricatorPeopleLdapController' => 'applications/people/controller/PhabricatorPeopleLdapController.php',
     'PhabricatorPeopleListController' => 'applications/people/controller/PhabricatorPeopleListController.php',
     'PhabricatorPeopleLogQuery' => 'applications/people/query/PhabricatorPeopleLogQuery.php',
     'PhabricatorPeopleLogSearchEngine' => 'applications/people/query/PhabricatorPeopleLogSearchEngine.php',
     'PhabricatorPeopleLogsController' => 'applications/people/controller/PhabricatorPeopleLogsController.php',
     'PhabricatorPeopleNewController' => 'applications/people/controller/PhabricatorPeopleNewController.php',
     'PhabricatorPeopleNoOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleNoOwnerDatasource.php',
     'PhabricatorPeopleOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleOwnerDatasource.php',
     'PhabricatorPeopleProfileController' => 'applications/people/controller/PhabricatorPeopleProfileController.php',
     'PhabricatorPeopleProfileEditController' => 'applications/people/controller/PhabricatorPeopleProfileEditController.php',
     'PhabricatorPeopleProfilePictureController' => 'applications/people/controller/PhabricatorPeopleProfilePictureController.php',
     'PhabricatorPeopleQuery' => 'applications/people/query/PhabricatorPeopleQuery.php',
     'PhabricatorPeopleRenameController' => 'applications/people/controller/PhabricatorPeopleRenameController.php',
     'PhabricatorPeopleSearchEngine' => 'applications/people/query/PhabricatorPeopleSearchEngine.php',
     'PhabricatorPeopleTestDataGenerator' => 'applications/people/lipsum/PhabricatorPeopleTestDataGenerator.php',
     'PhabricatorPeopleUserFunctionDatasource' => 'applications/people/typeahead/PhabricatorPeopleUserFunctionDatasource.php',
     'PhabricatorPeopleUserPHIDType' => 'applications/people/phid/PhabricatorPeopleUserPHIDType.php',
     'PhabricatorPeopleWelcomeController' => 'applications/people/controller/PhabricatorPeopleWelcomeController.php',
     'PhabricatorPersonaAuthProvider' => 'applications/auth/provider/PhabricatorPersonaAuthProvider.php',
     'PhabricatorPhabricatorAuthProvider' => 'applications/auth/provider/PhabricatorPhabricatorAuthProvider.php',
     'PhabricatorPhameApplication' => 'applications/phame/application/PhabricatorPhameApplication.php',
     'PhabricatorPhameBlogPHIDType' => 'applications/phame/phid/PhabricatorPhameBlogPHIDType.php',
     'PhabricatorPhameConfigOptions' => 'applications/phame/config/PhabricatorPhameConfigOptions.php',
     'PhabricatorPhamePostPHIDType' => 'applications/phame/phid/PhabricatorPhamePostPHIDType.php',
     'PhabricatorPhluxApplication' => 'applications/phlux/application/PhabricatorPhluxApplication.php',
     'PhabricatorPholioApplication' => 'applications/pholio/application/PhabricatorPholioApplication.php',
     'PhabricatorPholioConfigOptions' => 'applications/pholio/config/PhabricatorPholioConfigOptions.php',
     'PhabricatorPholioMockTestDataGenerator' => 'applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php',
     'PhabricatorPhortuneApplication' => 'applications/phortune/application/PhabricatorPhortuneApplication.php',
     'PhabricatorPhortuneManagementInvoiceWorkflow' => 'applications/phortune/management/PhabricatorPhortuneManagementInvoiceWorkflow.php',
     'PhabricatorPhortuneManagementWorkflow' => 'applications/phortune/management/PhabricatorPhortuneManagementWorkflow.php',
     'PhabricatorPhragmentApplication' => 'applications/phragment/application/PhabricatorPhragmentApplication.php',
     'PhabricatorPhrequentApplication' => 'applications/phrequent/application/PhabricatorPhrequentApplication.php',
     'PhabricatorPhrequentConfigOptions' => 'applications/phrequent/config/PhabricatorPhrequentConfigOptions.php',
     'PhabricatorPhrictionApplication' => 'applications/phriction/application/PhabricatorPhrictionApplication.php',
     'PhabricatorPhrictionConfigOptions' => 'applications/phriction/config/PhabricatorPhrictionConfigOptions.php',
     'PhabricatorPolicies' => 'applications/policy/constants/PhabricatorPolicies.php',
     'PhabricatorPolicy' => 'applications/policy/storage/PhabricatorPolicy.php',
     'PhabricatorPolicyApplication' => 'applications/policy/application/PhabricatorPolicyApplication.php',
     'PhabricatorPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorPolicyAwareQuery.php',
     'PhabricatorPolicyAwareTestQuery' => 'applications/policy/__tests__/PhabricatorPolicyAwareTestQuery.php',
     'PhabricatorPolicyCanEditCapability' => 'applications/policy/capability/PhabricatorPolicyCanEditCapability.php',
     'PhabricatorPolicyCanJoinCapability' => 'applications/policy/capability/PhabricatorPolicyCanJoinCapability.php',
     'PhabricatorPolicyCanViewCapability' => 'applications/policy/capability/PhabricatorPolicyCanViewCapability.php',
     'PhabricatorPolicyCapability' => 'applications/policy/capability/PhabricatorPolicyCapability.php',
     'PhabricatorPolicyConfigOptions' => 'applications/policy/config/PhabricatorPolicyConfigOptions.php',
     'PhabricatorPolicyConstants' => 'applications/policy/constants/PhabricatorPolicyConstants.php',
     'PhabricatorPolicyController' => 'applications/policy/controller/PhabricatorPolicyController.php',
     'PhabricatorPolicyDAO' => 'applications/policy/storage/PhabricatorPolicyDAO.php',
     'PhabricatorPolicyDataTestCase' => 'applications/policy/__tests__/PhabricatorPolicyDataTestCase.php',
     'PhabricatorPolicyEditController' => 'applications/policy/controller/PhabricatorPolicyEditController.php',
     'PhabricatorPolicyException' => 'applications/policy/exception/PhabricatorPolicyException.php',
     'PhabricatorPolicyExplainController' => 'applications/policy/controller/PhabricatorPolicyExplainController.php',
     'PhabricatorPolicyFilter' => 'applications/policy/filter/PhabricatorPolicyFilter.php',
     'PhabricatorPolicyInterface' => 'applications/policy/interface/PhabricatorPolicyInterface.php',
     'PhabricatorPolicyManagementShowWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementShowWorkflow.php',
     'PhabricatorPolicyManagementUnlockWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php',
     'PhabricatorPolicyManagementWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementWorkflow.php',
     'PhabricatorPolicyPHIDTypePolicy' => 'applications/policy/phid/PhabricatorPolicyPHIDTypePolicy.php',
     'PhabricatorPolicyQuery' => 'applications/policy/query/PhabricatorPolicyQuery.php',
     'PhabricatorPolicyRule' => 'applications/policy/rule/PhabricatorPolicyRule.php',
     'PhabricatorPolicyTestCase' => 'applications/policy/__tests__/PhabricatorPolicyTestCase.php',
     'PhabricatorPolicyTestObject' => 'applications/policy/__tests__/PhabricatorPolicyTestObject.php',
     'PhabricatorPolicyType' => 'applications/policy/constants/PhabricatorPolicyType.php',
     'PhabricatorPonderApplication' => 'applications/ponder/application/PhabricatorPonderApplication.php',
     'PhabricatorProject' => 'applications/project/storage/PhabricatorProject.php',
     'PhabricatorProjectApplication' => 'applications/project/application/PhabricatorProjectApplication.php',
     'PhabricatorProjectArchiveController' => 'applications/project/controller/PhabricatorProjectArchiveController.php',
     'PhabricatorProjectBoardController' => 'applications/project/controller/PhabricatorProjectBoardController.php',
     'PhabricatorProjectBoardImportController' => 'applications/project/controller/PhabricatorProjectBoardImportController.php',
     'PhabricatorProjectBoardReorderController' => 'applications/project/controller/PhabricatorProjectBoardReorderController.php',
     'PhabricatorProjectBoardViewController' => 'applications/project/controller/PhabricatorProjectBoardViewController.php',
     'PhabricatorProjectColumn' => 'applications/project/storage/PhabricatorProjectColumn.php',
     'PhabricatorProjectColumnDetailController' => 'applications/project/controller/PhabricatorProjectColumnDetailController.php',
     'PhabricatorProjectColumnEditController' => 'applications/project/controller/PhabricatorProjectColumnEditController.php',
     'PhabricatorProjectColumnHideController' => 'applications/project/controller/PhabricatorProjectColumnHideController.php',
     'PhabricatorProjectColumnPHIDType' => 'applications/project/phid/PhabricatorProjectColumnPHIDType.php',
     'PhabricatorProjectColumnPosition' => 'applications/project/storage/PhabricatorProjectColumnPosition.php',
     'PhabricatorProjectColumnPositionQuery' => 'applications/project/query/PhabricatorProjectColumnPositionQuery.php',
     'PhabricatorProjectColumnQuery' => 'applications/project/query/PhabricatorProjectColumnQuery.php',
     'PhabricatorProjectColumnTransaction' => 'applications/project/storage/PhabricatorProjectColumnTransaction.php',
     'PhabricatorProjectColumnTransactionEditor' => 'applications/project/editor/PhabricatorProjectColumnTransactionEditor.php',
     'PhabricatorProjectColumnTransactionQuery' => 'applications/project/query/PhabricatorProjectColumnTransactionQuery.php',
     'PhabricatorProjectConfigOptions' => 'applications/project/config/PhabricatorProjectConfigOptions.php',
     'PhabricatorProjectConfiguredCustomField' => 'applications/project/customfield/PhabricatorProjectConfiguredCustomField.php',
     'PhabricatorProjectController' => 'applications/project/controller/PhabricatorProjectController.php',
     'PhabricatorProjectCustomField' => 'applications/project/customfield/PhabricatorProjectCustomField.php',
     'PhabricatorProjectCustomFieldNumericIndex' => 'applications/project/storage/PhabricatorProjectCustomFieldNumericIndex.php',
     'PhabricatorProjectCustomFieldStorage' => 'applications/project/storage/PhabricatorProjectCustomFieldStorage.php',
     'PhabricatorProjectCustomFieldStringIndex' => 'applications/project/storage/PhabricatorProjectCustomFieldStringIndex.php',
     'PhabricatorProjectDAO' => 'applications/project/storage/PhabricatorProjectDAO.php',
     'PhabricatorProjectDatasource' => 'applications/project/typeahead/PhabricatorProjectDatasource.php',
     'PhabricatorProjectDescriptionField' => 'applications/project/customfield/PhabricatorProjectDescriptionField.php',
     'PhabricatorProjectEditDetailsController' => 'applications/project/controller/PhabricatorProjectEditDetailsController.php',
     'PhabricatorProjectEditIconController' => 'applications/project/controller/PhabricatorProjectEditIconController.php',
     'PhabricatorProjectEditPictureController' => 'applications/project/controller/PhabricatorProjectEditPictureController.php',
     'PhabricatorProjectEditorTestCase' => 'applications/project/editor/__tests__/PhabricatorProjectEditorTestCase.php',
     'PhabricatorProjectFeedController' => 'applications/project/controller/PhabricatorProjectFeedController.php',
     'PhabricatorProjectIcon' => 'applications/project/icon/PhabricatorProjectIcon.php',
     'PhabricatorProjectInterface' => 'applications/project/interface/PhabricatorProjectInterface.php',
     'PhabricatorProjectListController' => 'applications/project/controller/PhabricatorProjectListController.php',
     'PhabricatorProjectLogicalAndDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalAndDatasource.php',
     'PhabricatorProjectLogicalDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalDatasource.php',
     'PhabricatorProjectLogicalOrNotDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalOrNotDatasource.php',
     'PhabricatorProjectLogicalUserDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalUserDatasource.php',
     'PhabricatorProjectLogicalViewerDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalViewerDatasource.php',
     'PhabricatorProjectMemberOfProjectEdgeType' => 'applications/project/edge/PhabricatorProjectMemberOfProjectEdgeType.php',
     'PhabricatorProjectMembersDatasource' => 'applications/project/typeahead/PhabricatorProjectMembersDatasource.php',
     'PhabricatorProjectMembersEditController' => 'applications/project/controller/PhabricatorProjectMembersEditController.php',
     'PhabricatorProjectMembersRemoveController' => 'applications/project/controller/PhabricatorProjectMembersRemoveController.php',
     'PhabricatorProjectMoveController' => 'applications/project/controller/PhabricatorProjectMoveController.php',
     'PhabricatorProjectNoProjectsDatasource' => 'applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php',
     'PhabricatorProjectObjectHasProjectEdgeType' => 'applications/project/edge/PhabricatorProjectObjectHasProjectEdgeType.php',
     'PhabricatorProjectOrUserDatasource' => 'applications/project/typeahead/PhabricatorProjectOrUserDatasource.php',
     'PhabricatorProjectProfileController' => 'applications/project/controller/PhabricatorProjectProfileController.php',
     'PhabricatorProjectProjectHasMemberEdgeType' => 'applications/project/edge/PhabricatorProjectProjectHasMemberEdgeType.php',
     'PhabricatorProjectProjectHasObjectEdgeType' => 'applications/project/edge/PhabricatorProjectProjectHasObjectEdgeType.php',
     'PhabricatorProjectProjectPHIDType' => 'applications/project/phid/PhabricatorProjectProjectPHIDType.php',
     'PhabricatorProjectQuery' => 'applications/project/query/PhabricatorProjectQuery.php',
     'PhabricatorProjectSchemaSpec' => 'applications/project/storage/PhabricatorProjectSchemaSpec.php',
     'PhabricatorProjectSearchEngine' => 'applications/project/query/PhabricatorProjectSearchEngine.php',
     'PhabricatorProjectSearchIndexer' => 'applications/project/search/PhabricatorProjectSearchIndexer.php',
     'PhabricatorProjectSlug' => 'applications/project/storage/PhabricatorProjectSlug.php',
     'PhabricatorProjectStandardCustomField' => 'applications/project/customfield/PhabricatorProjectStandardCustomField.php',
     'PhabricatorProjectStatus' => 'applications/project/constants/PhabricatorProjectStatus.php',
     'PhabricatorProjectTestDataGenerator' => 'applications/project/lipsum/PhabricatorProjectTestDataGenerator.php',
     'PhabricatorProjectTransaction' => 'applications/project/storage/PhabricatorProjectTransaction.php',
     'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php',
     'PhabricatorProjectTransactionQuery' => 'applications/project/query/PhabricatorProjectTransactionQuery.php',
     'PhabricatorProjectUIEventListener' => 'applications/project/events/PhabricatorProjectUIEventListener.php',
     'PhabricatorProjectUpdateController' => 'applications/project/controller/PhabricatorProjectUpdateController.php',
     'PhabricatorProjectViewController' => 'applications/project/controller/PhabricatorProjectViewController.php',
     'PhabricatorProjectWatchController' => 'applications/project/controller/PhabricatorProjectWatchController.php',
     'PhabricatorProjectsPolicyRule' => 'applications/policy/rule/PhabricatorProjectsPolicyRule.php',
     'PhabricatorPygmentSetupCheck' => 'applications/config/check/PhabricatorPygmentSetupCheck.php',
     'PhabricatorQuery' => 'infrastructure/query/PhabricatorQuery.php',
     'PhabricatorQueryConstraint' => 'infrastructure/query/constraint/PhabricatorQueryConstraint.php',
     'PhabricatorQueryOrderItem' => 'infrastructure/query/order/PhabricatorQueryOrderItem.php',
     'PhabricatorQueryOrderTestCase' => 'infrastructure/query/order/__tests__/PhabricatorQueryOrderTestCase.php',
     'PhabricatorQueryOrderVector' => 'infrastructure/query/order/PhabricatorQueryOrderVector.php',
     'PhabricatorRecaptchaConfigOptions' => 'applications/config/option/PhabricatorRecaptchaConfigOptions.php',
     'PhabricatorRedirectController' => 'applications/base/controller/PhabricatorRedirectController.php',
     'PhabricatorRefreshCSRFController' => 'applications/auth/controller/PhabricatorRefreshCSRFController.php',
     'PhabricatorRegistrationProfile' => 'applications/people/storage/PhabricatorRegistrationProfile.php',
     'PhabricatorReleephApplication' => 'applications/releeph/application/PhabricatorReleephApplication.php',
     'PhabricatorReleephApplicationConfigOptions' => 'applications/releeph/config/PhabricatorReleephApplicationConfigOptions.php',
     'PhabricatorRemarkupControl' => 'view/form/control/PhabricatorRemarkupControl.php',
     'PhabricatorRemarkupCowsayBlockInterpreter' => 'infrastructure/markup/interpreter/PhabricatorRemarkupCowsayBlockInterpreter.php',
     'PhabricatorRemarkupCustomBlockRule' => 'infrastructure/markup/rule/PhabricatorRemarkupCustomBlockRule.php',
     'PhabricatorRemarkupCustomInlineRule' => 'infrastructure/markup/rule/PhabricatorRemarkupCustomInlineRule.php',
     'PhabricatorRemarkupFigletBlockInterpreter' => 'infrastructure/markup/interpreter/PhabricatorRemarkupFigletBlockInterpreter.php',
     'PhabricatorRemarkupGraphvizBlockInterpreter' => 'infrastructure/markup/interpreter/PhabricatorRemarkupGraphvizBlockInterpreter.php',
     'PhabricatorRemarkupUIExample' => 'applications/uiexample/examples/PhabricatorRemarkupUIExample.php',
     'PhabricatorRepositoriesApplication' => 'applications/repository/application/PhabricatorRepositoriesApplication.php',
     'PhabricatorRepositoriesSetupCheck' => 'applications/config/check/PhabricatorRepositoriesSetupCheck.php',
     'PhabricatorRepository' => 'applications/repository/storage/PhabricatorRepository.php',
     'PhabricatorRepositoryArcanistProject' => 'applications/repository/storage/PhabricatorRepositoryArcanistProject.php',
     'PhabricatorRepositoryArcanistProjectDeleteController' => 'applications/repository/controller/PhabricatorRepositoryArcanistProjectDeleteController.php',
     'PhabricatorRepositoryArcanistProjectEditController' => 'applications/repository/controller/PhabricatorRepositoryArcanistProjectEditController.php',
     'PhabricatorRepositoryArcanistProjectPHIDType' => 'applications/repository/phid/PhabricatorRepositoryArcanistProjectPHIDType.php',
     'PhabricatorRepositoryArcanistProjectQuery' => 'applications/repository/query/PhabricatorRepositoryArcanistProjectQuery.php',
     'PhabricatorRepositoryAuditRequest' => 'applications/repository/storage/PhabricatorRepositoryAuditRequest.php',
     'PhabricatorRepositoryBranch' => 'applications/repository/storage/PhabricatorRepositoryBranch.php',
     'PhabricatorRepositoryCommit' => 'applications/repository/storage/PhabricatorRepositoryCommit.php',
     'PhabricatorRepositoryCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php',
     'PhabricatorRepositoryCommitData' => 'applications/repository/storage/PhabricatorRepositoryCommitData.php',
     'PhabricatorRepositoryCommitHeraldWorker' => 'applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php',
     'PhabricatorRepositoryCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php',
     'PhabricatorRepositoryCommitOwnersWorker' => 'applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php',
     'PhabricatorRepositoryCommitPHIDType' => 'applications/repository/phid/PhabricatorRepositoryCommitPHIDType.php',
     'PhabricatorRepositoryCommitParserWorker' => 'applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php',
     'PhabricatorRepositoryCommitRef' => 'applications/repository/engine/PhabricatorRepositoryCommitRef.php',
     'PhabricatorRepositoryCommitSearchIndexer' => 'applications/repository/search/PhabricatorRepositoryCommitSearchIndexer.php',
     'PhabricatorRepositoryConfigOptions' => 'applications/repository/config/PhabricatorRepositoryConfigOptions.php',
     'PhabricatorRepositoryController' => 'applications/repository/controller/PhabricatorRepositoryController.php',
     'PhabricatorRepositoryDAO' => 'applications/repository/storage/PhabricatorRepositoryDAO.php',
     'PhabricatorRepositoryDiscoveryEngine' => 'applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php',
     'PhabricatorRepositoryEditor' => 'applications/repository/editor/PhabricatorRepositoryEditor.php',
     'PhabricatorRepositoryEngine' => 'applications/repository/engine/PhabricatorRepositoryEngine.php',
     'PhabricatorRepositoryGitCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php',
     'PhabricatorRepositoryGitCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryGitCommitMessageParserWorker.php',
     'PhabricatorRepositoryGraphCache' => 'applications/repository/graphcache/PhabricatorRepositoryGraphCache.php',
     'PhabricatorRepositoryGraphStream' => 'applications/repository/daemon/PhabricatorRepositoryGraphStream.php',
     'PhabricatorRepositoryListController' => 'applications/repository/controller/PhabricatorRepositoryListController.php',
     'PhabricatorRepositoryManagementCacheWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementCacheWorkflow.php',
     'PhabricatorRepositoryManagementDiscoverWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementDiscoverWorkflow.php',
     'PhabricatorRepositoryManagementEditWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementEditWorkflow.php',
     'PhabricatorRepositoryManagementImportingWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementImportingWorkflow.php',
     'PhabricatorRepositoryManagementListWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementListWorkflow.php',
     'PhabricatorRepositoryManagementLookupUsersWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementLookupUsersWorkflow.php',
     'PhabricatorRepositoryManagementMarkImportedWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMarkImportedWorkflow.php',
     'PhabricatorRepositoryManagementMirrorWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMirrorWorkflow.php',
     'PhabricatorRepositoryManagementParentsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementParentsWorkflow.php',
     'PhabricatorRepositoryManagementPullWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementPullWorkflow.php',
     'PhabricatorRepositoryManagementRefsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementRefsWorkflow.php',
     'PhabricatorRepositoryManagementReparseWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementReparseWorkflow.php',
     'PhabricatorRepositoryManagementUpdateWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementUpdateWorkflow.php',
     'PhabricatorRepositoryManagementWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementWorkflow.php',
     'PhabricatorRepositoryMercurialCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryMercurialCommitChangeParserWorker.php',
     'PhabricatorRepositoryMercurialCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryMercurialCommitMessageParserWorker.php',
     'PhabricatorRepositoryMirror' => 'applications/repository/storage/PhabricatorRepositoryMirror.php',
     'PhabricatorRepositoryMirrorEngine' => 'applications/repository/engine/PhabricatorRepositoryMirrorEngine.php',
     'PhabricatorRepositoryMirrorPHIDType' => 'applications/repository/phid/PhabricatorRepositoryMirrorPHIDType.php',
     'PhabricatorRepositoryMirrorQuery' => 'applications/repository/query/PhabricatorRepositoryMirrorQuery.php',
     'PhabricatorRepositoryParsedChange' => 'applications/repository/data/PhabricatorRepositoryParsedChange.php',
     'PhabricatorRepositoryPullEngine' => 'applications/repository/engine/PhabricatorRepositoryPullEngine.php',
     'PhabricatorRepositoryPullLocalDaemon' => 'applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php',
     'PhabricatorRepositoryPushEvent' => 'applications/repository/storage/PhabricatorRepositoryPushEvent.php',
     'PhabricatorRepositoryPushEventPHIDType' => 'applications/repository/phid/PhabricatorRepositoryPushEventPHIDType.php',
     'PhabricatorRepositoryPushEventQuery' => 'applications/repository/query/PhabricatorRepositoryPushEventQuery.php',
     'PhabricatorRepositoryPushLog' => 'applications/repository/storage/PhabricatorRepositoryPushLog.php',
     'PhabricatorRepositoryPushLogPHIDType' => 'applications/repository/phid/PhabricatorRepositoryPushLogPHIDType.php',
     'PhabricatorRepositoryPushLogQuery' => 'applications/repository/query/PhabricatorRepositoryPushLogQuery.php',
     'PhabricatorRepositoryPushLogSearchEngine' => 'applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php',
     'PhabricatorRepositoryPushMailWorker' => 'applications/repository/worker/PhabricatorRepositoryPushMailWorker.php',
     'PhabricatorRepositoryPushReplyHandler' => 'applications/repository/mail/PhabricatorRepositoryPushReplyHandler.php',
     'PhabricatorRepositoryQuery' => 'applications/repository/query/PhabricatorRepositoryQuery.php',
     'PhabricatorRepositoryRefCursor' => 'applications/repository/storage/PhabricatorRepositoryRefCursor.php',
     'PhabricatorRepositoryRefCursorQuery' => 'applications/repository/query/PhabricatorRepositoryRefCursorQuery.php',
     'PhabricatorRepositoryRefEngine' => 'applications/repository/engine/PhabricatorRepositoryRefEngine.php',
     'PhabricatorRepositoryRepositoryPHIDType' => 'applications/repository/phid/PhabricatorRepositoryRepositoryPHIDType.php',
     'PhabricatorRepositorySchemaSpec' => 'applications/repository/storage/PhabricatorRepositorySchemaSpec.php',
     'PhabricatorRepositorySearchEngine' => 'applications/repository/query/PhabricatorRepositorySearchEngine.php',
     'PhabricatorRepositoryStatusMessage' => 'applications/repository/storage/PhabricatorRepositoryStatusMessage.php',
     'PhabricatorRepositorySvnCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositorySvnCommitChangeParserWorker.php',
     'PhabricatorRepositorySvnCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositorySvnCommitMessageParserWorker.php',
     'PhabricatorRepositorySymbol' => 'applications/repository/storage/PhabricatorRepositorySymbol.php',
     'PhabricatorRepositoryTestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php',
     'PhabricatorRepositoryTransaction' => 'applications/repository/storage/PhabricatorRepositoryTransaction.php',
     'PhabricatorRepositoryTransactionQuery' => 'applications/repository/query/PhabricatorRepositoryTransactionQuery.php',
     'PhabricatorRepositoryType' => 'applications/repository/constants/PhabricatorRepositoryType.php',
     'PhabricatorRepositoryURINormalizer' => 'applications/repository/data/PhabricatorRepositoryURINormalizer.php',
     'PhabricatorRepositoryURINormalizerTestCase' => 'applications/repository/data/__tests__/PhabricatorRepositoryURINormalizerTestCase.php',
     'PhabricatorRepositoryURITestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryURITestCase.php',
     'PhabricatorRepositoryVCSPassword' => 'applications/repository/storage/PhabricatorRepositoryVCSPassword.php',
     'PhabricatorRepositoryVersion' => 'applications/repository/constants/PhabricatorRepositoryVersion.php',
     'PhabricatorRobotsController' => 'applications/system/controller/PhabricatorRobotsController.php',
     'PhabricatorS3FileStorageEngine' => 'applications/files/engine/PhabricatorS3FileStorageEngine.php',
     'PhabricatorSMS' => 'infrastructure/sms/storage/PhabricatorSMS.php',
     'PhabricatorSMSConfigOptions' => 'applications/config/option/PhabricatorSMSConfigOptions.php',
     'PhabricatorSMSDAO' => 'infrastructure/sms/storage/PhabricatorSMSDAO.php',
     'PhabricatorSMSDemultiplexWorker' => 'infrastructure/sms/worker/PhabricatorSMSDemultiplexWorker.php',
     'PhabricatorSMSImplementationAdapter' => 'infrastructure/sms/adapter/PhabricatorSMSImplementationAdapter.php',
     'PhabricatorSMSImplementationTestBlackholeAdapter' => 'infrastructure/sms/adapter/PhabricatorSMSImplementationTestBlackholeAdapter.php',
     'PhabricatorSMSImplementationTwilioAdapter' => 'infrastructure/sms/adapter/PhabricatorSMSImplementationTwilioAdapter.php',
     'PhabricatorSMSManagementListOutboundWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementListOutboundWorkflow.php',
     'PhabricatorSMSManagementSendTestWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementSendTestWorkflow.php',
     'PhabricatorSMSManagementShowOutboundWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementShowOutboundWorkflow.php',
     'PhabricatorSMSManagementWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementWorkflow.php',
     'PhabricatorSMSSendWorker' => 'infrastructure/sms/worker/PhabricatorSMSSendWorker.php',
     'PhabricatorSMSWorker' => 'infrastructure/sms/worker/PhabricatorSMSWorker.php',
     'PhabricatorSQLPatchList' => 'infrastructure/storage/patch/PhabricatorSQLPatchList.php',
     'PhabricatorSSHKeyGenerator' => 'infrastructure/util/PhabricatorSSHKeyGenerator.php',
     'PhabricatorSSHKeysSettingsPanel' => 'applications/settings/panel/PhabricatorSSHKeysSettingsPanel.php',
     'PhabricatorSSHLog' => 'infrastructure/log/PhabricatorSSHLog.php',
     'PhabricatorSSHPassthruCommand' => 'infrastructure/ssh/PhabricatorSSHPassthruCommand.php',
     'PhabricatorSSHPublicKeyInterface' => 'applications/auth/sshkey/PhabricatorSSHPublicKeyInterface.php',
     'PhabricatorSSHWorkflow' => 'infrastructure/ssh/PhabricatorSSHWorkflow.php',
     'PhabricatorSavedQuery' => 'applications/search/storage/PhabricatorSavedQuery.php',
     'PhabricatorSavedQueryQuery' => 'applications/search/query/PhabricatorSavedQueryQuery.php',
     'PhabricatorScheduleTaskTriggerAction' => 'infrastructure/daemon/workers/action/PhabricatorScheduleTaskTriggerAction.php',
     'PhabricatorScopedEnv' => 'infrastructure/env/PhabricatorScopedEnv.php',
     'PhabricatorSearchAbstractDocument' => 'applications/search/index/PhabricatorSearchAbstractDocument.php',
     'PhabricatorSearchApplication' => 'applications/search/application/PhabricatorSearchApplication.php',
     'PhabricatorSearchApplicationSearchEngine' => 'applications/search/query/PhabricatorSearchApplicationSearchEngine.php',
     'PhabricatorSearchAttachController' => 'applications/search/controller/PhabricatorSearchAttachController.php',
     'PhabricatorSearchBaseController' => 'applications/search/controller/PhabricatorSearchBaseController.php',
     'PhabricatorSearchConfigOptions' => 'applications/search/config/PhabricatorSearchConfigOptions.php',
     'PhabricatorSearchController' => 'applications/search/controller/PhabricatorSearchController.php',
     'PhabricatorSearchDAO' => 'applications/search/storage/PhabricatorSearchDAO.php',
     'PhabricatorSearchDatasource' => 'applications/search/typeahead/PhabricatorSearchDatasource.php',
     'PhabricatorSearchDeleteController' => 'applications/search/controller/PhabricatorSearchDeleteController.php',
     'PhabricatorSearchDocument' => 'applications/search/storage/document/PhabricatorSearchDocument.php',
     'PhabricatorSearchDocumentField' => 'applications/search/storage/document/PhabricatorSearchDocumentField.php',
     'PhabricatorSearchDocumentIndexer' => 'applications/search/index/PhabricatorSearchDocumentIndexer.php',
     'PhabricatorSearchDocumentQuery' => 'applications/search/query/PhabricatorSearchDocumentQuery.php',
     'PhabricatorSearchDocumentRelationship' => 'applications/search/storage/document/PhabricatorSearchDocumentRelationship.php',
     'PhabricatorSearchDocumentTypeDatasource' => 'applications/search/typeahead/PhabricatorSearchDocumentTypeDatasource.php',
     'PhabricatorSearchEditController' => 'applications/search/controller/PhabricatorSearchEditController.php',
     'PhabricatorSearchEngine' => 'applications/search/engine/PhabricatorSearchEngine.php',
     'PhabricatorSearchEngineSelector' => 'applications/search/selector/PhabricatorSearchEngineSelector.php',
     'PhabricatorSearchField' => 'applications/search/constants/PhabricatorSearchField.php',
     'PhabricatorSearchHovercardController' => 'applications/search/controller/PhabricatorSearchHovercardController.php',
     'PhabricatorSearchIndexer' => 'applications/search/index/PhabricatorSearchIndexer.php',
     'PhabricatorSearchManagementIndexWorkflow' => 'applications/search/management/PhabricatorSearchManagementIndexWorkflow.php',
     'PhabricatorSearchManagementInitWorkflow' => 'applications/search/management/PhabricatorSearchManagementInitWorkflow.php',
     'PhabricatorSearchManagementWorkflow' => 'applications/search/management/PhabricatorSearchManagementWorkflow.php',
     'PhabricatorSearchOrderController' => 'applications/search/controller/PhabricatorSearchOrderController.php',
     'PhabricatorSearchPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorSearchPreferencesSettingsPanel.php',
     'PhabricatorSearchRelationship' => 'applications/search/constants/PhabricatorSearchRelationship.php',
     'PhabricatorSearchResultView' => 'applications/search/view/PhabricatorSearchResultView.php',
     'PhabricatorSearchSelectController' => 'applications/search/controller/PhabricatorSearchSelectController.php',
     'PhabricatorSearchWorker' => 'applications/search/worker/PhabricatorSearchWorker.php',
     'PhabricatorSecurityConfigOptions' => 'applications/config/option/PhabricatorSecurityConfigOptions.php',
     'PhabricatorSecuritySetupCheck' => 'applications/config/check/PhabricatorSecuritySetupCheck.php',
     'PhabricatorSendGridConfigOptions' => 'applications/config/option/PhabricatorSendGridConfigOptions.php',
     'PhabricatorSessionsSettingsPanel' => 'applications/settings/panel/PhabricatorSessionsSettingsPanel.php',
     'PhabricatorSettingsAddEmailAction' => 'applications/settings/action/PhabricatorSettingsAddEmailAction.php',
     'PhabricatorSettingsAdjustController' => 'applications/settings/controller/PhabricatorSettingsAdjustController.php',
     'PhabricatorSettingsApplication' => 'applications/settings/application/PhabricatorSettingsApplication.php',
     'PhabricatorSettingsMainController' => 'applications/settings/controller/PhabricatorSettingsMainController.php',
     'PhabricatorSettingsPanel' => 'applications/settings/panel/PhabricatorSettingsPanel.php',
     'PhabricatorSetupCheck' => 'applications/config/check/PhabricatorSetupCheck.php',
     'PhabricatorSetupIssue' => 'applications/config/issue/PhabricatorSetupIssue.php',
     'PhabricatorSetupIssueUIExample' => 'applications/uiexample/examples/PhabricatorSetupIssueUIExample.php',
     'PhabricatorSetupIssueView' => 'applications/config/view/PhabricatorSetupIssueView.php',
     'PhabricatorSlowvoteApplication' => 'applications/slowvote/application/PhabricatorSlowvoteApplication.php',
     'PhabricatorSlowvoteChoice' => 'applications/slowvote/storage/PhabricatorSlowvoteChoice.php',
     'PhabricatorSlowvoteCloseController' => 'applications/slowvote/controller/PhabricatorSlowvoteCloseController.php',
     'PhabricatorSlowvoteCommentController' => 'applications/slowvote/controller/PhabricatorSlowvoteCommentController.php',
     'PhabricatorSlowvoteController' => 'applications/slowvote/controller/PhabricatorSlowvoteController.php',
     'PhabricatorSlowvoteDAO' => 'applications/slowvote/storage/PhabricatorSlowvoteDAO.php',
     'PhabricatorSlowvoteDefaultViewCapability' => 'applications/slowvote/capability/PhabricatorSlowvoteDefaultViewCapability.php',
     'PhabricatorSlowvoteEditController' => 'applications/slowvote/controller/PhabricatorSlowvoteEditController.php',
     'PhabricatorSlowvoteEditor' => 'applications/slowvote/editor/PhabricatorSlowvoteEditor.php',
     'PhabricatorSlowvoteListController' => 'applications/slowvote/controller/PhabricatorSlowvoteListController.php',
     'PhabricatorSlowvoteOption' => 'applications/slowvote/storage/PhabricatorSlowvoteOption.php',
     'PhabricatorSlowvotePoll' => 'applications/slowvote/storage/PhabricatorSlowvotePoll.php',
     'PhabricatorSlowvotePollController' => 'applications/slowvote/controller/PhabricatorSlowvotePollController.php',
     'PhabricatorSlowvotePollPHIDType' => 'applications/slowvote/phid/PhabricatorSlowvotePollPHIDType.php',
     'PhabricatorSlowvoteQuery' => 'applications/slowvote/query/PhabricatorSlowvoteQuery.php',
     'PhabricatorSlowvoteSchemaSpec' => 'applications/slowvote/storage/PhabricatorSlowvoteSchemaSpec.php',
     'PhabricatorSlowvoteSearchEngine' => 'applications/slowvote/query/PhabricatorSlowvoteSearchEngine.php',
     'PhabricatorSlowvoteTransaction' => 'applications/slowvote/storage/PhabricatorSlowvoteTransaction.php',
     'PhabricatorSlowvoteTransactionComment' => 'applications/slowvote/storage/PhabricatorSlowvoteTransactionComment.php',
     'PhabricatorSlowvoteTransactionQuery' => 'applications/slowvote/query/PhabricatorSlowvoteTransactionQuery.php',
     'PhabricatorSlowvoteVoteController' => 'applications/slowvote/controller/PhabricatorSlowvoteVoteController.php',
     'PhabricatorSlug' => 'infrastructure/util/PhabricatorSlug.php',
     'PhabricatorSlugTestCase' => 'infrastructure/util/__tests__/PhabricatorSlugTestCase.php',
     'PhabricatorSortTableUIExample' => 'applications/uiexample/examples/PhabricatorSortTableUIExample.php',
     'PhabricatorSourceCodeView' => 'view/layout/PhabricatorSourceCodeView.php',
     'PhabricatorStandardCustomField' => 'infrastructure/customfield/standard/PhabricatorStandardCustomField.php',
     'PhabricatorStandardCustomFieldBool' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php',
     'PhabricatorStandardCustomFieldCredential' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldCredential.php',
     'PhabricatorStandardCustomFieldDate' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php',
     'PhabricatorStandardCustomFieldHeader' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldHeader.php',
     'PhabricatorStandardCustomFieldInt' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php',
     'PhabricatorStandardCustomFieldInterface' => 'infrastructure/customfield/interface/PhabricatorStandardCustomFieldInterface.php',
     'PhabricatorStandardCustomFieldLink' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php',
     'PhabricatorStandardCustomFieldPHIDs' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php',
     'PhabricatorStandardCustomFieldRemarkup' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php',
     'PhabricatorStandardCustomFieldSelect' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php',
     'PhabricatorStandardCustomFieldText' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php',
     'PhabricatorStandardCustomFieldUsers' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldUsers.php',
     'PhabricatorStandardPageView' => 'view/page/PhabricatorStandardPageView.php',
     'PhabricatorStatusController' => 'applications/system/controller/PhabricatorStatusController.php',
     'PhabricatorStatusUIExample' => 'applications/uiexample/examples/PhabricatorStatusUIExample.php',
     'PhabricatorStorageFixtureScopeGuard' => 'infrastructure/testing/fixture/PhabricatorStorageFixtureScopeGuard.php',
     'PhabricatorStorageManagementAPI' => 'infrastructure/storage/management/PhabricatorStorageManagementAPI.php',
     'PhabricatorStorageManagementAdjustWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementAdjustWorkflow.php',
     'PhabricatorStorageManagementDatabasesWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDatabasesWorkflow.php',
     'PhabricatorStorageManagementDestroyWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php',
     'PhabricatorStorageManagementDumpWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php',
     'PhabricatorStorageManagementProbeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementProbeWorkflow.php',
     'PhabricatorStorageManagementQuickstartWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementQuickstartWorkflow.php',
     'PhabricatorStorageManagementRenamespaceWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementRenamespaceWorkflow.php',
     'PhabricatorStorageManagementShellWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementShellWorkflow.php',
     'PhabricatorStorageManagementStatusWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementStatusWorkflow.php',
     'PhabricatorStorageManagementUpgradeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php',
     'PhabricatorStorageManagementWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php',
     'PhabricatorStoragePatch' => 'infrastructure/storage/management/PhabricatorStoragePatch.php',
     'PhabricatorStorageSchemaSpec' => 'infrastructure/storage/schema/PhabricatorStorageSchemaSpec.php',
     'PhabricatorStorageSetupCheck' => 'applications/config/check/PhabricatorStorageSetupCheck.php',
     'PhabricatorSubscribableInterface' => 'applications/subscriptions/interface/PhabricatorSubscribableInterface.php',
     'PhabricatorSubscribedToObjectEdgeType' => 'applications/transactions/edges/PhabricatorSubscribedToObjectEdgeType.php',
     'PhabricatorSubscribersQuery' => 'applications/subscriptions/query/PhabricatorSubscribersQuery.php',
     'PhabricatorSubscriptionTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorSubscriptionTriggerClock.php',
     'PhabricatorSubscriptionsApplication' => 'applications/subscriptions/application/PhabricatorSubscriptionsApplication.php',
     'PhabricatorSubscriptionsEditController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php',
     'PhabricatorSubscriptionsEditor' => 'applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php',
     'PhabricatorSubscriptionsListController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsListController.php',
     'PhabricatorSubscriptionsSubscribeEmailCommand' => 'applications/subscriptions/command/PhabricatorSubscriptionsSubscribeEmailCommand.php',
     'PhabricatorSubscriptionsTransactionController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsTransactionController.php',
     'PhabricatorSubscriptionsUIEventListener' => 'applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php',
     'PhabricatorSubscriptionsUnsubscribeEmailCommand' => 'applications/subscriptions/command/PhabricatorSubscriptionsUnsubscribeEmailCommand.php',
     'PhabricatorSupportApplication' => 'applications/support/application/PhabricatorSupportApplication.php',
     'PhabricatorSyntaxHighlighter' => 'infrastructure/markup/PhabricatorSyntaxHighlighter.php',
     'PhabricatorSyntaxHighlightingConfigOptions' => 'applications/config/option/PhabricatorSyntaxHighlightingConfigOptions.php',
     'PhabricatorSystemAction' => 'applications/system/action/PhabricatorSystemAction.php',
     'PhabricatorSystemActionEngine' => 'applications/system/engine/PhabricatorSystemActionEngine.php',
     'PhabricatorSystemActionGarbageCollector' => 'applications/system/garbagecollector/PhabricatorSystemActionGarbageCollector.php',
     'PhabricatorSystemActionLog' => 'applications/system/storage/PhabricatorSystemActionLog.php',
     'PhabricatorSystemActionRateLimitException' => 'applications/system/exception/PhabricatorSystemActionRateLimitException.php',
     'PhabricatorSystemApplication' => 'applications/system/application/PhabricatorSystemApplication.php',
     'PhabricatorSystemDAO' => 'applications/system/storage/PhabricatorSystemDAO.php',
     'PhabricatorSystemDestructionGarbageCollector' => 'applications/system/garbagecollector/PhabricatorSystemDestructionGarbageCollector.php',
     'PhabricatorSystemDestructionLog' => 'applications/system/storage/PhabricatorSystemDestructionLog.php',
     'PhabricatorSystemRemoveDestroyWorkflow' => 'applications/system/management/PhabricatorSystemRemoveDestroyWorkflow.php',
     'PhabricatorSystemRemoveLogWorkflow' => 'applications/system/management/PhabricatorSystemRemoveLogWorkflow.php',
     'PhabricatorSystemRemoveWorkflow' => 'applications/system/management/PhabricatorSystemRemoveWorkflow.php',
     'PhabricatorSystemSelectEncodingController' => 'applications/system/controller/PhabricatorSystemSelectEncodingController.php',
     'PhabricatorSystemSelectHighlightController' => 'applications/system/controller/PhabricatorSystemSelectHighlightController.php',
     'PhabricatorTOTPAuthFactor' => 'applications/auth/factor/PhabricatorTOTPAuthFactor.php',
     'PhabricatorTOTPAuthFactorTestCase' => 'applications/auth/factor/__tests__/PhabricatorTOTPAuthFactorTestCase.php',
     'PhabricatorTaskmasterDaemon' => 'infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php',
     'PhabricatorTestApplication' => 'applications/base/controller/__tests__/PhabricatorTestApplication.php',
     'PhabricatorTestCase' => 'infrastructure/testing/PhabricatorTestCase.php',
     'PhabricatorTestController' => 'applications/base/controller/__tests__/PhabricatorTestController.php',
     'PhabricatorTestDataGenerator' => 'applications/lipsum/generator/PhabricatorTestDataGenerator.php',
     'PhabricatorTestNoCycleEdgeType' => 'applications/transactions/edges/PhabricatorTestNoCycleEdgeType.php',
     'PhabricatorTestStorageEngine' => 'applications/files/engine/PhabricatorTestStorageEngine.php',
     'PhabricatorTestWorker' => 'infrastructure/daemon/workers/__tests__/PhabricatorTestWorker.php',
     'PhabricatorTime' => 'infrastructure/time/PhabricatorTime.php',
     'PhabricatorTimeGuard' => 'infrastructure/time/PhabricatorTimeGuard.php',
     'PhabricatorTimeTestCase' => 'infrastructure/time/__tests__/PhabricatorTimeTestCase.php',
     'PhabricatorTimezoneSetupCheck' => 'applications/config/check/PhabricatorTimezoneSetupCheck.php',
     'PhabricatorToken' => 'applications/tokens/storage/PhabricatorToken.php',
     'PhabricatorTokenController' => 'applications/tokens/controller/PhabricatorTokenController.php',
     'PhabricatorTokenCount' => 'applications/tokens/storage/PhabricatorTokenCount.php',
     'PhabricatorTokenCountQuery' => 'applications/tokens/query/PhabricatorTokenCountQuery.php',
     'PhabricatorTokenDAO' => 'applications/tokens/storage/PhabricatorTokenDAO.php',
     'PhabricatorTokenGiveController' => 'applications/tokens/controller/PhabricatorTokenGiveController.php',
     'PhabricatorTokenGiven' => 'applications/tokens/storage/PhabricatorTokenGiven.php',
     'PhabricatorTokenGivenController' => 'applications/tokens/controller/PhabricatorTokenGivenController.php',
     'PhabricatorTokenGivenEditor' => 'applications/tokens/editor/PhabricatorTokenGivenEditor.php',
     'PhabricatorTokenGivenFeedStory' => 'applications/tokens/feed/PhabricatorTokenGivenFeedStory.php',
     'PhabricatorTokenGivenQuery' => 'applications/tokens/query/PhabricatorTokenGivenQuery.php',
     'PhabricatorTokenLeaderController' => 'applications/tokens/controller/PhabricatorTokenLeaderController.php',
     'PhabricatorTokenQuery' => 'applications/tokens/query/PhabricatorTokenQuery.php',
     'PhabricatorTokenReceiverInterface' => 'applications/tokens/interface/PhabricatorTokenReceiverInterface.php',
     'PhabricatorTokenReceiverQuery' => 'applications/tokens/query/PhabricatorTokenReceiverQuery.php',
     'PhabricatorTokenTokenPHIDType' => 'applications/tokens/phid/PhabricatorTokenTokenPHIDType.php',
     'PhabricatorTokenUIEventListener' => 'applications/tokens/event/PhabricatorTokenUIEventListener.php',
     'PhabricatorTokensApplication' => 'applications/tokens/application/PhabricatorTokensApplication.php',
     'PhabricatorTokensSettingsPanel' => 'applications/settings/panel/PhabricatorTokensSettingsPanel.php',
     'PhabricatorTooltipUIExample' => 'applications/uiexample/examples/PhabricatorTooltipUIExample.php',
     'PhabricatorTransactions' => 'applications/transactions/constants/PhabricatorTransactions.php',
     'PhabricatorTransactionsApplication' => 'applications/transactions/application/PhabricatorTransactionsApplication.php',
     'PhabricatorTransformedFile' => 'applications/files/storage/PhabricatorTransformedFile.php',
     'PhabricatorTranslationsConfigOptions' => 'applications/config/option/PhabricatorTranslationsConfigOptions.php',
     'PhabricatorTriggerAction' => 'infrastructure/daemon/workers/action/PhabricatorTriggerAction.php',
     'PhabricatorTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorTriggerClock.php',
     'PhabricatorTriggerClockTestCase' => 'infrastructure/daemon/workers/clock/__tests__/PhabricatorTriggerClockTestCase.php',
     'PhabricatorTriggerDaemon' => 'infrastructure/daemon/workers/PhabricatorTriggerDaemon.php',
     'PhabricatorTrivialTestCase' => 'infrastructure/testing/__tests__/PhabricatorTrivialTestCase.php',
     'PhabricatorTwitchAuthProvider' => 'applications/auth/provider/PhabricatorTwitchAuthProvider.php',
     'PhabricatorTwitterAuthProvider' => 'applications/auth/provider/PhabricatorTwitterAuthProvider.php',
     'PhabricatorTwoColumnUIExample' => 'applications/uiexample/examples/PhabricatorTwoColumnUIExample.php',
     'PhabricatorTypeaheadApplication' => 'applications/typeahead/application/PhabricatorTypeaheadApplication.php',
     'PhabricatorTypeaheadCompositeDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php',
     'PhabricatorTypeaheadDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php',
     'PhabricatorTypeaheadDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadDatasourceController.php',
     'PhabricatorTypeaheadFunctionHelpController' => 'applications/typeahead/controller/PhabricatorTypeaheadFunctionHelpController.php',
     'PhabricatorTypeaheadInvalidTokenException' => 'applications/typeahead/exception/PhabricatorTypeaheadInvalidTokenException.php',
     'PhabricatorTypeaheadModularDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php',
     'PhabricatorTypeaheadMonogramDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadMonogramDatasource.php',
     'PhabricatorTypeaheadResult' => 'applications/typeahead/storage/PhabricatorTypeaheadResult.php',
     'PhabricatorTypeaheadRuntimeCompositeDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadRuntimeCompositeDatasource.php',
     'PhabricatorTypeaheadTokenView' => 'applications/typeahead/view/PhabricatorTypeaheadTokenView.php',
     'PhabricatorUIConfigOptions' => 'applications/config/option/PhabricatorUIConfigOptions.php',
     'PhabricatorUIExample' => 'applications/uiexample/examples/PhabricatorUIExample.php',
     'PhabricatorUIExampleRenderController' => 'applications/uiexample/controller/PhabricatorUIExampleRenderController.php',
     'PhabricatorUIExamplesApplication' => 'applications/uiexample/application/PhabricatorUIExamplesApplication.php',
     'PhabricatorUSEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php',
     'PhabricatorUnitsTestCase' => 'view/__tests__/PhabricatorUnitsTestCase.php',
     'PhabricatorUnsubscribedFromObjectEdgeType' => 'applications/transactions/edges/PhabricatorUnsubscribedFromObjectEdgeType.php',
     'PhabricatorUser' => 'applications/people/storage/PhabricatorUser.php',
     'PhabricatorUserBlurbField' => 'applications/people/customfield/PhabricatorUserBlurbField.php',
     'PhabricatorUserConfigOptions' => 'applications/people/config/PhabricatorUserConfigOptions.php',
     'PhabricatorUserConfiguredCustomField' => 'applications/people/customfield/PhabricatorUserConfiguredCustomField.php',
     'PhabricatorUserConfiguredCustomFieldStorage' => 'applications/people/storage/PhabricatorUserConfiguredCustomFieldStorage.php',
     'PhabricatorUserCustomField' => 'applications/people/customfield/PhabricatorUserCustomField.php',
     'PhabricatorUserCustomFieldNumericIndex' => 'applications/people/storage/PhabricatorUserCustomFieldNumericIndex.php',
     'PhabricatorUserCustomFieldStringIndex' => 'applications/people/storage/PhabricatorUserCustomFieldStringIndex.php',
     'PhabricatorUserDAO' => 'applications/people/storage/PhabricatorUserDAO.php',
     'PhabricatorUserEditor' => 'applications/people/editor/PhabricatorUserEditor.php',
     'PhabricatorUserEditorTestCase' => 'applications/people/editor/__tests__/PhabricatorUserEditorTestCase.php',
     'PhabricatorUserEmail' => 'applications/people/storage/PhabricatorUserEmail.php',
     'PhabricatorUserEmailTestCase' => 'applications/people/storage/__tests__/PhabricatorUserEmailTestCase.php',
     'PhabricatorUserLog' => 'applications/people/storage/PhabricatorUserLog.php',
     'PhabricatorUserLogView' => 'applications/people/view/PhabricatorUserLogView.php',
     'PhabricatorUserPreferences' => 'applications/settings/storage/PhabricatorUserPreferences.php',
     'PhabricatorUserProfile' => 'applications/people/storage/PhabricatorUserProfile.php',
     'PhabricatorUserProfileEditor' => 'applications/people/editor/PhabricatorUserProfileEditor.php',
     'PhabricatorUserRealNameField' => 'applications/people/customfield/PhabricatorUserRealNameField.php',
     'PhabricatorUserRolesField' => 'applications/people/customfield/PhabricatorUserRolesField.php',
     'PhabricatorUserSchemaSpec' => 'applications/people/storage/PhabricatorUserSchemaSpec.php',
     'PhabricatorUserSearchIndexer' => 'applications/people/search/PhabricatorUserSearchIndexer.php',
     'PhabricatorUserSinceField' => 'applications/people/customfield/PhabricatorUserSinceField.php',
     'PhabricatorUserStatusField' => 'applications/people/customfield/PhabricatorUserStatusField.php',
     'PhabricatorUserTestCase' => 'applications/people/storage/__tests__/PhabricatorUserTestCase.php',
     'PhabricatorUserTitleField' => 'applications/people/customfield/PhabricatorUserTitleField.php',
     'PhabricatorUserTransaction' => 'applications/people/storage/PhabricatorUserTransaction.php',
     'PhabricatorUsersPolicyRule' => 'applications/policy/rule/PhabricatorUsersPolicyRule.php',
     'PhabricatorVCSResponse' => 'applications/repository/response/PhabricatorVCSResponse.php',
     'PhabricatorVeryWowEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorVeryWowEnglishTranslation.php',
     'PhabricatorViewerDatasource' => 'applications/people/typeahead/PhabricatorViewerDatasource.php',
     'PhabricatorWatcherHasObjectEdgeType' => 'applications/transactions/edges/PhabricatorWatcherHasObjectEdgeType.php',
     'PhabricatorWordPressAuthProvider' => 'applications/auth/provider/PhabricatorWordPressAuthProvider.php',
     'PhabricatorWorker' => 'infrastructure/daemon/workers/PhabricatorWorker.php',
     'PhabricatorWorkerActiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php',
     'PhabricatorWorkerArchiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php',
     'PhabricatorWorkerArchiveTaskQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerArchiveTaskQuery.php',
     'PhabricatorWorkerDAO' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerDAO.php',
     'PhabricatorWorkerLeaseQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php',
     'PhabricatorWorkerManagementCancelWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementCancelWorkflow.php',
     'PhabricatorWorkerManagementExecuteWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementExecuteWorkflow.php',
     'PhabricatorWorkerManagementFloodWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementFloodWorkflow.php',
     'PhabricatorWorkerManagementFreeWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementFreeWorkflow.php',
     'PhabricatorWorkerManagementRetryWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementRetryWorkflow.php',
     'PhabricatorWorkerManagementWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementWorkflow.php',
     'PhabricatorWorkerPermanentFailureException' => 'infrastructure/daemon/workers/exception/PhabricatorWorkerPermanentFailureException.php',
     'PhabricatorWorkerTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTask.php',
     'PhabricatorWorkerTaskData' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTaskData.php',
     'PhabricatorWorkerTaskDetailController' => 'applications/daemon/controller/PhabricatorWorkerTaskDetailController.php',
     'PhabricatorWorkerTestCase' => 'infrastructure/daemon/workers/__tests__/PhabricatorWorkerTestCase.php',
     'PhabricatorWorkerTrigger' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTrigger.php',
     'PhabricatorWorkerTriggerEvent' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTriggerEvent.php',
     'PhabricatorWorkerTriggerManagementFireWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementFireWorkflow.php',
     'PhabricatorWorkerTriggerManagementWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementWorkflow.php',
     'PhabricatorWorkerTriggerPHIDType' => 'infrastructure/daemon/workers/phid/PhabricatorWorkerTriggerPHIDType.php',
     'PhabricatorWorkerTriggerQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php',
     'PhabricatorWorkerYieldException' => 'infrastructure/daemon/workers/exception/PhabricatorWorkerYieldException.php',
     'PhabricatorWorkingCopyDiscoveryTestCase' => 'applications/repository/engine/__tests__/PhabricatorWorkingCopyDiscoveryTestCase.php',
     'PhabricatorWorkingCopyPullTestCase' => 'applications/repository/engine/__tests__/PhabricatorWorkingCopyPullTestCase.php',
     'PhabricatorWorkingCopyTestCase' => 'applications/repository/engine/__tests__/PhabricatorWorkingCopyTestCase.php',
     'PhabricatorXHPASTViewController' => 'applications/phpast/controller/PhabricatorXHPASTViewController.php',
     'PhabricatorXHPASTViewDAO' => 'applications/phpast/storage/PhabricatorXHPASTViewDAO.php',
     'PhabricatorXHPASTViewFrameController' => 'applications/phpast/controller/PhabricatorXHPASTViewFrameController.php',
     'PhabricatorXHPASTViewFramesetController' => 'applications/phpast/controller/PhabricatorXHPASTViewFramesetController.php',
     'PhabricatorXHPASTViewInputController' => 'applications/phpast/controller/PhabricatorXHPASTViewInputController.php',
     'PhabricatorXHPASTViewPanelController' => 'applications/phpast/controller/PhabricatorXHPASTViewPanelController.php',
     'PhabricatorXHPASTViewParseTree' => 'applications/phpast/storage/PhabricatorXHPASTViewParseTree.php',
     'PhabricatorXHPASTViewRunController' => 'applications/phpast/controller/PhabricatorXHPASTViewRunController.php',
     'PhabricatorXHPASTViewStreamController' => 'applications/phpast/controller/PhabricatorXHPASTViewStreamController.php',
     'PhabricatorXHPASTViewTreeController' => 'applications/phpast/controller/PhabricatorXHPASTViewTreeController.php',
     'PhabricatorXHProfApplication' => 'applications/xhprof/application/PhabricatorXHProfApplication.php',
     'PhabricatorXHProfController' => 'applications/xhprof/controller/PhabricatorXHProfController.php',
     'PhabricatorXHProfDAO' => 'applications/xhprof/storage/PhabricatorXHProfDAO.php',
     'PhabricatorXHProfProfileController' => 'applications/xhprof/controller/PhabricatorXHProfProfileController.php',
     'PhabricatorXHProfProfileSymbolView' => 'applications/xhprof/view/PhabricatorXHProfProfileSymbolView.php',
     'PhabricatorXHProfProfileTopLevelView' => 'applications/xhprof/view/PhabricatorXHProfProfileTopLevelView.php',
     'PhabricatorXHProfProfileView' => 'applications/xhprof/view/PhabricatorXHProfProfileView.php',
     'PhabricatorXHProfSample' => 'applications/xhprof/storage/PhabricatorXHProfSample.php',
     'PhabricatorXHProfSampleListController' => 'applications/xhprof/controller/PhabricatorXHProfSampleListController.php',
     'PhabricatorYoutubeRemarkupRule' => 'infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php',
     'PhameBasicBlogSkin' => 'applications/phame/skins/PhameBasicBlogSkin.php',
     'PhameBasicTemplateBlogSkin' => 'applications/phame/skins/PhameBasicTemplateBlogSkin.php',
     'PhameBlog' => 'applications/phame/storage/PhameBlog.php',
     'PhameBlogDeleteController' => 'applications/phame/controller/blog/PhameBlogDeleteController.php',
     'PhameBlogEditController' => 'applications/phame/controller/blog/PhameBlogEditController.php',
     'PhameBlogFeedController' => 'applications/phame/controller/blog/PhameBlogFeedController.php',
     'PhameBlogListController' => 'applications/phame/controller/blog/PhameBlogListController.php',
     'PhameBlogLiveController' => 'applications/phame/controller/blog/PhameBlogLiveController.php',
     'PhameBlogQuery' => 'applications/phame/query/PhameBlogQuery.php',
     'PhameBlogSkin' => 'applications/phame/skins/PhameBlogSkin.php',
     'PhameBlogViewController' => 'applications/phame/controller/blog/PhameBlogViewController.php',
     'PhameCelerityResources' => 'applications/phame/celerity/PhameCelerityResources.php',
     'PhameConduitAPIMethod' => 'applications/phame/conduit/PhameConduitAPIMethod.php',
     'PhameController' => 'applications/phame/controller/PhameController.php',
     'PhameCreatePostConduitAPIMethod' => 'applications/phame/conduit/PhameCreatePostConduitAPIMethod.php',
     'PhameDAO' => 'applications/phame/storage/PhameDAO.php',
     'PhamePost' => 'applications/phame/storage/PhamePost.php',
     'PhamePostDeleteController' => 'applications/phame/controller/post/PhamePostDeleteController.php',
     'PhamePostEditController' => 'applications/phame/controller/post/PhamePostEditController.php',
     'PhamePostFramedController' => 'applications/phame/controller/post/PhamePostFramedController.php',
     'PhamePostListController' => 'applications/phame/controller/post/PhamePostListController.php',
     'PhamePostNewController' => 'applications/phame/controller/post/PhamePostNewController.php',
     'PhamePostNotLiveController' => 'applications/phame/controller/post/PhamePostNotLiveController.php',
     'PhamePostPreviewController' => 'applications/phame/controller/post/PhamePostPreviewController.php',
     'PhamePostPublishController' => 'applications/phame/controller/post/PhamePostPublishController.php',
     'PhamePostQuery' => 'applications/phame/query/PhamePostQuery.php',
     'PhamePostUnpublishController' => 'applications/phame/controller/post/PhamePostUnpublishController.php',
     'PhamePostView' => 'applications/phame/view/PhamePostView.php',
     'PhamePostViewController' => 'applications/phame/controller/post/PhamePostViewController.php',
     'PhameQueryConduitAPIMethod' => 'applications/phame/conduit/PhameQueryConduitAPIMethod.php',
     'PhameQueryPostsConduitAPIMethod' => 'applications/phame/conduit/PhameQueryPostsConduitAPIMethod.php',
     'PhameResourceController' => 'applications/phame/controller/PhameResourceController.php',
     'PhameSchemaSpec' => 'applications/phame/storage/PhameSchemaSpec.php',
     'PhameSkinSpecification' => 'applications/phame/skins/PhameSkinSpecification.php',
     'PhluxController' => 'applications/phlux/controller/PhluxController.php',
     'PhluxDAO' => 'applications/phlux/storage/PhluxDAO.php',
     'PhluxEditController' => 'applications/phlux/controller/PhluxEditController.php',
     'PhluxListController' => 'applications/phlux/controller/PhluxListController.php',
     'PhluxTransaction' => 'applications/phlux/storage/PhluxTransaction.php',
     'PhluxTransactionQuery' => 'applications/phlux/query/PhluxTransactionQuery.php',
     'PhluxVariable' => 'applications/phlux/storage/PhluxVariable.php',
     'PhluxVariableEditor' => 'applications/phlux/editor/PhluxVariableEditor.php',
     'PhluxVariablePHIDType' => 'applications/phlux/phid/PhluxVariablePHIDType.php',
     'PhluxVariableQuery' => 'applications/phlux/query/PhluxVariableQuery.php',
     'PhluxViewController' => 'applications/phlux/controller/PhluxViewController.php',
     'PholioActionMenuEventListener' => 'applications/pholio/event/PholioActionMenuEventListener.php',
     'PholioConstants' => 'applications/pholio/constants/PholioConstants.php',
     'PholioController' => 'applications/pholio/controller/PholioController.php',
     'PholioDAO' => 'applications/pholio/storage/PholioDAO.php',
     'PholioDefaultEditCapability' => 'applications/pholio/capability/PholioDefaultEditCapability.php',
     'PholioDefaultViewCapability' => 'applications/pholio/capability/PholioDefaultViewCapability.php',
     'PholioImage' => 'applications/pholio/storage/PholioImage.php',
     'PholioImagePHIDType' => 'applications/pholio/phid/PholioImagePHIDType.php',
     'PholioImageQuery' => 'applications/pholio/query/PholioImageQuery.php',
     'PholioImageUploadController' => 'applications/pholio/controller/PholioImageUploadController.php',
     'PholioInlineController' => 'applications/pholio/controller/PholioInlineController.php',
     'PholioInlineListController' => 'applications/pholio/controller/PholioInlineListController.php',
-    'PholioInlineThumbController' => 'applications/pholio/controller/PholioInlineThumbController.php',
     'PholioMock' => 'applications/pholio/storage/PholioMock.php',
     'PholioMockCommentController' => 'applications/pholio/controller/PholioMockCommentController.php',
     'PholioMockEditController' => 'applications/pholio/controller/PholioMockEditController.php',
     'PholioMockEditor' => 'applications/pholio/editor/PholioMockEditor.php',
     'PholioMockEmbedView' => 'applications/pholio/view/PholioMockEmbedView.php',
     'PholioMockHasTaskEdgeType' => 'applications/pholio/edge/PholioMockHasTaskEdgeType.php',
     'PholioMockImagesView' => 'applications/pholio/view/PholioMockImagesView.php',
     'PholioMockListController' => 'applications/pholio/controller/PholioMockListController.php',
     'PholioMockMailReceiver' => 'applications/pholio/mail/PholioMockMailReceiver.php',
     'PholioMockPHIDType' => 'applications/pholio/phid/PholioMockPHIDType.php',
     'PholioMockQuery' => 'applications/pholio/query/PholioMockQuery.php',
     'PholioMockSearchEngine' => 'applications/pholio/query/PholioMockSearchEngine.php',
     'PholioMockThumbGridView' => 'applications/pholio/view/PholioMockThumbGridView.php',
     'PholioMockViewController' => 'applications/pholio/controller/PholioMockViewController.php',
     'PholioRemarkupRule' => 'applications/pholio/remarkup/PholioRemarkupRule.php',
     'PholioReplyHandler' => 'applications/pholio/mail/PholioReplyHandler.php',
     'PholioSchemaSpec' => 'applications/pholio/storage/PholioSchemaSpec.php',
     'PholioSearchIndexer' => 'applications/pholio/search/PholioSearchIndexer.php',
     'PholioTransaction' => 'applications/pholio/storage/PholioTransaction.php',
     'PholioTransactionComment' => 'applications/pholio/storage/PholioTransactionComment.php',
     'PholioTransactionQuery' => 'applications/pholio/query/PholioTransactionQuery.php',
     'PholioTransactionType' => 'applications/pholio/constants/PholioTransactionType.php',
     'PholioTransactionView' => 'applications/pholio/view/PholioTransactionView.php',
     'PholioUploadedImageView' => 'applications/pholio/view/PholioUploadedImageView.php',
     'PhortuneAccount' => 'applications/phortune/storage/PhortuneAccount.php',
     'PhortuneAccountEditController' => 'applications/phortune/controller/PhortuneAccountEditController.php',
     'PhortuneAccountEditor' => 'applications/phortune/editor/PhortuneAccountEditor.php',
     'PhortuneAccountHasMemberEdgeType' => 'applications/phortune/edge/PhortuneAccountHasMemberEdgeType.php',
     'PhortuneAccountListController' => 'applications/phortune/controller/PhortuneAccountListController.php',
     'PhortuneAccountPHIDType' => 'applications/phortune/phid/PhortuneAccountPHIDType.php',
     'PhortuneAccountQuery' => 'applications/phortune/query/PhortuneAccountQuery.php',
     'PhortuneAccountTransaction' => 'applications/phortune/storage/PhortuneAccountTransaction.php',
     'PhortuneAccountTransactionQuery' => 'applications/phortune/query/PhortuneAccountTransactionQuery.php',
     'PhortuneAccountViewController' => 'applications/phortune/controller/PhortuneAccountViewController.php',
     'PhortuneAdHocCart' => 'applications/phortune/cart/PhortuneAdHocCart.php',
     'PhortuneAdHocProduct' => 'applications/phortune/product/PhortuneAdHocProduct.php',
     'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php',
     'PhortuneCartAcceptController' => 'applications/phortune/controller/PhortuneCartAcceptController.php',
     'PhortuneCartCancelController' => 'applications/phortune/controller/PhortuneCartCancelController.php',
     'PhortuneCartCheckoutController' => 'applications/phortune/controller/PhortuneCartCheckoutController.php',
     'PhortuneCartController' => 'applications/phortune/controller/PhortuneCartController.php',
     'PhortuneCartEditor' => 'applications/phortune/editor/PhortuneCartEditor.php',
     'PhortuneCartImplementation' => 'applications/phortune/cart/PhortuneCartImplementation.php',
     'PhortuneCartListController' => 'applications/phortune/controller/PhortuneCartListController.php',
     'PhortuneCartPHIDType' => 'applications/phortune/phid/PhortuneCartPHIDType.php',
     'PhortuneCartQuery' => 'applications/phortune/query/PhortuneCartQuery.php',
     'PhortuneCartReplyHandler' => 'applications/phortune/mail/PhortuneCartReplyHandler.php',
     'PhortuneCartSearchEngine' => 'applications/phortune/query/PhortuneCartSearchEngine.php',
     'PhortuneCartTransaction' => 'applications/phortune/storage/PhortuneCartTransaction.php',
     'PhortuneCartTransactionQuery' => 'applications/phortune/query/PhortuneCartTransactionQuery.php',
     'PhortuneCartUpdateController' => 'applications/phortune/controller/PhortuneCartUpdateController.php',
     'PhortuneCartViewController' => 'applications/phortune/controller/PhortuneCartViewController.php',
     'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php',
     'PhortuneChargeListController' => 'applications/phortune/controller/PhortuneChargeListController.php',
     'PhortuneChargePHIDType' => 'applications/phortune/phid/PhortuneChargePHIDType.php',
     'PhortuneChargeQuery' => 'applications/phortune/query/PhortuneChargeQuery.php',
     'PhortuneChargeSearchEngine' => 'applications/phortune/query/PhortuneChargeSearchEngine.php',
     'PhortuneChargeTableView' => 'applications/phortune/view/PhortuneChargeTableView.php',
     'PhortuneConstants' => 'applications/phortune/constants/PhortuneConstants.php',
     'PhortuneController' => 'applications/phortune/controller/PhortuneController.php',
     'PhortuneCreditCardForm' => 'applications/phortune/view/PhortuneCreditCardForm.php',
     'PhortuneCurrency' => 'applications/phortune/currency/PhortuneCurrency.php',
     'PhortuneCurrencySerializer' => 'applications/phortune/currency/PhortuneCurrencySerializer.php',
     'PhortuneCurrencyTestCase' => 'applications/phortune/currency/__tests__/PhortuneCurrencyTestCase.php',
     'PhortuneDAO' => 'applications/phortune/storage/PhortuneDAO.php',
     'PhortuneErrCode' => 'applications/phortune/constants/PhortuneErrCode.php',
     'PhortuneLandingController' => 'applications/phortune/controller/PhortuneLandingController.php',
     'PhortuneMemberHasAccountEdgeType' => 'applications/phortune/edge/PhortuneMemberHasAccountEdgeType.php',
     'PhortuneMemberHasMerchantEdgeType' => 'applications/phortune/edge/PhortuneMemberHasMerchantEdgeType.php',
     'PhortuneMerchant' => 'applications/phortune/storage/PhortuneMerchant.php',
     'PhortuneMerchantCapability' => 'applications/phortune/capability/PhortuneMerchantCapability.php',
     'PhortuneMerchantController' => 'applications/phortune/controller/PhortuneMerchantController.php',
     'PhortuneMerchantEditController' => 'applications/phortune/controller/PhortuneMerchantEditController.php',
     'PhortuneMerchantEditor' => 'applications/phortune/editor/PhortuneMerchantEditor.php',
     'PhortuneMerchantHasMemberEdgeType' => 'applications/phortune/edge/PhortuneMerchantHasMemberEdgeType.php',
     'PhortuneMerchantInvoiceCreateController' => 'applications/phortune/controller/PhortuneMerchantInvoiceCreateController.php',
     'PhortuneMerchantListController' => 'applications/phortune/controller/PhortuneMerchantListController.php',
     'PhortuneMerchantPHIDType' => 'applications/phortune/phid/PhortuneMerchantPHIDType.php',
     'PhortuneMerchantQuery' => 'applications/phortune/query/PhortuneMerchantQuery.php',
     'PhortuneMerchantSearchEngine' => 'applications/phortune/query/PhortuneMerchantSearchEngine.php',
     'PhortuneMerchantTransaction' => 'applications/phortune/storage/PhortuneMerchantTransaction.php',
     'PhortuneMerchantTransactionQuery' => 'applications/phortune/query/PhortuneMerchantTransactionQuery.php',
     'PhortuneMerchantViewController' => 'applications/phortune/controller/PhortuneMerchantViewController.php',
     'PhortuneMonthYearExpiryControl' => 'applications/phortune/control/PhortuneMonthYearExpiryControl.php',
     'PhortuneNotImplementedException' => 'applications/phortune/exception/PhortuneNotImplementedException.php',
     'PhortuneOrderTableView' => 'applications/phortune/view/PhortuneOrderTableView.php',
     'PhortunePayPalPaymentProvider' => 'applications/phortune/provider/PhortunePayPalPaymentProvider.php',
     'PhortunePaymentMethod' => 'applications/phortune/storage/PhortunePaymentMethod.php',
     'PhortunePaymentMethodCreateController' => 'applications/phortune/controller/PhortunePaymentMethodCreateController.php',
     'PhortunePaymentMethodDisableController' => 'applications/phortune/controller/PhortunePaymentMethodDisableController.php',
     'PhortunePaymentMethodEditController' => 'applications/phortune/controller/PhortunePaymentMethodEditController.php',
     'PhortunePaymentMethodPHIDType' => 'applications/phortune/phid/PhortunePaymentMethodPHIDType.php',
     'PhortunePaymentMethodQuery' => 'applications/phortune/query/PhortunePaymentMethodQuery.php',
     'PhortunePaymentProvider' => 'applications/phortune/provider/PhortunePaymentProvider.php',
     'PhortunePaymentProviderConfig' => 'applications/phortune/storage/PhortunePaymentProviderConfig.php',
     'PhortunePaymentProviderConfigEditor' => 'applications/phortune/editor/PhortunePaymentProviderConfigEditor.php',
     'PhortunePaymentProviderConfigQuery' => 'applications/phortune/query/PhortunePaymentProviderConfigQuery.php',
     'PhortunePaymentProviderConfigTransaction' => 'applications/phortune/storage/PhortunePaymentProviderConfigTransaction.php',
     'PhortunePaymentProviderConfigTransactionQuery' => 'applications/phortune/query/PhortunePaymentProviderConfigTransactionQuery.php',
     'PhortunePaymentProviderPHIDType' => 'applications/phortune/phid/PhortunePaymentProviderPHIDType.php',
     'PhortuneProduct' => 'applications/phortune/storage/PhortuneProduct.php',
     'PhortuneProductImplementation' => 'applications/phortune/product/PhortuneProductImplementation.php',
     'PhortuneProductListController' => 'applications/phortune/controller/PhortuneProductListController.php',
     'PhortuneProductPHIDType' => 'applications/phortune/phid/PhortuneProductPHIDType.php',
     'PhortuneProductQuery' => 'applications/phortune/query/PhortuneProductQuery.php',
     'PhortuneProductViewController' => 'applications/phortune/controller/PhortuneProductViewController.php',
     'PhortuneProviderActionController' => 'applications/phortune/controller/PhortuneProviderActionController.php',
     'PhortuneProviderDisableController' => 'applications/phortune/controller/PhortuneProviderDisableController.php',
     'PhortuneProviderEditController' => 'applications/phortune/controller/PhortuneProviderEditController.php',
     'PhortunePurchase' => 'applications/phortune/storage/PhortunePurchase.php',
     'PhortunePurchasePHIDType' => 'applications/phortune/phid/PhortunePurchasePHIDType.php',
     'PhortunePurchaseQuery' => 'applications/phortune/query/PhortunePurchaseQuery.php',
     'PhortuneSchemaSpec' => 'applications/phortune/storage/PhortuneSchemaSpec.php',
     'PhortuneStripePaymentProvider' => 'applications/phortune/provider/PhortuneStripePaymentProvider.php',
     'PhortuneSubscription' => 'applications/phortune/storage/PhortuneSubscription.php',
     'PhortuneSubscriptionCart' => 'applications/phortune/cart/PhortuneSubscriptionCart.php',
     'PhortuneSubscriptionEditController' => 'applications/phortune/controller/PhortuneSubscriptionEditController.php',
     'PhortuneSubscriptionImplementation' => 'applications/phortune/subscription/PhortuneSubscriptionImplementation.php',
     'PhortuneSubscriptionListController' => 'applications/phortune/controller/PhortuneSubscriptionListController.php',
     'PhortuneSubscriptionPHIDType' => 'applications/phortune/phid/PhortuneSubscriptionPHIDType.php',
     'PhortuneSubscriptionProduct' => 'applications/phortune/product/PhortuneSubscriptionProduct.php',
     'PhortuneSubscriptionQuery' => 'applications/phortune/query/PhortuneSubscriptionQuery.php',
     'PhortuneSubscriptionSearchEngine' => 'applications/phortune/query/PhortuneSubscriptionSearchEngine.php',
     'PhortuneSubscriptionTableView' => 'applications/phortune/view/PhortuneSubscriptionTableView.php',
     'PhortuneSubscriptionViewController' => 'applications/phortune/controller/PhortuneSubscriptionViewController.php',
     'PhortuneSubscriptionWorker' => 'applications/phortune/worker/PhortuneSubscriptionWorker.php',
     'PhortuneTestPaymentProvider' => 'applications/phortune/provider/PhortuneTestPaymentProvider.php',
     'PhortuneWePayPaymentProvider' => 'applications/phortune/provider/PhortuneWePayPaymentProvider.php',
     'PhragmentBrowseController' => 'applications/phragment/controller/PhragmentBrowseController.php',
     'PhragmentCanCreateCapability' => 'applications/phragment/capability/PhragmentCanCreateCapability.php',
     'PhragmentConduitAPIMethod' => 'applications/phragment/conduit/PhragmentConduitAPIMethod.php',
     'PhragmentController' => 'applications/phragment/controller/PhragmentController.php',
     'PhragmentCreateController' => 'applications/phragment/controller/PhragmentCreateController.php',
     'PhragmentDAO' => 'applications/phragment/storage/PhragmentDAO.php',
     'PhragmentFragment' => 'applications/phragment/storage/PhragmentFragment.php',
     'PhragmentFragmentPHIDType' => 'applications/phragment/phid/PhragmentFragmentPHIDType.php',
     'PhragmentFragmentQuery' => 'applications/phragment/query/PhragmentFragmentQuery.php',
     'PhragmentFragmentVersion' => 'applications/phragment/storage/PhragmentFragmentVersion.php',
     'PhragmentFragmentVersionPHIDType' => 'applications/phragment/phid/PhragmentFragmentVersionPHIDType.php',
     'PhragmentFragmentVersionQuery' => 'applications/phragment/query/PhragmentFragmentVersionQuery.php',
     'PhragmentGetPatchConduitAPIMethod' => 'applications/phragment/conduit/PhragmentGetPatchConduitAPIMethod.php',
     'PhragmentHistoryController' => 'applications/phragment/controller/PhragmentHistoryController.php',
     'PhragmentPatchController' => 'applications/phragment/controller/PhragmentPatchController.php',
     'PhragmentPatchUtil' => 'applications/phragment/util/PhragmentPatchUtil.php',
     'PhragmentPolicyController' => 'applications/phragment/controller/PhragmentPolicyController.php',
     'PhragmentQueryFragmentsConduitAPIMethod' => 'applications/phragment/conduit/PhragmentQueryFragmentsConduitAPIMethod.php',
     'PhragmentRevertController' => 'applications/phragment/controller/PhragmentRevertController.php',
     'PhragmentSchemaSpec' => 'applications/phragment/storage/PhragmentSchemaSpec.php',
     'PhragmentSnapshot' => 'applications/phragment/storage/PhragmentSnapshot.php',
     'PhragmentSnapshotChild' => 'applications/phragment/storage/PhragmentSnapshotChild.php',
     'PhragmentSnapshotChildQuery' => 'applications/phragment/query/PhragmentSnapshotChildQuery.php',
     'PhragmentSnapshotCreateController' => 'applications/phragment/controller/PhragmentSnapshotCreateController.php',
     'PhragmentSnapshotDeleteController' => 'applications/phragment/controller/PhragmentSnapshotDeleteController.php',
     'PhragmentSnapshotPHIDType' => 'applications/phragment/phid/PhragmentSnapshotPHIDType.php',
     'PhragmentSnapshotPromoteController' => 'applications/phragment/controller/PhragmentSnapshotPromoteController.php',
     'PhragmentSnapshotQuery' => 'applications/phragment/query/PhragmentSnapshotQuery.php',
     'PhragmentSnapshotViewController' => 'applications/phragment/controller/PhragmentSnapshotViewController.php',
     'PhragmentUpdateController' => 'applications/phragment/controller/PhragmentUpdateController.php',
     'PhragmentVersionController' => 'applications/phragment/controller/PhragmentVersionController.php',
     'PhragmentZIPController' => 'applications/phragment/controller/PhragmentZIPController.php',
     'PhrequentConduitAPIMethod' => 'applications/phrequent/conduit/PhrequentConduitAPIMethod.php',
     'PhrequentController' => 'applications/phrequent/controller/PhrequentController.php',
     'PhrequentDAO' => 'applications/phrequent/storage/PhrequentDAO.php',
     'PhrequentListController' => 'applications/phrequent/controller/PhrequentListController.php',
     'PhrequentPopConduitAPIMethod' => 'applications/phrequent/conduit/PhrequentPopConduitAPIMethod.php',
     'PhrequentPushConduitAPIMethod' => 'applications/phrequent/conduit/PhrequentPushConduitAPIMethod.php',
     'PhrequentSearchEngine' => 'applications/phrequent/query/PhrequentSearchEngine.php',
     'PhrequentTimeBlock' => 'applications/phrequent/storage/PhrequentTimeBlock.php',
     'PhrequentTimeBlockTestCase' => 'applications/phrequent/storage/__tests__/PhrequentTimeBlockTestCase.php',
     'PhrequentTimeSlices' => 'applications/phrequent/storage/PhrequentTimeSlices.php',
     'PhrequentTrackController' => 'applications/phrequent/controller/PhrequentTrackController.php',
     'PhrequentTrackableInterface' => 'applications/phrequent/interface/PhrequentTrackableInterface.php',
     'PhrequentTrackingConduitAPIMethod' => 'applications/phrequent/conduit/PhrequentTrackingConduitAPIMethod.php',
     'PhrequentTrackingEditor' => 'applications/phrequent/editor/PhrequentTrackingEditor.php',
     'PhrequentUIEventListener' => 'applications/phrequent/event/PhrequentUIEventListener.php',
     'PhrequentUserTime' => 'applications/phrequent/storage/PhrequentUserTime.php',
     'PhrequentUserTimeQuery' => 'applications/phrequent/query/PhrequentUserTimeQuery.php',
     'PhrictionActionConstants' => 'applications/phriction/constants/PhrictionActionConstants.php',
     'PhrictionChangeType' => 'applications/phriction/constants/PhrictionChangeType.php',
     'PhrictionConduitAPIMethod' => 'applications/phriction/conduit/PhrictionConduitAPIMethod.php',
     'PhrictionConstants' => 'applications/phriction/constants/PhrictionConstants.php',
     'PhrictionContent' => 'applications/phriction/storage/PhrictionContent.php',
     'PhrictionController' => 'applications/phriction/controller/PhrictionController.php',
     'PhrictionCreateConduitAPIMethod' => 'applications/phriction/conduit/PhrictionCreateConduitAPIMethod.php',
     'PhrictionDAO' => 'applications/phriction/storage/PhrictionDAO.php',
     'PhrictionDeleteController' => 'applications/phriction/controller/PhrictionDeleteController.php',
     'PhrictionDiffController' => 'applications/phriction/controller/PhrictionDiffController.php',
     'PhrictionDocument' => 'applications/phriction/storage/PhrictionDocument.php',
     'PhrictionDocumentController' => 'applications/phriction/controller/PhrictionDocumentController.php',
     'PhrictionDocumentHeraldAdapter' => 'applications/phriction/herald/PhrictionDocumentHeraldAdapter.php',
     'PhrictionDocumentPHIDType' => 'applications/phriction/phid/PhrictionDocumentPHIDType.php',
     'PhrictionDocumentQuery' => 'applications/phriction/query/PhrictionDocumentQuery.php',
     'PhrictionDocumentStatus' => 'applications/phriction/constants/PhrictionDocumentStatus.php',
     'PhrictionEditConduitAPIMethod' => 'applications/phriction/conduit/PhrictionEditConduitAPIMethod.php',
     'PhrictionEditController' => 'applications/phriction/controller/PhrictionEditController.php',
     'PhrictionHistoryConduitAPIMethod' => 'applications/phriction/conduit/PhrictionHistoryConduitAPIMethod.php',
     'PhrictionHistoryController' => 'applications/phriction/controller/PhrictionHistoryController.php',
     'PhrictionInfoConduitAPIMethod' => 'applications/phriction/conduit/PhrictionInfoConduitAPIMethod.php',
     'PhrictionListController' => 'applications/phriction/controller/PhrictionListController.php',
     'PhrictionMoveController' => 'applications/phriction/controller/PhrictionMoveController.php',
     'PhrictionNewController' => 'applications/phriction/controller/PhrictionNewController.php',
     'PhrictionRemarkupRule' => 'applications/phriction/markup/PhrictionRemarkupRule.php',
     'PhrictionReplyHandler' => 'applications/phriction/mail/PhrictionReplyHandler.php',
     'PhrictionSchemaSpec' => 'applications/phriction/storage/PhrictionSchemaSpec.php',
     'PhrictionSearchEngine' => 'applications/phriction/query/PhrictionSearchEngine.php',
     'PhrictionSearchIndexer' => 'applications/phriction/search/PhrictionSearchIndexer.php',
     'PhrictionTransaction' => 'applications/phriction/storage/PhrictionTransaction.php',
     'PhrictionTransactionComment' => 'applications/phriction/storage/PhrictionTransactionComment.php',
     'PhrictionTransactionEditor' => 'applications/phriction/editor/PhrictionTransactionEditor.php',
     'PhrictionTransactionQuery' => 'applications/phriction/query/PhrictionTransactionQuery.php',
     'PolicyLockOptionType' => 'applications/policy/config/PolicyLockOptionType.php',
     'PonderAddAnswerView' => 'applications/ponder/view/PonderAddAnswerView.php',
     'PonderAnswer' => 'applications/ponder/storage/PonderAnswer.php',
     'PonderAnswerCommentController' => 'applications/ponder/controller/PonderAnswerCommentController.php',
     'PonderAnswerEditController' => 'applications/ponder/controller/PonderAnswerEditController.php',
     'PonderAnswerEditor' => 'applications/ponder/editor/PonderAnswerEditor.php',
     'PonderAnswerHasVotingUserEdgeType' => 'applications/ponder/edge/PonderAnswerHasVotingUserEdgeType.php',
     'PonderAnswerHistoryController' => 'applications/ponder/controller/PonderAnswerHistoryController.php',
     'PonderAnswerPHIDType' => 'applications/ponder/phid/PonderAnswerPHIDType.php',
     'PonderAnswerQuery' => 'applications/ponder/query/PonderAnswerQuery.php',
     'PonderAnswerSaveController' => 'applications/ponder/controller/PonderAnswerSaveController.php',
     'PonderAnswerTransaction' => 'applications/ponder/storage/PonderAnswerTransaction.php',
     'PonderAnswerTransactionComment' => 'applications/ponder/storage/PonderAnswerTransactionComment.php',
     'PonderAnswerTransactionQuery' => 'applications/ponder/query/PonderAnswerTransactionQuery.php',
     'PonderConstants' => 'applications/ponder/constants/PonderConstants.php',
     'PonderController' => 'applications/ponder/controller/PonderController.php',
     'PonderDAO' => 'applications/ponder/storage/PonderDAO.php',
     'PonderEditor' => 'applications/ponder/editor/PonderEditor.php',
     'PonderQuestion' => 'applications/ponder/storage/PonderQuestion.php',
     'PonderQuestionCommentController' => 'applications/ponder/controller/PonderQuestionCommentController.php',
     'PonderQuestionEditController' => 'applications/ponder/controller/PonderQuestionEditController.php',
     'PonderQuestionEditor' => 'applications/ponder/editor/PonderQuestionEditor.php',
     'PonderQuestionHasVotingUserEdgeType' => 'applications/ponder/edge/PonderQuestionHasVotingUserEdgeType.php',
     'PonderQuestionHistoryController' => 'applications/ponder/controller/PonderQuestionHistoryController.php',
     'PonderQuestionListController' => 'applications/ponder/controller/PonderQuestionListController.php',
     'PonderQuestionMailReceiver' => 'applications/ponder/mail/PonderQuestionMailReceiver.php',
     'PonderQuestionPHIDType' => 'applications/ponder/phid/PonderQuestionPHIDType.php',
     'PonderQuestionQuery' => 'applications/ponder/query/PonderQuestionQuery.php',
     'PonderQuestionReplyHandler' => 'applications/ponder/mail/PonderQuestionReplyHandler.php',
     'PonderQuestionSearchEngine' => 'applications/ponder/query/PonderQuestionSearchEngine.php',
     'PonderQuestionStatus' => 'applications/ponder/constants/PonderQuestionStatus.php',
     'PonderQuestionStatusController' => 'applications/ponder/controller/PonderQuestionStatusController.php',
     'PonderQuestionTransaction' => 'applications/ponder/storage/PonderQuestionTransaction.php',
     'PonderQuestionTransactionComment' => 'applications/ponder/storage/PonderQuestionTransactionComment.php',
     'PonderQuestionTransactionQuery' => 'applications/ponder/query/PonderQuestionTransactionQuery.php',
     'PonderQuestionViewController' => 'applications/ponder/controller/PonderQuestionViewController.php',
     'PonderRemarkupRule' => 'applications/ponder/remarkup/PonderRemarkupRule.php',
     'PonderSchemaSpec' => 'applications/ponder/storage/PonderSchemaSpec.php',
     'PonderSearchIndexer' => 'applications/ponder/search/PonderSearchIndexer.php',
     'PonderTransactionFeedStory' => 'applications/ponder/feed/PonderTransactionFeedStory.php',
     'PonderVotableInterface' => 'applications/ponder/storage/PonderVotableInterface.php',
     'PonderVotableView' => 'applications/ponder/view/PonderVotableView.php',
     'PonderVote' => 'applications/ponder/constants/PonderVote.php',
     'PonderVoteEditor' => 'applications/ponder/editor/PonderVoteEditor.php',
     'PonderVoteSaveController' => 'applications/ponder/controller/PonderVoteSaveController.php',
     'PonderVotingUserHasAnswerEdgeType' => 'applications/ponder/edge/PonderVotingUserHasAnswerEdgeType.php',
     'PonderVotingUserHasQuestionEdgeType' => 'applications/ponder/edge/PonderVotingUserHasQuestionEdgeType.php',
     'ProjectAddProjectsEmailCommand' => 'applications/project/command/ProjectAddProjectsEmailCommand.php',
     'ProjectBoardTaskCard' => 'applications/project/view/ProjectBoardTaskCard.php',
     'ProjectCanLockProjectsCapability' => 'applications/project/capability/ProjectCanLockProjectsCapability.php',
     'ProjectConduitAPIMethod' => 'applications/project/conduit/ProjectConduitAPIMethod.php',
     'ProjectCreateConduitAPIMethod' => 'applications/project/conduit/ProjectCreateConduitAPIMethod.php',
     'ProjectCreateProjectsCapability' => 'applications/project/capability/ProjectCreateProjectsCapability.php',
     'ProjectDefaultEditCapability' => 'applications/project/capability/ProjectDefaultEditCapability.php',
     'ProjectDefaultJoinCapability' => 'applications/project/capability/ProjectDefaultJoinCapability.php',
     'ProjectDefaultViewCapability' => 'applications/project/capability/ProjectDefaultViewCapability.php',
     'ProjectQueryConduitAPIMethod' => 'applications/project/conduit/ProjectQueryConduitAPIMethod.php',
     'ProjectRemarkupRule' => 'applications/project/remarkup/ProjectRemarkupRule.php',
     'ProjectRemarkupRuleTestCase' => 'applications/project/remarkup/__tests__/ProjectRemarkupRuleTestCase.php',
     'QueryFormattingTestCase' => 'infrastructure/storage/__tests__/QueryFormattingTestCase.php',
     'ReleephAuthorFieldSpecification' => 'applications/releeph/field/specification/ReleephAuthorFieldSpecification.php',
     'ReleephBranch' => 'applications/releeph/storage/ReleephBranch.php',
     'ReleephBranchAccessController' => 'applications/releeph/controller/branch/ReleephBranchAccessController.php',
     'ReleephBranchCommitFieldSpecification' => 'applications/releeph/field/specification/ReleephBranchCommitFieldSpecification.php',
     'ReleephBranchController' => 'applications/releeph/controller/branch/ReleephBranchController.php',
     'ReleephBranchCreateController' => 'applications/releeph/controller/branch/ReleephBranchCreateController.php',
     'ReleephBranchEditController' => 'applications/releeph/controller/branch/ReleephBranchEditController.php',
     'ReleephBranchEditor' => 'applications/releeph/editor/ReleephBranchEditor.php',
     'ReleephBranchHistoryController' => 'applications/releeph/controller/branch/ReleephBranchHistoryController.php',
     'ReleephBranchNamePreviewController' => 'applications/releeph/controller/branch/ReleephBranchNamePreviewController.php',
     'ReleephBranchPHIDType' => 'applications/releeph/phid/ReleephBranchPHIDType.php',
     'ReleephBranchPreviewView' => 'applications/releeph/view/branch/ReleephBranchPreviewView.php',
     'ReleephBranchQuery' => 'applications/releeph/query/ReleephBranchQuery.php',
     'ReleephBranchSearchEngine' => 'applications/releeph/query/ReleephBranchSearchEngine.php',
     'ReleephBranchTemplate' => 'applications/releeph/view/branch/ReleephBranchTemplate.php',
     'ReleephBranchTransaction' => 'applications/releeph/storage/ReleephBranchTransaction.php',
     'ReleephBranchTransactionQuery' => 'applications/releeph/query/ReleephBranchTransactionQuery.php',
     'ReleephBranchViewController' => 'applications/releeph/controller/branch/ReleephBranchViewController.php',
     'ReleephCommitFinder' => 'applications/releeph/commitfinder/ReleephCommitFinder.php',
     'ReleephCommitFinderException' => 'applications/releeph/commitfinder/ReleephCommitFinderException.php',
     'ReleephCommitMessageFieldSpecification' => 'applications/releeph/field/specification/ReleephCommitMessageFieldSpecification.php',
     'ReleephConduitAPIMethod' => 'applications/releeph/conduit/ReleephConduitAPIMethod.php',
     'ReleephController' => 'applications/releeph/controller/ReleephController.php',
     'ReleephDAO' => 'applications/releeph/storage/ReleephDAO.php',
     'ReleephDefaultFieldSelector' => 'applications/releeph/field/selector/ReleephDefaultFieldSelector.php',
     'ReleephDependsOnFieldSpecification' => 'applications/releeph/field/specification/ReleephDependsOnFieldSpecification.php',
     'ReleephDiffChurnFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffChurnFieldSpecification.php',
     'ReleephDiffMessageFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffMessageFieldSpecification.php',
     'ReleephDiffSizeFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffSizeFieldSpecification.php',
     'ReleephFieldParseException' => 'applications/releeph/field/exception/ReleephFieldParseException.php',
     'ReleephFieldSelector' => 'applications/releeph/field/selector/ReleephFieldSelector.php',
     'ReleephFieldSpecification' => 'applications/releeph/field/specification/ReleephFieldSpecification.php',
     'ReleephGetBranchesConduitAPIMethod' => 'applications/releeph/conduit/ReleephGetBranchesConduitAPIMethod.php',
     'ReleephIntentFieldSpecification' => 'applications/releeph/field/specification/ReleephIntentFieldSpecification.php',
     'ReleephLevelFieldSpecification' => 'applications/releeph/field/specification/ReleephLevelFieldSpecification.php',
     'ReleephOriginalCommitFieldSpecification' => 'applications/releeph/field/specification/ReleephOriginalCommitFieldSpecification.php',
     'ReleephProductActionController' => 'applications/releeph/controller/product/ReleephProductActionController.php',
     'ReleephProductController' => 'applications/releeph/controller/product/ReleephProductController.php',
     'ReleephProductCreateController' => 'applications/releeph/controller/product/ReleephProductCreateController.php',
     'ReleephProductEditController' => 'applications/releeph/controller/product/ReleephProductEditController.php',
     'ReleephProductEditor' => 'applications/releeph/editor/ReleephProductEditor.php',
     'ReleephProductHistoryController' => 'applications/releeph/controller/product/ReleephProductHistoryController.php',
     'ReleephProductListController' => 'applications/releeph/controller/product/ReleephProductListController.php',
     'ReleephProductPHIDType' => 'applications/releeph/phid/ReleephProductPHIDType.php',
     'ReleephProductQuery' => 'applications/releeph/query/ReleephProductQuery.php',
     'ReleephProductSearchEngine' => 'applications/releeph/query/ReleephProductSearchEngine.php',
     'ReleephProductTransaction' => 'applications/releeph/storage/ReleephProductTransaction.php',
     'ReleephProductTransactionQuery' => 'applications/releeph/query/ReleephProductTransactionQuery.php',
     'ReleephProductViewController' => 'applications/releeph/controller/product/ReleephProductViewController.php',
     'ReleephProject' => 'applications/releeph/storage/ReleephProject.php',
     'ReleephProjectInfoConduitAPIMethod' => 'applications/releeph/conduit/ReleephProjectInfoConduitAPIMethod.php',
     'ReleephQueryBranchesConduitAPIMethod' => 'applications/releeph/conduit/ReleephQueryBranchesConduitAPIMethod.php',
     'ReleephQueryProductsConduitAPIMethod' => 'applications/releeph/conduit/ReleephQueryProductsConduitAPIMethod.php',
     'ReleephQueryRequestsConduitAPIMethod' => 'applications/releeph/conduit/ReleephQueryRequestsConduitAPIMethod.php',
     'ReleephReasonFieldSpecification' => 'applications/releeph/field/specification/ReleephReasonFieldSpecification.php',
     'ReleephRequest' => 'applications/releeph/storage/ReleephRequest.php',
     'ReleephRequestActionController' => 'applications/releeph/controller/request/ReleephRequestActionController.php',
     'ReleephRequestCommentController' => 'applications/releeph/controller/request/ReleephRequestCommentController.php',
     'ReleephRequestConduitAPIMethod' => 'applications/releeph/conduit/ReleephRequestConduitAPIMethod.php',
     'ReleephRequestController' => 'applications/releeph/controller/request/ReleephRequestController.php',
     'ReleephRequestDifferentialCreateController' => 'applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php',
     'ReleephRequestEditController' => 'applications/releeph/controller/request/ReleephRequestEditController.php',
     'ReleephRequestMailReceiver' => 'applications/releeph/mail/ReleephRequestMailReceiver.php',
     'ReleephRequestPHIDType' => 'applications/releeph/phid/ReleephRequestPHIDType.php',
     'ReleephRequestQuery' => 'applications/releeph/query/ReleephRequestQuery.php',
     'ReleephRequestReplyHandler' => 'applications/releeph/mail/ReleephRequestReplyHandler.php',
     'ReleephRequestSearchEngine' => 'applications/releeph/query/ReleephRequestSearchEngine.php',
     'ReleephRequestStatus' => 'applications/releeph/constants/ReleephRequestStatus.php',
     'ReleephRequestTransaction' => 'applications/releeph/storage/ReleephRequestTransaction.php',
     'ReleephRequestTransactionComment' => 'applications/releeph/storage/ReleephRequestTransactionComment.php',
     'ReleephRequestTransactionQuery' => 'applications/releeph/query/ReleephRequestTransactionQuery.php',
     'ReleephRequestTransactionalEditor' => 'applications/releeph/editor/ReleephRequestTransactionalEditor.php',
     'ReleephRequestTypeaheadControl' => 'applications/releeph/view/request/ReleephRequestTypeaheadControl.php',
     'ReleephRequestTypeaheadController' => 'applications/releeph/controller/request/ReleephRequestTypeaheadController.php',
     'ReleephRequestView' => 'applications/releeph/view/ReleephRequestView.php',
     'ReleephRequestViewController' => 'applications/releeph/controller/request/ReleephRequestViewController.php',
     'ReleephRequestorFieldSpecification' => 'applications/releeph/field/specification/ReleephRequestorFieldSpecification.php',
     'ReleephRevisionFieldSpecification' => 'applications/releeph/field/specification/ReleephRevisionFieldSpecification.php',
     'ReleephSeverityFieldSpecification' => 'applications/releeph/field/specification/ReleephSeverityFieldSpecification.php',
     'ReleephSummaryFieldSpecification' => 'applications/releeph/field/specification/ReleephSummaryFieldSpecification.php',
     'ReleephWorkCanPushConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkCanPushConduitAPIMethod.php',
     'ReleephWorkGetAuthorInfoConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkGetAuthorInfoConduitAPIMethod.php',
     'ReleephWorkGetBranchCommitMessageConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkGetBranchCommitMessageConduitAPIMethod.php',
     'ReleephWorkGetBranchConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkGetBranchConduitAPIMethod.php',
     'ReleephWorkGetCommitMessageConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkGetCommitMessageConduitAPIMethod.php',
     'ReleephWorkNextRequestConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkNextRequestConduitAPIMethod.php',
     'ReleephWorkRecordConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkRecordConduitAPIMethod.php',
     'ReleephWorkRecordPickStatusConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkRecordPickStatusConduitAPIMethod.php',
     'RemarkupProcessConduitAPIMethod' => 'applications/remarkup/conduit/RemarkupProcessConduitAPIMethod.php',
     'RepositoryConduitAPIMethod' => 'applications/repository/conduit/RepositoryConduitAPIMethod.php',
     'RepositoryCreateConduitAPIMethod' => 'applications/repository/conduit/RepositoryCreateConduitAPIMethod.php',
     'RepositoryQueryConduitAPIMethod' => 'applications/repository/conduit/RepositoryQueryConduitAPIMethod.php',
     'ShellLogView' => 'applications/harbormaster/view/ShellLogView.php',
     'SlowvoteConduitAPIMethod' => 'applications/slowvote/conduit/SlowvoteConduitAPIMethod.php',
     'SlowvoteEmbedView' => 'applications/slowvote/view/SlowvoteEmbedView.php',
     'SlowvoteInfoConduitAPIMethod' => 'applications/slowvote/conduit/SlowvoteInfoConduitAPIMethod.php',
     'SlowvoteRemarkupRule' => 'applications/slowvote/remarkup/SlowvoteRemarkupRule.php',
     'SubscriptionListDialogBuilder' => 'applications/subscriptions/view/SubscriptionListDialogBuilder.php',
     'SubscriptionListStringBuilder' => 'applications/subscriptions/view/SubscriptionListStringBuilder.php',
     'TokenConduitAPIMethod' => 'applications/tokens/conduit/TokenConduitAPIMethod.php',
     'TokenGiveConduitAPIMethod' => 'applications/tokens/conduit/TokenGiveConduitAPIMethod.php',
     'TokenGivenConduitAPIMethod' => 'applications/tokens/conduit/TokenGivenConduitAPIMethod.php',
     'TokenQueryConduitAPIMethod' => 'applications/tokens/conduit/TokenQueryConduitAPIMethod.php',
     'UserAddStatusConduitAPIMethod' => 'applications/people/conduit/UserAddStatusConduitAPIMethod.php',
     'UserConduitAPIMethod' => 'applications/people/conduit/UserConduitAPIMethod.php',
     'UserDisableConduitAPIMethod' => 'applications/people/conduit/UserDisableConduitAPIMethod.php',
     'UserEnableConduitAPIMethod' => 'applications/people/conduit/UserEnableConduitAPIMethod.php',
     'UserFindConduitAPIMethod' => 'applications/people/conduit/UserFindConduitAPIMethod.php',
     'UserQueryConduitAPIMethod' => 'applications/people/conduit/UserQueryConduitAPIMethod.php',
     'UserRemoveStatusConduitAPIMethod' => 'applications/people/conduit/UserRemoveStatusConduitAPIMethod.php',
     'UserWhoAmIConduitAPIMethod' => 'applications/people/conduit/UserWhoAmIConduitAPIMethod.php',
   ),
   'function' => array(
     'celerity_generate_unique_node_id' => 'applications/celerity/api.php',
     'celerity_get_resource_uri' => 'applications/celerity/api.php',
     'javelin_tag' => 'infrastructure/javelin/markup.php',
     'phabricator_date' => 'view/viewutils.php',
     'phabricator_datetime' => 'view/viewutils.php',
     'phabricator_form' => 'infrastructure/javelin/markup.php',
     'phabricator_format_local_time' => 'view/viewutils.php',
     'phabricator_relative_date' => 'view/viewutils.php',
     'phabricator_time' => 'view/viewutils.php',
     'phabricator_time_format' => 'view/viewutils.php',
     'phid_get_subtype' => 'applications/phid/utils.php',
     'phid_get_type' => 'applications/phid/utils.php',
     'phid_group_by_type' => 'applications/phid/utils.php',
     'require_celerity_resource' => 'applications/celerity/api.php',
   ),
   'xmap' => array(
     'AlmanacAddress' => 'Phobject',
     'AlmanacBinding' => array(
       'AlmanacDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorCustomFieldInterface',
       'PhabricatorApplicationTransactionInterface',
       'AlmanacPropertyInterface',
     ),
     'AlmanacBindingEditController' => 'AlmanacServiceController',
     'AlmanacBindingEditor' => 'PhabricatorApplicationTransactionEditor',
     'AlmanacBindingPHIDType' => 'PhabricatorPHIDType',
     'AlmanacBindingQuery' => 'AlmanacQuery',
     'AlmanacBindingTableView' => 'AphrontView',
     'AlmanacBindingTransaction' => 'PhabricatorApplicationTransaction',
     'AlmanacBindingTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'AlmanacBindingViewController' => 'AlmanacServiceController',
     'AlmanacClusterDatabaseServiceType' => 'AlmanacClusterServiceType',
     'AlmanacClusterRepositoryServiceType' => 'AlmanacClusterServiceType',
     'AlmanacClusterServiceType' => 'AlmanacServiceType',
     'AlmanacConduitAPIMethod' => 'ConduitAPIMethod',
     'AlmanacConsoleController' => 'AlmanacController',
     'AlmanacController' => 'PhabricatorController',
     'AlmanacCoreCustomField' => array(
       'AlmanacCustomField',
       'PhabricatorStandardCustomFieldInterface',
     ),
     'AlmanacCreateClusterServicesCapability' => 'PhabricatorPolicyCapability',
     'AlmanacCreateDevicesCapability' => 'PhabricatorPolicyCapability',
     'AlmanacCreateNetworksCapability' => 'PhabricatorPolicyCapability',
     'AlmanacCreateServicesCapability' => 'PhabricatorPolicyCapability',
     'AlmanacCustomField' => 'PhabricatorCustomField',
     'AlmanacCustomServiceType' => 'AlmanacServiceType',
     'AlmanacDAO' => 'PhabricatorLiskDAO',
     'AlmanacDevice' => array(
       'AlmanacDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorCustomFieldInterface',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorProjectInterface',
       'PhabricatorSSHPublicKeyInterface',
       'AlmanacPropertyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'AlmanacDeviceController' => 'AlmanacController',
     'AlmanacDeviceEditController' => 'AlmanacDeviceController',
     'AlmanacDeviceEditor' => 'PhabricatorApplicationTransactionEditor',
     'AlmanacDeviceListController' => 'AlmanacDeviceController',
     'AlmanacDevicePHIDType' => 'PhabricatorPHIDType',
     'AlmanacDeviceQuery' => 'AlmanacQuery',
     'AlmanacDeviceSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'AlmanacDeviceTransaction' => 'PhabricatorApplicationTransaction',
     'AlmanacDeviceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'AlmanacDeviceViewController' => 'AlmanacDeviceController',
     'AlmanacInterface' => array(
       'AlmanacDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'AlmanacInterfaceDatasource' => 'PhabricatorTypeaheadDatasource',
     'AlmanacInterfaceEditController' => 'AlmanacDeviceController',
     'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType',
     'AlmanacInterfaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'AlmanacInterfaceTableView' => 'AphrontView',
     'AlmanacKeys' => 'Phobject',
     'AlmanacManagementLockWorkflow' => 'AlmanacManagementWorkflow',
     'AlmanacManagementRegisterWorkflow' => 'AlmanacManagementWorkflow',
     'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow',
     'AlmanacManagementUnlockWorkflow' => 'AlmanacManagementWorkflow',
     'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow',
     'AlmanacManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'AlmanacNames' => 'Phobject',
     'AlmanacNamesTestCase' => 'PhabricatorTestCase',
     'AlmanacNetwork' => array(
       'AlmanacDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'AlmanacNetworkController' => 'AlmanacController',
     'AlmanacNetworkEditController' => 'AlmanacNetworkController',
     'AlmanacNetworkEditor' => 'PhabricatorApplicationTransactionEditor',
     'AlmanacNetworkListController' => 'AlmanacNetworkController',
     'AlmanacNetworkPHIDType' => 'PhabricatorPHIDType',
     'AlmanacNetworkQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'AlmanacNetworkSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'AlmanacNetworkTransaction' => 'PhabricatorApplicationTransaction',
     'AlmanacNetworkTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'AlmanacNetworkViewController' => 'AlmanacNetworkController',
     'AlmanacProperty' => array(
       'PhabricatorCustomFieldStorage',
       'PhabricatorPolicyInterface',
     ),
     'AlmanacPropertyController' => 'AlmanacController',
     'AlmanacPropertyDeleteController' => 'AlmanacDeviceController',
     'AlmanacPropertyEditController' => 'AlmanacDeviceController',
     'AlmanacPropertyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'AlmanacQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'AlmanacQueryDevicesConduitAPIMethod' => 'AlmanacConduitAPIMethod',
     'AlmanacQueryServicesConduitAPIMethod' => 'AlmanacConduitAPIMethod',
     'AlmanacSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'AlmanacService' => array(
       'AlmanacDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorCustomFieldInterface',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorProjectInterface',
       'AlmanacPropertyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'AlmanacServiceController' => 'AlmanacController',
     'AlmanacServiceDatasource' => 'PhabricatorTypeaheadDatasource',
     'AlmanacServiceEditController' => 'AlmanacServiceController',
     'AlmanacServiceEditor' => 'PhabricatorApplicationTransactionEditor',
     'AlmanacServiceListController' => 'AlmanacServiceController',
     'AlmanacServicePHIDType' => 'PhabricatorPHIDType',
     'AlmanacServiceQuery' => 'AlmanacQuery',
     'AlmanacServiceSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'AlmanacServiceTransaction' => 'PhabricatorApplicationTransaction',
     'AlmanacServiceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'AlmanacServiceType' => 'Phobject',
     'AlmanacServiceViewController' => 'AlmanacServiceController',
     'Aphront304Response' => 'AphrontResponse',
     'Aphront400Response' => 'AphrontResponse',
     'Aphront403Response' => 'AphrontHTMLResponse',
     'Aphront404Response' => 'AphrontHTMLResponse',
     'AphrontAjaxResponse' => 'AphrontResponse',
     'AphrontBarView' => 'AphrontView',
     'AphrontCSRFException' => 'AphrontException',
     'AphrontCalendarEventView' => 'AphrontView',
     'AphrontController' => 'Phobject',
     'AphrontCursorPagerView' => 'AphrontView',
     'AphrontDefaultApplicationConfiguration' => 'AphrontApplicationConfiguration',
     'AphrontDialogResponse' => 'AphrontResponse',
     'AphrontDialogView' => 'AphrontView',
     'AphrontException' => 'Exception',
     'AphrontFileResponse' => 'AphrontResponse',
     'AphrontFormCheckboxControl' => 'AphrontFormControl',
     'AphrontFormChooseButtonControl' => 'AphrontFormControl',
     'AphrontFormControl' => 'AphrontView',
     'AphrontFormDateControl' => 'AphrontFormControl',
     'AphrontFormDateControlValue' => 'Phobject',
     'AphrontFormDividerControl' => 'AphrontFormControl',
     'AphrontFormFileControl' => 'AphrontFormControl',
     'AphrontFormMarkupControl' => 'AphrontFormControl',
     'AphrontFormPasswordControl' => 'AphrontFormControl',
     'AphrontFormPolicyControl' => 'AphrontFormControl',
     'AphrontFormRadioButtonControl' => 'AphrontFormControl',
     'AphrontFormRecaptchaControl' => 'AphrontFormControl',
     'AphrontFormSelectControl' => 'AphrontFormControl',
     'AphrontFormStaticControl' => 'AphrontFormControl',
     'AphrontFormSubmitControl' => 'AphrontFormControl',
     'AphrontFormTextAreaControl' => 'AphrontFormControl',
     'AphrontFormTextControl' => 'AphrontFormControl',
     'AphrontFormTextWithSubmitControl' => 'AphrontFormControl',
     'AphrontFormTokenizerControl' => 'AphrontFormControl',
     'AphrontFormTypeaheadControl' => 'AphrontFormControl',
     'AphrontFormView' => 'AphrontView',
     'AphrontGlyphBarView' => 'AphrontBarView',
     'AphrontHTMLResponse' => 'AphrontResponse',
     'AphrontHTTPProxyResponse' => 'AphrontResponse',
     'AphrontHTTPSinkTestCase' => 'PhabricatorTestCase',
     'AphrontIsolatedDatabaseConnectionTestCase' => 'PhabricatorTestCase',
     'AphrontIsolatedHTTPSink' => 'AphrontHTTPSink',
     'AphrontJSONResponse' => 'AphrontResponse',
     'AphrontJavelinView' => 'AphrontView',
     'AphrontKeyboardShortcutsAvailableView' => 'AphrontView',
     'AphrontListFilterView' => 'AphrontView',
     'AphrontMoreView' => 'AphrontView',
     'AphrontMultiColumnView' => 'AphrontView',
     'AphrontMySQLDatabaseConnectionTestCase' => 'PhabricatorTestCase',
     'AphrontNullView' => 'AphrontView',
     'AphrontPHPHTTPSink' => 'AphrontHTTPSink',
     'AphrontPageView' => 'AphrontView',
     'AphrontPagerView' => 'AphrontView',
     'AphrontPlainTextResponse' => 'AphrontResponse',
     'AphrontProgressBarView' => 'AphrontBarView',
     'AphrontProxyResponse' => 'AphrontResponse',
     'AphrontRedirectResponse' => 'AphrontResponse',
     'AphrontRedirectResponseTestCase' => 'PhabricatorTestCase',
     'AphrontReloadResponse' => 'AphrontRedirectResponse',
     'AphrontRequestTestCase' => 'PhabricatorTestCase',
     'AphrontSideNavFilterView' => 'AphrontView',
     'AphrontStackTraceView' => 'AphrontView',
     'AphrontStandaloneHTMLResponse' => 'AphrontHTMLResponse',
     'AphrontTableView' => 'AphrontView',
     'AphrontTagView' => 'AphrontView',
     'AphrontTokenizerTemplateView' => 'AphrontView',
     'AphrontTwoColumnView' => 'AphrontView',
     'AphrontTypeaheadTemplateView' => 'AphrontView',
     'AphrontUnhandledExceptionResponse' => 'AphrontStandaloneHTMLResponse',
     'AphrontUsageException' => 'AphrontException',
     'AphrontView' => array(
       'Phobject',
       'PhutilSafeHTMLProducerInterface',
     ),
     'AphrontWebpageResponse' => 'AphrontHTMLResponse',
     'ArcanistConduitAPIMethod' => 'ConduitAPIMethod',
     'ArcanistProjectInfoConduitAPIMethod' => 'ArcanistConduitAPIMethod',
     'AuditConduitAPIMethod' => 'ConduitAPIMethod',
     'AuditQueryConduitAPIMethod' => 'AuditConduitAPIMethod',
     'AuthManageProvidersCapability' => 'PhabricatorPolicyCapability',
     'CalendarColors' => 'CalendarConstants',
     'CalendarTimeUtilTestCase' => 'PhabricatorTestCase',
     'CelerityManagementMapWorkflow' => 'CelerityManagementWorkflow',
     'CelerityManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'CelerityPhabricatorResourceController' => 'CelerityResourceController',
     'CelerityPhabricatorResources' => 'CelerityResourcesOnDisk',
     'CelerityPhysicalResources' => 'CelerityResources',
     'CelerityResourceController' => 'PhabricatorController',
     'CelerityResourceGraph' => 'AbstractDirectedGraph',
     'CelerityResourceTransformerTestCase' => 'PhabricatorTestCase',
     'CelerityResourcesOnDisk' => 'CelerityPhysicalResources',
     'ChatLogConduitAPIMethod' => 'ConduitAPIMethod',
     'ChatLogQueryConduitAPIMethod' => 'ChatLogConduitAPIMethod',
     'ChatLogRecordConduitAPIMethod' => 'ChatLogConduitAPIMethod',
     'ConduitAPIMethod' => array(
       'Phobject',
       'PhabricatorPolicyInterface',
     ),
     'ConduitApplicationNotInstalledException' => 'ConduitMethodNotFoundException',
     'ConduitCallTestCase' => 'PhabricatorTestCase',
     'ConduitConnectConduitAPIMethod' => 'ConduitAPIMethod',
     'ConduitConnectionGarbageCollector' => 'PhabricatorGarbageCollector',
     'ConduitDeprecatedCallSetupCheck' => 'PhabricatorSetupCheck',
     'ConduitException' => 'Exception',
     'ConduitGetCapabilitiesConduitAPIMethod' => 'ConduitAPIMethod',
     'ConduitGetCertificateConduitAPIMethod' => 'ConduitAPIMethod',
     'ConduitLogGarbageCollector' => 'PhabricatorGarbageCollector',
     'ConduitMethodDoesNotExistException' => 'ConduitMethodNotFoundException',
     'ConduitMethodNotFoundException' => 'ConduitException',
     'ConduitPingConduitAPIMethod' => 'ConduitAPIMethod',
     'ConduitQueryConduitAPIMethod' => 'ConduitAPIMethod',
     'ConduitSSHWorkflow' => 'PhabricatorSSHWorkflow',
     'ConduitTokenGarbageCollector' => 'PhabricatorGarbageCollector',
     'ConpherenceColumnViewController' => 'ConpherenceController',
     'ConpherenceConduitAPIMethod' => 'ConduitAPIMethod',
     'ConpherenceConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'ConpherenceController' => 'PhabricatorController',
     'ConpherenceCreateThreadConduitAPIMethod' => 'ConpherenceConduitAPIMethod',
     'ConpherenceCreateThreadMailReceiver' => 'PhabricatorMailReceiver',
     'ConpherenceDAO' => 'PhabricatorLiskDAO',
     'ConpherenceDurableColumnView' => 'AphrontTagView',
     'ConpherenceEditor' => 'PhabricatorApplicationTransactionEditor',
     'ConpherenceFileWidgetView' => 'ConpherenceWidgetView',
     'ConpherenceFormDragAndDropUploadControl' => 'AphrontFormControl',
     'ConpherenceFulltextQuery' => 'PhabricatorOffsetPagedQuery',
     'ConpherenceHovercardEventListener' => 'PhabricatorEventListener',
     'ConpherenceImageData' => 'ConpherenceConstants',
     'ConpherenceIndex' => 'ConpherenceDAO',
     'ConpherenceLayoutView' => 'AphrontView',
     'ConpherenceListController' => 'ConpherenceController',
     'ConpherenceMenuItemView' => 'AphrontTagView',
     'ConpherenceNewController' => 'ConpherenceController',
     'ConpherenceNewRoomController' => 'ConpherenceController',
     'ConpherenceNotificationPanelController' => 'ConpherenceController',
     'ConpherenceParticipant' => 'ConpherenceDAO',
     'ConpherenceParticipantCountQuery' => 'PhabricatorOffsetPagedQuery',
     'ConpherenceParticipantQuery' => 'PhabricatorOffsetPagedQuery',
     'ConpherenceParticipationStatus' => 'ConpherenceConstants',
     'ConpherencePeopleWidgetView' => 'ConpherenceWidgetView',
     'ConpherencePicCropControl' => 'AphrontFormControl',
     'ConpherenceQueryThreadConduitAPIMethod' => 'ConpherenceConduitAPIMethod',
     'ConpherenceQueryTransactionConduitAPIMethod' => 'ConpherenceConduitAPIMethod',
     'ConpherenceReplyHandler' => 'PhabricatorMailReplyHandler',
     'ConpherenceRoomListController' => 'ConpherenceController',
     'ConpherenceRoomTestCase' => 'ConpherenceTestCase',
     'ConpherenceSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'ConpherenceSettings' => 'ConpherenceConstants',
     'ConpherenceTestCase' => 'PhabricatorTestCase',
     'ConpherenceThread' => array(
       'ConpherenceDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorMentionableInterface',
       'PhabricatorDestructibleInterface',
     ),
     'ConpherenceThreadIndexer' => 'PhabricatorSearchDocumentIndexer',
     'ConpherenceThreadListView' => 'AphrontView',
     'ConpherenceThreadMailReceiver' => 'PhabricatorObjectMailReceiver',
     'ConpherenceThreadQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'ConpherenceThreadRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'ConpherenceThreadSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'ConpherenceThreadTestCase' => 'ConpherenceTestCase',
     'ConpherenceTransaction' => 'PhabricatorApplicationTransaction',
     'ConpherenceTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'ConpherenceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'ConpherenceTransactionType' => 'ConpherenceConstants',
     'ConpherenceTransactionView' => 'AphrontView',
     'ConpherenceUpdateActions' => 'ConpherenceConstants',
     'ConpherenceUpdateController' => 'ConpherenceController',
     'ConpherenceUpdateThreadConduitAPIMethod' => 'ConpherenceConduitAPIMethod',
     'ConpherenceViewController' => 'ConpherenceController',
     'ConpherenceWidgetConfigConstants' => 'ConpherenceConstants',
     'ConpherenceWidgetController' => 'ConpherenceController',
     'ConpherenceWidgetView' => 'AphrontView',
     'DarkConsoleController' => 'PhabricatorController',
     'DarkConsoleDataController' => 'PhabricatorController',
     'DarkConsoleErrorLogPlugin' => 'DarkConsolePlugin',
     'DarkConsoleEventPlugin' => 'DarkConsolePlugin',
     'DarkConsoleEventPluginAPI' => 'PhabricatorEventListener',
     'DarkConsoleRequestPlugin' => 'DarkConsolePlugin',
     'DarkConsoleServicesPlugin' => 'DarkConsolePlugin',
     'DarkConsoleXHProfPlugin' => 'DarkConsolePlugin',
     'DefaultDatabaseConfigurationProvider' => 'DatabaseConfigurationProvider',
     'DifferentialActionEmailCommand' => 'MetaMTAEmailTransactionCommand',
     'DifferentialActionMenuEventListener' => 'PhabricatorEventListener',
     'DifferentialAddCommentView' => 'AphrontView',
+    'DifferentialAdjustmentMapTestCase' => 'ArcanistPhutilTestCase',
     'DifferentialAffectedPath' => 'DifferentialDAO',
     'DifferentialApplyPatchField' => 'DifferentialCustomField',
     'DifferentialArcanistProjectField' => 'DifferentialCustomField',
     'DifferentialAsanaRepresentationField' => 'DifferentialCustomField',
     'DifferentialAuditorsField' => 'DifferentialStoredCustomField',
     'DifferentialAuthorField' => 'DifferentialCustomField',
     'DifferentialBlameRevisionField' => 'DifferentialStoredCustomField',
     'DifferentialBranchField' => 'DifferentialCustomField',
     'DifferentialChangesSinceLastUpdateField' => 'DifferentialCustomField',
     'DifferentialChangeset' => array(
       'DifferentialDAO',
       'PhabricatorPolicyInterface',
     ),
     'DifferentialChangesetDetailView' => 'AphrontView',
     'DifferentialChangesetHTMLRenderer' => 'DifferentialChangesetRenderer',
     'DifferentialChangesetListView' => 'AphrontView',
     'DifferentialChangesetOneUpRenderer' => 'DifferentialChangesetHTMLRenderer',
     'DifferentialChangesetOneUpTestRenderer' => 'DifferentialChangesetTestRenderer',
     'DifferentialChangesetParserTestCase' => 'PhabricatorTestCase',
     'DifferentialChangesetQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'DifferentialChangesetTestRenderer' => 'DifferentialChangesetRenderer',
     'DifferentialChangesetTwoUpRenderer' => 'DifferentialChangesetHTMLRenderer',
     'DifferentialChangesetTwoUpTestRenderer' => 'DifferentialChangesetTestRenderer',
     'DifferentialChangesetViewController' => 'DifferentialController',
     'DifferentialCloseConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialCommentPreviewController' => 'DifferentialController',
     'DifferentialCommentSaveController' => 'DifferentialController',
     'DifferentialCommitMessageParserTestCase' => 'PhabricatorTestCase',
     'DifferentialCommitsField' => 'DifferentialCustomField',
     'DifferentialConduitAPIMethod' => 'ConduitAPIMethod',
     'DifferentialConflictsField' => 'DifferentialCustomField',
     'DifferentialController' => 'PhabricatorController',
     'DifferentialCoreCustomField' => 'DifferentialCustomField',
     'DifferentialCreateCommentConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialCreateDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialCreateInlineConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialCreateMailReceiver' => 'PhabricatorMailReceiver',
     'DifferentialCreateRawDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialCreateRevisionConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialCustomField' => 'PhabricatorCustomField',
     'DifferentialCustomFieldDependsOnParser' => 'PhabricatorCustomFieldMonogramParser',
     'DifferentialCustomFieldDependsOnParserTestCase' => 'PhabricatorTestCase',
     'DifferentialCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
     'DifferentialCustomFieldRevertsParser' => 'PhabricatorCustomFieldMonogramParser',
     'DifferentialCustomFieldRevertsParserTestCase' => 'PhabricatorTestCase',
     'DifferentialCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
     'DifferentialCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
     'DifferentialDAO' => 'PhabricatorLiskDAO',
     'DifferentialDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'DifferentialDependenciesField' => 'DifferentialCustomField',
     'DifferentialDependsOnField' => 'DifferentialCustomField',
     'DifferentialDiff' => array(
       'DifferentialDAO',
       'PhabricatorPolicyInterface',
       'HarbormasterBuildableInterface',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorDestructibleInterface',
     ),
     'DifferentialDiffCreateController' => 'DifferentialController',
     'DifferentialDiffEditor' => 'PhabricatorApplicationTransactionEditor',
     'DifferentialDiffInlineCommentQuery' => 'PhabricatorDiffInlineCommentQuery',
     'DifferentialDiffPHIDType' => 'PhabricatorPHIDType',
     'DifferentialDiffProperty' => 'DifferentialDAO',
     'DifferentialDiffQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'DifferentialDiffTableOfContentsView' => 'AphrontView',
     'DifferentialDiffTestCase' => 'ArcanistPhutilTestCase',
     'DifferentialDiffTransaction' => 'PhabricatorApplicationTransaction',
     'DifferentialDiffViewController' => 'DifferentialController',
     'DifferentialDoorkeeperRevisionFeedStoryPublisher' => 'DoorkeeperFeedStoryPublisher',
     'DifferentialDraft' => 'DifferentialDAO',
     'DifferentialEditPolicyField' => 'DifferentialCoreCustomField',
     'DifferentialFieldParseException' => 'Exception',
     'DifferentialFieldValidationException' => 'Exception',
     'DifferentialFindConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialFinishPostponedLintersConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialGetAllDiffsConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialGetCommitMessageConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialGetCommitPathsConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialGetDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialGetRawDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialGetRevisionCommentsConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialGetRevisionConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialGitHubLandingStrategy' => 'DifferentialLandingStrategy',
     'DifferentialGitSVNIDField' => 'DifferentialCustomField',
     'DifferentialHostField' => 'DifferentialCustomField',
     'DifferentialHostedGitLandingStrategy' => 'DifferentialLandingStrategy',
     'DifferentialHostedMercurialLandingStrategy' => 'DifferentialLandingStrategy',
     'DifferentialHovercardEventListener' => 'PhabricatorEventListener',
     'DifferentialHunk' => array(
       'DifferentialDAO',
       'PhabricatorPolicyInterface',
     ),
     'DifferentialHunkParserTestCase' => 'PhabricatorTestCase',
     'DifferentialHunkQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'DifferentialHunkTestCase' => 'ArcanistPhutilTestCase',
     'DifferentialInlineComment' => 'PhabricatorInlineCommentInterface',
     'DifferentialInlineCommentEditController' => 'PhabricatorInlineCommentController',
     'DifferentialInlineCommentPreviewController' => 'PhabricatorInlineCommentPreviewController',
     'DifferentialInlineCommentQuery' => 'PhabricatorOffsetPagedQuery',
     'DifferentialJIRAIssuesField' => 'DifferentialStoredCustomField',
     'DifferentialLandingActionMenuEventListener' => 'PhabricatorEventListener',
     'DifferentialLegacyHunk' => 'DifferentialHunk',
+    'DifferentialLineAdjustmentMap' => 'Phobject',
     'DifferentialLintField' => 'DifferentialCustomField',
     'DifferentialLocalCommitsView' => 'AphrontView',
     'DifferentialManiphestTasksField' => 'DifferentialCoreCustomField',
     'DifferentialModernHunk' => 'DifferentialHunk',
     'DifferentialParseCacheGarbageCollector' => 'PhabricatorGarbageCollector',
     'DifferentialParseCommitMessageConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialParseRenderTestCase' => 'PhabricatorTestCase',
     'DifferentialPathField' => 'DifferentialCustomField',
     'DifferentialPrimaryPaneView' => 'AphrontView',
     'DifferentialProjectReviewersField' => 'DifferentialCustomField',
     'DifferentialProjectsField' => 'DifferentialCoreCustomField',
     'DifferentialQueryConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialQueryDiffsConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'DifferentialReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'DifferentialRepositoryField' => 'DifferentialCoreCustomField',
     'DifferentialRepositoryLookup' => 'Phobject',
     'DifferentialRequiredSignaturesField' => 'DifferentialCoreCustomField',
     'DifferentialResultsTableView' => 'AphrontView',
     'DifferentialRevertPlanField' => 'DifferentialStoredCustomField',
     'DifferentialReviewedByField' => 'DifferentialCoreCustomField',
     'DifferentialReviewerForRevisionEdgeType' => 'PhabricatorEdgeType',
     'DifferentialReviewersField' => 'DifferentialCoreCustomField',
     'DifferentialReviewersView' => 'AphrontView',
     'DifferentialRevision' => array(
       'DifferentialDAO',
       'PhabricatorTokenReceiverInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorFlaggableInterface',
       'PhrequentTrackableInterface',
       'HarbormasterBuildableInterface',
       'PhabricatorSubscribableInterface',
       'PhabricatorCustomFieldInterface',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorMentionableInterface',
       'PhabricatorDestructibleInterface',
       'PhabricatorProjectInterface',
     ),
     'DifferentialRevisionCloseDetailsController' => 'DifferentialController',
     'DifferentialRevisionDependedOnByRevisionEdgeType' => 'PhabricatorEdgeType',
     'DifferentialRevisionDependsOnRevisionEdgeType' => 'PhabricatorEdgeType',
     'DifferentialRevisionDetailView' => 'AphrontView',
     'DifferentialRevisionEditController' => 'DifferentialController',
     'DifferentialRevisionHasCommitEdgeType' => 'PhabricatorEdgeType',
     'DifferentialRevisionHasReviewerEdgeType' => 'PhabricatorEdgeType',
     'DifferentialRevisionHasTaskEdgeType' => 'PhabricatorEdgeType',
     'DifferentialRevisionIDField' => 'DifferentialCustomField',
     'DifferentialRevisionLandController' => 'DifferentialController',
     'DifferentialRevisionListController' => 'DifferentialController',
     'DifferentialRevisionListView' => 'AphrontView',
     'DifferentialRevisionMailReceiver' => 'PhabricatorObjectMailReceiver',
     'DifferentialRevisionPHIDType' => 'PhabricatorPHIDType',
     'DifferentialRevisionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'DifferentialRevisionSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'DifferentialRevisionUpdateHistoryView' => 'AphrontView',
     'DifferentialRevisionViewController' => 'DifferentialController',
     'DifferentialSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'DifferentialSearchIndexer' => 'PhabricatorSearchDocumentIndexer',
     'DifferentialSetDiffPropertyConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialStoredCustomField' => 'DifferentialCustomField',
     'DifferentialSubscribersField' => 'DifferentialCoreCustomField',
     'DifferentialSummaryField' => 'DifferentialCoreCustomField',
     'DifferentialTestPlanField' => 'DifferentialCoreCustomField',
     'DifferentialTitleField' => 'DifferentialCoreCustomField',
     'DifferentialTransaction' => 'PhabricatorApplicationTransaction',
     'DifferentialTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'DifferentialTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
     'DifferentialTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'DifferentialTransactionView' => 'PhabricatorApplicationTransactionView',
     'DifferentialUnitField' => 'DifferentialCustomField',
     'DifferentialUpdateRevisionConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialUpdateUnitResultsConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialViewPolicyField' => 'DifferentialCoreCustomField',
     'DiffusionArcanistProjectDatasource' => 'PhabricatorTypeaheadDatasource',
     'DiffusionAuditorDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'DiffusionBranchQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionBranchTableController' => 'DiffusionController',
     'DiffusionBranchTableView' => 'DiffusionView',
     'DiffusionBrowseController' => 'DiffusionController',
     'DiffusionBrowseDirectoryController' => 'DiffusionBrowseController',
     'DiffusionBrowseFileController' => 'DiffusionBrowseController',
     'DiffusionBrowseMainController' => 'DiffusionBrowseController',
     'DiffusionBrowseQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionBrowseSearchController' => 'DiffusionBrowseController',
     'DiffusionBrowseTableView' => 'DiffusionView',
     'DiffusionCachedResolveRefsQuery' => 'DiffusionLowLevelQuery',
     'DiffusionChangeController' => 'DiffusionController',
     'DiffusionCommitBranchesController' => 'DiffusionController',
     'DiffusionCommitChangeTableView' => 'DiffusionView',
     'DiffusionCommitController' => 'DiffusionController',
     'DiffusionCommitEditController' => 'DiffusionController',
     'DiffusionCommitHasRevisionEdgeType' => 'PhabricatorEdgeType',
     'DiffusionCommitHasTaskEdgeType' => 'PhabricatorEdgeType',
     'DiffusionCommitHash' => 'Phobject',
     'DiffusionCommitHookEngine' => 'Phobject',
     'DiffusionCommitHookRejectException' => 'Exception',
     'DiffusionCommitParentsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionCommitQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'DiffusionCommitRef' => 'Phobject',
     'DiffusionCommitRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'DiffusionCommitRemarkupRuleTestCase' => 'PhabricatorTestCase',
     'DiffusionCommitRevertedByCommitEdgeType' => 'PhabricatorEdgeType',
     'DiffusionCommitRevertsCommitEdgeType' => 'PhabricatorEdgeType',
     'DiffusionCommitTagsController' => 'DiffusionController',
     'DiffusionConduitAPIMethod' => 'ConduitAPIMethod',
     'DiffusionController' => 'PhabricatorController',
     'DiffusionCreateCommentConduitAPIMethod' => 'DiffusionConduitAPIMethod',
     'DiffusionCreateRepositoriesCapability' => 'PhabricatorPolicyCapability',
     'DiffusionDefaultEditCapability' => 'PhabricatorPolicyCapability',
     'DiffusionDefaultPushCapability' => 'PhabricatorPolicyCapability',
     'DiffusionDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'DiffusionDiffController' => 'DiffusionController',
     'DiffusionDiffInlineCommentQuery' => 'PhabricatorDiffInlineCommentQuery',
     'DiffusionDiffQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionDoorkeeperCommitFeedStoryPublisher' => 'DoorkeeperFeedStoryPublisher',
     'DiffusionEmptyResultView' => 'DiffusionView',
     'DiffusionExistsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionExternalController' => 'DiffusionController',
     'DiffusionFileContentQuery' => 'DiffusionQuery',
     'DiffusionFileContentQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionFindSymbolsConduitAPIMethod' => 'DiffusionConduitAPIMethod',
     'DiffusionGetCommitsConduitAPIMethod' => 'DiffusionConduitAPIMethod',
     'DiffusionGetLintMessagesConduitAPIMethod' => 'DiffusionConduitAPIMethod',
     'DiffusionGetRecentCommitsByPathConduitAPIMethod' => 'DiffusionConduitAPIMethod',
     'DiffusionGitBranchTestCase' => 'PhabricatorTestCase',
     'DiffusionGitFileContentQuery' => 'DiffusionFileContentQuery',
     'DiffusionGitFileContentQueryTestCase' => 'PhabricatorTestCase',
     'DiffusionGitRawDiffQuery' => 'DiffusionRawDiffQuery',
     'DiffusionGitReceivePackSSHWorkflow' => 'DiffusionGitSSHWorkflow',
     'DiffusionGitRequest' => 'DiffusionRequest',
     'DiffusionGitResponse' => 'AphrontResponse',
     'DiffusionGitSSHWorkflow' => 'DiffusionSSHWorkflow',
     'DiffusionGitUploadPackSSHWorkflow' => 'DiffusionGitSSHWorkflow',
     'DiffusionHistoryController' => 'DiffusionController',
     'DiffusionHistoryQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionHistoryTableView' => 'DiffusionView',
     'DiffusionHovercardEventListener' => 'PhabricatorEventListener',
     'DiffusionInlineCommentController' => 'PhabricatorInlineCommentController',
     'DiffusionInlineCommentPreviewController' => 'PhabricatorInlineCommentPreviewController',
     'DiffusionLastModifiedController' => 'DiffusionController',
     'DiffusionLastModifiedQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionLintController' => 'DiffusionController',
     'DiffusionLintCountQuery' => 'PhabricatorQuery',
     'DiffusionLintDetailsController' => 'DiffusionController',
     'DiffusionLookSoonConduitAPIMethod' => 'DiffusionConduitAPIMethod',
     'DiffusionLowLevelCommitFieldsQuery' => 'DiffusionLowLevelQuery',
     'DiffusionLowLevelCommitQuery' => 'DiffusionLowLevelQuery',
     'DiffusionLowLevelGitRefQuery' => 'DiffusionLowLevelQuery',
     'DiffusionLowLevelMercurialBranchesQuery' => 'DiffusionLowLevelQuery',
     'DiffusionLowLevelMercurialPathsQuery' => 'DiffusionLowLevelQuery',
     'DiffusionLowLevelParentsQuery' => 'DiffusionLowLevelQuery',
     'DiffusionLowLevelQuery' => 'Phobject',
     'DiffusionLowLevelResolveRefsQuery' => 'DiffusionLowLevelQuery',
     'DiffusionMercurialFileContentQuery' => 'DiffusionFileContentQuery',
     'DiffusionMercurialRawDiffQuery' => 'DiffusionRawDiffQuery',
     'DiffusionMercurialRequest' => 'DiffusionRequest',
     'DiffusionMercurialResponse' => 'AphrontResponse',
     'DiffusionMercurialSSHWorkflow' => 'DiffusionSSHWorkflow',
     'DiffusionMercurialServeSSHWorkflow' => 'DiffusionMercurialSSHWorkflow',
     'DiffusionMercurialWireClientSSHProtocolChannel' => 'PhutilProtocolChannel',
     'DiffusionMercurialWireSSHTestCase' => 'PhabricatorTestCase',
     'DiffusionMergedCommitsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionMirrorDeleteController' => 'DiffusionController',
     'DiffusionMirrorEditController' => 'DiffusionController',
     'DiffusionPathCompleteController' => 'DiffusionController',
     'DiffusionPathQueryTestCase' => 'PhabricatorTestCase',
     'DiffusionPathTreeController' => 'DiffusionController',
     'DiffusionPathValidateController' => 'DiffusionController',
     'DiffusionPushCapability' => 'PhabricatorPolicyCapability',
     'DiffusionPushEventViewController' => 'DiffusionPushLogController',
     'DiffusionPushLogController' => 'DiffusionController',
     'DiffusionPushLogListController' => 'DiffusionPushLogController',
     'DiffusionPushLogListView' => 'AphrontView',
     'DiffusionQuery' => 'PhabricatorQuery',
     'DiffusionQueryCommitsConduitAPIMethod' => 'DiffusionConduitAPIMethod',
     'DiffusionQueryConduitAPIMethod' => 'DiffusionConduitAPIMethod',
     'DiffusionQueryPathsConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionRawDiffQuery' => 'DiffusionQuery',
     'DiffusionRawDiffQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionReadmeView' => 'DiffusionView',
     'DiffusionRefNotFoundException' => 'Exception',
     'DiffusionRefTableController' => 'DiffusionController',
     'DiffusionRefsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionRepositoryByIDRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'DiffusionRepositoryController' => 'DiffusionController',
     'DiffusionRepositoryCreateController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryDatasource' => 'PhabricatorTypeaheadDatasource',
     'DiffusionRepositoryDefaultController' => 'DiffusionController',
     'DiffusionRepositoryEditActionsController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryEditActivateController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryEditBasicController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryEditBranchesController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryEditController' => 'DiffusionController',
     'DiffusionRepositoryEditDangerousController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryEditDeleteController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryEditEncodingController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryEditHostingController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryEditMainController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryEditStorageController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryEditSubversionController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryEditUpdateController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryListController' => 'DiffusionController',
     'DiffusionRepositoryNewController' => 'DiffusionController',
     'DiffusionRepositoryRef' => 'Phobject',
     'DiffusionRepositoryRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'DiffusionResolveRefsConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionResolveUserQuery' => 'Phobject',
     'DiffusionSSHWorkflow' => 'PhabricatorSSHWorkflow',
     'DiffusionSearchQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionServeController' => 'DiffusionController',
     'DiffusionSetPasswordSettingsPanel' => 'PhabricatorSettingsPanel',
     'DiffusionSetupException' => 'AphrontUsageException',
     'DiffusionSubversionSSHWorkflow' => 'DiffusionSSHWorkflow',
     'DiffusionSubversionServeSSHWorkflow' => 'DiffusionSubversionSSHWorkflow',
     'DiffusionSubversionWireProtocol' => 'Phobject',
     'DiffusionSubversionWireProtocolTestCase' => 'PhabricatorTestCase',
     'DiffusionSvnFileContentQuery' => 'DiffusionFileContentQuery',
     'DiffusionSvnRawDiffQuery' => 'DiffusionRawDiffQuery',
     'DiffusionSvnRequest' => 'DiffusionRequest',
     'DiffusionSymbolController' => 'DiffusionController',
     'DiffusionSymbolDatasource' => 'PhabricatorTypeaheadDatasource',
     'DiffusionSymbolQuery' => 'PhabricatorOffsetPagedQuery',
     'DiffusionTagListController' => 'DiffusionController',
     'DiffusionTagListView' => 'DiffusionView',
     'DiffusionTagsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionURITestCase' => 'ArcanistPhutilTestCase',
     'DiffusionUpdateCoverageConduitAPIMethod' => 'DiffusionConduitAPIMethod',
     'DiffusionView' => 'AphrontView',
     'DivinerArticleAtomizer' => 'DivinerAtomizer',
     'DivinerAtomCache' => 'DivinerDiskCache',
     'DivinerAtomController' => 'DivinerController',
     'DivinerAtomListController' => 'DivinerController',
     'DivinerAtomPHIDType' => 'PhabricatorPHIDType',
     'DivinerAtomQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'DivinerAtomSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'DivinerAtomizeWorkflow' => 'DivinerWorkflow',
     'DivinerBookController' => 'DivinerController',
     'DivinerBookItemView' => 'AphrontTagView',
     'DivinerBookPHIDType' => 'PhabricatorPHIDType',
     'DivinerBookQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'DivinerController' => 'PhabricatorController',
     'DivinerDAO' => 'PhabricatorLiskDAO',
     'DivinerDefaultRenderer' => 'DivinerRenderer',
     'DivinerFileAtomizer' => 'DivinerAtomizer',
     'DivinerFindController' => 'DivinerController',
     'DivinerGenerateWorkflow' => 'DivinerWorkflow',
     'DivinerLiveAtom' => 'DivinerDAO',
     'DivinerLiveBook' => array(
       'DivinerDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'DivinerLivePublisher' => 'DivinerPublisher',
     'DivinerLiveSymbol' => array(
       'DivinerDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorMarkupInterface',
       'PhabricatorDestructibleInterface',
     ),
     'DivinerMainController' => 'DivinerController',
     'DivinerPHPAtomizer' => 'DivinerAtomizer',
     'DivinerParameterTableView' => 'AphrontTagView',
     'DivinerPublishCache' => 'DivinerDiskCache',
     'DivinerReturnTableView' => 'AphrontTagView',
     'DivinerSectionView' => 'AphrontTagView',
     'DivinerStaticPublisher' => 'DivinerPublisher',
     'DivinerSymbolRemarkupRule' => 'PhutilRemarkupRule',
     'DivinerWorkflow' => 'PhabricatorManagementWorkflow',
     'DoorkeeperAsanaFeedWorker' => 'DoorkeeperFeedWorker',
     'DoorkeeperBridge' => 'Phobject',
     'DoorkeeperBridgeAsana' => 'DoorkeeperBridge',
     'DoorkeeperBridgeJIRA' => 'DoorkeeperBridge',
     'DoorkeeperBridgeJIRATestCase' => 'PhabricatorTestCase',
     'DoorkeeperDAO' => 'PhabricatorLiskDAO',
     'DoorkeeperExternalObject' => array(
       'DoorkeeperDAO',
       'PhabricatorPolicyInterface',
     ),
     'DoorkeeperExternalObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'DoorkeeperFeedWorker' => 'FeedPushWorker',
     'DoorkeeperImportEngine' => 'Phobject',
     'DoorkeeperJIRAFeedWorker' => 'DoorkeeperFeedWorker',
     'DoorkeeperMissingLinkException' => 'Exception',
     'DoorkeeperObjectRef' => 'Phobject',
     'DoorkeeperRemarkupRule' => 'PhutilRemarkupRule',
     'DoorkeeperRemarkupRuleAsana' => 'DoorkeeperRemarkupRule',
     'DoorkeeperRemarkupRuleJIRA' => 'DoorkeeperRemarkupRule',
     'DoorkeeperSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'DoorkeeperTagView' => 'AphrontView',
     'DoorkeeperTagsController' => 'PhabricatorController',
     'DrydockAllocatorWorker' => 'PhabricatorWorker',
     'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface',
     'DrydockBlueprint' => array(
       'DrydockDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorCustomFieldInterface',
     ),
     'DrydockBlueprintController' => 'DrydockController',
     'DrydockBlueprintCoreCustomField' => array(
       'DrydockBlueprintCustomField',
       'PhabricatorStandardCustomFieldInterface',
     ),
     'DrydockBlueprintCreateController' => 'DrydockBlueprintController',
     'DrydockBlueprintCustomField' => 'PhabricatorCustomField',
     'DrydockBlueprintEditController' => 'DrydockBlueprintController',
     'DrydockBlueprintEditor' => 'PhabricatorApplicationTransactionEditor',
     'DrydockBlueprintListController' => 'DrydockBlueprintController',
     'DrydockBlueprintPHIDType' => 'PhabricatorPHIDType',
     'DrydockBlueprintQuery' => 'DrydockQuery',
     'DrydockBlueprintSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'DrydockBlueprintTransaction' => 'PhabricatorApplicationTransaction',
     'DrydockBlueprintTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'DrydockBlueprintViewController' => 'DrydockBlueprintController',
     'DrydockCommandInterface' => 'DrydockInterface',
     'DrydockConsoleController' => 'DrydockController',
     'DrydockController' => 'PhabricatorController',
     'DrydockCreateBlueprintsCapability' => 'PhabricatorPolicyCapability',
     'DrydockDAO' => 'PhabricatorLiskDAO',
     'DrydockDefaultEditCapability' => 'PhabricatorPolicyCapability',
     'DrydockDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'DrydockFilesystemInterface' => 'DrydockInterface',
     'DrydockLease' => array(
       'DrydockDAO',
       'PhabricatorPolicyInterface',
     ),
     'DrydockLeaseController' => 'DrydockController',
     'DrydockLeaseListController' => 'DrydockLeaseController',
     'DrydockLeaseListView' => 'AphrontView',
     'DrydockLeasePHIDType' => 'PhabricatorPHIDType',
     'DrydockLeaseQuery' => 'DrydockQuery',
     'DrydockLeaseReleaseController' => 'DrydockLeaseController',
     'DrydockLeaseSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'DrydockLeaseStatus' => 'DrydockConstants',
     'DrydockLeaseViewController' => 'DrydockLeaseController',
     'DrydockLocalCommandInterface' => 'DrydockCommandInterface',
     'DrydockLog' => array(
       'DrydockDAO',
       'PhabricatorPolicyInterface',
     ),
     'DrydockLogController' => 'DrydockController',
     'DrydockLogListController' => 'DrydockLogController',
     'DrydockLogListView' => 'AphrontView',
     'DrydockLogQuery' => 'DrydockQuery',
     'DrydockLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'DrydockManagementCloseWorkflow' => 'DrydockManagementWorkflow',
     'DrydockManagementCreateResourceWorkflow' => 'DrydockManagementWorkflow',
     'DrydockManagementLeaseWorkflow' => 'DrydockManagementWorkflow',
     'DrydockManagementReleaseWorkflow' => 'DrydockManagementWorkflow',
     'DrydockManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'DrydockPreallocatedHostBlueprintImplementation' => 'DrydockBlueprintImplementation',
     'DrydockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'DrydockResource' => array(
       'DrydockDAO',
       'PhabricatorPolicyInterface',
     ),
     'DrydockResourceCloseController' => 'DrydockResourceController',
     'DrydockResourceController' => 'DrydockController',
     'DrydockResourceListController' => 'DrydockResourceController',
     'DrydockResourceListView' => 'AphrontView',
     'DrydockResourcePHIDType' => 'PhabricatorPHIDType',
     'DrydockResourceQuery' => 'DrydockQuery',
     'DrydockResourceSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'DrydockResourceStatus' => 'DrydockConstants',
     'DrydockResourceViewController' => 'DrydockResourceController',
     'DrydockSFTPFilesystemInterface' => 'DrydockFilesystemInterface',
     'DrydockSSHCommandInterface' => 'DrydockCommandInterface',
     'DrydockWebrootInterface' => 'DrydockInterface',
     'DrydockWorkingCopyBlueprintImplementation' => 'DrydockBlueprintImplementation',
     'FeedConduitAPIMethod' => 'ConduitAPIMethod',
     'FeedPublishConduitAPIMethod' => 'FeedConduitAPIMethod',
     'FeedPublisherHTTPWorker' => 'FeedPushWorker',
     'FeedPublisherWorker' => 'FeedPushWorker',
     'FeedPushWorker' => 'PhabricatorWorker',
     'FeedQueryConduitAPIMethod' => 'FeedConduitAPIMethod',
     'FileAllocateConduitAPIMethod' => 'FileConduitAPIMethod',
     'FileConduitAPIMethod' => 'ConduitAPIMethod',
     'FileCreateMailReceiver' => 'PhabricatorMailReceiver',
     'FileDownloadConduitAPIMethod' => 'FileConduitAPIMethod',
     'FileInfoConduitAPIMethod' => 'FileConduitAPIMethod',
     'FileMailReceiver' => 'PhabricatorObjectMailReceiver',
     'FileQueryChunksConduitAPIMethod' => 'FileConduitAPIMethod',
     'FileReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'FileUploadChunkConduitAPIMethod' => 'FileConduitAPIMethod',
     'FileUploadConduitAPIMethod' => 'FileConduitAPIMethod',
     'FileUploadHashConduitAPIMethod' => 'FileConduitAPIMethod',
     'FilesDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'FlagConduitAPIMethod' => 'ConduitAPIMethod',
     'FlagDeleteConduitAPIMethod' => 'FlagConduitAPIMethod',
     'FlagEditConduitAPIMethod' => 'FlagConduitAPIMethod',
     'FlagQueryConduitAPIMethod' => 'FlagConduitAPIMethod',
     'FundBacker' => array(
       'FundDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorApplicationTransactionInterface',
     ),
     'FundBackerCart' => 'PhortuneCartImplementation',
     'FundBackerEditor' => 'PhabricatorApplicationTransactionEditor',
     'FundBackerListController' => 'FundController',
     'FundBackerPHIDType' => 'PhabricatorPHIDType',
     'FundBackerProduct' => 'PhortuneProductImplementation',
     'FundBackerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'FundBackerSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'FundBackerTransaction' => 'PhabricatorApplicationTransaction',
     'FundBackerTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'FundController' => 'PhabricatorController',
     'FundCreateInitiativesCapability' => 'PhabricatorPolicyCapability',
     'FundDAO' => 'PhabricatorLiskDAO',
     'FundDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'FundInitiative' => array(
       'FundDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorProjectInterface',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorSubscribableInterface',
       'PhabricatorMentionableInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorTokenReceiverInterface',
       'PhabricatorDestructibleInterface',
     ),
     'FundInitiativeBackController' => 'FundController',
     'FundInitiativeCloseController' => 'FundController',
     'FundInitiativeEditController' => 'FundController',
     'FundInitiativeEditor' => 'PhabricatorApplicationTransactionEditor',
     'FundInitiativeIndexer' => 'PhabricatorSearchDocumentIndexer',
     'FundInitiativeListController' => 'FundController',
     'FundInitiativePHIDType' => 'PhabricatorPHIDType',
     'FundInitiativeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'FundInitiativeRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'FundInitiativeReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'FundInitiativeSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'FundInitiativeTransaction' => 'PhabricatorApplicationTransaction',
     'FundInitiativeTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'FundInitiativeViewController' => 'FundController',
     'FundSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'HarbormasterBuild' => array(
       'HarbormasterDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
     ),
     'HarbormasterBuildAbortedException' => 'Exception',
     'HarbormasterBuildActionController' => 'HarbormasterController',
     'HarbormasterBuildArtifact' => array(
       'HarbormasterDAO',
       'PhabricatorPolicyInterface',
     ),
     'HarbormasterBuildArtifactQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'HarbormasterBuildCommand' => 'HarbormasterDAO',
     'HarbormasterBuildDependencyDatasource' => 'PhabricatorTypeaheadDatasource',
     'HarbormasterBuildEngine' => 'Phobject',
     'HarbormasterBuildFailureException' => 'Exception',
     'HarbormasterBuildGraph' => 'AbstractDirectedGraph',
     'HarbormasterBuildItem' => 'HarbormasterDAO',
     'HarbormasterBuildItemPHIDType' => 'PhabricatorPHIDType',
     'HarbormasterBuildItemQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'HarbormasterBuildLog' => array(
       'HarbormasterDAO',
       'PhabricatorPolicyInterface',
     ),
     'HarbormasterBuildLogPHIDType' => 'PhabricatorPHIDType',
     'HarbormasterBuildLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'HarbormasterBuildMessage' => array(
       'HarbormasterDAO',
       'PhabricatorPolicyInterface',
     ),
     'HarbormasterBuildMessageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'HarbormasterBuildPHIDType' => 'PhabricatorPHIDType',
     'HarbormasterBuildPlan' => array(
       'HarbormasterDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorSubscribableInterface',
     ),
     'HarbormasterBuildPlanDatasource' => 'PhabricatorTypeaheadDatasource',
     'HarbormasterBuildPlanEditor' => 'PhabricatorApplicationTransactionEditor',
     'HarbormasterBuildPlanPHIDType' => 'PhabricatorPHIDType',
     'HarbormasterBuildPlanQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'HarbormasterBuildPlanSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'HarbormasterBuildPlanTransaction' => 'PhabricatorApplicationTransaction',
     'HarbormasterBuildPlanTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'HarbormasterBuildQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'HarbormasterBuildStep' => array(
       'HarbormasterDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorCustomFieldInterface',
     ),
     'HarbormasterBuildStepCoreCustomField' => array(
       'HarbormasterBuildStepCustomField',
       'PhabricatorStandardCustomFieldInterface',
     ),
     'HarbormasterBuildStepCustomField' => 'PhabricatorCustomField',
     'HarbormasterBuildStepEditor' => 'PhabricatorApplicationTransactionEditor',
     'HarbormasterBuildStepPHIDType' => 'PhabricatorPHIDType',
     'HarbormasterBuildStepQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'HarbormasterBuildStepTransaction' => 'PhabricatorApplicationTransaction',
     'HarbormasterBuildStepTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'HarbormasterBuildTarget' => array(
       'HarbormasterDAO',
       'PhabricatorPolicyInterface',
     ),
     'HarbormasterBuildTargetPHIDType' => 'PhabricatorPHIDType',
     'HarbormasterBuildTargetQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'HarbormasterBuildTransaction' => 'PhabricatorApplicationTransaction',
     'HarbormasterBuildTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
     'HarbormasterBuildTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'HarbormasterBuildViewController' => 'HarbormasterController',
     'HarbormasterBuildWorker' => 'HarbormasterWorker',
     'HarbormasterBuildable' => array(
       'HarbormasterDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
       'HarbormasterBuildableInterface',
     ),
     'HarbormasterBuildableActionController' => 'HarbormasterController',
     'HarbormasterBuildableListController' => 'HarbormasterController',
     'HarbormasterBuildablePHIDType' => 'PhabricatorPHIDType',
     'HarbormasterBuildableQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'HarbormasterBuildableSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'HarbormasterBuildableTransaction' => 'PhabricatorApplicationTransaction',
     'HarbormasterBuildableTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
     'HarbormasterBuildableTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'HarbormasterBuildableViewController' => 'HarbormasterController',
     'HarbormasterCommandBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
     'HarbormasterConduitAPIMethod' => 'ConduitAPIMethod',
     'HarbormasterController' => 'PhabricatorController',
     'HarbormasterDAO' => 'PhabricatorLiskDAO',
     'HarbormasterHTTPRequestBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
     'HarbormasterLeaseHostBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
     'HarbormasterManagePlansCapability' => 'PhabricatorPolicyCapability',
     'HarbormasterManagementBuildWorkflow' => 'HarbormasterManagementWorkflow',
     'HarbormasterManagementUpdateWorkflow' => 'HarbormasterManagementWorkflow',
     'HarbormasterManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'HarbormasterObject' => 'HarbormasterDAO',
     'HarbormasterPlanController' => 'HarbormasterController',
     'HarbormasterPlanDisableController' => 'HarbormasterPlanController',
     'HarbormasterPlanEditController' => 'HarbormasterPlanController',
     'HarbormasterPlanListController' => 'HarbormasterPlanController',
     'HarbormasterPlanRunController' => 'HarbormasterController',
     'HarbormasterPlanViewController' => 'HarbormasterPlanController',
     'HarbormasterPublishFragmentBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
     'HarbormasterQueryBuildablesConduitAPIMethod' => 'HarbormasterConduitAPIMethod',
     'HarbormasterQueryBuildsConduitAPIMethod' => 'HarbormasterConduitAPIMethod',
     'HarbormasterRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'HarbormasterSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'HarbormasterScratchTable' => 'HarbormasterDAO',
     'HarbormasterSendMessageConduitAPIMethod' => 'HarbormasterConduitAPIMethod',
     'HarbormasterSleepBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
     'HarbormasterStepAddController' => 'HarbormasterController',
     'HarbormasterStepDeleteController' => 'HarbormasterController',
     'HarbormasterStepEditController' => 'HarbormasterController',
     'HarbormasterTargetWorker' => 'HarbormasterWorker',
     'HarbormasterThrowExceptionBuildStep' => 'HarbormasterBuildStepImplementation',
     'HarbormasterUIEventListener' => 'PhabricatorEventListener',
     'HarbormasterUploadArtifactBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
     'HarbormasterWaitForPreviousBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
     'HarbormasterWorker' => 'PhabricatorWorker',
     'HeraldAction' => 'HeraldDAO',
     'HeraldApplyTranscript' => 'Phobject',
     'HeraldCommitAdapter' => 'HeraldAdapter',
     'HeraldCondition' => 'HeraldDAO',
     'HeraldController' => 'PhabricatorController',
     'HeraldDAO' => 'PhabricatorLiskDAO',
     'HeraldDifferentialAdapter' => 'HeraldAdapter',
     'HeraldDifferentialDiffAdapter' => 'HeraldDifferentialAdapter',
     'HeraldDifferentialRevisionAdapter' => 'HeraldDifferentialAdapter',
     'HeraldDisableController' => 'HeraldController',
     'HeraldInvalidActionException' => 'Exception',
     'HeraldInvalidConditionException' => 'Exception',
     'HeraldManageGlobalRulesCapability' => 'PhabricatorPolicyCapability',
     'HeraldManiphestTaskAdapter' => 'HeraldAdapter',
     'HeraldNewController' => 'HeraldController',
     'HeraldPholioMockAdapter' => 'HeraldAdapter',
     'HeraldPreCommitAdapter' => 'HeraldAdapter',
     'HeraldPreCommitContentAdapter' => 'HeraldPreCommitAdapter',
     'HeraldPreCommitRefAdapter' => 'HeraldPreCommitAdapter',
     'HeraldRecursiveConditionsException' => 'Exception',
     'HeraldRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'HeraldRule' => array(
       'HeraldDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'HeraldRuleController' => 'HeraldController',
     'HeraldRuleEditor' => 'PhabricatorApplicationTransactionEditor',
     'HeraldRuleListController' => 'HeraldController',
     'HeraldRulePHIDType' => 'PhabricatorPHIDType',
     'HeraldRuleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'HeraldRuleSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'HeraldRuleTestCase' => 'PhabricatorTestCase',
     'HeraldRuleTransaction' => 'PhabricatorApplicationTransaction',
     'HeraldRuleTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'HeraldRuleViewController' => 'HeraldController',
     'HeraldSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'HeraldTestConsoleController' => 'HeraldController',
     'HeraldTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'HeraldTranscript' => array(
       'HeraldDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'HeraldTranscriptController' => 'HeraldController',
     'HeraldTranscriptGarbageCollector' => 'PhabricatorGarbageCollector',
     'HeraldTranscriptListController' => 'HeraldController',
     'HeraldTranscriptQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'HeraldTranscriptSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'HeraldTranscriptTestCase' => 'PhabricatorTestCase',
     'JavelinReactorUIExample' => 'PhabricatorUIExample',
     'JavelinUIExample' => 'PhabricatorUIExample',
     'JavelinViewExampleServerView' => 'AphrontView',
     'JavelinViewUIExample' => 'PhabricatorUIExample',
     'LegalpadController' => 'PhabricatorController',
     'LegalpadCreateDocumentsCapability' => 'PhabricatorPolicyCapability',
     'LegalpadDAO' => 'PhabricatorLiskDAO',
     'LegalpadDefaultEditCapability' => 'PhabricatorPolicyCapability',
     'LegalpadDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'LegalpadDocument' => array(
       'LegalpadDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorSubscribableInterface',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorDestructibleInterface',
     ),
     'LegalpadDocumentBody' => array(
       'LegalpadDAO',
       'PhabricatorMarkupInterface',
     ),
     'LegalpadDocumentCommentController' => 'LegalpadController',
     'LegalpadDocumentDatasource' => 'PhabricatorTypeaheadDatasource',
     'LegalpadDocumentDoneController' => 'LegalpadController',
     'LegalpadDocumentEditController' => 'LegalpadController',
     'LegalpadDocumentEditor' => 'PhabricatorApplicationTransactionEditor',
     'LegalpadDocumentListController' => 'LegalpadController',
     'LegalpadDocumentManageController' => 'LegalpadController',
     'LegalpadDocumentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'LegalpadDocumentRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'LegalpadDocumentSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'LegalpadDocumentSignController' => 'LegalpadController',
     'LegalpadDocumentSignature' => array(
       'LegalpadDAO',
       'PhabricatorPolicyInterface',
     ),
     'LegalpadDocumentSignatureAddController' => 'LegalpadController',
     'LegalpadDocumentSignatureListController' => 'LegalpadController',
     'LegalpadDocumentSignatureQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'LegalpadDocumentSignatureSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'LegalpadDocumentSignatureVerificationController' => 'LegalpadController',
     'LegalpadDocumentSignatureViewController' => 'LegalpadController',
     'LegalpadMailReceiver' => 'PhabricatorObjectMailReceiver',
     'LegalpadObjectNeedsSignatureEdgeType' => 'PhabricatorEdgeType',
     'LegalpadReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'LegalpadSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'LegalpadSignatureNeededByObjectEdgeType' => 'PhabricatorEdgeType',
     'LegalpadTransaction' => 'PhabricatorApplicationTransaction',
     'LegalpadTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'LegalpadTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'LegalpadTransactionType' => 'LegalpadConstants',
     'LegalpadTransactionView' => 'PhabricatorApplicationTransactionView',
     'LiskChunkTestCase' => 'PhabricatorTestCase',
     'LiskDAOTestCase' => 'PhabricatorTestCase',
     'LiskEphemeralObjectException' => 'Exception',
     'LiskFixtureTestCase' => 'PhabricatorTestCase',
     'LiskIsolationTestCase' => 'PhabricatorTestCase',
     'LiskIsolationTestDAO' => 'LiskDAO',
     'LiskIsolationTestDAOException' => 'Exception',
     'LiskMigrationIterator' => 'PhutilBufferedIterator',
     'LiskRawMigrationIterator' => 'PhutilBufferedIterator',
     'MacroConduitAPIMethod' => 'ConduitAPIMethod',
     'MacroCreateMemeConduitAPIMethod' => 'MacroConduitAPIMethod',
     'MacroQueryConduitAPIMethod' => 'MacroConduitAPIMethod',
     'ManiphestAssignEmailCommand' => 'ManiphestEmailCommand',
     'ManiphestAssigneeDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'ManiphestBatchEditController' => 'ManiphestController',
     'ManiphestBulkEditCapability' => 'PhabricatorPolicyCapability',
     'ManiphestClaimEmailCommand' => 'ManiphestEmailCommand',
     'ManiphestCloseEmailCommand' => 'ManiphestEmailCommand',
     'ManiphestConduitAPIMethod' => 'ConduitAPIMethod',
     'ManiphestConfiguredCustomField' => array(
       'ManiphestCustomField',
       'PhabricatorStandardCustomFieldInterface',
     ),
     'ManiphestController' => 'PhabricatorController',
     'ManiphestCreateMailReceiver' => 'PhabricatorMailReceiver',
     'ManiphestCreateTaskConduitAPIMethod' => 'ManiphestConduitAPIMethod',
     'ManiphestCustomField' => 'PhabricatorCustomField',
     'ManiphestCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
     'ManiphestCustomFieldStatusParser' => 'PhabricatorCustomFieldMonogramParser',
     'ManiphestCustomFieldStatusParserTestCase' => 'PhabricatorTestCase',
     'ManiphestCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
     'ManiphestCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
     'ManiphestDAO' => 'PhabricatorLiskDAO',
     'ManiphestDefaultEditCapability' => 'PhabricatorPolicyCapability',
     'ManiphestDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'ManiphestEditAssignCapability' => 'PhabricatorPolicyCapability',
     'ManiphestEditPoliciesCapability' => 'PhabricatorPolicyCapability',
     'ManiphestEditPriorityCapability' => 'PhabricatorPolicyCapability',
     'ManiphestEditProjectsCapability' => 'PhabricatorPolicyCapability',
     'ManiphestEditStatusCapability' => 'PhabricatorPolicyCapability',
     'ManiphestEmailCommand' => 'MetaMTAEmailTransactionCommand',
     'ManiphestExcelDefaultFormat' => 'ManiphestExcelFormat',
     'ManiphestExportController' => 'ManiphestController',
     'ManiphestGetTaskTransactionsConduitAPIMethod' => 'ManiphestConduitAPIMethod',
     'ManiphestHovercardEventListener' => 'PhabricatorEventListener',
     'ManiphestInfoConduitAPIMethod' => 'ManiphestConduitAPIMethod',
     'ManiphestNameIndex' => 'ManiphestDAO',
     'ManiphestNameIndexEventListener' => 'PhabricatorEventListener',
     'ManiphestPriorityEmailCommand' => 'ManiphestEmailCommand',
     'ManiphestQueryConduitAPIMethod' => 'ManiphestConduitAPIMethod',
     'ManiphestQueryStatusesConduitAPIMethod' => 'ManiphestConduitAPIMethod',
     'ManiphestRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'ManiphestReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'ManiphestReportController' => 'ManiphestController',
     'ManiphestSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'ManiphestSearchIndexer' => 'PhabricatorSearchDocumentIndexer',
     'ManiphestStatusConfigOptionType' => 'PhabricatorConfigJSONOptionType',
     'ManiphestStatusEmailCommand' => 'ManiphestEmailCommand',
     'ManiphestSubpriorityController' => 'ManiphestController',
     'ManiphestTask' => array(
       'ManiphestDAO',
       'PhabricatorSubscribableInterface',
       'PhabricatorMarkupInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorTokenReceiverInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorMentionableInterface',
       'PhrequentTrackableInterface',
       'PhabricatorCustomFieldInterface',
       'PhabricatorDestructibleInterface',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorProjectInterface',
     ),
     'ManiphestTaskClosedStatusDatasource' => 'PhabricatorTypeaheadDatasource',
     'ManiphestTaskDependedOnByTaskEdgeType' => 'PhabricatorEdgeType',
     'ManiphestTaskDependsOnTaskEdgeType' => 'PhabricatorEdgeType',
     'ManiphestTaskDetailController' => 'ManiphestController',
     'ManiphestTaskEditController' => 'ManiphestController',
     'ManiphestTaskHasCommitEdgeType' => 'PhabricatorEdgeType',
     'ManiphestTaskHasMockEdgeType' => 'PhabricatorEdgeType',
     'ManiphestTaskHasRevisionEdgeType' => 'PhabricatorEdgeType',
     'ManiphestTaskListController' => 'ManiphestController',
     'ManiphestTaskListView' => 'ManiphestView',
     'ManiphestTaskMailReceiver' => 'PhabricatorObjectMailReceiver',
     'ManiphestTaskOpenStatusDatasource' => 'PhabricatorTypeaheadDatasource',
     'ManiphestTaskPHIDType' => 'PhabricatorPHIDType',
     'ManiphestTaskPriority' => 'ManiphestConstants',
     'ManiphestTaskPriorityDatasource' => 'PhabricatorTypeaheadDatasource',
     'ManiphestTaskQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'ManiphestTaskResultListView' => 'ManiphestView',
     'ManiphestTaskSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'ManiphestTaskStatus' => 'ManiphestConstants',
     'ManiphestTaskStatusDatasource' => 'PhabricatorTypeaheadDatasource',
     'ManiphestTaskStatusFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'ManiphestTaskStatusTestCase' => 'PhabricatorTestCase',
     'ManiphestTaskTestCase' => 'PhabricatorTestCase',
     'ManiphestTransaction' => 'PhabricatorApplicationTransaction',
     'ManiphestTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'ManiphestTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
     'ManiphestTransactionPreviewController' => 'ManiphestController',
     'ManiphestTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'ManiphestTransactionSaveController' => 'ManiphestController',
     'ManiphestUpdateConduitAPIMethod' => 'ManiphestConduitAPIMethod',
     'ManiphestView' => 'AphrontView',
     'MetaMTAEmailTransactionCommand' => 'Phobject',
     'MetaMTAMailReceivedGarbageCollector' => 'PhabricatorGarbageCollector',
     'MetaMTAMailSentGarbageCollector' => 'PhabricatorGarbageCollector',
     'MetaMTANotificationType' => 'MetaMTAConstants',
     'MetaMTAReceivedMailStatus' => 'MetaMTAConstants',
     'MultimeterContext' => 'MultimeterDimension',
     'MultimeterController' => 'PhabricatorController',
     'MultimeterDAO' => 'PhabricatorLiskDAO',
     'MultimeterDimension' => 'MultimeterDAO',
     'MultimeterEvent' => 'MultimeterDAO',
     'MultimeterEventGarbageCollector' => 'PhabricatorGarbageCollector',
     'MultimeterHost' => 'MultimeterDimension',
     'MultimeterLabel' => 'MultimeterDimension',
     'MultimeterSampleController' => 'MultimeterController',
     'MultimeterViewer' => 'MultimeterDimension',
     'NuanceConduitAPIMethod' => 'ConduitAPIMethod',
     'NuanceController' => 'PhabricatorController',
     'NuanceCreateItemConduitAPIMethod' => 'NuanceConduitAPIMethod',
     'NuanceDAO' => 'PhabricatorLiskDAO',
     'NuanceItem' => array(
       'NuanceDAO',
       'PhabricatorPolicyInterface',
     ),
     'NuanceItemEditController' => 'NuanceController',
     'NuanceItemEditor' => 'PhabricatorApplicationTransactionEditor',
     'NuanceItemPHIDType' => 'PhabricatorPHIDType',
     'NuanceItemQuery' => 'NuanceQuery',
     'NuanceItemTransaction' => 'NuanceTransaction',
     'NuanceItemTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'NuanceItemTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'NuanceItemViewController' => 'NuanceController',
     'NuancePhabricatorFormSourceDefinition' => 'NuanceSourceDefinition',
     'NuanceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'NuanceQueue' => array(
       'NuanceDAO',
       'PhabricatorPolicyInterface',
     ),
     'NuanceQueueEditController' => 'NuanceController',
     'NuanceQueueEditor' => 'PhabricatorApplicationTransactionEditor',
     'NuanceQueueItem' => 'NuanceDAO',
     'NuanceQueuePHIDType' => 'PhabricatorPHIDType',
     'NuanceQueueQuery' => 'NuanceQuery',
     'NuanceQueueTransaction' => 'NuanceTransaction',
     'NuanceQueueTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'NuanceQueueTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'NuanceQueueViewController' => 'NuanceController',
     'NuanceRequestor' => 'NuanceDAO',
     'NuanceRequestorEditController' => 'NuanceController',
     'NuanceRequestorEditor' => 'PhabricatorApplicationTransactionEditor',
     'NuanceRequestorPHIDType' => 'PhabricatorPHIDType',
     'NuanceRequestorQuery' => 'NuanceQuery',
     'NuanceRequestorSource' => 'NuanceDAO',
     'NuanceRequestorTransaction' => 'NuanceTransaction',
     'NuanceRequestorTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'NuanceRequestorTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'NuanceRequestorViewController' => 'NuanceController',
     'NuanceSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'NuanceSource' => array(
       'NuanceDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
     ),
     'NuanceSourceDefaultEditCapability' => 'PhabricatorPolicyCapability',
     'NuanceSourceDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'NuanceSourceDefinition' => 'Phobject',
     'NuanceSourceEditController' => 'NuanceController',
     'NuanceSourceEditor' => 'PhabricatorApplicationTransactionEditor',
     'NuanceSourceManageCapability' => 'PhabricatorPolicyCapability',
     'NuanceSourcePHIDType' => 'PhabricatorPHIDType',
     'NuanceSourceQuery' => 'NuanceQuery',
     'NuanceSourceTransaction' => 'NuanceTransaction',
     'NuanceSourceTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'NuanceSourceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'NuanceSourceViewController' => 'NuanceController',
     'NuanceTransaction' => 'PhabricatorApplicationTransaction',
     'OwnersConduitAPIMethod' => 'ConduitAPIMethod',
     'OwnersPackageReplyHandler' => 'PhabricatorMailReplyHandler',
     'OwnersQueryConduitAPIMethod' => 'OwnersConduitAPIMethod',
     'PHIDConduitAPIMethod' => 'ConduitAPIMethod',
     'PHIDInfoConduitAPIMethod' => 'PHIDConduitAPIMethod',
     'PHIDLookupConduitAPIMethod' => 'PHIDConduitAPIMethod',
     'PHIDQueryConduitAPIMethod' => 'PHIDConduitAPIMethod',
     'PHUIActionHeaderExample' => 'PhabricatorUIExample',
     'PHUIActionHeaderView' => 'AphrontView',
     'PHUIActionPanelExample' => 'PhabricatorUIExample',
     'PHUIActionPanelView' => 'AphrontTagView',
     'PHUIBoxExample' => 'PhabricatorUIExample',
     'PHUIBoxView' => 'AphrontTagView',
     'PHUIButtonBarExample' => 'PhabricatorUIExample',
     'PHUIButtonBarView' => 'AphrontTagView',
     'PHUIButtonExample' => 'PhabricatorUIExample',
     'PHUIButtonView' => 'AphrontTagView',
     'PHUICalendarDayView' => 'AphrontView',
     'PHUICalendarListView' => 'AphrontTagView',
     'PHUICalendarMonthView' => 'AphrontView',
     'PHUICalendarWidgetView' => 'AphrontTagView',
     'PHUIColorPalletteExample' => 'PhabricatorUIExample',
     'PHUICrumbView' => 'AphrontView',
     'PHUICrumbsView' => 'AphrontView',
     'PHUIDiffInlineCommentDetailView' => 'PHUIDiffInlineCommentView',
     'PHUIDiffInlineCommentEditView' => 'PHUIDiffInlineCommentView',
     'PHUIDiffInlineCommentRowScaffold' => 'AphrontView',
     'PHUIDiffInlineCommentTableScaffold' => 'AphrontView',
     'PHUIDiffInlineCommentUndoView' => 'PHUIDiffInlineCommentView',
     'PHUIDiffInlineCommentView' => 'AphrontView',
     'PHUIDiffOneUpInlineCommentRowScaffold' => 'PHUIDiffInlineCommentRowScaffold',
     'PHUIDiffTwoUpInlineCommentRowScaffold' => 'PHUIDiffInlineCommentRowScaffold',
     'PHUIDocumentExample' => 'PhabricatorUIExample',
     'PHUIDocumentView' => 'AphrontTagView',
     'PHUIFeedStoryExample' => 'PhabricatorUIExample',
     'PHUIFeedStoryView' => 'AphrontView',
     'PHUIFormDividerControl' => 'AphrontFormControl',
     'PHUIFormFreeformDateControl' => 'AphrontFormControl',
     'PHUIFormInsetView' => 'AphrontView',
     'PHUIFormLayoutView' => 'AphrontView',
     'PHUIFormMultiSubmitControl' => 'AphrontFormControl',
     'PHUIFormPageView' => 'AphrontView',
     'PHUIHandleListView' => 'AphrontTagView',
     'PHUIHandleTagListView' => 'AphrontTagView',
     'PHUIHandleView' => 'AphrontView',
-    'PHUIHeaderView' => 'AphrontView',
+    'PHUIHeaderView' => 'AphrontTagView',
     'PHUIIconExample' => 'PhabricatorUIExample',
     'PHUIIconView' => 'AphrontTagView',
     'PHUIImageMaskExample' => 'PhabricatorUIExample',
     'PHUIImageMaskView' => 'AphrontTagView',
     'PHUIInfoExample' => 'PhabricatorUIExample',
     'PHUIInfoPanelExample' => 'PhabricatorUIExample',
     'PHUIInfoPanelView' => 'AphrontView',
     'PHUIInfoView' => 'AphrontView',
     'PHUIListExample' => 'PhabricatorUIExample',
     'PHUIListItemView' => 'AphrontTagView',
     'PHUIListView' => 'AphrontTagView',
     'PHUIListViewTestCase' => 'PhabricatorTestCase',
     'PHUIObjectBoxView' => 'AphrontView',
     'PHUIObjectItemListExample' => 'PhabricatorUIExample',
     'PHUIObjectItemListView' => 'AphrontTagView',
     'PHUIObjectItemView' => 'AphrontTagView',
     'PHUIPagedFormView' => 'AphrontView',
     'PHUIPinboardItemView' => 'AphrontView',
     'PHUIPinboardView' => 'AphrontView',
     'PHUIPropertyGroupView' => 'AphrontTagView',
     'PHUIPropertyListExample' => 'PhabricatorUIExample',
     'PHUIPropertyListView' => 'AphrontView',
     'PHUIRemarkupPreviewPanel' => 'AphrontTagView',
     'PHUIStatusItemView' => 'AphrontTagView',
     'PHUIStatusListView' => 'AphrontTagView',
     'PHUITagExample' => 'PhabricatorUIExample',
     'PHUITagView' => 'AphrontTagView',
     'PHUITextExample' => 'PhabricatorUIExample',
     'PHUITextView' => 'AphrontTagView',
     'PHUITimelineEventView' => 'AphrontView',
     'PHUITimelineExample' => 'PhabricatorUIExample',
     'PHUITimelineView' => 'AphrontView',
     'PHUITypeaheadExample' => 'PhabricatorUIExample',
     'PHUIWorkboardView' => 'AphrontTagView',
     'PHUIWorkpanelView' => 'AphrontTagView',
     'PackageCreateMail' => 'PackageMail',
     'PackageDeleteMail' => 'PackageMail',
     'PackageMail' => 'PhabricatorMail',
     'PackageModifyMail' => 'PackageMail',
     'PassphraseAbstractKey' => 'Phobject',
     'PassphraseConduitAPIMethod' => 'ConduitAPIMethod',
     'PassphraseController' => 'PhabricatorController',
     'PassphraseCredential' => array(
       'PassphraseDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PassphraseCredentialConduitController' => 'PassphraseController',
     'PassphraseCredentialControl' => 'AphrontFormControl',
     'PassphraseCredentialCreateController' => 'PassphraseController',
     'PassphraseCredentialDestroyController' => 'PassphraseController',
     'PassphraseCredentialEditController' => 'PassphraseController',
     'PassphraseCredentialListController' => 'PassphraseController',
     'PassphraseCredentialLockController' => 'PassphraseController',
     'PassphraseCredentialPHIDType' => 'PhabricatorPHIDType',
     'PassphraseCredentialPublicController' => 'PassphraseController',
     'PassphraseCredentialQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PassphraseCredentialRevealController' => 'PassphraseController',
     'PassphraseCredentialSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PassphraseCredentialTransaction' => 'PhabricatorApplicationTransaction',
     'PassphraseCredentialTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
     'PassphraseCredentialTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PassphraseCredentialType' => 'Phobject',
     'PassphraseCredentialTypePassword' => 'PassphraseCredentialType',
     'PassphraseCredentialTypeSSHGeneratedKey' => 'PassphraseCredentialTypeSSHPrivateKey',
     'PassphraseCredentialTypeSSHPrivateKey' => 'PassphraseCredentialType',
     'PassphraseCredentialTypeSSHPrivateKeyFile' => 'PassphraseCredentialTypeSSHPrivateKey',
     'PassphraseCredentialTypeSSHPrivateKeyText' => 'PassphraseCredentialTypeSSHPrivateKey',
     'PassphraseCredentialViewController' => 'PassphraseController',
     'PassphraseDAO' => 'PhabricatorLiskDAO',
     'PassphrasePasswordKey' => 'PassphraseAbstractKey',
     'PassphraseQueryConduitAPIMethod' => 'PassphraseConduitAPIMethod',
     'PassphraseRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'PassphraseSSHKey' => 'PassphraseAbstractKey',
     'PassphraseSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PassphraseSearchIndexer' => 'PhabricatorSearchDocumentIndexer',
     'PassphraseSecret' => 'PassphraseDAO',
     'PasteConduitAPIMethod' => 'ConduitAPIMethod',
     'PasteCreateConduitAPIMethod' => 'PasteConduitAPIMethod',
     'PasteCreateMailReceiver' => 'PhabricatorMailReceiver',
     'PasteDefaultEditCapability' => 'PhabricatorPolicyCapability',
     'PasteDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'PasteEmbedView' => 'AphrontView',
     'PasteInfoConduitAPIMethod' => 'PasteConduitAPIMethod',
     'PasteMailReceiver' => 'PhabricatorObjectMailReceiver',
     'PasteQueryConduitAPIMethod' => 'PasteConduitAPIMethod',
     'PasteReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'PeopleBrowseUserDirectoryCapability' => 'PhabricatorPolicyCapability',
     'PeopleCreateUsersCapability' => 'PhabricatorPolicyCapability',
     'PeopleUserLogGarbageCollector' => 'PhabricatorGarbageCollector',
     'Phabricator404Controller' => 'PhabricatorController',
     'PhabricatorAWSConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorAccessControlTestCase' => 'PhabricatorTestCase',
     'PhabricatorAccessLogConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorAccountSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorActionListView' => 'AphrontView',
     'PhabricatorActionView' => 'AphrontView',
     'PhabricatorActivitySettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorAdministratorsPolicyRule' => 'PhabricatorPolicyRule',
     'PhabricatorAlmanacApplication' => 'PhabricatorApplication',
     'PhabricatorAmazonAuthProvider' => 'PhabricatorOAuth2AuthProvider',
     'PhabricatorAnchorView' => 'AphrontView',
     'PhabricatorAphlictManagementDebugWorkflow' => 'PhabricatorAphlictManagementWorkflow',
     'PhabricatorAphlictManagementRestartWorkflow' => 'PhabricatorAphlictManagementWorkflow',
     'PhabricatorAphlictManagementStartWorkflow' => 'PhabricatorAphlictManagementWorkflow',
     'PhabricatorAphlictManagementStatusWorkflow' => 'PhabricatorAphlictManagementWorkflow',
     'PhabricatorAphlictManagementStopWorkflow' => 'PhabricatorAphlictManagementWorkflow',
     'PhabricatorAphlictManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorAphlictSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorAphrontBarUIExample' => 'PhabricatorUIExample',
     'PhabricatorAphrontViewTestCase' => 'PhabricatorTestCase',
     'PhabricatorAppSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorApplication' => 'PhabricatorPolicyInterface',
     'PhabricatorApplicationApplicationPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorApplicationConfigOptions' => 'Phobject',
     'PhabricatorApplicationConfigurationPanel' => 'Phobject',
     'PhabricatorApplicationDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorApplicationDetailViewController' => 'PhabricatorApplicationsController',
     'PhabricatorApplicationEditController' => 'PhabricatorApplicationsController',
     'PhabricatorApplicationEmailCommandsController' => 'PhabricatorApplicationsController',
     'PhabricatorApplicationLaunchView' => 'AphrontTagView',
     'PhabricatorApplicationPanelController' => 'PhabricatorApplicationsController',
     'PhabricatorApplicationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorApplicationSearchController' => 'PhabricatorSearchBaseController',
     'PhabricatorApplicationStatusView' => 'AphrontView',
     'PhabricatorApplicationTransaction' => array(
       'PhabricatorLiskDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PhabricatorApplicationTransactionComment' => array(
       'PhabricatorLiskDAO',
       'PhabricatorMarkupInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PhabricatorApplicationTransactionCommentEditController' => 'PhabricatorApplicationTransactionController',
     'PhabricatorApplicationTransactionCommentEditor' => 'PhabricatorEditor',
     'PhabricatorApplicationTransactionCommentHistoryController' => 'PhabricatorApplicationTransactionController',
     'PhabricatorApplicationTransactionCommentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorApplicationTransactionCommentQuoteController' => 'PhabricatorApplicationTransactionController',
     'PhabricatorApplicationTransactionCommentRawController' => 'PhabricatorApplicationTransactionController',
     'PhabricatorApplicationTransactionCommentRemoveController' => 'PhabricatorApplicationTransactionController',
     'PhabricatorApplicationTransactionCommentView' => 'AphrontView',
     'PhabricatorApplicationTransactionController' => 'PhabricatorController',
     'PhabricatorApplicationTransactionDetailController' => 'PhabricatorApplicationTransactionController',
     'PhabricatorApplicationTransactionEditor' => 'PhabricatorEditor',
     'PhabricatorApplicationTransactionFeedStory' => 'PhabricatorFeedStory',
     'PhabricatorApplicationTransactionNoEffectException' => 'Exception',
     'PhabricatorApplicationTransactionNoEffectResponse' => 'AphrontProxyResponse',
     'PhabricatorApplicationTransactionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorApplicationTransactionReplyHandler' => 'PhabricatorMailReplyHandler',
     'PhabricatorApplicationTransactionResponse' => 'AphrontProxyResponse',
     'PhabricatorApplicationTransactionShowOlderController' => 'PhabricatorApplicationTransactionController',
     'PhabricatorApplicationTransactionStructureException' => 'Exception',
     'PhabricatorApplicationTransactionTemplatedCommentQuery' => 'PhabricatorApplicationTransactionCommentQuery',
     'PhabricatorApplicationTransactionTextDiffDetailView' => 'AphrontView',
     'PhabricatorApplicationTransactionTransactionPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorApplicationTransactionValidationError' => 'Phobject',
     'PhabricatorApplicationTransactionValidationException' => 'Exception',
     'PhabricatorApplicationTransactionValidationResponse' => 'AphrontProxyResponse',
     'PhabricatorApplicationTransactionValueController' => 'PhabricatorApplicationTransactionController',
     'PhabricatorApplicationTransactionView' => 'AphrontView',
     'PhabricatorApplicationUninstallController' => 'PhabricatorApplicationsController',
     'PhabricatorApplicationsApplication' => 'PhabricatorApplication',
     'PhabricatorApplicationsController' => 'PhabricatorController',
     'PhabricatorApplicationsListController' => 'PhabricatorApplicationsController',
     'PhabricatorAsanaAuthProvider' => 'PhabricatorOAuth2AuthProvider',
     'PhabricatorAsanaConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorAsanaSubtaskHasObjectEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorAsanaTaskHasObjectEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorAuditAddCommentController' => 'PhabricatorAuditController',
     'PhabricatorAuditApplication' => 'PhabricatorApplication',
     'PhabricatorAuditCommentEditor' => 'PhabricatorEditor',
     'PhabricatorAuditController' => 'PhabricatorController',
     'PhabricatorAuditEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorAuditInlineComment' => 'PhabricatorInlineCommentInterface',
     'PhabricatorAuditListController' => 'PhabricatorAuditController',
     'PhabricatorAuditListView' => 'AphrontView',
     'PhabricatorAuditMailReceiver' => 'PhabricatorObjectMailReceiver',
     'PhabricatorAuditManagementDeleteWorkflow' => 'PhabricatorAuditManagementWorkflow',
     'PhabricatorAuditManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorAuditPreviewController' => 'PhabricatorAuditController',
     'PhabricatorAuditReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'PhabricatorAuditTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorAuditTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'PhabricatorAuditTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorAuditTransactionView' => 'PhabricatorApplicationTransactionView',
     'PhabricatorAuthAccountView' => 'AphrontView',
     'PhabricatorAuthApplication' => 'PhabricatorApplication',
     'PhabricatorAuthAuthFactorPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorAuthAuthProviderPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorAuthConduitAPIMethod' => 'ConduitAPIMethod',
     'PhabricatorAuthConfirmLinkController' => 'PhabricatorAuthController',
     'PhabricatorAuthController' => 'PhabricatorController',
     'PhabricatorAuthDAO' => 'PhabricatorLiskDAO',
     'PhabricatorAuthDisableController' => 'PhabricatorAuthProviderConfigController',
     'PhabricatorAuthDowngradeSessionController' => 'PhabricatorAuthController',
     'PhabricatorAuthEditController' => 'PhabricatorAuthProviderConfigController',
     'PhabricatorAuthFactor' => 'Phobject',
     'PhabricatorAuthFactorConfig' => 'PhabricatorAuthDAO',
     'PhabricatorAuthFinishController' => 'PhabricatorAuthController',
     'PhabricatorAuthHighSecurityRequiredException' => 'Exception',
     'PhabricatorAuthInvite' => array(
       'PhabricatorUserDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorAuthInviteAccountException' => 'PhabricatorAuthInviteDialogException',
     'PhabricatorAuthInviteAction' => 'Phobject',
     'PhabricatorAuthInviteActionTableView' => 'AphrontView',
     'PhabricatorAuthInviteController' => 'PhabricatorAuthController',
     'PhabricatorAuthInviteDialogException' => 'PhabricatorAuthInviteException',
     'PhabricatorAuthInviteEngine' => 'Phobject',
     'PhabricatorAuthInviteException' => 'Exception',
     'PhabricatorAuthInviteInvalidException' => 'PhabricatorAuthInviteDialogException',
     'PhabricatorAuthInviteLoginException' => 'PhabricatorAuthInviteDialogException',
     'PhabricatorAuthInvitePHIDType' => 'PhabricatorPHIDType',
     'PhabricatorAuthInviteQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorAuthInviteRegisteredException' => 'PhabricatorAuthInviteException',
     'PhabricatorAuthInviteSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorAuthInviteTestCase' => 'PhabricatorTestCase',
     'PhabricatorAuthInviteVerifyException' => 'PhabricatorAuthInviteDialogException',
     'PhabricatorAuthInviteWorker' => 'PhabricatorWorker',
     'PhabricatorAuthLinkController' => 'PhabricatorAuthController',
     'PhabricatorAuthListController' => 'PhabricatorAuthProviderConfigController',
     'PhabricatorAuthLoginController' => 'PhabricatorAuthController',
     'PhabricatorAuthManagementCachePKCS8Workflow' => 'PhabricatorAuthManagementWorkflow',
     'PhabricatorAuthManagementLDAPWorkflow' => 'PhabricatorAuthManagementWorkflow',
     'PhabricatorAuthManagementListFactorsWorkflow' => 'PhabricatorAuthManagementWorkflow',
     'PhabricatorAuthManagementRecoverWorkflow' => 'PhabricatorAuthManagementWorkflow',
     'PhabricatorAuthManagementRefreshWorkflow' => 'PhabricatorAuthManagementWorkflow',
     'PhabricatorAuthManagementStripWorkflow' => 'PhabricatorAuthManagementWorkflow',
     'PhabricatorAuthManagementTrustOAuthClientWorkflow' => 'PhabricatorAuthManagementWorkflow',
     'PhabricatorAuthManagementUntrustOAuthClientWorkflow' => 'PhabricatorAuthManagementWorkflow',
     'PhabricatorAuthManagementVerifyWorkflow' => 'PhabricatorAuthManagementWorkflow',
     'PhabricatorAuthManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorAuthNeedsApprovalController' => 'PhabricatorAuthController',
     'PhabricatorAuthNeedsMultiFactorController' => 'PhabricatorAuthController',
     'PhabricatorAuthNewController' => 'PhabricatorAuthProviderConfigController',
     'PhabricatorAuthOldOAuthRedirectController' => 'PhabricatorAuthController',
     'PhabricatorAuthOneTimeLoginController' => 'PhabricatorAuthController',
     'PhabricatorAuthProviderConfig' => array(
       'PhabricatorAuthDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorAuthProviderConfigController' => 'PhabricatorAuthController',
     'PhabricatorAuthProviderConfigEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorAuthProviderConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorAuthProviderConfigTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorAuthProviderConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorAuthQueryPublicKeysConduitAPIMethod' => 'PhabricatorAuthConduitAPIMethod',
     'PhabricatorAuthRegisterController' => 'PhabricatorAuthController',
     'PhabricatorAuthRevokeTokenController' => 'PhabricatorAuthController',
     'PhabricatorAuthSSHKey' => array(
       'PhabricatorAuthDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorAuthSSHKeyController' => 'PhabricatorAuthController',
     'PhabricatorAuthSSHKeyDeleteController' => 'PhabricatorAuthSSHKeyController',
     'PhabricatorAuthSSHKeyEditController' => 'PhabricatorAuthSSHKeyController',
     'PhabricatorAuthSSHKeyGenerateController' => 'PhabricatorAuthSSHKeyController',
     'PhabricatorAuthSSHKeyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorAuthSSHKeyTableView' => 'AphrontView',
     'PhabricatorAuthSSHPublicKey' => 'Phobject',
     'PhabricatorAuthSession' => array(
       'PhabricatorAuthDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorAuthSessionEngine' => 'Phobject',
     'PhabricatorAuthSessionGarbageCollector' => 'PhabricatorGarbageCollector',
     'PhabricatorAuthSessionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorAuthSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorAuthStartController' => 'PhabricatorAuthController',
     'PhabricatorAuthTemporaryToken' => array(
       'PhabricatorAuthDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorAuthTemporaryTokenGarbageCollector' => 'PhabricatorGarbageCollector',
     'PhabricatorAuthTemporaryTokenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorAuthTerminateSessionController' => 'PhabricatorAuthController',
     'PhabricatorAuthTryFactorAction' => 'PhabricatorSystemAction',
     'PhabricatorAuthUnlinkController' => 'PhabricatorAuthController',
     'PhabricatorAuthValidateController' => 'PhabricatorAuthController',
     'PhabricatorAuthenticationConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorAutoEventListener' => 'PhabricatorEventListener',
     'PhabricatorBarePageUIExample' => 'PhabricatorUIExample',
     'PhabricatorBarePageView' => 'AphrontPageView',
     'PhabricatorBaseURISetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorBcryptPasswordHasher' => 'PhabricatorPasswordHasher',
     'PhabricatorBinariesSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorBitbucketAuthProvider' => 'PhabricatorOAuth1AuthProvider',
     'PhabricatorBot' => 'PhabricatorDaemon',
     'PhabricatorBotBaseStreamingProtocolAdapter' => 'PhabricatorBaseProtocolAdapter',
     'PhabricatorBotChannel' => 'PhabricatorBotTarget',
     'PhabricatorBotDebugLogHandler' => 'PhabricatorBotHandler',
     'PhabricatorBotFeedNotificationHandler' => 'PhabricatorBotHandler',
     'PhabricatorBotFlowdockProtocolAdapter' => 'PhabricatorBotBaseStreamingProtocolAdapter',
     'PhabricatorBotLogHandler' => 'PhabricatorBotHandler',
     'PhabricatorBotMacroHandler' => 'PhabricatorBotHandler',
     'PhabricatorBotObjectNameHandler' => 'PhabricatorBotHandler',
     'PhabricatorBotSymbolHandler' => 'PhabricatorBotHandler',
     'PhabricatorBotUser' => 'PhabricatorBotTarget',
     'PhabricatorBotWhatsNewHandler' => 'PhabricatorBotHandler',
     'PhabricatorBritishEnglishTranslation' => 'PhutilTranslation',
     'PhabricatorBuiltinPatchList' => 'PhabricatorSQLPatchList',
     'PhabricatorBusyUIExample' => 'PhabricatorUIExample',
     'PhabricatorCacheDAO' => 'PhabricatorLiskDAO',
     'PhabricatorCacheGeneralGarbageCollector' => 'PhabricatorGarbageCollector',
     'PhabricatorCacheManagementPurgeWorkflow' => 'PhabricatorCacheManagementWorkflow',
     'PhabricatorCacheManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorCacheMarkupGarbageCollector' => 'PhabricatorGarbageCollector',
     'PhabricatorCacheSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhabricatorCacheSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorCacheSpec' => 'Phobject',
     'PhabricatorCacheTTLGarbageCollector' => 'PhabricatorGarbageCollector',
     'PhabricatorCalendarApplication' => 'PhabricatorApplication',
-    'PhabricatorCalendarBrowseController' => 'PhabricatorCalendarController',
     'PhabricatorCalendarController' => 'PhabricatorController',
     'PhabricatorCalendarDAO' => 'PhabricatorLiskDAO',
     'PhabricatorCalendarEvent' => array(
       'PhabricatorCalendarDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorMarkupInterface',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorSubscribableInterface',
       'PhabricatorTokenReceiverInterface',
       'PhabricatorDestructibleInterface',
       'PhabricatorMentionableInterface',
       'PhabricatorFlaggableInterface',
     ),
     'PhabricatorCalendarEventCancelController' => 'PhabricatorCalendarController',
     'PhabricatorCalendarEventCommentController' => 'PhabricatorCalendarController',
     'PhabricatorCalendarEventEditController' => 'PhabricatorCalendarController',
     'PhabricatorCalendarEventEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorCalendarEventEmailCommand' => 'MetaMTAEmailTransactionCommand',
     'PhabricatorCalendarEventInvitee' => array(
       'PhabricatorCalendarDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorCalendarEventInviteeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorCalendarEventJoinController' => 'PhabricatorCalendarController',
     'PhabricatorCalendarEventListController' => 'PhabricatorCalendarController',
     'PhabricatorCalendarEventMailReceiver' => 'PhabricatorObjectMailReceiver',
     'PhabricatorCalendarEventPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorCalendarEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorCalendarEventRSVPEmailCommand' => 'PhabricatorCalendarEventEmailCommand',
     'PhabricatorCalendarEventSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorCalendarEventSearchIndexer' => 'PhabricatorSearchDocumentIndexer',
     'PhabricatorCalendarEventTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorCalendarEventTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'PhabricatorCalendarEventTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorCalendarEventViewController' => 'PhabricatorCalendarController',
     'PhabricatorCalendarHoliday' => 'PhabricatorCalendarDAO',
     'PhabricatorCalendarHolidayTestCase' => 'PhabricatorTestCase',
     'PhabricatorCalendarRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'PhabricatorCalendarReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'PhabricatorCalendarSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhabricatorCampfireProtocolAdapter' => 'PhabricatorBotBaseStreamingProtocolAdapter',
     'PhabricatorCelerityApplication' => 'PhabricatorApplication',
     'PhabricatorCelerityTestCase' => 'PhabricatorTestCase',
     'PhabricatorChangeParserTestCase' => 'PhabricatorWorkingCopyTestCase',
     'PhabricatorChangesetResponse' => 'AphrontProxyResponse',
     'PhabricatorChatLogApplication' => 'PhabricatorApplication',
     'PhabricatorChatLogChannel' => array(
       'PhabricatorChatLogDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorChatLogChannelListController' => 'PhabricatorChatLogController',
     'PhabricatorChatLogChannelLogController' => 'PhabricatorChatLogController',
     'PhabricatorChatLogChannelQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorChatLogController' => 'PhabricatorController',
     'PhabricatorChatLogDAO' => 'PhabricatorLiskDAO',
     'PhabricatorChatLogEvent' => array(
       'PhabricatorChatLogDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorChatLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorChunkedFileStorageEngine' => 'PhabricatorFileStorageEngine',
     'PhabricatorClusterConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorCommitBranchesField' => 'PhabricatorCommitCustomField',
     'PhabricatorCommitCustomField' => 'PhabricatorCustomField',
     'PhabricatorCommitSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorCommitTagsField' => 'PhabricatorCommitCustomField',
     'PhabricatorCommonPasswords' => 'Phobject',
     'PhabricatorConduitAPIController' => 'PhabricatorConduitController',
     'PhabricatorConduitApplication' => 'PhabricatorApplication',
     'PhabricatorConduitCertificateSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorConduitCertificateToken' => 'PhabricatorConduitDAO',
     'PhabricatorConduitConnectionLog' => 'PhabricatorConduitDAO',
     'PhabricatorConduitConsoleController' => 'PhabricatorConduitController',
     'PhabricatorConduitController' => 'PhabricatorController',
     'PhabricatorConduitDAO' => 'PhabricatorLiskDAO',
     'PhabricatorConduitListController' => 'PhabricatorConduitController',
     'PhabricatorConduitLogController' => 'PhabricatorConduitController',
     'PhabricatorConduitLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorConduitMethodCallLog' => array(
       'PhabricatorConduitDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorConduitMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorConduitSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorConduitTestCase' => 'PhabricatorTestCase',
     'PhabricatorConduitToken' => array(
       'PhabricatorConduitDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorConduitTokenController' => 'PhabricatorConduitController',
     'PhabricatorConduitTokenEditController' => 'PhabricatorConduitController',
     'PhabricatorConduitTokenHandshakeController' => 'PhabricatorConduitController',
     'PhabricatorConduitTokenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorConduitTokenTerminateController' => 'PhabricatorConduitController',
     'PhabricatorConduitTokensSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorConfigAllController' => 'PhabricatorConfigController',
     'PhabricatorConfigApplication' => 'PhabricatorApplication',
     'PhabricatorConfigCacheController' => 'PhabricatorConfigController',
     'PhabricatorConfigColumnSchema' => 'PhabricatorConfigStorageSchema',
     'PhabricatorConfigConfigPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorConfigController' => 'PhabricatorController',
     'PhabricatorConfigCoreSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhabricatorConfigDatabaseController' => 'PhabricatorConfigController',
     'PhabricatorConfigDatabaseIssueController' => 'PhabricatorConfigDatabaseController',
     'PhabricatorConfigDatabaseSchema' => 'PhabricatorConfigStorageSchema',
     'PhabricatorConfigDatabaseSource' => 'PhabricatorConfigProxySource',
     'PhabricatorConfigDatabaseStatusController' => 'PhabricatorConfigDatabaseController',
     'PhabricatorConfigDefaultSource' => 'PhabricatorConfigProxySource',
     'PhabricatorConfigDictionarySource' => 'PhabricatorConfigSource',
     'PhabricatorConfigEditController' => 'PhabricatorConfigController',
     'PhabricatorConfigEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorConfigEntry' => array(
       'PhabricatorConfigEntryDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorConfigEntryDAO' => 'PhabricatorLiskDAO',
     'PhabricatorConfigEntryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorConfigFileSource' => 'PhabricatorConfigProxySource',
     'PhabricatorConfigGroupController' => 'PhabricatorConfigController',
     'PhabricatorConfigHistoryController' => 'PhabricatorConfigController',
     'PhabricatorConfigIgnoreController' => 'PhabricatorConfigController',
     'PhabricatorConfigIssueListController' => 'PhabricatorConfigController',
     'PhabricatorConfigIssueViewController' => 'PhabricatorConfigController',
     'PhabricatorConfigJSONOptionType' => 'PhabricatorConfigOptionType',
     'PhabricatorConfigKeySchema' => 'PhabricatorConfigStorageSchema',
     'PhabricatorConfigListController' => 'PhabricatorConfigController',
     'PhabricatorConfigLocalSource' => 'PhabricatorConfigProxySource',
     'PhabricatorConfigManagementDeleteWorkflow' => 'PhabricatorConfigManagementWorkflow',
     'PhabricatorConfigManagementGetWorkflow' => 'PhabricatorConfigManagementWorkflow',
     'PhabricatorConfigManagementListWorkflow' => 'PhabricatorConfigManagementWorkflow',
     'PhabricatorConfigManagementMigrateWorkflow' => 'PhabricatorConfigManagementWorkflow',
     'PhabricatorConfigManagementSetWorkflow' => 'PhabricatorConfigManagementWorkflow',
     'PhabricatorConfigManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorConfigOption' => array(
       'Phobject',
       'PhabricatorMarkupInterface',
     ),
     'PhabricatorConfigProxySource' => 'PhabricatorConfigSource',
     'PhabricatorConfigResponse' => 'AphrontStandaloneHTMLResponse',
     'PhabricatorConfigSchemaQuery' => 'Phobject',
     'PhabricatorConfigSchemaSpec' => 'Phobject',
     'PhabricatorConfigServerSchema' => 'PhabricatorConfigStorageSchema',
     'PhabricatorConfigSiteSource' => 'PhabricatorConfigProxySource',
     'PhabricatorConfigStackSource' => 'PhabricatorConfigSource',
     'PhabricatorConfigStorageSchema' => 'Phobject',
     'PhabricatorConfigTableSchema' => 'PhabricatorConfigStorageSchema',
     'PhabricatorConfigTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorConfigValidationException' => 'Exception',
     'PhabricatorConfigWelcomeController' => 'PhabricatorConfigController',
     'PhabricatorConpherenceApplication' => 'PhabricatorApplication',
     'PhabricatorConpherencePreferencesSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorConpherenceThreadPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorConsoleApplication' => 'PhabricatorApplication',
     'PhabricatorContentSourceView' => 'AphrontView',
     'PhabricatorContributedToObjectEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorController' => 'AphrontController',
     'PhabricatorCookies' => 'Phobject',
     'PhabricatorCoreConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorCountdown' => array(
       'PhabricatorCountdownDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorCountdownApplication' => 'PhabricatorApplication',
     'PhabricatorCountdownController' => 'PhabricatorController',
     'PhabricatorCountdownCountdownPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorCountdownDAO' => 'PhabricatorLiskDAO',
     'PhabricatorCountdownDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'PhabricatorCountdownDeleteController' => 'PhabricatorCountdownController',
     'PhabricatorCountdownEditController' => 'PhabricatorCountdownController',
     'PhabricatorCountdownListController' => 'PhabricatorCountdownController',
     'PhabricatorCountdownQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorCountdownRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'PhabricatorCountdownSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorCountdownView' => 'AphrontTagView',
     'PhabricatorCountdownViewController' => 'PhabricatorCountdownController',
     'PhabricatorCredentialsUsedByObjectEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorCursorPagedPolicyAwareQuery' => 'PhabricatorPolicyAwareQuery',
     'PhabricatorCustomFieldConfigOptionType' => 'PhabricatorConfigOptionType',
     'PhabricatorCustomFieldDataNotAvailableException' => 'Exception',
     'PhabricatorCustomFieldImplementationIncompleteException' => 'Exception',
     'PhabricatorCustomFieldIndexStorage' => 'PhabricatorLiskDAO',
     'PhabricatorCustomFieldList' => 'Phobject',
     'PhabricatorCustomFieldMonogramParser' => 'Phobject',
     'PhabricatorCustomFieldNotAttachedException' => 'Exception',
     'PhabricatorCustomFieldNotProxyException' => 'Exception',
     'PhabricatorCustomFieldNumericIndexStorage' => 'PhabricatorCustomFieldIndexStorage',
     'PhabricatorCustomFieldStorage' => 'PhabricatorLiskDAO',
     'PhabricatorCustomFieldStringIndexStorage' => 'PhabricatorCustomFieldIndexStorage',
     'PhabricatorCustomHeaderConfigType' => 'PhabricatorConfigOptionType',
     'PhabricatorDaemon' => 'PhutilDaemon',
     'PhabricatorDaemonConsoleController' => 'PhabricatorDaemonController',
     'PhabricatorDaemonController' => 'PhabricatorController',
     'PhabricatorDaemonDAO' => 'PhabricatorLiskDAO',
     'PhabricatorDaemonEventListener' => 'PhabricatorEventListener',
     'PhabricatorDaemonLog' => array(
       'PhabricatorDaemonDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorDaemonLogEvent' => 'PhabricatorDaemonDAO',
     'PhabricatorDaemonLogEventGarbageCollector' => 'PhabricatorGarbageCollector',
     'PhabricatorDaemonLogEventViewController' => 'PhabricatorDaemonController',
     'PhabricatorDaemonLogEventsView' => 'AphrontView',
     'PhabricatorDaemonLogGarbageCollector' => 'PhabricatorGarbageCollector',
     'PhabricatorDaemonLogListController' => 'PhabricatorDaemonController',
     'PhabricatorDaemonLogListView' => 'AphrontView',
     'PhabricatorDaemonLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorDaemonLogViewController' => 'PhabricatorDaemonController',
     'PhabricatorDaemonManagementDebugWorkflow' => 'PhabricatorDaemonManagementWorkflow',
     'PhabricatorDaemonManagementLaunchWorkflow' => 'PhabricatorDaemonManagementWorkflow',
     'PhabricatorDaemonManagementListWorkflow' => 'PhabricatorDaemonManagementWorkflow',
     'PhabricatorDaemonManagementLogWorkflow' => 'PhabricatorDaemonManagementWorkflow',
     'PhabricatorDaemonManagementReloadWorkflow' => 'PhabricatorDaemonManagementWorkflow',
     'PhabricatorDaemonManagementRestartWorkflow' => 'PhabricatorDaemonManagementWorkflow',
     'PhabricatorDaemonManagementStartWorkflow' => 'PhabricatorDaemonManagementWorkflow',
     'PhabricatorDaemonManagementStatusWorkflow' => 'PhabricatorDaemonManagementWorkflow',
     'PhabricatorDaemonManagementStopWorkflow' => 'PhabricatorDaemonManagementWorkflow',
     'PhabricatorDaemonManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorDaemonTaskGarbageCollector' => 'PhabricatorGarbageCollector',
     'PhabricatorDaemonTasksTableView' => 'AphrontView',
     'PhabricatorDaemonsApplication' => 'PhabricatorApplication',
     'PhabricatorDaemonsSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorDailyRoutineTriggerClock' => 'PhabricatorTriggerClock',
     'PhabricatorDashboard' => array(
       'PhabricatorDashboardDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PhabricatorDashboardAddPanelController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardApplication' => 'PhabricatorApplication',
     'PhabricatorDashboardController' => 'PhabricatorController',
     'PhabricatorDashboardCopyController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardDAO' => 'PhabricatorLiskDAO',
     'PhabricatorDashboardDashboardHasPanelEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorDashboardDashboardPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorDashboardEditController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardHistoryController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardInstall' => 'PhabricatorDashboardDAO',
     'PhabricatorDashboardInstallController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardListController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardManageController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardMovePanelController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardPanel' => array(
       'PhabricatorDashboardDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorCustomFieldInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PhabricatorDashboardPanelArchiveController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardPanelCoreCustomField' => array(
       'PhabricatorDashboardPanelCustomField',
       'PhabricatorStandardCustomFieldInterface',
     ),
     'PhabricatorDashboardPanelCustomField' => 'PhabricatorCustomField',
     'PhabricatorDashboardPanelEditController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardPanelHasDashboardEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorDashboardPanelListController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardPanelPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorDashboardPanelQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorDashboardPanelRenderController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardPanelRenderingEngine' => 'Phobject',
     'PhabricatorDashboardPanelSearchApplicationCustomField' => 'PhabricatorStandardCustomField',
     'PhabricatorDashboardPanelSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorDashboardPanelSearchQueryCustomField' => 'PhabricatorStandardCustomField',
     'PhabricatorDashboardPanelTabsCustomField' => 'PhabricatorStandardCustomField',
     'PhabricatorDashboardPanelTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorDashboardPanelTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorDashboardPanelTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorDashboardPanelType' => 'Phobject',
     'PhabricatorDashboardPanelViewController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorDashboardQueryPanelType' => 'PhabricatorDashboardPanelType',
     'PhabricatorDashboardRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'PhabricatorDashboardRemovePanelController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardRenderingEngine' => 'Phobject',
     'PhabricatorDashboardSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhabricatorDashboardSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorDashboardTabsPanelType' => 'PhabricatorDashboardPanelType',
     'PhabricatorDashboardTextPanelType' => 'PhabricatorDashboardPanelType',
     'PhabricatorDashboardTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorDashboardTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorDashboardTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorDashboardUninstallController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardViewController' => 'PhabricatorDashboardController',
     'PhabricatorDataCacheSpec' => 'PhabricatorCacheSpec',
     'PhabricatorDataNotAttachedException' => 'Exception',
     'PhabricatorDatabaseSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorDebugController' => 'PhabricatorController',
     'PhabricatorDefaultSearchEngineSelector' => 'PhabricatorSearchEngineSelector',
     'PhabricatorDestructionEngine' => 'Phobject',
     'PhabricatorDeveloperConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorDeveloperPreferencesSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorDiffInlineCommentQuery' => 'PhabricatorApplicationTransactionCommentQuery',
     'PhabricatorDiffPreferencesSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorDifferentialApplication' => 'PhabricatorApplication',
     'PhabricatorDifferentialConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorDifferentialRevisionTestDataGenerator' => 'PhabricatorTestDataGenerator',
     'PhabricatorDiffusionApplication' => 'PhabricatorApplication',
     'PhabricatorDiffusionConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorDisabledUserController' => 'PhabricatorAuthController',
     'PhabricatorDisplayPreferencesSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorDisqusAuthProvider' => 'PhabricatorOAuth2AuthProvider',
     'PhabricatorDisqusConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorDivinerApplication' => 'PhabricatorApplication',
     'PhabricatorDoorkeeperApplication' => 'PhabricatorApplication',
     'PhabricatorDraft' => 'PhabricatorDraftDAO',
     'PhabricatorDraftDAO' => 'PhabricatorLiskDAO',
     'PhabricatorDrydockApplication' => 'PhabricatorApplication',
     'PhabricatorEdgeConfig' => 'PhabricatorEdgeConstants',
     'PhabricatorEdgeCycleException' => 'Exception',
     'PhabricatorEdgeEditor' => 'Phobject',
     'PhabricatorEdgeGraph' => 'AbstractDirectedGraph',
     'PhabricatorEdgeQuery' => 'PhabricatorQuery',
     'PhabricatorEdgeTestCase' => 'PhabricatorTestCase',
     'PhabricatorEdgeType' => 'Phobject',
     'PhabricatorEditor' => 'Phobject',
     'PhabricatorElasticSearchEngine' => 'PhabricatorSearchEngine',
     'PhabricatorElasticSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorEmailAddressesSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorEmailFormatSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorEmailLoginController' => 'PhabricatorAuthController',
     'PhabricatorEmailPreferencesSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorEmailVerificationController' => 'PhabricatorAuthController',
     'PhabricatorEmbedFileRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'PhabricatorEmojiRemarkupRule' => 'PhutilRemarkupRule',
     'PhabricatorEmptyQueryException' => 'Exception',
     'PhabricatorEnvTestCase' => 'PhabricatorTestCase',
     'PhabricatorEvent' => 'PhutilEvent',
     'PhabricatorEventListener' => 'PhutilEventListener',
     'PhabricatorEventType' => 'PhutilEventType',
     'PhabricatorExampleEventListener' => 'PhabricatorEventListener',
     'PhabricatorExtendingPhabricatorConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorExtensionsSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorExternalAccount' => array(
       'PhabricatorUserDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorExternalAccountQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorExternalAccountsSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorExtraConfigSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorFacebookAuthProvider' => 'PhabricatorOAuth2AuthProvider',
     'PhabricatorFactAggregate' => 'PhabricatorFactDAO',
     'PhabricatorFactApplication' => 'PhabricatorApplication',
     'PhabricatorFactChartController' => 'PhabricatorFactController',
     'PhabricatorFactController' => 'PhabricatorController',
     'PhabricatorFactCountEngine' => 'PhabricatorFactEngine',
     'PhabricatorFactCursor' => 'PhabricatorFactDAO',
     'PhabricatorFactDAO' => 'PhabricatorLiskDAO',
     'PhabricatorFactDaemon' => 'PhabricatorDaemon',
     'PhabricatorFactHomeController' => 'PhabricatorFactController',
     'PhabricatorFactLastUpdatedEngine' => 'PhabricatorFactEngine',
     'PhabricatorFactManagementAnalyzeWorkflow' => 'PhabricatorFactManagementWorkflow',
     'PhabricatorFactManagementCursorsWorkflow' => 'PhabricatorFactManagementWorkflow',
     'PhabricatorFactManagementDestroyWorkflow' => 'PhabricatorFactManagementWorkflow',
     'PhabricatorFactManagementListWorkflow' => 'PhabricatorFactManagementWorkflow',
     'PhabricatorFactManagementStatusWorkflow' => 'PhabricatorFactManagementWorkflow',
     'PhabricatorFactManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorFactRaw' => 'PhabricatorFactDAO',
     'PhabricatorFactSimpleSpec' => 'PhabricatorFactSpec',
     'PhabricatorFactUpdateIterator' => 'PhutilBufferedIterator',
     'PhabricatorFeedApplication' => 'PhabricatorApplication',
     'PhabricatorFeedConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorFeedController' => 'PhabricatorController',
     'PhabricatorFeedDAO' => 'PhabricatorLiskDAO',
     'PhabricatorFeedDetailController' => 'PhabricatorFeedController',
     'PhabricatorFeedListController' => 'PhabricatorFeedController',
     'PhabricatorFeedManagementRepublishWorkflow' => 'PhabricatorFeedManagementWorkflow',
     'PhabricatorFeedManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorFeedPublicStreamController' => 'PhabricatorFeedController',
     'PhabricatorFeedQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorFeedSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorFeedStory' => array(
       'PhabricatorPolicyInterface',
       'PhabricatorMarkupInterface',
     ),
     'PhabricatorFeedStoryAggregate' => 'PhabricatorFeedStory',
     'PhabricatorFeedStoryAudit' => 'PhabricatorFeedStory',
     'PhabricatorFeedStoryCommit' => 'PhabricatorFeedStory',
     'PhabricatorFeedStoryData' => 'PhabricatorFeedDAO',
     'PhabricatorFeedStoryDifferential' => 'PhabricatorFeedStory',
     'PhabricatorFeedStoryDifferentialAggregate' => 'PhabricatorFeedStoryAggregate',
     'PhabricatorFeedStoryManiphestAggregate' => 'PhabricatorFeedStoryAggregate',
     'PhabricatorFeedStoryNotification' => 'PhabricatorFeedDAO',
     'PhabricatorFeedStoryPhriction' => 'PhabricatorFeedStory',
     'PhabricatorFeedStoryReference' => 'PhabricatorFeedDAO',
     'PhabricatorFile' => array(
       'PhabricatorFileDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorTokenReceiverInterface',
       'PhabricatorSubscribableInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PhabricatorFileChunk' => array(
       'PhabricatorFileDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PhabricatorFileChunkIterator' => array(
       'Phobject',
       'Iterator',
     ),
     'PhabricatorFileChunkQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorFileCommentController' => 'PhabricatorFileController',
     'PhabricatorFileComposeController' => 'PhabricatorFileController',
     'PhabricatorFileController' => 'PhabricatorController',
     'PhabricatorFileDAO' => 'PhabricatorLiskDAO',
     'PhabricatorFileDataController' => 'PhabricatorFileController',
     'PhabricatorFileDeleteController' => 'PhabricatorFileController',
     'PhabricatorFileDropUploadController' => 'PhabricatorFileController',
     'PhabricatorFileEditController' => 'PhabricatorFileController',
     'PhabricatorFileEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorFileFilePHIDType' => 'PhabricatorPHIDType',
     'PhabricatorFileHasObjectEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorFileImageMacro' => array(
       'PhabricatorFileDAO',
       'PhabricatorSubscribableInterface',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorPolicyInterface',
     ),
+    'PhabricatorFileImageTransform' => 'PhabricatorFileTransform',
     'PhabricatorFileInfoController' => 'PhabricatorFileController',
     'PhabricatorFileLinkListView' => 'AphrontView',
     'PhabricatorFileLinkView' => 'AphrontView',
     'PhabricatorFileListController' => 'PhabricatorFileController',
     'PhabricatorFileQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorFileSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhabricatorFileSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorFileStorageBlob' => 'PhabricatorFileDAO',
     'PhabricatorFileStorageConfigurationException' => 'Exception',
     'PhabricatorFileTemporaryGarbageCollector' => 'PhabricatorGarbageCollector',
     'PhabricatorFileTestCase' => 'PhabricatorTestCase',
     'PhabricatorFileTestDataGenerator' => 'PhabricatorTestDataGenerator',
+    'PhabricatorFileThumbnailTransform' => 'PhabricatorFileImageTransform',
     'PhabricatorFileTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorFileTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'PhabricatorFileTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+    'PhabricatorFileTransform' => 'Phobject',
     'PhabricatorFileTransformController' => 'PhabricatorFileController',
+    'PhabricatorFileTransformListController' => 'PhabricatorFileController',
     'PhabricatorFileUploadController' => 'PhabricatorFileController',
     'PhabricatorFileUploadDialogController' => 'PhabricatorFileController',
     'PhabricatorFileUploadException' => 'Exception',
     'PhabricatorFileinfoSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorFilesApplication' => 'PhabricatorApplication',
     'PhabricatorFilesApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel',
     'PhabricatorFilesConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorFilesManagementCatWorkflow' => 'PhabricatorFilesManagementWorkflow',
     'PhabricatorFilesManagementCompactWorkflow' => 'PhabricatorFilesManagementWorkflow',
     'PhabricatorFilesManagementEnginesWorkflow' => 'PhabricatorFilesManagementWorkflow',
     'PhabricatorFilesManagementMigrateWorkflow' => 'PhabricatorFilesManagementWorkflow',
     'PhabricatorFilesManagementPurgeWorkflow' => 'PhabricatorFilesManagementWorkflow',
     'PhabricatorFilesManagementRebuildWorkflow' => 'PhabricatorFilesManagementWorkflow',
     'PhabricatorFilesManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorFilesOutboundRequestAction' => 'PhabricatorSystemAction',
     'PhabricatorFlag' => array(
       'PhabricatorFlagDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorFlagColor' => 'PhabricatorFlagConstants',
     'PhabricatorFlagController' => 'PhabricatorController',
     'PhabricatorFlagDAO' => 'PhabricatorLiskDAO',
     'PhabricatorFlagDeleteController' => 'PhabricatorFlagController',
     'PhabricatorFlagEditController' => 'PhabricatorFlagController',
     'PhabricatorFlagListController' => 'PhabricatorFlagController',
     'PhabricatorFlagQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorFlagSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorFlagSelectControl' => 'AphrontFormControl',
     'PhabricatorFlaggableInterface' => 'PhabricatorPHIDInterface',
     'PhabricatorFlagsApplication' => 'PhabricatorApplication',
     'PhabricatorFlagsUIEventListener' => 'PhabricatorEventListener',
     'PhabricatorFundApplication' => 'PhabricatorApplication',
     'PhabricatorGDSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorGarbageCollector' => 'Phobject',
     'PhabricatorGarbageCollectorConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorGestureUIExample' => 'PhabricatorUIExample',
     'PhabricatorGitGraphStream' => 'PhabricatorRepositoryGraphStream',
     'PhabricatorGitHubAuthProvider' => 'PhabricatorOAuth2AuthProvider',
     'PhabricatorGlobalLock' => 'PhutilLock',
     'PhabricatorGlobalUploadTargetView' => 'AphrontView',
     'PhabricatorGoogleAuthProvider' => 'PhabricatorOAuth2AuthProvider',
     'PhabricatorHandleList' => array(
       'Phobject',
       'Iterator',
       'ArrayAccess',
       'Countable',
     ),
     'PhabricatorHandlePool' => 'Phobject',
     'PhabricatorHandlePoolTestCase' => 'PhabricatorTestCase',
     'PhabricatorHandleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorHarbormasterApplication' => 'PhabricatorApplication',
     'PhabricatorHarbormasterConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorHash' => 'Phobject',
     'PhabricatorHashTestCase' => 'PhabricatorTestCase',
     'PhabricatorHelpApplication' => 'PhabricatorApplication',
     'PhabricatorHelpController' => 'PhabricatorController',
     'PhabricatorHelpDocumentationController' => 'PhabricatorHelpController',
     'PhabricatorHelpEditorProtocolController' => 'PhabricatorHelpController',
     'PhabricatorHelpKeyboardShortcutController' => 'PhabricatorHelpController',
     'PhabricatorHeraldApplication' => 'PhabricatorApplication',
     'PhabricatorHomeApplication' => 'PhabricatorApplication',
     'PhabricatorHomeController' => 'PhabricatorController',
     'PhabricatorHomeMainController' => 'PhabricatorHomeController',
     'PhabricatorHomePreferencesSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorHomeQuickCreateController' => 'PhabricatorHomeController',
     'PhabricatorHovercardUIExample' => 'PhabricatorUIExample',
     'PhabricatorHovercardView' => 'AphrontView',
     'PhabricatorHunksManagementMigrateWorkflow' => 'PhabricatorHunksManagementWorkflow',
     'PhabricatorHunksManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorIRCProtocolAdapter' => 'PhabricatorBaseProtocolAdapter',
     'PhabricatorIconRemarkupRule' => 'PhutilRemarkupRule',
     'PhabricatorImageMacroRemarkupRule' => 'PhutilRemarkupRule',
     'PhabricatorImagemagickSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorInfrastructureTestCase' => 'PhabricatorTestCase',
     'PhabricatorInlineCommentController' => 'PhabricatorController',
     'PhabricatorInlineCommentInterface' => 'PhabricatorMarkupInterface',
     'PhabricatorInlineCommentPreviewController' => 'PhabricatorController',
     'PhabricatorInlineSummaryView' => 'AphrontView',
     'PhabricatorInternationalizationManagementExtractWorkflow' => 'PhabricatorInternationalizationManagementWorkflow',
     'PhabricatorInternationalizationManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorInvalidConfigSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorIteratedMD5PasswordHasher' => 'PhabricatorPasswordHasher',
     'PhabricatorJIRAAuthProvider' => 'PhabricatorOAuth1AuthProvider',
     'PhabricatorJavelinLinter' => 'ArcanistLinter',
     'PhabricatorJiraIssueHasObjectEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorKeyValueDatabaseCache' => 'PhutilKeyValueCache',
     'PhabricatorLDAPAuthProvider' => 'PhabricatorAuthProvider',
     'PhabricatorLegalpadApplication' => 'PhabricatorApplication',
     'PhabricatorLegalpadConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorLegalpadDocumentPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorLegalpadSignaturePolicyRule' => 'PhabricatorPolicyRule',
     'PhabricatorLibraryTestCase' => 'PhutilLibraryTestCase',
     'PhabricatorLipsumGenerateWorkflow' => 'PhabricatorLipsumManagementWorkflow',
     'PhabricatorLipsumManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorLipsumMondrianArtist' => 'PhabricatorLipsumArtist',
     'PhabricatorLiskDAO' => 'LiskDAO',
     'PhabricatorListFilterUIExample' => 'PhabricatorUIExample',
     'PhabricatorLocalDiskFileStorageEngine' => 'PhabricatorFileStorageEngine',
     'PhabricatorLocalTimeTestCase' => 'PhabricatorTestCase',
     'PhabricatorLogTriggerAction' => 'PhabricatorTriggerAction',
     'PhabricatorLogoutController' => 'PhabricatorAuthController',
     'PhabricatorLunarPhasePolicyRule' => 'PhabricatorPolicyRule',
     'PhabricatorMacroApplication' => 'PhabricatorApplication',
     'PhabricatorMacroAudioController' => 'PhabricatorMacroController',
     'PhabricatorMacroCommentController' => 'PhabricatorMacroController',
     'PhabricatorMacroConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorMacroController' => 'PhabricatorController',
     'PhabricatorMacroDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorMacroDisableController' => 'PhabricatorMacroController',
     'PhabricatorMacroEditController' => 'PhabricatorMacroController',
     'PhabricatorMacroEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorMacroListController' => 'PhabricatorMacroController',
     'PhabricatorMacroMacroPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorMacroMailReceiver' => 'PhabricatorObjectMailReceiver',
     'PhabricatorMacroManageCapability' => 'PhabricatorPolicyCapability',
     'PhabricatorMacroMemeController' => 'PhabricatorMacroController',
     'PhabricatorMacroMemeDialogController' => 'PhabricatorMacroController',
     'PhabricatorMacroQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorMacroReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'PhabricatorMacroSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorMacroTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorMacroTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'PhabricatorMacroTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorMacroViewController' => 'PhabricatorMacroController',
     'PhabricatorMailImplementationAmazonSESAdapter' => 'PhabricatorMailImplementationPHPMailerLiteAdapter',
     'PhabricatorMailImplementationMailgunAdapter' => 'PhabricatorMailImplementationAdapter',
     'PhabricatorMailImplementationPHPMailerAdapter' => 'PhabricatorMailImplementationAdapter',
     'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'PhabricatorMailImplementationAdapter',
     'PhabricatorMailImplementationSendGridAdapter' => 'PhabricatorMailImplementationAdapter',
     'PhabricatorMailImplementationTestAdapter' => 'PhabricatorMailImplementationAdapter',
     'PhabricatorMailManagementListInboundWorkflow' => 'PhabricatorMailManagementWorkflow',
     'PhabricatorMailManagementListOutboundWorkflow' => 'PhabricatorMailManagementWorkflow',
     'PhabricatorMailManagementReceiveTestWorkflow' => 'PhabricatorMailManagementWorkflow',
     'PhabricatorMailManagementResendWorkflow' => 'PhabricatorMailManagementWorkflow',
     'PhabricatorMailManagementSendTestWorkflow' => 'PhabricatorMailManagementWorkflow',
     'PhabricatorMailManagementShowInboundWorkflow' => 'PhabricatorMailManagementWorkflow',
     'PhabricatorMailManagementShowOutboundWorkflow' => 'PhabricatorMailManagementWorkflow',
     'PhabricatorMailManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorMailReceiverTestCase' => 'PhabricatorTestCase',
     'PhabricatorMailSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorMailgunConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorMailingListDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorMailingListListPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorMailingListQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorMailingListSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorMailingListsApplication' => 'PhabricatorApplication',
     'PhabricatorMailingListsController' => 'PhabricatorController',
     'PhabricatorMailingListsEditController' => 'PhabricatorMailingListsController',
     'PhabricatorMailingListsListController' => 'PhabricatorMailingListsController',
     'PhabricatorMailingListsManageCapability' => 'PhabricatorPolicyCapability',
     'PhabricatorMainMenuSearchView' => 'AphrontView',
     'PhabricatorMainMenuView' => 'AphrontView',
     'PhabricatorManagementWorkflow' => 'PhutilArgumentWorkflow',
     'PhabricatorManiphestApplication' => 'PhabricatorApplication',
     'PhabricatorManiphestConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorManiphestTaskTestDataGenerator' => 'PhabricatorTestDataGenerator',
     'PhabricatorMarkupCache' => 'PhabricatorCacheDAO',
     'PhabricatorMarkupOneOff' => 'PhabricatorMarkupInterface',
     'PhabricatorMarkupPreviewController' => 'PhabricatorController',
     'PhabricatorMemeRemarkupRule' => 'PhutilRemarkupRule',
     'PhabricatorMentionRemarkupRule' => 'PhutilRemarkupRule',
     'PhabricatorMercurialGraphStream' => 'PhabricatorRepositoryGraphStream',
     'PhabricatorMetaMTAActorQuery' => 'PhabricatorQuery',
     'PhabricatorMetaMTAApplication' => 'PhabricatorApplication',
     'PhabricatorMetaMTAApplicationEmail' => array(
       'PhabricatorMetaMTADAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorMetaMTAApplicationEmailDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorMetaMTAApplicationEmailPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorMetaMTAApplicationEmailPanel' => 'PhabricatorApplicationConfigurationPanel',
     'PhabricatorMetaMTAApplicationEmailQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorMetaMTAConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorMetaMTAController' => 'PhabricatorController',
     'PhabricatorMetaMTADAO' => 'PhabricatorLiskDAO',
     'PhabricatorMetaMTAEmailBodyParserTestCase' => 'PhabricatorTestCase',
     'PhabricatorMetaMTAErrorMailAction' => 'PhabricatorSystemAction',
     'PhabricatorMetaMTAMail' => 'PhabricatorMetaMTADAO',
     'PhabricatorMetaMTAMailBodyTestCase' => 'PhabricatorTestCase',
     'PhabricatorMetaMTAMailTestCase' => 'PhabricatorTestCase',
     'PhabricatorMetaMTAMailableDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'PhabricatorMetaMTAMailableFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'PhabricatorMetaMTAMailgunReceiveController' => 'PhabricatorMetaMTAController',
     'PhabricatorMetaMTAMailingList' => array(
       'PhabricatorMetaMTADAO',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PhabricatorMetaMTAMemberQuery' => 'PhabricatorQuery',
     'PhabricatorMetaMTAPermanentFailureException' => 'Exception',
     'PhabricatorMetaMTAReceivedMail' => 'PhabricatorMetaMTADAO',
     'PhabricatorMetaMTAReceivedMailProcessingException' => 'Exception',
     'PhabricatorMetaMTAReceivedMailTestCase' => 'PhabricatorTestCase',
     'PhabricatorMetaMTASchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhabricatorMetaMTASendGridReceiveController' => 'PhabricatorMetaMTAController',
     'PhabricatorMetaMTAWorker' => 'PhabricatorWorker',
     'PhabricatorMetronomicTriggerClock' => 'PhabricatorTriggerClock',
     'PhabricatorMultiColumnUIExample' => 'PhabricatorUIExample',
     'PhabricatorMultiFactorSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorMultimeterApplication' => 'PhabricatorApplication',
     'PhabricatorMustVerifyEmailController' => 'PhabricatorAuthController',
     'PhabricatorMySQLConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorMySQLFileStorageEngine' => 'PhabricatorFileStorageEngine',
     'PhabricatorMySQLSearchEngine' => 'PhabricatorSearchEngine',
     'PhabricatorMySQLSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorNamedQuery' => array(
       'PhabricatorSearchDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorNamedQueryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorNavigationRemarkupRule' => 'PhutilRemarkupRule',
     'PhabricatorNeverTriggerClock' => 'PhabricatorTriggerClock',
     'PhabricatorNotificationAdHocFeedStory' => 'PhabricatorFeedStory',
     'PhabricatorNotificationClearController' => 'PhabricatorNotificationController',
     'PhabricatorNotificationConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorNotificationController' => 'PhabricatorController',
     'PhabricatorNotificationIndividualController' => 'PhabricatorNotificationController',
     'PhabricatorNotificationListController' => 'PhabricatorNotificationController',
     'PhabricatorNotificationPanelController' => 'PhabricatorNotificationController',
     'PhabricatorNotificationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorNotificationSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorNotificationStatusController' => 'PhabricatorNotificationController',
     'PhabricatorNotificationStatusView' => 'AphrontTagView',
     'PhabricatorNotificationTestController' => 'PhabricatorNotificationController',
     'PhabricatorNotificationUIExample' => 'PhabricatorUIExample',
     'PhabricatorNotificationsApplication' => 'PhabricatorApplication',
     'PhabricatorNuanceApplication' => 'PhabricatorApplication',
     'PhabricatorOAuth1AuthProvider' => 'PhabricatorOAuthAuthProvider',
     'PhabricatorOAuth2AuthProvider' => 'PhabricatorOAuthAuthProvider',
     'PhabricatorOAuthAuthProvider' => 'PhabricatorAuthProvider',
     'PhabricatorOAuthClientAuthorization' => array(
       'PhabricatorOAuthServerDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorOAuthClientAuthorizationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorOAuthClientController' => 'PhabricatorOAuthServerController',
     'PhabricatorOAuthClientDeleteController' => 'PhabricatorOAuthClientController',
     'PhabricatorOAuthClientEditController' => 'PhabricatorOAuthClientController',
     'PhabricatorOAuthClientListController' => 'PhabricatorOAuthClientController',
     'PhabricatorOAuthClientSecretController' => 'PhabricatorOAuthClientController',
     'PhabricatorOAuthClientViewController' => 'PhabricatorOAuthClientController',
     'PhabricatorOAuthResponse' => 'AphrontResponse',
     'PhabricatorOAuthServerAccessToken' => 'PhabricatorOAuthServerDAO',
     'PhabricatorOAuthServerApplication' => 'PhabricatorApplication',
     'PhabricatorOAuthServerAuthController' => 'PhabricatorAuthController',
     'PhabricatorOAuthServerAuthorizationCode' => 'PhabricatorOAuthServerDAO',
     'PhabricatorOAuthServerAuthorizationsSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorOAuthServerClient' => array(
       'PhabricatorOAuthServerDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PhabricatorOAuthServerClientAuthorizationPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorOAuthServerClientPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorOAuthServerClientQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorOAuthServerClientSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorOAuthServerController' => 'PhabricatorController',
     'PhabricatorOAuthServerCreateClientsCapability' => 'PhabricatorPolicyCapability',
     'PhabricatorOAuthServerDAO' => 'PhabricatorLiskDAO',
     'PhabricatorOAuthServerTestCase' => 'PhabricatorTestCase',
     'PhabricatorOAuthServerTestController' => 'PhabricatorOAuthServerController',
     'PhabricatorOAuthServerTokenController' => 'PhabricatorAuthController',
     'PhabricatorObjectHandle' => 'PhabricatorPolicyInterface',
     'PhabricatorObjectHandleStatus' => 'PhabricatorObjectHandleConstants',
     'PhabricatorObjectHasAsanaSubtaskEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorObjectHasAsanaTaskEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorObjectHasContributorEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorObjectHasFileEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorObjectHasJiraIssueEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorObjectHasSubscriberEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorObjectHasUnsubscriberEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorObjectHasWatcherEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorObjectListQueryTestCase' => 'PhabricatorTestCase',
     'PhabricatorObjectMailReceiver' => 'PhabricatorMailReceiver',
     'PhabricatorObjectMailReceiverTestCase' => 'PhabricatorTestCase',
     'PhabricatorObjectMentionedByObjectEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorObjectMentionsObjectEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorObjectRemarkupRule' => 'PhutilRemarkupRule',
     'PhabricatorObjectUsesCredentialsEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorOffsetPagedQuery' => 'PhabricatorQuery',
     'PhabricatorOneTimeTriggerClock' => 'PhabricatorTriggerClock',
     'PhabricatorOpcodeCacheSpec' => 'PhabricatorCacheSpec',
     'PhabricatorOwnersApplication' => 'PhabricatorApplication',
     'PhabricatorOwnersConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorOwnersController' => 'PhabricatorController',
     'PhabricatorOwnersDAO' => 'PhabricatorLiskDAO',
     'PhabricatorOwnersDeleteController' => 'PhabricatorOwnersController',
     'PhabricatorOwnersDetailController' => 'PhabricatorOwnersController',
     'PhabricatorOwnersEditController' => 'PhabricatorOwnersController',
     'PhabricatorOwnersListController' => 'PhabricatorOwnersController',
     'PhabricatorOwnersOwner' => 'PhabricatorOwnersDAO',
     'PhabricatorOwnersPackage' => array(
       'PhabricatorOwnersDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorOwnersPackageDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorOwnersPackageEditor' => 'PhabricatorEditor',
     'PhabricatorOwnersPackagePHIDType' => 'PhabricatorPHIDType',
     'PhabricatorOwnersPackageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorOwnersPackageTestCase' => 'PhabricatorTestCase',
     'PhabricatorOwnersPath' => 'PhabricatorOwnersDAO',
     'PhabricatorPHDConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorPHPASTApplication' => 'PhabricatorApplication',
     'PhabricatorPHPConfigSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorPHPMailerConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorPagedFormUIExample' => 'PhabricatorUIExample',
     'PhabricatorPagerUIExample' => 'PhabricatorUIExample',
     'PhabricatorPassphraseApplication' => 'PhabricatorApplication',
     'PhabricatorPasswordAuthProvider' => 'PhabricatorAuthProvider',
     'PhabricatorPasswordHasher' => 'Phobject',
     'PhabricatorPasswordHasherTestCase' => 'PhabricatorTestCase',
     'PhabricatorPasswordHasherUnavailableException' => 'Exception',
     'PhabricatorPasswordSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorPaste' => array(
       'PhabricatorPasteDAO',
       'PhabricatorSubscribableInterface',
       'PhabricatorTokenReceiverInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorMentionableInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorProjectInterface',
       'PhabricatorDestructibleInterface',
       'PhabricatorApplicationTransactionInterface',
     ),
     'PhabricatorPasteApplication' => 'PhabricatorApplication',
     'PhabricatorPasteCommentController' => 'PhabricatorPasteController',
     'PhabricatorPasteConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorPasteController' => 'PhabricatorController',
     'PhabricatorPasteDAO' => 'PhabricatorLiskDAO',
     'PhabricatorPasteEditController' => 'PhabricatorPasteController',
     'PhabricatorPasteEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorPasteListController' => 'PhabricatorPasteController',
     'PhabricatorPastePastePHIDType' => 'PhabricatorPHIDType',
     'PhabricatorPasteQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorPasteRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'PhabricatorPasteSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhabricatorPasteSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorPasteTestDataGenerator' => 'PhabricatorTestDataGenerator',
     'PhabricatorPasteTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorPasteTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'PhabricatorPasteTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorPasteViewController' => 'PhabricatorPasteController',
     'PhabricatorPathSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorPeopleAnyOwnerDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorPeopleApplication' => 'PhabricatorApplication',
     'PhabricatorPeopleApproveController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleCalendarController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleController' => 'PhabricatorController',
     'PhabricatorPeopleCreateController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorPeopleDeleteController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleDisableController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleEmpowerController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleExternalPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorPeopleFeedController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleHovercardEventListener' => 'PhabricatorEventListener',
     'PhabricatorPeopleInviteController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleInviteListController' => 'PhabricatorPeopleInviteController',
     'PhabricatorPeopleInviteSendController' => 'PhabricatorPeopleInviteController',
     'PhabricatorPeopleLdapController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleListController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorPeopleLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorPeopleLogsController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleNewController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleNoOwnerDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorPeopleOwnerDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'PhabricatorPeopleProfileController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleProfileEditController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleProfilePictureController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorPeopleRenameController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorPeopleTestDataGenerator' => 'PhabricatorTestDataGenerator',
     'PhabricatorPeopleUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'PhabricatorPeopleUserPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorPeopleWelcomeController' => 'PhabricatorPeopleController',
     'PhabricatorPersonaAuthProvider' => 'PhabricatorAuthProvider',
     'PhabricatorPhabricatorAuthProvider' => 'PhabricatorOAuth2AuthProvider',
     'PhabricatorPhameApplication' => 'PhabricatorApplication',
     'PhabricatorPhameBlogPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorPhameConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorPhamePostPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorPhluxApplication' => 'PhabricatorApplication',
     'PhabricatorPholioApplication' => 'PhabricatorApplication',
     'PhabricatorPholioConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorPholioMockTestDataGenerator' => 'PhabricatorTestDataGenerator',
     'PhabricatorPhortuneApplication' => 'PhabricatorApplication',
     'PhabricatorPhortuneManagementInvoiceWorkflow' => 'PhabricatorPhortuneManagementWorkflow',
     'PhabricatorPhortuneManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorPhragmentApplication' => 'PhabricatorApplication',
     'PhabricatorPhrequentApplication' => 'PhabricatorApplication',
     'PhabricatorPhrequentConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorPhrictionApplication' => 'PhabricatorApplication',
     'PhabricatorPhrictionConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorPolicies' => 'PhabricatorPolicyConstants',
     'PhabricatorPolicy' => array(
       'PhabricatorPolicyDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PhabricatorPolicyApplication' => 'PhabricatorApplication',
     'PhabricatorPolicyAwareQuery' => 'PhabricatorOffsetPagedQuery',
     'PhabricatorPolicyAwareTestQuery' => 'PhabricatorPolicyAwareQuery',
     'PhabricatorPolicyCanEditCapability' => 'PhabricatorPolicyCapability',
     'PhabricatorPolicyCanJoinCapability' => 'PhabricatorPolicyCapability',
     'PhabricatorPolicyCanViewCapability' => 'PhabricatorPolicyCapability',
     'PhabricatorPolicyCapability' => 'Phobject',
     'PhabricatorPolicyConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorPolicyController' => 'PhabricatorController',
     'PhabricatorPolicyDAO' => 'PhabricatorLiskDAO',
     'PhabricatorPolicyDataTestCase' => 'PhabricatorTestCase',
     'PhabricatorPolicyEditController' => 'PhabricatorPolicyController',
     'PhabricatorPolicyException' => 'Exception',
     'PhabricatorPolicyExplainController' => 'PhabricatorPolicyController',
     'PhabricatorPolicyInterface' => 'PhabricatorPHIDInterface',
     'PhabricatorPolicyManagementShowWorkflow' => 'PhabricatorPolicyManagementWorkflow',
     'PhabricatorPolicyManagementUnlockWorkflow' => 'PhabricatorPolicyManagementWorkflow',
     'PhabricatorPolicyManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorPolicyPHIDTypePolicy' => 'PhabricatorPHIDType',
     'PhabricatorPolicyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorPolicyTestCase' => 'PhabricatorTestCase',
     'PhabricatorPolicyTestObject' => 'PhabricatorPolicyInterface',
     'PhabricatorPolicyType' => 'PhabricatorPolicyConstants',
     'PhabricatorPonderApplication' => 'PhabricatorApplication',
     'PhabricatorProject' => array(
       'PhabricatorProjectDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorSubscribableInterface',
       'PhabricatorCustomFieldInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PhabricatorProjectApplication' => 'PhabricatorApplication',
     'PhabricatorProjectArchiveController' => 'PhabricatorProjectController',
     'PhabricatorProjectBoardController' => 'PhabricatorProjectController',
     'PhabricatorProjectBoardImportController' => 'PhabricatorProjectBoardController',
     'PhabricatorProjectBoardReorderController' => 'PhabricatorProjectBoardController',
     'PhabricatorProjectBoardViewController' => 'PhabricatorProjectBoardController',
     'PhabricatorProjectColumn' => array(
       'PhabricatorProjectDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PhabricatorProjectColumnDetailController' => 'PhabricatorProjectBoardController',
     'PhabricatorProjectColumnEditController' => 'PhabricatorProjectBoardController',
     'PhabricatorProjectColumnHideController' => 'PhabricatorProjectBoardController',
     'PhabricatorProjectColumnPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorProjectColumnPosition' => array(
       'PhabricatorProjectDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorProjectColumnPositionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorProjectColumnQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorProjectColumnTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorProjectColumnTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorProjectColumnTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorProjectConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorProjectConfiguredCustomField' => array(
       'PhabricatorProjectStandardCustomField',
       'PhabricatorStandardCustomFieldInterface',
     ),
     'PhabricatorProjectController' => 'PhabricatorController',
     'PhabricatorProjectCustomField' => 'PhabricatorCustomField',
     'PhabricatorProjectCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
     'PhabricatorProjectCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
     'PhabricatorProjectCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
     'PhabricatorProjectDAO' => 'PhabricatorLiskDAO',
     'PhabricatorProjectDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorProjectDescriptionField' => 'PhabricatorProjectStandardCustomField',
     'PhabricatorProjectEditDetailsController' => 'PhabricatorProjectController',
     'PhabricatorProjectEditIconController' => 'PhabricatorProjectController',
     'PhabricatorProjectEditPictureController' => 'PhabricatorProjectController',
     'PhabricatorProjectEditorTestCase' => 'PhabricatorTestCase',
     'PhabricatorProjectFeedController' => 'PhabricatorProjectController',
     'PhabricatorProjectIcon' => 'Phobject',
     'PhabricatorProjectListController' => 'PhabricatorProjectController',
     'PhabricatorProjectLogicalAndDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'PhabricatorProjectLogicalDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'PhabricatorProjectLogicalOrNotDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'PhabricatorProjectLogicalUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'PhabricatorProjectLogicalViewerDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorProjectMemberOfProjectEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorProjectMembersDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'PhabricatorProjectMembersEditController' => 'PhabricatorProjectController',
     'PhabricatorProjectMembersRemoveController' => 'PhabricatorProjectController',
     'PhabricatorProjectMoveController' => 'PhabricatorProjectController',
     'PhabricatorProjectNoProjectsDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorProjectObjectHasProjectEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorProjectOrUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'PhabricatorProjectProfileController' => 'PhabricatorProjectController',
     'PhabricatorProjectProjectHasMemberEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorProjectProjectHasObjectEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorProjectProjectPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorProjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorProjectSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhabricatorProjectSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorProjectSearchIndexer' => 'PhabricatorSearchDocumentIndexer',
     'PhabricatorProjectSlug' => 'PhabricatorProjectDAO',
     'PhabricatorProjectStandardCustomField' => array(
       'PhabricatorProjectCustomField',
       'PhabricatorStandardCustomFieldInterface',
     ),
     'PhabricatorProjectTestDataGenerator' => 'PhabricatorTestDataGenerator',
     'PhabricatorProjectTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorProjectTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorProjectUIEventListener' => 'PhabricatorEventListener',
     'PhabricatorProjectUpdateController' => 'PhabricatorProjectController',
     'PhabricatorProjectViewController' => 'PhabricatorProjectController',
     'PhabricatorProjectWatchController' => 'PhabricatorProjectController',
     'PhabricatorProjectsPolicyRule' => 'PhabricatorPolicyRule',
     'PhabricatorPygmentSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorQueryConstraint' => 'Phobject',
     'PhabricatorQueryOrderItem' => 'Phobject',
     'PhabricatorQueryOrderTestCase' => 'PhabricatorTestCase',
     'PhabricatorQueryOrderVector' => array(
       'Phobject',
       'Iterator',
     ),
     'PhabricatorRecaptchaConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorRedirectController' => 'PhabricatorController',
     'PhabricatorRefreshCSRFController' => 'PhabricatorAuthController',
     'PhabricatorRegistrationProfile' => 'Phobject',
     'PhabricatorReleephApplication' => 'PhabricatorApplication',
     'PhabricatorReleephApplicationConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorRemarkupControl' => 'AphrontFormTextAreaControl',
     'PhabricatorRemarkupCowsayBlockInterpreter' => 'PhutilRemarkupBlockInterpreter',
     'PhabricatorRemarkupCustomBlockRule' => 'PhutilRemarkupBlockRule',
     'PhabricatorRemarkupCustomInlineRule' => 'PhutilRemarkupRule',
     'PhabricatorRemarkupFigletBlockInterpreter' => 'PhutilRemarkupBlockInterpreter',
     'PhabricatorRemarkupGraphvizBlockInterpreter' => 'PhutilRemarkupBlockInterpreter',
     'PhabricatorRemarkupUIExample' => 'PhabricatorUIExample',
     'PhabricatorRepositoriesApplication' => 'PhabricatorApplication',
     'PhabricatorRepositoriesSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorRepository' => array(
       'PhabricatorRepositoryDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorMarkupInterface',
       'PhabricatorDestructibleInterface',
       'PhabricatorProjectInterface',
     ),
     'PhabricatorRepositoryArcanistProject' => array(
       'PhabricatorRepositoryDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PhabricatorRepositoryArcanistProjectDeleteController' => 'PhabricatorRepositoryController',
     'PhabricatorRepositoryArcanistProjectEditController' => 'PhabricatorRepositoryController',
     'PhabricatorRepositoryArcanistProjectPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorRepositoryArcanistProjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorRepositoryAuditRequest' => array(
       'PhabricatorRepositoryDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorRepositoryBranch' => 'PhabricatorRepositoryDAO',
     'PhabricatorRepositoryCommit' => array(
       'PhabricatorRepositoryDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorProjectInterface',
       'PhabricatorTokenReceiverInterface',
       'PhabricatorSubscribableInterface',
       'PhabricatorMentionableInterface',
       'HarbormasterBuildableInterface',
       'PhabricatorCustomFieldInterface',
       'PhabricatorApplicationTransactionInterface',
     ),
     'PhabricatorRepositoryCommitChangeParserWorker' => 'PhabricatorRepositoryCommitParserWorker',
     'PhabricatorRepositoryCommitData' => 'PhabricatorRepositoryDAO',
     'PhabricatorRepositoryCommitHeraldWorker' => 'PhabricatorRepositoryCommitParserWorker',
     'PhabricatorRepositoryCommitMessageParserWorker' => 'PhabricatorRepositoryCommitParserWorker',
     'PhabricatorRepositoryCommitOwnersWorker' => 'PhabricatorRepositoryCommitParserWorker',
     'PhabricatorRepositoryCommitPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorRepositoryCommitParserWorker' => 'PhabricatorWorker',
     'PhabricatorRepositoryCommitSearchIndexer' => 'PhabricatorSearchDocumentIndexer',
     'PhabricatorRepositoryConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorRepositoryController' => 'PhabricatorController',
     'PhabricatorRepositoryDAO' => 'PhabricatorLiskDAO',
     'PhabricatorRepositoryDiscoveryEngine' => 'PhabricatorRepositoryEngine',
     'PhabricatorRepositoryEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorRepositoryGitCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker',
     'PhabricatorRepositoryGitCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker',
     'PhabricatorRepositoryGraphStream' => 'Phobject',
     'PhabricatorRepositoryListController' => 'PhabricatorRepositoryController',
     'PhabricatorRepositoryManagementCacheWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
     'PhabricatorRepositoryManagementDiscoverWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
     'PhabricatorRepositoryManagementEditWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
     'PhabricatorRepositoryManagementImportingWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
     'PhabricatorRepositoryManagementListWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
     'PhabricatorRepositoryManagementLookupUsersWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
     'PhabricatorRepositoryManagementMarkImportedWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
     'PhabricatorRepositoryManagementMirrorWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
     'PhabricatorRepositoryManagementParentsWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
     'PhabricatorRepositoryManagementPullWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
     'PhabricatorRepositoryManagementRefsWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
     'PhabricatorRepositoryManagementReparseWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
     'PhabricatorRepositoryManagementUpdateWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
     'PhabricatorRepositoryManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorRepositoryMercurialCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker',
     'PhabricatorRepositoryMercurialCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker',
     'PhabricatorRepositoryMirror' => array(
       'PhabricatorRepositoryDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorRepositoryMirrorEngine' => 'PhabricatorRepositoryEngine',
     'PhabricatorRepositoryMirrorPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorRepositoryMirrorQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorRepositoryParsedChange' => 'Phobject',
     'PhabricatorRepositoryPullEngine' => 'PhabricatorRepositoryEngine',
     'PhabricatorRepositoryPullLocalDaemon' => 'PhabricatorDaemon',
     'PhabricatorRepositoryPushEvent' => array(
       'PhabricatorRepositoryDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorRepositoryPushEventPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorRepositoryPushEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorRepositoryPushLog' => array(
       'PhabricatorRepositoryDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorRepositoryPushLogPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorRepositoryPushLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorRepositoryPushLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorRepositoryPushMailWorker' => 'PhabricatorWorker',
     'PhabricatorRepositoryPushReplyHandler' => 'PhabricatorMailReplyHandler',
     'PhabricatorRepositoryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorRepositoryRefCursor' => array(
       'PhabricatorRepositoryDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorRepositoryRefCursorQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorRepositoryRefEngine' => 'PhabricatorRepositoryEngine',
     'PhabricatorRepositoryRepositoryPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorRepositorySchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhabricatorRepositorySearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorRepositoryStatusMessage' => 'PhabricatorRepositoryDAO',
     'PhabricatorRepositorySvnCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker',
     'PhabricatorRepositorySvnCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker',
     'PhabricatorRepositorySymbol' => 'PhabricatorRepositoryDAO',
     'PhabricatorRepositoryTestCase' => 'PhabricatorTestCase',
     'PhabricatorRepositoryTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorRepositoryTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorRepositoryURINormalizer' => 'Phobject',
     'PhabricatorRepositoryURINormalizerTestCase' => 'PhabricatorTestCase',
     'PhabricatorRepositoryURITestCase' => 'PhabricatorTestCase',
     'PhabricatorRepositoryVCSPassword' => 'PhabricatorRepositoryDAO',
     'PhabricatorRepositoryVersion' => 'Phobject',
     'PhabricatorRobotsController' => 'PhabricatorController',
     'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine',
     'PhabricatorSMS' => 'PhabricatorSMSDAO',
     'PhabricatorSMSConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorSMSDAO' => 'PhabricatorLiskDAO',
     'PhabricatorSMSDemultiplexWorker' => 'PhabricatorSMSWorker',
     'PhabricatorSMSImplementationTestBlackholeAdapter' => 'PhabricatorSMSImplementationAdapter',
     'PhabricatorSMSImplementationTwilioAdapter' => 'PhabricatorSMSImplementationAdapter',
     'PhabricatorSMSManagementListOutboundWorkflow' => 'PhabricatorSMSManagementWorkflow',
     'PhabricatorSMSManagementSendTestWorkflow' => 'PhabricatorSMSManagementWorkflow',
     'PhabricatorSMSManagementShowOutboundWorkflow' => 'PhabricatorSMSManagementWorkflow',
     'PhabricatorSMSManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorSMSSendWorker' => 'PhabricatorSMSWorker',
     'PhabricatorSMSWorker' => 'PhabricatorWorker',
     'PhabricatorSSHKeyGenerator' => 'Phobject',
     'PhabricatorSSHKeysSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorSSHLog' => 'Phobject',
     'PhabricatorSSHPassthruCommand' => 'Phobject',
     'PhabricatorSSHWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorSavedQuery' => array(
       'PhabricatorSearchDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorSavedQueryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorScheduleTaskTriggerAction' => 'PhabricatorTriggerAction',
     'PhabricatorSearchApplication' => 'PhabricatorApplication',
     'PhabricatorSearchApplicationSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorSearchAttachController' => 'PhabricatorSearchBaseController',
     'PhabricatorSearchBaseController' => 'PhabricatorController',
     'PhabricatorSearchConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorSearchController' => 'PhabricatorSearchBaseController',
     'PhabricatorSearchDAO' => 'PhabricatorLiskDAO',
     'PhabricatorSearchDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'PhabricatorSearchDeleteController' => 'PhabricatorSearchBaseController',
     'PhabricatorSearchDocument' => 'PhabricatorSearchDAO',
     'PhabricatorSearchDocumentField' => 'PhabricatorSearchDAO',
     'PhabricatorSearchDocumentIndexer' => 'Phobject',
     'PhabricatorSearchDocumentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorSearchDocumentRelationship' => 'PhabricatorSearchDAO',
     'PhabricatorSearchDocumentTypeDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorSearchEditController' => 'PhabricatorSearchBaseController',
     'PhabricatorSearchHovercardController' => 'PhabricatorSearchBaseController',
     'PhabricatorSearchManagementIndexWorkflow' => 'PhabricatorSearchManagementWorkflow',
     'PhabricatorSearchManagementInitWorkflow' => 'PhabricatorSearchManagementWorkflow',
     'PhabricatorSearchManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorSearchOrderController' => 'PhabricatorSearchBaseController',
     'PhabricatorSearchPreferencesSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorSearchResultView' => 'AphrontView',
     'PhabricatorSearchSelectController' => 'PhabricatorSearchBaseController',
     'PhabricatorSearchWorker' => 'PhabricatorWorker',
     'PhabricatorSecurityConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorSecuritySetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorSendGridConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorSessionsSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorSettingsAddEmailAction' => 'PhabricatorSystemAction',
     'PhabricatorSettingsAdjustController' => 'PhabricatorController',
     'PhabricatorSettingsApplication' => 'PhabricatorApplication',
     'PhabricatorSettingsMainController' => 'PhabricatorController',
     'PhabricatorSetupIssueUIExample' => 'PhabricatorUIExample',
     'PhabricatorSetupIssueView' => 'AphrontView',
     'PhabricatorSlowvoteApplication' => 'PhabricatorApplication',
     'PhabricatorSlowvoteChoice' => 'PhabricatorSlowvoteDAO',
     'PhabricatorSlowvoteCloseController' => 'PhabricatorSlowvoteController',
     'PhabricatorSlowvoteCommentController' => 'PhabricatorSlowvoteController',
     'PhabricatorSlowvoteController' => 'PhabricatorController',
     'PhabricatorSlowvoteDAO' => 'PhabricatorLiskDAO',
     'PhabricatorSlowvoteDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'PhabricatorSlowvoteEditController' => 'PhabricatorSlowvoteController',
     'PhabricatorSlowvoteEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorSlowvoteListController' => 'PhabricatorSlowvoteController',
     'PhabricatorSlowvoteOption' => 'PhabricatorSlowvoteDAO',
     'PhabricatorSlowvotePoll' => array(
       'PhabricatorSlowvoteDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorSubscribableInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorTokenReceiverInterface',
       'PhabricatorProjectInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PhabricatorSlowvotePollController' => 'PhabricatorSlowvoteController',
     'PhabricatorSlowvotePollPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorSlowvoteQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorSlowvoteSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhabricatorSlowvoteSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorSlowvoteTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorSlowvoteTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'PhabricatorSlowvoteTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorSlowvoteVoteController' => 'PhabricatorSlowvoteController',
     'PhabricatorSlugTestCase' => 'PhabricatorTestCase',
     'PhabricatorSortTableUIExample' => 'PhabricatorUIExample',
     'PhabricatorSourceCodeView' => 'AphrontView',
     'PhabricatorStandardCustomField' => 'PhabricatorCustomField',
     'PhabricatorStandardCustomFieldBool' => 'PhabricatorStandardCustomField',
     'PhabricatorStandardCustomFieldCredential' => 'PhabricatorStandardCustomField',
     'PhabricatorStandardCustomFieldDate' => 'PhabricatorStandardCustomField',
     'PhabricatorStandardCustomFieldHeader' => 'PhabricatorStandardCustomField',
     'PhabricatorStandardCustomFieldInt' => 'PhabricatorStandardCustomField',
     'PhabricatorStandardCustomFieldLink' => 'PhabricatorStandardCustomField',
     'PhabricatorStandardCustomFieldPHIDs' => 'PhabricatorStandardCustomField',
     'PhabricatorStandardCustomFieldRemarkup' => 'PhabricatorStandardCustomField',
     'PhabricatorStandardCustomFieldSelect' => 'PhabricatorStandardCustomField',
     'PhabricatorStandardCustomFieldText' => 'PhabricatorStandardCustomField',
     'PhabricatorStandardCustomFieldUsers' => 'PhabricatorStandardCustomFieldPHIDs',
     'PhabricatorStandardPageView' => 'PhabricatorBarePageView',
     'PhabricatorStatusController' => 'PhabricatorController',
     'PhabricatorStatusUIExample' => 'PhabricatorUIExample',
     'PhabricatorStorageManagementAdjustWorkflow' => 'PhabricatorStorageManagementWorkflow',
     'PhabricatorStorageManagementDatabasesWorkflow' => 'PhabricatorStorageManagementWorkflow',
     'PhabricatorStorageManagementDestroyWorkflow' => 'PhabricatorStorageManagementWorkflow',
     'PhabricatorStorageManagementDumpWorkflow' => 'PhabricatorStorageManagementWorkflow',
     'PhabricatorStorageManagementProbeWorkflow' => 'PhabricatorStorageManagementWorkflow',
     'PhabricatorStorageManagementQuickstartWorkflow' => 'PhabricatorStorageManagementWorkflow',
     'PhabricatorStorageManagementRenamespaceWorkflow' => 'PhabricatorStorageManagementWorkflow',
     'PhabricatorStorageManagementShellWorkflow' => 'PhabricatorStorageManagementWorkflow',
     'PhabricatorStorageManagementStatusWorkflow' => 'PhabricatorStorageManagementWorkflow',
     'PhabricatorStorageManagementUpgradeWorkflow' => 'PhabricatorStorageManagementWorkflow',
     'PhabricatorStorageManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorStorageSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhabricatorStorageSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorSubscribedToObjectEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorSubscribersQuery' => 'PhabricatorQuery',
     'PhabricatorSubscriptionTriggerClock' => 'PhabricatorTriggerClock',
     'PhabricatorSubscriptionsApplication' => 'PhabricatorApplication',
     'PhabricatorSubscriptionsEditController' => 'PhabricatorController',
     'PhabricatorSubscriptionsEditor' => 'PhabricatorEditor',
     'PhabricatorSubscriptionsListController' => 'PhabricatorController',
     'PhabricatorSubscriptionsSubscribeEmailCommand' => 'MetaMTAEmailTransactionCommand',
     'PhabricatorSubscriptionsTransactionController' => 'PhabricatorController',
     'PhabricatorSubscriptionsUIEventListener' => 'PhabricatorEventListener',
     'PhabricatorSubscriptionsUnsubscribeEmailCommand' => 'MetaMTAEmailTransactionCommand',
     'PhabricatorSupportApplication' => 'PhabricatorApplication',
     'PhabricatorSyntaxHighlightingConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorSystemActionEngine' => 'Phobject',
     'PhabricatorSystemActionGarbageCollector' => 'PhabricatorGarbageCollector',
     'PhabricatorSystemActionLog' => 'PhabricatorSystemDAO',
     'PhabricatorSystemActionRateLimitException' => 'Exception',
     'PhabricatorSystemApplication' => 'PhabricatorApplication',
     'PhabricatorSystemDAO' => 'PhabricatorLiskDAO',
     'PhabricatorSystemDestructionGarbageCollector' => 'PhabricatorGarbageCollector',
     'PhabricatorSystemDestructionLog' => 'PhabricatorSystemDAO',
     'PhabricatorSystemRemoveDestroyWorkflow' => 'PhabricatorSystemRemoveWorkflow',
     'PhabricatorSystemRemoveLogWorkflow' => 'PhabricatorSystemRemoveWorkflow',
     'PhabricatorSystemRemoveWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorSystemSelectEncodingController' => 'PhabricatorController',
     'PhabricatorSystemSelectHighlightController' => 'PhabricatorController',
     'PhabricatorTOTPAuthFactor' => 'PhabricatorAuthFactor',
     'PhabricatorTOTPAuthFactorTestCase' => 'PhabricatorTestCase',
     'PhabricatorTaskmasterDaemon' => 'PhabricatorDaemon',
     'PhabricatorTestApplication' => 'PhabricatorApplication',
     'PhabricatorTestCase' => 'ArcanistPhutilTestCase',
     'PhabricatorTestController' => 'PhabricatorController',
     'PhabricatorTestNoCycleEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorTestStorageEngine' => 'PhabricatorFileStorageEngine',
     'PhabricatorTestWorker' => 'PhabricatorWorker',
     'PhabricatorTimeTestCase' => 'PhabricatorTestCase',
     'PhabricatorTimezoneSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorToken' => array(
       'PhabricatorTokenDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorTokenController' => 'PhabricatorController',
     'PhabricatorTokenCount' => 'PhabricatorTokenDAO',
     'PhabricatorTokenCountQuery' => 'PhabricatorOffsetPagedQuery',
     'PhabricatorTokenDAO' => 'PhabricatorLiskDAO',
     'PhabricatorTokenGiveController' => 'PhabricatorTokenController',
     'PhabricatorTokenGiven' => array(
       'PhabricatorTokenDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorTokenGivenController' => 'PhabricatorTokenController',
     'PhabricatorTokenGivenEditor' => 'PhabricatorEditor',
     'PhabricatorTokenGivenFeedStory' => 'PhabricatorFeedStory',
     'PhabricatorTokenGivenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorTokenLeaderController' => 'PhabricatorTokenController',
     'PhabricatorTokenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorTokenReceiverQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorTokenTokenPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorTokenUIEventListener' => 'PhabricatorEventListener',
     'PhabricatorTokensApplication' => 'PhabricatorApplication',
     'PhabricatorTokensSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorTooltipUIExample' => 'PhabricatorUIExample',
     'PhabricatorTransactionsApplication' => 'PhabricatorApplication',
     'PhabricatorTransformedFile' => 'PhabricatorFileDAO',
     'PhabricatorTranslationsConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorTriggerAction' => 'Phobject',
     'PhabricatorTriggerClock' => 'Phobject',
     'PhabricatorTriggerClockTestCase' => 'PhabricatorTestCase',
     'PhabricatorTriggerDaemon' => 'PhabricatorDaemon',
     'PhabricatorTrivialTestCase' => 'PhabricatorTestCase',
     'PhabricatorTwitchAuthProvider' => 'PhabricatorOAuth2AuthProvider',
     'PhabricatorTwitterAuthProvider' => 'PhabricatorOAuth1AuthProvider',
     'PhabricatorTwoColumnUIExample' => 'PhabricatorUIExample',
     'PhabricatorTypeaheadApplication' => 'PhabricatorApplication',
     'PhabricatorTypeaheadCompositeDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorTypeaheadDatasource' => 'Phobject',
     'PhabricatorTypeaheadDatasourceController' => 'PhabricatorController',
     'PhabricatorTypeaheadFunctionHelpController' => 'PhabricatorTypeaheadDatasourceController',
     'PhabricatorTypeaheadInvalidTokenException' => 'Exception',
     'PhabricatorTypeaheadModularDatasourceController' => 'PhabricatorTypeaheadDatasourceController',
     'PhabricatorTypeaheadMonogramDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorTypeaheadRuntimeCompositeDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'PhabricatorTypeaheadTokenView' => 'AphrontTagView',
     'PhabricatorUIConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorUIExampleRenderController' => 'PhabricatorController',
     'PhabricatorUIExamplesApplication' => 'PhabricatorApplication',
     'PhabricatorUSEnglishTranslation' => 'PhutilTranslation',
     'PhabricatorUnitsTestCase' => 'PhabricatorTestCase',
     'PhabricatorUnsubscribedFromObjectEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorUser' => array(
       'PhabricatorUserDAO',
       'PhutilPerson',
       'PhabricatorPolicyInterface',
       'PhabricatorCustomFieldInterface',
       'PhabricatorDestructibleInterface',
       'PhabricatorSSHPublicKeyInterface',
     ),
     'PhabricatorUserBlurbField' => 'PhabricatorUserCustomField',
     'PhabricatorUserConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorUserConfiguredCustomField' => array(
       'PhabricatorUserCustomField',
       'PhabricatorStandardCustomFieldInterface',
     ),
     'PhabricatorUserConfiguredCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
     'PhabricatorUserCustomField' => 'PhabricatorCustomField',
     'PhabricatorUserCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
     'PhabricatorUserCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
     'PhabricatorUserDAO' => 'PhabricatorLiskDAO',
     'PhabricatorUserEditor' => 'PhabricatorEditor',
     'PhabricatorUserEditorTestCase' => 'PhabricatorTestCase',
     'PhabricatorUserEmail' => 'PhabricatorUserDAO',
     'PhabricatorUserEmailTestCase' => 'PhabricatorTestCase',
     'PhabricatorUserLog' => array(
       'PhabricatorUserDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorUserLogView' => 'AphrontView',
     'PhabricatorUserPreferences' => 'PhabricatorUserDAO',
     'PhabricatorUserProfile' => 'PhabricatorUserDAO',
     'PhabricatorUserProfileEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorUserRealNameField' => 'PhabricatorUserCustomField',
     'PhabricatorUserRolesField' => 'PhabricatorUserCustomField',
     'PhabricatorUserSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhabricatorUserSearchIndexer' => 'PhabricatorSearchDocumentIndexer',
     'PhabricatorUserSinceField' => 'PhabricatorUserCustomField',
     'PhabricatorUserStatusField' => 'PhabricatorUserCustomField',
     'PhabricatorUserTestCase' => 'PhabricatorTestCase',
     'PhabricatorUserTitleField' => 'PhabricatorUserCustomField',
     'PhabricatorUserTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorUsersPolicyRule' => 'PhabricatorPolicyRule',
     'PhabricatorVCSResponse' => 'AphrontResponse',
     'PhabricatorVeryWowEnglishTranslation' => 'PhutilTranslation',
     'PhabricatorViewerDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorWatcherHasObjectEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorWordPressAuthProvider' => 'PhabricatorOAuth2AuthProvider',
     'PhabricatorWorkerActiveTask' => 'PhabricatorWorkerTask',
     'PhabricatorWorkerArchiveTask' => 'PhabricatorWorkerTask',
     'PhabricatorWorkerArchiveTaskQuery' => 'PhabricatorQuery',
     'PhabricatorWorkerDAO' => 'PhabricatorLiskDAO',
     'PhabricatorWorkerLeaseQuery' => 'PhabricatorQuery',
     'PhabricatorWorkerManagementCancelWorkflow' => 'PhabricatorWorkerManagementWorkflow',
     'PhabricatorWorkerManagementExecuteWorkflow' => 'PhabricatorWorkerManagementWorkflow',
     'PhabricatorWorkerManagementFloodWorkflow' => 'PhabricatorWorkerManagementWorkflow',
     'PhabricatorWorkerManagementFreeWorkflow' => 'PhabricatorWorkerManagementWorkflow',
     'PhabricatorWorkerManagementRetryWorkflow' => 'PhabricatorWorkerManagementWorkflow',
     'PhabricatorWorkerManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorWorkerPermanentFailureException' => 'Exception',
     'PhabricatorWorkerTask' => 'PhabricatorWorkerDAO',
     'PhabricatorWorkerTaskData' => 'PhabricatorWorkerDAO',
     'PhabricatorWorkerTaskDetailController' => 'PhabricatorDaemonController',
     'PhabricatorWorkerTestCase' => 'PhabricatorTestCase',
     'PhabricatorWorkerTrigger' => array(
       'PhabricatorWorkerDAO',
       'PhabricatorDestructibleInterface',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorWorkerTriggerEvent' => 'PhabricatorWorkerDAO',
     'PhabricatorWorkerTriggerManagementFireWorkflow' => 'PhabricatorWorkerTriggerManagementWorkflow',
     'PhabricatorWorkerTriggerManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorWorkerTriggerPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorWorkerTriggerQuery' => 'PhabricatorPolicyAwareQuery',
     'PhabricatorWorkerYieldException' => 'Exception',
     'PhabricatorWorkingCopyDiscoveryTestCase' => 'PhabricatorWorkingCopyTestCase',
     'PhabricatorWorkingCopyPullTestCase' => 'PhabricatorWorkingCopyTestCase',
     'PhabricatorWorkingCopyTestCase' => 'PhabricatorTestCase',
     'PhabricatorXHPASTViewController' => 'PhabricatorController',
     'PhabricatorXHPASTViewDAO' => 'PhabricatorLiskDAO',
     'PhabricatorXHPASTViewFrameController' => 'PhabricatorXHPASTViewController',
     'PhabricatorXHPASTViewFramesetController' => 'PhabricatorXHPASTViewController',
     'PhabricatorXHPASTViewInputController' => 'PhabricatorXHPASTViewPanelController',
     'PhabricatorXHPASTViewPanelController' => 'PhabricatorXHPASTViewController',
     'PhabricatorXHPASTViewParseTree' => 'PhabricatorXHPASTViewDAO',
     'PhabricatorXHPASTViewRunController' => 'PhabricatorXHPASTViewController',
     'PhabricatorXHPASTViewStreamController' => 'PhabricatorXHPASTViewPanelController',
     'PhabricatorXHPASTViewTreeController' => 'PhabricatorXHPASTViewPanelController',
     'PhabricatorXHProfApplication' => 'PhabricatorApplication',
     'PhabricatorXHProfController' => 'PhabricatorController',
     'PhabricatorXHProfDAO' => 'PhabricatorLiskDAO',
     'PhabricatorXHProfProfileController' => 'PhabricatorXHProfController',
     'PhabricatorXHProfProfileSymbolView' => 'PhabricatorXHProfProfileView',
     'PhabricatorXHProfProfileTopLevelView' => 'PhabricatorXHProfProfileView',
     'PhabricatorXHProfProfileView' => 'AphrontView',
     'PhabricatorXHProfSample' => 'PhabricatorXHProfDAO',
     'PhabricatorXHProfSampleListController' => 'PhabricatorXHProfController',
     'PhabricatorYoutubeRemarkupRule' => 'PhutilRemarkupRule',
     'PhameBasicBlogSkin' => 'PhameBlogSkin',
     'PhameBasicTemplateBlogSkin' => 'PhameBasicBlogSkin',
     'PhameBlog' => array(
       'PhameDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorMarkupInterface',
     ),
     'PhameBlogDeleteController' => 'PhameController',
     'PhameBlogEditController' => 'PhameController',
     'PhameBlogFeedController' => 'PhameController',
     'PhameBlogListController' => 'PhameController',
     'PhameBlogLiveController' => 'PhameController',
     'PhameBlogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhameBlogSkin' => 'PhabricatorController',
     'PhameBlogViewController' => 'PhameController',
     'PhameCelerityResources' => 'CelerityResources',
     'PhameConduitAPIMethod' => 'ConduitAPIMethod',
     'PhameController' => 'PhabricatorController',
     'PhameCreatePostConduitAPIMethod' => 'PhameConduitAPIMethod',
     'PhameDAO' => 'PhabricatorLiskDAO',
     'PhamePost' => array(
       'PhameDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorMarkupInterface',
       'PhabricatorTokenReceiverInterface',
     ),
     'PhamePostDeleteController' => 'PhameController',
     'PhamePostEditController' => 'PhameController',
     'PhamePostFramedController' => 'PhameController',
     'PhamePostListController' => 'PhameController',
     'PhamePostNewController' => 'PhameController',
     'PhamePostNotLiveController' => 'PhameController',
     'PhamePostPreviewController' => 'PhameController',
     'PhamePostPublishController' => 'PhameController',
     'PhamePostQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhamePostUnpublishController' => 'PhameController',
     'PhamePostView' => 'AphrontView',
     'PhamePostViewController' => 'PhameController',
     'PhameQueryConduitAPIMethod' => 'PhameConduitAPIMethod',
     'PhameQueryPostsConduitAPIMethod' => 'PhameConduitAPIMethod',
     'PhameResourceController' => 'CelerityResourceController',
     'PhameSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhluxController' => 'PhabricatorController',
     'PhluxDAO' => 'PhabricatorLiskDAO',
     'PhluxEditController' => 'PhluxController',
     'PhluxListController' => 'PhluxController',
     'PhluxTransaction' => 'PhabricatorApplicationTransaction',
     'PhluxTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhluxVariable' => array(
       'PhluxDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorPolicyInterface',
     ),
     'PhluxVariableEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhluxVariablePHIDType' => 'PhabricatorPHIDType',
     'PhluxVariableQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhluxViewController' => 'PhluxController',
     'PholioActionMenuEventListener' => 'PhabricatorEventListener',
     'PholioController' => 'PhabricatorController',
     'PholioDAO' => 'PhabricatorLiskDAO',
     'PholioDefaultEditCapability' => 'PhabricatorPolicyCapability',
     'PholioDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'PholioImage' => array(
       'PholioDAO',
       'PhabricatorMarkupInterface',
       'PhabricatorPolicyInterface',
     ),
     'PholioImagePHIDType' => 'PhabricatorPHIDType',
     'PholioImageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PholioImageUploadController' => 'PholioController',
     'PholioInlineController' => 'PholioController',
     'PholioInlineListController' => 'PholioController',
-    'PholioInlineThumbController' => 'PholioController',
     'PholioMock' => array(
       'PholioDAO',
       'PhabricatorMarkupInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorSubscribableInterface',
       'PhabricatorTokenReceiverInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorProjectInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PholioMockCommentController' => 'PholioController',
     'PholioMockEditController' => 'PholioController',
     'PholioMockEditor' => 'PhabricatorApplicationTransactionEditor',
     'PholioMockEmbedView' => 'AphrontView',
     'PholioMockHasTaskEdgeType' => 'PhabricatorEdgeType',
     'PholioMockImagesView' => 'AphrontView',
     'PholioMockListController' => 'PholioController',
     'PholioMockMailReceiver' => 'PhabricatorObjectMailReceiver',
     'PholioMockPHIDType' => 'PhabricatorPHIDType',
     'PholioMockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PholioMockSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PholioMockThumbGridView' => 'AphrontView',
     'PholioMockViewController' => 'PholioController',
     'PholioRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'PholioReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'PholioSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PholioSearchIndexer' => 'PhabricatorSearchDocumentIndexer',
     'PholioTransaction' => 'PhabricatorApplicationTransaction',
     'PholioTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'PholioTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PholioTransactionType' => 'PholioConstants',
     'PholioTransactionView' => 'PhabricatorApplicationTransactionView',
     'PholioUploadedImageView' => 'AphrontView',
     'PhortuneAccount' => array(
       'PhortuneDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
     ),
     'PhortuneAccountEditController' => 'PhortuneController',
     'PhortuneAccountEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhortuneAccountHasMemberEdgeType' => 'PhabricatorEdgeType',
     'PhortuneAccountListController' => 'PhortuneController',
     'PhortuneAccountPHIDType' => 'PhabricatorPHIDType',
     'PhortuneAccountQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhortuneAccountTransaction' => 'PhabricatorApplicationTransaction',
     'PhortuneAccountTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhortuneAccountViewController' => 'PhortuneController',
     'PhortuneAdHocCart' => 'PhortuneCartImplementation',
     'PhortuneAdHocProduct' => 'PhortuneProductImplementation',
     'PhortuneCart' => array(
       'PhortuneDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
     ),
     'PhortuneCartAcceptController' => 'PhortuneCartController',
     'PhortuneCartCancelController' => 'PhortuneCartController',
     'PhortuneCartCheckoutController' => 'PhortuneCartController',
     'PhortuneCartController' => 'PhortuneController',
     'PhortuneCartEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhortuneCartListController' => 'PhortuneController',
     'PhortuneCartPHIDType' => 'PhabricatorPHIDType',
     'PhortuneCartQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhortuneCartReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'PhortuneCartSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhortuneCartTransaction' => 'PhabricatorApplicationTransaction',
     'PhortuneCartTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhortuneCartUpdateController' => 'PhortuneCartController',
     'PhortuneCartViewController' => 'PhortuneCartController',
     'PhortuneCharge' => array(
       'PhortuneDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhortuneChargeListController' => 'PhortuneController',
     'PhortuneChargePHIDType' => 'PhabricatorPHIDType',
     'PhortuneChargeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhortuneChargeSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhortuneChargeTableView' => 'AphrontView',
     'PhortuneController' => 'PhabricatorController',
     'PhortuneCurrency' => 'Phobject',
     'PhortuneCurrencySerializer' => 'PhabricatorLiskSerializer',
     'PhortuneCurrencyTestCase' => 'PhabricatorTestCase',
     'PhortuneDAO' => 'PhabricatorLiskDAO',
     'PhortuneErrCode' => 'PhortuneConstants',
     'PhortuneLandingController' => 'PhortuneController',
     'PhortuneMemberHasAccountEdgeType' => 'PhabricatorEdgeType',
     'PhortuneMemberHasMerchantEdgeType' => 'PhabricatorEdgeType',
     'PhortuneMerchant' => array(
       'PhortuneDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
     ),
     'PhortuneMerchantCapability' => 'PhabricatorPolicyCapability',
     'PhortuneMerchantController' => 'PhortuneController',
     'PhortuneMerchantEditController' => 'PhortuneMerchantController',
     'PhortuneMerchantEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhortuneMerchantHasMemberEdgeType' => 'PhabricatorEdgeType',
     'PhortuneMerchantInvoiceCreateController' => 'PhortuneMerchantController',
     'PhortuneMerchantListController' => 'PhortuneMerchantController',
     'PhortuneMerchantPHIDType' => 'PhabricatorPHIDType',
     'PhortuneMerchantQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhortuneMerchantSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhortuneMerchantTransaction' => 'PhabricatorApplicationTransaction',
     'PhortuneMerchantTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhortuneMerchantViewController' => 'PhortuneMerchantController',
     'PhortuneMonthYearExpiryControl' => 'AphrontFormControl',
     'PhortuneNotImplementedException' => 'Exception',
     'PhortuneOrderTableView' => 'AphrontView',
     'PhortunePayPalPaymentProvider' => 'PhortunePaymentProvider',
     'PhortunePaymentMethod' => array(
       'PhortuneDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhortunePaymentMethodCreateController' => 'PhortuneController',
     'PhortunePaymentMethodDisableController' => 'PhortuneController',
     'PhortunePaymentMethodEditController' => 'PhortuneController',
     'PhortunePaymentMethodPHIDType' => 'PhabricatorPHIDType',
     'PhortunePaymentMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhortunePaymentProviderConfig' => array(
       'PhortuneDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhortunePaymentProviderConfigEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhortunePaymentProviderConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhortunePaymentProviderConfigTransaction' => 'PhabricatorApplicationTransaction',
     'PhortunePaymentProviderConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhortunePaymentProviderPHIDType' => 'PhabricatorPHIDType',
     'PhortuneProduct' => array(
       'PhortuneDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhortuneProductListController' => 'PhabricatorController',
     'PhortuneProductPHIDType' => 'PhabricatorPHIDType',
     'PhortuneProductQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhortuneProductViewController' => 'PhortuneController',
     'PhortuneProviderActionController' => 'PhortuneController',
     'PhortuneProviderDisableController' => 'PhortuneMerchantController',
     'PhortuneProviderEditController' => 'PhortuneMerchantController',
     'PhortunePurchase' => array(
       'PhortuneDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhortunePurchasePHIDType' => 'PhabricatorPHIDType',
     'PhortunePurchaseQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhortuneSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhortuneStripePaymentProvider' => 'PhortunePaymentProvider',
     'PhortuneSubscription' => array(
       'PhortuneDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhortuneSubscriptionCart' => 'PhortuneCartImplementation',
     'PhortuneSubscriptionEditController' => 'PhortuneController',
     'PhortuneSubscriptionListController' => 'PhortuneController',
     'PhortuneSubscriptionPHIDType' => 'PhabricatorPHIDType',
     'PhortuneSubscriptionProduct' => 'PhortuneProductImplementation',
     'PhortuneSubscriptionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhortuneSubscriptionSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhortuneSubscriptionTableView' => 'AphrontView',
     'PhortuneSubscriptionViewController' => 'PhortuneController',
     'PhortuneSubscriptionWorker' => 'PhabricatorWorker',
     'PhortuneTestPaymentProvider' => 'PhortunePaymentProvider',
     'PhortuneWePayPaymentProvider' => 'PhortunePaymentProvider',
     'PhragmentBrowseController' => 'PhragmentController',
     'PhragmentCanCreateCapability' => 'PhabricatorPolicyCapability',
     'PhragmentConduitAPIMethod' => 'ConduitAPIMethod',
     'PhragmentController' => 'PhabricatorController',
     'PhragmentCreateController' => 'PhragmentController',
     'PhragmentDAO' => 'PhabricatorLiskDAO',
     'PhragmentFragment' => array(
       'PhragmentDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhragmentFragmentPHIDType' => 'PhabricatorPHIDType',
     'PhragmentFragmentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhragmentFragmentVersion' => array(
       'PhragmentDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhragmentFragmentVersionPHIDType' => 'PhabricatorPHIDType',
     'PhragmentFragmentVersionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhragmentGetPatchConduitAPIMethod' => 'PhragmentConduitAPIMethod',
     'PhragmentHistoryController' => 'PhragmentController',
     'PhragmentPatchController' => 'PhragmentController',
     'PhragmentPatchUtil' => 'Phobject',
     'PhragmentPolicyController' => 'PhragmentController',
     'PhragmentQueryFragmentsConduitAPIMethod' => 'PhragmentConduitAPIMethod',
     'PhragmentRevertController' => 'PhragmentController',
     'PhragmentSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhragmentSnapshot' => array(
       'PhragmentDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhragmentSnapshotChild' => array(
       'PhragmentDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhragmentSnapshotChildQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhragmentSnapshotCreateController' => 'PhragmentController',
     'PhragmentSnapshotDeleteController' => 'PhragmentController',
     'PhragmentSnapshotPHIDType' => 'PhabricatorPHIDType',
     'PhragmentSnapshotPromoteController' => 'PhragmentController',
     'PhragmentSnapshotQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhragmentSnapshotViewController' => 'PhragmentController',
     'PhragmentUpdateController' => 'PhragmentController',
     'PhragmentVersionController' => 'PhragmentController',
     'PhragmentZIPController' => 'PhragmentController',
     'PhrequentConduitAPIMethod' => 'ConduitAPIMethod',
     'PhrequentController' => 'PhabricatorController',
     'PhrequentDAO' => 'PhabricatorLiskDAO',
     'PhrequentListController' => 'PhrequentController',
     'PhrequentPopConduitAPIMethod' => 'PhrequentConduitAPIMethod',
     'PhrequentPushConduitAPIMethod' => 'PhrequentConduitAPIMethod',
     'PhrequentSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhrequentTimeBlock' => 'Phobject',
     'PhrequentTimeBlockTestCase' => 'PhabricatorTestCase',
     'PhrequentTimeSlices' => 'Phobject',
     'PhrequentTrackController' => 'PhrequentController',
     'PhrequentTrackingConduitAPIMethod' => 'PhrequentConduitAPIMethod',
     'PhrequentTrackingEditor' => 'PhabricatorEditor',
     'PhrequentUIEventListener' => 'PhabricatorEventListener',
     'PhrequentUserTime' => array(
       'PhrequentDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhrequentUserTimeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhrictionActionConstants' => 'PhrictionConstants',
     'PhrictionChangeType' => 'PhrictionConstants',
     'PhrictionConduitAPIMethod' => 'ConduitAPIMethod',
     'PhrictionContent' => array(
       'PhrictionDAO',
       'PhabricatorMarkupInterface',
     ),
     'PhrictionController' => 'PhabricatorController',
     'PhrictionCreateConduitAPIMethod' => 'PhrictionConduitAPIMethod',
     'PhrictionDAO' => 'PhabricatorLiskDAO',
     'PhrictionDeleteController' => 'PhrictionController',
     'PhrictionDiffController' => 'PhrictionController',
     'PhrictionDocument' => array(
       'PhrictionDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorSubscribableInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorTokenReceiverInterface',
       'PhabricatorDestructibleInterface',
       'PhabricatorApplicationTransactionInterface',
     ),
     'PhrictionDocumentController' => 'PhrictionController',
     'PhrictionDocumentHeraldAdapter' => 'HeraldAdapter',
     'PhrictionDocumentPHIDType' => 'PhabricatorPHIDType',
     'PhrictionDocumentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhrictionDocumentStatus' => 'PhrictionConstants',
     'PhrictionEditConduitAPIMethod' => 'PhrictionConduitAPIMethod',
     'PhrictionEditController' => 'PhrictionController',
     'PhrictionHistoryConduitAPIMethod' => 'PhrictionConduitAPIMethod',
     'PhrictionHistoryController' => 'PhrictionController',
     'PhrictionInfoConduitAPIMethod' => 'PhrictionConduitAPIMethod',
     'PhrictionListController' => 'PhrictionController',
     'PhrictionMoveController' => 'PhrictionController',
     'PhrictionNewController' => 'PhrictionController',
     'PhrictionRemarkupRule' => 'PhutilRemarkupRule',
     'PhrictionReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'PhrictionSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhrictionSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhrictionSearchIndexer' => 'PhabricatorSearchDocumentIndexer',
     'PhrictionTransaction' => 'PhabricatorApplicationTransaction',
     'PhrictionTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'PhrictionTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhrictionTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PolicyLockOptionType' => 'PhabricatorConfigJSONOptionType',
     'PonderAddAnswerView' => 'AphrontView',
     'PonderAnswer' => array(
       'PonderDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorMarkupInterface',
       'PonderVotableInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorSubscribableInterface',
       'PhabricatorTokenReceiverInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PonderAnswerCommentController' => 'PonderController',
     'PonderAnswerEditController' => 'PonderController',
     'PonderAnswerEditor' => 'PonderEditor',
     'PonderAnswerHasVotingUserEdgeType' => 'PhabricatorEdgeType',
     'PonderAnswerHistoryController' => 'PonderController',
     'PonderAnswerPHIDType' => 'PhabricatorPHIDType',
     'PonderAnswerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PonderAnswerSaveController' => 'PonderController',
     'PonderAnswerTransaction' => 'PhabricatorApplicationTransaction',
     'PonderAnswerTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'PonderAnswerTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PonderController' => 'PhabricatorController',
     'PonderDAO' => 'PhabricatorLiskDAO',
     'PonderEditor' => 'PhabricatorApplicationTransactionEditor',
     'PonderQuestion' => array(
       'PonderDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorMarkupInterface',
       'PonderVotableInterface',
       'PhabricatorSubscribableInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorTokenReceiverInterface',
       'PhabricatorProjectInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PonderQuestionCommentController' => 'PonderController',
     'PonderQuestionEditController' => 'PonderController',
     'PonderQuestionEditor' => 'PonderEditor',
     'PonderQuestionHasVotingUserEdgeType' => 'PhabricatorEdgeType',
     'PonderQuestionHistoryController' => 'PonderController',
     'PonderQuestionListController' => 'PonderController',
     'PonderQuestionMailReceiver' => 'PhabricatorObjectMailReceiver',
     'PonderQuestionPHIDType' => 'PhabricatorPHIDType',
     'PonderQuestionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PonderQuestionReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'PonderQuestionSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PonderQuestionStatus' => 'PonderConstants',
     'PonderQuestionStatusController' => 'PonderController',
     'PonderQuestionTransaction' => 'PhabricatorApplicationTransaction',
     'PonderQuestionTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'PonderQuestionTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PonderQuestionViewController' => 'PonderController',
     'PonderRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'PonderSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PonderSearchIndexer' => 'PhabricatorSearchDocumentIndexer',
     'PonderTransactionFeedStory' => 'PhabricatorApplicationTransactionFeedStory',
     'PonderVotableView' => 'AphrontView',
     'PonderVote' => 'PonderConstants',
     'PonderVoteEditor' => 'PhabricatorEditor',
     'PonderVoteSaveController' => 'PonderController',
     'PonderVotingUserHasAnswerEdgeType' => 'PhabricatorEdgeType',
     'PonderVotingUserHasQuestionEdgeType' => 'PhabricatorEdgeType',
     'ProjectAddProjectsEmailCommand' => 'MetaMTAEmailTransactionCommand',
     'ProjectCanLockProjectsCapability' => 'PhabricatorPolicyCapability',
     'ProjectConduitAPIMethod' => 'ConduitAPIMethod',
     'ProjectCreateConduitAPIMethod' => 'ProjectConduitAPIMethod',
     'ProjectCreateProjectsCapability' => 'PhabricatorPolicyCapability',
     'ProjectDefaultEditCapability' => 'PhabricatorPolicyCapability',
     'ProjectDefaultJoinCapability' => 'PhabricatorPolicyCapability',
     'ProjectDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'ProjectQueryConduitAPIMethod' => 'ProjectConduitAPIMethod',
     'ProjectRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'ProjectRemarkupRuleTestCase' => 'PhabricatorTestCase',
     'QueryFormattingTestCase' => 'PhabricatorTestCase',
     'ReleephAuthorFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephBranch' => array(
       'ReleephDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
     ),
     'ReleephBranchAccessController' => 'ReleephBranchController',
     'ReleephBranchCommitFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephBranchController' => 'ReleephController',
     'ReleephBranchCreateController' => 'ReleephProductController',
     'ReleephBranchEditController' => 'ReleephBranchController',
     'ReleephBranchEditor' => 'PhabricatorEditor',
     'ReleephBranchHistoryController' => 'ReleephBranchController',
     'ReleephBranchNamePreviewController' => 'ReleephController',
     'ReleephBranchPHIDType' => 'PhabricatorPHIDType',
     'ReleephBranchPreviewView' => 'AphrontFormControl',
     'ReleephBranchQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'ReleephBranchSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'ReleephBranchTransaction' => 'PhabricatorApplicationTransaction',
     'ReleephBranchTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'ReleephBranchViewController' => array(
       'ReleephBranchController',
       'PhabricatorApplicationSearchResultsControllerInterface',
     ),
     'ReleephCommitFinderException' => 'Exception',
     'ReleephCommitMessageFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephConduitAPIMethod' => 'ConduitAPIMethod',
     'ReleephController' => 'PhabricatorController',
     'ReleephDAO' => 'PhabricatorLiskDAO',
     'ReleephDefaultFieldSelector' => 'ReleephFieldSelector',
     'ReleephDependsOnFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephDiffChurnFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephDiffMessageFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephDiffSizeFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephFieldParseException' => 'Exception',
     'ReleephFieldSpecification' => array(
       'PhabricatorCustomField',
       'PhabricatorMarkupInterface',
     ),
     'ReleephGetBranchesConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'ReleephIntentFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephLevelFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephOriginalCommitFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephProductActionController' => 'ReleephProductController',
     'ReleephProductController' => 'ReleephController',
     'ReleephProductCreateController' => 'ReleephProductController',
     'ReleephProductEditController' => 'ReleephProductController',
     'ReleephProductEditor' => 'PhabricatorApplicationTransactionEditor',
     'ReleephProductHistoryController' => 'ReleephProductController',
     'ReleephProductListController' => 'ReleephController',
     'ReleephProductPHIDType' => 'PhabricatorPHIDType',
     'ReleephProductQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'ReleephProductSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'ReleephProductTransaction' => 'PhabricatorApplicationTransaction',
     'ReleephProductTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'ReleephProductViewController' => array(
       'ReleephProductController',
       'PhabricatorApplicationSearchResultsControllerInterface',
     ),
     'ReleephProject' => array(
       'ReleephDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
     ),
     'ReleephProjectInfoConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'ReleephQueryBranchesConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'ReleephQueryProductsConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'ReleephQueryRequestsConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'ReleephReasonFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephRequest' => array(
       'ReleephDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorCustomFieldInterface',
     ),
     'ReleephRequestActionController' => 'ReleephRequestController',
     'ReleephRequestCommentController' => 'ReleephRequestController',
     'ReleephRequestConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'ReleephRequestController' => 'ReleephController',
     'ReleephRequestDifferentialCreateController' => 'ReleephController',
     'ReleephRequestEditController' => 'ReleephBranchController',
     'ReleephRequestMailReceiver' => 'PhabricatorObjectMailReceiver',
     'ReleephRequestPHIDType' => 'PhabricatorPHIDType',
     'ReleephRequestQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'ReleephRequestReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'ReleephRequestSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'ReleephRequestTransaction' => 'PhabricatorApplicationTransaction',
     'ReleephRequestTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'ReleephRequestTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'ReleephRequestTransactionalEditor' => 'PhabricatorApplicationTransactionEditor',
     'ReleephRequestTypeaheadControl' => 'AphrontFormControl',
     'ReleephRequestTypeaheadController' => 'PhabricatorTypeaheadDatasourceController',
     'ReleephRequestView' => 'AphrontView',
     'ReleephRequestViewController' => 'ReleephBranchController',
     'ReleephRequestorFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephRevisionFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephSeverityFieldSpecification' => 'ReleephLevelFieldSpecification',
     'ReleephSummaryFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephWorkCanPushConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'ReleephWorkGetAuthorInfoConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'ReleephWorkGetBranchCommitMessageConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'ReleephWorkGetBranchConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'ReleephWorkGetCommitMessageConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'ReleephWorkNextRequestConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'ReleephWorkRecordConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'ReleephWorkRecordPickStatusConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'RemarkupProcessConduitAPIMethod' => 'ConduitAPIMethod',
     'RepositoryConduitAPIMethod' => 'ConduitAPIMethod',
     'RepositoryCreateConduitAPIMethod' => 'RepositoryConduitAPIMethod',
     'RepositoryQueryConduitAPIMethod' => 'RepositoryConduitAPIMethod',
     'ShellLogView' => 'AphrontView',
     'SlowvoteConduitAPIMethod' => 'ConduitAPIMethod',
     'SlowvoteEmbedView' => 'AphrontView',
     'SlowvoteInfoConduitAPIMethod' => 'SlowvoteConduitAPIMethod',
     'SlowvoteRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'TokenConduitAPIMethod' => 'ConduitAPIMethod',
     'TokenGiveConduitAPIMethod' => 'TokenConduitAPIMethod',
     'TokenGivenConduitAPIMethod' => 'TokenConduitAPIMethod',
     'TokenQueryConduitAPIMethod' => 'TokenConduitAPIMethod',
     'UserAddStatusConduitAPIMethod' => 'UserConduitAPIMethod',
     'UserConduitAPIMethod' => 'ConduitAPIMethod',
     'UserDisableConduitAPIMethod' => 'UserConduitAPIMethod',
     'UserEnableConduitAPIMethod' => 'UserConduitAPIMethod',
     'UserFindConduitAPIMethod' => 'UserConduitAPIMethod',
     'UserQueryConduitAPIMethod' => 'UserConduitAPIMethod',
     'UserRemoveStatusConduitAPIMethod' => 'UserConduitAPIMethod',
     'UserWhoAmIConduitAPIMethod' => 'UserConduitAPIMethod',
   ),
 ));
diff --git a/src/aphront/AphrontRequest.php b/src/aphront/AphrontRequest.php
index 197e5a437c..ab6e0d9820 100644
--- a/src/aphront/AphrontRequest.php
+++ b/src/aphront/AphrontRequest.php
@@ -1,763 +1,763 @@
 <?php
 
 /**
  * @task data     Accessing Request Data
  * @task cookie   Managing Cookies
  * @task cluster  Working With a Phabricator Cluster
  */
 final class AphrontRequest {
 
   // NOTE: These magic request-type parameters are automatically included in
   // certain requests (e.g., by phabricator_form(), JX.Request,
   // JX.Workflow, and ConduitClient) and help us figure out what sort of
   // response the client expects.
 
   const TYPE_AJAX = '__ajax__';
   const TYPE_FORM = '__form__';
   const TYPE_CONDUIT = '__conduit__';
   const TYPE_WORKFLOW = '__wflow__';
   const TYPE_CONTINUE = '__continue__';
   const TYPE_PREVIEW = '__preview__';
   const TYPE_HISEC = '__hisec__';
   const TYPE_QUICKSAND = '__quicksand__';
 
   private $host;
   private $path;
   private $requestData;
   private $user;
   private $applicationConfiguration;
   private $uriData;
 
   final public function __construct($host, $path) {
     $this->host = $host;
     $this->path = $path;
   }
 
   final public function setURIMap(array $uri_data) {
     $this->uriData = $uri_data;
     return $this;
   }
 
   final public function getURIMap() {
     return $this->uriData;
   }
 
   final public function getURIData($key, $default = null) {
     return idx($this->uriData, $key, $default);
   }
 
   final public function setApplicationConfiguration(
     $application_configuration) {
     $this->applicationConfiguration = $application_configuration;
     return $this;
   }
 
   final public function getApplicationConfiguration() {
     return $this->applicationConfiguration;
   }
 
   final public function setPath($path) {
     $this->path = $path;
     return $this;
   }
 
   final public function getPath() {
     return $this->path;
   }
 
   final public function getHost() {
     // The "Host" header may include a port number, or may be a malicious
     // header in the form "realdomain.com:ignored@evil.com". Invoke the full
     // parser to extract the real domain correctly. See here for coverage of
     // a similar issue in Django:
     //
     //  https://www.djangoproject.com/weblog/2012/oct/17/security/
     $uri = new PhutilURI('http://'.$this->host);
     return $uri->getDomain();
   }
 
 
 /* -(  Accessing Request Data  )--------------------------------------------- */
 
 
   /**
    * @task data
    */
   final public function setRequestData(array $request_data) {
     $this->requestData = $request_data;
     return $this;
   }
 
 
   /**
    * @task data
    */
   final public function getRequestData() {
     return $this->requestData;
   }
 
 
   /**
    * @task data
    */
   final public function getInt($name, $default = null) {
     if (isset($this->requestData[$name])) {
       return (int)$this->requestData[$name];
     } else {
       return $default;
     }
   }
 
 
   /**
    * @task data
    */
   final public function getBool($name, $default = null) {
     if (isset($this->requestData[$name])) {
       if ($this->requestData[$name] === 'true') {
         return true;
       } else if ($this->requestData[$name] === 'false') {
         return false;
       } else {
         return (bool)$this->requestData[$name];
       }
     } else {
       return $default;
     }
   }
 
 
   /**
    * @task data
    */
   final public function getStr($name, $default = null) {
     if (isset($this->requestData[$name])) {
       $str = (string)$this->requestData[$name];
       // Normalize newline craziness.
       $str = str_replace(
         array("\r\n", "\r"),
         array("\n", "\n"),
         $str);
       return $str;
     } else {
       return $default;
     }
   }
 
 
   /**
    * @task data
    */
   final public function getArr($name, $default = array()) {
     if (isset($this->requestData[$name]) &&
         is_array($this->requestData[$name])) {
       return $this->requestData[$name];
     } else {
       return $default;
     }
   }
 
 
   /**
    * @task data
    */
   final public function getStrList($name, $default = array()) {
     if (!isset($this->requestData[$name])) {
       return $default;
     }
     $list = $this->getStr($name);
     $list = preg_split('/[\s,]+/', $list, $limit = -1, PREG_SPLIT_NO_EMPTY);
     return $list;
   }
 
 
   /**
    * @task data
    */
   final public function getExists($name) {
     return array_key_exists($name, $this->requestData);
   }
 
   final public function getFileExists($name) {
     return isset($_FILES[$name]) &&
            (idx($_FILES[$name], 'error') !== UPLOAD_ERR_NO_FILE);
   }
 
   final public function isHTTPGet() {
     return ($_SERVER['REQUEST_METHOD'] == 'GET');
   }
 
   final public function isHTTPPost() {
     return ($_SERVER['REQUEST_METHOD'] == 'POST');
   }
 
   final public function isAjax() {
     return $this->getExists(self::TYPE_AJAX) && !$this->isQuicksand();
   }
 
   final public function isWorkflow() {
     return $this->getExists(self::TYPE_WORKFLOW) && !$this->isQuicksand();
   }
 
   final public function isQuicksand() {
     return $this->getExists(self::TYPE_QUICKSAND);
   }
 
   final public function isConduit() {
     return $this->getExists(self::TYPE_CONDUIT);
   }
 
   public static function getCSRFTokenName() {
     return '__csrf__';
   }
 
   public static function getCSRFHeaderName() {
     return 'X-Phabricator-Csrf';
   }
 
   final public function validateCSRF() {
     $token_name = self::getCSRFTokenName();
     $token = $this->getStr($token_name);
 
     // No token in the request, check the HTTP header which is added for Ajax
     // requests.
     if (empty($token)) {
       $token = self::getHTTPHeader(self::getCSRFHeaderName());
     }
 
     $valid = $this->getUser()->validateCSRFToken($token);
     if (!$valid) {
 
       // Add some diagnostic details so we can figure out if some CSRF issues
       // are JS problems or people accessing Ajax URIs directly with their
       // browsers.
       $more_info = array();
 
       if ($this->isAjax()) {
         $more_info[] = pht('This was an Ajax request.');
       } else {
         $more_info[] = pht('This was a Web request.');
       }
 
       if ($token) {
         $more_info[] = pht('This request had an invalid CSRF token.');
       } else {
         $more_info[] = pht('This request had no CSRF token.');
       }
 
       // Give a more detailed explanation of how to avoid the exception
       // in developer mode.
       if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
         // TODO: Clean this up, see T1921.
         $more_info[] =
           "To avoid this error, use phabricator_form() to construct forms. ".
           "If you are already using phabricator_form(), make sure the form ".
           "'action' uses a relative URI (i.e., begins with a '/'). Forms ".
           "using absolute URIs do not include CSRF tokens, to prevent ".
           "leaking tokens to external sites.\n\n".
           "If this page performs writes which do not require CSRF ".
           "protection (usually, filling caches or logging), you can use ".
           "AphrontWriteGuard::beginScopedUnguardedWrites() to temporarily ".
           "bypass CSRF protection while writing. You should use this only ".
           "for writes which can not be protected with normal CSRF ".
           "mechanisms.\n\n".
           "Some UI elements (like PhabricatorActionListView) also have ".
           "methods which will allow you to render links as forms (like ".
           "setRenderAsForm(true)).";
       }
 
       // This should only be able to happen if you load a form, pull your
       // internet for 6 hours, and then reconnect and immediately submit,
       // but give the user some indication of what happened since the workflow
       // is incredibly confusing otherwise.
       throw new AphrontCSRFException(
         pht(
           "You are trying to save some data to Phabricator, but the request ".
           "your browser made included an incorrect token. Reload the page ".
           "and try again. You may need to clear your cookies.\n\n%s",
           implode("\n", $more_info)));
     }
 
     return true;
   }
 
   final public function isFormPost() {
     $post = $this->getExists(self::TYPE_FORM) &&
             !$this->getExists(self::TYPE_HISEC) &&
             $this->isHTTPPost();
 
     if (!$post) {
       return false;
     }
 
     return $this->validateCSRF();
   }
 
   final public function isFormOrHisecPost() {
     $post = $this->getExists(self::TYPE_FORM) &&
             $this->isHTTPPost();
 
     if (!$post) {
       return false;
     }
 
     return $this->validateCSRF();
   }
 
 
   final public function setCookiePrefix($prefix) {
     $this->cookiePrefix = $prefix;
     return $this;
   }
 
   final private function getPrefixedCookieName($name) {
     if (strlen($this->cookiePrefix)) {
       return $this->cookiePrefix.'_'.$name;
     } else {
       return $name;
     }
   }
 
   final public function getCookie($name, $default = null) {
     $name = $this->getPrefixedCookieName($name);
     $value = idx($_COOKIE, $name, $default);
 
     // Internally, PHP deletes cookies by setting them to the value 'deleted'
     // with an expiration date in the past.
 
     // At least in Safari, the browser may send this cookie anyway in some
     // circumstances. After logging out, the 302'd GET to /login/ consistently
     // includes deleted cookies on my local install. If a cookie value is
     // literally 'deleted', pretend it does not exist.
 
     if ($value === 'deleted') {
       return null;
     }
 
     return $value;
   }
 
   final public function clearCookie($name) {
     $this->setCookieWithExpiration($name, '', time() - (60 * 60 * 24 * 30));
     unset($_COOKIE[$name]);
   }
 
   /**
    * Get the domain which cookies should be set on for this request, or null
    * if the request does not correspond to a valid cookie domain.
    *
    * @return PhutilURI|null   Domain URI, or null if no valid domain exists.
    *
    * @task cookie
    */
   private function getCookieDomainURI() {
     if (PhabricatorEnv::getEnvConfig('security.require-https') &&
         !$this->isHTTPS()) {
       return null;
     }
 
     $host = $this->getHost();
 
     // If there's no base domain configured, just use whatever the request
     // domain is. This makes setup easier, and we'll tell administrators to
     // configure a base domain during the setup process.
     $base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
     if (!strlen($base_uri)) {
       return new PhutilURI('http://'.$host.'/');
     }
 
     $alternates = PhabricatorEnv::getEnvConfig('phabricator.allowed-uris');
     $allowed_uris = array_merge(
       array($base_uri),
       $alternates);
 
     foreach ($allowed_uris as $allowed_uri) {
       $uri = new PhutilURI($allowed_uri);
       if ($uri->getDomain() == $host) {
         return $uri;
       }
     }
 
     return null;
   }
 
   /**
    * Determine if security policy rules will allow cookies to be set when
    * responding to the request.
    *
    * @return bool True if setCookie() will succeed. If this method returns
    *              false, setCookie() will throw.
    *
    * @task cookie
    */
   final public function canSetCookies() {
     return (bool)$this->getCookieDomainURI();
   }
 
 
   /**
    * Set a cookie which does not expire for a long time.
    *
    * To set a temporary cookie, see @{method:setTemporaryCookie}.
    *
    * @param string  Cookie name.
    * @param string  Cookie value.
    * @return this
    * @task cookie
    */
   final public function setCookie($name, $value) {
     $far_future = time() + (60 * 60 * 24 * 365 * 5);
     return $this->setCookieWithExpiration($name, $value, $far_future);
   }
 
 
   /**
    * Set a cookie which expires soon.
    *
    * To set a durable cookie, see @{method:setCookie}.
    *
    * @param string  Cookie name.
    * @param string  Cookie value.
    * @return this
    * @task cookie
    */
   final public function setTemporaryCookie($name, $value) {
     return $this->setCookieWithExpiration($name, $value, 0);
   }
 
 
   /**
    * Set a cookie with a given expiration policy.
    *
    * @param string  Cookie name.
    * @param string  Cookie value.
    * @param int     Epoch timestamp for cookie expiration.
    * @return this
    * @task cookie
    */
   final private function setCookieWithExpiration(
     $name,
     $value,
     $expire) {
 
     $is_secure = false;
 
     $base_domain_uri = $this->getCookieDomainURI();
     if (!$base_domain_uri) {
       $configured_as = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
       $accessed_as = $this->getHost();
 
       throw new Exception(
         pht(
           'This Phabricator install is configured as "%s", but you are '.
           'using the domain name "%s" to access a page which is trying to '.
           'set a cookie. Acccess Phabricator on the configured primary '.
           'domain or a configured alternate domain. Phabricator will not '.
           'set cookies on other domains for security reasons.',
           $configured_as,
           $accessed_as));
     }
 
     $base_domain = $base_domain_uri->getDomain();
     $is_secure = ($base_domain_uri->getProtocol() == 'https');
 
     $name = $this->getPrefixedCookieName($name);
 
     if (php_sapi_name() == 'cli') {
       // Do nothing, to avoid triggering "Cannot modify header information"
       // warnings.
 
       // TODO: This is effectively a test for whether we're running in a unit
       // test or not. Move this actual call to HTTPSink?
     } else {
       setcookie(
         $name,
         $value,
         $expire,
         $path = '/',
         $base_domain,
         $is_secure,
         $http_only = true);
     }
 
     $_COOKIE[$name] = $value;
 
     return $this;
   }
 
   final public function setUser($user) {
     $this->user = $user;
     return $this;
   }
 
   final public function getUser() {
     return $this->user;
   }
 
   final public function getViewer() {
     return $this->user;
   }
 
   final public function getRequestURI() {
     $get = $_GET;
     unset($get['__path__']);
     $path = phutil_escape_uri($this->getPath());
     return id(new PhutilURI($path))->setQueryParams($get);
   }
 
   final public function isDialogFormPost() {
     return $this->isFormPost() && $this->getStr('__dialog__');
   }
 
   final public function getRemoteAddr() {
     return $_SERVER['REMOTE_ADDR'];
   }
 
   public function isHTTPS() {
     if (empty($_SERVER['HTTPS'])) {
       return false;
     }
     if (!strcasecmp($_SERVER['HTTPS'], 'off')) {
       return false;
     }
     return true;
   }
 
   public function isContinueRequest() {
     return $this->isFormPost() && $this->getStr('__continue__');
   }
 
   public function isPreviewRequest() {
     return $this->isFormPost() && $this->getStr('__preview__');
   }
 
   /**
    * Get application request parameters in a flattened form suitable for
    * inclusion in an HTTP request, excluding parameters with special meanings.
    * This is primarily useful if you want to ask the user for more input and
    * then resubmit their request.
    *
    * @return  dict<string, string>  Original request parameters.
    */
   public function getPassthroughRequestParameters($include_quicksand = false) {
     return self::flattenData(
       $this->getPassthroughRequestData($include_quicksand));
   }
 
   /**
    * Get request data other than "magic" parameters.
    *
    * @return dict<string, wild> Request data, with magic filtered out.
    */
   public function getPassthroughRequestData($include_quicksand = false) {
     $data = $this->getRequestData();
 
     // Remove magic parameters like __dialog__ and __ajax__.
     foreach ($data as $key => $value) {
       if ($include_quicksand && $key == self::TYPE_QUICKSAND) {
         continue;
       }
       if (!strncmp($key, '__', 2)) {
         unset($data[$key]);
       }
     }
 
     return $data;
   }
 
 
   /**
    * Flatten an array of key-value pairs (possibly including arrays as values)
    * into a list of key-value pairs suitable for submitting via HTTP request
    * (with arrays flattened).
    *
    * @param   dict<string, wild>    Data to flatten.
    * @return  dict<string, string>  Flat data suitable for inclusion in an HTTP
    *                                request.
    */
   public static function flattenData(array $data) {
     $result = array();
     foreach ($data as $key => $value) {
       if (is_array($value)) {
         foreach (self::flattenData($value) as $fkey => $fvalue) {
           $fkey = '['.preg_replace('/(?=\[)|$/', ']', $fkey, $limit = 1);
           $result[$key.$fkey] = $fvalue;
         }
       } else {
         $result[$key] = (string)$value;
       }
     }
 
     ksort($result);
 
     return $result;
   }
 
 
   /**
    * Read the value of an HTTP header from `$_SERVER`, or a similar datasource.
    *
    * This function accepts a canonical header name, like `"Accept-Encoding"`,
    * and looks up the appropriate value in `$_SERVER` (in this case,
    * `"HTTP_ACCEPT_ENCODING"`).
    *
    * @param   string        Canonical header name, like `"Accept-Encoding"`.
    * @param   wild          Default value to return if header is not present.
    * @param   array?        Read this instead of `$_SERVER`.
    * @return  string|wild   Header value if present, or `$default` if not.
    */
   public static function getHTTPHeader($name, $default = null, $data = null) {
     // PHP mangles HTTP headers by uppercasing them and replacing hyphens with
     // underscores, then prepending 'HTTP_'.
     $php_index = strtoupper($name);
     $php_index = str_replace('-', '_', $php_index);
 
     $try_names = array();
 
     $try_names[] = 'HTTP_'.$php_index;
     if ($php_index == 'CONTENT_TYPE' || $php_index == 'CONTENT_LENGTH') {
       // These headers may be available under alternate names. See
       // http://www.php.net/manual/en/reserved.variables.server.php#110763
       $try_names[] = $php_index;
     }
 
     if ($data === null) {
       $data = $_SERVER;
     }
 
     foreach ($try_names as $try_name) {
       if (array_key_exists($try_name, $data)) {
         return $data[$try_name];
       }
     }
 
     return $default;
   }
 
 
 /* -(  Working With a Phabricator Cluster  )--------------------------------- */
 
 
   /**
    * Is this a proxied request originating from within the Phabricator cluster?
    *
    * IMPORTANT: This means the request is dangerous!
    *
    * These requests are **more dangerous** than normal requests (they can not
    * be safely proxied, because proxying them may cause a loop). Cluster
    * requests are not guaranteed to come from a trusted source, and should
    * never be treated as safer than normal requests. They are strictly less
    * safe.
    */
   public function isProxiedClusterRequest() {
-    return (bool)AphrontRequest::getHTTPHeader('X-Phabricator-Cluster');
+    return (bool)self::getHTTPHeader('X-Phabricator-Cluster');
   }
 
 
   /**
    * Build a new @{class:HTTPSFuture} which proxies this request to another
    * node in the cluster.
    *
    * IMPORTANT: This is very dangerous!
    *
    * The future forwards authentication information present in the request.
    * Proxied requests must only be sent to trusted hosts. (We attempt to
    * enforce this.)
    *
    * This is not a general-purpose proxying method; it is a specialized
    * method with niche applications and severe security implications.
    *
    * @param string URI identifying the host we are proxying the request to.
    * @return HTTPSFuture New proxy future.
    *
    * @phutil-external-symbol class PhabricatorStartup
    */
   public function newClusterProxyFuture($uri) {
     $uri = new PhutilURI($uri);
 
     $domain = $uri->getDomain();
     $ip = gethostbyname($domain);
     if (!$ip) {
       throw new Exception(
         pht(
           'Unable to resolve domain "%s"!',
           $domain));
     }
 
     if (!PhabricatorEnv::isClusterAddress($ip)) {
       throw new Exception(
         pht(
           'Refusing to proxy a request to IP address ("%s") which is not '.
           'in the cluster address block (this address was derived by '.
           'resolving the domain "%s").',
           $ip,
           $domain));
     }
 
     $uri->setPath($this->getPath());
     $uri->setQueryParams(self::flattenData($_GET));
 
     $input = PhabricatorStartup::getRawInput();
 
     $future = id(new HTTPSFuture($uri))
       ->addHeader('Host', self::getHost())
       ->addHeader('X-Phabricator-Cluster', true)
       ->setMethod($_SERVER['REQUEST_METHOD'])
       ->write($input);
 
     if (isset($_SERVER['PHP_AUTH_USER'])) {
       $future->setHTTPBasicAuthCredentials(
         $_SERVER['PHP_AUTH_USER'],
         new PhutilOpaqueEnvelope(idx($_SERVER, 'PHP_AUTH_PW', '')));
     }
 
     $headers = array();
     $seen = array();
 
     // NOTE: apache_request_headers() might provide a nicer way to do this,
     // but isn't available under FCGI until PHP 5.4.0.
     foreach ($_SERVER as $key => $value) {
       if (preg_match('/^HTTP_/', $key)) {
         // Unmangle the header as best we can.
         $key = str_replace('_', ' ', $key);
         $key = strtolower($key);
         $key = ucwords($key);
         $key = str_replace(' ', '-', $key);
 
         $headers[] = array($key, $value);
         $seen[$key] = true;
       }
     }
 
     // In some situations, this may not be mapped into the HTTP_X constants.
     // CONTENT_LENGTH is similarly affected, but we trust cURL to take care
     // of that if it matters, since we're handing off a request body.
     if (empty($seen['Content-Type'])) {
       if (isset($_SERVER['CONTENT_TYPE'])) {
         $headers[] = array('Content-Type', $_SERVER['CONTENT_TYPE']);
       }
     }
 
     foreach ($headers as $header) {
       list($key, $value) = $header;
       switch ($key) {
         case 'Host':
         case 'Authorization':
           // Don't forward these headers, we've already handled them elsewhere.
           unset($headers[$key]);
           break;
         default:
           break;
       }
     }
 
     foreach ($headers as $header) {
       list($key, $value) = $header;
       $future->addHeader($key, $value);
     }
 
     return $future;
   }
 
 
 }
diff --git a/src/aphront/response/AphrontProxyResponse.php b/src/aphront/response/AphrontProxyResponse.php
index 7e27ea2b1b..175f2d2c53 100644
--- a/src/aphront/response/AphrontProxyResponse.php
+++ b/src/aphront/response/AphrontProxyResponse.php
@@ -1,71 +1,74 @@
 <?php
 
 /**
  * Base class for responses which augment other types of responses. For example,
  * a response might be substantially an Ajax response, but add structure to the
  * response content. It can do this by extending @{class:AphrontProxyResponse},
  * instantiating an @{class:AphrontAjaxResponse} in @{method:buildProxy}, and
  * then constructing a real @{class:AphrontAjaxResponse} in
  * @{method:reduceProxyResponse}.
  */
 abstract class AphrontProxyResponse extends AphrontResponse {
 
   private $proxy;
 
   protected function getProxy() {
     if (!$this->proxy) {
       $this->proxy = $this->buildProxy();
     }
     return $this->proxy;
   }
 
   public function setRequest($request) {
     $this->getProxy()->setRequest($request);
     return $this;
   }
 
   public function getRequest() {
     return $this->getProxy()->getRequest();
   }
 
   public function getHeaders() {
     return $this->getProxy()->getHeaders();
   }
 
   public function setCacheDurationInSeconds($duration) {
     $this->getProxy()->setCacheDurationInSeconds($duration);
     return $this;
   }
 
   public function setLastModified($epoch_timestamp) {
     $this->getProxy()->setLastModified($epoch_timestamp);
     return $this;
   }
 
   public function setHTTPResponseCode($code) {
     $this->getProxy()->setHTTPResponseCode($code);
     return $this;
   }
 
   public function getHTTPResponseCode() {
     return $this->getProxy()->getHTTPResponseCode();
   }
 
   public function setFrameable($frameable) {
     $this->getProxy()->setFrameable($frameable);
     return $this;
   }
 
   public function getCacheHeaders() {
     return $this->getProxy()->getCacheHeaders();
   }
 
   abstract protected function buildProxy();
   abstract public function reduceProxyResponse();
 
   final public function buildResponseString() {
     throw new Exception(
-      'AphrontProxyResponse must implement reduceProxyResponse().');
+      pht(
+        '%s must implement %s.',
+        __CLASS__,
+        'reduceProxyResponse()'));
   }
 
 }
diff --git a/src/aphront/response/AphrontResponse.php b/src/aphront/response/AphrontResponse.php
index 0bbe22b5ec..b1e72f5529 100644
--- a/src/aphront/response/AphrontResponse.php
+++ b/src/aphront/response/AphrontResponse.php
@@ -1,233 +1,233 @@
 <?php
 
 abstract class AphrontResponse {
 
   private $request;
   private $cacheable = false;
   private $responseCode = 200;
   private $lastModified = null;
 
   protected $frameable;
 
   public function setRequest($request) {
     $this->request = $request;
     return $this;
   }
 
   public function getRequest() {
     return $this->request;
   }
 
 
 /* -(  Content  )------------------------------------------------------------ */
 
 
   public function getContentIterator() {
     return array($this->buildResponseString());
   }
 
   public function buildResponseString() {
     throw new PhutilMethodNotImplementedException();
   }
 
 
 /* -(  Metadata  )----------------------------------------------------------- */
 
 
   public function getHeaders() {
     $headers = array();
     if (!$this->frameable) {
       $headers[] = array('X-Frame-Options', 'Deny');
     }
 
     if ($this->getRequest() && $this->getRequest()->isHTTPS()) {
       $hsts_key = 'security.strict-transport-security';
       $use_hsts = PhabricatorEnv::getEnvConfig($hsts_key);
       if ($use_hsts) {
         $duration = phutil_units('365 days in seconds');
       } else {
         // If HSTS has been disabled, tell browsers to turn it off. This may
         // not be effective because we can only disable it over a valid HTTPS
         // connection, but it best represents the configured intent.
         $duration = 0;
       }
 
       $headers[] = array(
         'Strict-Transport-Security',
         "max-age={$duration}; includeSubdomains; preload",
       );
     }
 
     return $headers;
   }
 
   public function setCacheDurationInSeconds($duration) {
     $this->cacheable = $duration;
     return $this;
   }
 
   public function setLastModified($epoch_timestamp) {
     $this->lastModified = $epoch_timestamp;
     return $this;
   }
 
   public function setHTTPResponseCode($code) {
     $this->responseCode = $code;
     return $this;
   }
 
   public function getHTTPResponseCode() {
     return $this->responseCode;
   }
 
   public function getHTTPResponseMessage() {
     switch ($this->getHTTPResponseCode()) {
       case 100: return 'Continue';
       case 101: return 'Switching Protocols';
       case 200: return 'OK';
       case 201: return 'Created';
       case 202: return 'Accepted';
       case 203: return 'Non-Authoritative Information';
       case 204: return 'No Content';
       case 205: return 'Reset Content';
       case 206: return 'Partial Content';
       case 300: return 'Multiple Choices';
       case 301: return 'Moved Permanently';
       case 302: return 'Found';
       case 303: return 'See Other';
       case 304: return 'Not Modified';
       case 305: return 'Use Proxy';
       case 306: return 'Switch Proxy';
       case 307: return 'Temporary Redirect';
       case 400: return 'Bad Request';
       case 401: return 'Unauthorized';
       case 402: return 'Payment Required';
       case 403: return 'Forbidden';
       case 404: return 'Not Found';
       case 405: return 'Method Not Allowed';
       case 406: return 'Not Acceptable';
       case 407: return 'Proxy Authentication Required';
       case 408: return 'Request Timeout';
       case 409: return 'Conflict';
       case 410: return 'Gone';
       case 411: return 'Length Required';
       case 412: return 'Precondition Failed';
       case 413: return 'Request Entity Too Large';
       case 414: return 'Request-URI Too Long';
       case 415: return 'Unsupported Media Type';
       case 416: return 'Requested Range Not Satisfiable';
       case 417: return 'Expectation Failed';
       case 418: return "I'm a teapot";
       case 426: return 'Upgrade Required';
       case 500: return 'Internal Server Error';
       case 501: return 'Not Implemented';
       case 502: return 'Bad Gateway';
       case 503: return 'Service Unavailable';
       case 504: return 'Gateway Timeout';
       case 505: return 'HTTP Version Not Supported';
       default:  return '';
     }
   }
 
   public function setFrameable($frameable) {
     $this->frameable = $frameable;
     return $this;
   }
 
   public static function processValueForJSONEncoding(&$value, $key) {
     if ($value instanceof PhutilSafeHTMLProducerInterface) {
       // This renders the producer down to PhutilSafeHTML, which will then
       // be simplified into a string below.
       $value = hsprintf('%s', $value);
     }
 
     if ($value instanceof PhutilSafeHTML) {
       // TODO: Javelin supports implicity conversion of '__html' objects to
       // JX.HTML, but only for Ajax responses, not behaviors. Just leave things
       // as they are for now (where behaviors treat responses as HTML or plain
       // text at their discretion).
       $value = $value->getHTMLContent();
     }
   }
 
   public static function encodeJSONForHTTPResponse(array $object) {
 
     array_walk_recursive(
       $object,
-      array('AphrontResponse', 'processValueForJSONEncoding'));
+      array(__CLASS__, 'processValueForJSONEncoding'));
 
     $response = json_encode($object);
 
     // Prevent content sniffing attacks by encoding "<" and ">", so browsers
     // won't try to execute the document as HTML even if they ignore
     // Content-Type and X-Content-Type-Options. See T865.
     $response = str_replace(
       array('<', '>'),
       array('\u003c', '\u003e'),
       $response);
 
     return $response;
   }
 
   protected function addJSONShield($json_response) {
     // Add a shield to prevent "JSON Hijacking" attacks where an attacker
     // requests a JSON response using a normal <script /> tag and then uses
     // Object.prototype.__defineSetter__() or similar to read response data.
     // This header causes the browser to loop infinitely instead of handing over
     // sensitive data.
 
     $shield = 'for (;;);';
 
     $response = $shield.$json_response;
 
     return $response;
   }
 
   public function getCacheHeaders() {
     $headers = array();
     if ($this->cacheable) {
       $headers[] = array(
         'Expires',
         $this->formatEpochTimestampForHTTPHeader(time() + $this->cacheable),
       );
     } else {
       $headers[] = array(
         'Cache-Control',
         'private, no-cache, no-store, must-revalidate',
       );
       $headers[] = array(
         'Pragma',
         'no-cache',
       );
       $headers[] = array(
         'Expires',
         'Sat, 01 Jan 2000 00:00:00 GMT',
       );
     }
 
     if ($this->lastModified) {
       $headers[] = array(
         'Last-Modified',
         $this->formatEpochTimestampForHTTPHeader($this->lastModified),
       );
     }
 
     // IE has a feature where it may override an explicit Content-Type
     // declaration by inferring a content type. This can be a security risk
     // and we always explicitly transmit the correct Content-Type header, so
     // prevent IE from using inferred content types. This only offers protection
     // on recent versions of IE; IE6/7 and Opera currently ignore this header.
     $headers[] = array('X-Content-Type-Options', 'nosniff');
 
     return $headers;
   }
 
   private function formatEpochTimestampForHTTPHeader($epoch_timestamp) {
     return gmdate('D, d M Y H:i:s', $epoch_timestamp).' GMT';
   }
 
   public function didCompleteWrite($aborted) {
     return;
   }
 
 }
diff --git a/src/applications/audit/constants/PhabricatorAuditStatusConstants.php b/src/applications/audit/constants/PhabricatorAuditStatusConstants.php
index e1a4378ab3..78924905b9 100644
--- a/src/applications/audit/constants/PhabricatorAuditStatusConstants.php
+++ b/src/applications/audit/constants/PhabricatorAuditStatusConstants.php
@@ -1,97 +1,97 @@
 <?php
 
 final class PhabricatorAuditStatusConstants {
 
   const NONE                    = '';
   const AUDIT_NOT_REQUIRED      = 'audit-not-required';
   const AUDIT_REQUIRED          = 'audit-required';
   const CONCERNED               = 'concerned';
   const ACCEPTED                = 'accepted';
   const AUDIT_REQUESTED         = 'requested';
   const RESIGNED                = 'resigned';
   const CLOSED                  = 'closed';
   const CC                      = 'cc';
 
   public static function getStatusNameMap() {
     $map = array(
       self::NONE                => pht('Not Applicable'),
       self::AUDIT_NOT_REQUIRED  => pht('Audit Not Required'),
       self::AUDIT_REQUIRED      => pht('Audit Required'),
       self::CONCERNED           => pht('Concern Raised'),
       self::ACCEPTED            => pht('Accepted'),
       self::AUDIT_REQUESTED     => pht('Audit Requested'),
       self::RESIGNED            => pht('Resigned'),
       self::CLOSED              => pht('Closed'),
       self::CC                  => pht("Was CC'd"),
     );
 
     return $map;
   }
 
   public static function getStatusName($code) {
     return idx(self::getStatusNameMap(), $code, pht('Unknown'));
   }
 
   public static function getStatusColor($code) {
     switch ($code) {
       case self::CONCERNED:
         $color = 'red';
         break;
       case self::AUDIT_REQUIRED:
       case self::AUDIT_REQUESTED:
         $color = 'orange';
         break;
       case self::ACCEPTED:
         $color = 'green';
         break;
       case self::AUDIT_NOT_REQUIRED:
         $color = 'blue';
         break;
       case self::RESIGNED:
       case self::CLOSED:
         $color = 'dark';
         break;
       default:
         $color = 'bluegrey';
         break;
     }
     return $color;
   }
 
   public static function getStatusIcon($code) {
     switch ($code) {
-      case PhabricatorAuditStatusConstants::AUDIT_NOT_REQUIRED:
-      case PhabricatorAuditStatusConstants::RESIGNED:
+      case self::AUDIT_NOT_REQUIRED:
+      case self::RESIGNED:
         $icon = PHUIStatusItemView::ICON_OPEN;
         break;
-      case PhabricatorAuditStatusConstants::AUDIT_REQUIRED:
-      case PhabricatorAuditStatusConstants::AUDIT_REQUESTED:
+      case self::AUDIT_REQUIRED:
+      case self::AUDIT_REQUESTED:
         $icon = PHUIStatusItemView::ICON_WARNING;
         break;
-      case PhabricatorAuditStatusConstants::CONCERNED:
+      case self::CONCERNED:
         $icon = PHUIStatusItemView::ICON_REJECT;
         break;
-      case PhabricatorAuditStatusConstants::ACCEPTED:
-      case PhabricatorAuditStatusConstants::CLOSED:
+      case self::ACCEPTED:
+      case self::CLOSED:
         $icon = PHUIStatusItemView::ICON_ACCEPT;
         break;
       default:
         $icon = PHUIStatusItemView::ICON_QUESTION;
         break;
     }
     return $icon;
   }
 
   public static function getOpenStatusConstants() {
     return array(
       self::AUDIT_REQUIRED,
       self::AUDIT_REQUESTED,
       self::CONCERNED,
     );
   }
 
   public static function isOpenStatus($status) {
     return in_array($status, self::getOpenStatusConstants());
   }
 
 }
diff --git a/src/applications/audit/storage/PhabricatorAuditInlineComment.php b/src/applications/audit/storage/PhabricatorAuditInlineComment.php
index 8c0aa78d5d..292e0274a5 100644
--- a/src/applications/audit/storage/PhabricatorAuditInlineComment.php
+++ b/src/applications/audit/storage/PhabricatorAuditInlineComment.php
@@ -1,346 +1,346 @@
 <?php
 
 final class PhabricatorAuditInlineComment
   implements PhabricatorInlineCommentInterface {
 
   private $proxy;
   private $syntheticAuthor;
   private $isGhost;
 
   public function __construct() {
     $this->proxy = new PhabricatorAuditTransactionComment();
   }
 
   public function __clone() {
     $this->proxy = clone $this->proxy;
   }
 
   public function getTransactionPHID() {
     return $this->proxy->getTransactionPHID();
   }
 
   public function getTransactionComment() {
     return $this->proxy;
   }
 
   public function getTransactionCommentForSave() {
     $content_source = PhabricatorContentSource::newForSource(
       PhabricatorContentSource::SOURCE_LEGACY,
       array());
 
     $this->proxy
       ->setViewPolicy('public')
       ->setEditPolicy($this->getAuthorPHID())
       ->setContentSource($content_source)
       ->setCommentVersion(1);
 
     return $this->proxy;
   }
 
   public static function loadID($id) {
     $inlines = id(new PhabricatorAuditTransactionComment())->loadAllWhere(
       'id = %d',
       $id);
     if (!$inlines) {
       return null;
     }
 
     return head(self::buildProxies($inlines));
   }
 
   public static function loadPHID($phid) {
     $inlines = id(new PhabricatorAuditTransactionComment())->loadAllWhere(
       'phid = %s',
       $phid);
     if (!$inlines) {
       return null;
     }
     return head(self::buildProxies($inlines));
   }
 
   public static function loadDraftComments(
     PhabricatorUser $viewer,
     $commit_phid) {
 
     $inlines = id(new DiffusionDiffInlineCommentQuery())
       ->setViewer($viewer)
       ->withAuthorPHIDs(array($viewer->getPHID()))
       ->withCommitPHIDs(array($commit_phid))
       ->withHasTransaction(false)
       ->withHasPath(true)
       ->withIsDeleted(false)
       ->needReplyToComments(true)
       ->execute();
 
     return self::buildProxies($inlines);
   }
 
   public static function loadPublishedComments(
     PhabricatorUser $viewer,
     $commit_phid) {
 
     $inlines = id(new DiffusionDiffInlineCommentQuery())
       ->setViewer($viewer)
       ->withCommitPHIDs(array($commit_phid))
       ->withHasTransaction(true)
       ->withHasPath(true)
       ->execute();
 
     return self::buildProxies($inlines);
   }
 
   public static function loadDraftAndPublishedComments(
     PhabricatorUser $viewer,
     $commit_phid,
     $path_id = null) {
 
     if ($path_id === null) {
       $inlines = id(new PhabricatorAuditTransactionComment())->loadAllWhere(
         'commitPHID = %s AND (transactionPHID IS NOT NULL OR authorPHID = %s)
           AND pathID IS NOT NULL',
         $commit_phid,
         $viewer->getPHID());
     } else {
       $inlines = id(new PhabricatorAuditTransactionComment())->loadAllWhere(
         'commitPHID = %s AND pathID = %d AND
           ((authorPHID = %s AND isDeleted = 0) OR transactionPHID IS NOT NULL)',
         $commit_phid,
         $path_id,
         $viewer->getPHID());
     }
 
     return self::buildProxies($inlines);
   }
 
   private static function buildProxies(array $inlines) {
     $results = array();
     foreach ($inlines as $key => $inline) {
-      $results[$key] = PhabricatorAuditInlineComment::newFromModernComment(
+      $results[$key] = self::newFromModernComment(
         $inline);
     }
     return $results;
   }
 
   public function setSyntheticAuthor($synthetic_author) {
     $this->syntheticAuthor = $synthetic_author;
     return $this;
   }
 
   public function getSyntheticAuthor() {
     return $this->syntheticAuthor;
   }
 
   public function openTransaction() {
     $this->proxy->openTransaction();
   }
 
   public function saveTransaction() {
     $this->proxy->saveTransaction();
   }
 
   public function save() {
     $this->getTransactionCommentForSave()->save();
 
     return $this;
   }
 
   public function delete() {
     $this->proxy->delete();
 
     return $this;
   }
 
   public function getID() {
     return $this->proxy->getID();
   }
 
   public function getPHID() {
     return $this->proxy->getPHID();
   }
 
   public static function newFromModernComment(
     PhabricatorAuditTransactionComment $comment) {
 
     $obj = new PhabricatorAuditInlineComment();
     $obj->proxy = $comment;
 
     return $obj;
   }
 
   public function isCompatible(PhabricatorInlineCommentInterface $comment) {
     return
       ($this->getAuthorPHID() === $comment->getAuthorPHID()) &&
       ($this->getSyntheticAuthor() === $comment->getSyntheticAuthor()) &&
       ($this->getContent() === $comment->getContent());
   }
 
   public function setContent($content) {
     $this->proxy->setContent($content);
     return $this;
   }
 
   public function getContent() {
     return $this->proxy->getContent();
   }
 
   public function isDraft() {
     return !$this->proxy->getTransactionPHID();
   }
 
   public function setPathID($id) {
     $this->proxy->setPathID($id);
     return $this;
   }
 
   public function getPathID() {
     return $this->proxy->getPathID();
   }
 
   public function setIsNewFile($is_new) {
     $this->proxy->setIsNewFile($is_new);
     return $this;
   }
 
   public function getIsNewFile() {
     return $this->proxy->getIsNewFile();
   }
 
   public function setLineNumber($number) {
     $this->proxy->setLineNumber($number);
     return $this;
   }
 
   public function getLineNumber() {
     return $this->proxy->getLineNumber();
   }
 
   public function setLineLength($length) {
     $this->proxy->setLineLength($length);
     return $this;
   }
 
   public function getLineLength() {
     return $this->proxy->getLineLength();
   }
 
   public function setCache($cache) {
     return $this;
   }
 
   public function getCache() {
     return null;
   }
 
   public function setAuthorPHID($phid) {
     $this->proxy->setAuthorPHID($phid);
     return $this;
   }
 
   public function getAuthorPHID() {
     return $this->proxy->getAuthorPHID();
   }
 
   public function setCommitPHID($commit_phid) {
     $this->proxy->setCommitPHID($commit_phid);
     return $this;
   }
 
   public function getCommitPHID() {
     return $this->proxy->getCommitPHID();
   }
 
   // When setting a comment ID, we also generate a phantom transaction PHID for
   // the future transaction.
 
   public function setAuditCommentID($id) {
     $this->proxy->setLegacyCommentID($id);
     $this->proxy->setTransactionPHID(
       PhabricatorPHID::generateNewPHID(
         PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST,
         PhabricatorRepositoryCommitPHIDType::TYPECONST));
     return $this;
   }
 
   public function getAuditCommentID() {
     return $this->proxy->getLegacyCommentID();
   }
 
   public function setChangesetID($id) {
     return $this->setPathID($id);
   }
 
   public function getChangesetID() {
     return $this->getPathID();
   }
 
   public function setReplyToCommentPHID($phid) {
     $this->proxy->setReplyToCommentPHID($phid);
     return $this;
   }
 
   public function getReplyToCommentPHID() {
     return $this->proxy->getReplyToCommentPHID();
   }
 
   public function setHasReplies($has_replies) {
     $this->proxy->setHasReplies($has_replies);
     return $this;
   }
 
   public function getHasReplies() {
     return $this->proxy->getHasReplies();
   }
 
   public function setIsDeleted($is_deleted) {
     $this->proxy->setIsDeleted($is_deleted);
     return $this;
   }
 
   public function getIsDeleted() {
     return $this->proxy->getIsDeleted();
   }
 
   public function setFixedState($state) {
     $this->proxy->setFixedState($state);
     return $this;
   }
 
   public function getFixedState() {
     return $this->proxy->getFixedState();
   }
 
   public function setIsGhost($is_ghost) {
     $this->isGhost = $is_ghost;
     return $this;
   }
 
   public function getIsGhost() {
     return $this->isGhost;
   }
 
 
 /* -(  PhabricatorMarkupInterface Implementation  )-------------------------- */
 
 
   public function getMarkupFieldKey($field) {
     return 'AI:'.$this->getID();
   }
 
   public function newMarkupEngine($field) {
     return PhabricatorMarkupEngine::newDifferentialMarkupEngine();
   }
 
   public function getMarkupText($field) {
     return $this->getContent();
   }
 
   public function didMarkupText($field, $output, PhutilMarkupEngine $engine) {
     return $output;
   }
 
   public function shouldUseMarkupCache($field) {
     // Only cache submitted comments.
     return ($this->getID() && $this->getAuditCommentID());
   }
 
 }
diff --git a/src/applications/auth/controller/PhabricatorAuthRegisterController.php b/src/applications/auth/controller/PhabricatorAuthRegisterController.php
index 6c40375227..d27a644480 100644
--- a/src/applications/auth/controller/PhabricatorAuthRegisterController.php
+++ b/src/applications/auth/controller/PhabricatorAuthRegisterController.php
@@ -1,665 +1,657 @@
 <?php
 
 final class PhabricatorAuthRegisterController
   extends PhabricatorAuthController {
 
   private $accountKey;
 
   public function shouldRequireLogin() {
     return false;
   }
 
   public function willProcessRequest(array $data) {
     $this->accountKey = idx($data, 'akey');
   }
 
   public function processRequest() {
     $request = $this->getRequest();
 
     if ($request->getUser()->isLoggedIn()) {
       return $this->renderError(pht('You are already logged in.'));
     }
 
     $is_setup = false;
     if (strlen($this->accountKey)) {
       $result = $this->loadAccountForRegistrationOrLinking($this->accountKey);
       list($account, $provider, $response) = $result;
       $is_default = false;
     } else if ($this->isFirstTimeSetup()) {
       list($account, $provider, $response) = $this->loadSetupAccount();
       $is_default = true;
       $is_setup = true;
     } else {
       list($account, $provider, $response) = $this->loadDefaultAccount();
       $is_default = true;
     }
 
     if ($response) {
       return $response;
     }
 
     $invite = $this->loadInvite();
 
     if (!$provider->shouldAllowRegistration()) {
       if ($invite) {
         // If the user has an invite, we allow them to register with any
         // provider, even a login-only provider.
       } else {
         // TODO: This is a routine error if you click "Login" on an external
         // auth source which doesn't allow registration. The error should be
         // more tailored.
 
         return $this->renderError(
           pht(
             'The account you are attempting to register with uses an '.
             'authentication provider ("%s") which does not allow '.
             'registration. An administrator may have recently disabled '.
             'registration with this provider.',
             $provider->getProviderName()));
       }
     }
 
     $user = new PhabricatorUser();
 
     $default_username = $account->getUsername();
     $default_realname = $account->getRealName();
 
     $default_email = $account->getEmail();
 
     if ($invite) {
       $default_email = $invite->getEmailAddress();
     }
 
     if (!PhabricatorUserEmail::isValidAddress($default_email)) {
       $default_email = null;
     }
 
     if ($default_email !== null) {
       // We should bypass policy here becase e.g. limiting an application use
       // to a subset of users should not allow the others to overwrite
       // configured application emails
       $application_email = id(new PhabricatorMetaMTAApplicationEmailQuery())
         ->setViewer(PhabricatorUser::getOmnipotentUser())
         ->withAddresses(array($default_email))
         ->executeOne();
       if ($application_email) {
         $default_email = null;
       }
     }
 
     if ($default_email !== null) {
       // If the account source provided an email, but it's not allowed by
       // the configuration, roadblock the user. Previously, we let the user
       // pick a valid email address instead, but this does not align well with
       // user expectation and it's not clear the cases it enables are valuable.
       // See discussion in T3472.
       if (!PhabricatorUserEmail::isAllowedAddress($default_email)) {
         return $this->renderError(
           array(
             pht(
               'The account you are attempting to register with has an invalid '.
               'email address (%s). This Phabricator install only allows '.
               'registration with specific email addresses:',
               $default_email),
             phutil_tag('br'),
             phutil_tag('br'),
             PhabricatorUserEmail::describeAllowedAddresses(),
           ));
       }
 
       // If the account source provided an email, but another account already
       // has that email, just pretend we didn't get an email.
 
       // TODO: See T3472.
 
       if ($default_email !== null) {
         $same_email = id(new PhabricatorUserEmail())->loadOneWhere(
           'address = %s',
           $default_email);
         if ($same_email) {
           if ($invite) {
             // We're allowing this to continue. The fact that we loaded the
             // invite means that the address is nonprimary and unverified and
             // we're OK to steal it.
           } else {
             $default_email = null;
           }
         }
       }
     }
 
     $profile = id(new PhabricatorRegistrationProfile())
       ->setDefaultUsername($default_username)
       ->setDefaultEmail($default_email)
       ->setDefaultRealName($default_realname)
       ->setCanEditUsername(true)
       ->setCanEditEmail(($default_email === null))
       ->setCanEditRealName(true)
       ->setShouldVerifyEmail(false);
 
     $event_type = PhabricatorEventType::TYPE_AUTH_WILLREGISTERUSER;
     $event_data = array(
       'account' => $account,
       'profile' => $profile,
     );
 
     $event = id(new PhabricatorEvent($event_type, $event_data))
       ->setUser($user);
     PhutilEventEngine::dispatchEvent($event);
 
     $default_username = $profile->getDefaultUsername();
     $default_email = $profile->getDefaultEmail();
     $default_realname = $profile->getDefaultRealName();
 
     $can_edit_username = $profile->getCanEditUsername();
     $can_edit_email = $profile->getCanEditEmail();
     $can_edit_realname = $profile->getCanEditRealName();
 
     $must_set_password = $provider->shouldRequireRegistrationPassword();
 
     $can_edit_anything = $profile->getCanEditAnything() || $must_set_password;
     $force_verify = $profile->getShouldVerifyEmail();
 
     // Automatically verify the administrator's email address during first-time
     // setup.
     if ($is_setup) {
       $force_verify = true;
     }
 
     $value_username = $default_username;
     $value_realname = $default_realname;
     $value_email = $default_email;
     $value_password = null;
 
     $errors = array();
 
     $require_real_name = PhabricatorEnv::getEnvConfig('user.require-real-name');
 
     $e_username = strlen($value_username) ? null : true;
     $e_realname = $require_real_name ? true : null;
     $e_email = strlen($value_email) ? null : true;
     $e_password = true;
     $e_captcha = true;
 
     $skip_captcha = false;
     if ($invite) {
       // If the user is accepting an invite, assume they're trustworthy enough
       // that we don't need to CAPTCHA them.
       $skip_captcha = true;
     }
 
     $min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length');
     $min_len = (int)$min_len;
 
     $from_invite = $request->getStr('invite');
     if ($from_invite && $can_edit_username) {
       $value_username = $request->getStr('username');
       $e_username = null;
     }
 
     if (($request->isFormPost() || !$can_edit_anything) && !$from_invite) {
       $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
 
       if ($must_set_password && !$skip_captcha) {
         $e_captcha = pht('Again');
 
         $captcha_ok = AphrontFormRecaptchaControl::processCaptcha($request);
         if (!$captcha_ok) {
           $errors[] = pht('Captcha response is incorrect, try again.');
           $e_captcha = pht('Invalid');
         }
       }
 
       if ($can_edit_username) {
         $value_username = $request->getStr('username');
         if (!strlen($value_username)) {
           $e_username = pht('Required');
           $errors[] = pht('Username is required.');
         } else if (!PhabricatorUser::validateUsername($value_username)) {
           $e_username = pht('Invalid');
           $errors[] = PhabricatorUser::describeValidUsername();
         } else {
           $e_username = null;
         }
       }
 
       if ($must_set_password) {
         $value_password = $request->getStr('password');
         $value_confirm = $request->getStr('confirm');
         if (!strlen($value_password)) {
           $e_password = pht('Required');
           $errors[] = pht('You must choose a password.');
         } else if ($value_password !== $value_confirm) {
           $e_password = pht('No Match');
           $errors[] = pht('Password and confirmation must match.');
         } else if (strlen($value_password) < $min_len) {
           $e_password = pht('Too Short');
           $errors[] = pht(
             'Password is too short (must be at least %d characters long).',
             $min_len);
         } else if (
           PhabricatorCommonPasswords::isCommonPassword($value_password)) {
 
           $e_password = pht('Very Weak');
           $errors[] = pht(
             'Password is pathologically weak. This password is one of the '.
             'most common passwords in use, and is extremely easy for '.
             'attackers to guess. You must choose a stronger password.');
         } else {
           $e_password = null;
         }
       }
 
       if ($can_edit_email) {
         $value_email = $request->getStr('email');
         if (!strlen($value_email)) {
           $e_email = pht('Required');
           $errors[] = pht('Email is required.');
         } else if (!PhabricatorUserEmail::isValidAddress($value_email)) {
           $e_email = pht('Invalid');
           $errors[] = PhabricatorUserEmail::describeValidAddresses();
         } else if (!PhabricatorUserEmail::isAllowedAddress($value_email)) {
           $e_email = pht('Disallowed');
           $errors[] = PhabricatorUserEmail::describeAllowedAddresses();
         } else {
           $e_email = null;
         }
       }
 
       if ($can_edit_realname) {
         $value_realname = $request->getStr('realName');
         if (!strlen($value_realname) && $require_real_name) {
           $e_realname = pht('Required');
           $errors[] = pht('Real name is required.');
         } else {
           $e_realname = null;
         }
       }
 
       if (!$errors) {
         $image = $this->loadProfilePicture($account);
         if ($image) {
           $user->setProfileImagePHID($image->getPHID());
         }
 
         try {
           $verify_email = false;
 
           if ($force_verify) {
             $verify_email = true;
           }
 
           if ($value_email === $default_email) {
             if ($account->getEmailVerified()) {
               $verify_email = true;
             }
 
             if ($provider->shouldTrustEmails()) {
               $verify_email = true;
             }
 
             if ($invite) {
               $verify_email = true;
             }
           }
 
           $email_obj = null;
           if ($invite) {
             // If we have a valid invite, this email may exist but be
             // nonprimary and unverified, so we'll reassign it.
             $email_obj = id(new PhabricatorUserEmail())->loadOneWhere(
               'address = %s',
               $value_email);
           }
           if (!$email_obj) {
             $email_obj = id(new PhabricatorUserEmail())
               ->setAddress($value_email);
           }
 
           $email_obj->setIsVerified((int)$verify_email);
 
           $user->setUsername($value_username);
           $user->setRealname($value_realname);
 
           if ($is_setup) {
             $must_approve = false;
           } else if ($invite) {
             $must_approve = false;
           } else {
             $must_approve = PhabricatorEnv::getEnvConfig(
               'auth.require-approval');
           }
 
           if ($must_approve) {
             $user->setIsApproved(0);
           } else {
             $user->setIsApproved(1);
           }
 
           if ($invite) {
             $allow_reassign_email = true;
           } else {
             $allow_reassign_email = false;
           }
 
           $user->openTransaction();
 
             $editor = id(new PhabricatorUserEditor())
               ->setActor($user);
 
             $editor->createNewUser($user, $email_obj, $allow_reassign_email);
             if ($must_set_password) {
               $envelope = new PhutilOpaqueEnvelope($value_password);
               $editor->changePassword($user, $envelope);
             }
 
             if ($is_setup) {
               $editor->makeAdminUser($user, true);
             }
 
             $account->setUserPHID($user->getPHID());
             $provider->willRegisterAccount($account);
             $account->save();
 
           $user->saveTransaction();
 
           if (!$email_obj->getIsVerified()) {
             $email_obj->sendVerificationEmail($user);
           }
 
           if ($must_approve) {
             $this->sendWaitingForApprovalEmail($user);
           }
 
           if ($invite) {
             $invite->setAcceptedByPHID($user->getPHID())->save();
           }
 
           return $this->loginUser($user);
         } catch (AphrontDuplicateKeyQueryException $exception) {
           $same_username = id(new PhabricatorUser())->loadOneWhere(
             'userName = %s',
             $user->getUserName());
 
           $same_email = id(new PhabricatorUserEmail())->loadOneWhere(
             'address = %s',
             $value_email);
 
           if ($same_username) {
             $e_username = pht('Duplicate');
             $errors[] = pht('Another user already has that username.');
           }
 
           if ($same_email) {
             // TODO: See T3340.
             $e_email = pht('Duplicate');
             $errors[] = pht('Another user already has that email.');
           }
 
           if (!$same_username && !$same_email) {
             throw $exception;
           }
         }
       }
 
       unset($unguarded);
     }
 
     $form = id(new AphrontFormView())
       ->setUser($request->getUser());
 
     if (!$is_default) {
       $form->appendChild(
         id(new AphrontFormMarkupControl())
           ->setLabel(pht('External Account'))
           ->setValue(
             id(new PhabricatorAuthAccountView())
               ->setUser($request->getUser())
               ->setExternalAccount($account)
               ->setAuthProvider($provider)));
     }
 
 
     if ($can_edit_username) {
       $form->appendChild(
         id(new AphrontFormTextControl())
           ->setLabel(pht('Phabricator Username'))
           ->setName('username')
           ->setValue($value_username)
           ->setError($e_username));
     } else {
       $form->appendChild(
         id(new AphrontFormMarkupControl())
           ->setLabel(pht('Phabricator Username'))
           ->setValue($value_username)
           ->setError($e_username));
     }
 
     if ($can_edit_realname) {
       $form->appendChild(
         id(new AphrontFormTextControl())
           ->setLabel(pht('Real Name'))
           ->setName('realName')
           ->setValue($value_realname)
           ->setError($e_realname));
     }
 
     if ($must_set_password) {
       $form->appendChild(
         id(new AphrontFormPasswordControl())
           ->setLabel(pht('Password'))
           ->setName('password')
           ->setError($e_password));
       $form->appendChild(
         id(new AphrontFormPasswordControl())
           ->setLabel(pht('Confirm Password'))
           ->setName('confirm')
           ->setError($e_password)
           ->setCaption(
             $min_len
               ? pht('Minimum length of %d characters.', $min_len)
               : null));
     }
 
     if ($can_edit_email) {
       $form->appendChild(
         id(new AphrontFormTextControl())
           ->setLabel(pht('Email'))
           ->setName('email')
           ->setValue($value_email)
           ->setCaption(PhabricatorUserEmail::describeAllowedAddresses())
           ->setError($e_email));
     }
 
     if ($must_set_password && !$skip_captcha) {
       $form->appendChild(
         id(new AphrontFormRecaptchaControl())
           ->setLabel(pht('Captcha'))
           ->setError($e_captcha));
     }
 
     $submit = id(new AphrontFormSubmitControl());
 
     if ($is_setup) {
       $submit
         ->setValue(pht('Create Admin Account'));
     } else {
       $submit
         ->addCancelButton($this->getApplicationURI('start/'))
         ->setValue(pht('Register Phabricator Account'));
     }
 
 
     $form->appendChild($submit);
 
     $crumbs = $this->buildApplicationCrumbs();
 
     if ($is_setup) {
       $crumbs->addTextCrumb(pht('Setup Admin Account'));
         $title = pht('Welcome to Phabricator');
     } else {
       $crumbs->addTextCrumb(pht('Register'));
       $crumbs->addTextCrumb($provider->getProviderName());
         $title = pht('Phabricator Registration');
     }
 
     $welcome_view = null;
     if ($is_setup) {
       $welcome_view = id(new PHUIInfoView())
         ->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
         ->setTitle(pht('Welcome to Phabricator'))
         ->appendChild(
           pht(
             'Installation is complete. Register your administrator account '.
             'below to log in. You will be able to configure options and add '.
             'other authentication mechanisms (like LDAP or OAuth) later on.'));
     }
 
     $object_box = id(new PHUIObjectBoxView())
       ->setHeaderText($title)
       ->setForm($form)
       ->setFormErrors($errors);
 
     $invite_header = null;
     if ($invite) {
       $invite_header = $this->renderInviteHeader($invite);
     }
 
     return $this->buildApplicationPage(
       array(
         $crumbs,
         $welcome_view,
         $invite_header,
         $object_box,
       ),
       array(
         'title' => $title,
       ));
   }
 
   private function loadDefaultAccount() {
     $providers = PhabricatorAuthProvider::getAllEnabledProviders();
     $account = null;
     $provider = null;
     $response = null;
 
     foreach ($providers as $key => $candidate_provider) {
       if (!$candidate_provider->shouldAllowRegistration()) {
         unset($providers[$key]);
         continue;
       }
       if (!$candidate_provider->isDefaultRegistrationProvider()) {
         unset($providers[$key]);
       }
     }
 
     if (!$providers) {
       $response = $this->renderError(
         pht(
           'There are no configured default registration providers.'));
       return array($account, $provider, $response);
     } else if (count($providers) > 1) {
       $response = $this->renderError(
         pht(
           'There are too many configured default registration providers.'));
       return array($account, $provider, $response);
     }
 
     $provider = head($providers);
     $account = $provider->getDefaultExternalAccount();
 
     return array($account, $provider, $response);
   }
 
   private function loadSetupAccount() {
     $provider = new PhabricatorPasswordAuthProvider();
     $provider->attachProviderConfig(
       id(new PhabricatorAuthProviderConfig())
         ->setShouldAllowRegistration(1)
         ->setShouldAllowLogin(1)
         ->setIsEnabled(true));
 
     $account = $provider->getDefaultExternalAccount();
     $response = null;
     return array($account, $provider, $response);
   }
 
   private function loadProfilePicture(PhabricatorExternalAccount $account) {
     $phid = $account->getProfileImagePHID();
     if (!$phid) {
       return null;
     }
 
     // NOTE: Use of omnipotent user is okay here because the registering user
     // can not control the field value, and we can't use their user object to
     // do meaningful policy checks anyway since they have not registered yet.
     // Reaching this means the user holds the account secret key and the
     // registration secret key, and thus has permission to view the image.
 
     $file = id(new PhabricatorFileQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withPHIDs(array($phid))
       ->executeOne();
     if (!$file) {
       return null;
     }
 
-    try {
-      $xformer = new PhabricatorImageTransformer();
-      return $xformer->executeProfileTransform(
-        $file,
-        $width = 50,
-        $min_height = 50,
-        $max_height = 50);
-    } catch (Exception $ex) {
-      phlog($ex);
-      return null;
-    }
+    $xform = PhabricatorFileTransform::getTransformByKey(
+      PhabricatorFileThumbnailTransform::TRANSFORM_PROFILE);
+    return $xform->executeTransform($file);
   }
 
   protected function renderError($message) {
     return $this->renderErrorPage(
       pht('Registration Failed'),
       array($message));
   }
 
   private function sendWaitingForApprovalEmail(PhabricatorUser $user) {
     $title = '[Phabricator] '.pht(
       'New User "%s" Awaiting Approval',
       $user->getUsername());
 
     $body = new PhabricatorMetaMTAMailBody();
 
     $body->addRawSection(
       pht(
         'Newly registered user "%s" is awaiting account approval by an '.
         'administrator.',
         $user->getUsername()));
 
     $body->addLinkSection(
       pht('APPROVAL QUEUE'),
       PhabricatorEnv::getProductionURI(
         '/people/query/approval/'));
 
     $body->addLinkSection(
       pht('DISABLE APPROVAL QUEUE'),
       PhabricatorEnv::getProductionURI(
         '/config/edit/auth.require-approval/'));
 
     $admins = id(new PhabricatorPeopleQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withIsAdmin(true)
       ->execute();
 
     if (!$admins) {
       return;
     }
 
     $mail = id(new PhabricatorMetaMTAMail())
       ->addTos(mpull($admins, 'getPHID'))
       ->setSubject($title)
       ->setBody($body->render())
       ->saveAndSend();
   }
 
 }
diff --git a/src/applications/auth/provider/PhabricatorAuthProvider.php b/src/applications/auth/provider/PhabricatorAuthProvider.php
index b1b064c363..5ffb18b41a 100644
--- a/src/applications/auth/provider/PhabricatorAuthProvider.php
+++ b/src/applications/auth/provider/PhabricatorAuthProvider.php
@@ -1,498 +1,497 @@
 <?php
 
 abstract class PhabricatorAuthProvider {
 
   private $providerConfig;
 
   public function attachProviderConfig(PhabricatorAuthProviderConfig $config) {
     $this->providerConfig = $config;
     return $this;
   }
 
   public function hasProviderConfig() {
     return (bool)$this->providerConfig;
   }
 
   public function getProviderConfig() {
     if ($this->providerConfig === null) {
-      throw new Exception(
-        'Call attachProviderConfig() before getProviderConfig()!');
+      throw new PhutilInvalidStateException('attachProviderConfig');
     }
     return $this->providerConfig;
   }
 
   public function getConfigurationHelp() {
     return null;
   }
 
   public function getDefaultProviderConfig() {
     return id(new PhabricatorAuthProviderConfig())
       ->setProviderClass(get_class($this))
       ->setIsEnabled(1)
       ->setShouldAllowLogin(1)
       ->setShouldAllowRegistration(1)
       ->setShouldAllowLink(1)
       ->setShouldAllowUnlink(1);
   }
 
   public function getNameForCreate() {
     return $this->getProviderName();
   }
 
   public function getDescriptionForCreate() {
     return null;
   }
 
   public function getProviderKey() {
     return $this->getAdapter()->getAdapterKey();
   }
 
   public function getProviderType() {
     return $this->getAdapter()->getAdapterType();
   }
 
   public function getProviderDomain() {
     return $this->getAdapter()->getAdapterDomain();
   }
 
   public static function getAllBaseProviders() {
     static $providers;
 
     if ($providers === null) {
       $objects = id(new PhutilSymbolLoader())
         ->setAncestorClass(__CLASS__)
         ->loadObjects();
       $providers = $objects;
     }
 
     return $providers;
   }
 
   public static function getAllProviders() {
     static $providers;
 
     if ($providers === null) {
       $objects = self::getAllBaseProviders();
 
       $configs = id(new PhabricatorAuthProviderConfigQuery())
         ->setViewer(PhabricatorUser::getOmnipotentUser())
         ->execute();
 
       $providers = array();
       foreach ($configs as $config) {
         if (!isset($objects[$config->getProviderClass()])) {
           // This configuration is for a provider which is not installed.
           continue;
         }
 
         $object = clone $objects[$config->getProviderClass()];
         $object->attachProviderConfig($config);
 
         $key = $object->getProviderKey();
         if (isset($providers[$key])) {
           throw new Exception(
             pht(
               "Two authentication providers use the same provider key ".
               "('%s'). Each provider must be identified by a unique key.",
               $key));
         }
         $providers[$key] = $object;
       }
     }
 
     return $providers;
   }
 
   public static function getAllEnabledProviders() {
     $providers = self::getAllProviders();
     foreach ($providers as $key => $provider) {
       if (!$provider->isEnabled()) {
         unset($providers[$key]);
       }
     }
     return $providers;
   }
 
   public static function getEnabledProviderByKey($provider_key) {
     return idx(self::getAllEnabledProviders(), $provider_key);
   }
 
   abstract public function getProviderName();
   abstract public function getAdapter();
 
   public function isEnabled() {
     return $this->getProviderConfig()->getIsEnabled();
   }
 
   public function shouldAllowLogin() {
     return $this->getProviderConfig()->getShouldAllowLogin();
   }
 
   public function shouldAllowRegistration() {
     return $this->getProviderConfig()->getShouldAllowRegistration();
   }
 
   public function shouldAllowAccountLink() {
     return $this->getProviderConfig()->getShouldAllowLink();
   }
 
   public function shouldAllowAccountUnlink() {
     return $this->getProviderConfig()->getShouldAllowUnlink();
   }
 
   public function shouldTrustEmails() {
     return $this->shouldAllowEmailTrustConfiguration() &&
            $this->getProviderConfig()->getShouldTrustEmails();
   }
 
   /**
    * Should we allow the adapter to be marked as "trusted". This is true for
    * all adapters except those that allow the user to type in emails (see
    * @{class:PhabricatorPasswordAuthProvider}).
    */
   public function shouldAllowEmailTrustConfiguration() {
     return true;
   }
 
   public function buildLoginForm(PhabricatorAuthStartController $controller) {
     return $this->renderLoginForm($controller->getRequest(), $mode = 'start');
   }
 
   public function buildInviteForm(PhabricatorAuthStartController $controller) {
     return $this->renderLoginForm($controller->getRequest(), $mode = 'invite');
   }
 
   abstract public function processLoginRequest(
     PhabricatorAuthLoginController $controller);
 
   public function buildLinkForm(PhabricatorAuthLinkController $controller) {
     return $this->renderLoginForm($controller->getRequest(), $mode = 'link');
   }
 
   public function shouldAllowAccountRefresh() {
     return true;
   }
 
   public function buildRefreshForm(
     PhabricatorAuthLinkController $controller) {
     return $this->renderLoginForm($controller->getRequest(), $mode = 'refresh');
   }
 
   protected function renderLoginForm(AphrontRequest $request, $mode) {
     throw new PhutilMethodNotImplementedException();
   }
 
   public function createProviders() {
     return array($this);
   }
 
   protected function willSaveAccount(PhabricatorExternalAccount $account) {
     return;
   }
 
   public function willRegisterAccount(PhabricatorExternalAccount $account) {
     return;
   }
 
   protected function loadOrCreateAccount($account_id) {
     if (!strlen($account_id)) {
       throw new Exception('loadOrCreateAccount(...): empty account ID!');
     }
 
     $adapter = $this->getAdapter();
     $adapter_class = get_class($adapter);
 
     if (!strlen($adapter->getAdapterType())) {
       throw new Exception(
         "AuthAdapter (of class '{$adapter_class}') has an invalid ".
         "implementation: no adapter type.");
     }
 
     if (!strlen($adapter->getAdapterDomain())) {
       throw new Exception(
         "AuthAdapter (of class '{$adapter_class}') has an invalid ".
         "implementation: no adapter domain.");
     }
 
     $account = id(new PhabricatorExternalAccount())->loadOneWhere(
       'accountType = %s AND accountDomain = %s AND accountID = %s',
       $adapter->getAdapterType(),
       $adapter->getAdapterDomain(),
       $account_id);
     if (!$account) {
       $account = id(new PhabricatorExternalAccount())
         ->setAccountType($adapter->getAdapterType())
         ->setAccountDomain($adapter->getAdapterDomain())
         ->setAccountID($account_id);
     }
 
     $account->setUsername($adapter->getAccountName());
     $account->setRealName($adapter->getAccountRealName());
     $account->setEmail($adapter->getAccountEmail());
     $account->setAccountURI($adapter->getAccountURI());
 
     $account->setProfileImagePHID(null);
     $image_uri = $adapter->getAccountImageURI();
     if ($image_uri) {
       try {
         $name = PhabricatorSlug::normalize($this->getProviderName());
         $name = $name.'-profile.jpg';
 
         // TODO: If the image has not changed, we do not need to make a new
         // file entry for it, but there's no convenient way to do this with
         // PhabricatorFile right now. The storage will get shared, so the impact
         // here is negligible.
         $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
           $image_file = PhabricatorFile::newFromFileDownload(
             $image_uri,
             array(
               'name' => $name,
               'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
             ));
           if ($image_file->isViewableImage()) {
             $image_file
               ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
               ->setCanCDN(true)
               ->save();
             $account->setProfileImagePHID($image_file->getPHID());
           } else {
             $image_file->delete();
           }
         unset($unguarded);
 
       } catch (Exception $ex) {
         // Log this but proceed, it's not especially important that we
         // be able to pull profile images.
         phlog($ex);
       }
     }
 
     $this->willSaveAccount($account);
 
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
     $account->save();
     unset($unguarded);
 
     return $account;
   }
 
   public function getLoginURI() {
     $app = PhabricatorApplication::getByClass('PhabricatorAuthApplication');
     return $app->getApplicationURI('/login/'.$this->getProviderKey().'/');
   }
 
   public function getSettingsURI() {
     return '/settings/panel/external/';
   }
 
   public function getStartURI() {
     $app = PhabricatorApplication::getByClass('PhabricatorAuthApplication');
     $uri = $app->getApplicationURI('/start/');
     return $uri;
   }
 
   public function isDefaultRegistrationProvider() {
     return false;
   }
 
   public function shouldRequireRegistrationPassword() {
     return false;
   }
 
   public function getDefaultExternalAccount() {
     throw new PhutilMethodNotImplementedException();
   }
 
   public function getLoginOrder() {
     return '500-'.$this->getProviderName();
   }
 
   protected function getLoginIcon() {
     return 'Generic';
   }
 
   public function isLoginFormAButton() {
     return false;
   }
 
   public function renderConfigPropertyTransactionTitle(
     PhabricatorAuthProviderConfigTransaction $xaction) {
 
     return null;
   }
 
   public function readFormValuesFromProvider() {
     return array();
   }
 
   public function readFormValuesFromRequest(AphrontRequest $request) {
     return array();
   }
 
   public function processEditForm(
     AphrontRequest $request,
     array $values) {
 
     $errors = array();
     $issues = array();
 
     return array($errors, $issues, $values);
   }
 
   public function extendEditForm(
     AphrontRequest $request,
     AphrontFormView $form,
     array $values,
     array $issues) {
 
     return;
   }
 
   public function willRenderLinkedAccount(
     PhabricatorUser $viewer,
     PHUIObjectItemView $item,
     PhabricatorExternalAccount $account) {
 
     $account_view = id(new PhabricatorAuthAccountView())
       ->setExternalAccount($account)
       ->setAuthProvider($this);
 
     $item->appendChild(
       phutil_tag(
         'div',
         array(
           'class' => 'mmr mml mst mmb',
         ),
         $account_view));
   }
 
   /**
    * Return true to use a two-step configuration (setup, configure) instead of
    * the default single-step configuration. In practice, this means that
    * creating a new provider instance will redirect back to the edit page
    * instead of the provider list.
    *
    * @return bool True if this provider uses two-step configuration.
    */
   public function hasSetupStep() {
     return false;
   }
 
   /**
    * Render a standard login/register button element.
    *
    * The `$attributes` parameter takes these keys:
    *
    *   - `uri`: URI the button should take the user to when clicked.
    *   - `method`: Optional HTTP method the button should use, defaults to GET.
    *
    * @param   AphrontRequest  HTTP request.
    * @param   string          Request mode string.
    * @param   map             Additional parameters, see above.
    * @return  wild            Login button.
    */
   protected function renderStandardLoginButton(
     AphrontRequest $request,
     $mode,
     array $attributes = array()) {
 
     PhutilTypeSpec::checkMap(
       $attributes,
       array(
         'method' => 'optional string',
         'uri' => 'string',
         'sigil' => 'optional string',
       ));
 
     $viewer = $request->getUser();
     $adapter = $this->getAdapter();
 
     if ($mode == 'link') {
       $button_text = pht('Link External Account');
     } else if ($mode == 'refresh') {
       $button_text = pht('Refresh Account Link');
     } else if ($mode == 'invite') {
       $button_text = pht('Register Account');
     } else if ($this->shouldAllowRegistration()) {
       $button_text = pht('Login or Register');
     } else {
       $button_text = pht('Login');
     }
 
     $icon = id(new PHUIIconView())
       ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN)
       ->setSpriteIcon($this->getLoginIcon());
 
     $button = id(new PHUIButtonView())
       ->setSize(PHUIButtonView::BIG)
       ->setColor(PHUIButtonView::GREY)
       ->setIcon($icon)
       ->setText($button_text)
       ->setSubtext($this->getProviderName());
 
     $uri = $attributes['uri'];
     $uri = new PhutilURI($uri);
     $params = $uri->getQueryParams();
     $uri->setQueryParams(array());
 
     $content = array($button);
 
     foreach ($params as $key => $value) {
       $content[] = phutil_tag(
         'input',
         array(
           'type' => 'hidden',
           'name' => $key,
           'value' => $value,
         ));
     }
 
     return phabricator_form(
       $viewer,
       array(
         'method' => idx($attributes, 'method', 'GET'),
         'action' => (string)$uri,
         'sigil'  => idx($attributes, 'sigil'),
       ),
       $content);
   }
 
   public function renderConfigurationFooter() {
     return null;
   }
 
   public function getAuthCSRFCode(AphrontRequest $request) {
     $phcid = $request->getCookie(PhabricatorCookies::COOKIE_CLIENTID);
     if (!strlen($phcid)) {
       throw new Exception(
         pht(
           'Your browser did not submit a "%s" cookie with client state '.
           'information in the request. Check that cookies are enabled. '.
           'If this problem persists, you may need to clear your cookies.',
           PhabricatorCookies::COOKIE_CLIENTID));
     }
 
     return PhabricatorHash::digest($phcid);
   }
 
   protected function verifyAuthCSRFCode(AphrontRequest $request, $actual) {
     $expect = $this->getAuthCSRFCode($request);
 
     if (!strlen($actual)) {
       throw new Exception(
         pht(
           'The authentication provider did not return a client state '.
           'parameter in its response, but one was expected. If this '.
           'problem persists, you may need to clear your cookies.'));
     }
 
     if ($actual !== $expect) {
       throw new Exception(
         pht(
           'The authentication provider did not return the correct client '.
           'state parameter in its response. If this problem persists, you may '.
           'need to clear your cookies.'));
     }
   }
 
 }
diff --git a/src/applications/auth/view/PhabricatorAuthAccountView.php b/src/applications/auth/view/PhabricatorAuthAccountView.php
index a68cdfff07..8eb73144aa 100644
--- a/src/applications/auth/view/PhabricatorAuthAccountView.php
+++ b/src/applications/auth/view/PhabricatorAuthAccountView.php
@@ -1,103 +1,116 @@
 <?php
 
 final class PhabricatorAuthAccountView extends AphrontView {
 
   private $externalAccount;
   private $provider;
 
   public function setExternalAccount(
     PhabricatorExternalAccount $external_account) {
     $this->externalAccount = $external_account;
     return $this;
   }
 
   public function setAuthProvider(PhabricatorAuthProvider $provider) {
     $this->provider = $provider;
     return $this;
   }
 
   public function render() {
     $account = $this->externalAccount;
     $provider = $this->provider;
 
     require_celerity_resource('auth-css');
 
     $content = array();
 
     $dispname = $account->getDisplayName();
     $username = $account->getUsername();
     $realname = $account->getRealName();
 
     $use_name = null;
     if (strlen($dispname)) {
       $use_name = $dispname;
     } else if (strlen($username) && strlen($realname)) {
       $use_name = $username.' ('.$realname.')';
     } else if (strlen($username)) {
       $use_name = $username;
     } else if (strlen($realname)) {
       $use_name = $realname;
     } else {
       $use_name = $account->getAccountID();
     }
 
     $content[] = phutil_tag(
       'div',
       array(
         'class' => 'auth-account-view-name',
       ),
       $use_name);
 
     if ($provider) {
       $prov_name = pht('%s Account', $provider->getProviderName());
     } else {
       $prov_name = pht('"%s" Account', $account->getProviderType());
     }
 
     $content[] = phutil_tag(
       'div',
       array(
         'class' => 'auth-account-view-provider-name',
       ),
       array(
         $prov_name,
         " \xC2\xB7 ",
         $account->getAccountID(),
       ));
 
     $account_uri = $account->getAccountURI();
     if (strlen($account_uri)) {
 
       // Make sure we don't link a "javascript:" URI if a user somehow
       // managed to get one here.
 
       if (PhabricatorEnv::isValidRemoteURIForLink($account_uri)) {
         $account_uri = phutil_tag(
           'a',
           array(
             'href' => $account_uri,
             'target' => '_blank',
           ),
           $account_uri);
       }
 
       $content[] = phutil_tag(
         'div',
         array(
           'class' => 'auth-account-view-account-uri',
         ),
         $account_uri);
     }
 
-    $image_uri = $account->getProfileImageFile()->getProfileThumbURI();
+    $image_file = $account->getProfileImageFile();
+    $xform = PhabricatorFileTransform::getTransformByKey(
+      PhabricatorFileThumbnailTransform::TRANSFORM_PROFILE);
+    $image_uri = $image_file->getURIForTransform($xform);
+    list($x, $y) = $xform->getTransformedDimensions($image_file);
+
+    $profile_image = phutil_tag(
+      'div',
+      array(
+        'class' => 'auth-account-view-profile-image',
+        'style' => 'background-image: url('.$image_uri.');',
+      ));
 
     return phutil_tag(
       'div',
       array(
         'class' => 'auth-account-view',
-        'style' => 'background-image: url('.$image_uri.')',
       ),
-      $content);
+      array(
+        $profile_image,
+        $content,
+      ));
   }
 
 }
diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php
index d5237adc11..295d4db180 100644
--- a/src/applications/base/PhabricatorApplication.php
+++ b/src/applications/base/PhabricatorApplication.php
@@ -1,589 +1,589 @@
 <?php
 
 /**
  * @task  info  Application Information
  * @task  ui    UI Integration
  * @task  uri   URI Routing
  * @task  mail  Email integration
  * @task  fact  Fact Integration
  * @task  meta  Application Management
  */
 abstract class PhabricatorApplication implements PhabricatorPolicyInterface {
 
   const MAX_STATUS_ITEMS      = 100;
 
   const GROUP_CORE            = 'core';
   const GROUP_UTILITIES       = 'util';
   const GROUP_ADMIN           = 'admin';
   const GROUP_DEVELOPER       = 'developer';
 
   public static function getApplicationGroups() {
     return array(
       self::GROUP_CORE          => pht('Core Applications'),
       self::GROUP_UTILITIES     => pht('Utilities'),
       self::GROUP_ADMIN         => pht('Administration'),
       self::GROUP_DEVELOPER     => pht('Developer Tools'),
     );
   }
 
 
 /* -(  Application Information  )-------------------------------------------- */
 
   public abstract function getName();
 
   public function getShortDescription() {
     return $this->getName().' Application';
   }
 
   public function isInstalled() {
     if (!$this->canUninstall()) {
       return true;
     }
 
     $prototypes = PhabricatorEnv::getEnvConfig('phabricator.show-prototypes');
     if (!$prototypes && $this->isPrototype()) {
       return false;
     }
 
     $uninstalled = PhabricatorEnv::getEnvConfig(
       'phabricator.uninstalled-applications');
 
     return empty($uninstalled[get_class($this)]);
   }
 
 
   public function isPrototype() {
     return false;
   }
 
 
   /**
    * Return `true` if this application should never appear in application lists
    * in the UI. Primarily intended for unit test applications or other
    * pseudo-applications.
    *
    * Few applications should be unlisted. For most applications, use
    * @{method:isLaunchable} to hide them from main launch views instead.
    *
    * @return bool True to remove application from UI lists.
    */
   public function isUnlisted() {
     return false;
   }
 
 
   /**
    * Return `true` if this application is a normal application with a base
    * URI and a web interface.
    *
    * Launchable applications can be pinned to the home page, and show up in the
    * "Launcher" view of the Applications application. Making an application
    * unlauncahble prevents pinning and hides it from this view.
    *
    * Usually, an application should be marked unlaunchable if:
    *
    *   - it is available on every page anyway (like search); or
    *   - it does not have a web interface (like subscriptions); or
    *   - it is still pre-release and being intentionally buried.
    *
    * To hide applications more completely, use @{method:isUnlisted}.
    *
    * @return bool True if the application is launchable.
    */
   public function isLaunchable() {
     return true;
   }
 
 
   /**
    * Return `true` if this application should be pinned by default.
    *
    * Users who have not yet set preferences see a default list of applications.
    *
    * @param PhabricatorUser User viewing the pinned application list.
    * @return bool True if this application should be pinned by default.
    */
   public function isPinnedByDefault(PhabricatorUser $viewer) {
     return false;
   }
 
 
   /**
    * Returns true if an application is first-party (developed by Phacility)
    * and false otherwise.
    *
    * @return bool True if this application is developed by Phacility.
    */
   final public function isFirstParty() {
     $where = id(new ReflectionClass($this))->getFileName();
     $root = phutil_get_library_root('phabricator');
 
     if (!Filesystem::isDescendant($where, $root)) {
       return false;
     }
 
     if (Filesystem::isDescendant($where, $root.'/extensions')) {
       return false;
     }
 
     return true;
   }
 
   public function canUninstall() {
     return true;
   }
 
   public function getPHID() {
     return 'PHID-APPS-'.get_class($this);
   }
 
   public function getTypeaheadURI() {
     return $this->isLaunchable() ? $this->getBaseURI() : null;
   }
 
   public function getBaseURI() {
     return null;
   }
 
   public function getApplicationURI($path = '') {
     return $this->getBaseURI().ltrim($path, '/');
   }
 
   public function getIconURI() {
     return null;
   }
 
   public function getFontIcon() {
     return 'fa-puzzle-piece';
   }
 
   public function getApplicationOrder() {
     return PHP_INT_MAX;
   }
 
   public function getApplicationGroup() {
     return self::GROUP_CORE;
   }
 
   public function getTitleGlyph() {
     return null;
   }
 
   public function getHelpMenuItems(PhabricatorUser $viewer) {
     $items = array();
 
     $articles = $this->getHelpDocumentationArticles($viewer);
     if ($articles) {
       $items[] = id(new PHUIListItemView())
         ->setType(PHUIListItemView::TYPE_LABEL)
         ->setName(pht('%s Documentation', $this->getName()));
       foreach ($articles as $article) {
         $item = id(new PHUIListItemView())
           ->setName($article['name'])
           ->setIcon('fa-book')
           ->setHref($article['href']);
 
         $items[] = $item;
       }
     }
 
     $command_specs = $this->getMailCommandObjects();
     if ($command_specs) {
       $items[] = id(new PHUIListItemView())
         ->setType(PHUIListItemView::TYPE_LABEL)
         ->setName(pht('Email Help'));
       foreach ($command_specs as $key => $spec) {
         $object = $spec['object'];
 
         $class = get_class($this);
         $href = '/applications/mailcommands/'.$class.'/'.$key.'/';
 
         $item = id(new PHUIListItemView())
           ->setName($spec['name'])
           ->setIcon('fa-envelope-o')
           ->setHref($href);
         $items[] = $item;
       }
     }
 
     return $items;
   }
 
   public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
     return array();
   }
 
   public function getOverview() {
     return null;
   }
 
   public function getEventListeners() {
     return array();
   }
 
   public function getRemarkupRules() {
     return array();
   }
 
   public function getQuicksandURIPatternBlacklist() {
     return array();
   }
 
   public function getMailCommandObjects() {
     return array();
   }
 
 
 /* -(  URI Routing  )-------------------------------------------------------- */
 
 
   public function getRoutes() {
     return array();
   }
 
 
 /* -(  Email Integration  )-------------------------------------------------- */
 
 
   public function supportsEmailIntegration() {
     return false;
   }
 
   protected function getInboundEmailSupportLink() {
     return PhabricatorEnv::getDocLink('Configuring Inbound Email');
   }
 
   public function getAppEmailBlurb() {
     throw new Exception('Not Implemented.');
   }
 
 
 /* -(  Fact Integration  )--------------------------------------------------- */
 
 
   public function getFactObjectsForAnalysis() {
     return array();
   }
 
 
 /* -(  UI Integration  )----------------------------------------------------- */
 
 
   /**
    * Render status elements (like "3 Waiting Reviews") for application list
    * views. These provide a way to alert users to new or pending action items
    * in applications.
    *
    * @param PhabricatorUser Viewing user.
    * @return list<PhabricatorApplicationStatusView> Application status elements.
    * @task ui
    */
   public function loadStatus(PhabricatorUser $user) {
     return array();
   }
 
   /**
    * @return string
    * @task ui
    */
   public static function formatStatusCount(
     $count,
     $limit_string = '%s',
     $base_string = '%d') {
     if ($count == self::MAX_STATUS_ITEMS) {
       $count_str = pht($limit_string, ($count - 1).'+');
     } else {
       $count_str = pht($base_string, $count);
     }
     return $count_str;
   }
 
 
   /**
    * You can provide an optional piece of flavor text for the application. This
    * is currently rendered in application launch views if the application has no
    * status elements.
    *
    * @return string|null Flavor text.
    * @task ui
    */
   public function getFlavorText() {
     return null;
   }
 
 
   /**
    * Build items for the main menu.
    *
    * @param  PhabricatorUser    The viewing user.
    * @param  AphrontController  The current controller. May be null for special
    *                            pages like 404, exception handlers, etc.
    * @return list<PHUIListItemView> List of menu items.
    * @task ui
    */
   public function buildMainMenuItems(
     PhabricatorUser $user,
     PhabricatorController $controller = null) {
     return array();
   }
 
 
   /**
    * Build extra items for the main menu. Generally, this is used to render
    * static dropdowns.
    *
    * @param  PhabricatorUser    The viewing user.
    * @param  AphrontController  The current controller. May be null for special
    *                            pages like 404, exception handlers, etc.
    * @return view               List of menu items.
    * @task ui
    */
   public function buildMainMenuExtraNodes(
     PhabricatorUser $viewer,
     PhabricatorController $controller = null) {
     return array();
   }
 
 
   /**
    * Build items for the "quick create" menu.
    *
    * @param   PhabricatorUser         The viewing user.
    * @return  list<PHUIListItemView>  List of menu items.
    */
   public function getQuickCreateItems(PhabricatorUser $viewer) {
     return array();
   }
 
 
 /* -(  Application Management  )--------------------------------------------- */
 
 
   public static function getByClass($class_name) {
     $selected = null;
-    $applications = PhabricatorApplication::getAllApplications();
+    $applications = self::getAllApplications();
 
     foreach ($applications as $application) {
       if (get_class($application) == $class_name) {
         $selected = $application;
         break;
       }
     }
 
     if (!$selected) {
       throw new Exception("No application '{$class_name}'!");
     }
 
     return $selected;
   }
 
   public static function getAllApplications() {
     static $applications;
 
     if ($applications === null) {
       $apps = id(new PhutilSymbolLoader())
         ->setAncestorClass(__CLASS__)
         ->loadObjects();
 
       // Reorder the applications into "application order". Notably, this
       // ensures their event handlers register in application order.
       $apps = msort($apps, 'getApplicationOrder');
       $apps = mgroup($apps, 'getApplicationGroup');
 
       $group_order = array_keys(self::getApplicationGroups());
       $apps = array_select_keys($apps, $group_order) + $apps;
 
       $apps = array_mergev($apps);
 
       $applications = $apps;
     }
 
     return $applications;
   }
 
   public static function getAllInstalledApplications() {
     $all_applications = self::getAllApplications();
     $apps = array();
     foreach ($all_applications as $app) {
       if (!$app->isInstalled()) {
         continue;
       }
 
       $apps[] = $app;
     }
 
     return $apps;
   }
 
 
   /**
    * Determine if an application is installed, by application class name.
    *
    * To check if an application is installed //and// available to a particular
    * viewer, user @{method:isClassInstalledForViewer}.
    *
    * @param string  Application class name.
    * @return bool   True if the class is installed.
    * @task meta
    */
   public static function isClassInstalled($class) {
     return self::getByClass($class)->isInstalled();
   }
 
 
   /**
    * Determine if an application is installed and available to a viewer, by
    * application class name.
    *
    * To check if an application is installed at all, use
    * @{method:isClassInstalled}.
    *
    * @param string Application class name.
    * @param PhabricatorUser Viewing user.
    * @return bool True if the class is installed for the viewer.
    * @task meta
    */
   public static function isClassInstalledForViewer(
     $class,
     PhabricatorUser $viewer) {
 
     if (!self::isClassInstalled($class)) {
       return false;
     }
 
     return PhabricatorPolicyFilter::hasCapability(
       $viewer,
       self::getByClass($class),
       PhabricatorPolicyCapability::CAN_VIEW);
   }
 
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array_merge(
       array(
         PhabricatorPolicyCapability::CAN_VIEW,
         PhabricatorPolicyCapability::CAN_EDIT,
       ),
       array_keys($this->getCustomCapabilities()));
   }
 
   public function getPolicy($capability) {
     $default = $this->getCustomPolicySetting($capability);
     if ($default) {
       return $default;
     }
 
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         return PhabricatorPolicies::getMostOpenPolicy();
       case PhabricatorPolicyCapability::CAN_EDIT:
         return PhabricatorPolicies::POLICY_ADMIN;
       default:
         $spec = $this->getCustomCapabilitySpecification($capability);
         return idx($spec, 'default', PhabricatorPolicies::POLICY_USER);
     }
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     return false;
   }
 
   public function describeAutomaticCapability($capability) {
     return null;
   }
 
 
 /* -(  Policies  )----------------------------------------------------------- */
 
   protected function getCustomCapabilities() {
     return array();
   }
 
   private function getCustomPolicySetting($capability) {
     if (!$this->isCapabilityEditable($capability)) {
       return null;
     }
 
     $policy_locked = PhabricatorEnv::getEnvConfig('policy.locked');
     if (isset($policy_locked[$capability])) {
       return $policy_locked[$capability];
     }
 
     $config = PhabricatorEnv::getEnvConfig('phabricator.application-settings');
 
     $app = idx($config, $this->getPHID());
     if (!$app) {
       return null;
     }
 
     $policy = idx($app, 'policy');
     if (!$policy) {
       return null;
     }
 
     return idx($policy, $capability);
   }
 
 
   private function getCustomCapabilitySpecification($capability) {
     $custom = $this->getCustomCapabilities();
     if (!isset($custom[$capability])) {
       throw new Exception("Unknown capability '{$capability}'!");
     }
     return $custom[$capability];
   }
 
   public function getCapabilityLabel($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         return pht('Can Use Application');
       case PhabricatorPolicyCapability::CAN_EDIT:
         return pht('Can Configure Application');
     }
 
     $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability);
     if ($capobj) {
       return $capobj->getCapabilityName();
     }
 
     return null;
   }
 
   public function isCapabilityEditable($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         return $this->canUninstall();
       case PhabricatorPolicyCapability::CAN_EDIT:
         return false;
       default:
         $spec = $this->getCustomCapabilitySpecification($capability);
         return idx($spec, 'edit', true);
     }
   }
 
   public function getCapabilityCaption($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         if (!$this->canUninstall()) {
           return pht(
             'This application is required for Phabricator to operate, so all '.
             'users must have access to it.');
         } else {
           return null;
         }
       case PhabricatorPolicyCapability::CAN_EDIT:
         return null;
       default:
         $spec = $this->getCustomCapabilitySpecification($capability);
         return idx($spec, 'caption');
     }
   }
 
   public function getApplicationSearchDocumentTypes() {
     return array();
   }
 
 }
diff --git a/src/applications/cache/PhabricatorCaches.php b/src/applications/cache/PhabricatorCaches.php
index 37708457b7..4e3af7a6a4 100644
--- a/src/applications/cache/PhabricatorCaches.php
+++ b/src/applications/cache/PhabricatorCaches.php
@@ -1,347 +1,347 @@
 <?php
 
 /**
  * @task immutable  Immutable Cache
  * @task setup      Setup Cache
  * @task compress   Compression
  */
 final class PhabricatorCaches {
 
   public static function getNamespace() {
     return PhabricatorEnv::getEnvConfig('phabricator.cache-namespace');
   }
 
   private static function newStackFromCaches(array $caches) {
     $caches = self::addNamespaceToCaches($caches);
     $caches = self::addProfilerToCaches($caches);
     return id(new PhutilKeyValueCacheStack())
       ->setCaches($caches);
   }
 
 
 /* -(  Local Cache  )-------------------------------------------------------- */
 
 
   /**
    * Gets an immutable cache stack.
    *
    * This stack trades mutability away for improved performance. Normally, it is
    * APC + DB.
    *
    * In the general case with multiple web frontends, this stack can not be
    * cleared, so it is only appropriate for use if the value of a given key is
    * permanent and immutable.
    *
    * @return PhutilKeyValueCacheStack Best immutable stack available.
    * @task immutable
    */
   public static function getImmutableCache() {
     static $cache;
     if (!$cache) {
       $caches = self::buildImmutableCaches();
       $cache = self::newStackFromCaches($caches);
     }
     return $cache;
   }
 
 
   /**
    * Build the immutable cache stack.
    *
    * @return list<PhutilKeyValueCache> List of caches.
    * @task immutable
    */
   private static function buildImmutableCaches() {
     $caches = array();
 
     $apc = new PhutilAPCKeyValueCache();
     if ($apc->isAvailable()) {
       $caches[] = $apc;
     }
 
     $caches[] = new PhabricatorKeyValueDatabaseCache();
 
     return $caches;
   }
 
 
 /* -(  Repository Graph Cache  )--------------------------------------------- */
 
 
   public static function getRepositoryGraphL1Cache() {
     static $cache;
     if (!$cache) {
       $caches = self::buildRepositoryGraphL1Caches();
       $cache = self::newStackFromCaches($caches);
     }
     return $cache;
   }
 
   private static function buildRepositoryGraphL1Caches() {
     $caches = array();
 
     $request = new PhutilInRequestKeyValueCache();
     $request->setLimit(32);
     $caches[] = $request;
 
     $apc = new PhutilAPCKeyValueCache();
     if ($apc->isAvailable()) {
       $caches[] = $apc;
     }
 
     return $caches;
   }
 
   public static function getRepositoryGraphL2Cache() {
     static $cache;
     if (!$cache) {
       $caches = self::buildRepositoryGraphL2Caches();
       $cache = self::newStackFromCaches($caches);
     }
     return $cache;
   }
 
   private static function buildRepositoryGraphL2Caches() {
     $caches = array();
     $caches[] = new PhabricatorKeyValueDatabaseCache();
     return $caches;
   }
 
 
 /* -(  Setup Cache  )-------------------------------------------------------- */
 
 
   /**
    * Highly specialized cache for performing setup checks. We use this cache
    * to determine if we need to run expensive setup checks when the page
    * loads. Without it, we would need to run these checks every time.
    *
    * Normally, this cache is just APC. In the absence of APC, this cache
    * degrades into a slow, quirky on-disk cache.
    *
    * NOTE: Do not use this cache for anything else! It is not a general-purpose
    * cache!
    *
    * @return PhutilKeyValueCacheStack Most qualified available cache stack.
    * @task setup
    */
   public static function getSetupCache() {
     static $cache;
     if (!$cache) {
       $caches = self::buildSetupCaches();
       $cache = self::newStackFromCaches($caches);
     }
     return $cache;
   }
 
 
   /**
    * @task setup
    */
   private static function buildSetupCaches() {
     // In most cases, we should have APC. This is an ideal cache for our
     // purposes -- it's fast and empties on server restart.
     $apc = new PhutilAPCKeyValueCache();
     if ($apc->isAvailable()) {
       return array($apc);
     }
 
     // If we don't have APC, build a poor approximation on disk. This is still
     // much better than nothing; some setup steps are quite slow.
     $disk_path = self::getSetupCacheDiskCachePath();
     if ($disk_path) {
       $disk = new PhutilOnDiskKeyValueCache();
       $disk->setCacheFile($disk_path);
       $disk->setWait(0.1);
       if ($disk->isAvailable()) {
         return array($disk);
       }
     }
 
     return array();
   }
 
 
   /**
    * @task setup
    */
   private static function getSetupCacheDiskCachePath() {
     // The difficulty here is in choosing a path which will change on server
     // restart (we MUST have this property), but as rarely as possible
     // otherwise (we desire this property to give the cache the best hit rate
     // we can).
 
     // In some setups, the parent PID is more stable and longer-lived that the
     // PID (e.g., under apache, our PID will be a worker while the ppid will
     // be the main httpd process). If we're confident we're running under such
     // a setup, we can try to use the PPID as the basis for our cache instead
     // of our own PID.
     $use_ppid = false;
 
     switch (php_sapi_name()) {
       case 'cli-server':
         // This is the PHP5.4+ built-in webserver. We should use the pid
         // (the server), not the ppid (probably a shell or something).
         $use_ppid = false;
         break;
       case 'fpm-fcgi':
         // We should be safe to use PPID here.
         $use_ppid = true;
         break;
       case 'apache2handler':
         // We're definitely safe to use the PPID.
         $use_ppid = true;
         break;
     }
 
     $pid_basis = getmypid();
     if ($use_ppid) {
       if (function_exists('posix_getppid')) {
         $parent_pid = posix_getppid();
         // On most systems, normal processes can never have PIDs lower than 100,
         // so something likely went wrong if we we get one of these.
         if ($parent_pid > 100) {
           $pid_basis = $parent_pid;
         }
       }
     }
 
     // If possible, we also want to know when the process launched, so we can
     // drop the cache if a process restarts but gets the same PID an earlier
     // process had. "/proc" is not available everywhere (e.g., not on OSX), but
     // check if we have it.
     $epoch_basis = null;
     $stat = @stat("/proc/{$pid_basis}");
     if ($stat !== false) {
       $epoch_basis = $stat['ctime'];
     }
 
     $tmp_dir = sys_get_temp_dir();
 
     $tmp_path = $tmp_dir.DIRECTORY_SEPARATOR.'phabricator-setup';
     if (!file_exists($tmp_path)) {
       @mkdir($tmp_path);
     }
 
     $is_ok = self::testTemporaryDirectory($tmp_path);
     if (!$is_ok) {
       $tmp_path = $tmp_dir;
       $is_ok = self::testTemporaryDirectory($tmp_path);
       if (!$is_ok) {
         // We can't find anywhere to write the cache, so just bail.
         return null;
       }
     }
 
     $tmp_name = 'setup-'.$pid_basis;
     if ($epoch_basis) {
       $tmp_name .= '.'.$epoch_basis;
     }
     $tmp_name .= '.cache';
 
     return $tmp_path.DIRECTORY_SEPARATOR.$tmp_name;
   }
 
 
   /**
    * @task setup
    */
   private static function testTemporaryDirectory($dir) {
     if (!@file_exists($dir)) {
       return false;
     }
     if (!@is_dir($dir)) {
       return false;
     }
     if (!@is_writable($dir)) {
       return false;
     }
 
     return true;
   }
 
   private static function addProfilerToCaches(array $caches) {
     foreach ($caches as $key => $cache) {
       $pcache = new PhutilKeyValueCacheProfiler($cache);
       $pcache->setProfiler(PhutilServiceProfiler::getInstance());
       $caches[$key] = $pcache;
     }
     return $caches;
   }
 
   private static function addNamespaceToCaches(array $caches) {
-    $namespace = PhabricatorCaches::getNamespace();
+    $namespace = self::getNamespace();
     if (!$namespace) {
       return $caches;
     }
 
     foreach ($caches as $key => $cache) {
       $ncache = new PhutilKeyValueCacheNamespace($cache);
       $ncache->setNamespace($namespace);
       $caches[$key] = $ncache;
     }
 
     return $caches;
   }
 
 
   /**
    * Deflate a value, if deflation is available and has an impact.
    *
    * If the value is larger than 1KB, we have `gzdeflate()`, we successfully
    * can deflate it, and it benefits from deflation, we deflate it. Otherwise
    * we leave it as-is.
    *
    * Data can later be inflated with @{method:inflateData}.
    *
    * @param string String to attempt to deflate.
    * @return string|null Deflated string, or null if it was not deflated.
    * @task compress
    */
   public static function maybeDeflateData($value) {
     $len = strlen($value);
     if ($len <= 1024) {
       return null;
     }
 
     if (!function_exists('gzdeflate')) {
       return null;
     }
 
     $deflated = gzdeflate($value);
     if ($deflated === false) {
       return null;
     }
 
     $deflated_len = strlen($deflated);
     if ($deflated_len >= ($len / 2)) {
       return null;
     }
 
     return $deflated;
   }
 
 
   /**
    * Inflate data previously deflated by @{method:maybeDeflateData}.
    *
    * @param string Deflated data, from @{method:maybeDeflateData}.
    * @return string Original, uncompressed data.
    * @task compress
    */
   public static function inflateData($value) {
     if (!function_exists('gzinflate')) {
       throw new Exception(
         pht('gzinflate() is not available; unable to read deflated data!'));
     }
 
     $value = gzinflate($value);
     if ($value === false) {
       throw new Exception(pht('Failed to inflate data!'));
     }
 
     return $value;
   }
 
 
 }
diff --git a/src/applications/calendar/controller/PhabricatorCalendarBrowseController.php b/src/applications/calendar/controller/PhabricatorCalendarBrowseController.php
deleted file mode 100644
index ba2017cba9..0000000000
--- a/src/applications/calendar/controller/PhabricatorCalendarBrowseController.php
+++ /dev/null
@@ -1,97 +0,0 @@
-<?php
-
-final class PhabricatorCalendarBrowseController
-  extends PhabricatorCalendarController {
-
-  public function shouldAllowPublic() {
-    return true;
-  }
-
-  public function handleRequest(AphrontRequest $request) {
-    $viewer = $this->getViewer();
-
-    $now     = time();
-    $year_d  = phabricator_format_local_time($now, $viewer, 'Y');
-    $year    = $request->getInt('year', $year_d);
-    $month_d = phabricator_format_local_time($now, $viewer, 'm');
-    $month   = $request->getInt('month', $month_d);
-    $day   = phabricator_format_local_time($now, $viewer, 'j');
-
-    $holidays = id(new PhabricatorCalendarHoliday())->loadAllWhere(
-      'day BETWEEN %s AND %s',
-      "{$year}-{$month}-01",
-      "{$year}-{$month}-31");
-
-    $statuses = id(new PhabricatorCalendarEventQuery())
-      ->setViewer($viewer)
-      ->withDateRange(
-        strtotime("{$year}-{$month}-01"),
-        strtotime("{$year}-{$month}-01 next month"))
-      ->execute();
-
-    if ($month == $month_d && $year == $year_d) {
-      $month_view = new PHUICalendarMonthView($month, $year, $day);
-    } else {
-      $month_view = new PHUICalendarMonthView($month, $year);
-    }
-
-    $month_view->setBrowseURI($request->getRequestURI());
-    $month_view->setUser($viewer);
-    $month_view->setHolidays($holidays);
-
-    $phids = mpull($statuses, 'getUserPHID');
-    $handles = $viewer->loadHandles($phids);
-
-    /* Assign Colors */
-    $unique = array_unique($phids);
-    $allblue = false;
-    $calcolors = CalendarColors::getColors();
-    if (count($unique) > count($calcolors)) {
-      $allblue = true;
-    }
-    $i = 0;
-    $eventcolor = array();
-    foreach ($unique as $phid) {
-      if ($allblue) {
-        $eventcolor[$phid] = CalendarColors::COLOR_SKY;
-      } else {
-        $eventcolor[$phid] = $calcolors[$i];
-      }
-      $i++;
-    }
-
-    foreach ($statuses as $status) {
-      $event = new AphrontCalendarEventView();
-      $event->setEpochRange($status->getDateFrom(), $status->getDateTo());
-
-      $name_text = $handles[$status->getUserPHID()]->getName();
-      $status_text = $status->getHumanStatus();
-      $event->setUserPHID($status->getUserPHID());
-      $event->setDescription(pht('%s (%s)', $name_text, $status_text));
-      $event->setName($status_text);
-      $event->setEventID($status->getID());
-      $event->setColor($eventcolor[$status->getUserPHID()]);
-      $month_view->addEvent($event);
-    }
-
-    $date = new DateTime("{$year}-{$month}-01");
-    $crumbs = $this->buildApplicationCrumbs();
-    $crumbs->addTextCrumb(pht('All Events'));
-    $crumbs->addTextCrumb($date->format('F Y'));
-
-    $nav = $this->buildSideNavView();
-    $nav->selectFilter('all/');
-    $nav->appendChild(
-      array(
-        $crumbs,
-        $month_view,
-      ));
-
-    return $this->buildApplicationPage(
-     $nav,
-     array(
-        'title' => pht('Calendar'),
-      ));
-  }
-
-}
diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php b/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php
index 38d12b099b..e31b9bdf0e 100644
--- a/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php
+++ b/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php
@@ -1,315 +1,345 @@
 <?php
 
 final class PhabricatorCalendarEventEditController
   extends PhabricatorCalendarController {
 
   private $id;
 
   public function willProcessRequest(array $data) {
     $this->id = idx($data, 'id');
   }
 
   public function isCreate() {
     return !$this->id;
   }
 
   public function processRequest() {
     $request = $this->getRequest();
     $user = $request->getUser();
     $user_phid = $user->getPHID();
     $error_name = true;
     $error_start_date = true;
     $error_end_date = true;
     $validation_exception = null;
 
     if ($this->isCreate()) {
       $event = PhabricatorCalendarEvent::initializeNewCalendarEvent($user);
       $end_value = AphrontFormDateControlValue::newFromEpoch($user, time());
       $start_value = AphrontFormDateControlValue::newFromEpoch($user, time());
       $submit_label = pht('Create');
       $page_title = pht('Create Event');
       $redirect = 'created';
       $subscribers = array();
       $invitees = array($user_phid);
       $cancel_uri = $this->getApplicationURI();
     } else {
       $event = id(new PhabricatorCalendarEventQuery())
         ->setViewer($user)
         ->withIDs(array($this->id))
         ->requireCapabilities(
           array(
             PhabricatorPolicyCapability::CAN_VIEW,
             PhabricatorPolicyCapability::CAN_EDIT,
           ))
         ->executeOne();
       if (!$event) {
         return new Aphront404Response();
       }
 
       $end_value = AphrontFormDateControlValue::newFromEpoch(
         $user,
         $event->getDateTo());
       $start_value = AphrontFormDateControlValue::newFromEpoch(
         $user,
         $event->getDateFrom());
 
       $submit_label = pht('Update');
       $page_title   = pht('Update Event');
 
       $subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID(
         $event->getPHID());
 
       $invitees = array();
       foreach ($event->getInvitees() as $invitee) {
         if ($invitee->isUninvited()) {
           continue;
         } else {
           $invitees[] = $invitee->getInviteePHID();
         }
       }
 
       $cancel_uri = '/'.$event->getMonogram();
     }
 
     $name = $event->getName();
     $description = $event->getDescription();
     $type = $event->getStatus();
+    $is_all_day = $event->getIsAllDay();
 
     $current_policies = id(new PhabricatorPolicyQuery())
       ->setViewer($user)
       ->setObject($event)
       ->execute();
 
     if ($request->isFormPost()) {
       $xactions = array();
       $name = $request->getStr('name');
       $type = $request->getInt('status');
 
       $start_value = AphrontFormDateControlValue::newFromRequest(
         $request,
         'start');
       $end_value = AphrontFormDateControlValue::newFromRequest(
         $request,
         'end');
       $description = $request->getStr('description');
       $subscribers = $request->getArr('subscribers');
       $edit_policy = $request->getStr('editPolicy');
       $view_policy = $request->getStr('viewPolicy');
+      $is_all_day = $request->getStr('isAllDay');
 
       $invitees = $request->getArr('invitees');
       $new_invitees = $this->getNewInviteeList($invitees, $event);
       $status_attending = PhabricatorCalendarEventInvitee::STATUS_ATTENDING;
       if ($this->isCreate()) {
         $status = idx($new_invitees, $user->getPHID());
         if ($status) {
           $new_invitees[$user->getPHID()] = $status_attending;
         }
       }
 
       $xactions[] = id(new PhabricatorCalendarEventTransaction())
         ->setTransactionType(
           PhabricatorCalendarEventTransaction::TYPE_NAME)
         ->setNewValue($name);
 
+      $xactions[] = id(new PhabricatorCalendarEventTransaction())
+        ->setTransactionType(
+          PhabricatorCalendarEventTransaction::TYPE_ALL_DAY)
+        ->setNewValue($is_all_day);
+
       $xactions[] = id(new PhabricatorCalendarEventTransaction())
         ->setTransactionType(
           PhabricatorCalendarEventTransaction::TYPE_START_DATE)
         ->setNewValue($start_value);
 
       $xactions[] = id(new PhabricatorCalendarEventTransaction())
         ->setTransactionType(
           PhabricatorCalendarEventTransaction::TYPE_END_DATE)
         ->setNewValue($end_value);
 
       $xactions[] = id(new PhabricatorCalendarEventTransaction())
         ->setTransactionType(
           PhabricatorCalendarEventTransaction::TYPE_STATUS)
         ->setNewValue($type);
 
       $xactions[] = id(new PhabricatorCalendarEventTransaction())
         ->setTransactionType(
           PhabricatorTransactions::TYPE_SUBSCRIBERS)
         ->setNewValue(array('=' => array_fuse($subscribers)));
 
       $xactions[] = id(new PhabricatorCalendarEventTransaction())
         ->setTransactionType(
           PhabricatorCalendarEventTransaction::TYPE_INVITE)
         ->setNewValue($new_invitees);
 
       $xactions[] = id(new PhabricatorCalendarEventTransaction())
         ->setTransactionType(
           PhabricatorCalendarEventTransaction::TYPE_DESCRIPTION)
         ->setNewValue($description);
 
       $xactions[] = id(new PhabricatorCalendarEventTransaction())
         ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
         ->setNewValue($request->getStr('viewPolicy'));
 
       $xactions[] = id(new PhabricatorCalendarEventTransaction())
         ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY)
         ->setNewValue($request->getStr('editPolicy'));
 
       $editor = id(new PhabricatorCalendarEventEditor())
         ->setActor($user)
         ->setContentSourceFromRequest($request)
         ->setContinueOnNoEffect(true);
 
       try {
         $xactions = $editor->applyTransactions($event, $xactions);
         $response = id(new AphrontRedirectResponse());
         return $response->setURI('/E'.$event->getID());
       } catch (PhabricatorApplicationTransactionValidationException $ex) {
         $validation_exception = $ex;
         $error_name = $ex->getShortMessage(
             PhabricatorCalendarEventTransaction::TYPE_NAME);
         $error_start_date = $ex->getShortMessage(
             PhabricatorCalendarEventTransaction::TYPE_START_DATE);
         $error_end_date = $ex->getShortMessage(
             PhabricatorCalendarEventTransaction::TYPE_END_DATE);
 
         $event->setViewPolicy($view_policy);
         $event->setEditPolicy($edit_policy);
       }
     }
 
+    $all_day_id = celerity_generate_unique_node_id();
+    $start_date_id = celerity_generate_unique_node_id();
+    $end_date_id = celerity_generate_unique_node_id();
+
+    Javelin::initBehavior('event-all-day', array(
+      'allDayID' => $all_day_id,
+      'startDateID' => $start_date_id,
+      'endDateID' => $end_date_id,
+    ));
+
     $name = id(new AphrontFormTextControl())
       ->setLabel(pht('Name'))
       ->setName('name')
       ->setValue($name)
       ->setError($error_name);
 
     $status_select = id(new AphrontFormSelectControl())
       ->setLabel(pht('Status'))
       ->setName('status')
       ->setValue($type)
       ->setOptions($event->getStatusOptions());
 
+    $all_day_checkbox = id(new AphrontFormCheckboxControl())
+      ->addCheckbox(
+        'isAllDay',
+        1,
+        pht('All Day Event'),
+        $is_all_day,
+        $all_day_id);
+
     $start_control = id(new AphrontFormDateControl())
       ->setUser($user)
       ->setName('start')
       ->setLabel(pht('Start'))
       ->setError($error_start_date)
-      ->setValue($start_value);
+      ->setValue($start_value)
+      ->setID($start_date_id)
+      ->setIsTimeDisabled($is_all_day);
 
     $end_control = id(new AphrontFormDateControl())
       ->setUser($user)
       ->setName('end')
       ->setLabel(pht('End'))
       ->setError($error_end_date)
-      ->setValue($end_value);
+      ->setValue($end_value)
+      ->setID($end_date_id)
+      ->setIsTimeDisabled($is_all_day);
 
     $description = id(new AphrontFormTextAreaControl())
       ->setLabel(pht('Description'))
       ->setName('description')
       ->setValue($description);
 
     $view_policies = id(new AphrontFormPolicyControl())
       ->setUser($user)
       ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
       ->setPolicyObject($event)
       ->setPolicies($current_policies)
       ->setName('viewPolicy');
     $edit_policies = id(new AphrontFormPolicyControl())
       ->setUser($user)
       ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
       ->setPolicyObject($event)
       ->setPolicies($current_policies)
       ->setName('editPolicy');
 
     $subscribers = id(new AphrontFormTokenizerControl())
       ->setLabel(pht('Subscribers'))
       ->setName('subscribers')
       ->setValue($subscribers)
       ->setUser($user)
       ->setDatasource(new PhabricatorMetaMTAMailableDatasource());
 
     $invitees = id(new AphrontFormTokenizerControl())
       ->setLabel(pht('Invitees'))
       ->setName('invitees')
       ->setValue($invitees)
       ->setUser($user)
       ->setDatasource(new PhabricatorMetaMTAMailableDatasource());
 
     $form = id(new AphrontFormView())
       ->setUser($user)
       ->appendChild($name)
       ->appendChild($status_select)
+      ->appendChild($all_day_checkbox)
       ->appendChild($start_control)
       ->appendChild($end_control)
       ->appendControl($view_policies)
       ->appendControl($edit_policies)
       ->appendControl($subscribers)
       ->appendControl($invitees)
       ->appendChild($description);
 
 
     if ($request->isAjax()) {
       return $this->newDialog()
         ->setTitle($page_title)
         ->setWidth(AphrontDialogView::WIDTH_FULL)
         ->appendForm($form)
         ->addCancelButton($cancel_uri)
         ->addSubmitButton($submit_label);
     }
 
     $submit = id(new AphrontFormSubmitControl())
       ->addCancelButton($cancel_uri)
       ->setValue($submit_label);
 
     $form->appendChild($submit);
 
     $form_box = id(new PHUIObjectBoxView())
       ->setHeaderText($page_title)
       ->setForm($form);
 
     $crumbs = $this->buildApplicationCrumbs();
 
     if (!$this->isCreate()) {
       $crumbs->addTextCrumb('E'.$event->getId(), '/E'.$event->getId());
     }
 
     $crumbs->addTextCrumb($page_title);
 
     $object_box = id(new PHUIObjectBoxView())
       ->setHeaderText($page_title)
       ->setValidationException($validation_exception)
       ->appendChild($form);
 
     return $this->buildApplicationPage(
       array(
         $crumbs,
         $object_box,
         ),
       array(
         'title' => $page_title,
       ));
   }
 
 
   public function getNewInviteeList(array $phids, $event) {
     $invitees = $event->getInvitees();
     $invitees = mpull($invitees, null, 'getInviteePHID');
     $invited_status = PhabricatorCalendarEventInvitee::STATUS_INVITED;
     $uninvited_status = PhabricatorCalendarEventInvitee::STATUS_UNINVITED;
     $phids = array_fuse($phids);
 
     $new = array();
     foreach ($phids as $phid) {
       $old_status = $event->getUserInviteStatus($phid);
       if ($old_status != $uninvited_status) {
         continue;
       }
       $new[$phid] = $invited_status;
     }
 
     foreach ($invitees as $invitee) {
       $deleted_invitee = !idx($phids, $invitee->getInviteePHID());
       if ($deleted_invitee) {
         $new[$invitee->getInviteePHID()] = $uninvited_status;
       }
     }
 
     return $new;
   }
 
 }
diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php
index 6f6fa66e6e..742ec95e3f 100644
--- a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php
+++ b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php
@@ -1,257 +1,275 @@
 <?php
 
 final class PhabricatorCalendarEventViewController
   extends PhabricatorCalendarController {
 
   private $id;
 
   public function shouldAllowPublic() {
     return true;
   }
 
   public function willProcessRequest(array $data) {
     $this->id = $data['id'];
   }
 
   public function processRequest() {
     $request = $this->getRequest();
     $viewer = $request->getUser();
 
     $event = id(new PhabricatorCalendarEventQuery())
       ->setViewer($viewer)
       ->withIDs(array($this->id))
       ->executeOne();
     if (!$event) {
       return new Aphront404Response();
     }
 
     $title = 'E'.$event->getID();
     $page_title = $title.' '.$event->getName();
     $crumbs = $this->buildApplicationCrumbs();
     $crumbs->addTextCrumb($title, '/E'.$event->getID());
 
     $timeline = $this->buildTransactionTimeline(
       $event,
       new PhabricatorCalendarEventTransactionQuery());
 
     $header = $this->buildHeaderView($event);
     $actions = $this->buildActionView($event);
     $properties = $this->buildPropertyView($event);
 
     $properties->setActionList($actions);
     $box = id(new PHUIObjectBoxView())
       ->setHeader($header)
       ->addPropertyList($properties);
 
     $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
     $add_comment_header = $is_serious
       ? pht('Add Comment')
       : pht('Add To Plate');
     $draft = PhabricatorDraft::newFromUserAndKey($viewer, $event->getPHID());
     $add_comment_form = id(new PhabricatorApplicationTransactionCommentView())
       ->setUser($viewer)
       ->setObjectPHID($event->getPHID())
       ->setDraft($draft)
       ->setHeaderText($add_comment_header)
       ->setAction(
         $this->getApplicationURI('/event/comment/'.$event->getID().'/'))
       ->setSubmitButtonName(pht('Add Comment'));
 
     return $this->buildApplicationPage(
       array(
         $crumbs,
         $box,
         $timeline,
         $add_comment_form,
       ),
       array(
         'title' => $page_title,
       ));
   }
 
   private function buildHeaderView(PhabricatorCalendarEvent $event) {
     $viewer = $this->getRequest()->getUser();
     $id = $event->getID();
 
     $is_cancelled = $event->getIsCancelled();
     $icon = $is_cancelled ? ('fa-times') : ('fa-calendar');
     $color = $is_cancelled ? ('grey') : ('green');
     $status = $is_cancelled ? pht('Cancelled') : pht('Active');
 
     $invite_status = $event->getUserInviteStatus($viewer->getPHID());
     $status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED;
     $is_invite_pending = ($invite_status == $status_invited);
 
     $header = id(new PHUIHeaderView())
       ->setUser($viewer)
       ->setHeader($event->getName())
       ->setStatus($icon, $color, $status)
       ->setPolicyObject($event);
 
     if ($is_invite_pending) {
       $decline_button = id(new PHUIButtonView())
         ->setTag('a')
         ->setIcon(id(new PHUIIconView())
           ->setIconFont('fa-times grey'))
         ->setHref($this->getApplicationURI("/event/decline/{$id}/"))
         ->setWorkflow(true)
         ->setText(pht('Decline'));
 
       $accept_button = id(new PHUIButtonView())
         ->setTag('a')
         ->setIcon(id(new PHUIIconView())
           ->setIconFont('fa-check green'))
         ->setHref($this->getApplicationURI("/event/accept/{$id}/"))
         ->setWorkflow(true)
         ->setText(pht('Accept'));
 
       $header->addActionLink($decline_button)
         ->addActionLink($accept_button);
     }
     return $header;
   }
 
   private function buildActionView(PhabricatorCalendarEvent $event) {
     $viewer = $this->getRequest()->getUser();
     $id = $event->getID();
     $is_cancelled = $event->getIsCancelled();
     $is_attending = $event->getIsUserAttending($viewer->getPHID());
 
     $actions = id(new PhabricatorActionListView())
       ->setObjectURI($this->getApplicationURI('event/'.$id.'/'))
       ->setUser($viewer)
       ->setObject($event);
 
     $can_edit = PhabricatorPolicyFilter::hasCapability(
       $viewer,
       $event,
       PhabricatorPolicyCapability::CAN_EDIT);
 
     $actions->addAction(
       id(new PhabricatorActionView())
         ->setName(pht('Edit Event'))
         ->setIcon('fa-pencil')
         ->setHref($this->getApplicationURI("event/edit/{$id}/"))
         ->setDisabled(!$can_edit)
         ->setWorkflow(!$can_edit));
 
     if ($is_attending) {
       $actions->addAction(
         id(new PhabricatorActionView())
           ->setName(pht('Decline Event'))
           ->setIcon('fa-user-times')
           ->setHref($this->getApplicationURI("event/join/{$id}/"))
           ->setWorkflow(true));
     } else {
       $actions->addAction(
         id(new PhabricatorActionView())
           ->setName(pht('Join Event'))
           ->setIcon('fa-user-plus')
           ->setHref($this->getApplicationURI("event/join/{$id}/"))
           ->setWorkflow(true));
     }
 
     if ($is_cancelled) {
       $actions->addAction(
         id(new PhabricatorActionView())
           ->setName(pht('Reinstate Event'))
           ->setIcon('fa-plus')
           ->setHref($this->getApplicationURI("event/cancel/{$id}/"))
           ->setDisabled(!$can_edit)
           ->setWorkflow(true));
     } else {
       $actions->addAction(
         id(new PhabricatorActionView())
           ->setName(pht('Cancel Event'))
           ->setIcon('fa-times')
           ->setHref($this->getApplicationURI("event/cancel/{$id}/"))
           ->setDisabled(!$can_edit)
           ->setWorkflow(true));
     }
 
     return $actions;
   }
 
   private function buildPropertyView(PhabricatorCalendarEvent $event) {
     $viewer = $this->getRequest()->getUser();
 
     $properties = id(new PHUIPropertyListView())
       ->setUser($viewer)
       ->setObject($event);
 
-    $properties->addProperty(
-      pht('Starts'),
-      phabricator_datetime($event->getDateFrom(), $viewer));
+    if ($event->getIsAllDay()) {
+      $date_start = phabricator_date($event->getDateFrom(), $viewer);
+      $date_end = phabricator_date($event->getDateTo(), $viewer);
+
+      if ($date_start == $date_end) {
+        $properties->addProperty(
+          pht('Time'),
+          phabricator_date($event->getDateFrom(), $viewer));
+      } else {
+        $properties->addProperty(
+          pht('Starts'),
+          phabricator_date($event->getDateFrom(), $viewer));
+        $properties->addProperty(
+          pht('Ends'),
+          phabricator_date($event->getDateTo(), $viewer));
+      }
+    } else {
+      $properties->addProperty(
+        pht('Starts'),
+        phabricator_datetime($event->getDateFrom(), $viewer));
 
-    $properties->addProperty(
-      pht('Ends'),
-      phabricator_datetime($event->getDateTo(), $viewer));
+      $properties->addProperty(
+        pht('Ends'),
+        phabricator_datetime($event->getDateTo(), $viewer));
+    }
 
     $properties->addProperty(
       pht('Host'),
       $viewer->renderHandle($event->getUserPHID()));
 
     $invitees = $event->getInvitees();
     foreach ($invitees as $key => $invitee) {
       if ($invitee->isUninvited()) {
         unset($invitees[$key]);
       }
     }
 
     if ($invitees) {
       $invitee_list = new PHUIStatusListView();
 
       $icon_invited = PHUIStatusItemView::ICON_OPEN;
       $icon_attending = PHUIStatusItemView::ICON_ACCEPT;
       $icon_declined = PHUIStatusItemView::ICON_REJECT;
 
       $status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED;
       $status_attending = PhabricatorCalendarEventInvitee::STATUS_ATTENDING;
       $status_declined = PhabricatorCalendarEventInvitee::STATUS_DECLINED;
 
       $icon_map = array(
         $status_invited => $icon_invited,
         $status_attending => $icon_attending,
         $status_declined => $icon_declined,
       );
 
       $icon_color_map = array(
         $status_invited => null,
         $status_attending => 'green',
         $status_declined => 'red',
       );
 
       foreach ($invitees as $invitee) {
         $item = new PHUIStatusItemView();
         $invitee_phid = $invitee->getInviteePHID();
         $status = $invitee->getStatus();
         $target = $viewer->renderHandle($invitee_phid);
         $icon = $icon_map[$status];
         $icon_color = $icon_color_map[$status];
 
         $item->setIcon($icon, $icon_color)
           ->setTarget($target);
         $invitee_list->addItem($item);
       }
     } else {
       $invitee_list = phutil_tag(
         'em',
         array(),
         pht('None'));
     }
 
     $properties->addProperty(
       pht('Invitees'),
       $invitee_list);
 
     $properties->invokeWillRenderEvent();
 
     $properties->addSectionHeader(
       pht('Description'),
       PHUIPropertyListView::ICON_SUMMARY);
     $properties->addTextContent($event->getDescription());
 
     return $properties;
   }
 
 }
diff --git a/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php b/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php
index 09ab1309eb..6e80d19236 100644
--- a/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php
+++ b/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php
@@ -1,343 +1,362 @@
 <?php
 
 final class PhabricatorCalendarEventEditor
   extends PhabricatorApplicationTransactionEditor {
 
   public function getEditorApplicationClass() {
     return 'PhabricatorCalendarApplication';
   }
 
   public function getEditorObjectsDescription() {
     return pht('Calendar');
   }
 
   public function getTransactionTypes() {
     $types = parent::getTransactionTypes();
 
     $types[] = PhabricatorCalendarEventTransaction::TYPE_NAME;
     $types[] = PhabricatorCalendarEventTransaction::TYPE_START_DATE;
     $types[] = PhabricatorCalendarEventTransaction::TYPE_END_DATE;
     $types[] = PhabricatorCalendarEventTransaction::TYPE_STATUS;
     $types[] = PhabricatorCalendarEventTransaction::TYPE_DESCRIPTION;
     $types[] = PhabricatorCalendarEventTransaction::TYPE_CANCEL;
     $types[] = PhabricatorCalendarEventTransaction::TYPE_INVITE;
+    $types[] = PhabricatorCalendarEventTransaction::TYPE_ALL_DAY;
 
     $types[] = PhabricatorTransactions::TYPE_COMMENT;
     $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
     $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
 
     return $types;
   }
 
   protected function getCustomTransactionOldValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     switch ($xaction->getTransactionType()) {
       case PhabricatorCalendarEventTransaction::TYPE_NAME:
         return $object->getName();
       case PhabricatorCalendarEventTransaction::TYPE_START_DATE:
         return $object->getDateFrom();
       case PhabricatorCalendarEventTransaction::TYPE_END_DATE:
         return $object->getDateTo();
       case PhabricatorCalendarEventTransaction::TYPE_STATUS:
         $status = $object->getStatus();
         if ($status === null) {
           return null;
         }
         return (int)$status;
       case PhabricatorCalendarEventTransaction::TYPE_DESCRIPTION:
         return $object->getDescription();
       case PhabricatorCalendarEventTransaction::TYPE_CANCEL:
         return $object->getIsCancelled();
+      case PhabricatorCalendarEventTransaction::TYPE_ALL_DAY:
+        return (int)$object->getIsAllDay();
       case PhabricatorCalendarEventTransaction::TYPE_INVITE:
         $map = $xaction->getNewValue();
         $phids = array_keys($map);
         $invitees = array();
 
         if ($map && !$this->getIsNewObject()) {
           $invitees = id(new PhabricatorCalendarEventInviteeQuery())
             ->setViewer($this->getActor())
             ->withEventPHIDs(array($object->getPHID()))
             ->withInviteePHIDs($phids)
             ->execute();
           $invitees = mpull($invitees, null, 'getInviteePHID');
         }
 
         $old = array();
         foreach ($phids as $phid) {
           $invitee = idx($invitees, $phid);
           if ($invitee) {
             $old[$phid] = $invitee->getStatus();
           } else {
             $old[$phid] = PhabricatorCalendarEventInvitee::STATUS_UNINVITED;
           }
         }
         return $old;
     }
 
     return parent::getCustomTransactionOldValue($object, $xaction);
   }
 
   protected function getCustomTransactionNewValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     switch ($xaction->getTransactionType()) {
       case PhabricatorCalendarEventTransaction::TYPE_NAME:
       case PhabricatorCalendarEventTransaction::TYPE_DESCRIPTION:
       case PhabricatorCalendarEventTransaction::TYPE_CANCEL:
       case PhabricatorCalendarEventTransaction::TYPE_INVITE:
         return $xaction->getNewValue();
+      case PhabricatorCalendarEventTransaction::TYPE_ALL_DAY:
+        return (int)$xaction->getNewValue();
       case PhabricatorCalendarEventTransaction::TYPE_STATUS:
         return (int)$xaction->getNewValue();
       case PhabricatorCalendarEventTransaction::TYPE_START_DATE:
       case PhabricatorCalendarEventTransaction::TYPE_END_DATE:
         return $xaction->getNewValue()->getEpoch();
     }
 
     return parent::getCustomTransactionNewValue($object, $xaction);
   }
 
   protected function applyCustomInternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case PhabricatorCalendarEventTransaction::TYPE_NAME:
         $object->setName($xaction->getNewValue());
         return;
       case PhabricatorCalendarEventTransaction::TYPE_START_DATE:
         $object->setDateFrom($xaction->getNewValue());
         return;
       case PhabricatorCalendarEventTransaction::TYPE_END_DATE:
         $object->setDateTo($xaction->getNewValue());
         return;
       case PhabricatorCalendarEventTransaction::TYPE_STATUS:
         $object->setStatus($xaction->getNewValue());
         return;
       case PhabricatorCalendarEventTransaction::TYPE_DESCRIPTION:
         $object->setDescription($xaction->getNewValue());
         return;
       case PhabricatorCalendarEventTransaction::TYPE_CANCEL:
         $object->setIsCancelled((int)$xaction->getNewValue());
         return;
+      case PhabricatorCalendarEventTransaction::TYPE_ALL_DAY:
+        $object->setIsAllDay((int)$xaction->getNewValue());
+        return;
       case PhabricatorCalendarEventTransaction::TYPE_INVITE:
       case PhabricatorTransactions::TYPE_COMMENT:
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
       case PhabricatorTransactions::TYPE_EDGE:
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         return;
     }
 
     return parent::applyCustomInternalTransaction($object, $xaction);
   }
 
   protected function applyCustomExternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case PhabricatorCalendarEventTransaction::TYPE_NAME:
       case PhabricatorCalendarEventTransaction::TYPE_START_DATE:
       case PhabricatorCalendarEventTransaction::TYPE_END_DATE:
       case PhabricatorCalendarEventTransaction::TYPE_STATUS:
       case PhabricatorCalendarEventTransaction::TYPE_DESCRIPTION:
       case PhabricatorCalendarEventTransaction::TYPE_CANCEL:
+      case PhabricatorCalendarEventTransaction::TYPE_ALL_DAY:
         return;
       case PhabricatorCalendarEventTransaction::TYPE_INVITE:
         $map = $xaction->getNewValue();
         $phids = array_keys($map);
         $invitees = $object->getInvitees();
         $invitees = mpull($invitees, null, 'getInviteePHID');
 
         foreach ($phids as $phid) {
           $invitee = idx($invitees, $phid);
           if (!$invitee) {
             $invitee = id(new PhabricatorCalendarEventInvitee())
               ->setEventPHID($object->getPHID())
               ->setInviteePHID($phid)
               ->setInviterPHID($this->getActingAsPHID());
             $invitees[] = $invitee;
           }
           $invitee->setStatus($map[$phid])
             ->save();
         }
         $object->attachInvitees($invitees);
         return;
       case PhabricatorTransactions::TYPE_COMMENT:
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
       case PhabricatorTransactions::TYPE_EDGE:
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         return;
     }
 
     return parent::applyCustomExternalTransaction($object, $xaction);
   }
 
+  protected function didApplyInternalEffects(
+    PhabricatorLiskDAO $object,
+    array $xactions) {
+
+    $object->removeViewerTimezone($this->requireActor());
+
+    return $xactions;
+  }
+
+
   protected function validateAllTransactions(
     PhabricatorLiskDAO $object,
     array $xactions) {
     $start_date_xaction = PhabricatorCalendarEventTransaction::TYPE_START_DATE;
     $end_date_xaction = PhabricatorCalendarEventTransaction::TYPE_END_DATE;
     $start_date = $object->getDateFrom();
     $end_date = $object->getDateTo();
     $errors = array();
 
     foreach ($xactions as $xaction) {
       if ($xaction->getTransactionType() == $start_date_xaction) {
         $start_date = $xaction->getNewValue()->getEpoch();
       } else if ($xaction->getTransactionType() == $end_date_xaction) {
         $end_date = $xaction->getNewValue()->getEpoch();
       }
     }
     if ($start_date > $end_date) {
       $type = PhabricatorCalendarEventTransaction::TYPE_END_DATE;
       $errors[] = new PhabricatorApplicationTransactionValidationError(
         $type,
         pht('Invalid'),
         pht('End date must be after start date.'),
         null);
     }
 
     return $errors;
   }
 
   protected function validateTransaction(
     PhabricatorLiskDAO $object,
     $type,
     array $xactions) {
 
     $errors = parent::validateTransaction($object, $type, $xactions);
 
     switch ($type) {
       case PhabricatorCalendarEventTransaction::TYPE_NAME:
         $missing = $this->validateIsEmptyTextField(
           $object->getName(),
           $xactions);
 
         if ($missing) {
           $error = new PhabricatorApplicationTransactionValidationError(
             $type,
             pht('Required'),
             pht('Event name is required.'),
             nonempty(last($xactions), null));
 
           $error->setIsMissingFieldError(true);
           $errors[] = $error;
         }
         break;
       case PhabricatorCalendarEventTransaction::TYPE_START_DATE:
       case PhabricatorCalendarEventTransaction::TYPE_END_DATE:
         foreach ($xactions as $xaction) {
           $date_value = $xaction->getNewValue();
           if (!$date_value->isValid()) {
             $errors[] = new PhabricatorApplicationTransactionValidationError(
               $type,
               pht('Invalid'),
               pht('Invalid date.'),
               $xaction);
           }
         }
         break;
     }
 
     return $errors;
   }
 
   protected function shouldPublishFeedStory(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return true;
   }
 
   protected function supportsSearch() {
     return true;
   }
 
   protected function shouldSendMail(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $xactions = mfilter($xactions, 'shouldHide', true);
     return $xactions;
   }
 
   protected function getMailSubjectPrefix() {
     return pht('[Calendar]');
   }
 
   protected function getMailTo(PhabricatorLiskDAO $object) {
     $phids = array();
 
     if ($object->getUserPHID()) {
       $phids[] = $object->getUserPHID();
     }
     $phids[] = $this->getActingAsPHID();
 
     $invitees = $object->getInvitees();
     foreach ($invitees as $invitee) {
       $status = $invitee->getStatus();
       if ($status === PhabricatorCalendarEventInvitee::STATUS_ATTENDING
         || $status === PhabricatorCalendarEventInvitee::STATUS_INVITED) {
         $phids[] = $invitee->getInviteePHID();
       }
     }
 
     $phids = array_unique($phids);
 
     return $phids;
   }
 
   public function getMailTagsMap() {
     return array(
       PhabricatorCalendarEventTransaction::MAILTAG_CONTENT =>
         pht(
           "An event's name, status, invite list, ".
           "and description changes."),
       PhabricatorCalendarEventTransaction::MAILTAG_RESCHEDULE =>
         pht(
           "An event's start and end date ".
           "and cancellation status changes."),
       PhabricatorCalendarEventTransaction::MAILTAG_OTHER =>
         pht('Other event activity not listed above occurs.'),
     );
   }
 
   protected function buildReplyHandler(PhabricatorLiskDAO $object) {
     return id(new PhabricatorCalendarReplyHandler())
       ->setMailReceiver($object);
   }
 
   protected function buildMailTemplate(PhabricatorLiskDAO $object) {
     $id = $object->getID();
     $name = $object->getName();
 
     return id(new PhabricatorMetaMTAMail())
       ->setSubject("E{$id}: {$name}")
       ->addHeader('Thread-Topic', "E{$id}: ".$object->getName());
   }
 
   protected function buildMailBody(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $description = $object->getDescription();
     $body = parent::buildMailBody($object, $xactions);
 
     if (strlen($description)) {
       $body->addTextSection(
         pht('EVENT DESCRIPTION'),
         $object->getDescription());
     }
 
     $body->addLinkSection(
       pht('EVENT DETAIL'),
       PhabricatorEnv::getProductionURI('/E'.$object->getID()));
 
 
     return $body;
   }
 
 
 }
diff --git a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php
index 1fd294781f..c66e367793 100644
--- a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php
+++ b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php
@@ -1,173 +1,179 @@
 <?php
 
 final class PhabricatorCalendarEventQuery
   extends PhabricatorCursorPagedPolicyAwareQuery {
 
   private $ids;
   private $phids;
   private $rangeBegin;
   private $rangeEnd;
   private $inviteePHIDs;
   private $creatorPHIDs;
   private $isCancelled;
 
   public function withIDs(array $ids) {
     $this->ids = $ids;
     return $this;
   }
 
   public function withPHIDs(array $phids) {
     $this->phids = $phids;
     return $this;
   }
 
   public function withDateRange($begin, $end) {
     $this->rangeBegin = $begin;
     $this->rangeEnd = $end;
     return $this;
   }
 
   public function withInvitedPHIDs(array $phids) {
     $this->inviteePHIDs = $phids;
     return $this;
   }
 
   public function withCreatorPHIDs(array $phids) {
     $this->creatorPHIDs = $phids;
     return $this;
   }
 
   public function withIsCancelled($is_cancelled) {
     $this->isCancelled = $is_cancelled;
     return $this;
   }
 
   protected function loadPage() {
     $table = new PhabricatorCalendarEvent();
     $conn_r = $table->establishConnection('r');
 
     $data = queryfx_all(
       $conn_r,
       'SELECT event.* FROM %T event %Q %Q %Q %Q %Q',
       $table->getTableName(),
       $this->buildJoinClause($conn_r),
       $this->buildWhereClause($conn_r),
       $this->buildGroupClause($conn_r),
       $this->buildOrderClause($conn_r),
       $this->buildLimitClause($conn_r));
 
-    return $table->loadAllFromArray($data);
+    $events = $table->loadAllFromArray($data);
+
+    foreach ($events as $event) {
+      $event->applyViewerTimezone($this->getViewer());
+    }
+
+    return $events;
   }
 
   protected function buildJoinClauseParts(AphrontDatabaseConnection $conn_r) {
     $parts = parent::buildJoinClauseParts($conn_r);
     if ($this->inviteePHIDs !== null) {
       $parts[] = qsprintf(
         $conn_r,
         'JOIN %T invitee ON invitee.eventPHID = event.phid
           AND invitee.status != %s',
         id(new PhabricatorCalendarEventInvitee())->getTableName(),
         PhabricatorCalendarEventInvitee::STATUS_UNINVITED);
     }
     return $parts;
   }
 
   protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
     $where = array();
 
     if ($this->ids) {
       $where[] = qsprintf(
         $conn_r,
         'event.id IN (%Ld)',
         $this->ids);
     }
 
     if ($this->phids) {
       $where[] = qsprintf(
         $conn_r,
         'event.phid IN (%Ls)',
         $this->phids);
     }
 
     if ($this->rangeBegin) {
       $where[] = qsprintf(
         $conn_r,
         'event.dateTo >= %d',
         $this->rangeBegin);
     }
 
     if ($this->rangeEnd) {
       $where[] = qsprintf(
         $conn_r,
         'event.dateFrom <= %d',
         $this->rangeEnd);
     }
 
     if ($this->inviteePHIDs !== null) {
       $where[] = qsprintf(
         $conn_r,
         'invitee.inviteePHID IN (%Ls)',
         $this->inviteePHIDs);
     }
 
     if ($this->creatorPHIDs) {
       $where[] = qsprintf(
         $conn_r,
         'event.userPHID IN (%Ls)',
         $this->creatorPHIDs);
     }
 
     if ($this->isCancelled !== null) {
       $where[] = qsprintf(
         $conn_r,
         'event.isCancelled = %d',
         (int)$this->isCancelled);
     }
 
     $where[] = $this->buildPagingClause($conn_r);
 
     return $this->formatWhereClause($where);
   }
 
   protected function getPrimaryTableAlias() {
     return 'event';
   }
 
   protected function shouldGroupQueryResultRows() {
     if ($this->inviteePHIDs !== null) {
       return true;
     }
     return parent::shouldGroupQueryResultRows();
   }
 
   protected function getApplicationSearchObjectPHIDColumn() {
     return 'event.phid';
   }
 
   public function getQueryApplicationClass() {
     return 'PhabricatorCalendarApplication';
   }
 
 
   protected function willFilterPage(array $events) {
     $phids = array();
 
     foreach ($events as $event) {
       $phids[] = $event->getPHID();
     }
 
     $invitees = id(new PhabricatorCalendarEventInviteeQuery())
       ->setViewer($this->getViewer())
       ->withEventPHIDs($phids)
       ->execute();
     $invitees = mgroup($invitees, 'getEventPHID');
 
     foreach ($events as $event) {
       $event_invitees = idx($invitees, $event->getPHID(), array());
       $event->attachInvitees($event_invitees);
     }
 
     return $events;
   }
 
 }
diff --git a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php
index b525420ca3..c8a788adb8 100644
--- a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php
+++ b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php
@@ -1,459 +1,517 @@
 <?php
 
 final class PhabricatorCalendarEventSearchEngine
   extends PhabricatorApplicationSearchEngine {
 
   private $calendarYear;
   private $calendarMonth;
   private $calendarDay;
 
   public function getResultTypeDescription() {
     return pht('Calendar Events');
   }
 
   public function getApplicationClassName() {
     return 'PhabricatorCalendarApplication';
   }
 
   public function buildSavedQueryFromRequest(AphrontRequest $request) {
     $saved = new PhabricatorSavedQuery();
 
     $saved->setParameter(
       'rangeStart',
       $this->readDateFromRequest($request, 'rangeStart'));
 
     $saved->setParameter(
       'rangeEnd',
       $this->readDateFromRequest($request, 'rangeEnd'));
 
     $saved->setParameter(
       'upcoming',
       $this->readBoolFromRequest($request, 'upcoming'));
 
     $saved->setParameter(
       'invitedPHIDs',
       $this->readUsersFromRequest($request, 'invited'));
 
     $saved->setParameter(
       'creatorPHIDs',
       $this->readUsersFromRequest($request, 'creators'));
 
     $saved->setParameter(
       'isCancelled',
       $request->getStr('isCancelled'));
 
     $saved->setParameter(
       'display',
       $request->getStr('display'));
 
     return $saved;
   }
 
   public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
     $query = id(new PhabricatorCalendarEventQuery());
     $viewer = $this->requireViewer();
     $timezone = new DateTimeZone($viewer->getTimezoneIdentifier());
 
     $min_range = $this->getDateFrom($saved)->getEpoch();
     $max_range = $this->getDateTo($saved)->getEpoch();
 
-    if ($saved->getParameter('display') == 'month' ||
-      $saved->getParameter('display') == 'day') {
+    if ($this->isMonthView($saved) ||
+      $this->isDayView($saved)) {
       list($start_year, $start_month, $start_day) =
         $this->getDisplayYearAndMonthAndDay($saved);
 
       $start_day = new DateTime(
         "{$start_year}-{$start_month}-{$start_day}",
         $timezone);
       $next = clone $start_day;
 
-      if ($saved->getParameter('display') == 'month') {
+      if ($this->isMonthView($saved)) {
         $next->modify('+1 month');
-      } else if ($saved->getParameter('display') == 'day') {
+      } else if ($this->isDayView($saved)) {
         $next->modify('+6 day');
       }
 
       $display_start = $start_day->format('U');
       $display_end = $next->format('U');
 
+      // 0 = Sunday is always the start of the week, for now
+      $start_of_week = 0;
+      $end_of_week = 6 - $start_of_week;
+
+      $first_of_month = $start_day->format('w');
+      $last_of_month = id(clone $next)->modify('-1 day')->format('w');
+
       if (!$min_range || ($min_range < $display_start)) {
         $min_range = $display_start;
+
+        if ($this->isMonthView($saved) &&
+          $first_of_month > $start_of_week) {
+          $min_range = id(clone $start_day)
+            ->modify('-'.$first_of_month.' days')
+            ->format('U');
+        }
       }
       if (!$max_range || ($max_range > $display_end)) {
         $max_range = $display_end;
+
+        if ($this->isMonthView($saved) &&
+          $last_of_month < $end_of_week) {
+          $max_range = id(clone $next)
+            ->modify('+'.(6 - $first_of_month).' days')
+            ->format('U');
+        }
+
       }
     }
 
     if ($saved->getParameter('upcoming')) {
       if ($min_range) {
         $min_range = max(time(), $min_range);
       } else {
         $min_range = time();
       }
     }
 
     if ($min_range || $max_range) {
       $query->withDateRange($min_range, $max_range);
     }
 
     $invited_phids = $saved->getParameter('invitedPHIDs');
     if ($invited_phids) {
       $query->withInvitedPHIDs($invited_phids);
     }
 
     $creator_phids = $saved->getParameter('creatorPHIDs');
     if ($creator_phids) {
       $query->withCreatorPHIDs($creator_phids);
     }
 
     $is_cancelled = $saved->getParameter('isCancelled');
     switch ($is_cancelled) {
       case 'active':
         $query->withIsCancelled(false);
         break;
       case 'cancelled':
         $query->withIsCancelled(true);
         break;
     }
 
     return $query;
   }
 
   public function buildSearchForm(
     AphrontFormView $form,
     PhabricatorSavedQuery $saved) {
 
     $range_start = $this->getDateFrom($saved);
     $e_start = null;
 
     $range_end = $this->getDateTo($saved);
     $e_end = null;
 
     if (!$range_start->isValid()) {
       $this->addError(pht('Start date is not valid.'));
       $e_start = pht('Invalid');
     }
 
     if (!$range_end->isValid()) {
       $this->addError(pht('End date is not valid.'));
       $e_end = pht('Invalid');
     }
 
     $start_epoch = $range_start->getEpoch();
     $end_epoch = $range_end->getEpoch();
 
     if ($start_epoch && $end_epoch && ($start_epoch > $end_epoch)) {
       $this->addError(pht('End date must be after start date.'));
       $e_start = pht('Invalid');
       $e_end = pht('Invalid');
     }
 
     $upcoming = $saved->getParameter('upcoming');
     $is_cancelled = $saved->getParameter('isCancelled', 'active');
     $display = $saved->getParameter('display', 'month');
 
     $invited_phids = $saved->getParameter('invitedPHIDs', array());
     $creator_phids = $saved->getParameter('creatorPHIDs', array());
     $resolution_types = array(
       'active' => pht('Active Events Only'),
       'cancelled' => pht('Cancelled Events Only'),
       'both' => pht('Both Cancelled and Active Events'),
     );
     $display_options = array(
       'month' => pht('Month View'),
       'day' => pht('Day View (beta)'),
       'list' => pht('List View'),
     );
 
     $form
       ->appendControl(
         id(new AphrontFormTokenizerControl())
           ->setDatasource(new PhabricatorPeopleDatasource())
           ->setName('creators')
           ->setLabel(pht('Created By'))
           ->setValue($creator_phids))
       ->appendControl(
         id(new AphrontFormTokenizerControl())
           ->setDatasource(new PhabricatorPeopleDatasource())
           ->setName('invited')
           ->setLabel(pht('Invited'))
           ->setValue($invited_phids))
       ->appendChild(
         id(new AphrontFormDateControl())
           ->setLabel(pht('Occurs After'))
           ->setUser($this->requireViewer())
           ->setName('rangeStart')
           ->setError($e_start)
           ->setValue($range_start))
       ->appendChild(
         id(new AphrontFormDateControl())
           ->setLabel(pht('Occurs Before'))
           ->setUser($this->requireViewer())
           ->setName('rangeEnd')
           ->setError($e_end)
           ->setValue($range_end))
       ->appendChild(
         id(new AphrontFormCheckboxControl())
           ->addCheckbox(
             'upcoming',
             1,
             pht('Show only upcoming events.'),
             $upcoming))
       ->appendChild(
         id(new AphrontFormSelectControl())
           ->setLabel(pht('Cancelled Events'))
           ->setName('isCancelled')
           ->setValue($is_cancelled)
           ->setOptions($resolution_types))
       ->appendChild(
         id(new AphrontFormSelectControl())
           ->setLabel(pht('Display Options'))
           ->setName('display')
           ->setValue($display)
           ->setOptions($display_options));
   }
 
   protected function getURI($path) {
     return '/calendar/'.$path;
   }
 
   protected function getBuiltinQueryNames() {
     $names = array(
       'month' => pht('Month View'),
+      'day' => pht('Day View'),
       'upcoming' => pht('Upcoming Events'),
       'all' => pht('All Events'),
     );
 
     return $names;
   }
 
   public function setCalendarYearAndMonthAndDay($year, $month, $day = null) {
     $this->calendarYear = $year;
     $this->calendarMonth = $month;
     $this->calendarDay = $day;
 
     return $this;
   }
 
   public function buildSavedQueryFromBuiltin($query_key) {
     $query = $this->newSavedQuery();
     $query->setQueryKey($query_key);
 
     switch ($query_key) {
       case 'month':
         return $query->setParameter('display', 'month');
+      case 'day':
+        return $query->setParameter('display', 'day');
       case 'upcoming':
         return $query->setParameter('upcoming', true);
       case 'all':
         return $query;
     }
 
     return parent::buildSavedQueryFromBuiltin($query_key);
   }
 
   protected function getRequiredHandlePHIDsForResultList(
     array $objects,
     PhabricatorSavedQuery $query) {
     $phids = array();
     foreach ($objects as $event) {
       $phids[$event->getUserPHID()] = 1;
     }
     return array_keys($phids);
   }
 
   protected function renderResultList(
     array $events,
     PhabricatorSavedQuery $query,
     array $handles) {
 
-    if ($query->getParameter('display') == 'month') {
+    if ($this->isMonthView($query)) {
       return $this->buildCalendarView($events, $query, $handles);
-    } else if ($query->getParameter('display') == 'day') {
+    } else if ($this->isDayView($query)) {
       return $this->buildCalendarDayView($events, $query, $handles);
     }
 
     assert_instances_of($events, 'PhabricatorCalendarEvent');
     $viewer = $this->requireViewer();
     $list = new PHUIObjectItemListView();
     foreach ($events as $event) {
       $href = '/E'.$event->getID();
       $from = phabricator_datetime($event->getDateFrom(), $viewer);
       $to   = phabricator_datetime($event->getDateTo(), $viewer);
       $creator_handle = $handles[$event->getUserPHID()];
 
-      $name = (strlen($event->getName())) ?
-        $event->getName() : $event->getTerseSummary($viewer);
-
       $color = ($event->getStatus() == PhabricatorCalendarEvent::STATUS_AWAY)
         ? 'red'
         : 'yellow';
 
       $item = id(new PHUIObjectItemView())
-        ->setHeader($name)
+        ->setHeader($event->getName())
         ->setHref($href)
         ->setBarColor($color)
         ->addByline(pht('Creator: %s', $creator_handle->renderLink()))
         ->addAttribute(pht('From %s to %s', $from, $to))
         ->addAttribute(id(new PhutilUTF8StringTruncator())
           ->setMaximumGlyphs(64)
           ->truncateString($event->getDescription()));
 
       $list->addItem($item);
     }
 
     return $list;
   }
 
   private function buildCalendarView(
     array $statuses,
     PhabricatorSavedQuery $query,
     array $handles) {
     $viewer = $this->requireViewer();
     $now = time();
 
     list($start_year, $start_month) =
       $this->getDisplayYearAndMonthAndDay($query);
 
     $now_year  = phabricator_format_local_time($now, $viewer, 'Y');
     $now_month = phabricator_format_local_time($now, $viewer, 'm');
     $now_day   = phabricator_format_local_time($now, $viewer, 'j');
 
     if ($start_month == $now_month && $start_year == $now_year) {
       $month_view = new PHUICalendarMonthView(
+        $this->getDateFrom($query),
+        $this->getDateTo($query),
         $start_month,
         $start_year,
         $now_day);
     } else {
       $month_view = new PHUICalendarMonthView(
+        $this->getDateFrom($query),
+        $this->getDateTo($query),
         $start_month,
         $start_year);
     }
 
     $month_view->setUser($viewer);
 
     $phids = mpull($statuses, 'getUserPHID');
 
     /* Assign Colors */
     $unique = array_unique($phids);
     $allblue = false;
     $calcolors = CalendarColors::getColors();
     if (count($unique) > count($calcolors)) {
       $allblue = true;
     }
     $i = 0;
     $eventcolor = array();
     foreach ($unique as $phid) {
       if ($allblue) {
         $eventcolor[$phid] = CalendarColors::COLOR_SKY;
       } else {
         $eventcolor[$phid] = $calcolors[$i];
       }
       $i++;
     }
 
     foreach ($statuses as $status) {
       $event = new AphrontCalendarEventView();
       $event->setEpochRange($status->getDateFrom(), $status->getDateTo());
+      $event->setIsAllDay($status->getIsAllDay());
 
       $name_text = $handles[$status->getUserPHID()]->getName();
-      $status_text = $status->getHumanStatus();
+      $status_text = $status->getName();
       $event->setUserPHID($status->getUserPHID());
       $event->setDescription(pht('%s (%s)', $name_text, $status_text));
       $event->setName($status_text);
       $event->setEventID($status->getID());
       $event->setColor($eventcolor[$status->getUserPHID()]);
       $month_view->addEvent($event);
     }
 
     $month_view->setBrowseURI(
       $this->getURI('query/'.$query->getQueryKey().'/'));
 
     return $month_view;
   }
 
   private function buildCalendarDayView(
     array $statuses,
     PhabricatorSavedQuery $query,
     array $handles) {
     $viewer = $this->requireViewer();
     list($start_year, $start_month, $start_day) =
       $this->getDisplayYearAndMonthAndDay($query);
 
     $day_view = new PHUICalendarDayView(
+      $this->getDateFrom($query),
+      $this->getDateTo($query),
       $start_year,
       $start_month,
       $start_day);
 
     $day_view->setUser($viewer);
 
     $phids = mpull($statuses, 'getUserPHID');
 
     foreach ($statuses as $status) {
+      if ($status->getIsCancelled()) {
+        continue;
+      }
+
       $event = new AphrontCalendarEventView();
       $event->setEventID($status->getID());
       $event->setEpochRange($status->getDateFrom(), $status->getDateTo());
+      $event->setIsAllDay($status->getIsAllDay());
 
       $event->setName($status->getName());
       $event->setURI('/'.$status->getMonogram());
       $day_view->addEvent($event);
     }
 
     $day_view->setBrowseURI(
       $this->getURI('query/'.$query->getQueryKey().'/'));
 
     return $day_view;
   }
 
   private function getDisplayYearAndMonthAndDay(
     PhabricatorSavedQuery $query) {
     $viewer = $this->requireViewer();
     if ($this->calendarYear && $this->calendarMonth) {
       $start_year = $this->calendarYear;
       $start_month = $this->calendarMonth;
       $start_day = $this->calendarDay ? $this->calendarDay : 1;
     } else {
       $epoch = $this->getDateFrom($query)->getEpoch();
       if (!$epoch) {
         $epoch = $this->getDateTo($query)->getEpoch();
         if (!$epoch) {
           $epoch = time();
         }
       }
+      if ($this->isMonthView($query)) {
+        $day = 1;
+      } else {
+        $day = phabricator_format_local_time($epoch, $viewer, 'd');
+      }
       $start_year = phabricator_format_local_time($epoch, $viewer, 'Y');
       $start_month = phabricator_format_local_time($epoch, $viewer, 'm');
-      $start_day = phabricator_format_local_time($epoch, $viewer, 'd');
+      $start_day = $day;
     }
     return array($start_year, $start_month, $start_day);
   }
 
   public function getPageSize(PhabricatorSavedQuery $saved) {
     return $saved->getParameter('limit', 1000);
   }
 
   private function getDateFrom(PhabricatorSavedQuery $saved) {
     return $this->getDate($saved, 'rangeStart');
   }
 
   private function getDateTo(PhabricatorSavedQuery $saved) {
     return $this->getDate($saved, 'rangeEnd');
   }
 
   private function getDate(PhabricatorSavedQuery $saved, $key) {
     $viewer = $this->requireViewer();
 
     $wild = $saved->getParameter($key);
     if ($wild) {
       $value = AphrontFormDateControlValue::newFromWild($viewer, $wild);
     } else {
       $value = AphrontFormDateControlValue::newFromEpoch(
         $viewer,
         PhabricatorTime::getTodayMidnightDateTime($viewer)->format('U'));
       $value->setEnabled(false);
     }
 
     $value->setOptional(true);
 
     return $value;
   }
 
+  private function isMonthView(PhabricatorSavedQuery $query) {
+    if ($this->isDayView($query)) {
+      return false;
+    }
+    if ($query->getParameter('display') == 'month') {
+      return true;
+    }
+  }
+
+  private function isDayView(PhabricatorSavedQuery $query) {
+    if ($query->getParameter('display') == 'day') {
+      return true;
+    }
+    if ($this->calendarDay) {
+      return true;
+    }
+
+    return false;
+  }
 }
diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php
index 03da6851a5..3dd984ef7d 100644
--- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php
+++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php
@@ -1,315 +1,410 @@
 <?php
 
 final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
   implements PhabricatorPolicyInterface,
   PhabricatorMarkupInterface,
   PhabricatorApplicationTransactionInterface,
   PhabricatorSubscribableInterface,
   PhabricatorTokenReceiverInterface,
   PhabricatorDestructibleInterface,
   PhabricatorMentionableInterface,
   PhabricatorFlaggableInterface {
 
   protected $name;
   protected $userPHID;
   protected $dateFrom;
   protected $dateTo;
   protected $status;
   protected $description;
   protected $isCancelled;
+  protected $isAllDay;
   protected $mailKey;
 
   protected $viewPolicy;
   protected $editPolicy;
 
   private $invitees = self::ATTACHABLE;
+  private $appliedViewer;
 
   const STATUS_AWAY = 1;
   const STATUS_SPORADIC = 2;
 
   public static function initializeNewCalendarEvent(PhabricatorUser $actor) {
     $app = id(new PhabricatorApplicationQuery())
       ->setViewer($actor)
       ->withClasses(array('PhabricatorCalendarApplication'))
       ->executeOne();
 
     return id(new PhabricatorCalendarEvent())
       ->setUserPHID($actor->getPHID())
       ->setIsCancelled(0)
+      ->setIsAllDay(0)
       ->setViewPolicy($actor->getPHID())
       ->setEditPolicy($actor->getPHID())
-      ->attachInvitees(array());
+      ->attachInvitees(array())
+      ->applyViewerTimezone($actor);
+  }
+
+  public function applyViewerTimezone(PhabricatorUser $viewer) {
+    if ($this->appliedViewer) {
+      throw new Exception(pht('Viewer timezone is already applied!'));
+    }
+
+    $this->appliedViewer = $viewer;
+
+    if (!$this->getIsAllDay()) {
+      return $this;
+    }
+
+    $zone = $viewer->getTimeZone();
+
+
+    $this->setDateFrom(
+      $this->getDateEpochForTimeZone(
+        $this->getDateFrom(),
+        new DateTimeZone('Pacific/Kiritimati'),
+        'Y-m-d',
+        null,
+        $zone));
+
+    $this->setDateTo(
+      $this->getDateEpochForTimeZone(
+        $this->getDateTo(),
+        new DateTimeZone('Pacific/Midway'),
+        'Y-m-d 23:59:00',
+        '-1 day',
+        $zone));
+
+    return $this;
+  }
+
+
+  public function removeViewerTimezone(PhabricatorUser $viewer) {
+    if (!$this->appliedViewer) {
+      throw new Exception(pht('Viewer timezone is not applied!'));
+    }
+
+    if ($viewer->getPHID() != $this->appliedViewer->getPHID()) {
+      throw new Exception(pht('Removed viewer must match applied viewer!'));
+    }
+
+    $this->appliedViewer = null;
+
+    if (!$this->getIsAllDay()) {
+      return $this;
+    }
+
+    $zone = $viewer->getTimeZone();
+
+    $this->setDateFrom(
+      $this->getDateEpochForTimeZone(
+        $this->getDateFrom(),
+        $zone,
+        'Y-m-d',
+        null,
+        new DateTimeZone('Pacific/Kiritimati')));
+
+    $this->setDateTo(
+      $this->getDateEpochForTimeZone(
+        $this->getDateTo(),
+        $zone,
+        'Y-m-d',
+        '+1 day',
+        new DateTimeZone('Pacific/Midway')));
+
+    return $this;
+  }
+
+  private function getDateEpochForTimeZone(
+    $epoch,
+    $src_zone,
+    $format,
+    $adjust,
+    $dst_zone) {
+
+    $src = new DateTime('@'.$epoch);
+    $src->setTimeZone($src_zone);
+
+    if (strlen($adjust)) {
+      $adjust = ' '.$adjust;
+    }
+
+    $dst = new DateTime($src->format($format).$adjust, $dst_zone);
+    return $dst->format('U');
   }
 
   public function save() {
+    if ($this->appliedViewer) {
+      throw new Exception(
+        pht(
+          'Can not save event with viewer timezone still applied!'));
+    }
+
     if (!$this->mailKey) {
       $this->mailKey = Filesystem::readRandomCharacters(20);
     }
+
     return parent::save();
   }
 
   private static $statusTexts = array(
     self::STATUS_AWAY => 'away',
     self::STATUS_SPORADIC => 'sporadic',
   );
 
   public function setTextStatus($status) {
     $statuses = array_flip(self::$statusTexts);
     return $this->setStatus($statuses[$status]);
   }
 
   public function getTextStatus() {
     return self::$statusTexts[$this->status];
   }
 
   public function getStatusOptions() {
     return array(
       self::STATUS_AWAY     => pht('Away'),
       self::STATUS_SPORADIC => pht('Sporadic'),
     );
   }
 
-  public function getHumanStatus() {
-    $options = $this->getStatusOptions();
-    return $options[$this->status];
-  }
-
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_COLUMN_SCHEMA => array(
         'name' => 'text',
         'dateFrom' => 'epoch',
         'dateTo' => 'epoch',
         'status' => 'uint32',
         'description' => 'text',
         'isCancelled' => 'bool',
+        'isAllDay' => 'bool',
         'mailKey' => 'bytes20',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'userPHID_dateFrom' => array(
           'columns' => array('userPHID', 'dateTo'),
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       PhabricatorCalendarEventPHIDType::TYPECONST);
   }
 
   public function getMonogram() {
     return 'E'.$this->getID();
   }
 
   public function getTerseSummary(PhabricatorUser $viewer) {
     $until = phabricator_date($this->dateTo, $viewer);
-    if ($this->status == PhabricatorCalendarEvent::STATUS_SPORADIC) {
+    if ($this->status == self::STATUS_SPORADIC) {
       return pht('Sporadic until %s', $until);
     } else {
       return pht('Away until %s', $until);
     }
   }
 
   public static function getNameForStatus($value) {
     switch ($value) {
       case self::STATUS_AWAY:
         return pht('Away');
       case self::STATUS_SPORADIC:
         return pht('Sporadic');
       default:
         return pht('Unknown');
     }
   }
 
   public function loadCurrentStatuses($user_phids) {
     if (!$user_phids) {
       return array();
     }
 
     $statuses = $this->loadAllWhere(
       'userPHID IN (%Ls) AND UNIX_TIMESTAMP() BETWEEN dateFrom AND dateTo',
       $user_phids);
 
     return mpull($statuses, null, 'getUserPHID');
   }
 
   public function getInvitees() {
     return $this->assertAttached($this->invitees);
   }
 
   public function attachInvitees(array $invitees) {
     $this->invitees = $invitees;
     return $this;
   }
 
   public function getUserInviteStatus($phid) {
     $invitees = $this->getInvitees();
     $invitees = mpull($invitees, null, 'getInviteePHID');
 
     $invited = idx($invitees, $phid);
     if (!$invited) {
       return PhabricatorCalendarEventInvitee::STATUS_UNINVITED;
     }
     $invited = $invited->getStatus();
     return $invited;
   }
 
   public function getIsUserAttending($phid) {
     $attending_status = PhabricatorCalendarEventInvitee::STATUS_ATTENDING;
 
     $old_status = $this->getUserInviteStatus($phid);
     $is_attending = ($old_status == $attending_status);
 
     return $is_attending;
   }
 
 /* -(  Markup Interface  )--------------------------------------------------- */
 
 
   /**
    * @task markup
    */
   public function getMarkupFieldKey($field) {
     $hash = PhabricatorHash::digest($this->getMarkupText($field));
     $id = $this->getID();
     return "calendar:T{$id}:{$field}:{$hash}";
   }
 
 
   /**
    * @task markup
    */
   public function getMarkupText($field) {
     return $this->getDescription();
   }
 
 
   /**
    * @task markup
    */
   public function newMarkupEngine($field) {
     return PhabricatorMarkupEngine::newCalendarMarkupEngine();
   }
 
 
   /**
    * @task markup
    */
   public function didMarkupText(
     $field,
     $output,
     PhutilMarkupEngine $engine) {
     return $output;
   }
 
 
   /**
    * @task markup
    */
   public function shouldUseMarkupCache($field) {
     return (bool)$this->getID();
   }
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
       PhabricatorPolicyCapability::CAN_EDIT,
     );
   }
 
   public function getPolicy($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         return $this->getViewPolicy();
       case PhabricatorPolicyCapability::CAN_EDIT:
         return $this->getEditPolicy();
     }
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     // The owner of a task can always view and edit it.
     $user_phid = $this->getUserPHID();
     if ($user_phid) {
       $viewer_phid = $viewer->getPHID();
       if ($viewer_phid == $user_phid) {
         return true;
       }
     }
 
     if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
       $status = $this->getUserInviteStatus($viewer->getPHID());
       if ($status == PhabricatorCalendarEventInvitee::STATUS_INVITED ||
         $status == PhabricatorCalendarEventInvitee::STATUS_ATTENDING ||
         $status == PhabricatorCalendarEventInvitee::STATUS_DECLINED) {
         return true;
       }
     }
 
     return false;
   }
 
   public function describeAutomaticCapability($capability) {
     return pht('The owner of an event can always view and edit it,
       and invitees can always view it.');
   }
 
 /* -(  PhabricatorApplicationTransactionInterface  )------------------------- */
 
 
   public function getApplicationTransactionEditor() {
     return new PhabricatorCalendarEventEditor();
   }
 
   public function getApplicationTransactionObject() {
     return $this;
   }
 
   public function getApplicationTransactionTemplate() {
     return new PhabricatorCalendarEventTransaction();
   }
 
   public function willRenderTimeline(
     PhabricatorApplicationTransactionView $timeline,
     AphrontRequest $request) {
 
     return $timeline;
   }
 
 /* -(  PhabricatorSubscribableInterface  )----------------------------------- */
 
 
   public function isAutomaticallySubscribed($phid) {
     return ($phid == $this->getUserPHID());
   }
 
   public function shouldShowSubscribersProperty() {
     return true;
   }
 
   public function shouldAllowSubscription($phid) {
     return true;
   }
 
 /* -(  PhabricatorTokenReceiverInterface  )---------------------------------- */
 
 
   public function getUsersToNotifyOfTokenGiven() {
     return array($this->getUserPHID());
   }
 
 /* -(  PhabricatorDestructibleInterface  )----------------------------------- */
 
 
   public function destroyObjectPermanently(
     PhabricatorDestructionEngine $engine) {
 
     $this->openTransaction();
     $this->delete();
     $this->saveTransaction();
   }
 }
diff --git a/src/applications/calendar/storage/PhabricatorCalendarEventTransaction.php b/src/applications/calendar/storage/PhabricatorCalendarEventTransaction.php
index 7e4e7ff512..d3e7a25efd 100644
--- a/src/applications/calendar/storage/PhabricatorCalendarEventTransaction.php
+++ b/src/applications/calendar/storage/PhabricatorCalendarEventTransaction.php
@@ -1,460 +1,486 @@
 <?php
 
 final class PhabricatorCalendarEventTransaction
   extends PhabricatorApplicationTransaction {
 
   const TYPE_NAME = 'calendar.name';
   const TYPE_START_DATE = 'calendar.startdate';
   const TYPE_END_DATE = 'calendar.enddate';
   const TYPE_STATUS = 'calendar.status';
   const TYPE_DESCRIPTION = 'calendar.description';
   const TYPE_CANCEL = 'calendar.cancel';
+  const TYPE_ALL_DAY = 'calendar.allday';
   const TYPE_INVITE = 'calendar.invite';
 
   const MAILTAG_RESCHEDULE = 'calendar-reschedule';
   const MAILTAG_CONTENT = 'calendar-content';
   const MAILTAG_OTHER = 'calendar-other';
 
   public function getApplicationName() {
     return 'calendar';
   }
 
   public function getApplicationTransactionType() {
     return PhabricatorCalendarEventPHIDType::TYPECONST;
   }
 
   public function getApplicationTransactionCommentObject() {
     return new PhabricatorCalendarEventTransactionComment();
   }
 
   public function getRequiredHandlePHIDs() {
     $phids = parent::getRequiredHandlePHIDs();
 
     switch ($this->getTransactionType()) {
       case self::TYPE_NAME:
       case self::TYPE_START_DATE:
       case self::TYPE_END_DATE:
       case self::TYPE_STATUS:
       case self::TYPE_DESCRIPTION:
       case self::TYPE_CANCEL:
+      case self::TYPE_ALL_DAY:
         $phids[] = $this->getObjectPHID();
         break;
       case self::TYPE_INVITE:
         $new = $this->getNewValue();
         foreach ($new as $phid => $status) {
           $phids[] = $phid;
         }
         break;
     }
 
     return $phids;
   }
 
   public function shouldHide() {
     $old = $this->getOldValue();
     switch ($this->getTransactionType()) {
       case self::TYPE_START_DATE:
       case self::TYPE_END_DATE:
       case self::TYPE_STATUS:
       case self::TYPE_DESCRIPTION:
       case self::TYPE_CANCEL:
+      case self::TYPE_ALL_DAY:
       case self::TYPE_INVITE:
         return ($old === null);
     }
     return parent::shouldHide();
   }
 
   public function getIcon() {
     switch ($this->getTransactionType()) {
       case self::TYPE_NAME:
       case self::TYPE_START_DATE:
       case self::TYPE_END_DATE:
       case self::TYPE_STATUS:
       case self::TYPE_DESCRIPTION:
+      case self::TYPE_ALL_DAY:
       case self::TYPE_CANCEL:
         return 'fa-pencil';
         break;
       case self::TYPE_INVITE:
         return 'fa-user-plus';
         break;
     }
     return parent::getIcon();
   }
 
   public function getTitle() {
     $author_phid = $this->getAuthorPHID();
     $object_phid = $this->getObjectPHID();
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     $type = $this->getTransactionType();
     switch ($type) {
       case self::TYPE_NAME:
         if ($old === null) {
           return pht(
             '%s created this event.',
             $this->renderHandleLink($author_phid));
         } else {
           return pht(
             '%s changed the name of this event from %s to %s.',
             $this->renderHandleLink($author_phid),
             $old,
             $new);
         }
       case self::TYPE_START_DATE:
         if ($old) {
           return pht(
             '%s edited the start date of this event.',
             $this->renderHandleLink($author_phid));
         }
         break;
       case self::TYPE_END_DATE:
         if ($old) {
           return pht(
             '%s edited the end date of this event.',
             $this->renderHandleLink($author_phid));
         }
         break;
       case self::TYPE_STATUS:
         $old_name = PhabricatorCalendarEvent::getNameForStatus($old);
         $new_name = PhabricatorCalendarEvent::getNameForStatus($new);
         return pht(
           '%s updated the event status from %s to %s.',
           $this->renderHandleLink($author_phid),
           $old_name,
           $new_name);
       case self::TYPE_DESCRIPTION:
         return pht(
           "%s updated the event's description.",
           $this->renderHandleLink($author_phid));
+      case self::TYPE_ALL_DAY:
+        if ($new) {
+          return pht(
+            '%s made this an all day event.',
+            $this->renderHandleLink($author_phid));
+        } else {
+          return pht(
+            '%s converted this from an all day event.',
+            $this->renderHandleLink($author_phid));
+        }
       case self::TYPE_CANCEL:
         if ($new) {
           return pht(
             '%s cancelled this event.',
             $this->renderHandleLink($author_phid));
         } else {
           return pht(
             '%s reinstated this event.',
             $this->renderHandleLink($author_phid));
         }
       case self::TYPE_INVITE:
         $text = null;
 
         if (count($old) === 1
           && count($new) === 1
           && isset($old[$author_phid])) {
           // user joined/declined/accepted event themself
           $old_status = $old[$author_phid];
           $new_status = $new[$author_phid];
 
           if ($old_status !== $new_status) {
             switch ($new_status) {
               case PhabricatorCalendarEventInvitee::STATUS_INVITED:
                 $text = pht(
                   '%s has joined this event.',
                   $this->renderHandleLink($author_phid));
                   break;
               case PhabricatorCalendarEventInvitee::STATUS_ATTENDING:
                 $text = pht(
                   '%s is attending this event.',
                   $this->renderHandleLink($author_phid));
                   break;
               case PhabricatorCalendarEventInvitee::STATUS_DECLINED:
               case PhabricatorCalendarEventInvitee::STATUS_UNINVITED:
                 $text = pht(
                   '%s has declined this event.',
                   $this->renderHandleLink($author_phid));
                   break;
               default:
                 $text = pht(
                   '%s has changed their status for this event.',
                   $this->renderHandleLink($author_phid));
                 break;
             }
           }
         } else {
           // user changed status for many users
           $self_joined = null;
           $self_declined = null;
           $added = array();
           $uninvited = array();
 
           foreach ($new as $phid => $status) {
             if ($status == PhabricatorCalendarEventInvitee::STATUS_INVITED
               || $status == PhabricatorCalendarEventInvitee::STATUS_ATTENDING) {
               // added users
               $added[] = $phid;
             } else if (
               $status == PhabricatorCalendarEventInvitee::STATUS_DECLINED
               || $status == PhabricatorCalendarEventInvitee::STATUS_UNINVITED) {
               $uninvited[] = $phid;
             }
           }
 
           $count_added = count($added);
           $count_uninvited = count($uninvited);
           $added_text = null;
           $uninvited_text = null;
 
           if ($count_added > 0 && $count_uninvited == 0) {
             $added_text = $this->renderHandleList($added);
             $text = pht('%s invited %s.',
               $this->renderHandleLink($author_phid),
               $added_text);
           } else if ($count_added > 0 && $count_uninvited > 0) {
             $added_text = $this->renderHandleList($added);
             $uninvited_text = $this->renderHandleList($uninvited);
             $text = pht('%s invited %s and uninvited: %s',
               $this->renderHandleLink($author_phid),
               $added_text,
               $uninvited_text);
           } else if ($count_added == 0 && $count_uninvited > 0) {
             $uninvited_text = $this->renderHandleList($uninvited);
             $text = pht('%s uninvited %s.',
               $this->renderHandleLink($author_phid),
               $uninvited_text);
           } else {
             $text = pht('%s updated the invitee list.',
               $this->renderHandleLink($author_phid));
           }
         }
         return $text;
     }
     return parent::getTitle();
   }
 
   public function getTitleForFeed() {
     $author_phid = $this->getAuthorPHID();
     $object_phid = $this->getObjectPHID();
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     $viewer = $this->getViewer();
 
     $type = $this->getTransactionType();
     switch ($type) {
       case self::TYPE_NAME:
         if ($old === null) {
           return pht(
             '%s created %s',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid));
         } else {
           return pht(
             '%s changed the name of %s from %s to %s.',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid),
             $old,
             $new);
         }
       case self::TYPE_START_DATE:
         if ($old) {
           $old = phabricator_datetime($old, $viewer);
           $new = phabricator_datetime($new, $viewer);
           return pht(
             '%s changed the start date of %s from %s to %s.',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid),
             $old,
             $new);
         }
         break;
       case self::TYPE_END_DATE:
         if ($old) {
           $old = phabricator_datetime($old, $viewer);
           $new = phabricator_datetime($new, $viewer);
           return pht(
             '%s edited the end date of %s from %s to %s.',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid),
             $old,
             $new);
         }
         break;
       case self::TYPE_STATUS:
         $old_name = PhabricatorCalendarEvent::getNameForStatus($old);
         $new_name = PhabricatorCalendarEvent::getNameForStatus($new);
         return pht(
           '%s updated the status of %s from %s to %s.',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($object_phid),
           $old_name,
           $new_name);
       case self::TYPE_DESCRIPTION:
         return pht(
           '%s updated the description of %s.',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($object_phid));
+      case self::TYPE_ALL_DAY:
+        if ($new) {
+          return pht(
+            '%s made %s an all day event.',
+            $this->renderHandleLink($author_phid),
+            $this->renderHandleLink($object_phid));
+        } else {
+          return pht(
+            '%s converted %s from an all day event.',
+            $this->renderHandleLink($author_phid),
+            $this->renderHandleLink($object_phid));
+        }
       case self::TYPE_CANCEL:
         if ($new) {
           return pht(
             '%s cancelled %s.',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid));
         } else {
           return pht(
             '%s reinstated %s.',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid));
         }
       case self::TYPE_INVITE:
         $text = null;
 
         if (count($old) === 1
           && count($new) === 1
           && isset($old[$author_phid])) {
           // user joined/declined/accepted event themself
           $old_status = $old[$author_phid];
           $new_status = $new[$author_phid];
 
           if ($old_status !== $new_status) {
             switch ($new_status) {
               case PhabricatorCalendarEventInvitee::STATUS_INVITED:
                 $text = pht(
                   '%s has joined %s.',
                   $this->renderHandleLink($author_phid),
                   $this->renderHandleLink($object_phid));
                   break;
               case PhabricatorCalendarEventInvitee::STATUS_ATTENDING:
                 $text = pht(
                   '%s is attending %s.',
                   $this->renderHandleLink($author_phid),
                   $this->renderHandleLink($object_phid));
                   break;
               case PhabricatorCalendarEventInvitee::STATUS_DECLINED:
               case PhabricatorCalendarEventInvitee::STATUS_UNINVITED:
                 $text = pht(
                   '%s has declined %s.',
                   $this->renderHandleLink($author_phid),
                   $this->renderHandleLink($object_phid));
                   break;
               default:
                 $text = pht(
                   '%s has changed their status of %s.',
                   $this->renderHandleLink($author_phid),
                   $this->renderHandleLink($object_phid));
                 break;
             }
           }
         } else {
           // user changed status for many users
           $self_joined = null;
           $self_declined = null;
           $added = array();
           $uninvited = array();
 
           // $event = $this->renderHandleLink($object_phid);
 
           foreach ($new as $phid => $status) {
             if ($status == PhabricatorCalendarEventInvitee::STATUS_INVITED
               || $status == PhabricatorCalendarEventInvitee::STATUS_ATTENDING) {
               // added users
               $added[] = $phid;
             } else if (
               $status == PhabricatorCalendarEventInvitee::STATUS_DECLINED
               || $status == PhabricatorCalendarEventInvitee::STATUS_UNINVITED) {
               $uninvited[] = $phid;
             }
           }
 
           $count_added = count($added);
           $count_uninvited = count($uninvited);
           $added_text = null;
           $uninvited_text = null;
 
           if ($count_added > 0 && $count_uninvited == 0) {
             $added_text = $this->renderHandleList($added);
             $text = pht('%s invited %s to %s.',
               $this->renderHandleLink($author_phid),
               $added_text,
               $this->renderHandleLink($object_phid));
           } else if ($count_added > 0 && $count_uninvited > 0) {
             $added_text = $this->renderHandleList($added);
             $uninvited_text = $this->renderHandleList($uninvited);
             $text = pht('%s invited %s and uninvited %s to %s',
               $this->renderHandleLink($author_phid),
               $added_text,
               $uninvited_text,
               $this->renderHandleLink($object_phid));
           } else if ($count_added == 0 && $count_uninvited > 0) {
             $uninvited_text = $this->renderHandleList($uninvited);
             $text = pht('%s uninvited %s to %s.',
               $this->renderHandleLink($author_phid),
               $uninvited_text,
               $this->renderHandleLink($object_phid));
           } else {
             $text = pht('%s updated the invitee list of %s.',
               $this->renderHandleLink($author_phid),
               $this->renderHandleLink($object_phid));
           }
         }
         return $text;
     }
 
     return parent::getTitleForFeed();
   }
 
   public function getColor() {
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
       case self::TYPE_NAME:
       case self::TYPE_START_DATE:
       case self::TYPE_END_DATE:
       case self::TYPE_STATUS:
       case self::TYPE_DESCRIPTION:
       case self::TYPE_CANCEL:
       case self::TYPE_INVITE:
         return PhabricatorTransactions::COLOR_GREEN;
     }
 
     return parent::getColor();
   }
 
 
   public function hasChangeDetails() {
     switch ($this->getTransactionType()) {
       case self::TYPE_DESCRIPTION:
         return ($this->getOldValue() !== null);
     }
 
     return parent::hasChangeDetails();
   }
 
   public function renderChangeDetails(PhabricatorUser $viewer) {
     switch ($this->getTransactionType()) {
       case self::TYPE_DESCRIPTION:
         $old = $this->getOldValue();
         $new = $this->getNewValue();
 
         return $this->renderTextCorpusChangeDetails(
           $viewer,
           $old,
           $new);
     }
 
     return parent::renderChangeDetails($viewer);
   }
 
   public function getMailTags() {
     $tags = array();
     switch ($this->getTransactionType()) {
       case self::TYPE_NAME:
       case self::TYPE_STATUS:
       case self::TYPE_DESCRIPTION:
       case self::TYPE_INVITE:
         $tags[] = self::MAILTAG_CONTENT;
         break;
       case self::TYPE_START_DATE:
       case self::TYPE_END_DATE:
       case self::TYPE_CANCEL:
         $tags[] = self::MAILTAG_RESCHEDULE;
         break;
     }
     return $tags;
   }
 
 }
diff --git a/src/applications/calendar/view/AphrontCalendarEventView.php b/src/applications/calendar/view/AphrontCalendarEventView.php
index aebfaa9a50..11dbff3847 100644
--- a/src/applications/calendar/view/AphrontCalendarEventView.php
+++ b/src/applications/calendar/view/AphrontCalendarEventView.php
@@ -1,104 +1,107 @@
 <?php
 
 final class AphrontCalendarEventView extends AphrontView {
 
   private $userPHID;
   private $name;
   private $epochStart;
   private $epochEnd;
   private $description;
   private $eventID;
   private $color;
   private $uri;
+  private $isAllDay;
 
   public function setURI($uri) {
     $this->uri = $uri;
     return $this;
   }
 
   public function getURI() {
     return $this->uri;
   }
 
   public function setEventID($event_id) {
     $this->eventID = $event_id;
     return $this;
   }
   public function getEventID() {
     return $this->eventID;
   }
 
   public function setUserPHID($user_phid) {
     $this->userPHID = $user_phid;
     return $this;
   }
 
   public function getUserPHID() {
     return $this->userPHID;
   }
 
   public function setName($name) {
     $this->name = $name;
     return $this;
   }
 
   public function setEpochRange($start, $end) {
     $this->epochStart = $start;
     $this->epochEnd   = $end;
     return $this;
   }
 
   public function getEpochStart() {
     return $this->epochStart;
   }
 
   public function getEpochEnd() {
     return $this->epochEnd;
   }
 
   public function getName() {
     return $this->name;
   }
 
   public function setDescription($description) {
     $this->description = $description;
     return $this;
   }
 
   public function getDescription() {
     return $this->description;
   }
 
   public function setColor($color) {
     $this->color = $color;
     return $this;
   }
   public function getColor() {
     if ($this->color) {
       return $this->color;
     } else {
       return CalendarColors::COLOR_SKY;
     }
   }
 
-  public function getAllDay() {
-    $time = (60 * 60 * 22);
-    if (($this->getEpochEnd() - $this->getEpochStart()) >= $time) {
-      return true;
-    }
-    return false;
+  public function setIsAllDay($is_all_day) {
+    $this->isAllDay = $is_all_day;
+    return $this;
   }
 
+  public function getIsAllDay() {
+    return $this->isAllDay;
+  }
+
+
   public function getMultiDay() {
     $nextday = strtotime('12:00 AM Tomorrow', $this->getEpochStart());
     if ($this->getEpochEnd() > $nextday) {
       return true;
     }
     return false;
   }
 
   public function render() {
     throw new Exception('Events are only rendered indirectly.');
   }
 
 }
diff --git a/src/applications/celerity/CelerityResourceGraph.php b/src/applications/celerity/CelerityResourceGraph.php
index a85c3ffbd8..715a64c35c 100644
--- a/src/applications/celerity/CelerityResourceGraph.php
+++ b/src/applications/celerity/CelerityResourceGraph.php
@@ -1,32 +1,30 @@
 <?php
 
 final class CelerityResourceGraph extends AbstractDirectedGraph {
 
   private $resourceGraph = array();
   private $graphSet = false;
 
   protected function loadEdges(array $nodes) {
     if (!$this->graphSet) {
-      throw new Exception(
-        'Call setResourceGraph before loading the graph!'
-      );
+      throw new PhutilInvalidStateException('setResourceGraph');
     }
 
     $graph = $this->getResourceGraph();
     $edges = array();
     foreach ($nodes as $node) {
       $edges[$node] = idx($graph, $node, array());
     }
     return $edges;
   }
 
   final public function setResourceGraph(array $graph) {
     $this->resourceGraph = $graph;
     $this->graphSet = true;
     return $this;
   }
 
   private function getResourceGraph() {
     return $this->resourceGraph;
   }
 }
diff --git a/src/applications/celerity/resources/CelerityPhysicalResources.php b/src/applications/celerity/resources/CelerityPhysicalResources.php
index 995b9f1a55..41665d15f2 100644
--- a/src/applications/celerity/resources/CelerityPhysicalResources.php
+++ b/src/applications/celerity/resources/CelerityPhysicalResources.php
@@ -1,61 +1,61 @@
 <?php
 
 /**
  * Defines the location of physical static resources which exist at build time
  * and are precomputed into a resource map.
  */
 abstract class CelerityPhysicalResources extends CelerityResources {
 
   private $map;
 
   abstract public function getPathToMap();
   abstract public function findBinaryResources();
   abstract public function findTextResources();
 
   public function loadMap() {
     if ($this->map === null) {
       $this->map = include $this->getPathToMap();
     }
     return $this->map;
   }
 
   public static function getAll() {
     static $resources_map;
     if ($resources_map === null) {
       $resources_map = array();
 
       $resources_list = id(new PhutilSymbolLoader())
-        ->setAncestorClass('CelerityPhysicalResources')
+        ->setAncestorClass(__CLASS__)
         ->loadObjects();
 
       foreach ($resources_list as $resources) {
         $name = $resources->getName();
 
         if (!preg_match('/^[a-z0-9]+/', $name)) {
           throw new Exception(
             pht(
               'Resources name "%s" is not valid; it must contain only '.
               'lowercase latin letters and digits.',
               $name));
         }
 
         if (empty($resources_map[$name])) {
           $resources_map[$name] = $resources;
         } else {
           $old = get_class($resources_map[$name]);
           $new = get_class($resources);
           throw new Exception(
             pht(
               'Celerity resource maps must have unique names, but maps %s and '.
               '%s share the same name, "%s".',
               $old,
               $new,
               $name));
         }
       }
     }
 
     return $resources_map;
   }
 
 }
diff --git a/src/applications/conduit/call/ConduitCall.php b/src/applications/conduit/call/ConduitCall.php
index aa55d1b895..7d767cd8c1 100644
--- a/src/applications/conduit/call/ConduitCall.php
+++ b/src/applications/conduit/call/ConduitCall.php
@@ -1,154 +1,158 @@
 <?php
 
 /**
  * Run a conduit method in-process, without requiring HTTP requests. Usage:
  *
  *   $call = new ConduitCall('method.name', array('param' => 'value'));
  *   $call->setUser($user);
  *   $result = $call->execute();
  *
  */
 final class ConduitCall {
 
   private $method;
   private $request;
   private $user;
 
   public function __construct($method, array $params) {
     $this->method = $method;
     $this->handler = $this->buildMethodHandler($method);
 
     $param_types = $this->handler->getParamTypes();
 
     foreach ($param_types as $key => $spec) {
       if (ConduitAPIMethod::getParameterMetadataKey($key) !== null) {
         throw new ConduitException(
           pht(
             'API Method "%s" defines a disallowed parameter, "%s". This '.
             'parameter name is reserved.',
             $method,
             $key));
       }
     }
 
     $invalid_params = array_diff_key($params, $param_types);
     if ($invalid_params) {
       throw new ConduitException(
         pht(
           'API Method "%s" does not define these parameters: %s.',
           $method,
           "'".implode("', '", array_keys($invalid_params))."'"));
     }
 
     $this->request = new ConduitAPIRequest($params);
   }
 
   public function getAPIRequest() {
     return $this->request;
   }
 
   public function setUser(PhabricatorUser $user) {
     $this->user = $user;
     return $this;
   }
 
   public function getUser() {
     return $this->user;
   }
 
   public function shouldRequireAuthentication() {
     return $this->handler->shouldRequireAuthentication();
   }
 
   public function shouldAllowUnguardedWrites() {
     return $this->handler->shouldAllowUnguardedWrites();
   }
 
   public function getRequiredScope() {
     return $this->handler->getRequiredScope();
   }
 
   public function getErrorDescription($code) {
     return $this->handler->getErrorDescription($code);
   }
 
   public function execute() {
     $profiler = PhutilServiceProfiler::getInstance();
     $call_id = $profiler->beginServiceCall(
       array(
         'type' => 'conduit',
         'method' => $this->method,
       ));
 
     try {
       $result = $this->executeMethod();
     } catch (Exception $ex) {
       $profiler->endServiceCall($call_id, array());
       throw $ex;
     }
 
     $profiler->endServiceCall($call_id, array());
     return $result;
   }
 
   private function executeMethod() {
     $user = $this->getUser();
     if (!$user) {
       $user = new PhabricatorUser();
     }
 
     $this->request->setUser($user);
 
     if (!$this->shouldRequireAuthentication()) {
       // No auth requirement here.
     } else {
 
       $allow_public = $this->handler->shouldAllowPublic() &&
                       PhabricatorEnv::getEnvConfig('policy.allow-public');
       if (!$allow_public) {
         if (!$user->isLoggedIn() && !$user->isOmnipotent()) {
           // TODO: As per below, this should get centralized and cleaned up.
           throw new ConduitException('ERR-INVALID-AUTH');
         }
       }
 
       // TODO: This would be slightly cleaner by just using a Query, but the
       // Conduit auth workflow requires the Call and User be built separately.
       // Just do it this way for the moment.
       $application = $this->handler->getApplication();
       if ($application) {
         $can_view = PhabricatorPolicyFilter::hasCapability(
           $user,
           $application,
           PhabricatorPolicyCapability::CAN_VIEW);
 
         if (!$can_view) {
           throw new ConduitException(
             pht(
               'You do not have access to the application which provides this '.
               'API method.'));
         }
       }
     }
 
     return $this->handler->executeMethod($this->request);
   }
 
   protected function buildMethodHandler($method_name) {
     $method = ConduitAPIMethod::getConduitMethod($method_name);
 
     if (!$method) {
       throw new ConduitMethodDoesNotExistException($method_name);
     }
 
     $application = $method->getApplication();
     if ($application && !$application->isInstalled()) {
       $app_name = $application->getName();
       throw new ConduitApplicationNotInstalledException($method, $app_name);
     }
 
     return $method;
   }
 
+  public function getMethodImplementation() {
+    return $this->handler;
+  }
+
 
 }
diff --git a/src/applications/conduit/controller/PhabricatorConduitAPIController.php b/src/applications/conduit/controller/PhabricatorConduitAPIController.php
index e8e69f18e3..cdc14da27d 100644
--- a/src/applications/conduit/controller/PhabricatorConduitAPIController.php
+++ b/src/applications/conduit/controller/PhabricatorConduitAPIController.php
@@ -1,683 +1,696 @@
 <?php
 
 final class PhabricatorConduitAPIController
   extends PhabricatorConduitController {
 
   public function shouldRequireLogin() {
     return false;
   }
 
   private $method;
 
   public function willProcessRequest(array $data) {
     $this->method = $data['method'];
     return $this;
   }
 
   public function processRequest() {
     $time_start = microtime(true);
     $request = $this->getRequest();
 
     $method = $this->method;
 
     $api_request = null;
+    $method_implementation = null;
 
     $log = new PhabricatorConduitMethodCallLog();
     $log->setMethod($method);
     $metadata = array();
 
     $multimeter = MultimeterControl::getInstance();
     if ($multimeter) {
       $multimeter->setEventContext('api.'.$method);
     }
 
     try {
 
       list($metadata, $params) = $this->decodeConduitParams($request, $method);
 
       $call = new ConduitCall($method, $params);
+      $method_implementation = $call->getMethodImplementation();
 
       $result = null;
 
       // TODO: The relationship between ConduitAPIRequest and ConduitCall is a
       // little odd here and could probably be improved. Specifically, the
       // APIRequest is a sub-object of the Call, which does not parallel the
       // role of AphrontRequest (which is an indepenent object).
       // In particular, the setUser() and getUser() existing independently on
       // the Call and APIRequest is very awkward.
 
       $api_request = $call->getAPIRequest();
 
       $allow_unguarded_writes = false;
       $auth_error = null;
       $conduit_username = '-';
       if ($call->shouldRequireAuthentication()) {
         $metadata['scope'] = $call->getRequiredScope();
         $auth_error = $this->authenticateUser($api_request, $metadata);
         // If we've explicitly authenticated the user here and either done
         // CSRF validation or are using a non-web authentication mechanism.
         $allow_unguarded_writes = true;
 
         if (isset($metadata['actAsUser'])) {
           $this->actAsUser($api_request, $metadata['actAsUser']);
         }
 
         if ($auth_error === null) {
           $conduit_user = $api_request->getUser();
           if ($conduit_user && $conduit_user->getPHID()) {
             $conduit_username = $conduit_user->getUsername();
           }
           $call->setUser($api_request->getUser());
         }
       }
 
       $access_log = PhabricatorAccessLog::getLog();
       if ($access_log) {
         $access_log->setData(
           array(
             'u' => $conduit_username,
             'm' => $method,
           ));
       }
 
       if ($call->shouldAllowUnguardedWrites()) {
         $allow_unguarded_writes = true;
       }
 
       if ($auth_error === null) {
         if ($allow_unguarded_writes) {
           $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
         }
 
         try {
           $result = $call->execute();
           $error_code = null;
           $error_info = null;
         } catch (ConduitException $ex) {
           $result = null;
           $error_code = $ex->getMessage();
           if ($ex->getErrorDescription()) {
             $error_info = $ex->getErrorDescription();
           } else {
             $error_info = $call->getErrorDescription($error_code);
           }
         }
         if ($allow_unguarded_writes) {
           unset($unguarded);
         }
       } else {
         list($error_code, $error_info) = $auth_error;
       }
     } catch (Exception $ex) {
       if (!($ex instanceof ConduitMethodNotFoundException)) {
         phlog($ex);
       }
       $result = null;
       $error_code = ($ex instanceof ConduitException
         ? 'ERR-CONDUIT-CALL'
         : 'ERR-CONDUIT-CORE');
       $error_info = $ex->getMessage();
     }
 
     $time_end = microtime(true);
 
     $connection_id = null;
     if (idx($metadata, 'connectionID')) {
       $connection_id = $metadata['connectionID'];
     } else if (($method == 'conduit.connect') && $result) {
       $connection_id = idx($result, 'connectionID');
     }
 
     $log
       ->setCallerPHID(
         isset($conduit_user)
           ? $conduit_user->getPHID()
           : null)
       ->setConnectionID($connection_id)
       ->setError((string)$error_code)
       ->setDuration(1000000 * ($time_end - $time_start));
 
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
     $log->save();
     unset($unguarded);
 
     $response = id(new ConduitAPIResponse())
       ->setResult($result)
       ->setErrorCode($error_code)
       ->setErrorInfo($error_info);
 
     switch ($request->getStr('output')) {
       case 'human':
         return $this->buildHumanReadableResponse(
           $method,
           $api_request,
-          $response->toDictionary());
+          $response->toDictionary(),
+          $method_implementation);
       case 'json':
       default:
         return id(new AphrontJSONResponse())
           ->setAddJSONShield(false)
           ->setContent($response->toDictionary());
     }
   }
 
   /**
    * Change the api request user to the user that we want to act as.
    * Only admins can use actAsUser
    *
    * @param   ConduitAPIRequest Request being executed.
    * @param   string            The username of the user we want to act as
    */
   private function actAsUser(
     ConduitAPIRequest $api_request,
     $user_name) {
 
     $config_key = 'security.allow-conduit-act-as-user';
     if (!PhabricatorEnv::getEnvConfig($config_key)) {
       throw new Exception('security.allow-conduit-act-as-user is disabled');
     }
 
     if (!$api_request->getUser()->getIsAdmin()) {
       throw new Exception('Only administrators can use actAsUser');
     }
 
     $user = id(new PhabricatorUser())->loadOneWhere(
       'userName = %s',
       $user_name);
 
     if (!$user) {
       throw new Exception(
         "The actAsUser username '{$user_name}' is not a valid user."
       );
     }
 
     $api_request->setUser($user);
   }
 
   /**
    * Authenticate the client making the request to a Phabricator user account.
    *
    * @param   ConduitAPIRequest Request being executed.
    * @param   dict              Request metadata.
    * @return  null|pair         Null to indicate successful authentication, or
    *                            an error code and error message pair.
    */
   private function authenticateUser(
     ConduitAPIRequest $api_request,
     array $metadata) {
 
     $request = $this->getRequest();
 
     if ($request->getUser()->getPHID()) {
       $request->validateCSRF();
       return $this->validateAuthenticatedUser(
         $api_request,
         $request->getUser());
     }
 
     $auth_type = idx($metadata, 'auth.type');
     if ($auth_type === ConduitClient::AUTH_ASYMMETRIC) {
       $host = idx($metadata, 'auth.host');
       if (!$host) {
         return array(
           'ERR-INVALID-AUTH',
           pht(
             'Request is missing required "auth.host" parameter.'),
         );
       }
 
       // TODO: Validate that we are the host!
 
       $raw_key = idx($metadata, 'auth.key');
       $public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($raw_key);
       $ssl_public_key = $public_key->toPKCS8();
 
       // First, verify the signature.
       try {
         $protocol_data = $metadata;
 
         // TODO: We should stop writing this into the protocol data when
         // processing a request.
         unset($protocol_data['scope']);
 
         ConduitClient::verifySignature(
           $this->method,
           $api_request->getAllParameters(),
           $protocol_data,
           $ssl_public_key);
       } catch (Exception $ex) {
         return array(
           'ERR-INVALID-AUTH',
           pht(
             'Signature verification failure. %s',
             $ex->getMessage()),
         );
       }
 
       // If the signature is valid, find the user or device which is
       // associated with this public key.
 
       $stored_key = id(new PhabricatorAuthSSHKeyQuery())
         ->setViewer(PhabricatorUser::getOmnipotentUser())
         ->withKeys(array($public_key))
         ->executeOne();
       if (!$stored_key) {
         return array(
           'ERR-INVALID-AUTH',
           pht(
             'No user or device is associated with that public key.'),
         );
       }
 
       $object = $stored_key->getObject();
 
       if ($object instanceof PhabricatorUser) {
         $user = $object;
       } else {
         if (!$stored_key->getIsTrusted()) {
           return array(
             'ERR-INVALID-AUTH',
             pht(
               'The key which signed this request is not trusted. Only '.
               'trusted keys can be used to sign API calls.'),
           );
         }
 
         if (!PhabricatorEnv::isClusterRemoteAddress()) {
           return array(
             'ERR-INVALID-AUTH',
             pht(
               'This request originates from outside of the Phabricator '.
               'cluster address range. Requests signed with trusted '.
               'device keys must originate from within the cluster.'),
           );
         }
 
         $user = PhabricatorUser::getOmnipotentUser();
 
         // Flag this as an intracluster request.
         $api_request->setIsClusterRequest(true);
       }
 
       return $this->validateAuthenticatedUser(
         $api_request,
         $user);
     } else if ($auth_type === null) {
       // No specified authentication type, continue with other authentication
       // methods below.
     } else {
       return array(
         'ERR-INVALID-AUTH',
         pht(
           'Provided "auth.type" ("%s") is not recognized.',
           $auth_type),
       );
     }
 
     $token_string = idx($metadata, 'token');
     if (strlen($token_string)) {
 
       if (strlen($token_string) != 32) {
         return array(
           'ERR-INVALID-AUTH',
           pht(
             'API token "%s" has the wrong length. API tokens should be '.
             '32 characters long.',
             $token_string),
         );
       }
 
       $type = head(explode('-', $token_string));
       $valid_types = PhabricatorConduitToken::getAllTokenTypes();
       $valid_types = array_fuse($valid_types);
       if (empty($valid_types[$type])) {
         return array(
           'ERR-INVALID-AUTH',
           pht(
             'API token "%s" has the wrong format. API tokens should be '.
             '32 characters long and begin with one of these prefixes: %s.',
             $token_string,
             implode(', ', $valid_types)),
           );
       }
 
       $token = id(new PhabricatorConduitTokenQuery())
         ->setViewer(PhabricatorUser::getOmnipotentUser())
         ->withTokens(array($token_string))
         ->withExpired(false)
         ->executeOne();
       if (!$token) {
         $token = id(new PhabricatorConduitTokenQuery())
           ->setViewer(PhabricatorUser::getOmnipotentUser())
           ->withTokens(array($token_string))
           ->withExpired(true)
           ->executeOne();
         if ($token) {
           return array(
             'ERR-INVALID-AUTH',
             pht(
               'API token "%s" was previously valid, but has expired.',
               $token_string),
           );
         } else {
           return array(
             'ERR-INVALID-AUTH',
             pht(
               'API token "%s" is not valid.',
               $token_string),
           );
         }
       }
 
       // If this is a "cli-" token, it expires shortly after it is generated
       // by default. Once it is actually used, we extend its lifetime and make
       // it permanent. This allows stray tokens to get cleaned up automatically
       // if they aren't being used.
       if ($token->getTokenType() == PhabricatorConduitToken::TYPE_COMMANDLINE) {
         if ($token->getExpires()) {
           $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
             $token->setExpires(null);
             $token->save();
           unset($unguarded);
         }
       }
 
       // If this is a "clr-" token, Phabricator must be configured in cluster
       // mode and the remote address must be a cluster node.
       if ($token->getTokenType() == PhabricatorConduitToken::TYPE_CLUSTER) {
         if (!PhabricatorEnv::isClusterRemoteAddress()) {
           return array(
             'ERR-INVALID-AUTH',
             pht(
               'This request originates from outside of the Phabricator '.
               'cluster address range. Requests signed with cluster API '.
               'tokens must originate from within the cluster.'),
           );
         }
 
         // Flag this as an intracluster request.
         $api_request->setIsClusterRequest(true);
       }
 
       $user = $token->getObject();
       if (!($user instanceof PhabricatorUser)) {
         return array(
           'ERR-INVALID-AUTH',
           pht(
             'API token is not associated with a valid user.'),
         );
       }
 
       return $this->validateAuthenticatedUser(
         $api_request,
         $user);
     }
 
     // handle oauth
     $access_token = idx($metadata, 'access_token');
     $method_scope = idx($metadata, 'scope');
     if ($access_token &&
         $method_scope != PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE) {
       $token = id(new PhabricatorOAuthServerAccessToken())
         ->loadOneWhere('token = %s',
                        $access_token);
       if (!$token) {
         return array(
           'ERR-INVALID-AUTH',
           'Access token does not exist.',
         );
       }
 
       $oauth_server = new PhabricatorOAuthServer();
       $valid = $oauth_server->validateAccessToken($token,
                                                   $method_scope);
       if (!$valid) {
         return array(
           'ERR-INVALID-AUTH',
           'Access token is invalid.',
         );
       }
 
       // valid token, so let's log in the user!
       $user_phid = $token->getUserPHID();
       $user = id(new PhabricatorUser())
         ->loadOneWhere('phid = %s',
                        $user_phid);
       if (!$user) {
         return array(
           'ERR-INVALID-AUTH',
           'Access token is for invalid user.',
         );
       }
       return $this->validateAuthenticatedUser(
         $api_request,
         $user);
     }
 
     // Handle sessionless auth.
     // TODO: This is super messy.
     // TODO: Remove this in favor of token-based auth.
 
     if (isset($metadata['authUser'])) {
       $user = id(new PhabricatorUser())->loadOneWhere(
         'userName = %s',
         $metadata['authUser']);
       if (!$user) {
         return array(
           'ERR-INVALID-AUTH',
           'Authentication is invalid.',
         );
       }
       $token = idx($metadata, 'authToken');
       $signature = idx($metadata, 'authSignature');
       $certificate = $user->getConduitCertificate();
       if (sha1($token.$certificate) !== $signature) {
         return array(
           'ERR-INVALID-AUTH',
           'Authentication is invalid.',
         );
       }
       return $this->validateAuthenticatedUser(
         $api_request,
         $user);
     }
 
     // Handle session-based auth.
     // TODO: Remove this in favor of token-based auth.
 
     $session_key = idx($metadata, 'sessionKey');
     if (!$session_key) {
       return array(
         'ERR-INVALID-SESSION',
         'Session key is not present.',
       );
     }
 
     $user = id(new PhabricatorAuthSessionEngine())
       ->loadUserForSession(PhabricatorAuthSession::TYPE_CONDUIT, $session_key);
 
     if (!$user) {
       return array(
         'ERR-INVALID-SESSION',
         'Session key is invalid.',
       );
     }
 
     return $this->validateAuthenticatedUser(
       $api_request,
       $user);
   }
 
   private function validateAuthenticatedUser(
     ConduitAPIRequest $request,
     PhabricatorUser $user) {
 
     if (!$user->isUserActivated()) {
       return array(
         'ERR-USER-DISABLED',
         pht('User account is not activated.'),
       );
     }
 
     $request->setUser($user);
     return null;
   }
 
   private function buildHumanReadableResponse(
     $method,
     ConduitAPIRequest $request = null,
-    $result = null) {
+    $result = null,
+    ConduitAPIMethod $method_implementation = null) {
 
     $param_rows = array();
     $param_rows[] = array('Method', $this->renderAPIValue($method));
     if ($request) {
       foreach ($request->getAllParameters() as $key => $value) {
         $param_rows[] = array(
           $key,
           $this->renderAPIValue($value),
         );
       }
     }
 
     $param_table = new AphrontTableView($param_rows);
     $param_table->setColumnClasses(
       array(
         'header',
         'wide',
       ));
 
     $result_rows = array();
     foreach ($result as $key => $value) {
       $result_rows[] = array(
         $key,
         $this->renderAPIValue($value),
       );
     }
 
     $result_table = new AphrontTableView($result_rows);
     $result_table->setColumnClasses(
       array(
         'header',
         'wide',
       ));
 
     $param_panel = new PHUIObjectBoxView();
     $param_panel->setHeaderText(pht('Method Parameters'));
     $param_panel->appendChild($param_table);
 
     $result_panel = new PHUIObjectBoxView();
     $result_panel->setHeaderText(pht('Method Result'));
     $result_panel->appendChild($result_table);
 
     $method_uri = $this->getApplicationURI('method/'.$method.'/');
 
     $crumbs = $this->buildApplicationCrumbs()
       ->addTextCrumb($method, $method_uri)
       ->addTextCrumb(pht('Call'));
 
+    $example_panel = null;
+    if ($request && $method_implementation) {
+      $params = $request->getAllParameters();
+      $example_panel = $this->renderExampleBox(
+        $method_implementation,
+        $params);
+    }
+
     return $this->buildApplicationPage(
       array(
         $crumbs,
         $param_panel,
         $result_panel,
+        $example_panel,
       ),
       array(
         'title' => pht('Method Call Result'),
       ));
   }
 
   private function renderAPIValue($value) {
     $json = new PhutilJSON();
     if (is_array($value)) {
       $value = $json->encodeFormatted($value);
     }
 
     $value = phutil_tag(
       'pre',
       array('style' => 'white-space: pre-wrap;'),
       $value);
 
     return $value;
   }
 
   private function decodeConduitParams(
     AphrontRequest $request,
     $method) {
 
     // Look for parameters from the Conduit API Console, which are encoded
     // as HTTP POST parameters in an array, e.g.:
     //
     //   params[name]=value&params[name2]=value2
     //
     // The fields are individually JSON encoded, since we require users to
     // enter JSON so that we avoid type ambiguity.
 
     $params = $request->getArr('params', null);
     if ($params !== null) {
       foreach ($params as $key => $value) {
         if ($value == '') {
           // Interpret empty string null (e.g., the user didn't type anything
           // into the box).
           $value = 'null';
         }
         $decoded_value = json_decode($value, true);
         if ($decoded_value === null && strtolower($value) != 'null') {
           // When json_decode() fails, it returns null. This almost certainly
           // indicates that a user was using the web UI and didn't put quotes
           // around a string value. We can either do what we think they meant
           // (treat it as a string) or fail. For now, err on the side of
           // caution and fail. In the future, if we make the Conduit API
           // actually do type checking, it might be reasonable to treat it as
           // a string if the parameter type is string.
           throw new Exception(
             "The value for parameter '{$key}' is not valid JSON. All ".
             "parameters must be encoded as JSON values, including strings ".
             "(which means you need to surround them in double quotes). ".
             "Check your syntax. Value was: {$value}");
         }
         $params[$key] = $decoded_value;
       }
 
       $metadata = idx($params, '__conduit__', array());
       unset($params['__conduit__']);
 
       return array($metadata, $params);
     }
 
     // Otherwise, look for a single parameter called 'params' which has the
     // entire param dictionary JSON encoded.
     $params_json = $request->getStr('params');
     if (strlen($params_json)) {
       $params = null;
       try {
         $params = phutil_json_decode($params_json);
       } catch (PhutilJSONParserException $ex) {
         throw new PhutilProxyException(
           pht(
             "Invalid parameter information was passed to method '%s'",
             $method),
           $ex);
       }
 
       $metadata = idx($params, '__conduit__', array());
       unset($params['__conduit__']);
 
       return array($metadata, $params);
     }
 
     // If we do not have `params`, assume this is a simple HTTP request with
     // HTTP key-value pairs.
     $params = array();
     $metadata = array();
     foreach ($request->getPassthroughRequestData() as $key => $value) {
       $meta_key = ConduitAPIMethod::getParameterMetadataKey($key);
       if ($meta_key !== null) {
         $metadata[$meta_key] = $value;
       } else {
         $params[$key] = $value;
       }
     }
 
     return array($metadata, $params);
   }
 
 }
diff --git a/src/applications/conduit/controller/PhabricatorConduitConsoleController.php b/src/applications/conduit/controller/PhabricatorConduitConsoleController.php
index 62cdb7ba77..5c0ccfe6fb 100644
--- a/src/applications/conduit/controller/PhabricatorConduitConsoleController.php
+++ b/src/applications/conduit/controller/PhabricatorConduitConsoleController.php
@@ -1,196 +1,210 @@
 <?php
 
 final class PhabricatorConduitConsoleController
   extends PhabricatorConduitController {
 
-  private $method;
-
   public function shouldAllowPublic() {
     return true;
   }
 
-  public function willProcessRequest(array $data) {
-    $this->method = $data['method'];
-  }
-
-  public function processRequest() {
-
-    $request = $this->getRequest();
-    $viewer = $request->getUser();
+  public function handleRequest(AphrontRequest $request) {
+    $viewer = $this->getViewer();
+    $method_name = $request->getURIData('method');
 
     $method = id(new PhabricatorConduitMethodQuery())
       ->setViewer($viewer)
-      ->withMethods(array($this->method))
+      ->withMethods(array($method_name))
       ->executeOne();
-
     if (!$method) {
       return new Aphront404Response();
     }
 
-    $can_call_method = false;
+    $call_uri = '/api/'.$method->getAPIMethodName();
 
     $status = $method->getMethodStatus();
     $reason = $method->getMethodStatusDescription();
     $errors = array();
 
     switch ($status) {
       case ConduitAPIMethod::METHOD_STATUS_DEPRECATED:
         $reason = nonempty($reason, pht('This method is deprecated.'));
         $errors[] = pht('Deprecated Method: %s', $reason);
         break;
       case ConduitAPIMethod::METHOD_STATUS_UNSTABLE:
         $reason = nonempty(
           $reason,
           pht(
             'This method is new and unstable. Its interface is subject '.
             'to change.'));
         $errors[] = pht('Unstable Method: %s', $reason);
         break;
     }
 
-    $error_types = $method->getErrorTypes();
-    $error_types['ERR-CONDUIT-CORE'] = pht('See error message for details.');
-    $error_description = array();
-    foreach ($error_types as $error => $meaning) {
-      $error_description[] = hsprintf(
-        '<li><strong>%s:</strong> %s</li>',
-        $error,
-        $meaning);
-    }
-    $error_description = phutil_tag('ul', array(), $error_description);
-
-    $form = new AphrontFormView();
-    $form
+    $form = id(new AphrontFormView())
+      ->setAction($call_uri)
       ->setUser($request->getUser())
-      ->setAction('/api/'.$this->method)
-      ->appendChild(
-        id(new AphrontFormStaticControl())
-          ->setLabel('Description')
-          ->setValue($method->getMethodDescription()))
-      ->appendChild(
-        id(new AphrontFormStaticControl())
-          ->setLabel('Returns')
-          ->setValue($method->getReturnType()))
-      ->appendChild(
-        id(new AphrontFormMarkupControl())
-          ->setLabel('Errors')
-          ->setValue($error_description))
-      ->appendChild(hsprintf(
-        '<p class="aphront-form-instructions">Enter parameters using '.
-        '<strong>JSON</strong>. For instance, to enter a list, type: '.
-        '<tt>["apple", "banana", "cherry"]</tt>'));
+      ->appendRemarkupInstructions(
+        pht(
+          'Enter parameters using **JSON**. For instance, to enter a '.
+          'list, type: `["apple", "banana", "cherry"]`'));
 
     $params = $method->getParamTypes();
     foreach ($params as $param => $desc) {
       $form->appendChild(
         id(new AphrontFormTextControl())
           ->setLabel($param)
           ->setName("params[{$param}]")
           ->setCaption($desc));
     }
 
     $must_login = !$viewer->isLoggedIn() &&
                   $method->shouldRequireAuthentication();
     if ($must_login) {
       $errors[] = pht(
         'Login Required: This method requires authentication. You must '.
         'log in before you can make calls to it.');
     } else {
       $form
         ->appendChild(
           id(new AphrontFormSelectControl())
             ->setLabel('Output Format')
             ->setName('output')
             ->setOptions(
               array(
                 'human' => 'Human Readable',
                 'json'  => 'JSON',
               )))
         ->appendChild(
           id(new AphrontFormSubmitControl())
             ->addCancelButton($this->getApplicationURI())
             ->setValue(pht('Call Method')));
     }
 
     $header = id(new PHUIHeaderView())
       ->setUser($viewer)
       ->setHeader($method->getAPIMethodName());
 
     $form_box = id(new PHUIObjectBoxView())
-      ->setHeader($header)
-      ->setFormErrors($errors)
+      ->setHeaderText(pht('Call Method'))
       ->appendChild($form);
 
     $content = array();
 
+    $properties = $this->buildMethodProperties($method);
+
+    $info_box = id(new PHUIObjectBoxView())
+      ->setHeaderText(pht('API Method: %s', $method->getAPIMethodName()))
+      ->setFormErrors($errors)
+      ->appendChild($properties);
+
+    $content[] = $info_box;
+    $content[] = $form_box;
+    $content[] = $this->renderExampleBox($method, null);
+
     $query = $method->newQueryObject();
     if ($query) {
       $orders = $query->getBuiltinOrders();
 
       $rows = array();
       foreach ($orders as $key => $order) {
         $rows[] = array(
           $key,
           $order['name'],
           implode(', ', $order['vector']),
         );
       }
 
       $table = id(new AphrontTableView($rows))
         ->setHeaders(
           array(
             pht('Key'),
             pht('Description'),
             pht('Columns'),
           ))
         ->setColumnClasses(
           array(
             'pri',
             '',
             'wide',
           ));
       $content[] = id(new PHUIObjectBoxView())
         ->setHeaderText(pht('Builtin Orders'))
         ->appendChild($table);
 
       $columns = $query->getOrderableColumns();
 
       $rows = array();
       foreach ($columns as $key => $column) {
         $rows[] = array(
           $key,
           idx($column, 'unique') ? pht('Yes') : pht('No'),
         );
       }
 
       $table = id(new AphrontTableView($rows))
         ->setHeaders(
           array(
             pht('Key'),
             pht('Unique'),
           ))
         ->setColumnClasses(
           array(
             'pri',
             'wide',
           ));
       $content[] = id(new PHUIObjectBoxView())
         ->setHeaderText(pht('Column Orders'))
         ->appendChild($table);
     }
 
     $crumbs = $this->buildApplicationCrumbs();
     $crumbs->addTextCrumb($method->getAPIMethodName());
 
     return $this->buildApplicationPage(
       array(
         $crumbs,
-        $form_box,
         $content,
       ),
       array(
         'title' => $method->getAPIMethodName(),
       ));
   }
 
+  private function buildMethodProperties(ConduitAPIMethod $method) {
+    $viewer = $this->getViewer();
+
+    $view = id(new PHUIPropertyListView());
+
+    $view->addProperty(
+      pht('Returns'),
+      $method->getReturnType());
+
+    $error_types = $method->getErrorTypes();
+    $error_types['ERR-CONDUIT-CORE'] = pht('See error message for details.');
+    $error_description = array();
+    foreach ($error_types as $error => $meaning) {
+      $error_description[] = hsprintf(
+        '<li><strong>%s:</strong> %s</li>',
+        $error,
+        $meaning);
+    }
+    $error_description = phutil_tag('ul', array(), $error_description);
+
+    $view->addProperty(
+      pht('Errors'),
+      $error_description);
+
+
+    $description = $method->getMethodDescription();
+    $description = PhabricatorMarkupEngine::renderOneObject(
+      id(new PhabricatorMarkupOneOff())->setContent($description),
+      'default',
+      $viewer);
+    $view->addSectionHeader(pht('Description'));
+    $view->addTextContent($description);
+
+    return $view;
+  }
+
+
 }
diff --git a/src/applications/conduit/controller/PhabricatorConduitController.php b/src/applications/conduit/controller/PhabricatorConduitController.php
index 4643511952..4fa11dbfad 100644
--- a/src/applications/conduit/controller/PhabricatorConduitController.php
+++ b/src/applications/conduit/controller/PhabricatorConduitController.php
@@ -1,27 +1,273 @@
 <?php
 
 abstract class PhabricatorConduitController extends PhabricatorController {
 
   protected function buildSideNavView() {
     $viewer = $this->getRequest()->getUser();
 
     $nav = new AphrontSideNavFilterView();
     $nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
 
     id(new PhabricatorConduitSearchEngine())
       ->setViewer($viewer)
       ->addNavigationItems($nav->getMenu());
 
     $nav->addLabel('Logs');
     $nav->addFilter('log', pht('Call Logs'));
 
     $nav->selectFilter(null);
 
     return $nav;
   }
 
   public function buildApplicationMenu() {
     return $this->buildSideNavView()->getMenu();
   }
 
+  protected function renderExampleBox(ConduitAPIMethod $method, $params) {
+    $arc_example = id(new PHUIPropertyListView())
+      ->addRawContent($this->renderExample($method, 'arc', $params));
+
+    $curl_example = id(new PHUIPropertyListView())
+      ->addRawContent($this->renderExample($method, 'curl', $params));
+
+    $php_example = id(new PHUIPropertyListView())
+      ->addRawContent($this->renderExample($method, 'php', $params));
+
+    $panel_link = phutil_tag(
+      'a',
+      array(
+        'href' => '/settings/panel/apitokens/',
+      ),
+      pht('Conduit API Tokens'));
+
+    $panel_link = phutil_tag('strong', array(), $panel_link);
+
+    $messages = array(
+      pht(
+        'Use the %s panel in Settings to generate or manage API tokens.',
+        $panel_link),
+    );
+
+    $info_view = id(new PHUIInfoView())
+      ->setErrors($messages)
+      ->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
+
+    return id(new PHUIObjectBoxView())
+      ->setHeaderText(pht('Examples'))
+      ->setInfoView($info_view)
+      ->addPropertyList($arc_example, pht('arc call-conduit'))
+      ->addPropertyList($curl_example, pht('cURL'))
+      ->addPropertyList($php_example, pht('PHP'));
+  }
+
+  private function renderExample(
+    ConduitAPIMethod $method,
+    $kind,
+    $params) {
+
+    switch ($kind) {
+      case 'arc':
+        $example = $this->buildArcanistExample($method, $params);
+        break;
+      case 'php':
+        $example = $this->buildPHPExample($method, $params);
+        break;
+      case 'curl':
+        $example = $this->buildCURLExample($method, $params);
+        break;
+      default:
+        throw new Exception(pht('Conduit client "%s" is not known.', $kind));
+    }
+
+    return $example;
+  }
+
+  private function buildArcanistExample(
+    ConduitAPIMethod $method,
+    $params) {
+
+    $parts = array();
+
+    $parts[] = '$ echo ';
+    if ($params === null) {
+      $parts[] = phutil_tag('strong', array(), '<json-parameters>');
+    } else {
+      $params = $this->simplifyParams($params);
+      $params = id(new PhutilJSON())->encodeFormatted($params);
+      $params = trim($params);
+      $params = csprintf('%s', $params);
+      $parts[] = phutil_tag('strong', array('class' => 'real'), $params);
+    }
+
+    $parts[] = ' | ';
+    $parts[] = 'arc call-conduit ';
+
+    $parts[] = '--conduit-uri ';
+    $parts[] = phutil_tag(
+      'strong',
+      array('class' => 'real'),
+      PhabricatorEnv::getURI('/'));
+    $parts[] = ' ';
+
+    $parts[] = '--conduit-token ';
+    $parts[] = phutil_tag('strong', array(), '<conduit-token>');
+    $parts[] = ' ';
+
+    $parts[] = $method->getAPIMethodName();
+
+    return $this->renderExampleCode($parts);
+  }
+
+  private function buildPHPExample(
+    ConduitAPIMethod $method,
+    $params) {
+
+    $parts = array();
+
+    $libphutil_path = 'path/to/libphutil/src/__phutil_library_init__.php';
+
+    $parts[] = '<?php';
+    $parts[] = "\n\n";
+
+    $parts[] = 'require_once ';
+    $parts[] = phutil_var_export($libphutil_path, true);
+    $parts[] = ";\n\n";
+
+    $parts[] = '$api_token = "';
+    $parts[] = phutil_tag('strong', array(), pht('<api-token>'));
+    $parts[] = "\";\n";
+
+    $parts[] = '$api_parameters = ';
+    if ($params === null) {
+      $parts[] = 'array(';
+      $parts[] = phutil_tag('strong', array(), pht('<parameters>'));
+      $parts[] = ');';
+    } else {
+      $params = $this->simplifyParams($params);
+      $params = phutil_var_export($params, true);
+      $parts[] = phutil_tag('strong', array('class' => 'real'), $params);
+      $parts[] = ';';
+    }
+    $parts[] = "\n\n";
+
+    $parts[] = '$client = new ConduitClient(';
+    $parts[] = phutil_tag(
+      'strong',
+      array('class' => 'real'),
+      phutil_var_export(PhabricatorEnv::getURI('/'), true));
+    $parts[] = ");\n";
+
+    $parts[] = '$client->setConduitToken($api_token);';
+    $parts[] = "\n\n";
+
+    $parts[] = '$result = $client->callMethodSynchronous(';
+    $parts[] = phutil_tag(
+      'strong',
+      array('class' => 'real'),
+      phutil_var_export($method->getAPIMethodName(), true));
+    $parts[] = ', ';
+    $parts[] = '$api_parameters';
+    $parts[] = ");\n";
+
+    $parts[] = 'print_r($result);';
+
+    return $this->renderExampleCode($parts);
+  }
+
+  private function buildCURLExample(
+    ConduitAPIMethod $method,
+    $params) {
+
+    $call_uri = '/api/'.$method->getAPIMethodName();
+
+    $parts = array();
+
+    $linebreak = array('\\', phutil_tag('br'), '    ');
+
+    $parts[] = '$ curl ';
+    $parts[] = phutil_tag(
+      'strong',
+      array('class' => 'real'),
+      csprintf('%R', PhabricatorEnv::getURI($call_uri)));
+    $parts[] = ' ';
+    $parts[] = $linebreak;
+
+    $parts[] = '-d api.token=';
+    $parts[] = phutil_tag('strong', array(), 'api-token');
+    $parts[] = ' ';
+    $parts[] = $linebreak;
+
+    if ($params === null) {
+      $parts[] = '-d ';
+      $parts[] = phutil_tag('strong', array(), 'param');
+      $parts[] = '=';
+      $parts[] = phutil_tag('strong', array(), 'value');
+      $parts[] = ' ';
+      $parts[] = $linebreak;
+      $parts[] = phutil_tag('strong', array(), '...');
+    } else {
+      $lines = array();
+      $params = $this->simplifyParams($params);
+
+      foreach ($params as $key => $value) {
+        $pieces = $this->getQueryStringParts(null, $key, $value);
+        foreach ($pieces as $piece) {
+          $lines[] = array(
+            '-d ',
+            phutil_tag('strong', array('class' => 'real'), $piece),
+          );
+        }
+      }
+
+      $parts[] = phutil_implode_html(array(' ', $linebreak), $lines);
+    }
+
+    return $this->renderExampleCode($parts);
+  }
+
+  private function renderExampleCode($example) {
+    require_celerity_resource('conduit-api-css');
+
+    return phutil_tag(
+      'div',
+      array(
+        'class' => 'PhabricatorMonospaced conduit-api-example-code',
+      ),
+      $example);
+  }
+
+  private function simplifyParams(array $params) {
+    foreach ($params as $key => $value) {
+      if ($value === null) {
+        unset($params[$key]);
+      }
+    }
+    return $params;
+  }
+
+  private function getQueryStringParts($prefix, $key, $value) {
+    if ($prefix === null) {
+      $head = phutil_escape_uri($key);
+    } else {
+      $head = $prefix.'['.phutil_escape_uri($key).']';
+    }
+
+    if (!is_array($value)) {
+      return array(
+        $head.'='.phutil_escape_uri($value),
+      );
+    }
+
+    $results = array();
+    foreach ($value as $subkey => $subvalue) {
+      $subparts = $this->getQueryStringParts($head, $subkey, $subvalue);
+      foreach ($subparts as $subpart) {
+        $results[] = $subpart;
+      }
+    }
+
+    return $results;
+  }
+
 }
diff --git a/src/applications/conduit/method/ConduitAPIMethod.php b/src/applications/conduit/method/ConduitAPIMethod.php
index a048e3e0f0..f6d464cfc5 100644
--- a/src/applications/conduit/method/ConduitAPIMethod.php
+++ b/src/applications/conduit/method/ConduitAPIMethod.php
@@ -1,375 +1,375 @@
 <?php
 
 /**
  * @task  status  Method Status
  * @task  pager   Paging Results
  */
 abstract class ConduitAPIMethod
   extends Phobject
   implements PhabricatorPolicyInterface {
 
   const METHOD_STATUS_STABLE      = 'stable';
   const METHOD_STATUS_UNSTABLE    = 'unstable';
   const METHOD_STATUS_DEPRECATED  = 'deprecated';
 
   abstract public function getMethodDescription();
   abstract protected function defineParamTypes();
   abstract protected function defineReturnType();
 
   protected function defineErrorTypes() {
     return array();
   }
 
   abstract protected function execute(ConduitAPIRequest $request);
 
 
   public function __construct() {}
 
   public function getParamTypes() {
     $types = $this->defineParamTypes();
 
     $query = $this->newQueryObject();
     if ($query) {
       $types['order'] = 'order';
       $types += $this->getPagerParamTypes();
     }
 
     return $types;
   }
 
   public function getReturnType() {
     return $this->defineReturnType();
   }
 
   public function getErrorTypes() {
     return $this->defineErrorTypes();
   }
 
   /**
    * This is mostly for compatibility with
    * @{class:PhabricatorCursorPagedPolicyAwareQuery}.
    */
   public function getID() {
     return $this->getAPIMethodName();
   }
 
   /**
    * Get the status for this method (e.g., stable, unstable or deprecated).
    * Should return a METHOD_STATUS_* constant. By default, methods are
    * "stable".
    *
    * @return const  METHOD_STATUS_* constant.
    * @task status
    */
   public function getMethodStatus() {
     return self::METHOD_STATUS_STABLE;
   }
 
   /**
    * Optional description to supplement the method status. In particular, if
    * a method is deprecated, you can return a string here describing the reason
    * for deprecation and stable alternatives.
    *
    * @return string|null  Description of the method status, if available.
    * @task status
    */
   public function getMethodStatusDescription() {
     return null;
   }
 
   public function getErrorDescription($error_code) {
     return idx($this->getErrorTypes(), $error_code, 'Unknown Error');
   }
 
   public function getRequiredScope() {
     // by default, conduit methods are not accessible via OAuth
     return PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE;
   }
 
   public function executeMethod(ConduitAPIRequest $request) {
     return $this->execute($request);
   }
 
   public abstract function getAPIMethodName();
 
   /**
    * Return a key which sorts methods by application name, then method status,
    * then method name.
    */
   public function getSortOrder() {
     $name = $this->getAPIMethodName();
 
     $map = array(
-      ConduitAPIMethod::METHOD_STATUS_STABLE      => 0,
-      ConduitAPIMethod::METHOD_STATUS_UNSTABLE    => 1,
-      ConduitAPIMethod::METHOD_STATUS_DEPRECATED  => 2,
+      self::METHOD_STATUS_STABLE      => 0,
+      self::METHOD_STATUS_UNSTABLE    => 1,
+      self::METHOD_STATUS_DEPRECATED  => 2,
     );
     $ord = idx($map, $this->getMethodStatus(), 0);
 
     list($head, $tail) = explode('.', $name, 2);
 
     return "{$head}.{$ord}.{$tail}";
   }
 
   public function getApplicationName() {
     return head(explode('.', $this->getAPIMethodName(), 2));
   }
 
   public static function getConduitMethod($method_name) {
     static $method_map = null;
 
     if ($method_map === null) {
       $methods = id(new PhutilSymbolLoader())
         ->setAncestorClass(__CLASS__)
         ->loadObjects();
 
       foreach ($methods as $method) {
         $name = $method->getAPIMethodName();
 
         if (empty($method_map[$name])) {
           $method_map[$name] = $method;
           continue;
         }
 
         $orig_class = get_class($method_map[$name]);
         $this_class = get_class($method);
         throw new Exception(
           "Two Conduit API method classes ({$orig_class}, {$this_class}) ".
           "both have the same method name ({$name}). API methods ".
           "must have unique method names.");
       }
     }
 
     return idx($method_map, $method_name);
   }
 
   public function shouldRequireAuthentication() {
     return true;
   }
 
   public function shouldAllowPublic() {
     return false;
   }
 
   public function shouldAllowUnguardedWrites() {
     return false;
   }
 
 
   /**
    * Optionally, return a @{class:PhabricatorApplication} which this call is
    * part of. The call will be disabled when the application is uninstalled.
    *
    * @return PhabricatorApplication|null  Related application.
    */
   public function getApplication() {
     return null;
   }
 
   protected function formatStringConstants($constants) {
     foreach ($constants as $key => $value) {
       $constants[$key] = '"'.$value.'"';
     }
     $constants = implode(', ', $constants);
     return 'string-constant<'.$constants.'>';
   }
 
   public static function getParameterMetadataKey($key) {
     if (strncmp($key, 'api.', 4) === 0) {
       // All keys passed beginning with "api." are always metadata keys.
       return substr($key, 4);
     } else {
       switch ($key) {
         // These are real keys which always belong to request metadata.
         case 'access_token':
         case 'scope':
         case 'output':
 
         // This is not a real metadata key; it is included here only to
         // prevent Conduit methods from defining it.
         case '__conduit__':
 
         // This is prevented globally as a blanket defense against OAuth
         // redirection attacks. It is included here to stop Conduit methods
         // from defining it.
         case 'code':
 
         // This is not a real metadata key, but the presence of this
         // parameter triggers an alternate request decoding pathway.
         case 'params':
           return $key;
       }
     }
 
     return null;
   }
 
 /* -(  Paging Results  )----------------------------------------------------- */
 
 
   /**
    * @task pager
    */
   protected function getPagerParamTypes() {
     return array(
       'before' => 'optional string',
       'after'  => 'optional string',
       'limit'  => 'optional int (default = 100)',
     );
   }
 
 
   /**
    * @task pager
    */
   protected function newPager(ConduitAPIRequest $request) {
     $limit = $request->getValue('limit', 100);
     $limit = min(1000, $limit);
     $limit = max(1, $limit);
 
     $pager = id(new AphrontCursorPagerView())
       ->setPageSize($limit);
 
     $before_id = $request->getValue('before');
     if ($before_id !== null) {
       $pager->setBeforeID($before_id);
     }
 
     $after_id = $request->getValue('after');
     if ($after_id !== null) {
       $pager->setAfterID($after_id);
     }
 
     return $pager;
   }
 
 
   /**
    * @task pager
    */
   protected function addPagerResults(
     array $results,
     AphrontCursorPagerView $pager) {
 
     $results['cursor'] = array(
       'limit' => $pager->getPageSize(),
       'after' => $pager->getNextPageID(),
       'before' => $pager->getPrevPageID(),
     );
 
     return $results;
   }
 
 
 /* -(  Implementing Query Methods  )----------------------------------------- */
 
 
   public function newQueryObject() {
     return null;
   }
 
 
   protected function newQueryForRequest(ConduitAPIRequest $request) {
     $query = $this->newQueryObject();
 
     if (!$query) {
       throw new Exception(
         pht(
           'You can not call newQueryFromRequest() in this method ("%s") '.
           'because it does not implement newQueryObject().',
           get_class($this)));
     }
 
     if (!($query instanceof PhabricatorCursorPagedPolicyAwareQuery)) {
       throw new Exception(
         pht(
           'Call to method newQueryObject() did not return an object of class '.
           '"%s".',
           'PhabricatorCursorPagedPolicyAwareQuery'));
     }
 
     $query->setViewer($request->getUser());
 
     $order = $request->getValue('order');
     if ($order !== null) {
       if (is_scalar($order)) {
         $query->setOrder($order);
       } else {
         $query->setOrderVector($order);
       }
     }
 
     return $query;
   }
 
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getPHID() {
     return null;
   }
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
     );
   }
 
   public function getPolicy($capability) {
     // Application methods get application visibility; other methods get open
     // visibility.
 
     $application = $this->getApplication();
     if ($application) {
       return $application->getPolicy($capability);
     }
 
     return PhabricatorPolicies::getMostOpenPolicy();
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     if (!$this->shouldRequireAuthentication()) {
       // Make unauthenticated methods universally visible.
       return true;
     }
 
     return false;
   }
 
   public function describeAutomaticCapability($capability) {
     return null;
   }
 
   protected function hasApplicationCapability(
     $capability,
     PhabricatorUser $viewer) {
 
     $application = $this->getApplication();
 
     if (!$application) {
       return false;
     }
 
     return PhabricatorPolicyFilter::hasCapability(
       $viewer,
       $application,
       $capability);
   }
 
   protected function requireApplicationCapability(
     $capability,
     PhabricatorUser $viewer) {
 
     $application = $this->getApplication();
     if (!$application) {
       return;
     }
 
     PhabricatorPolicyFilter::requireCapability(
       $viewer,
       $this->getApplication(),
       $capability);
   }
 
 }
diff --git a/src/applications/conduit/query/PhabricatorConduitLogQuery.php b/src/applications/conduit/query/PhabricatorConduitLogQuery.php
index 4d66cccae5..a08fd25a68 100644
--- a/src/applications/conduit/query/PhabricatorConduitLogQuery.php
+++ b/src/applications/conduit/query/PhabricatorConduitLogQuery.php
@@ -1,46 +1,46 @@
 <?php
 
 final class PhabricatorConduitLogQuery
   extends PhabricatorCursorPagedPolicyAwareQuery {
 
   private $methods;
 
   public function withMethods(array $methods) {
     $this->methods = $methods;
     return $this;
   }
 
   protected function loadPage() {
     $table = new PhabricatorConduitMethodCallLog();
     $conn_r = $table->establishConnection('r');
 
     $data = queryfx_all(
       $conn_r,
       'SELECT * FROM %T %Q %Q %Q',
       $table->getTableName(),
       $this->buildWhereClause($conn_r),
       $this->buildOrderClause($conn_r),
       $this->buildLimitClause($conn_r));
 
-    return $table->loadAllFromArray($data);;
+    return $table->loadAllFromArray($data);
   }
 
   protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
     $where = array();
 
     if ($this->methods) {
       $where[] = qsprintf(
         $conn_r,
         'method IN (%Ls)',
         $this->methods);
     }
 
     $where[] = $this->buildPagingClause($conn_r);
     return $this->formatWhereClause($where);
   }
 
   public function getQueryApplicationClass() {
     return 'PhabricatorConduitApplication';
   }
 
 }
diff --git a/src/applications/conduit/query/PhabricatorConduitTokenQuery.php b/src/applications/conduit/query/PhabricatorConduitTokenQuery.php
index 870043cac8..44586f1815 100644
--- a/src/applications/conduit/query/PhabricatorConduitTokenQuery.php
+++ b/src/applications/conduit/query/PhabricatorConduitTokenQuery.php
@@ -1,128 +1,128 @@
 <?php
 
 final class PhabricatorConduitTokenQuery
   extends PhabricatorCursorPagedPolicyAwareQuery {
 
   private $ids;
   private $objectPHIDs;
   private $expired;
   private $tokens;
   private $tokenTypes;
 
   public function withExpired($expired) {
     $this->expired = $expired;
     return $this;
   }
 
   public function withIDs(array $ids) {
     $this->ids = $ids;
     return $this;
   }
 
   public function withObjectPHIDs(array $phids) {
     $this->objectPHIDs = $phids;
     return $this;
   }
 
   public function withTokens(array $tokens) {
     $this->tokens = $tokens;
     return $this;
   }
 
   public function withTokenTypes(array $types) {
     $this->tokenTypes = $types;
     return $this;
   }
 
   protected function loadPage() {
     $table = new PhabricatorConduitToken();
     $conn_r = $table->establishConnection('r');
 
     $data = queryfx_all(
       $conn_r,
       'SELECT * FROM %T %Q %Q %Q',
       $table->getTableName(),
       $this->buildWhereClause($conn_r),
       $this->buildOrderClause($conn_r),
       $this->buildLimitClause($conn_r));
 
-    return $table->loadAllFromArray($data);;
+    return $table->loadAllFromArray($data);
   }
 
   protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
     $where = array();
 
     if ($this->ids !== null) {
       $where[] = qsprintf(
         $conn_r,
         'id IN (%Ld)',
         $this->ids);
     }
 
     if ($this->objectPHIDs !== null) {
       $where[] = qsprintf(
         $conn_r,
         'objectPHID IN (%Ls)',
         $this->objectPHIDs);
     }
 
     if ($this->tokens !== null) {
       $where[] = qsprintf(
         $conn_r,
         'token IN (%Ls)',
         $this->tokens);
     }
 
     if ($this->tokenTypes !== null) {
       $where[] = qsprintf(
         $conn_r,
         'tokenType IN (%Ls)',
         $this->tokenTypes);
     }
 
     if ($this->expired !== null) {
       if ($this->expired) {
         $where[] = qsprintf(
           $conn_r,
           'expires <= %d',
           PhabricatorTime::getNow());
       } else {
         $where[] = qsprintf(
           $conn_r,
           'expires IS NULL OR expires > %d',
           PhabricatorTime::getNow());
       }
     }
 
     $where[] = $this->buildPagingClause($conn_r);
 
     return $this->formatWhereClause($where);
   }
 
   protected function willFilterPage(array $tokens) {
     $object_phids = mpull($tokens, 'getObjectPHID');
     $objects = id(new PhabricatorObjectQuery())
       ->setViewer($this->getViewer())
       ->setParentQuery($this)
       ->withPHIDs($object_phids)
       ->execute();
     $objects = mpull($objects, null, 'getPHID');
 
     foreach ($tokens as $key => $token) {
       $object = idx($objects, $token->getObjectPHID(), null);
       if (!$object) {
         $this->didRejectResult($token);
         unset($tokens[$key]);
         continue;
       }
       $token->attachObject($object);
     }
 
     return $tokens;
   }
 
   public function getQueryApplicationClass() {
     return 'PhabricatorConduitApplication';
   }
 
 }
diff --git a/src/applications/conduit/storage/PhabricatorConduitToken.php b/src/applications/conduit/storage/PhabricatorConduitToken.php
index ab4d88335e..f5673fdab3 100644
--- a/src/applications/conduit/storage/PhabricatorConduitToken.php
+++ b/src/applications/conduit/storage/PhabricatorConduitToken.php
@@ -1,165 +1,165 @@
 <?php
 
 final class PhabricatorConduitToken
   extends PhabricatorConduitDAO
   implements PhabricatorPolicyInterface {
 
   protected $objectPHID;
   protected $tokenType;
   protected $token;
   protected $expires;
 
   private $object = self::ATTACHABLE;
 
   const TYPE_STANDARD = 'api';
   const TYPE_COMMANDLINE = 'cli';
   const TYPE_CLUSTER = 'clr';
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_COLUMN_SCHEMA => array(
         'tokenType' => 'text32',
         'token' => 'text32',
         'expires' => 'epoch?',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_object' => array(
           'columns' => array('objectPHID', 'tokenType'),
         ),
         'key_token' => array(
           'columns' => array('token'),
           'unique' => true,
         ),
         'key_expires' => array(
           'columns' => array('expires'),
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public static function loadClusterTokenForUser(PhabricatorUser $user) {
     if (!$user->isLoggedIn()) {
       return null;
     }
 
     $tokens = id(new PhabricatorConduitTokenQuery())
       ->setViewer($user)
       ->withObjectPHIDs(array($user->getPHID()))
       ->withTokenTypes(array(self::TYPE_CLUSTER))
       ->withExpired(false)
       ->execute();
 
     // Only return a token if it has at least 5 minutes left before
     // expiration. Cluster tokens cycle regularly, so we don't want to use
     // one that's going to expire momentarily.
     $now = PhabricatorTime::getNow();
     $must_expire_after = $now + phutil_units('5 minutes in seconds');
 
     foreach ($tokens as $token) {
       if ($token->getExpires() > $must_expire_after) {
         return $token;
       }
     }
 
     // We didn't find any existing tokens (or the existing tokens are all about
     // to expire) so generate a new token.
 
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
-      $token = PhabricatorConduitToken::initializeNewToken(
+      $token = self::initializeNewToken(
         $user->getPHID(),
         self::TYPE_CLUSTER);
       $token->save();
     unset($unguarded);
 
     return $token;
   }
 
   public static function initializeNewToken($object_phid, $token_type) {
     $token = new PhabricatorConduitToken();
     $token->objectPHID = $object_phid;
     $token->tokenType = $token_type;
     $token->expires = $token->getTokenExpires($token_type);
 
     $secret = $token_type.'-'.Filesystem::readRandomCharacters(32);
     $secret = substr($secret, 0, 32);
     $token->token = $secret;
 
     return $token;
   }
 
   public static function getTokenTypeName($type) {
     $map = array(
       self::TYPE_STANDARD => pht('Standard API Token'),
       self::TYPE_COMMANDLINE => pht('Command Line API Token'),
       self::TYPE_CLUSTER => pht('Cluster API Token'),
     );
 
     return idx($map, $type, $type);
   }
 
   public static function getAllTokenTypes() {
     return array(
       self::TYPE_STANDARD,
       self::TYPE_COMMANDLINE,
       self::TYPE_CLUSTER,
     );
   }
 
   private function getTokenExpires($token_type) {
     $now = PhabricatorTime::getNow();
     switch ($token_type) {
       case self::TYPE_STANDARD:
         return null;
       case self::TYPE_COMMANDLINE:
         return $now + phutil_units('1 hour in seconds');
       case self::TYPE_CLUSTER:
         return $now + phutil_units('30 minutes in seconds');
       default:
         throw new Exception(
           pht('Unknown Conduit token type "%s"!', $token_type));
     }
   }
 
   public function getPublicTokenName() {
     switch ($this->getTokenType()) {
       case self::TYPE_CLUSTER:
         return pht('Cluster API Token');
       default:
         return substr($this->getToken(), 0, 8).'...';
     }
   }
 
   public function getObject() {
     return $this->assertAttached($this->object);
   }
 
   public function attachObject(PhabricatorUser $object) {
     $this->object = $object;
     return $this;
   }
 
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
       PhabricatorPolicyCapability::CAN_EDIT,
     );
   }
 
   public function getPolicy($capability) {
     return $this->getObject()->getPolicy($capability);
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     return $this->getObject()->hasAutomaticCapability($capability, $viewer);
   }
 
   public function describeAutomaticCapability($capability) {
     return pht(
       'Conduit tokens inherit the policies of the user they authenticate.');
   }
 
 }
diff --git a/src/applications/config/check/PhabricatorSetupCheck.php b/src/applications/config/check/PhabricatorSetupCheck.php
index b40dd4c8cc..a689d14943 100644
--- a/src/applications/config/check/PhabricatorSetupCheck.php
+++ b/src/applications/config/check/PhabricatorSetupCheck.php
@@ -1,170 +1,170 @@
 <?php
 
 abstract class PhabricatorSetupCheck {
 
   private $issues;
 
   abstract protected function executeChecks();
 
   const GROUP_OTHER       = 'other';
   const GROUP_MYSQL       = 'mysql';
   const GROUP_PHP         = 'php';
   const GROUP_IMPORTANT   = 'important';
 
   public function getExecutionOrder() {
     return 1;
   }
 
   final protected function newIssue($key) {
     $issue = id(new PhabricatorSetupIssue())
       ->setIssueKey($key);
     $this->issues[$key] = $issue;
 
     if ($this->getDefaultGroup()) {
       $issue->setGroup($this->getDefaultGroup());
     }
 
     return $issue;
   }
 
   final public function getIssues() {
     return $this->issues;
   }
 
   protected function addIssue(PhabricatorSetupIssue $issue) {
     $this->issues[$issue->getIssueKey()] = $issue;
     return $this;
   }
 
   public function getDefaultGroup() {
     return null;
   }
 
   final public function runSetupChecks() {
     $this->issues = array();
     $this->executeChecks();
   }
 
   final public static function getOpenSetupIssueKeys() {
     $cache = PhabricatorCaches::getSetupCache();
     return $cache->getKey('phabricator.setup.issue-keys');
   }
 
   final public static function setOpenSetupIssueKeys(array $keys) {
     $cache = PhabricatorCaches::getSetupCache();
     $cache->setKey('phabricator.setup.issue-keys', $keys);
   }
 
   final public static function getUnignoredIssueKeys(array $all_issues) {
     assert_instances_of($all_issues, 'PhabricatorSetupIssue');
     $keys = array();
     foreach ($all_issues as $issue) {
       if (!$issue->getIsIgnored()) {
         $keys[] = $issue->getIssueKey();
       }
     }
     return $keys;
   }
 
   final public static function getConfigNeedsRepair() {
     $cache = PhabricatorCaches::getSetupCache();
     return $cache->getKey('phabricator.setup.needs-repair');
   }
 
   final public static function setConfigNeedsRepair($needs_repair) {
     $cache = PhabricatorCaches::getSetupCache();
     $cache->setKey('phabricator.setup.needs-repair', $needs_repair);
   }
 
   final public static function deleteSetupCheckCache() {
     $cache = PhabricatorCaches::getSetupCache();
     $cache->deleteKeys(
       array(
         'phabricator.setup.needs-repair',
         'phabricator.setup.issue-keys',
       ));
   }
 
   final public static function willProcessRequest() {
     $issue_keys = self::getOpenSetupIssueKeys();
     if ($issue_keys === null) {
       $issues = self::runAllChecks();
       foreach ($issues as $issue) {
         if ($issue->getIsFatal()) {
           $view = id(new PhabricatorSetupIssueView())
             ->setIssue($issue);
           return id(new PhabricatorConfigResponse())
             ->setView($view);
         }
       }
       self::setOpenSetupIssueKeys(self::getUnignoredIssueKeys($issues));
     }
 
     // Try to repair configuration unless we have a clean bill of health on it.
     // We need to keep doing this on every page load until all the problems
     // are fixed, which is why it's separate from setup checks (which run
     // once per restart).
     $needs_repair = self::getConfigNeedsRepair();
     if ($needs_repair !== false) {
       $needs_repair = self::repairConfig();
       self::setConfigNeedsRepair($needs_repair);
     }
   }
 
   final public static function runAllChecks() {
     $symbols = id(new PhutilSymbolLoader())
-      ->setAncestorClass('PhabricatorSetupCheck')
+      ->setAncestorClass(__CLASS__)
       ->setConcreteOnly(true)
       ->selectAndLoadSymbols();
 
     $checks = array();
     foreach ($symbols as $symbol) {
       $checks[] = newv($symbol['name'], array());
     }
 
     $checks = msort($checks, 'getExecutionOrder');
 
     $issues = array();
     foreach ($checks as $check) {
       $check->runSetupChecks();
       foreach ($check->getIssues() as $key => $issue) {
         if (isset($issues[$key])) {
           throw new Exception(
             "Two setup checks raised an issue with key '{$key}'!");
         }
         $issues[$key] = $issue;
         if ($issue->getIsFatal()) {
           break 2;
         }
       }
     }
 
     foreach (PhabricatorEnv::getEnvConfig('config.ignore-issues')
               as $ignorable => $derp) {
       if (isset($issues[$ignorable])) {
         $issues[$ignorable]->setIsIgnored(true);
       }
     }
 
     return $issues;
   }
 
   final public static function repairConfig() {
     $needs_repair = false;
 
     $options = PhabricatorApplicationConfigOptions::loadAllOptions();
     foreach ($options as $option) {
       try {
         $option->getGroup()->validateOption(
           $option,
           PhabricatorEnv::getEnvConfig($option->getKey()));
       } catch (PhabricatorConfigValidationException $ex) {
         PhabricatorEnv::repairConfig($option->getKey(), $option->getDefault());
         $needs_repair = true;
       }
     }
 
     return $needs_repair;
   }
 
 }
diff --git a/src/applications/config/option/PhabricatorApplicationConfigOptions.php b/src/applications/config/option/PhabricatorApplicationConfigOptions.php
index 6982ef513f..7ac1df3e16 100644
--- a/src/applications/config/option/PhabricatorApplicationConfigOptions.php
+++ b/src/applications/config/option/PhabricatorApplicationConfigOptions.php
@@ -1,243 +1,249 @@
 <?php
 
 abstract class PhabricatorApplicationConfigOptions extends Phobject {
 
   abstract public function getName();
   abstract public function getDescription();
   abstract public function getGroup();
   abstract public function getOptions();
 
   public function getFontIcon() {
     return 'fa-sliders';
   }
 
   public function validateOption(PhabricatorConfigOption $option, $value) {
     if ($value === $option->getDefault()) {
       return;
     }
 
     if ($value === null) {
       return;
     }
 
     if ($option->isCustomType()) {
       return $option->getCustomObject()->validateOption($option, $value);
     }
 
     switch ($option->getType()) {
       case 'bool':
         if ($value !== true &&
             $value !== false) {
           throw new PhabricatorConfigValidationException(
             pht(
               "Option '%s' is of type bool, but value is not true or false.",
               $option->getKey()));
         }
         break;
       case 'int':
         if (!is_int($value)) {
           throw new PhabricatorConfigValidationException(
             pht(
               "Option '%s' is of type int, but value is not an integer.",
               $option->getKey()));
         }
         break;
       case 'string':
         if (!is_string($value)) {
           throw new PhabricatorConfigValidationException(
             pht(
               "Option '%s' is of type string, but value is not a string.",
               $option->getKey()));
         }
         break;
       case 'class':
         $symbols = id(new PhutilSymbolLoader())
           ->setType('class')
           ->setAncestorClass($option->getBaseClass())
           ->setConcreteOnly(true)
           ->selectSymbolsWithoutLoading();
         $names = ipull($symbols, 'name', 'name');
         if (empty($names[$value])) {
           throw new PhabricatorConfigValidationException(
             pht(
               "Option '%s' value must name a class extending '%s'.",
               $option->getKey(),
               $option->getBaseClass()));
         }
         break;
       case 'set':
         $valid = true;
         if (!is_array($value)) {
           throw new PhabricatorConfigValidationException(
             pht(
               "Option '%s' must be a set, but value is not an array.",
               $option->getKey()));
         }
         foreach ($value as $v) {
           if ($v !== true) {
             throw new PhabricatorConfigValidationException(
               pht(
                 "Option '%s' must be a set, but array contains values other ".
                 "than 'true'.",
                 $option->getKey()));
           }
         }
         break;
       case 'list<regex>':
         $valid = true;
         if (!is_array($value)) {
           throw new PhabricatorConfigValidationException(
             pht(
               "Option '%s' must be a list of regular expressions, but value ".
               "is not an array.",
               $option->getKey()));
         }
         if ($value && array_keys($value) != range(0, count($value) - 1)) {
           throw new PhabricatorConfigValidationException(
             pht(
               "Option '%s' must be a list of regular expressions, but the ".
               "value is a map with unnatural keys.",
               $option->getKey()));
         }
         foreach ($value as $v) {
           $ok = @preg_match($v, '');
           if ($ok === false) {
             throw new PhabricatorConfigValidationException(
               pht(
                 "Option '%s' must be a list of regular expressions, but the ".
                 "value '%s' is not a valid regular expression.",
                 $option->getKey(),
                 $v));
           }
         }
         break;
       case 'list<string>':
         $valid = true;
         if (!is_array($value)) {
           throw new PhabricatorConfigValidationException(
             pht(
               "Option '%s' must be a list of strings, but value is not ".
               "an array.",
               $option->getKey()));
         }
         if ($value && array_keys($value) != range(0, count($value) - 1)) {
           throw new PhabricatorConfigValidationException(
             pht(
               "Option '%s' must be a list of strings, but the value is a ".
               "map with unnatural keys.",
               $option->getKey()));
         }
         foreach ($value as $v) {
           if (!is_string($v)) {
             throw new PhabricatorConfigValidationException(
               pht(
                 "Option '%s' must be a list of strings, but it contains one ".
                 "or more non-strings.",
                 $option->getKey()));
           }
         }
         break;
       case 'wild':
       default:
         break;
     }
 
     $this->didValidateOption($option, $value);
   }
 
   protected function didValidateOption(
     PhabricatorConfigOption $option,
     $value) {
     // Hook for subclasses to do complex validation.
     return;
   }
 
   /**
    * Hook to render additional hints based on, e.g., the viewing user, request,
    * or other context. For example, this is used to show workspace IDs when
    * configuring `asana.workspace-id`.
    *
    * @param   PhabricatorConfigOption   Option being rendered.
    * @param   AphrontRequest            Active request.
    * @return  wild                      Additional contextual description
    *                                    information.
    */
   public function renderContextualDescription(
     PhabricatorConfigOption $option,
     AphrontRequest $request) {
     return null;
   }
 
   public function getKey() {
     $class = get_class($this);
     $matches = null;
     if (preg_match('/^Phabricator(.*)ConfigOptions$/', $class, $matches)) {
       return strtolower($matches[1]);
     }
     return strtolower(get_class($this));
   }
 
   final protected function newOption($key, $type, $default) {
     return id(new PhabricatorConfigOption())
       ->setKey($key)
       ->setType($type)
       ->setDefault($default)
       ->setGroup($this);
   }
 
   final public static function loadAll($external_only = false) {
     $symbols = id(new PhutilSymbolLoader())
-      ->setAncestorClass('PhabricatorApplicationConfigOptions')
+      ->setAncestorClass(__CLASS__)
       ->setConcreteOnly(true)
       ->selectAndLoadSymbols();
 
     $groups = array();
     foreach ($symbols as $symbol) {
       if ($external_only && $symbol['library'] == 'phabricator') {
         continue;
       }
 
       $obj = newv($symbol['name'], array());
       $key = $obj->getKey();
       if (isset($groups[$key])) {
         $pclass = get_class($groups[$key]);
         $nclass = $symbol['name'];
 
         throw new Exception(
-          "Multiple PhabricatorApplicationConfigOptions subclasses have the ".
-          "same key ('{$key}'): {$pclass}, {$nclass}.");
+          pht(
+            "Multiple %s subclasses have the same key ('%s'): %s, %s.",
+            __CLASS__,
+            $key,
+            $pclass,
+            $nclass));
       }
       $groups[$key] = $obj;
     }
 
     return $groups;
   }
 
   final public static function loadAllOptions($external_only = false) {
     $groups = self::loadAll($external_only);
 
     $options = array();
     foreach ($groups as $group) {
       foreach ($group->getOptions() as $option) {
         $key = $option->getKey();
         if (isset($options[$key])) {
           throw new Exception(
-            "Mulitple PhabricatorApplicationConfigOptions subclasses contain ".
-            "an option named '{$key}'!");
+            pht(
+              "Mulitple % subclasses contain an option named '%s'!",
+              __CLASS__,
+              $key));
         }
         $options[$key] = $option;
       }
     }
 
     return $options;
   }
 
   /**
    * Deformat a HEREDOC for use in remarkup by converting line breaks to
    * spaces.
    */
   final protected function deformat($string) {
     return preg_replace('/(?<=\S)\n(?=\S)/', ' ', $string);
   }
 
 }
diff --git a/src/applications/config/option/PhabricatorConfigOption.php b/src/applications/config/option/PhabricatorConfigOption.php
index f11c436402..e5c9773611 100644
--- a/src/applications/config/option/PhabricatorConfigOption.php
+++ b/src/applications/config/option/PhabricatorConfigOption.php
@@ -1,239 +1,238 @@
 <?php
 
 final class PhabricatorConfigOption
   extends Phobject
   implements PhabricatorMarkupInterface {
 
   private $key;
   private $default;
   private $summary;
   private $description;
   private $type;
   private $boolOptions;
   private $enumOptions;
   private $group;
   private $examples;
   private $locked;
   private $lockedMessage;
   private $hidden;
   private $baseClass;
   private $customData;
   private $customObject;
 
   public function setBaseClass($base_class) {
     $this->baseClass = $base_class;
     return $this;
   }
 
   public function getBaseClass() {
     return $this->baseClass;
   }
 
   public function setHidden($hidden) {
     $this->hidden = $hidden;
     return $this;
   }
 
   public function getHidden() {
     if ($this->hidden) {
       return true;
     }
 
     return idx(
       PhabricatorEnv::getEnvConfig('config.hide'),
       $this->getKey(),
       false);
   }
 
   public function setLocked($locked) {
     $this->locked = $locked;
     return $this;
   }
 
   public function getLocked() {
     if ($this->locked) {
       return true;
     }
 
     if ($this->getHidden()) {
       return true;
     }
 
     return idx(
       PhabricatorEnv::getEnvConfig('config.lock'),
       $this->getKey(),
       false);
   }
 
   public function setLockedMessage($message) {
     $this->lockedMessage = $message;
     return $this;
   }
 
   public function getLockedMessage() {
     if ($this->lockedMessage !== null) {
       return $this->lockedMessage;
     }
     return pht(
       'This configuration is locked and can not be edited from the web '.
       'interface. Use `./bin/config` in `phabricator/` to edit it.');
   }
 
   public function addExample($value, $description) {
     $this->examples[] = array($value, $description);
     return $this;
   }
 
   public function getExamples() {
     return $this->examples;
   }
 
   public function setGroup(PhabricatorApplicationConfigOptions $group) {
     $this->group = $group;
     return $this;
   }
 
   public function getGroup() {
     return $this->group;
   }
 
   public function setBoolOptions(array $options) {
     $this->boolOptions = $options;
     return $this;
   }
 
   public function getBoolOptions() {
     if ($this->boolOptions) {
       return $this->boolOptions;
     }
     return array(
       pht('True'),
       pht('False'),
     );
   }
 
   public function setEnumOptions(array $options) {
     $this->enumOptions = $options;
     return $this;
   }
 
   public function getEnumOptions() {
     if ($this->enumOptions) {
       return $this->enumOptions;
     }
 
-    throw new Exception(
-      'Call setEnumOptions() before trying to access them!');
+    throw new PhutilInvalidStateException('setEnumOptions');
   }
 
   public function setKey($key) {
     $this->key = $key;
     return $this;
   }
 
   public function getKey() {
     return $this->key;
   }
 
   public function setDefault($default) {
     $this->default = $default;
     return $this;
   }
 
   public function getDefault() {
     return $this->default;
   }
 
   public function setSummary($summary) {
     $this->summary = $summary;
     return $this;
   }
 
   public function getSummary() {
     if (empty($this->summary)) {
       return $this->getDescription();
     }
     return $this->summary;
   }
 
   public function setDescription($description) {
     $this->description = $description;
     return $this;
   }
 
   public function getDescription() {
     return $this->description;
   }
 
   public function setType($type) {
     $this->type = $type;
     return $this;
   }
 
   public function getType() {
     return $this->type;
   }
 
   public function isCustomType() {
     return !strncmp($this->getType(), 'custom:', 7);
   }
 
   public function getCustomObject() {
     if (!$this->customObject) {
       if (!$this->isCustomType()) {
         throw new Exception('This option does not have a custom type!');
       }
       $this->customObject = newv(substr($this->getType(), 7), array());
     }
     return $this->customObject;
   }
 
   public function getCustomData() {
     return $this->customData;
   }
 
   public function setCustomData($data) {
     $this->customData = $data;
     return $this;
   }
 
 /* -(  PhabricatorMarkupInterface  )----------------------------------------- */
 
   public function getMarkupFieldKey($field) {
     return $this->getKey().':'.$field;
   }
 
   public function newMarkupEngine($field) {
     return PhabricatorMarkupEngine::newMarkupEngine(array());
   }
 
   public function getMarkupText($field) {
     switch ($field) {
       case 'description':
         $text = $this->getDescription();
         break;
       case 'summary':
         $text = $this->getSummary();
         break;
     }
 
     // TODO: We should probably implement this as a real Markup rule, but
     // markup rules are a bit of a mess right now and it doesn't hurt us to
     // fake this.
     $text = preg_replace(
       '/{{([^}]+)}}/',
       '[[/config/edit/\\1/ | \\1]]',
       $text);
 
     return $text;
   }
 
   public function didMarkupText($field, $output, PhutilMarkupEngine $engine) {
     return $output;
   }
 
   public function shouldUseMarkupCache($field) {
     return false;
   }
 
 }
diff --git a/src/applications/config/schema/PhabricatorConfigSchemaQuery.php b/src/applications/config/schema/PhabricatorConfigSchemaQuery.php
index 7a11d64684..57db889956 100644
--- a/src/applications/config/schema/PhabricatorConfigSchemaQuery.php
+++ b/src/applications/config/schema/PhabricatorConfigSchemaQuery.php
@@ -1,287 +1,287 @@
 <?php
 
 final class PhabricatorConfigSchemaQuery extends Phobject {
 
   private $api;
 
   public function setAPI(PhabricatorStorageManagementAPI $api) {
     $this->api = $api;
     return $this;
   }
 
   protected function getAPI() {
     if (!$this->api) {
-      throw new Exception(pht('Call setAPI() before issuing a query!'));
+      throw new PhutilInvalidStateException('setAPI');
     }
     return $this->api;
   }
 
   protected function getConn() {
     return $this->getAPI()->getConn(null);
   }
 
   private function getDatabaseNames() {
     $api = $this->getAPI();
     $patches = PhabricatorSQLPatchList::buildAllPatches();
     return $api->getDatabaseList(
       $patches,
       $only_living = true);
   }
 
   public function loadActualSchema() {
     $databases = $this->getDatabaseNames();
 
     $conn = $this->getConn();
     $tables = queryfx_all(
       $conn,
       'SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_COLLATION
         FROM INFORMATION_SCHEMA.TABLES
         WHERE TABLE_SCHEMA IN (%Ls)',
       $databases);
 
     $database_info = queryfx_all(
       $conn,
       'SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME
         FROM INFORMATION_SCHEMA.SCHEMATA
         WHERE SCHEMA_NAME IN (%Ls)',
       $databases);
     $database_info = ipull($database_info, null, 'SCHEMA_NAME');
 
     $sql = array();
     foreach ($tables as $table) {
       $sql[] = qsprintf(
         $conn,
         '(TABLE_SCHEMA = %s AND TABLE_NAME = %s)',
         $table['TABLE_SCHEMA'],
         $table['TABLE_NAME']);
     }
 
     if ($sql) {
       $column_info = queryfx_all(
         $conn,
         'SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, CHARACTER_SET_NAME,
             COLLATION_NAME, COLUMN_TYPE, IS_NULLABLE, EXTRA
           FROM INFORMATION_SCHEMA.COLUMNS
           WHERE (%Q)',
         '('.implode(') OR (', $sql).')');
       $column_info = igroup($column_info, 'TABLE_SCHEMA');
     } else {
       $column_info = array();
     }
 
     // NOTE: Tables like KEY_COLUMN_USAGE and TABLE_CONSTRAINTS only contain
     // primary, unique, and foreign keys, so we can't use them here. We pull
     // indexes later on using SHOW INDEXES.
 
     $server_schema = new PhabricatorConfigServerSchema();
 
     $tables = igroup($tables, 'TABLE_SCHEMA');
     foreach ($tables as $database_name => $database_tables) {
       $info = $database_info[$database_name];
 
       $database_schema = id(new PhabricatorConfigDatabaseSchema())
         ->setName($database_name)
         ->setCharacterSet($info['DEFAULT_CHARACTER_SET_NAME'])
         ->setCollation($info['DEFAULT_COLLATION_NAME']);
 
       $database_column_info = idx($column_info, $database_name, array());
       $database_column_info = igroup($database_column_info, 'TABLE_NAME');
 
       foreach ($database_tables as $table) {
         $table_name = $table['TABLE_NAME'];
 
         $table_schema = id(new PhabricatorConfigTableSchema())
           ->setName($table_name)
           ->setCollation($table['TABLE_COLLATION']);
 
         $columns = idx($database_column_info, $table_name, array());
         foreach ($columns as $column) {
           if (strpos($column['EXTRA'], 'auto_increment') === false) {
             $auto_increment = false;
           } else {
             $auto_increment = true;
           }
 
           $column_schema = id(new PhabricatorConfigColumnSchema())
             ->setName($column['COLUMN_NAME'])
             ->setCharacterSet($column['CHARACTER_SET_NAME'])
             ->setCollation($column['COLLATION_NAME'])
             ->setColumnType($column['COLUMN_TYPE'])
             ->setNullable($column['IS_NULLABLE'] == 'YES')
             ->setAutoIncrement($auto_increment);
 
           $table_schema->addColumn($column_schema);
         }
 
         $key_parts = queryfx_all(
           $conn,
           'SHOW INDEXES FROM %T.%T',
           $database_name,
           $table_name);
         $keys = igroup($key_parts, 'Key_name');
         foreach ($keys as $key_name => $key_pieces) {
           $key_pieces = isort($key_pieces, 'Seq_in_index');
           $head = head($key_pieces);
 
           // This handles string indexes which index only a prefix of a field.
           $column_names = array();
           foreach ($key_pieces as $piece) {
             $name = $piece['Column_name'];
             if ($piece['Sub_part']) {
               $name = $name.'('.$piece['Sub_part'].')';
             }
             $column_names[] = $name;
           }
 
           $key_schema = id(new PhabricatorConfigKeySchema())
             ->setName($key_name)
             ->setColumnNames($column_names)
             ->setUnique(!$head['Non_unique'])
             ->setIndexType($head['Index_type']);
 
           $table_schema->addKey($key_schema);
         }
 
         $database_schema->addTable($table_schema);
       }
 
       $server_schema->addDatabase($database_schema);
     }
 
     return $server_schema;
   }
 
   public function loadExpectedSchema() {
     $databases = $this->getDatabaseNames();
     $info = $this->getAPI()->getCharsetInfo();
 
     $specs = id(new PhutilSymbolLoader())
       ->setAncestorClass('PhabricatorConfigSchemaSpec')
       ->loadObjects();
 
     $server_schema = new PhabricatorConfigServerSchema();
     foreach ($specs as $spec) {
       $spec
         ->setUTF8Charset(
           $info[PhabricatorStorageManagementAPI::CHARSET_DEFAULT])
         ->setUTF8BinaryCollation(
           $info[PhabricatorStorageManagementAPI::COLLATE_TEXT])
         ->setUTF8SortingCollation(
           $info[PhabricatorStorageManagementAPI::COLLATE_SORT])
         ->setServer($server_schema)
         ->buildSchemata($server_schema);
     }
 
     return $server_schema;
   }
 
   public function buildComparisonSchema(
     PhabricatorConfigServerSchema $expect,
     PhabricatorConfigServerSchema $actual) {
 
     $comp_server = $actual->newEmptyClone();
 
     $all_databases = $actual->getDatabases() + $expect->getDatabases();
     foreach ($all_databases as $database_name => $database_template) {
       $actual_database = $actual->getDatabase($database_name);
       $expect_database = $expect->getDatabase($database_name);
 
       $issues = $this->compareSchemata($expect_database, $actual_database);
 
       $comp_database = $database_template->newEmptyClone()
         ->setIssues($issues);
 
       if (!$actual_database) {
         $actual_database = $expect_database->newEmptyClone();
       }
       if (!$expect_database) {
         $expect_database = $actual_database->newEmptyClone();
       }
 
       $all_tables =
         $actual_database->getTables() +
         $expect_database->getTables();
       foreach ($all_tables as $table_name => $table_template) {
         $actual_table = $actual_database->getTable($table_name);
         $expect_table = $expect_database->getTable($table_name);
 
         $issues = $this->compareSchemata($expect_table, $actual_table);
 
         $comp_table = $table_template->newEmptyClone()
           ->setIssues($issues);
 
         if (!$actual_table) {
           $actual_table = $expect_table->newEmptyClone();
         }
         if (!$expect_table) {
           $expect_table = $actual_table->newEmptyClone();
         }
 
         $all_columns =
           $actual_table->getColumns() +
           $expect_table->getColumns();
         foreach ($all_columns as $column_name => $column_template) {
           $actual_column = $actual_table->getColumn($column_name);
           $expect_column = $expect_table->getColumn($column_name);
 
           $issues = $this->compareSchemata($expect_column, $actual_column);
 
           $comp_column = $column_template->newEmptyClone()
             ->setIssues($issues);
 
           $comp_table->addColumn($comp_column);
         }
 
         $all_keys =
           $actual_table->getKeys() +
           $expect_table->getKeys();
         foreach ($all_keys as $key_name => $key_template) {
           $actual_key = $actual_table->getKey($key_name);
           $expect_key = $expect_table->getKey($key_name);
 
           $issues = $this->compareSchemata($expect_key, $actual_key);
 
           $comp_key = $key_template->newEmptyClone()
             ->setIssues($issues);
 
           $comp_table->addKey($comp_key);
         }
 
         $comp_database->addTable($comp_table);
       }
       $comp_server->addDatabase($comp_database);
     }
 
     return $comp_server;
   }
 
   private function compareSchemata(
     PhabricatorConfigStorageSchema $expect = null,
     PhabricatorConfigStorageSchema $actual = null) {
 
     $expect_is_key = ($expect instanceof PhabricatorConfigKeySchema);
     $actual_is_key = ($actual instanceof PhabricatorConfigKeySchema);
 
     if ($expect_is_key || $actual_is_key) {
       $missing_issue = PhabricatorConfigStorageSchema::ISSUE_MISSINGKEY;
       $surplus_issue = PhabricatorConfigStorageSchema::ISSUE_SURPLUSKEY;
     } else {
       $missing_issue = PhabricatorConfigStorageSchema::ISSUE_MISSING;
       $surplus_issue = PhabricatorConfigStorageSchema::ISSUE_SURPLUS;
     }
 
     if (!$expect && !$actual) {
       throw new Exception(pht('Can not compare two missing schemata!'));
     } else if ($expect && !$actual) {
       $issues = array($missing_issue);
     } else if ($actual && !$expect) {
       $issues = array($surplus_issue);
     } else {
       $issues = $actual->compareTo($expect);
     }
 
     return $issues;
   }
 
 
 }
diff --git a/src/applications/conpherence/ConpherenceTransactionRenderer.php b/src/applications/conpherence/ConpherenceTransactionRenderer.php
index d6be83fb65..1b14f5f0d6 100644
--- a/src/applications/conpherence/ConpherenceTransactionRenderer.php
+++ b/src/applications/conpherence/ConpherenceTransactionRenderer.php
@@ -1,172 +1,191 @@
 <?php
 
 final class ConpherenceTransactionRenderer {
 
   public static function renderTransactions(
     PhabricatorUser $user,
     ConpherenceThread $conpherence,
     $full_display = true,
     $marker_type = 'older') {
 
     $transactions = $conpherence->getTransactions();
 
     $oldest_transaction_id = 0;
     $newest_transaction_id = 0;
     $too_many = ConpherenceThreadQuery::TRANSACTION_LIMIT + 1;
     if (count($transactions) == $too_many) {
       if ($marker_type == 'olderandnewer') {
         $last_transaction = end($transactions);
         $first_transaction = reset($transactions);
         unset($transactions[$last_transaction->getID()]);
         unset($transactions[$first_transaction->getID()]);
         $oldest_transaction_id = $last_transaction->getID();
         $newest_transaction_id = $first_transaction->getID();
       } else if ($marker_type == 'newer') {
         $first_transaction = reset($transactions);
         unset($transactions[$first_transaction->getID()]);
         $newest_transaction_id = $first_transaction->getID();
       } else if ($marker_type == 'older') {
         $last_transaction = end($transactions);
         unset($transactions[$last_transaction->getID()]);
         $oldest_transaction = end($transactions);
         $oldest_transaction_id = $oldest_transaction->getID();
       }
     // we need **at least** the newer marker in this mode even if
     // we didn't get a full set of transactions
     } else if ($marker_type == 'olderandnewer') {
       $first_transaction = reset($transactions);
       unset($transactions[$first_transaction->getID()]);
       $newest_transaction_id = $first_transaction->getID();
     }
 
     $transactions = array_reverse($transactions);
     $handles = $conpherence->getHandles();
     $rendered_transactions = array();
     $engine = id(new PhabricatorMarkupEngine())
       ->setViewer($user)
       ->setContextObject($conpherence);
     foreach ($transactions as $key => $transaction) {
       if ($transaction->shouldHide()) {
         unset($transactions[$key]);
         continue;
       }
       if ($transaction->getComment()) {
         $engine->addObject(
           $transaction->getComment(),
           PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT);
       }
     }
     $engine->process();
     // we're going to insert a dummy date marker transaction for breaks
     // between days. some setup required!
     $previous_transaction = null;
     $date_marker_transaction = id(new ConpherenceTransaction())
       ->setTransactionType(ConpherenceTransactionType::TYPE_DATE_MARKER)
       ->makeEphemeral();
     $date_marker_transaction_view = id(new ConpherenceTransactionView())
       ->setUser($user)
       ->setConpherenceTransaction($date_marker_transaction)
       ->setConpherenceThread($conpherence)
       ->setHandles($handles)
       ->setMarkupEngine($engine);
 
     $transaction_view_template = id(new ConpherenceTransactionView())
       ->setUser($user)
       ->setConpherenceThread($conpherence)
       ->setHandles($handles)
       ->setMarkupEngine($engine);
 
     foreach ($transactions as $transaction) {
       if ($previous_transaction) {
         $previous_day = phabricator_format_local_time(
           $previous_transaction->getDateCreated(),
           $user,
           'Ymd');
         $current_day = phabricator_format_local_time(
           $transaction->getDateCreated(),
           $user,
           'Ymd');
         // date marker transaction time!
         if ($previous_day != $current_day) {
           $date_marker_transaction->setDateCreated(
             $transaction->getDateCreated());
+          $date_marker_transaction->setID($previous_transaction->getID());
           $rendered_transactions[] = $date_marker_transaction_view->render();
         }
       }
       $transaction_view = id(clone $transaction_view_template)
         ->setConpherenceTransaction($transaction);
       if ($full_display) {
         $transaction_view
           ->setAnchor(
             $transaction->getID(),
             phabricator_time($transaction->getDateCreated(), $user));
         $transaction_view->setContentSource($transaction->getContentSource());
         $transaction_view->setShowImages(true);
       } else {
         $transaction_view
           ->setEpoch(
             $transaction->getDateCreated(),
             '/'.$conpherence->getMonogram().'#'.$transaction->getID())
             ->setTimeOnly(true);
         $transaction_view->setShowImages(false);
       }
 
       $rendered_transactions[] = $transaction_view->render();
       $previous_transaction = $transaction;
     }
     $latest_transaction_id = $transaction->getID();
 
     return array(
       'transactions' => $rendered_transactions,
       'latest_transaction' => $transaction,
       'latest_transaction_id' => $latest_transaction_id,
       'oldest_transaction_id' => $oldest_transaction_id,
       'newest_transaction_id' => $newest_transaction_id,
     );
   }
 
   public static function renderMessagePaneContent(
     array $transactions,
     $oldest_transaction_id,
     $newest_transaction_id) {
 
     $oldscrollbutton = '';
     if ($oldest_transaction_id) {
       $oldscrollbutton = javelin_tag(
         'a',
         array(
           'href' => '#',
           'mustcapture' => true,
           'sigil' => 'show-older-messages',
           'class' => 'conpherence-show-more-messages',
           'meta' => array(
             'oldest_transaction_id' => $oldest_transaction_id,
           ),
         ),
         pht('Show Older Messages'));
+      $oldscrollbutton = javelin_tag(
+        'div',
+        array(
+          'sigil' => 'conpherence-transaction-view',
+          'meta' => array(
+            'id' => $oldest_transaction_id - 0.5,
+          ),
+        ),
+        $oldscrollbutton);
     }
 
     $newscrollbutton = '';
     if ($newest_transaction_id) {
       $newscrollbutton = javelin_tag(
         'a',
         array(
           'href' => '#',
           'mustcapture' => true,
           'sigil' => 'show-newer-messages',
           'class' => 'conpherence-show-more-messages',
           'meta' => array(
             'newest_transaction_id' => $newest_transaction_id,
           ),
         ),
         pht('Show Newer Messages'));
+      $newscrollbutton = javelin_tag(
+        'div',
+        array(
+          'sigil' => 'conpherence-transaction-view',
+          'meta' => array(
+            'id' => $newest_transaction_id + 0.5,
+          ),
+        ),
+        $newscrollbutton);
     }
 
     return hsprintf(
       '%s%s%s',
       $oldscrollbutton,
       $transactions,
       $newscrollbutton);
   }
 
 }
diff --git a/src/applications/conpherence/controller/ConpherenceColumnViewController.php b/src/applications/conpherence/controller/ConpherenceColumnViewController.php
index 0155784015..9a146b4b23 100644
--- a/src/applications/conpherence/controller/ConpherenceColumnViewController.php
+++ b/src/applications/conpherence/controller/ConpherenceColumnViewController.php
@@ -1,102 +1,109 @@
 <?php
 
 final class ConpherenceColumnViewController extends
   ConpherenceController {
 
   public function handleRequest(AphrontRequest $request) {
     $user = $request->getUser();
 
     $latest_conpherences = array();
     $latest_participant = id(new ConpherenceParticipantQuery())
       ->withParticipantPHIDs(array($user->getPHID()))
       ->setLimit(6)
       ->execute();
     if ($latest_participant) {
       $conpherence_phids = mpull($latest_participant, 'getConpherencePHID');
       $latest_conpherences = id(new ConpherenceThreadQuery())
         ->setViewer($user)
         ->withPHIDs($conpherence_phids)
         ->needCropPics(true)
         ->needParticipantCache(true)
         ->execute();
       $latest_conpherences = mpull($latest_conpherences, null, 'getPHID');
       $latest_conpherences = array_select_keys(
         $latest_conpherences,
         $conpherence_phids);
     }
 
     $conpherence = null;
     $should_404 = false;
     if ($request->getInt('id')) {
       $conpherence = id(new ConpherenceThreadQuery())
         ->setViewer($user)
         ->withIDs(array($request->getInt('id')))
         ->needCropPics(true)
         ->needTransactions(true)
         ->setTransactionLimit(ConpherenceThreadQuery::TRANSACTION_LIMIT)
         ->executeOne();
       $should_404 = true;
     } else if ($latest_participant) {
       $participant = head($latest_participant);
       $conpherence = id(new ConpherenceThreadQuery())
         ->setViewer($user)
         ->withPHIDs(array($participant->getConpherencePHID()))
         ->needCropPics(true)
         ->needTransactions(true)
         ->setTransactionLimit(ConpherenceThreadQuery::TRANSACTION_LIMIT)
         ->executeOne();
       $should_404 = true;
     }
 
     $durable_column = id(new ConpherenceDurableColumnView())
       ->setUser($user)
       ->setVisible(true);
     if (!$conpherence) {
       if ($should_404) {
         return new Aphront404Response();
       }
 
       $conpherence_id = null;
       $conpherence_phid = null;
       $latest_transaction_id = null;
       $can_edit = false;
 
     } else {
       $this->setConpherence($conpherence);
 
       $participant = $conpherence->getParticipant($user->getPHID());
       $transactions = $conpherence->getTransactions();
       $latest_transaction = head($transactions);
       $write_guard = AphrontWriteGuard::beginScopedUnguardedWrites();
       $participant->markUpToDate($conpherence, $latest_transaction);
       unset($write_guard);
 
       $draft = PhabricatorDraft::newFromUserAndKey(
         $user,
         $conpherence->getPHID());
 
       $durable_column
         ->setDraft($draft)
         ->setSelectedConpherence($conpherence)
         ->setConpherences($latest_conpherences);
       $conpherence_id = $conpherence->getID();
       $conpherence_phid = $conpherence->getPHID();
       $latest_transaction_id = $latest_transaction->getID();
       $can_edit = PhabricatorPolicyFilter::hasCapability(
         $user,
         $conpherence,
         PhabricatorPolicyCapability::CAN_EDIT);
     }
 
+    $dropdown_query = id(new AphlictDropdownDataQuery())
+      ->setViewer($user);
+    $dropdown_query->execute();
     $response = array(
       'content' => hsprintf('%s', $durable_column),
       'threadID' => $conpherence_id,
       'threadPHID' => $conpherence_phid,
       'latestTransactionID' => $latest_transaction_id,
       'canEdit' => $can_edit,
+      'aphlictDropdownData' => array(
+        $dropdown_query->getNotificationData(),
+        $dropdown_query->getConpherenceData(),
+      ),
     );
 
     return id(new AphrontAjaxResponse())->setContent($response);
   }
 
 }
diff --git a/src/applications/conpherence/controller/ConpherenceListController.php b/src/applications/conpherence/controller/ConpherenceListController.php
index f8be8f7de6..ad672ff6d8 100644
--- a/src/applications/conpherence/controller/ConpherenceListController.php
+++ b/src/applications/conpherence/controller/ConpherenceListController.php
@@ -1,152 +1,156 @@
 <?php
 
 final class ConpherenceListController extends ConpherenceController {
 
   const SELECTED_MODE = 'selected';
   const UNSELECTED_MODE = 'unselected';
 
   /**
    * Two main modes of operation...
    *
    * 1 - /conpherence/ - UNSELECTED_MODE
    * 2 - /conpherence/<id>/ - SELECTED_MODE
    *
    * UNSELECTED_MODE is not an Ajax request while the other two are Ajax
    * requests.
    */
   private function determineMode() {
     $request = $this->getRequest();
 
     $mode = self::UNSELECTED_MODE;
     if ($request->isAjax()) {
       $mode = self::SELECTED_MODE;
     }
 
     return $mode;
   }
 
+  public function shouldAllowPublic() {
+    return true;
+  }
+
   public function handleRequest(AphrontRequest $request) {
     $user = $request->getUser();
     $title = pht('Conpherence');
     $conpherence = null;
 
     $limit = ConpherenceThreadListView::SEE_MORE_LIMIT * 5;
     $all_participation = array();
 
     $mode = $this->determineMode();
     switch ($mode) {
       case self::SELECTED_MODE:
         $conpherence_id = $request->getURIData('id');
         $conpherence = id(new ConpherenceThreadQuery())
           ->setViewer($user)
           ->withIDs(array($conpherence_id))
           ->executeOne();
         if (!$conpherence) {
           return new Aphront404Response();
         }
         if ($conpherence->getTitle()) {
           $title = $conpherence->getTitle();
         }
         $cursor = $conpherence->getParticipantIfExists($user->getPHID());
         $data = $this->loadDefaultParticipation($limit);
         $all_participation = $data['all_participation'];
         if (!$cursor) {
           $menu_participation = id(new ConpherenceParticipant())
             ->makeEphemeral()
             ->setConpherencePHID($conpherence->getPHID())
             ->setParticipantPHID($user->getPHID());
         } else {
           $menu_participation = $cursor;
         }
         $all_participation =
           array($conpherence->getPHID() => $menu_participation) +
           $all_participation;
         break;
       case self::UNSELECTED_MODE:
       default:
         $data = $this->loadDefaultParticipation($limit);
         $all_participation = $data['all_participation'];
         break;
     }
 
     $threads = $this->loadConpherenceThreadData(
       $all_participation);
 
     $thread_view = id(new ConpherenceThreadListView())
       ->setUser($user)
       ->setBaseURI($this->getApplicationURI())
       ->setThreads($threads);
 
     switch ($mode) {
       case self::SELECTED_MODE:
         $response = id(new AphrontAjaxResponse())->setContent($thread_view);
         break;
       case self::UNSELECTED_MODE:
       default:
         $layout = id(new ConpherenceLayoutView())
           ->setUser($user)
           ->setBaseURI($this->getApplicationURI())
           ->setThreadView($thread_view)
           ->setRole('list');
         if ($conpherence) {
           $policy_objects = id(new PhabricatorPolicyQuery())
             ->setViewer($user)
             ->setObject($conpherence)
             ->execute();
           $layout->setHeader($this->buildHeaderPaneContent(
             $conpherence,
             $policy_objects));
           $layout->setThread($conpherence);
         } else {
           $thread = ConpherenceThread::initializeNewThread($user);
           $thread->attachHandles(array());
           $thread->attachTransactions(array());
           $thread->makeEphemeral();
           $layout->setHeader(
             $this->buildHeaderPaneContent($thread, array()));
         }
         $response = $this->buildApplicationPage(
           $layout,
           array(
             'title' => $title,
           ));
         break;
     }
 
     return $response;
 
   }
 
   private function loadDefaultParticipation($limit) {
     $viewer = $this->getRequest()->getUser();
 
     $all_participation = id(new ConpherenceParticipantQuery())
       ->withParticipantPHIDs(array($viewer->getPHID()))
       ->setLimit($limit)
       ->execute();
 
     return array(
       'all_participation' => $all_participation,
     );
   }
 
   private function loadConpherenceThreadData($participation) {
     $user = $this->getRequest()->getUser();
     $conpherence_phids = array_keys($participation);
     $conpherences = array();
     if ($conpherence_phids) {
       $conpherences = id(new ConpherenceThreadQuery())
         ->setViewer($user)
         ->withPHIDs($conpherence_phids)
         ->needCropPics(true)
         ->needParticipantCache(true)
         ->execute();
 
       // this will re-sort by participation data
       $conpherences = array_select_keys($conpherences, $conpherence_phids);
     }
 
     return $conpherences;
   }
 
 }
diff --git a/src/applications/conpherence/controller/ConpherenceNewController.php b/src/applications/conpherence/controller/ConpherenceNewController.php
index a887404c7d..102cf0e5ea 100644
--- a/src/applications/conpherence/controller/ConpherenceNewController.php
+++ b/src/applications/conpherence/controller/ConpherenceNewController.php
@@ -1,91 +1,90 @@
 <?php
 
 final class ConpherenceNewController extends ConpherenceController {
 
   public function handleRequest(AphrontRequest $request) {
     $user = $request->getUser();
 
     $title = pht('New Message');
     $participants = array();
     $participant_prefill = null;
     $message = '';
     $e_participants = null;
     $e_message = null;
     $errors = array();
 
     // this comes from ajax requests from all over. should be a single phid.
 
     if ($request->isFormPost()) {
       $participants = $request->getArr('participants');
       $message = $request->getStr('message');
       list($error_codes, $conpherence) = ConpherenceEditor::createThread(
         $user,
         $participants,
         $conpherence_title = null,
         $message,
         PhabricatorContentSource::newFromRequest($request));
 
       if ($error_codes) {
         foreach ($error_codes as $error_code) {
           switch ($error_code) {
             case ConpherenceEditor::ERROR_EMPTY_MESSAGE:
               $e_message = pht('Required');
               $errors[] = pht(
                 'You can not send an empty message.');
               break;
             case ConpherenceEditor::ERROR_EMPTY_PARTICIPANTS:
               $e_participants = pht('Required');
               $errors[] = pht(
                 'You must choose at least one recipient for your '.
                 'message.');
               break;
           }
         }
       } else {
-        $uri = $this->getApplicationURI($conpherence->getID());
         return id(new AphrontRedirectResponse())
-          ->setURI($uri);
+          ->setURI('/'.$conpherence->getMonogram());
       }
     } else {
       $participant_prefill = $request->getStr('participant');
       if ($participant_prefill) {
         $participants[] = $participant_prefill;
       }
     }
 
     $submit_uri = $this->getApplicationURI('new/');
     $cancel_uri = $this->getApplicationURI();
 
     $dialog = id(new AphrontDialogView())
       ->setWidth(AphrontDialogView::WIDTH_FORM)
       ->setErrors($errors)
       ->setUser($user)
       ->setTitle($title)
       ->addCancelButton($cancel_uri)
       ->addSubmitButton(pht('Send Message'));
 
     $form = id(new AphrontFormView())
       ->setUser($user)
       ->setFullWidth(true)
       ->appendControl(
         id(new AphrontFormTokenizerControl())
           ->setName('participants')
           ->setValue($participants)
           ->setUser($user)
           ->setDatasource(new PhabricatorPeopleDatasource())
           ->setLabel(pht('To'))
           ->setError($e_participants))
       ->appendChild(
         id(new PhabricatorRemarkupControl())
           ->setUser($user)
           ->setName('message')
           ->setValue($message)
           ->setLabel(pht('Message'))
           ->setError($e_message));
 
     $dialog->appendForm($form);
 
     return id(new AphrontDialogResponse())->setDialog($dialog);
   }
 
 }
diff --git a/src/applications/conpherence/controller/ConpherenceNewRoomController.php b/src/applications/conpherence/controller/ConpherenceNewRoomController.php
index ce9afcb195..a401da0a19 100644
--- a/src/applications/conpherence/controller/ConpherenceNewRoomController.php
+++ b/src/applications/conpherence/controller/ConpherenceNewRoomController.php
@@ -1,102 +1,101 @@
 <?php
 
 final class ConpherenceNewRoomController extends ConpherenceController {
 
   public function handleRequest(AphrontRequest $request) {
     $user = $request->getUser();
 
     $title = pht('New Room');
     $e_title = true;
     $validation_exception = null;
 
     $conpherence = ConpherenceThread::initializeNewRoom($user);
     if ($request->isFormPost()) {
 
       $xactions = array();
       $xactions[] = id(new ConpherenceTransaction())
         ->setTransactionType(ConpherenceTransactionType::TYPE_PARTICIPANTS)
         ->setNewValue(array('+' => array($user->getPHID())));
       $xactions[] = id(new ConpherenceTransaction())
         ->setTransactionType(ConpherenceTransactionType::TYPE_TITLE)
         ->setNewValue($request->getStr('title'));
       $xactions[] = id(new ConpherenceTransaction())
         ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
         ->setNewValue($request->getStr('viewPolicy'));
       $xactions[] = id(new ConpherenceTransaction())
         ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY)
         ->setNewValue($request->getStr('editPolicy'));
       $xactions[] = id(new ConpherenceTransaction())
         ->setTransactionType(PhabricatorTransactions::TYPE_JOIN_POLICY)
         ->setNewValue($request->getStr('joinPolicy'));
 
       try {
         id(new ConpherenceEditor())
           ->setContentSourceFromRequest($request)
           ->setContinueOnNoEffect(true)
           ->setActor($user)
           ->applyTransactions($conpherence, $xactions);
 
-        $uri = $this->getApplicationURI($conpherence->getID());
         return id(new AphrontRedirectResponse())
-                    ->setURI($uri);
+          ->setURI('/'.$conpherence->getMonogram());
       } catch (PhabricatorApplicationTransactionValidationException $ex) {
         $validation_exception = $ex;
 
         $e_title = $ex->getShortMessage(ConpherenceTransactionType::TYPE_TITLE);
 
         $conpherence->setViewPolicy($request->getStr('viewPolicy'));
         $conpherence->setEditPolicy($request->getStr('editPolicy'));
         $conpherence->setJoinPolicy($request->getStr('joinPolicy'));
       }
     }
 
     $policies = id(new PhabricatorPolicyQuery())
       ->setViewer($user)
       ->setObject($conpherence)
       ->execute();
 
     $submit_uri = $this->getApplicationURI('room/new/');
     $cancel_uri = $this->getApplicationURI('search/');
 
     $dialog = $this->newDialog()
       ->setWidth(AphrontDialogView::WIDTH_FORM)
       ->setValidationException($validation_exception)
       ->setUser($user)
       ->setTitle($title)
       ->addCancelButton($cancel_uri)
       ->addSubmitButton(pht('Create Room'));
 
     $form = id(new PHUIFormLayoutView())
       ->setUser($user)
       ->setFullWidth(true)
       ->appendChild(
         id(new AphrontFormTextControl())
         ->setError($e_title)
         ->setLabel(pht('Title'))
         ->setName('title')
         ->setValue($request->getStr('title')))
       ->appendChild(
         id(new AphrontFormPolicyControl())
         ->setName('viewPolicy')
         ->setPolicyObject($conpherence)
         ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
         ->setPolicies($policies))
       ->appendChild(
         id(new AphrontFormPolicyControl())
         ->setName('editPolicy')
         ->setPolicyObject($conpherence)
         ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
         ->setPolicies($policies))
       ->appendChild(
         id(new AphrontFormPolicyControl())
         ->setName('joinPolicy')
         ->setPolicyObject($conpherence)
         ->setCapability(PhabricatorPolicyCapability::CAN_JOIN)
         ->setPolicies($policies));
 
     $dialog->appendChild($form);
 
     return id(new AphrontDialogResponse())->setDialog($dialog);
   }
 
 }
diff --git a/src/applications/conpherence/controller/ConpherenceRoomListController.php b/src/applications/conpherence/controller/ConpherenceRoomListController.php
index 299ca6b8d0..43c36d20d1 100644
--- a/src/applications/conpherence/controller/ConpherenceRoomListController.php
+++ b/src/applications/conpherence/controller/ConpherenceRoomListController.php
@@ -1,45 +1,49 @@
 <?php
 
 final class ConpherenceRoomListController extends ConpherenceController {
 
+  public function shouldAllowPublic() {
+    return true;
+  }
+
   public function handleRequest(AphrontRequest $request) {
     $user = $request->getUser();
 
     $controller = id(new PhabricatorApplicationSearchController())
       ->setQueryKey($request->getURIData('queryKey'))
       ->setSearchEngine(
         new ConpherenceThreadSearchEngine())
       ->setNavigation($this->buildRoomsSideNavView());
 
     return $this->delegateToController($controller);
   }
 
   protected function buildApplicationCrumbs() {
     return $this->buildConpherenceApplicationCrumbs($is_rooms = true);
   }
 
   public function buildApplicationMenu() {
     return $this->buildRoomsSideNavView(true)->getMenu();
   }
 
   private function buildRoomsSideNavView($for_app = false) {
     $user = $this->getRequest()->getUser();
 
     $nav = new AphrontSideNavFilterView();
     $nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
 
     if ($for_app) {
       $nav->addFilter('room/new/', pht('Create Room'));
     }
 
     id(new ConpherenceThreadSearchEngine())
       ->setViewer($user)
       ->addNavigationItems($nav->getMenu());
 
     $nav->selectFilter(null);
 
     return $nav;
   }
 
 
 }
diff --git a/src/applications/conpherence/controller/ConpherenceUpdateController.php b/src/applications/conpherence/controller/ConpherenceUpdateController.php
index 269b87f0e6..eea1d1dc8a 100644
--- a/src/applications/conpherence/controller/ConpherenceUpdateController.php
+++ b/src/applications/conpherence/controller/ConpherenceUpdateController.php
@@ -1,558 +1,569 @@
 <?php
 
 final class ConpherenceUpdateController
   extends ConpherenceController {
 
   public function handleRequest(AphrontRequest $request) {
     $user = $request->getUser();
     $conpherence_id = $request->getURIData('id');
     if (!$conpherence_id) {
       return new Aphront404Response();
     }
 
     $need_participants = false;
     $needed_capabilities = array(PhabricatorPolicyCapability::CAN_VIEW);
     $action = $request->getStr('action', ConpherenceUpdateActions::METADATA);
     switch ($action) {
       case ConpherenceUpdateActions::REMOVE_PERSON:
         $person_phid = $request->getStr('remove_person');
         if ($person_phid != $user->getPHID()) {
           $needed_capabilities[] = PhabricatorPolicyCapability::CAN_EDIT;
         }
         break;
       case ConpherenceUpdateActions::ADD_PERSON:
       case ConpherenceUpdateActions::METADATA:
         $needed_capabilities[] = PhabricatorPolicyCapability::CAN_EDIT;
         break;
       case ConpherenceUpdateActions::JOIN_ROOM:
         $needed_capabilities[] = PhabricatorPolicyCapability::CAN_JOIN;
         break;
       case ConpherenceUpdateActions::NOTIFICATIONS:
         $need_participants = true;
         break;
       case ConpherenceUpdateActions::LOAD:
         break;
     }
     $conpherence = id(new ConpherenceThreadQuery())
       ->setViewer($user)
       ->withIDs(array($conpherence_id))
       ->needFilePHIDs(true)
       ->needOrigPics(true)
       ->needCropPics(true)
       ->needParticipants($need_participants)
       ->requireCapabilities($needed_capabilities)
       ->executeOne();
 
     $latest_transaction_id = null;
     $response_mode = $request->isAjax() ? 'ajax' : 'redirect';
     $error_view = null;
     $e_file = array();
     $errors = array();
     $delete_draft = false;
     $xactions = array();
     if ($request->isFormPost() || ($action == ConpherenceUpdateActions::LOAD)) {
       $editor = id(new ConpherenceEditor())
         ->setContinueOnNoEffect($request->isContinueRequest())
         ->setContentSourceFromRequest($request)
         ->setActor($user);
 
       switch ($action) {
         case ConpherenceUpdateActions::DRAFT:
           $draft = PhabricatorDraft::newFromUserAndKey(
             $user,
             $conpherence->getPHID());
           $draft->setDraft($request->getStr('text'));
           $draft->replaceOrDelete();
           return new AphrontAjaxResponse();
         case ConpherenceUpdateActions::JOIN_ROOM:
           $xactions[] = id(new ConpherenceTransaction())
             ->setTransactionType(
               ConpherenceTransactionType::TYPE_PARTICIPANTS)
             ->setNewValue(array('+' => array($user->getPHID())));
           $delete_draft = true;
           $message = $request->getStr('text');
           if ($message) {
             $message_xactions = $editor->generateTransactionsFromText(
               $user,
               $conpherence,
               $message);
             $xactions = array_merge($xactions, $message_xactions);
           }
           // for now, just redirect back to the conpherence so everything
           // will work okay...!
           $response_mode = 'redirect';
           break;
         case ConpherenceUpdateActions::MESSAGE:
           $message = $request->getStr('text');
           if (strlen($message)) {
             $xactions = $editor->generateTransactionsFromText(
               $user,
               $conpherence,
               $message);
             $delete_draft = true;
           } else {
             $action = ConpherenceUpdateActions::LOAD;
             $updated = false;
             $response_mode = 'ajax';
           }
           break;
         case ConpherenceUpdateActions::ADD_PERSON:
           $person_phids = $request->getArr('add_person');
           if (!empty($person_phids)) {
             $xactions[] = id(new ConpherenceTransaction())
               ->setTransactionType(
                 ConpherenceTransactionType::TYPE_PARTICIPANTS)
               ->setNewValue(array('+' => $person_phids));
           }
           break;
         case ConpherenceUpdateActions::REMOVE_PERSON:
           if (!$request->isContinueRequest()) {
             // do nothing; we'll display a confirmation dialogue instead
             break;
           }
           $person_phid = $request->getStr('remove_person');
           if ($person_phid && $person_phid == $user->getPHID()) {
             $xactions[] = id(new ConpherenceTransaction())
               ->setTransactionType(
                 ConpherenceTransactionType::TYPE_PARTICIPANTS)
               ->setNewValue(array('-' => array($person_phid)));
             $response_mode = 'go-home';
           }
           break;
         case ConpherenceUpdateActions::NOTIFICATIONS:
           $notifications = $request->getStr('notifications');
           $participant = $conpherence->getParticipantIfExists($user->getPHID());
           if (!$participant) {
             return id(new Aphront404Response());
           }
           $participant->setSettings(array('notifications' => $notifications));
           $participant->save();
           $result = pht(
             'Updated notification settings to "%s".',
             ConpherenceSettings::getHumanString($notifications));
           return id(new AphrontAjaxResponse())
             ->setContent($result);
           break;
         case ConpherenceUpdateActions::METADATA:
           $top = $request->getInt('image_y');
           $left = $request->getInt('image_x');
           $file_id = $request->getInt('file_id');
           $title = $request->getStr('title');
           if ($file_id) {
             $orig_file = id(new PhabricatorFileQuery())
               ->setViewer($user)
               ->withIDs(array($file_id))
               ->executeOne();
             $xactions[] = id(new ConpherenceTransaction())
               ->setTransactionType(ConpherenceTransactionType::TYPE_PICTURE)
               ->setNewValue($orig_file);
             $okay = $orig_file->isTransformableImage();
             if ($okay) {
               $xformer = new PhabricatorImageTransformer();
               $crop_file = $xformer->executeConpherenceTransform(
                 $orig_file,
                 0,
                 0,
                 ConpherenceImageData::CROP_WIDTH,
                 ConpherenceImageData::CROP_HEIGHT);
               $xactions[] = id(new ConpherenceTransaction())
                 ->setTransactionType(
                   ConpherenceTransactionType::TYPE_PICTURE_CROP)
                 ->setNewValue($crop_file->getPHID());
             }
             $response_mode = 'redirect';
           }
 
           // all other metadata updates are continue requests
           if (!$request->isContinueRequest()) {
             break;
           }
 
           if ($top !== null || $left !== null) {
             $file = $conpherence->getImage(ConpherenceImageData::SIZE_ORIG);
             $xformer = new PhabricatorImageTransformer();
             $xformed = $xformer->executeConpherenceTransform(
               $file,
               $top,
               $left,
               ConpherenceImageData::CROP_WIDTH,
               ConpherenceImageData::CROP_HEIGHT);
             $image_phid = $xformed->getPHID();
 
             $xactions[] = id(new ConpherenceTransaction())
               ->setTransactionType(
                 ConpherenceTransactionType::TYPE_PICTURE_CROP)
               ->setNewValue($image_phid);
           }
           $title = $request->getStr('title');
           $xactions[] = id(new ConpherenceTransaction())
             ->setTransactionType(ConpherenceTransactionType::TYPE_TITLE)
             ->setNewValue($title);
           if ($conpherence->getIsRoom()) {
             $xactions[] = id(new ConpherenceTransaction())
               ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
               ->setNewValue($request->getStr('viewPolicy'));
             $xactions[] = id(new ConpherenceTransaction())
               ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY)
               ->setNewValue($request->getStr('editPolicy'));
             $xactions[] = id(new ConpherenceTransaction())
               ->setTransactionType(PhabricatorTransactions::TYPE_JOIN_POLICY)
               ->setNewValue($request->getStr('joinPolicy'));
           }
           if (!$request->getExists('force_ajax')) {
             $response_mode = 'redirect';
           }
           break;
         case ConpherenceUpdateActions::LOAD:
           $updated = false;
           $response_mode = 'ajax';
           break;
         default:
           throw new Exception('Unknown action: '.$action);
           break;
       }
 
       if ($xactions) {
         try {
           $xactions = $editor->applyTransactions($conpherence, $xactions);
           if ($delete_draft) {
             $draft = PhabricatorDraft::newFromUserAndKey(
               $user,
               $conpherence->getPHID());
             $draft->delete();
           }
         } catch (PhabricatorApplicationTransactionNoEffectException $ex) {
           return id(new PhabricatorApplicationTransactionNoEffectResponse())
             ->setCancelURI($this->getApplicationURI($conpherence_id.'/'))
             ->setException($ex);
         }
         // xactions had no effect...!
         if (empty($xactions)) {
           $errors[] = pht(
             'That was a non-update. Try cancel.');
         }
       }
 
       if ($xactions || ($action == ConpherenceUpdateActions::LOAD)) {
         switch ($response_mode) {
           case 'ajax':
             $latest_transaction_id = $request->getInt('latest_transaction_id');
             $content = $this->loadAndRenderUpdates(
               $action,
               $conpherence_id,
               $latest_transaction_id);
             return id(new AphrontAjaxResponse())
               ->setContent($content);
             break;
           case 'go-home':
             return id(new AphrontRedirectResponse())
               ->setURI($this->getApplicationURI());
             break;
           case 'redirect':
           default:
             return id(new AphrontRedirectResponse())
-              ->setURI($this->getApplicationURI($conpherence->getID().'/'));
+              ->setURI('/'.$conpherence->getMonogram());
             break;
         }
       }
     }
 
     if ($errors) {
       $error_view = id(new PHUIInfoView())
         ->setErrors($errors);
     }
 
     switch ($action) {
       case ConpherenceUpdateActions::ADD_PERSON:
         $dialogue = $this->renderAddPersonDialogue($conpherence);
         break;
       case ConpherenceUpdateActions::REMOVE_PERSON:
         $dialogue = $this->renderRemovePersonDialogue($conpherence);
         break;
       case ConpherenceUpdateActions::METADATA:
       default:
         $dialogue = $this->renderMetadataDialogue($conpherence, $error_view);
         break;
     }
 
     return id(new AphrontDialogResponse())
       ->setDialog($dialogue
         ->setUser($user)
         ->setWidth(AphrontDialogView::WIDTH_FORM)
         ->setSubmitURI($this->getApplicationURI('update/'.$conpherence_id.'/'))
         ->addSubmitButton()
         ->addCancelButton($this->getApplicationURI($conpherence->getID().'/')));
 
   }
 
   private function renderAddPersonDialogue(
     ConpherenceThread $conpherence) {
 
     $request = $this->getRequest();
     $user = $request->getUser();
     $add_person = $request->getStr('add_person');
 
     $form = id(new AphrontFormView())
       ->setUser($user)
       ->setFullWidth(true)
       ->appendControl(
         id(new AphrontFormTokenizerControl())
           ->setName('add_person')
           ->setUser($user)
           ->setDatasource(new PhabricatorPeopleDatasource()));
 
     require_celerity_resource('conpherence-update-css');
     $view = id(new AphrontDialogView())
       ->setTitle(pht('Add Participants'))
       ->addHiddenInput('action', 'add_person')
       ->addHiddenInput(
         'latest_transaction_id',
         $request->getInt('latest_transaction_id'))
       ->appendForm($form);
 
     if ($request->getExists('minimal_display')) {
       $view->addHiddenInput('minimal_display', true);
     }
     return $view;
   }
 
   private function renderRemovePersonDialogue(
     ConpherenceThread $conpherence) {
 
     $request = $this->getRequest();
     $user = $request->getUser();
     $remove_person = $request->getStr('remove_person');
     $participants = $conpherence->getParticipants();
     if ($conpherence->getIsRoom()) {
       $message = pht(
         'Are you sure you want to remove yourself from this room?');
     } else {
       $message = pht(
         'Are you sure you want to remove yourself from this thread?');
       if (count($participants) == 1) {
         $message .= pht(
           'The thread will be inaccessible forever and ever.');
       } else {
         $message .= pht(
           'Someone else in the thread can add you back later.');
       }
     }
     $body = phutil_tag(
       'p',
       array(
       ),
       $message);
 
     require_celerity_resource('conpherence-update-css');
     return id(new AphrontDialogView())
       ->setTitle(pht('Remove Participants'))
       ->addHiddenInput('action', 'remove_person')
       ->addHiddenInput('remove_person', $remove_person)
       ->addHiddenInput(
         'latest_transaction_id',
         $request->getInt('latest_transaction_id'))
       ->addHiddenInput('__continue__', true)
       ->appendChild($body);
   }
 
   private function renderMetadataDialogue(
     ConpherenceThread $conpherence,
     $error_view) {
 
     $request = $this->getRequest();
     $user = $request->getUser();
 
     $form = id(new PHUIFormLayoutView())
       ->appendChild($error_view)
       ->appendChild(
         id(new AphrontFormTextControl())
         ->setLabel(pht('Title'))
         ->setName('title')
         ->setValue($conpherence->getTitle()));
 
     $nopic = $this->getRequest()->getExists('nopic');
     $image = $conpherence->getImage(ConpherenceImageData::SIZE_ORIG);
     if ($nopic) {
       // do not render any pic related controls
     } else if ($image) {
       $crop_uri = $conpherence->loadImageURI(ConpherenceImageData::SIZE_CROP);
       $form
         ->appendChild(
           id(new AphrontFormMarkupControl())
           ->setLabel(pht('Image'))
           ->setValue(phutil_tag(
             'img',
             array(
               'src' => $crop_uri,
               ))))
         ->appendChild(
           id(new ConpherencePicCropControl())
           ->setLabel(pht('Crop Image'))
           ->setValue($image))
         ->appendChild(
           id(new ConpherenceFormDragAndDropUploadControl())
           ->setLabel(pht('Change Image')));
     } else {
       $form
         ->appendChild(
           id(new ConpherenceFormDragAndDropUploadControl())
           ->setLabel(pht('Image')));
     }
 
     if ($conpherence->getIsRoom()) {
       $title = pht('Update Room');
       $policies = id(new PhabricatorPolicyQuery())
         ->setViewer($user)
         ->setObject($conpherence)
         ->execute();
 
       $form->appendChild(
         id(new AphrontFormPolicyControl())
         ->setName('viewPolicy')
         ->setPolicyObject($conpherence)
         ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
         ->setPolicies($policies))
       ->appendChild(
         id(new AphrontFormPolicyControl())
         ->setName('editPolicy')
         ->setPolicyObject($conpherence)
         ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
         ->setPolicies($policies))
       ->appendChild(
         id(new AphrontFormPolicyControl())
         ->setName('joinPolicy')
         ->setPolicyObject($conpherence)
         ->setCapability(PhabricatorPolicyCapability::CAN_JOIN)
         ->setPolicies($policies));
     } else {
       $title = pht('Update Thread');
     }
 
     require_celerity_resource('conpherence-update-css');
     $view = id(new AphrontDialogView())
       ->setTitle($title)
       ->addHiddenInput('action', 'metadata')
       ->addHiddenInput(
         'latest_transaction_id',
         $request->getInt('latest_transaction_id'))
       ->addHiddenInput('__continue__', true)
       ->appendChild($form);
 
     if ($request->getExists('minimal_display')) {
       $view->addHiddenInput('minimal_display', true);
     }
     if ($request->getExists('force_ajax')) {
       $view->addHiddenInput('force_ajax', true);
     }
 
     return $view;
   }
 
   private function loadAndRenderUpdates(
     $action,
     $conpherence_id,
     $latest_transaction_id) {
 
+    $minimal_display = $this->getRequest()->getExists('minimal_display');
     $need_widget_data = false;
     $need_transactions = false;
     $need_participant_cache = true;
     switch ($action) {
       case ConpherenceUpdateActions::METADATA:
       case ConpherenceUpdateActions::LOAD:
         $need_transactions = true;
         break;
       case ConpherenceUpdateActions::MESSAGE:
       case ConpherenceUpdateActions::ADD_PERSON:
         $need_transactions = true;
-        $need_widget_data = true;
+        $need_widget_data = !$minimal_display;
         break;
       case ConpherenceUpdateActions::REMOVE_PERSON:
       case ConpherenceUpdateActions::NOTIFICATIONS:
       default:
         break;
 
     }
     $user = $this->getRequest()->getUser();
     $conpherence = id(new ConpherenceThreadQuery())
       ->setViewer($user)
       ->setAfterTransactionID($latest_transaction_id)
       ->needCropPics(true)
       ->needParticipantCache($need_participant_cache)
       ->needWidgetData($need_widget_data)
       ->needTransactions($need_transactions)
       ->withIDs(array($conpherence_id))
       ->executeOne();
 
     $non_update = false;
     if ($need_transactions && $conpherence->getTransactions()) {
       $data = ConpherenceTransactionRenderer::renderTransactions(
         $user,
         $conpherence,
-        !$this->getRequest()->getExists('minimal_display'));
+        !$minimal_display);
       $participant_obj = $conpherence->getParticipant($user->getPHID());
       $participant_obj->markUpToDate($conpherence, $data['latest_transaction']);
     } else if ($need_transactions) {
       $non_update = true;
       $data = array();
     } else {
       $data = array();
     }
     $rendered_transactions = idx($data, 'transactions');
     $new_latest_transaction_id = idx($data, 'latest_transaction_id');
 
     $widget_uri = $this->getApplicationURI('update/'.$conpherence->getID().'/');
     $nav_item = null;
     $header = null;
     $people_widget = null;
     $file_widget = null;
-    switch ($action) {
-      case ConpherenceUpdateActions::METADATA:
-        $policy_objects = id(new PhabricatorPolicyQuery())
-          ->setViewer($user)
-          ->setObject($conpherence)
-          ->execute();
-        $header = $this->buildHeaderPaneContent($conpherence, $policy_objects);
-        $nav_item = id(new ConpherenceThreadListView())
-          ->setUser($user)
-          ->setBaseURI($this->getApplicationURI())
-          ->renderSingleThread($conpherence);
-        break;
-      case ConpherenceUpdateActions::MESSAGE:
-        $file_widget = id(new ConpherenceFileWidgetView())
-          ->setUser($this->getRequest()->getUser())
-          ->setConpherence($conpherence)
-          ->setUpdateURI($widget_uri);
-        break;
-      case ConpherenceUpdateActions::ADD_PERSON:
-        $people_widget = id(new ConpherencePeopleWidgetView())
-          ->setUser($user)
-          ->setConpherence($conpherence)
-          ->setUpdateURI($widget_uri);
-        break;
-      case ConpherenceUpdateActions::REMOVE_PERSON:
-      case ConpherenceUpdateActions::NOTIFICATIONS:
-      default:
-        break;
-    }
-
-    $people_html = null;
-    if ($people_widget) {
-      $people_html = hsprintf('%s', $people_widget->render());
+    if (!$minimal_display) {
+      switch ($action) {
+        case ConpherenceUpdateActions::METADATA:
+          $policy_objects = id(new PhabricatorPolicyQuery())
+            ->setViewer($user)
+            ->setObject($conpherence)
+            ->execute();
+          $header = $this->buildHeaderPaneContent(
+            $conpherence,
+            $policy_objects);
+          $header = hsprintf('%s', $header);
+          $nav_item = id(new ConpherenceThreadListView())
+            ->setUser($user)
+            ->setBaseURI($this->getApplicationURI())
+            ->renderSingleThread($conpherence);
+          $nav_item = hsprintf('%s', $nav_item);
+          break;
+        case ConpherenceUpdateActions::MESSAGE:
+          $file_widget = id(new ConpherenceFileWidgetView())
+            ->setUser($this->getRequest()->getUser())
+            ->setConpherence($conpherence)
+            ->setUpdateURI($widget_uri);
+          $file_widget = $file_widget->render();
+          break;
+        case ConpherenceUpdateActions::ADD_PERSON:
+          $people_widget = id(new ConpherencePeopleWidgetView())
+            ->setUser($user)
+            ->setConpherence($conpherence)
+            ->setUpdateURI($widget_uri);
+          $people_widget = $people_widget->render();
+          break;
+        case ConpherenceUpdateActions::REMOVE_PERSON:
+        case ConpherenceUpdateActions::NOTIFICATIONS:
+        default:
+          break;
+      }
     }
     $data = $conpherence->getDisplayData($user);
+    $dropdown_query = id(new AphlictDropdownDataQuery())
+      ->setViewer($user);
+    $dropdown_query->execute();
     $content = array(
       'non_update' => $non_update,
       'transactions' => hsprintf('%s', $rendered_transactions),
       'conpherence_title' => (string) $data['title'],
       'latest_transaction_id' => $new_latest_transaction_id,
-      'nav_item' => hsprintf('%s', $nav_item),
+      'nav_item' => $nav_item,
       'conpherence_phid' => $conpherence->getPHID(),
-      'header' => hsprintf('%s', $header),
-      'file_widget' => $file_widget ? $file_widget->render() : null,
-      'people_widget' => $people_html,
+      'header' => $header,
+      'file_widget' => $file_widget,
+      'people_widget' => $people_widget,
+      'aphlictDropdownData' => array(
+        $dropdown_query->getNotificationData(),
+        $dropdown_query->getConpherenceData(),
+      ),
     );
 
     return $content;
   }
 
 }
diff --git a/src/applications/conpherence/controller/ConpherenceViewController.php b/src/applications/conpherence/controller/ConpherenceViewController.php
index 7a3027dc53..6293c1bbdd 100644
--- a/src/applications/conpherence/controller/ConpherenceViewController.php
+++ b/src/applications/conpherence/controller/ConpherenceViewController.php
@@ -1,206 +1,228 @@
 <?php
 
 final class ConpherenceViewController extends
   ConpherenceController {
 
   const OLDER_FETCH_LIMIT = 5;
 
+  public function shouldAllowPublic() {
+    return true;
+  }
+
   public function handleRequest(AphrontRequest $request) {
     $user = $request->getUser();
 
     $conpherence_id = $request->getURIData('id');
     if (!$conpherence_id) {
       return new Aphront404Response();
     }
     $query = id(new ConpherenceThreadQuery())
       ->setViewer($user)
       ->withIDs(array($conpherence_id))
       ->needCropPics(true)
       ->needParticipantCache(true)
       ->needTransactions(true)
       ->setTransactionLimit($this->getMainQueryLimit());
 
     $before_transaction_id = $request->getInt('oldest_transaction_id');
     $after_transaction_id = $request->getInt('newest_transaction_id');
     $old_message_id = $request->getURIData('messageID');
     if ($before_transaction_id && ($old_message_id || $after_transaction_id)) {
       throw new Aphront400Response();
     }
     if ($old_message_id && $after_transaction_id) {
       throw new Aphront400Response();
     }
 
     $marker_type = 'older';
     if ($before_transaction_id) {
       $query
         ->setBeforeTransactionID($before_transaction_id);
     }
     if ($old_message_id) {
       $marker_type = 'olderandnewer';
       $query
         ->setAfterTransactionID($old_message_id - 1);
     }
     if ($after_transaction_id) {
       $marker_type = 'newer';
       $query
         ->setAfterTransactionID($after_transaction_id);
     }
 
     $conpherence = $query->executeOne();
     if (!$conpherence) {
       return new Aphront404Response();
     }
     $this->setConpherence($conpherence);
 
     $transactions = $this->getNeededTransactions(
       $conpherence,
       $old_message_id);
     $latest_transaction = head($transactions);
     $participant = $conpherence->getParticipantIfExists($user->getPHID());
     if ($participant) {
       $write_guard = AphrontWriteGuard::beginScopedUnguardedWrites();
       $participant->markUpToDate($conpherence, $latest_transaction);
       unset($write_guard);
     }
 
     $data = ConpherenceTransactionRenderer::renderTransactions(
       $user,
       $conpherence,
       $full_display = true,
       $marker_type);
     $messages = ConpherenceTransactionRenderer::renderMessagePaneContent(
       $data['transactions'],
       $data['oldest_transaction_id'],
       $data['newest_transaction_id']);
     if ($before_transaction_id || $after_transaction_id) {
       $header = null;
       $form = null;
-      $content = array('messages' => $messages);
+      $content = array('transactions' => $messages);
     } else {
       $policy_objects = id(new PhabricatorPolicyQuery())
         ->setViewer($user)
         ->setObject($conpherence)
         ->execute();
       $header = $this->buildHeaderPaneContent($conpherence, $policy_objects);
       $form = $this->renderFormContent();
       $content = array(
         'header' => $header,
-        'messages' => $messages,
+        'transactions' => $messages,
         'form' => $form,
       );
     }
 
     $d_data = $conpherence->getDisplayData($user);
     $content['title'] = $title = $d_data['title'];
 
     if ($request->isAjax()) {
+      $dropdown_query = id(new AphlictDropdownDataQuery())
+        ->setViewer($user);
+      $dropdown_query->execute();
       $content['threadID'] = $conpherence->getID();
       $content['threadPHID'] = $conpherence->getPHID();
       $content['latestTransactionID'] = $data['latest_transaction_id'];
       $content['canEdit'] = PhabricatorPolicyFilter::hasCapability(
         $user,
         $conpherence,
         PhabricatorPolicyCapability::CAN_EDIT);
+      $content['aphlictDropdownData'] = array(
+        $dropdown_query->getNotificationData(),
+        $dropdown_query->getConpherenceData(),
+      );
       return id(new AphrontAjaxResponse())->setContent($content);
     }
 
     $layout = id(new ConpherenceLayoutView())
       ->setUser($user)
       ->setBaseURI($this->getApplicationURI())
       ->setThread($conpherence)
       ->setHeader($header)
       ->setMessages($messages)
       ->setReplyForm($form)
       ->setLatestTransactionID($data['latest_transaction_id'])
       ->setRole('thread');
 
    return $this->buildApplicationPage(
       $layout,
       array(
         'title' => $title,
         'pageObjects' => array($conpherence->getPHID()),
       ));
   }
 
   private function renderFormContent() {
 
     $conpherence = $this->getConpherence();
     $user = $this->getRequest()->getUser();
     $can_join = PhabricatorPolicyFilter::hasCapability(
       $user,
       $conpherence,
       PhabricatorPolicyCapability::CAN_JOIN);
     $participating = $conpherence->getParticipantIfExists($user->getPHID());
-    if (!$can_join && !$participating) {
+    if (!$can_join && !$participating && $user->isLoggedIn()) {
       return null;
     }
     $draft = PhabricatorDraft::newFromUserAndKey(
       $user,
       $conpherence->getPHID());
     if ($participating) {
       $action = ConpherenceUpdateActions::MESSAGE;
       $button_text = pht('Send');
-    } else {
+    } else if ($user->isLoggedIn()) {
       $action = ConpherenceUpdateActions::JOIN_ROOM;
       $button_text = pht('Join');
+    } else {
+      // user not logged in so give them a login button.
+      $login_href = id(new PhutilURI('/auth/start/'))
+        ->setQueryParam('next', '/'.$conpherence->getMonogram());
+      return id(new PHUIFormLayoutView())
+        ->addClass('login-to-participate')
+        ->appendChild(
+          id(new PHUIButtonView())
+          ->setTag('a')
+          ->setText(pht('Login to Participate'))
+          ->setHref((string)$login_href));
     }
     $update_uri = $this->getApplicationURI('update/'.$conpherence->getID().'/');
 
     $this->initBehavior('conpherence-pontificate');
 
     $form =
       id(new AphrontFormView())
+      ->setUser($user)
       ->setAction($update_uri)
       ->addSigil('conpherence-pontificate')
       ->setWorkflow(true)
-      ->setUser($user)
       ->addHiddenInput('action', $action)
       ->appendChild(
         id(new PhabricatorRemarkupControl())
         ->setUser($user)
         ->setName('text')
         ->setValue($draft->getDraft()))
       ->appendChild(
         id(new AphrontFormSubmitControl())
         ->setValue($button_text))
       ->render();
 
     return $form;
   }
 
   private function getNeededTransactions(
     ConpherenceThread $conpherence,
     $message_id) {
 
     if ($message_id) {
       $newer_transactions = $conpherence->getTransactions();
       $query = id(new ConpherenceTransactionQuery())
         ->setViewer($this->getRequest()->getUser())
         ->withObjectPHIDs(array($conpherence->getPHID()))
         ->setAfterID($message_id)
         ->needHandles(true)
         ->setLimit(self::OLDER_FETCH_LIMIT);
       $older_transactions = $query->execute();
       $handles = array();
       foreach ($older_transactions as $transaction) {
         $handles += $transaction->getHandles();
       }
       $conpherence->attachHandles($conpherence->getHandles() + $handles);
       $transactions = array_merge($newer_transactions, $older_transactions);
       $conpherence->attachTransactions($transactions);
     } else {
       $transactions = $conpherence->getTransactions();
     }
 
     return $transactions;
   }
 
   private function getMainQueryLimit() {
     $request = $this->getRequest();
     $base_limit = ConpherenceThreadQuery::TRANSACTION_LIMIT;
     if ($request->getURIData('messageID')) {
       $base_limit = $base_limit - self::OLDER_FETCH_LIMIT;
     }
     return $base_limit;
   }
 }
diff --git a/src/applications/conpherence/controller/ConpherenceWidgetController.php b/src/applications/conpherence/controller/ConpherenceWidgetController.php
index f7775dcc32..9a06f0dcf9 100644
--- a/src/applications/conpherence/controller/ConpherenceWidgetController.php
+++ b/src/applications/conpherence/controller/ConpherenceWidgetController.php
@@ -1,401 +1,411 @@
 <?php
 
 final class ConpherenceWidgetController extends ConpherenceController {
 
   private $userPreferences;
 
   public function setUserPreferences(PhabricatorUserPreferences $pref) {
     $this->userPreferences = $pref;
     return $this;
   }
 
   public function getUserPreferences() {
     return $this->userPreferences;
   }
 
+  public function shouldAllowPublic() {
+    return true;
+  }
+
   public function handleRequest(AphrontRequest $request) {
     $request = $this->getRequest();
     $user = $request->getUser();
 
     $conpherence_id = $request->getURIData('id');
     if (!$conpherence_id) {
       return new Aphront404Response();
     }
     $conpherence = id(new ConpherenceThreadQuery())
       ->setViewer($user)
       ->withIDs(array($conpherence_id))
       ->needWidgetData(true)
       ->executeOne();
+    if (!$conpherence) {
+      return new Aphront404Response();
+    }
     $this->setConpherence($conpherence);
 
     $this->setUserPreferences($user->loadPreferences());
 
     switch ($request->getStr('widget')) {
       case 'widgets-people':
         $content = $this->renderPeopleWidgetPaneContent();
         break;
       case 'widgets-files':
         $content = $this->renderFileWidgetPaneContent();
         break;
       case 'widgets-calendar':
         $widget = $this->renderCalendarWidgetPaneContent();
         $content = phutil_implode_html('', $widget);
         break;
       case 'widgets-settings':
         $content = $this->renderSettingsWidgetPaneContent();
         break;
       default:
         $widgets = $this->renderWidgetPaneContent();
         $content = $widgets;
         break;
     }
     return id(new AphrontAjaxResponse())->setContent($content);
   }
 
   private function renderWidgetPaneContent() {
     $conpherence = $this->getConpherence();
 
     $widgets = array();
     $new_icon = id(new PHUIIconView())
       ->setIconFont('fa-plus')
       ->setHref($this->getWidgetURI())
       ->setMetadata(array('widget' => null))
       ->addSigil('conpherence-widget-adder');
     $widgets[] = phutil_tag(
       'div',
       array(
         'class' => 'widgets-header',
       ),
       id(new PHUIActionHeaderView())
       ->setHeaderTitle(pht('Participants'))
       ->setHeaderHref('#')
       ->setDropdown(true)
       ->addAction($new_icon)
       ->addHeaderSigil('widgets-selector'));
     $user = $this->getRequest()->getUser();
     // now the widget bodies
     $widgets[] = javelin_tag(
       'div',
       array(
         'class' => 'widgets-body',
         'id' => 'widgets-people',
         'sigil' => 'widgets-people',
       ),
       $this->renderPeopleWidgetPaneContent());
    $widgets[] = javelin_tag(
       'div',
       array(
         'class' => 'widgets-body',
         'id' => 'widgets-files',
         'sigil' => 'widgets-files',
         'style' => 'display: none;',
       ),
       $this->renderFileWidgetPaneContent());
    $widgets[] = phutil_tag(
       'div',
       array(
         'class' => 'widgets-body',
         'id' => 'widgets-calendar',
         'style' => 'display: none;',
       ),
       $this->renderCalendarWidgetPaneContent());
     $widgets[] = phutil_tag(
       'div',
       array(
         'class' => 'widgets-body',
         'id' => 'widgets-settings',
         'style' => 'display: none',
       ),
       $this->renderSettingsWidgetPaneContent());
 
     // without this implosion we get "," between each element in our widgets
     // array
     return array('widgets' => phutil_implode_html('', $widgets));
   }
 
   private function renderPeopleWidgetPaneContent() {
     return id(new ConpherencePeopleWidgetView())
       ->setUser($this->getViewer())
       ->setConpherence($this->getConpherence())
       ->setUpdateURI($this->getWidgetURI());
   }
 
   private function renderFileWidgetPaneContent() {
     return  id(new ConpherenceFileWidgetView())
       ->setUser($this->getViewer())
       ->setConpherence($this->getConpherence())
       ->setUpdateURI($this->getWidgetURI());
   }
 
   private function renderSettingsWidgetPaneContent() {
     $viewer = $this->getViewer();
     $conpherence = $this->getConpherence();
     $participant = $conpherence->getParticipantIfExists($viewer->getPHID());
     if (!$participant) {
       $can_join = PhabricatorPolicyFilter::hasCapability(
         $viewer,
         $conpherence,
         PhabricatorPolicyCapability::CAN_JOIN);
       if ($can_join) {
         $text = pht('Settings are available after joining the room.');
-      } else {
+      } else if ($viewer->isLoggedIn()) {
         $text = pht('Settings not applicable to rooms you can not join.');
+      } else {
+        $text = pht(
+          'Settings are available after logging in and joining the room.');
       }
       return phutil_tag(
         'div',
         array(
           'class' => 'no-settings',
         ),
         $text);
     }
     $default = ConpherenceSettings::EMAIL_ALWAYS;
     $preference = $this->getUserPreferences();
     if ($preference) {
       $default = $preference->getPreference(
         PhabricatorUserPreferences::PREFERENCE_CONPH_NOTIFICATIONS,
         ConpherenceSettings::EMAIL_ALWAYS);
     }
     $settings = $participant->getSettings();
     $notifications = idx(
       $settings,
       'notifications',
       $default);
     $options = id(new AphrontFormRadioButtonControl())
       ->addButton(
         ConpherenceSettings::EMAIL_ALWAYS,
         ConpherenceSettings::getHumanString(
           ConpherenceSettings::EMAIL_ALWAYS),
         '')
       ->addButton(
         ConpherenceSettings::NOTIFICATIONS_ONLY,
         ConpherenceSettings::getHumanString(
           ConpherenceSettings::NOTIFICATIONS_ONLY),
         '')
       ->setName('notifications')
       ->setValue($notifications);
 
     $layout = array(
       $options,
       phutil_tag(
         'input',
         array(
           'type' => 'hidden',
           'name' => 'action',
           'value' => 'notifications',
         )),
       phutil_tag(
         'button',
         array(
           'type' => 'submit',
           'class' => 'notifications-update',
         ),
         pht('Save')),
     );
 
     return phabricator_form(
       $viewer,
       array(
         'method' => 'POST',
         'action' => $this->getWidgetURI(),
         'sigil' => 'notifications-update',
       ),
       $layout);
   }
 
   private function renderCalendarWidgetPaneContent() {
     $user = $this->getRequest()->getUser();
 
     $conpherence = $this->getConpherence();
     $participants = $conpherence->getParticipants();
     $widget_data = $conpherence->getWidgetData();
     $statuses = $widget_data['statuses'];
     $handles = $conpherence->getHandles();
     $content = array();
     $layout = id(new AphrontMultiColumnView())
       ->setFluidLayout(true);
     $timestamps = CalendarTimeUtil::getCalendarWidgetTimestamps($user);
     $today = $timestamps['today'];
     $epoch_stamps = $timestamps['epoch_stamps'];
     $one_day = 24 * 60 * 60;
     $is_today = false;
     $calendar_columns = 0;
     $list_days = 0;
     foreach ($epoch_stamps as $day) {
       // build a header for the new day
       if ($day->format('Ymd') == $today->format('Ymd')) {
         $active_class = 'today';
         $is_today = true;
       } else {
         $active_class = '';
         $is_today = false;
       }
 
       $should_draw_list = $list_days < 7;
       $list_days++;
 
       if ($should_draw_list) {
         $content[] = phutil_tag(
           'div',
           array(
             'class' => 'day-header '.$active_class,
           ),
           array(
             phutil_tag(
               'div',
               array(
                 'class' => 'day-name',
               ),
               $day->format('l')),
             phutil_tag(
               'div',
               array(
                 'class' => 'day-date',
               ),
               $day->format('m/d/y')),
           ));
       }
 
       $week_day_number = $day->format('w');
 
       $epoch_start = $day->format('U');
       $next_day = clone $day;
       $next_day->modify('+1 day');
       $epoch_end = $next_day->format('U');
 
       $first_status_of_the_day = true;
       $statuses_of_the_day = array();
       // keep looking through statuses where we last left off
       foreach ($statuses as $status) {
         if ($status->getDateFrom() >= $epoch_end) {
           // This list is sorted, so we can stop looking.
           break;
         }
 
         if ($status->getDateFrom() < $epoch_end &&
             $status->getDateTo() > $epoch_start) {
           $statuses_of_the_day[$status->getUserPHID()] = $status;
           if ($should_draw_list) {
             $top_border = '';
             if (!$first_status_of_the_day) {
               $top_border = ' top-border';
             }
             $timespan = $status->getDateTo() - $status->getDateFrom();
             if ($timespan > $one_day) {
               $time_str = 'm/d';
             } else {
               $time_str = 'h:i A';
             }
             $epoch_range =
               phabricator_format_local_time(
                 $status->getDateFrom(),
                 $user,
                 $time_str).
               ' - '.
               phabricator_format_local_time(
                 $status->getDateTo(),
                 $user,
                 $time_str);
 
             $secondary_info = pht('%s, %s',
               $handles[$status->getUserPHID()]->getName(), $epoch_range);
 
             $content[] = phutil_tag(
               'div',
               array(
                 'class' => 'user-status '.$status->getTextStatus().$top_border,
               ),
               array(
                 phutil_tag(
                   'div',
                   array(
                     'class' => 'icon',
                   ),
                   ''),
                 phutil_tag(
                   'div',
                   array(
                     'class' => 'description',
                   ),
                   array(
                     $status->getTerseSummary($user),
                     phutil_tag(
                       'div',
                       array(
                         'class' => 'participant',
                       ),
                       $secondary_info),
                   )),
               ));
           }
           $first_status_of_the_day = false;
         }
       }
 
       // we didn't get a status on this day so add a spacer
       if ($first_status_of_the_day && $should_draw_list) {
         $content[] = phutil_tag(
           'div',
           array('class' => 'no-events pm'),
           pht('No Events Scheduled.'));
       }
       if ($is_today || ($calendar_columns && $calendar_columns < 3)) {
         $active_class = '';
         if ($is_today) {
           $active_class = '-active';
         }
         $inner_layout = array();
         foreach ($participants as $phid => $participant) {
           $status = idx($statuses_of_the_day, $phid, false);
           if ($status) {
             $inner_layout[] = phutil_tag(
               'div',
               array(
                 'class' => $status->getTextStatus(),
               ),
               '');
           } else {
             $inner_layout[] = phutil_tag(
               'div',
               array(
                 'class' => 'present',
               ),
               '');
           }
         }
         $layout->addColumn(
           phutil_tag(
             'div',
             array(
               'class' => 'day-column'.$active_class,
             ),
             array(
               phutil_tag(
                 'div',
                 array(
                   'class' => 'day-name',
                 ),
                 $day->format('D')),
               phutil_tag(
                 'div',
                 array(
                   'class' => 'day-number',
                 ),
                 $day->format('j')),
               $inner_layout,
             )));
         $calendar_columns++;
       }
     }
 
     return array(
       $layout,
       $content,
     );
   }
 
   private function getWidgetURI() {
     $conpherence = $this->getConpherence();
     return $this->getApplicationURI('update/'.$conpherence->getID().'/');
   }
 
 }
diff --git a/src/applications/conpherence/query/ConpherenceThreadQuery.php b/src/applications/conpherence/query/ConpherenceThreadQuery.php
index 03c341f090..b17044ead0 100644
--- a/src/applications/conpherence/query/ConpherenceThreadQuery.php
+++ b/src/applications/conpherence/query/ConpherenceThreadQuery.php
@@ -1,467 +1,466 @@
 <?php
 
 final class ConpherenceThreadQuery
   extends PhabricatorCursorPagedPolicyAwareQuery {
 
   const TRANSACTION_LIMIT = 100;
 
   private $phids;
   private $ids;
   private $participantPHIDs;
   private $isRoom;
   private $needParticipants;
   private $needWidgetData;
   private $needCropPics;
   private $needOrigPics;
   private $needTransactions;
   private $needParticipantCache;
   private $needFilePHIDs;
   private $afterTransactionID;
   private $beforeTransactionID;
   private $transactionLimit;
   private $fulltext;
 
   public function needFilePHIDs($need_file_phids) {
     $this->needFilePHIDs = $need_file_phids;
     return $this;
   }
 
   public function needParticipantCache($participant_cache) {
     $this->needParticipantCache = $participant_cache;
     return $this;
   }
 
   public function needParticipants($need) {
     $this->needParticipants = $need;
     return $this;
   }
 
   public function needWidgetData($need_widget_data) {
     $this->needWidgetData = $need_widget_data;
     return $this;
   }
 
   public function needCropPics($need) {
     $this->needCropPics = $need;
     return $this;
   }
 
   public function needOrigPics($need_widget_data) {
     $this->needOrigPics = $need_widget_data;
     return $this;
   }
 
   public function needTransactions($need_transactions) {
     $this->needTransactions = $need_transactions;
     return $this;
   }
 
   public function withIDs(array $ids) {
     $this->ids = $ids;
     return $this;
   }
 
   public function withPHIDs(array $phids) {
     $this->phids = $phids;
     return $this;
   }
 
   public function withParticipantPHIDs(array $phids) {
     $this->participantPHIDs = $phids;
     return $this;
   }
 
   public function withIsRoom($bool) {
     $this->isRoom = $bool;
     return $this;
   }
 
   public function setAfterTransactionID($id) {
     $this->afterTransactionID = $id;
     return $this;
   }
 
   public function setBeforeTransactionID($id) {
     $this->beforeTransactionID = $id;
     return $this;
   }
 
   public function setTransactionLimit($transaction_limit) {
     $this->transactionLimit = $transaction_limit;
     return $this;
   }
 
   public function getTransactionLimit() {
     return $this->transactionLimit;
   }
 
   public function withFulltext($query) {
     $this->fulltext = $query;
     return $this;
   }
 
   protected function loadPage() {
     $table = new ConpherenceThread();
     $conn_r = $table->establishConnection('r');
 
     $data = queryfx_all(
       $conn_r,
       'SELECT conpherence_thread.* FROM %T conpherence_thread %Q %Q %Q %Q %Q',
       $table->getTableName(),
       $this->buildJoinClause($conn_r),
       $this->buildWhereClause($conn_r),
       $this->buildGroupClause($conn_r),
       $this->buildOrderClause($conn_r),
       $this->buildLimitClause($conn_r));
 
     $conpherences = $table->loadAllFromArray($data);
 
     if ($conpherences) {
       $conpherences = mpull($conpherences, null, 'getPHID');
       $this->loadParticipantsAndInitHandles($conpherences);
       if ($this->needParticipantCache) {
         $this->loadCoreHandles($conpherences, 'getRecentParticipantPHIDs');
       }
       if ($this->needWidgetData || $this->needParticipants) {
         $this->loadCoreHandles($conpherences, 'getParticipantPHIDs');
       }
       if ($this->needTransactions) {
         $this->loadTransactionsAndHandles($conpherences);
       }
       if ($this->needFilePHIDs || $this->needWidgetData) {
         $this->loadFilePHIDs($conpherences);
       }
       if ($this->needWidgetData) {
         $this->loadWidgetData($conpherences);
       }
       if ($this->needOrigPics || $this->needCropPics) {
         $this->initImages($conpherences);
       }
       if ($this->needOrigPics) {
         $this->loadOrigPics($conpherences);
       }
       if ($this->needCropPics) {
         $this->loadCropPics($conpherences);
       }
     }
 
     return $conpherences;
   }
 
   protected function buildGroupClause(AphrontDatabaseConnection $conn_r) {
     if ($this->participantPHIDs !== null || strlen($this->fulltext)) {
       return 'GROUP BY conpherence_thread.id';
     } else {
       return $this->buildApplicationSearchGroupClause($conn_r);
     }
   }
 
   protected function buildJoinClause(AphrontDatabaseConnection $conn_r) {
     $joins = array();
 
     if ($this->participantPHIDs !== null) {
       $joins[] = qsprintf(
         $conn_r,
         'JOIN %T p ON p.conpherencePHID = conpherence_thread.phid',
         id(new ConpherenceParticipant())->getTableName());
     }
 
     $viewer = $this->getViewer();
     if ($this->shouldJoinForViewer($viewer)) {
       $joins[] = qsprintf(
         $conn_r,
         'LEFT JOIN %T v ON v.conpherencePHID = conpherence_thread.phid '.
         'AND v.participantPHID = %s',
         id(new ConpherenceParticipant())->getTableName(),
         $viewer->getPHID());
     }
 
     if (strlen($this->fulltext)) {
       $joins[] = qsprintf(
         $conn_r,
         'JOIN %T idx ON idx.threadPHID = conpherence_thread.phid',
         id(new ConpherenceIndex())->getTableName());
     }
 
     $joins[] = $this->buildApplicationSearchJoinClause($conn_r);
     return implode(' ', $joins);
   }
 
   private function shouldJoinForViewer(PhabricatorUser $viewer) {
     if ($viewer->isLoggedIn() &&
       $this->ids === null &&
       $this->phids === null) {
       return true;
     }
     return false;
   }
 
   protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
     $where = array();
 
     $where[] = $this->buildPagingClause($conn_r);
 
     if ($this->ids !== null) {
       $where[] = qsprintf(
         $conn_r,
         'conpherence_thread.id IN (%Ld)',
         $this->ids);
     }
 
     if ($this->phids !== null) {
       $where[] = qsprintf(
         $conn_r,
         'conpherence_thread.phid IN (%Ls)',
         $this->phids);
     }
 
     if ($this->participantPHIDs !== null) {
       $where[] = qsprintf(
         $conn_r,
         'p.participantPHID IN (%Ls)',
         $this->participantPHIDs);
     }
 
     if ($this->isRoom !== null) {
       $where[] = qsprintf(
         $conn_r,
         'conpherence_thread.isRoom = %d',
         (int)$this->isRoom);
     }
 
     if (strlen($this->fulltext)) {
       $where[] = qsprintf(
         $conn_r,
         'MATCH(idx.corpus) AGAINST (%s IN BOOLEAN MODE)',
         $this->fulltext);
     }
 
     $viewer = $this->getViewer();
     if ($this->shouldJoinForViewer($viewer)) {
       $where[] = qsprintf(
         $conn_r,
         'conpherence_thread.isRoom = 1 OR v.participantPHID IS NOT NULL');
     } else if ($this->phids === null && $this->ids === null) {
       $where[] = qsprintf(
         $conn_r,
         'conpherence_thread.isRoom = 1');
     }
 
     return $this->formatWhereClause($where);
   }
 
   private function loadParticipantsAndInitHandles(array $conpherences) {
     $participants = id(new ConpherenceParticipant())
       ->loadAllWhere('conpherencePHID IN (%Ls)', array_keys($conpherences));
     $map = mgroup($participants, 'getConpherencePHID');
 
     foreach ($conpherences as $current_conpherence) {
       $conpherence_phid = $current_conpherence->getPHID();
 
       $conpherence_participants = idx(
         $map,
         $conpherence_phid,
         array());
 
       $conpherence_participants = mpull(
         $conpherence_participants,
         null,
         'getParticipantPHID');
 
       $current_conpherence->attachParticipants($conpherence_participants);
       $current_conpherence->attachHandles(array());
     }
 
     return $this;
   }
 
   private function loadCoreHandles(
     array $conpherences,
     $method) {
 
     $handle_phids = array();
     foreach ($conpherences as $conpherence) {
       $handle_phids[$conpherence->getPHID()] =
         $conpherence->$method();
     }
     $flat_phids = array_mergev($handle_phids);
-    $handles = id(new PhabricatorHandleQuery())
-      ->setViewer($this->getViewer())
-      ->withPHIDs($flat_phids)
-      ->execute();
+    $viewer = $this->getViewer();
+    $handles = $viewer->loadHandles($flat_phids);
+    $handles = iterator_to_array($handles);
     foreach ($handle_phids as $conpherence_phid => $phids) {
       $conpherence = $conpherences[$conpherence_phid];
       $conpherence->attachHandles(
         $conpherence->getHandles() + array_select_keys($handles, $phids));
     }
     return $this;
   }
 
   private function loadTransactionsAndHandles(array $conpherences) {
     $query = id(new ConpherenceTransactionQuery())
       ->setViewer($this->getViewer())
       ->withObjectPHIDs(array_keys($conpherences))
       ->needHandles(true);
 
     // We have to flip these for the underyling query class. The semantics of
     // paging are tricky business.
     if ($this->afterTransactionID) {
       $query->setBeforeID($this->afterTransactionID);
     } else if ($this->beforeTransactionID) {
       $query->setAfterID($this->beforeTransactionID);
     }
     if ($this->getTransactionLimit()) {
       // fetch an extra for "show older" scenarios
       $query->setLimit($this->getTransactionLimit() + 1);
     }
     $transactions = $query->execute();
     $transactions = mgroup($transactions, 'getObjectPHID');
     foreach ($conpherences as $phid => $conpherence) {
       $current_transactions = idx($transactions, $phid, array());
       $handles = array();
       foreach ($current_transactions as $transaction) {
         $handles += $transaction->getHandles();
       }
       $conpherence->attachHandles($conpherence->getHandles() + $handles);
       $conpherence->attachTransactions($current_transactions);
     }
     return $this;
   }
 
   private function loadFilePHIDs(array $conpherences) {
     $edge_type = PhabricatorObjectHasFileEdgeType::EDGECONST;
     $file_edges = id(new PhabricatorEdgeQuery())
       ->withSourcePHIDs(array_keys($conpherences))
       ->withEdgeTypes(array($edge_type))
       ->execute();
     foreach ($file_edges as $conpherence_phid => $data) {
       $conpherence = $conpherences[$conpherence_phid];
       $conpherence->attachFilePHIDs(array_keys($data[$edge_type]));
     }
     return $this;
   }
 
   private function loadWidgetData(array $conpherences) {
     $participant_phids = array();
     $file_phids = array();
     foreach ($conpherences as $conpherence) {
       $participant_phids[] = array_keys($conpherence->getParticipants());
       $file_phids[] = $conpherence->getFilePHIDs();
     }
     $participant_phids = array_mergev($participant_phids);
     $file_phids = array_mergev($file_phids);
 
     $epochs = CalendarTimeUtil::getCalendarEventEpochs(
       $this->getViewer());
     $start_epoch = $epochs['start_epoch'];
     $end_epoch = $epochs['end_epoch'];
     $statuses = id(new PhabricatorCalendarEventQuery())
       ->setViewer($this->getViewer())
       ->withInvitedPHIDs($participant_phids)
       ->withDateRange($start_epoch, $end_epoch)
       ->execute();
 
     $statuses = mgroup($statuses, 'getUserPHID');
 
     // attached files
     $files = array();
     $file_author_phids = array();
     $authors = array();
     if ($file_phids) {
       $files = id(new PhabricatorFileQuery())
         ->setViewer($this->getViewer())
         ->withPHIDs($file_phids)
         ->execute();
       $files = mpull($files, null, 'getPHID');
       $file_author_phids = mpull($files, 'getAuthorPHID', 'getPHID');
       $authors = id(new PhabricatorHandleQuery())
         ->setViewer($this->getViewer())
         ->withPHIDs($file_author_phids)
         ->execute();
       $authors = mpull($authors, null, 'getPHID');
     }
 
     foreach ($conpherences as $phid => $conpherence) {
       $participant_phids = array_keys($conpherence->getParticipants());
       $statuses = array_select_keys($statuses, $participant_phids);
       $statuses = array_mergev($statuses);
       $statuses = msort($statuses, 'getDateFrom');
 
       $conpherence_files = array();
       $files_authors = array();
       foreach ($conpherence->getFilePHIDs() as $curr_phid) {
         $curr_file = idx($files, $curr_phid);
         if (!$curr_file) {
           // this file was deleted or user doesn't have permission to see it
           // this is generally weird
           continue;
         }
         $conpherence_files[$curr_phid] = $curr_file;
         // some files don't have authors so be careful
         $current_author = null;
         $current_author_phid = idx($file_author_phids, $curr_phid);
         if ($current_author_phid) {
           $current_author = $authors[$current_author_phid];
         }
         $files_authors[$curr_phid] = $current_author;
       }
       $widget_data = array(
         'statuses' => $statuses,
         'files' => $conpherence_files,
         'files_authors' => $files_authors,
       );
       $conpherence->attachWidgetData($widget_data);
     }
 
     return $this;
   }
 
   private function loadOrigPics(array $conpherences) {
     return $this->loadPics(
       $conpherences,
       ConpherenceImageData::SIZE_ORIG);
   }
 
   private function loadCropPics(array $conpherences) {
     return $this->loadPics(
       $conpherences,
       ConpherenceImageData::SIZE_CROP);
   }
 
   private function initImages($conpherences) {
     foreach ($conpherences as $conpherence) {
       $conpherence->attachImages(array());
     }
   }
 
   private function loadPics(array $conpherences, $size) {
     $conpherence_pic_phids = array();
     foreach ($conpherences as $conpherence) {
       $phid = $conpherence->getImagePHID($size);
       if ($phid) {
         $conpherence_pic_phids[$conpherence->getPHID()] = $phid;
       }
     }
 
     if (!$conpherence_pic_phids) {
       return $this;
     }
 
     $files = id(new PhabricatorFileQuery())
       ->setViewer($this->getViewer())
       ->withPHIDs($conpherence_pic_phids)
       ->execute();
     $files = mpull($files, null, 'getPHID');
 
     foreach ($conpherence_pic_phids as $conpherence_phid => $pic_phid) {
       $conpherences[$conpherence_phid]->setImage($files[$pic_phid], $size);
     }
 
     return $this;
   }
 
   public function getQueryApplicationClass() {
     return 'PhabricatorConpherenceApplication';
   }
 
 }
diff --git a/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php b/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php
index 2a23d6c073..f2a2d920bd 100644
--- a/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php
+++ b/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php
@@ -1,464 +1,481 @@
 <?php
 
 final class ConpherenceThreadSearchEngine
   extends PhabricatorApplicationSearchEngine {
 
   public function getResultTypeDescription() {
     return pht('Threads');
   }
 
   public function getApplicationClassName() {
     return 'PhabricatorConpherenceApplication';
   }
 
   public function buildSavedQueryFromRequest(AphrontRequest $request) {
     $saved = new PhabricatorSavedQuery();
 
     $saved->setParameter(
       'participantPHIDs',
       $this->readUsersFromRequest($request, 'participants'));
 
     $saved->setParameter('fulltext', $request->getStr('fulltext'));
 
     $saved->setParameter(
       'threadType',
       $request->getStr('threadType'));
 
     return $saved;
   }
 
   public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
     $query = id(new ConpherenceThreadQuery())
       ->needParticipantCache(true);
 
     $participant_phids = $saved->getParameter('participantPHIDs', array());
     if ($participant_phids && is_array($participant_phids)) {
       $query->withParticipantPHIDs($participant_phids);
     }
 
     $fulltext = $saved->getParameter('fulltext');
     if (strlen($fulltext)) {
       $query->withFulltext($fulltext);
     }
 
     $thread_type = $saved->getParameter('threadType');
     if (idx($this->getTypeOptions(), $thread_type)) {
       switch ($thread_type) {
         case 'rooms':
           $query->withIsRoom(true);
           break;
         case 'messages':
           $query->withIsRoom(false);
           break;
         case 'both':
           $query->withIsRoom(null);
           break;
       }
     }
 
     return $query;
   }
 
   public function buildSearchForm(
     AphrontFormView $form,
     PhabricatorSavedQuery $saved) {
 
     $participant_phids = $saved->getParameter('participantPHIDs', array());
     $fulltext = $saved->getParameter('fulltext');
 
     $form
       ->appendControl(
         id(new AphrontFormTokenizerControl())
           ->setDatasource(new PhabricatorPeopleDatasource())
           ->setName('participants')
           ->setLabel(pht('Participants'))
           ->setValue($participant_phids))
       ->appendControl(
         id(new AphrontFormTextControl())
           ->setName('fulltext')
           ->setLabel(pht('Contains Words'))
           ->setValue($fulltext))
       ->appendControl(
         id(new AphrontFormSelectControl())
         ->setLabel(pht('Type'))
         ->setName('threadType')
         ->setOptions($this->getTypeOptions())
         ->setValue($saved->getParameter('threadType')));
   }
 
   protected function getURI($path) {
     return '/conpherence/search/'.$path;
   }
 
   protected function getBuiltinQueryNames() {
     $names = array();
 
     $names = array(
       'all' => pht('All Rooms'),
     );
 
     if ($this->requireViewer()->isLoggedIn()) {
       $names['participant'] = pht('Joined Rooms');
       $names['messages'] = pht('All Messages');
     }
 
     return $names;
   }
 
   public function buildSavedQueryFromBuiltin($query_key) {
 
     $query = $this->newSavedQuery();
     $query->setQueryKey($query_key);
 
     switch ($query_key) {
       case 'all':
         $query->setParameter('threadType', 'rooms');
         return $query;
       case 'participant':
         $query->setParameter('threadType', 'rooms');
         return $query->setParameter(
           'participantPHIDs',
           array($this->requireViewer()->getPHID()));
       case 'messages':
         $query->setParameter('threadType', 'messages');
         return $query->setParameter(
           'participantPHIDs',
           array($this->requireViewer()->getPHID()));
     }
 
     return parent::buildSavedQueryFromBuiltin($query_key);
   }
 
   protected function getRequiredHandlePHIDsForResultList(
     array $conpherences,
     PhabricatorSavedQuery $query) {
 
     $recent = mpull($conpherences, 'getRecentParticipantPHIDs');
     return array_unique(array_mergev($recent));
   }
 
   protected function renderResultList(
     array $conpherences,
     PhabricatorSavedQuery $query,
     array $handles) {
     assert_instances_of($conpherences, 'ConpherenceThread');
 
     $viewer = $this->requireViewer();
 
     $policy_objects = ConpherenceThread::loadPolicyObjects(
       $viewer,
       $conpherences);
 
+    $engines = array();
+
     $fulltext = $query->getParameter('fulltext');
     if (strlen($fulltext) && $conpherences) {
       $context = $this->loadContextMessages($conpherences, $fulltext);
 
       $author_phids = array();
-      foreach ($context as $messages) {
+      foreach ($context as $phid => $messages) {
+        $conpherence = $conpherences[$phid];
+
+        $engine = id(new PhabricatorMarkupEngine())
+          ->setViewer($viewer)
+          ->setContextObject($conpherence);
+
         foreach ($messages as $group) {
           foreach ($group as $message) {
             $xaction = $message['xaction'];
             if ($xaction) {
               $author_phids[] = $xaction->getAuthorPHID();
+              $engine->addObject(
+                $xaction->getComment(),
+                PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT);
             }
           }
         }
+        $engine->process();
+
+        $engines[$phid] = $engine;
       }
 
       $handles = $viewer->loadHandles($author_phids);
+      $handles = iterator_to_array($handles);
     } else {
       $context = array();
     }
 
     $list = new PHUIObjectItemListView();
     $list->setUser($viewer);
-    foreach ($conpherences as $conpherence) {
+    foreach ($conpherences as $conpherence_phid => $conpherence) {
       $created = phabricator_date($conpherence->getDateCreated(), $viewer);
       $title = $conpherence->getDisplayTitle($viewer);
+      $monogram = $conpherence->getMonogram();
 
       if ($conpherence->getIsRoom()) {
         $icon_name = $conpherence->getPolicyIconName($policy_objects);
       } else {
         $icon_name = 'fa-envelope-o';
       }
       $icon = id(new PHUIIconView())
         ->setIconFont($icon_name);
       $item = id(new PHUIObjectItemView())
         ->setObjectName($conpherence->getMonogram())
         ->setHeader($title)
-        ->setHref('/conpherence/'.$conpherence->getID().'/')
+        ->setHref('/'.$conpherence->getMonogram())
         ->setObject($conpherence)
         ->addIcon('none', $created)
         ->addIcon(
           'none',
           pht('Messages: %d', $conpherence->getMessageCount()))
         ->addAttribute(
           array(
             $icon,
             ' ',
             pht(
               'Last updated %s',
               phabricator_datetime($conpherence->getDateModified(), $viewer)),
           ));
 
-      $messages = idx($context, $conpherence->getPHID());
+      $messages = idx($context, $conpherence_phid);
       if ($messages) {
-
-        // TODO: This is egregiously under-designed.
-
         foreach ($messages as $group) {
           $rows = array();
-          $rowc = array();
           foreach ($group as $message) {
             $xaction = $message['xaction'];
             if (!$xaction) {
               continue;
             }
 
-            $rowc[] = ($message['match'] ? 'highlighted' : null);
-            $rows[] = array(
-              $handles->renderHandle($xaction->getAuthorPHID()),
-              $xaction->getComment()->getContent(),
-              phabricator_datetime($xaction->getDateCreated(), $viewer),
-            );
+            $history_href = '/'.$monogram.'#'.$xaction->getID();
+
+            $view = id(new ConpherenceTransactionView())
+              ->setUser($viewer)
+              ->setHandles($handles)
+              ->setMarkupEngine($engines[$conpherence_phid])
+              ->setConpherenceThread($conpherence)
+              ->setConpherenceTransaction($xaction)
+              ->setEpoch($xaction->getDateCreated(), $history_href)
+              ->addClass('conpherence-fulltext-result');
+
+            if ($message['match']) {
+              $view->addClass('conpherence-fulltext-match');
+            }
+
+            $rows[] = $view;
           }
-          $table = id(new AphrontTableView($rows))
-            ->setHeaders(
-              array(
-                pht('User'),
-                pht('Message'),
-                pht('At'),
-              ))
-            ->setRowClasses($rowc)
-            ->setColumnClasses(
-              array(
-                '',
-                'wide',
-              ));
+
           $box = id(new PHUIBoxView())
-            ->appendChild($table)
-            ->addMargin(PHUI::MARGIN_SMALL);
+            ->appendChild($rows)
+            ->addClass('conpherence-fulltext-results');
           $item->appendChild($box);
         }
       }
 
       $list->addItem($item);
     }
 
     return $list;
   }
 
   private function getTypeOptions() {
     return array(
       'rooms' => pht('Rooms'),
       'messages' => pht('Messages'),
       'both' => pht('Both'),
     );
   }
 
   private function loadContextMessages(array $threads, $fulltext) {
     $phids = mpull($threads, 'getPHID');
 
     // We want to load a few messages for each thread in the result list, to
     // show some of the actual content hits to help the user find what they
     // are looking for.
 
     // This method is trying to batch this lookup in most cases, so we do
     // between one and "a handful" of queries instead of one per thread in
     // most cases. To do this:
     //
     //   - Load a big block of results for all of the threads.
     //   - If we didn't get a full block back, we have everything that matches
     //     the query. Sort it out and exit.
     //   - Otherwise, some threads had a ton of hits, so we might not be
     //     getting everything we want (we could be getting back 1,000 hits for
     //     the first thread). Remove any threads which we have enough results
     //     for and try again.
     //   - Repeat until we have everything or every thread has enough results.
     //
     // In the worst case, we could end up degrading to one query per thread,
     // but this is incredibly unlikely on real data.
 
     // Size of the result blocks we're going to load.
     $limit = 1000;
 
     // Number of messages we want for each thread.
     $want = 3;
 
     $need = $phids;
     $hits = array();
     while ($need) {
       $rows = id(new ConpherenceFulltextQuery())
         ->withThreadPHIDs($need)
         ->withFulltext($fulltext)
         ->setLimit($limit)
         ->execute();
 
       foreach ($rows as $row) {
         $hits[$row['threadPHID']][] = $row;
       }
 
       if (count($rows) < $limit) {
         break;
       }
 
       foreach ($need as $key => $phid) {
         if (count($hits[$phid]) >= $want) {
           unset($need[$key]);
         }
       }
     }
 
     // Now that we have all the fulltext matches, throw away any extras that we
     // aren't going to render so we don't need to do lookups on them.
     foreach ($hits as $phid => $rows) {
       if (count($rows) > $want) {
         $hits[$phid] = array_slice($rows, 0, $want);
       }
     }
 
     // For each fulltext match, we want to render a message before and after
     // the match to give it some context. We already know the transactions
     // before each match because the rows have a "previousTransactionPHID",
     // but we need to do one more query to figure out the transactions after
     // each match.
 
     // Collect the transactions we want to find the next transactions for.
     $after = array();
     foreach ($hits as $phid => $rows) {
       foreach ($rows as $row) {
         $after[] = $row['transactionPHID'];
       }
     }
 
     // Look up the next transactions.
     if ($after) {
       $after_rows = id(new ConpherenceFulltextQuery())
         ->withPreviousTransactionPHIDs($after)
         ->execute();
     } else {
       $after_rows = array();
     }
 
     // Build maps from PHIDs to the previous and next PHIDs.
     $prev_map = array();
     $next_map = array();
     foreach ($after_rows as $row) {
       $next_map[$row['previousTransactionPHID']] = $row['transactionPHID'];
     }
 
     foreach ($hits as $phid => $rows) {
       foreach ($rows as $row) {
         $prev = $row['previousTransactionPHID'];
         if ($prev) {
           $prev_map[$row['transactionPHID']] = $prev;
           $next_map[$prev] = $row['transactionPHID'];
         }
       }
     }
 
     // Now we're going to collect the actual transaction PHIDs, in order, that
     // we want to show for each thread.
     $groups = array();
     foreach ($hits as $thread_phid => $rows) {
       $rows = ipull($rows, null, 'transactionPHID');
+      $done = array();
       foreach ($rows as $phid => $row) {
-        unset($rows[$phid]);
+        if (isset($done[$phid])) {
+          continue;
+        }
+        $done[$phid] = true;
 
         $group = array();
 
         // Walk backward, finding all the previous results. We can just keep
         // going until we run out of results because we've only loaded things
         // that we want to show.
         $prev = $phid;
         while (true) {
           if (!isset($prev_map[$prev])) {
             // No previous transaction, so we're done.
             break;
           }
 
           $prev = $prev_map[$prev];
 
           if (isset($rows[$prev])) {
             $match = true;
-            unset($rows[$prev]);
+            $done[$prev] = true;
           } else {
             $match = false;
           }
 
           $group[] = array(
             'phid' => $prev,
             'match' => $match,
           );
         }
 
         if (count($group) > 1) {
           $group = array_reverse($group);
         }
 
         $group[] = array(
           'phid' => $phid,
           'match' => true,
         );
 
         $next = $phid;
         while (true) {
           if (!isset($next_map[$next])) {
             break;
           }
 
           $next = $next_map[$next];
 
           if (isset($rows[$next])) {
             $match = true;
-            unset($rows[$next]);
+            $done[$next] = true;
           } else {
             $match = false;
           }
 
           $group[] = array(
             'phid' => $next,
             'match' => $match,
           );
         }
 
         $groups[$thread_phid][] = $group;
       }
     }
 
     // Load all the actual transactions we need.
     $xaction_phids = array();
     foreach ($groups as $thread_phid => $group) {
       foreach ($group as $list) {
         foreach ($list as $item) {
           $xaction_phids[] = $item['phid'];
         }
       }
     }
 
     if ($xaction_phids) {
       $xactions = id(new ConpherenceTransactionQuery())
         ->setViewer($this->requireViewer())
         ->withPHIDs($xaction_phids)
         ->needComments(true)
         ->execute();
       $xactions = mpull($xactions, null, 'getPHID');
     } else {
       $xactions = array();
     }
 
     foreach ($groups as $thread_phid => $group) {
       foreach ($group as $key => $list) {
         foreach ($list as $lkey => $item) {
           $xaction = idx($xactions, $item['phid']);
+          if ($xaction->shouldHide()) {
+            continue;
+          }
           $groups[$thread_phid][$key][$lkey]['xaction'] = $xaction;
         }
       }
     }
 
     // TODO: Sort the groups chronologically?
 
     return $groups;
   }
 
 }
diff --git a/src/applications/conpherence/storage/ConpherenceThread.php b/src/applications/conpherence/storage/ConpherenceThread.php
index 03f8f6997e..8ed0e4fd18 100644
--- a/src/applications/conpherence/storage/ConpherenceThread.php
+++ b/src/applications/conpherence/storage/ConpherenceThread.php
@@ -1,484 +1,484 @@
 <?php
 
 final class ConpherenceThread extends ConpherenceDAO
   implements
     PhabricatorPolicyInterface,
     PhabricatorApplicationTransactionInterface,
     PhabricatorMentionableInterface,
     PhabricatorDestructibleInterface {
 
   protected $title;
   protected $imagePHIDs = array();
   protected $isRoom = 0;
   protected $messageCount;
   protected $recentParticipantPHIDs = array();
   protected $mailKey;
   protected $viewPolicy;
   protected $editPolicy;
   protected $joinPolicy;
 
   private $participants = self::ATTACHABLE;
   private $transactions = self::ATTACHABLE;
   private $handles = self::ATTACHABLE;
   private $filePHIDs = self::ATTACHABLE;
   private $widgetData = self::ATTACHABLE;
   private $images = self::ATTACHABLE;
 
   public static function initializeNewThread(PhabricatorUser $sender) {
     return id(new ConpherenceThread())
       ->setMessageCount(0)
       ->setTitle('')
       ->attachParticipants(array())
       ->attachFilePHIDs(array())
       ->attachImages(array())
       ->setViewPolicy(PhabricatorPolicies::POLICY_USER)
       ->setEditPolicy(PhabricatorPolicies::POLICY_USER)
       ->setJoinPolicy(PhabricatorPolicies::POLICY_USER);
   }
 
   public static function initializeNewRoom(PhabricatorUser $creator) {
 
     return id(new ConpherenceThread())
       ->setIsRoom(1)
       ->setMessageCount(0)
       ->setTitle('')
       ->attachParticipants(array())
       ->attachFilePHIDs(array())
       ->attachImages(array())
       ->setViewPolicy(PhabricatorPolicies::POLICY_USER)
       ->setEditPolicy($creator->getPHID())
       ->setJoinPolicy(PhabricatorPolicies::POLICY_USER);
   }
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_SERIALIZATION => array(
         'recentParticipantPHIDs' => self::SERIALIZATION_JSON,
         'imagePHIDs' => self::SERIALIZATION_JSON,
       ),
       self::CONFIG_COLUMN_SCHEMA => array(
         'title' => 'text255?',
         'isRoom' => 'bool',
         'messageCount' => 'uint64',
         'mailKey' => 'text20',
         'joinPolicy' => 'policy',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_room' => array(
           'columns' => array('isRoom', 'dateModified'),
         ),
         'key_phid' => null,
         'phid' => array(
           'columns' => array('phid'),
           'unique' => true,
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       PhabricatorConpherenceThreadPHIDType::TYPECONST);
   }
 
   public function save() {
     if (!$this->getMailKey()) {
       $this->setMailKey(Filesystem::readRandomCharacters(20));
     }
     return parent::save();
   }
 
   public function getMonogram() {
     return 'Z'.$this->getID();
   }
 
   public function getImagePHID($size) {
     $image_phids = $this->getImagePHIDs();
     return idx($image_phids, $size);
   }
   public function setImagePHID($phid, $size) {
     $image_phids = $this->getImagePHIDs();
     $image_phids[$size] = $phid;
     return $this->setImagePHIDs($image_phids);
   }
 
   public function getImage($size) {
     $images = $this->getImages();
     return idx($images, $size);
   }
   public function setImage(PhabricatorFile $file, $size) {
     $files = $this->getImages();
     $files[$size] = $file;
     return $this->attachImages($files);
   }
   public function attachImages(array $files) {
     assert_instances_of($files, 'PhabricatorFile');
     $this->images = $files;
     return $this;
   }
   private function getImages() {
     return $this->assertAttached($this->images);
   }
 
   public function attachParticipants(array $participants) {
     assert_instances_of($participants, 'ConpherenceParticipant');
     $this->participants = $participants;
     return $this;
   }
   public function getParticipants() {
     return $this->assertAttached($this->participants);
   }
   public function getParticipant($phid) {
     $participants = $this->getParticipants();
     return $participants[$phid];
   }
   public function getParticipantIfExists($phid, $default = null) {
     $participants = $this->getParticipants();
     return idx($participants, $phid, $default);
   }
 
   public function getParticipantPHIDs() {
     $participants = $this->getParticipants();
     return array_keys($participants);
   }
 
   public function attachHandles(array $handles) {
     assert_instances_of($handles, 'PhabricatorObjectHandle');
     $this->handles = $handles;
     return $this;
   }
   public function getHandles() {
     return $this->assertAttached($this->handles);
   }
 
   public function attachTransactions(array $transactions) {
     assert_instances_of($transactions, 'ConpherenceTransaction');
     $this->transactions = $transactions;
     return $this;
   }
   public function getTransactions($assert_attached = true) {
     return $this->assertAttached($this->transactions);
   }
   public function hasAttachedTransactions() {
     return $this->transactions !== self::ATTACHABLE;
   }
 
   public function getTransactionsFrom($begin = 0, $amount = null) {
     $length = count($this->transactions);
 
     return array_slice(
       $this->getTransactions(),
       $length - $begin - $amount,
       $amount);
   }
 
   public function attachFilePHIDs(array $file_phids) {
     $this->filePHIDs = $file_phids;
     return $this;
   }
   public function getFilePHIDs() {
     return $this->assertAttached($this->filePHIDs);
   }
 
   public function attachWidgetData(array $widget_data) {
     $this->widgetData = $widget_data;
     return $this;
   }
   public function getWidgetData() {
     return $this->assertAttached($this->widgetData);
   }
 
   public function loadImageURI($size) {
     $file = $this->getImage($size);
 
     if ($file) {
       return $file->getBestURI();
     }
 
     return PhabricatorUser::getDefaultProfileImageURI();
   }
 
 
   /**
    * Get the thread's display title for a user.
    *
    * If a thread doesn't have a title set, this will return a string describing
    * recent participants.
    *
    * @param PhabricatorUser Viewer.
    * @return string Thread title.
    */
   public function getDisplayTitle(PhabricatorUser $viewer) {
     $title = $this->getTitle();
     if (strlen($title)) {
       return $title;
     }
 
     return $this->getRecentParticipantsString($viewer);
   }
 
 
   /**
    * Get recent participants (other than the viewer) as a string.
    *
    * For example, this method might return "alincoln, htaft, gwashington...".
    *
    * @param PhabricatorUser Viewer.
    * @return string Description of other participants.
    */
   private function getRecentParticipantsString(PhabricatorUser $viewer) {
     $handles = $this->getHandles();
     $phids = $this->getOtherRecentParticipantPHIDs($viewer);
 
     $limit = 3;
     $more = (count($phids) > $limit);
     $phids = array_slice($phids, 0, $limit);
 
     $names = array_select_keys($handles, $phids);
     $names = mpull($names, 'getName');
     $names = implode(', ', $names);
 
     if ($more) {
       $names = $names.'...';
     }
 
     return $names;
   }
 
 
   /**
    * Get PHIDs for recent participants who are not the viewer.
    *
    * @param PhabricatorUser Viewer.
    * @return list<phid> Participants who are not the viewer.
    */
   private function getOtherRecentParticipantPHIDs(PhabricatorUser $viewer) {
     $phids = $this->getRecentParticipantPHIDs();
     $phids = array_fuse($phids);
     unset($phids[$viewer->getPHID()]);
     return array_values($phids);
   }
 
 
   public function getDisplayData(PhabricatorUser $viewer) {
     $handles = $this->getHandles();
 
     if ($this->hasAttachedTransactions()) {
       $transactions = $this->getTransactions();
     } else {
       $transactions = array();
     }
 
     if ($transactions) {
       $subtitle_mode = 'message';
     } else {
       $subtitle_mode = 'recent';
     }
 
     $lucky_phid = head($this->getOtherRecentParticipantPHIDs($viewer));
     if ($lucky_phid) {
       $lucky_handle = $handles[$lucky_phid];
     } else {
       // This will be just the user talking to themselves. Weirdo.
       $lucky_handle = reset($handles);
     }
 
     $img_src = null;
     $size = ConpherenceImageData::SIZE_CROP;
     if ($this->getImagePHID($size)) {
       $img_src = $this->getImage($size)->getBestURI();
     } else if ($lucky_handle) {
       $img_src = $lucky_handle->getImageURI();
     }
 
     $message_title = null;
     if ($subtitle_mode == 'message') {
       $message_transaction = null;
       foreach ($transactions as $transaction) {
         switch ($transaction->getTransactionType()) {
           case PhabricatorTransactions::TYPE_COMMENT:
             $message_transaction = $transaction;
             break 2;
           default:
             break;
         }
       }
       if ($message_transaction) {
         $message_handle = $handles[$message_transaction->getAuthorPHID()];
         $message_title = sprintf(
           '%s: %s',
           $message_handle->getName(),
           id(new PhutilUTF8StringTruncator())
             ->setMaximumGlyphs(60)
             ->truncateString(
               $message_transaction->getComment()->getContent()));
       }
     }
     switch ($subtitle_mode) {
       case 'recent':
         $subtitle = $this->getRecentParticipantsString($viewer);
         break;
       case 'message':
         if ($message_title) {
           $subtitle = $message_title;
         } else {
           $subtitle = $this->getRecentParticipantsString($viewer);
         }
         break;
     }
 
     $user_participation = $this->getParticipantIfExists($viewer->getPHID());
     if ($user_participation) {
       $user_seen_count = $user_participation->getSeenMessageCount();
     } else {
       $user_seen_count = 0;
     }
     $unread_count = $this->getMessageCount() - $user_seen_count;
 
     $title = $this->getDisplayTitle($viewer);
 
     return array(
       'title' => $title,
       'subtitle' => $subtitle,
       'unread_count' => $unread_count,
       'epoch' => $this->getDateModified(),
       'image' => $img_src,
     );
   }
 
 
 /* -(  PhabricatorPolicyInterface Implementation  )-------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
       PhabricatorPolicyCapability::CAN_EDIT,
       PhabricatorPolicyCapability::CAN_JOIN,
     );
   }
 
   public function getPolicy($capability) {
     if ($this->getIsRoom()) {
       switch ($capability) {
         case PhabricatorPolicyCapability::CAN_VIEW:
           return $this->getViewPolicy();
         case PhabricatorPolicyCapability::CAN_EDIT:
           return $this->getEditPolicy();
         case PhabricatorPolicyCapability::CAN_JOIN:
           return $this->getJoinPolicy();
       }
     }
     return PhabricatorPolicies::POLICY_NOONE;
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $user) {
     // this bad boy isn't even created yet so go nuts $user
     if (!$this->getID()) {
       return true;
     }
 
     if ($this->getIsRoom()) {
       switch ($capability) {
         case PhabricatorPolicyCapability::CAN_EDIT:
         case PhabricatorPolicyCapability::CAN_JOIN:
           return false;
       }
     }
 
     $participants = $this->getParticipants();
     return isset($participants[$user->getPHID()]);
   }
 
   public function describeAutomaticCapability($capability) {
     if ($this->getIsRoom()) {
       switch ($capability) {
         case PhabricatorPolicyCapability::CAN_VIEW:
           return pht('Participants in a room can always view it.');
           break;
       }
     } else {
       return pht('Participants in a thread can always view and edit it.');
     }
   }
 
   public static function loadPolicyObjects(
     PhabricatorUser $viewer,
     array $conpherences) {
 
-    assert_instances_of($conpherences, 'ConpherenceThread');
+    assert_instances_of($conpherences, __CLASS__);
 
     $grouped = mgroup($conpherences, 'getIsRoom');
     $rooms = idx($grouped, 1, array());
 
     $policies = array();
     foreach ($rooms as $room) {
       $policies[] = $room->getViewPolicy();
     }
     $policy_objects = array();
     if ($policies) {
       $policy_objects = id(new PhabricatorPolicyQuery())
         ->setViewer($viewer)
         ->withPHIDs($policies)
         ->execute();
     }
 
     return $policy_objects;
   }
 
   public function getPolicyIconName(array $policy_objects) {
     assert_instances_of($policy_objects, 'PhabricatorPolicy');
 
     if ($this->getIsRoom()) {
       $icon = $policy_objects[$this->getViewPolicy()]->getIcon();
     } else if (count($this->getRecentParticipantPHIDs()) > 2) {
       $icon = 'fa-users';
     } else {
       $icon = 'fa-user';
     }
     return $icon;
   }
 
 
 /* -(  PhabricatorApplicationTransactionInterface  )------------------------- */
 
 
   public function getApplicationTransactionEditor() {
     return new ConpherenceEditor();
   }
 
   public function getApplicationTransactionObject() {
     return $this;
   }
 
   public function getApplicationTransactionTemplate() {
     return new ConpherenceTransaction();
   }
 
   public function willRenderTimeline(
     PhabricatorApplicationTransactionView $timeline,
     AphrontRequest $request) {
     return $timeline;
   }
 
 
 /* -(  PhabricatorDestructibleInterface  )----------------------------------- */
 
 
   public function destroyObjectPermanently(
     PhabricatorDestructionEngine $engine) {
 
     $this->openTransaction();
       $this->delete();
 
       $participants = id(new ConpherenceParticipant())
         ->loadAllWhere('conpherencePHID = %s', $this->getPHID());
       foreach ($participants as $participant) {
         $participant->delete();
       }
 
     $this->saveTransaction();
 
   }
 }
diff --git a/src/applications/conpherence/view/ConpherenceTransactionView.php b/src/applications/conpherence/view/ConpherenceTransactionView.php
index 3ed5fa14eb..2dba770e54 100644
--- a/src/applications/conpherence/view/ConpherenceTransactionView.php
+++ b/src/applications/conpherence/view/ConpherenceTransactionView.php
@@ -1,293 +1,301 @@
 <?php
 
 final class ConpherenceTransactionView extends AphrontView {
 
   private $conpherenceThread;
   private $conpherenceTransaction;
   private $handles;
   private $markupEngine;
   private $epoch;
   private $epochHref;
   private $contentSource;
   private $anchorName;
   private $anchorText;
   private $classes = array();
   private $timeOnly;
   private $showImages = true;
 
   public function setConpherenceThread(ConpherenceThread $t) {
     $this->conpherenceThread = $t;
     return $this;
   }
 
   private function getConpherenceThread() {
     return $this->conpherenceThread;
   }
 
   public function setConpherenceTransaction(ConpherenceTransaction $tx) {
     $this->conpherenceTransaction = $tx;
     return $this;
   }
 
   private function getConpherenceTransaction() {
     return $this->conpherenceTransaction;
   }
 
   public function setHandles(array $handles) {
     assert_instances_of($handles, 'PhabricatorObjectHandle');
     $this->handles = $handles;
     return $this;
   }
 
   public function getHandles() {
     return $this->handles;
   }
 
   public function setMarkupEngine(PhabricatorMarkupEngine $markup_engine) {
     $this->markupEngine = $markup_engine;
     return $this;
   }
 
   private function getMarkupEngine() {
     return $this->markupEngine;
   }
 
   public function setEpoch($epoch, $epoch_href = null) {
     $this->epoch = $epoch;
     $this->epochHref = $epoch_href;
     return $this;
   }
 
   public function setContentSource(PhabricatorContentSource $source) {
     $this->contentSource = $source;
     return $this;
   }
 
   private function getContentSource() {
     return $this->contentSource;
   }
 
   public function setAnchor($anchor_name, $anchor_text) {
     $this->anchorName = $anchor_name;
     $this->anchorText = $anchor_text;
     return $this;
   }
 
   public function addClass($class) {
     $this->classes[] = $class;
     return $this;
   }
 
   public function setTimeOnly($time) {
     $this->timeOnly = $time;
     return $this;
   }
 
   public function setShowImages($bool) {
     $this->showImages = $bool;
     return $this;
   }
 
   private function getShowImages() {
     return $this->showImages;
   }
 
   public function render() {
     $user = $this->getUser();
     if (!$user) {
       throw new Exception(pht('Call setUser() before render()!'));
     }
 
     require_celerity_resource('conpherence-transaction-css');
 
     $transaction = $this->getConpherenceTransaction();
     switch ($transaction->getTransactionType()) {
       case ConpherenceTransactionType::TYPE_DATE_MARKER:
-        return phutil_tag(
+        return javelin_tag(
           'div',
           array(
             'class' => 'conpherence-transaction-view date-marker',
+            'sigil' => 'conpherence-transaction-view',
+            'meta' => array(
+              'id' => $transaction->getID() + 0.5,
+            ),
           ),
           array(
             phutil_tag(
               'span',
               array(
                 'class' => 'date',
               ),
               phabricator_format_local_time(
                 $transaction->getDateCreated(),
                 $user,
               'M jS, Y')),
           ));
         break;
     }
 
     $info = $this->renderTransactionInfo();
     $actions = $this->renderTransactionActions();
     $image = $this->renderTransactionImage();
     $content = $this->renderTransactionContent();
     $classes = implode(' ', $this->classes);
 
     $transaction_id = $this->anchorName ? 'anchor-'.$this->anchorName : null;
 
     $header = phutil_tag_div(
       'conpherence-transaction-header grouped',
       array($actions, $info));
 
-    return phutil_tag(
+    return javelin_tag(
       'div',
       array(
         'class' => 'conpherence-transaction-view '.$classes,
         'id'    => $transaction_id,
+        'sigil' => 'conpherence-transaction-view',
+        'meta' => array(
+          'id' => $transaction->getID(),
+        ),
       ),
       array(
         $image,
         phutil_tag_div('conpherence-transaction-detail grouped',
           array($header, $content)),
       ));
   }
 
   private function renderTransactionInfo() {
     $info = array();
 
     if ($this->getContentSource()) {
       $content_source = id(new PhabricatorContentSourceView())
         ->setContentSource($this->getContentSource())
         ->setUser($this->user)
         ->render();
       if ($content_source) {
         $info[] = $content_source;
       }
     }
 
     if ($this->epoch) {
       if ($this->timeOnly) {
         $epoch = phabricator_time($this->epoch, $this->user);
       } else {
         $epoch = phabricator_datetime($this->epoch, $this->user);
       }
       if ($this->epochHref) {
         $epoch = phutil_tag(
           'a',
           array(
             'href' => $this->epochHref,
             'class' => 'epoch-link',
           ),
           $epoch);
       }
       $info[] = $epoch;
     }
 
     if ($this->anchorName) {
       Javelin::initBehavior('phabricator-watch-anchor');
 
       $anchor = id(new PhabricatorAnchorView())
         ->setAnchorName($this->anchorName)
         ->render();
 
       $info[] = hsprintf(
         '%s%s',
         $anchor,
         phutil_tag(
           'a',
           array(
             'href'  => '#'.$this->anchorName,
             'class' => 'anchor-link',
           ),
           $this->anchorText));
     }
 
     $info = phutil_implode_html(" \xC2\xB7 ", $info);
 
     return phutil_tag(
       'span',
       array(
         'class' => 'conpherence-transaction-info',
       ),
       $info);
   }
 
   private function renderTransactionActions() {
     $transaction = $this->getConpherenceTransaction();
 
     switch ($transaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         $handles = $this->getHandles();
         $author = $handles[$transaction->getAuthorPHID()];
         $actions = array($author->renderLink());
         break;
       default:
         $actions = null;
         break;
     }
 
     return $actions;
   }
 
   private function renderTransactionImage() {
     $image = null;
     if ($this->getShowImages()) {
       $transaction = $this->getConpherenceTransaction();
       switch ($transaction->getTransactionType()) {
         case PhabricatorTransactions::TYPE_COMMENT:
           $handles = $this->getHandles();
           $author = $handles[$transaction->getAuthorPHID()];
           $image_uri = $author->getImageURI();
           $image = phutil_tag(
             'span',
             array(
               'class' => 'conpherence-transaction-image',
               'style' => 'background-image: url('.$image_uri.');',
             ));
           break;
       }
     }
     return $image;
   }
 
   private function renderTransactionContent() {
     $transaction = $this->getConpherenceTransaction();
     $content = null;
     $content_class = null;
     $content = null;
     $handles = $this->getHandles();
     switch ($transaction->getTransactionType()) {
       case ConpherenceTransactionType::TYPE_FILES:
         $content = $transaction->getTitle();
         break;
       case ConpherenceTransactionType::TYPE_TITLE:
       case ConpherenceTransactionType::TYPE_PICTURE:
       case ConpherenceTransactionType::TYPE_PICTURE_CROP:
       case ConpherenceTransactionType::TYPE_PARTICIPANTS:
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
       case PhabricatorTransactions::TYPE_EDGE:
         $content = $transaction->getTitle();
         $this->addClass('conpherence-edited');
         break;
       case PhabricatorTransactions::TYPE_COMMENT:
         $this->addClass('conpherence-comment');
         $author = $handles[$transaction->getAuthorPHID()];
         $comment = $transaction->getComment();
         $content = $this->getMarkupEngine()->getOutput(
           $comment,
           PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT);
         $content_class = 'conpherence-message';
        break;
     }
 
     $this->appendChild(
       phutil_tag(
         'div',
         array(
           'class' => $content_class,
         ),
         $content));
 
     return phutil_tag_div(
       'conpherence-transaction-content',
       $this->renderChildren());
   }
 
 }
diff --git a/src/applications/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php b/src/applications/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php
index 0708c98eec..5d9723cc4b 100644
--- a/src/applications/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php
+++ b/src/applications/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php
@@ -1,75 +1,75 @@
 <?php
 
 final class DarkConsoleErrorLogPluginAPI {
 
   private static $errors = array();
 
   private static $discardMode = false;
 
   public static function registerErrorHandler() {
     // NOTE: This forces PhutilReadableSerializer to load, so that we are
     // able to handle errors which fire from inside autoloaders (PHP will not
     // reenter autoloaders).
     PhutilReadableSerializer::printableValue(null);
     PhutilErrorHandler::setErrorListener(
-      array('DarkConsoleErrorLogPluginAPI', 'handleErrors'));
+      array(__CLASS__, 'handleErrors'));
   }
 
   public static function enableDiscardMode() {
     self::$discardMode = true;
   }
 
   public static function disableDiscardMode() {
     self::$discardMode = false;
   }
 
   public static function getErrors() {
     return self::$errors;
   }
 
   public static function handleErrors($event, $value, $metadata) {
     if (self::$discardMode) {
       return;
     }
 
     switch ($event) {
       case PhutilErrorHandler::EXCEPTION:
         // $value is of type Exception
         self::$errors[] = array(
           'details'   => $value->getMessage(),
           'event'     => $event,
           'file'      => $value->getFile(),
           'line'      => $value->getLine(),
           'str'       => $value->getMessage(),
           'trace'     => $metadata['trace'],
         );
         break;
       case PhutilErrorHandler::ERROR:
         // $value is a simple string
         self::$errors[] = array(
           'details'   => $value,
           'event'     => $event,
           'file'      => $metadata['file'],
           'line'      => $metadata['line'],
           'str'       => $value,
           'trace'     => $metadata['trace'],
         );
         break;
       case PhutilErrorHandler::PHLOG:
         // $value can be anything
         self::$errors[] = array(
           'details' => PhutilReadableSerializer::printShallow($value, 3),
           'event'   => $event,
           'file'    => $metadata['file'],
           'line'    => $metadata['line'],
           'str'     => PhutilReadableSerializer::printShort($value),
           'trace'   => $metadata['trace'],
         );
         break;
       default:
         error_log('Unknown event : '.$event);
         break;
     }
   }
 
 }
diff --git a/src/applications/daemon/query/PhabricatorDaemonLogQuery.php b/src/applications/daemon/query/PhabricatorDaemonLogQuery.php
index 5822194d06..961c1cfc61 100644
--- a/src/applications/daemon/query/PhabricatorDaemonLogQuery.php
+++ b/src/applications/daemon/query/PhabricatorDaemonLogQuery.php
@@ -1,194 +1,194 @@
 <?php
 
 final class PhabricatorDaemonLogQuery
   extends PhabricatorCursorPagedPolicyAwareQuery {
 
   const STATUS_ALL = 'status-all';
   const STATUS_ALIVE = 'status-alive';
   const STATUS_RUNNING = 'status-running';
 
   private $ids;
   private $notIDs;
   private $status = self::STATUS_ALL;
   private $daemonClasses;
   private $allowStatusWrites;
   private $daemonIDs;
 
   public static function getTimeUntilUnknown() {
     return 3 * PhutilDaemonHandle::getHeartbeatEventFrequency();
   }
 
   public static function getTimeUntilDead() {
     return 30 * PhutilDaemonHandle::getHeartbeatEventFrequency();
   }
 
   public function withIDs(array $ids) {
     $this->ids = $ids;
     return $this;
   }
 
   public function withoutIDs(array $ids) {
     $this->notIDs = $ids;
     return $this;
   }
 
   public function withStatus($status) {
     $this->status = $status;
     return $this;
   }
 
   public function withDaemonClasses(array $classes) {
     $this->daemonClasses = $classes;
     return $this;
   }
 
   public function setAllowStatusWrites($allow) {
     $this->allowStatusWrites = $allow;
     return $this;
   }
 
   public function withDaemonIDs(array $daemon_ids) {
     $this->daemonIDs = $daemon_ids;
     return $this;
   }
 
   protected function loadPage() {
     $table = new PhabricatorDaemonLog();
     $conn_r = $table->establishConnection('r');
 
     $data = queryfx_all(
       $conn_r,
       'SELECT * FROM %T %Q %Q %Q',
       $table->getTableName(),
       $this->buildWhereClause($conn_r),
       $this->buildOrderClause($conn_r),
       $this->buildLimitClause($conn_r));
 
     return $table->loadAllFromArray($data);
   }
 
   protected function willFilterPage(array $daemons) {
-    $unknown_delay = PhabricatorDaemonLogQuery::getTimeUntilUnknown();
-    $dead_delay = PhabricatorDaemonLogQuery::getTimeUntilDead();
+    $unknown_delay = self::getTimeUntilUnknown();
+    $dead_delay = self::getTimeUntilDead();
 
     $status_running = PhabricatorDaemonLog::STATUS_RUNNING;
     $status_unknown = PhabricatorDaemonLog::STATUS_UNKNOWN;
     $status_wait = PhabricatorDaemonLog::STATUS_WAIT;
     $status_exiting = PhabricatorDaemonLog::STATUS_EXITING;
     $status_exited = PhabricatorDaemonLog::STATUS_EXITED;
     $status_dead = PhabricatorDaemonLog::STATUS_DEAD;
 
     $filter = array_fuse($this->getStatusConstants());
 
     foreach ($daemons as $key => $daemon) {
       $status = $daemon->getStatus();
       $seen = $daemon->getDateModified();
 
       $is_running = ($status == $status_running) ||
                     ($status == $status_wait) ||
                     ($status == $status_exiting);
 
       // If we haven't seen the daemon recently, downgrade its status to
       // unknown.
       $unknown_time = ($seen + $unknown_delay);
       if ($is_running && ($unknown_time < time())) {
         $status = $status_unknown;
       }
 
       // If the daemon hasn't been seen in quite a while, assume it is dead.
       $dead_time = ($seen + $dead_delay);
       if (($status == $status_unknown) && ($dead_time < time())) {
         $status = $status_dead;
       }
 
       // If we changed the daemon's status, adjust it.
       if ($status != $daemon->getStatus()) {
         $daemon->setStatus($status);
 
         // ...and write it, if we're in a context where that's reasonable.
         if ($this->allowStatusWrites) {
           $guard = AphrontWriteGuard::beginScopedUnguardedWrites();
             $daemon->save();
           unset($guard);
         }
       }
 
       // If the daemon no longer matches the filter, get rid of it.
       if ($filter) {
         if (empty($filter[$daemon->getStatus()])) {
           unset($daemons[$key]);
         }
       }
     }
 
     return $daemons;
   }
 
   protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
     $where = array();
 
     if ($this->ids !== null) {
       $where[] = qsprintf(
         $conn_r,
         'id IN (%Ld)',
         $this->ids);
     }
 
     if ($this->notIDs !== null) {
       $where[] = qsprintf(
         $conn_r,
         'id NOT IN (%Ld)',
         $this->notIDs);
     }
 
     if ($this->getStatusConstants()) {
       $where[] = qsprintf(
         $conn_r,
         'status IN (%Ls)',
         $this->getStatusConstants());
     }
 
     if ($this->daemonClasses !== null) {
       $where[] = qsprintf(
         $conn_r,
         'daemon IN (%Ls)',
         $this->daemonClasses);
     }
 
     if ($this->daemonIDs !== null) {
       $where[] = qsprintf(
         $conn_r,
         'daemonID IN (%Ls)',
         $this->daemonIDs);
     }
 
     $where[] = $this->buildPagingClause($conn_r);
     return $this->formatWhereClause($where);
   }
 
   private function getStatusConstants() {
     $status = $this->status;
     switch ($status) {
       case self::STATUS_ALL:
         return array();
       case self::STATUS_RUNNING:
         return array(
           PhabricatorDaemonLog::STATUS_RUNNING,
         );
       case self::STATUS_ALIVE:
         return array(
           PhabricatorDaemonLog::STATUS_UNKNOWN,
           PhabricatorDaemonLog::STATUS_RUNNING,
           PhabricatorDaemonLog::STATUS_WAIT,
           PhabricatorDaemonLog::STATUS_EXITING,
         );
       default:
         throw new Exception(pht('Unknown status "%s"!', $status));
     }
   }
 
   public function getQueryApplicationClass() {
     return 'PhabricatorDaemonsApplication';
   }
 
 }
diff --git a/src/applications/daemon/view/PhabricatorDaemonLogEventsView.php b/src/applications/daemon/view/PhabricatorDaemonLogEventsView.php
index a3598bfe25..b06ebdd7d2 100644
--- a/src/applications/daemon/view/PhabricatorDaemonLogEventsView.php
+++ b/src/applications/daemon/view/PhabricatorDaemonLogEventsView.php
@@ -1,134 +1,134 @@
 <?php
 
 final class PhabricatorDaemonLogEventsView extends AphrontView {
 
   private $events;
   private $combinedLog;
   private $showFullMessage;
 
 
   public function setShowFullMessage($show_full_message) {
     $this->showFullMessage = $show_full_message;
     return $this;
   }
 
   public function setEvents(array $events) {
     assert_instances_of($events, 'PhabricatorDaemonLogEvent');
     $this->events = $events;
     return $this;
   }
 
   public function setCombinedLog($is_combined) {
     $this->combinedLog = $is_combined;
     return $this;
   }
 
   public function render() {
     $rows = array();
 
     if (!$this->user) {
-      throw new Exception('Call setUser() before rendering!');
+      throw new PhutilInvalidStateException('setUser');
     }
 
     foreach ($this->events as $event) {
 
       // Limit display log size. If a daemon gets stuck in an output loop this
       // page can be like >100MB if we don't truncate stuff. Try to do cheap
       // line-based truncation first, and fall back to expensive UTF-8 character
       // truncation if that doesn't get things short enough.
 
       $message = $event->getMessage();
       $more = null;
 
       if (!$this->showFullMessage) {
         $more_lines = null;
         $more_chars = null;
         $line_limit = 12;
         if (substr_count($message, "\n") > $line_limit) {
           $message = explode("\n", $message);
           $more_lines = count($message) - $line_limit;
           $message = array_slice($message, 0, $line_limit);
           $message = implode("\n", $message);
         }
 
         $char_limit = 8192;
         if (strlen($message) > $char_limit) {
           $message = phutil_utf8v($message);
           $more_chars = count($message) - $char_limit;
           $message = array_slice($message, 0, $char_limit);
           $message = implode('', $message);
         }
 
         if ($more_chars) {
           $more = new PhutilNumber($more_chars);
           $more = pht('Show %d more character(s)...', $more);
         } else if ($more_lines) {
           $more = new PhutilNumber($more_lines);
           $more = pht('Show %d more line(s)...', $more);
         }
 
         if ($more) {
           $id = $event->getID();
           $more = array(
             "\n...\n",
             phutil_tag(
               'a',
               array(
                 'href' => "/daemon/event/{$id}/",
               ),
               $more),
           );
         }
       }
 
       $row = array(
         $event->getLogType(),
         phabricator_date($event->getEpoch(), $this->user),
         phabricator_time($event->getEpoch(), $this->user),
         array(
           $message,
           $more,
         ),
       );
 
       if ($this->combinedLog) {
         array_unshift(
           $row,
           phutil_tag(
             'a',
             array(
               'href' => '/daemon/log/'.$event->getLogID().'/',
             ),
             'Daemon '.$event->getLogID()));
       }
 
       $rows[] = $row;
     }
 
     $classes = array(
       '',
       '',
       'right',
       'wide prewrap',
     );
 
     $headers = array(
       'Type',
       'Date',
       'Time',
       'Message',
     );
 
     if ($this->combinedLog) {
       array_unshift($classes, 'pri');
       array_unshift($headers, 'Daemon');
     }
 
     $log_table = new AphrontTableView($rows);
     $log_table->setHeaders($headers);
     $log_table->setColumnClasses($classes);
 
     return $log_table->render();
   }
 
 }
diff --git a/src/applications/daemon/view/PhabricatorDaemonLogListView.php b/src/applications/daemon/view/PhabricatorDaemonLogListView.php
index 88a92ced98..f45606e9e7 100644
--- a/src/applications/daemon/view/PhabricatorDaemonLogListView.php
+++ b/src/applications/daemon/view/PhabricatorDaemonLogListView.php
@@ -1,88 +1,88 @@
 <?php
 
 final class PhabricatorDaemonLogListView extends AphrontView {
 
   private $daemonLogs;
 
   public function setDaemonLogs(array $daemon_logs) {
     assert_instances_of($daemon_logs, 'PhabricatorDaemonLog');
     $this->daemonLogs = $daemon_logs;
     return $this;
   }
 
   public function render() {
     $rows = array();
 
     if (!$this->user) {
-      throw new Exception('Call setUser() before rendering!');
+      throw new PhutilInvalidStateException('setUser');
     }
 
     $env_hash = PhabricatorEnv::calculateEnvironmentHash();
     $list = new PHUIObjectItemListView();
     $list->setFlush(true);
     foreach ($this->daemonLogs as $log) {
       $id = $log->getID();
       $epoch = $log->getDateCreated();
 
       $item = id(new PHUIObjectItemView())
         ->setObjectName(pht('Daemon %s', $id))
         ->setHeader($log->getDaemon())
         ->setHref("/daemon/log/{$id}/")
         ->addIcon('none', phabricator_datetime($epoch, $this->user));
 
       $status = $log->getStatus();
       switch ($status) {
         case PhabricatorDaemonLog::STATUS_RUNNING:
           if ($env_hash != $log->getEnvHash()) {
             $item->setBarColor('yellow');
             $item->addAttribute(pht(
               'This daemon is running with an out of date configuration and '.
               'should be restarted.'));
           } else {
             $item->setBarColor('green');
             $item->addAttribute(pht('This daemon is running.'));
           }
           break;
         case PhabricatorDaemonLog::STATUS_DEAD:
           $item->setBarColor('red');
           $item->addAttribute(
             pht(
               'This daemon is lost or exited uncleanly, and is presumed '.
               'dead.'));
           $item->addIcon('fa-times grey', pht('Dead'));
           break;
         case PhabricatorDaemonLog::STATUS_EXITING:
           $item->addAttribute(pht('This daemon is exiting.'));
           $item->addIcon('fa-check', pht('Exiting'));
           break;
         case PhabricatorDaemonLog::STATUS_EXITED:
           $item->setDisabled(true);
           $item->addAttribute(pht('This daemon exited cleanly.'));
           $item->addIcon('fa-check grey', pht('Exited'));
           break;
         case PhabricatorDaemonLog::STATUS_WAIT:
           $item->setBarColor('blue');
           $item->addAttribute(
             pht(
               'This daemon encountered an error recently and is waiting a '.
               'moment to restart.'));
           $item->addIcon('fa-clock-o grey', pht('Waiting'));
           break;
         case PhabricatorDaemonLog::STATUS_UNKNOWN:
         default:
           $item->setBarColor('orange');
           $item->addAttribute(
             pht(
               'This daemon has not reported its status recently. It may '.
               'have exited uncleanly.'));
           $item->addIcon('fa-exclamation-circle', pht('Unknown'));
           break;
       }
 
       $list->addItem($item);
     }
 
     return $list;
   }
 
 }
diff --git a/src/applications/differential/constants/DifferentialAction.php b/src/applications/differential/constants/DifferentialAction.php
index 9238990161..48a263c45a 100644
--- a/src/applications/differential/constants/DifferentialAction.php
+++ b/src/applications/differential/constants/DifferentialAction.php
@@ -1,138 +1,138 @@
 <?php
 
 final class DifferentialAction {
 
   const ACTION_CLOSE          = 'commit';
   const ACTION_COMMENT        = 'none';
   const ACTION_ACCEPT         = 'accept';
   const ACTION_REJECT         = 'reject';
   const ACTION_RETHINK        = 'rethink';
   const ACTION_ABANDON        = 'abandon';
   const ACTION_REQUEST        = 'request_review';
   const ACTION_RECLAIM        = 'reclaim';
   const ACTION_UPDATE         = 'update';
   const ACTION_RESIGN         = 'resign';
   const ACTION_SUMMARIZE      = 'summarize';
   const ACTION_TESTPLAN       = 'testplan';
   const ACTION_CREATE         = 'create';
   const ACTION_ADDREVIEWERS   = 'add_reviewers';
   const ACTION_ADDCCS         = 'add_ccs';
   const ACTION_CLAIM          = 'claim';
   const ACTION_REOPEN         = 'reopen';
 
   public static function getBasicStoryText($action, $author_name) {
     switch ($action) {
-      case DifferentialAction::ACTION_COMMENT:
+      case self::ACTION_COMMENT:
         $title = pht('%s commented on this revision.',
           $author_name);
         break;
-      case DifferentialAction::ACTION_ACCEPT:
+      case self::ACTION_ACCEPT:
         $title = pht('%s accepted this revision.',
           $author_name);
         break;
-      case DifferentialAction::ACTION_REJECT:
+      case self::ACTION_REJECT:
         $title = pht('%s requested changes to this revision.',
           $author_name);
         break;
-      case DifferentialAction::ACTION_RETHINK:
+      case self::ACTION_RETHINK:
         $title = pht('%s planned changes to this revision.',
           $author_name);
         break;
-      case DifferentialAction::ACTION_ABANDON:
+      case self::ACTION_ABANDON:
         $title = pht('%s abandoned this revision.',
           $author_name);
         break;
-      case DifferentialAction::ACTION_CLOSE:
+      case self::ACTION_CLOSE:
         $title = pht('%s closed this revision.',
           $author_name);
         break;
-      case DifferentialAction::ACTION_REQUEST:
+      case self::ACTION_REQUEST:
         $title = pht('%s requested a review of this revision.',
           $author_name);
         break;
-      case DifferentialAction::ACTION_RECLAIM:
+      case self::ACTION_RECLAIM:
         $title = pht('%s reclaimed this revision.',
           $author_name);
         break;
-      case DifferentialAction::ACTION_UPDATE:
+      case self::ACTION_UPDATE:
         $title = pht('%s updated this revision.',
           $author_name);
         break;
-      case DifferentialAction::ACTION_RESIGN:
+      case self::ACTION_RESIGN:
         $title = pht('%s resigned from this revision.',
           $author_name);
         break;
-      case DifferentialAction::ACTION_SUMMARIZE:
+      case self::ACTION_SUMMARIZE:
         $title = pht('%s summarized this revision.',
           $author_name);
         break;
-      case DifferentialAction::ACTION_TESTPLAN:
+      case self::ACTION_TESTPLAN:
         $title = pht('%s explained the test plan for this revision.',
           $author_name);
         break;
-      case DifferentialAction::ACTION_CREATE:
+      case self::ACTION_CREATE:
         $title = pht('%s created this revision.',
           $author_name);
         break;
-      case DifferentialAction::ACTION_ADDREVIEWERS:
+      case self::ACTION_ADDREVIEWERS:
         $title = pht('%s added reviewers to this revision.',
           $author_name);
         break;
-      case DifferentialAction::ACTION_ADDCCS:
+      case self::ACTION_ADDCCS:
         $title = pht('%s added CCs to this revision.',
           $author_name);
         break;
-      case DifferentialAction::ACTION_CLAIM:
+      case self::ACTION_CLAIM:
         $title = pht('%s commandeered this revision.',
           $author_name);
         break;
-      case DifferentialAction::ACTION_REOPEN:
+      case self::ACTION_REOPEN:
         $title = pht('%s reopened this revision.',
           $author_name);
         break;
       case DifferentialTransaction::TYPE_INLINE:
         $title = pht(
           '%s added an inline comment.',
           $author_name);
         break;
       default:
         $title = pht('Ghosts happened to this revision.');
         break;
     }
     return $title;
   }
 
   public static function getActionVerb($action) {
     $verbs = array(
       self::ACTION_COMMENT        => pht('Comment'),
       self::ACTION_ACCEPT         => pht("Accept Revision \xE2\x9C\x94"),
       self::ACTION_REJECT         => pht("Request Changes \xE2\x9C\x98"),
       self::ACTION_RETHINK        => pht("Plan Changes \xE2\x9C\x98"),
       self::ACTION_ABANDON        => pht('Abandon Revision'),
       self::ACTION_REQUEST        => pht('Request Review'),
       self::ACTION_RECLAIM        => pht('Reclaim Revision'),
       self::ACTION_RESIGN         => pht('Resign as Reviewer'),
       self::ACTION_ADDREVIEWERS   => pht('Add Reviewers'),
       self::ACTION_ADDCCS         => pht('Add Subscribers'),
       self::ACTION_CLOSE          => pht('Close Revision'),
       self::ACTION_CLAIM          => pht('Commandeer Revision'),
       self::ACTION_REOPEN         => pht('Reopen'),
     );
 
     if (!empty($verbs[$action])) {
       return $verbs[$action];
     } else {
       return 'brazenly '.$action;
     }
   }
 
   public static function allowReviewers($action) {
-    if ($action == DifferentialAction::ACTION_ADDREVIEWERS ||
-        $action == DifferentialAction::ACTION_REQUEST ||
-        $action == DifferentialAction::ACTION_RESIGN) {
+    if ($action == self::ACTION_ADDREVIEWERS ||
+        $action == self::ACTION_REQUEST ||
+        $action == self::ACTION_RESIGN) {
       return true;
     }
     return false;
   }
 
 }
diff --git a/src/applications/differential/constants/DifferentialChangeType.php b/src/applications/differential/constants/DifferentialChangeType.php
index 0a4f7861cc..2ce5392165 100644
--- a/src/applications/differential/constants/DifferentialChangeType.php
+++ b/src/applications/differential/constants/DifferentialChangeType.php
@@ -1,111 +1,111 @@
 <?php
 
 final class DifferentialChangeType {
 
   const TYPE_ADD        = 1;
   const TYPE_CHANGE     = 2;
   const TYPE_DELETE     = 3;
   const TYPE_MOVE_AWAY  = 4;
   const TYPE_COPY_AWAY  = 5;
   const TYPE_MOVE_HERE  = 6;
   const TYPE_COPY_HERE  = 7;
   const TYPE_MULTICOPY  = 8;
   const TYPE_MESSAGE    = 9;
   const TYPE_CHILD      = 10;
 
   const FILE_TEXT       = 1;
   const FILE_IMAGE      = 2;
   const FILE_BINARY     = 3;
   const FILE_DIRECTORY  = 4;
   const FILE_SYMLINK    = 5;
   const FILE_DELETED    = 6;
   const FILE_NORMAL     = 7;
   const FILE_SUBMODULE  = 8;
 
   public static function getSummaryCharacterForChangeType($type) {
     static $types = array(
       self::TYPE_ADD        => 'A',
       self::TYPE_CHANGE     => 'M',
       self::TYPE_DELETE     => 'D',
       self::TYPE_MOVE_AWAY  => 'V',
       self::TYPE_COPY_AWAY  => 'P',
       self::TYPE_MOVE_HERE  => 'V',
       self::TYPE_COPY_HERE  => 'P',
       self::TYPE_MULTICOPY  => 'P',
       self::TYPE_MESSAGE    => 'Q',
       self::TYPE_CHILD      => '@',
     );
     return idx($types, coalesce($type, '?'), '~');
   }
 
   public static function getShortNameForFileType($type) {
     static $names = array(
       self::FILE_TEXT       => null,
       self::FILE_DIRECTORY  => 'dir',
       self::FILE_IMAGE      => 'img',
       self::FILE_BINARY     => 'bin',
       self::FILE_SYMLINK    => 'sym',
       self::FILE_SUBMODULE  => 'sub',
     );
     return idx($names, coalesce($type, '?'), '???');
   }
 
   public static function isOldLocationChangeType($type) {
     static $types = array(
-      DifferentialChangeType::TYPE_MOVE_AWAY  => true,
-      DifferentialChangeType::TYPE_COPY_AWAY  => true,
-      DifferentialChangeType::TYPE_MULTICOPY  => true,
+      self::TYPE_MOVE_AWAY  => true,
+      self::TYPE_COPY_AWAY  => true,
+      self::TYPE_MULTICOPY  => true,
     );
     return isset($types[$type]);
   }
 
   public static function isNewLocationChangeType($type) {
     static $types = array(
-      DifferentialChangeType::TYPE_MOVE_HERE  => true,
-      DifferentialChangeType::TYPE_COPY_HERE  => true,
+      self::TYPE_MOVE_HERE  => true,
+      self::TYPE_COPY_HERE  => true,
     );
     return isset($types[$type]);
   }
 
   public static function isDeleteChangeType($type) {
     static $types = array(
-      DifferentialChangeType::TYPE_DELETE     => true,
-      DifferentialChangeType::TYPE_MOVE_AWAY  => true,
-      DifferentialChangeType::TYPE_MULTICOPY  => true,
+      self::TYPE_DELETE     => true,
+      self::TYPE_MOVE_AWAY  => true,
+      self::TYPE_MULTICOPY  => true,
     );
     return isset($types[$type]);
   }
 
   public static function isCreateChangeType($type) {
     static $types = array(
-      DifferentialChangeType::TYPE_ADD        => true,
-      DifferentialChangeType::TYPE_COPY_HERE  => true,
-      DifferentialChangeType::TYPE_MOVE_HERE  => true,
+      self::TYPE_ADD        => true,
+      self::TYPE_COPY_HERE  => true,
+      self::TYPE_MOVE_HERE  => true,
     );
     return isset($types[$type]);
   }
 
   public static function isModifyChangeType($type) {
     static $types = array(
-      DifferentialChangeType::TYPE_CHANGE     => true,
+      self::TYPE_CHANGE     => true,
     );
     return isset($types[$type]);
   }
 
   public static function getFullNameForChangeType($type) {
     $types = array(
       self::TYPE_ADD        => pht('Added'),
       self::TYPE_CHANGE     => pht('Modified'),
       self::TYPE_DELETE     => pht('Deleted'),
       self::TYPE_MOVE_AWAY  => pht('Moved Away'),
       self::TYPE_COPY_AWAY  => pht('Copied Away'),
       self::TYPE_MOVE_HERE  => pht('Moved Here'),
       self::TYPE_COPY_HERE  => pht('Copied Here'),
       self::TYPE_MULTICOPY  => pht('Deleted After Multiple Copy'),
       self::TYPE_MESSAGE    => pht('Commit Message'),
       self::TYPE_CHILD      => pht('Contents Modified'),
     );
     return idx($types, coalesce($type, '?'), 'Unknown');
   }
 
 }
diff --git a/src/applications/differential/customfield/DifferentialRevisionIDField.php b/src/applications/differential/customfield/DifferentialRevisionIDField.php
index 3741c0126f..bb54951dc4 100644
--- a/src/applications/differential/customfield/DifferentialRevisionIDField.php
+++ b/src/applications/differential/customfield/DifferentialRevisionIDField.php
@@ -1,78 +1,86 @@
 <?php
 
 final class DifferentialRevisionIDField
   extends DifferentialCustomField {
 
   private $revisionID;
 
   public function getFieldKey() {
     return 'differential:revision-id';
   }
 
   public function getFieldKeyForConduit() {
     return 'revisionID';
   }
 
   public function getFieldName() {
     return pht('Differential Revision');
   }
 
   public function getFieldDescription() {
     return pht(
       'Ties commits to revisions and provides a permananent link between '.
       'them.');
   }
 
   public function canDisableField() {
     return false;
   }
 
   public function shouldAppearInCommitMessage() {
     return true;
   }
 
   public function parseValueFromCommitMessage($value) {
+    // If the value is just "D123" or similar, parse the ID from it directly.
+    $value = trim($value);
+    $matches = null;
+    if (preg_match('/^[dD]([1-9]\d*)\z/', $value, $matches)) {
+      return (int)$matches[1];
+    }
+
+    // Otherwise, try to extract a URI value.
     return self::parseRevisionIDFromURI($value);
   }
 
   public function renderCommitMessageValue(array $handles) {
     $id = coalesce($this->revisionID, $this->getObject()->getID());
     if (!$id) {
       return null;
     }
     return PhabricatorEnv::getProductionURI('/D'.$id);
   }
 
   public function readValueFromCommitMessage($value) {
     $this->revisionID = $value;
   }
 
   private static function parseRevisionIDFromURI($uri_string) {
     $uri = new PhutilURI($uri_string);
     $path = $uri->getPath();
 
     $matches = null;
     if (preg_match('#^/D(\d+)$#', $path, $matches)) {
       $id = (int)$matches[1];
 
       $prod_uri = new PhutilURI(PhabricatorEnv::getProductionURI('/D'.$id));
 
       // Make sure the URI is the same as our URI. Basically, we want to ignore
       // commits from other Phabricator installs.
       if ($uri->getDomain() == $prod_uri->getDomain()) {
         return $id;
       }
 
       $allowed_uris = PhabricatorEnv::getAllowedURIs('/D'.$id);
 
       foreach ($allowed_uris as $allowed_uri) {
         if ($uri_string == $allowed_uri) {
           return $id;
         }
       }
     }
 
     return null;
   }
 
 }
diff --git a/src/applications/differential/parser/DifferentialLineAdjustmentMap.php b/src/applications/differential/parser/DifferentialLineAdjustmentMap.php
new file mode 100644
index 0000000000..fde8f61f7d
--- /dev/null
+++ b/src/applications/differential/parser/DifferentialLineAdjustmentMap.php
@@ -0,0 +1,376 @@
+<?php
+
+/**
+ * Datastructure which follows lines of code across source changes.
+ *
+ * This map is used to update the positions of inline comments after diff
+ * updates. For example, if a inline comment appeared on line 30 of a diff
+ * but the next update adds 15 more lines above it, the comment should move
+ * down to line 45.
+ *
+ */
+final class DifferentialLineAdjustmentMap extends Phobject {
+
+  private $map;
+  private $nearestMap;
+  private $isInverse;
+  private $finalOffset;
+  private $nextMapInChain;
+
+  /**
+   * Get the raw adjustment map.
+   */
+  public function getMap() {
+    return $this->map;
+  }
+
+  public function getNearestMap() {
+    if ($this->nearestMap === null) {
+      $this->buildNearestMap();
+    }
+
+    return $this->nearestMap;
+  }
+
+  public function getFinalOffset() {
+    // Make sure we've built this map already.
+    $this->getNearestMap();
+    return $this->finalOffset;
+  }
+
+
+  /**
+   * Add a map to the end of the chain.
+   *
+   * When a line is mapped with @{method:mapLine}, it is mapped through all
+   * maps in the chain.
+   */
+  public function addMapToChain(DifferentialLineAdjustmentMap $map) {
+    if ($this->nextMapInChain) {
+      $this->nextMapInChain->addMapToChain($map);
+    } else {
+      $this->nextMapInChain = $map;
+    }
+    return $this;
+  }
+
+
+  /**
+   * Map a line across a change, or a series of changes.
+   *
+   * @param int Line to map
+   * @param bool True to map it as the end of a range.
+   * @return wild Spooky magic.
+   */
+  public function mapLine($line, $is_end) {
+    $nmap = $this->getNearestMap();
+
+    $deleted = false;
+    $offset = false;
+    if (isset($nmap[$line])) {
+      $line_range = $nmap[$line];
+      if ($is_end) {
+        $to_line = end($line_range);
+      } else {
+        $to_line = reset($line_range);
+      }
+      if ($to_line <= 0) {
+        // If we're tracing the first line and this block is collapsing,
+        // compute the offset from the top of the block.
+        if (!$is_end && $this->isInverse) {
+          $offset = 0;
+          $cursor = $line - 1;
+          while (isset($nmap[$cursor])) {
+            $prev = $nmap[$cursor];
+            $prev = reset($prev);
+            if ($prev == $to_line) {
+              $offset++;
+            } else {
+              break;
+            }
+            $cursor--;
+          }
+        }
+
+        $to_line = -$to_line;
+        if (!$this->isInverse) {
+          $deleted = true;
+        }
+      }
+      $line = $to_line;
+    } else {
+      $line = $line + $this->finalOffset;
+    }
+
+    if ($this->nextMapInChain) {
+      $chain = $this->nextMapInChain->mapLine($line, $is_end);
+      list($chain_deleted, $chain_offset, $line) = $chain;
+      $deleted = ($deleted || $chain_deleted);
+      if ($chain_offset !== false) {
+        if ($offset === false) {
+          $offset = 0;
+        }
+        $offset += $chain_offset;
+      }
+    }
+
+    return array($deleted, $offset, $line);
+  }
+
+
+  /**
+   * Build a derived map which maps deleted lines to the nearest valid line.
+   *
+   * This computes a "nearest line" map and a final-line offset. These
+   * derived maps allow us to map deleted code to the previous (or next) line
+   * which actually exists.
+   */
+  private function buildNearestMap() {
+    $map = $this->map;
+    $nmap = array();
+
+    $nearest = 0;
+    foreach ($map as $key => $value) {
+      if ($value) {
+        $nmap[$key] = $value;
+        $nearest = end($value);
+      } else {
+        $nmap[$key][0] = -$nearest;
+      }
+    }
+
+    if (isset($key)) {
+      $this->finalOffset = ($nearest - $key);
+    } else {
+      $this->finalOffset = 0;
+    }
+
+    foreach (array_reverse($map, true) as $key => $value) {
+      if ($value) {
+        $nearest = reset($value);
+      } else {
+        $nmap[$key][1] = -$nearest;
+      }
+    }
+
+    $this->nearestMap = $nmap;
+
+    return $this;
+  }
+
+  public static function newFromHunks(array $hunks) {
+    assert_instances_of($hunks, 'DifferentialHunk');
+
+    $map = array();
+    $o = 0;
+    $n = 0;
+
+    $hunks = msort($hunks, 'getOldOffset');
+    foreach ($hunks as $hunk) {
+
+      // If the hunks are disjoint, add the implied missing lines where
+      // nothing changed.
+      $min = ($hunk->getOldOffset() - 1);
+      while ($o < $min) {
+        $o++;
+        $n++;
+        $map[$o][] = $n;
+      }
+
+      $lines = $hunk->getStructuredLines();
+      foreach ($lines as $line) {
+        switch ($line['type']) {
+          case '-':
+            $o++;
+            $map[$o] = array();
+            break;
+          case '+':
+            $n++;
+            $map[$o][] = $n;
+            break;
+          case ' ':
+            $o++;
+            $n++;
+            $map[$o][] = $n;
+            break;
+          default:
+            break;
+        }
+      }
+    }
+
+    $map = self::reduceMapRanges($map);
+
+    return self::newFromMap($map);
+  }
+
+  public static function newFromMap(array $map) {
+    $obj = new DifferentialLineAdjustmentMap();
+    $obj->map = $map;
+    return $obj;
+  }
+
+  public static function newInverseMap(DifferentialLineAdjustmentMap $map) {
+    $old = $map->getMap();
+    $inv = array();
+    $last = 0;
+    foreach ($old as $k => $v) {
+      if (count($v) > 1) {
+        $v = range(reset($v), end($v));
+      }
+      if ($k == 0) {
+        foreach ($v as $line) {
+          $inv[$line] = array();
+          $last = $line;
+        }
+      } else if ($v) {
+        $first = true;
+        foreach ($v as $line) {
+          if ($first) {
+            $first = false;
+            $inv[$line][] = $k;
+            $last = $line;
+          } else {
+            $inv[$line] = array();
+          }
+        }
+      } else {
+        $inv[$last][] = $k;
+      }
+    }
+
+    $inv = self::reduceMapRanges($inv);
+
+    $obj = new DifferentialLineAdjustmentMap();
+    $obj->map = $inv;
+    $obj->isInverse = !$map->isInverse;
+    return $obj;
+  }
+
+  private static function reduceMapRanges(array $map) {
+    foreach ($map as $key => $values) {
+      if (count($values) > 2) {
+        $map[$key] = array(reset($values), end($values));
+      }
+    }
+    return $map;
+  }
+
+
+  public static function loadMaps(array $maps) {
+    $keys = array();
+    foreach ($maps as $map) {
+      list($u, $v) = $map;
+      $keys[self::getCacheKey($u, $v)] = $map;
+    }
+
+    $cache = new PhabricatorKeyValueDatabaseCache();
+    $cache = new PhutilKeyValueCacheProfiler($cache);
+    $cache->setProfiler(PhutilServiceProfiler::getInstance());
+
+    $results = array();
+
+    if ($keys) {
+      $caches = $cache->getKeys(array_keys($keys));
+      foreach ($caches as $key => $value) {
+        list($u, $v) = $keys[$key];
+        try {
+          $results[$u][$v] = self::newFromMap(
+            phutil_json_decode($value));
+        } catch (Exception $ex) {
+          // Ignore, rebuild below.
+        }
+        unset($keys[$key]);
+      }
+    }
+
+    if ($keys) {
+      $built = self::buildMaps($maps);
+
+      $write = array();
+      foreach ($built as $u => $list) {
+        foreach ($list as $v => $map) {
+          $write[self::getCacheKey($u, $v)] = json_encode($map->getMap());
+          $results[$u][$v] = $map;
+        }
+      }
+
+      $cache->setKeys($write);
+    }
+
+    return $results;
+  }
+
+  private static function buildMaps(array $maps) {
+    $need = array();
+    foreach ($maps as $map) {
+      list($u, $v) = $map;
+      $need[$u] = $u;
+      $need[$v] = $v;
+    }
+
+    if ($need) {
+      $changesets = id(new DifferentialChangesetQuery())
+        ->setViewer(PhabricatorUser::getOmnipotentUser())
+        ->withIDs($need)
+        ->needHunks(true)
+        ->execute();
+      $changesets = mpull($changesets, null, 'getID');
+    }
+
+    $results = array();
+    foreach ($maps as $map) {
+      list($u, $v) = $map;
+      $u_set = idx($changesets, $u);
+      $v_set = idx($changesets, $v);
+
+      if (!$u_set || !$v_set) {
+        continue;
+      }
+
+      // This is the simple case.
+      if ($u == $v) {
+        $results[$u][$v] = self::newFromHunks(
+          $u_set->getHunks());
+        continue;
+      }
+
+      $u_old = $u_set->makeOldFile();
+      $v_old = $v_set->makeOldFile();
+
+      // No difference between the two left sides.
+      if ($u_old == $v_old) {
+        $results[$u][$v] = self::newFromMap(
+          array());
+        continue;
+      }
+
+      // If we're missing context, this won't currently work. We can
+      // make this case work, but it's fairly rare.
+      $u_hunks = $u_set->getHunks();
+      $v_hunks = $v_set->getHunks();
+      if (count($u_hunks) != 1 ||
+          count($v_hunks) != 1 ||
+          head($u_hunks)->getOldOffset() != 1 ||
+          head($u_hunks)->getNewOffset() != 1 ||
+          head($v_hunks)->getOldOffset() != 1 ||
+          head($v_hunks)->getNewOffset() != 1) {
+        continue;
+      }
+
+      $changeset = id(new PhabricatorDifferenceEngine())
+        ->setIgnoreWhitespace(true)
+        ->generateChangesetFromFileContent($u_old, $v_old);
+
+      $results[$u][$v] = self::newFromHunks(
+        $changeset->getHunks());
+    }
+
+    return $results;
+  }
+
+  private static function getCacheKey($u, $v) {
+    return 'diffadjust.v1('.$u.','.$v.')';
+  }
+
+}
diff --git a/src/applications/differential/query/DifferentialInlineCommentQuery.php b/src/applications/differential/query/DifferentialInlineCommentQuery.php
index 711c638bf1..3fb26739c0 100644
--- a/src/applications/differential/query/DifferentialInlineCommentQuery.php
+++ b/src/applications/differential/query/DifferentialInlineCommentQuery.php
@@ -1,354 +1,456 @@
 <?php
 
 /**
  * Temporary wrapper for transitioning Differential to ApplicationTransactions.
  */
 final class DifferentialInlineCommentQuery
   extends PhabricatorOffsetPagedQuery {
 
   // TODO: Remove this when this query eventually moves to PolicyAware.
   private $viewer;
 
   private $ids;
   private $phids;
   private $drafts;
   private $authorPHIDs;
   private $revisionPHIDs;
   private $deletedDrafts;
 
   public function setViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
   public function getViewer() {
     return $this->viewer;
   }
 
   public function withIDs(array $ids) {
     $this->ids = $ids;
     return $this;
   }
 
   public function withPHIDs(array $phids) {
     $this->phids = $phids;
     return $this;
   }
 
   public function withDrafts($drafts) {
     $this->drafts = $drafts;
     return $this;
   }
 
   public function withAuthorPHIDs(array $author_phids) {
     $this->authorPHIDs = $author_phids;
     return $this;
   }
 
   public function withRevisionPHIDs(array $revision_phids) {
     $this->revisionPHIDs = $revision_phids;
     return $this;
   }
 
   public function withDeletedDrafts($deleted_drafts) {
     $this->deletedDrafts = $deleted_drafts;
     return $this;
   }
 
   public function execute() {
     $table = new DifferentialTransactionComment();
     $conn_r = $table->establishConnection('r');
 
     $data = queryfx_all(
       $conn_r,
       'SELECT * FROM %T %Q %Q',
       $table->getTableName(),
       $this->buildWhereClause($conn_r),
       $this->buildLimitClause($conn_r));
 
     $comments = $table->loadAllFromArray($data);
 
     foreach ($comments as $key => $value) {
       $comments[$key] = DifferentialInlineComment::newFromModernComment(
         $value);
     }
 
     return $comments;
   }
 
   public function executeOne() {
     // TODO: Remove when this query moves to PolicyAware.
     return head($this->execute());
   }
 
   protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
     $where = array();
 
     // Only find inline comments.
     $where[] = qsprintf(
       $conn_r,
       'changesetID IS NOT NULL');
 
     if ($this->ids !== null) {
       $where[] = qsprintf(
         $conn_r,
         'id IN (%Ld)',
         $this->ids);
     }
 
     if ($this->phids !== null) {
       $where[] = qsprintf(
         $conn_r,
         'phid IN (%Ls)',
         $this->phids);
     }
 
     if ($this->revisionPHIDs !== null) {
       $where[] = qsprintf(
         $conn_r,
         'revisionPHID IN (%Ls)',
         $this->revisionPHIDs);
     }
 
     if ($this->drafts === null) {
       if ($this->deletedDrafts) {
         $where[] = qsprintf(
           $conn_r,
           '(authorPHID = %s) OR (transactionPHID IS NOT NULL)',
           $this->getViewer()->getPHID());
       } else {
         $where[] = qsprintf(
           $conn_r,
           '(authorPHID = %s AND isDeleted = 0)
             OR (transactionPHID IS NOT NULL)',
           $this->getViewer()->getPHID());
       }
     } else if ($this->drafts) {
       $where[] = qsprintf(
         $conn_r,
         '(authorPHID = %s AND isDeleted = 0) AND (transactionPHID IS NULL)',
         $this->getViewer()->getPHID());
     } else {
       $where[] = qsprintf(
         $conn_r,
         'transactionPHID IS NOT NULL');
     }
 
     return $this->formatWhereClause($where);
   }
 
   public function adjustInlinesForChangesets(
     array $inlines,
     array $old,
     array $new,
     DifferentialRevision $revision) {
 
     assert_instances_of($inlines, 'DifferentialInlineComment');
     assert_instances_of($old, 'DifferentialChangeset');
     assert_instances_of($new, 'DifferentialChangeset');
 
     $viewer = $this->getViewer();
 
     $pref = $viewer->loadPreferences()->getPreference(
       PhabricatorUserPreferences::PREFERENCE_DIFF_GHOSTS);
     if ($pref == 'disabled') {
       return $inlines;
     }
 
     $all = array_merge($old, $new);
 
     $changeset_ids = mpull($inlines, 'getChangesetID');
     $changeset_ids = array_unique($changeset_ids);
 
     $all_map = mpull($all, null, 'getID');
 
     // We already have at least some changesets, and we might not need to do
     // any more data fetching. Remove everything we already have so we can
     // tell if we need new stuff.
     foreach ($changeset_ids as $key => $id) {
       if (isset($all_map[$id])) {
         unset($changeset_ids[$key]);
       }
     }
 
     if ($changeset_ids) {
       $changesets = id(new DifferentialChangesetQuery())
         ->setViewer($viewer)
         ->withIDs($changeset_ids)
         ->execute();
       $changesets = mpull($changesets, null, 'getID');
     } else {
       $changesets = array();
     }
     $changesets += $all_map;
 
     $id_map = array();
     foreach ($all as $changeset) {
       $id_map[$changeset->getID()] = $changeset->getID();
     }
 
     // Generate filename maps for older and newer comments. If we're bringing
     // an older comment forward in a diff-of-diffs, we want to put it on the
     // left side of the screen, not the right side. Both sides are "new" files
     // with the same name, so they're both appropriate targets, but the left
     // is a better target conceptually for users because it's more consistent
     // with the rest of the UI, which shows old information on the left and
     // new information on the right.
     $move_here = DifferentialChangeType::TYPE_MOVE_HERE;
 
     $name_map_old = array();
     $name_map_new = array();
     $move_map = array();
     foreach ($all as $changeset) {
       $changeset_id = $changeset->getID();
 
       $filenames = array();
       $filenames[] = $changeset->getFilename();
 
       // If this is the target of a move, also map comments on the old filename
       // to this changeset.
       if ($changeset->getChangeType() == $move_here) {
         $old_file = $changeset->getOldFile();
         $filenames[] = $old_file;
         $move_map[$changeset_id][$old_file] = true;
       }
 
       foreach ($filenames as $filename) {
         // We update the old map only if we don't already have an entry (oldest
         // changeset persists).
         if (empty($name_map_old[$filename])) {
           $name_map_old[$filename] = $changeset_id;
         }
 
         // We always update the new map (newest changeset overwrites).
         $name_map_new[$changeset->getFilename()] = $changeset_id;
       }
     }
 
     // Find the smallest "new" changeset ID. We'll consider everything
     // larger than this to be "newer", and everything smaller to be "older".
     $first_new_id = min(mpull($new, 'getID'));
 
     $results = array();
     foreach ($inlines as $inline) {
       $changeset_id = $inline->getChangesetID();
       if (isset($id_map[$changeset_id])) {
         // This inline is legitimately on one of the current changesets, so
         // we can include it in the result set unmodified.
         $results[] = $inline;
         continue;
       }
 
       $changeset = idx($changesets, $changeset_id);
       if (!$changeset) {
         // Just discard this inline, as it has bogus data.
         continue;
       }
 
       $target_id = null;
 
       if ($changeset_id >= $first_new_id) {
         $name_map = $name_map_new;
         $is_new = true;
       } else {
         $name_map = $name_map_old;
         $is_new = false;
       }
 
       $filename = $changeset->getFilename();
       if (isset($name_map[$filename])) {
         // This changeset is on a file with the same name as the current
         // changeset, so we're going to port it forward or backward.
         $target_id = $name_map[$filename];
 
         $is_move = isset($move_map[$target_id][$filename]);
         if ($is_new) {
           if ($is_move) {
             $reason = pht(
               'This comment was made on a file with the same name as the '.
               'file this file was moved from, but in a newer diff.');
           } else {
             $reason = pht(
               'This comment was made on a file with the same name, but '.
               'in a newer diff.');
           }
         } else {
           if ($is_move) {
             $reason = pht(
               'This comment was made on a file with the same name as the '.
               'file this file was moved from, but in an older diff.');
           } else {
             $reason = pht(
               'This comment was made on a file with the same name, but '.
               'in an older diff.');
           }
         }
       }
 
 
       // If we didn't find a target and this change is the target of a move,
       // look for a match against the old filename.
       if (!$target_id) {
         if ($changeset->getChangeType() == $move_here) {
           $filename = $changeset->getOldFile();
           if (isset($name_map[$filename])) {
             $target_id = $name_map[$filename];
             if ($is_new) {
               $reason = pht(
                 'This comment was made on a file which this file was moved '.
                 'to, but in a newer diff.');
             } else {
               $reason = pht(
                 'This comment was made on a file which this file was moved '.
                 'to, but in an older diff.');
             }
           }
         }
       }
 
 
       // If we found a changeset to port this comment to, bring it forward
       // or backward and mark it.
       if ($target_id) {
         $diff_id = $changeset->getDiffID();
         $inline_id = $inline->getID();
         $revision_id = $revision->getID();
         $href = "/D{$revision_id}?id={$diff_id}#inline-{$inline_id}";
 
         $inline
           ->makeEphemeral(true)
           ->setChangesetID($target_id)
           ->setIsGhost(
             array(
               'new' => $is_new,
               'reason' => $reason,
               'href' => $href,
+              'originalID' => $changeset->getID(),
             ));
 
         $results[] = $inline;
       }
     }
 
     // Filter out the inlines we ported forward which won't be visible because
     // they appear on the wrong side of a file.
     $keep_map = array();
     foreach ($old as $changeset) {
       $keep_map[$changeset->getID()][0] = true;
     }
     foreach ($new as $changeset) {
       $keep_map[$changeset->getID()][1] = true;
     }
 
     foreach ($results as $key => $inline) {
       $is_new = (int)$inline->getIsNewFile();
       $changeset_id = $inline->getChangesetID();
       if (!isset($keep_map[$changeset_id][$is_new])) {
         unset($results[$key]);
         continue;
       }
     }
 
+    // Adjust inline line numbers to account for content changes across
+    // updates and rebases.
+    $plan = array();
+    $need = array();
+    foreach ($results as $inline) {
+      $ghost = $inline->getIsGhost();
+      if (!$ghost) {
+        // If this isn't a "ghost" inline, ignore it.
+        continue;
+      }
+
+      $src_id = $ghost['originalID'];
+      $dst_id = $inline->getChangesetID();
+
+      $xforms = array();
+
+      // If the comment is on the right, transform it through the inverse map
+      // back to the left.
+      if ($inline->getIsNewFile()) {
+        $xforms[] = array($src_id, $src_id, true);
+      }
+
+      // Transform it across rebases.
+      $xforms[] = array($src_id, $dst_id, false);
+
+      // If the comment is on the right, transform it back onto the right.
+      if ($inline->getIsNewFile()) {
+        $xforms[] = array($dst_id, $dst_id, false);
+      }
+
+      $key = array();
+      foreach ($xforms as $xform) {
+        list($u, $v, $inverse) = $xform;
+
+        $short = $u.'/'.$v;
+        $need[$short] = array($u, $v);
+
+        $part = $u.($inverse ? '<' : '>').$v;
+        $key[] = $part;
+      }
+      $key = implode(',', $key);
+
+      if (empty($plan[$key])) {
+        $plan[$key] = array(
+          'xforms' => $xforms,
+          'inlines' => array(),
+        );
+      }
+
+      $plan[$key]['inlines'][] = $inline;
+    }
+
+    if ($need) {
+      $maps = DifferentialLineAdjustmentMap::loadMaps($need);
+    } else {
+      $maps = array();
+    }
+
+    foreach ($plan as $step) {
+      $xforms = $step['xforms'];
+
+      $chain = null;
+      foreach ($xforms as $xform) {
+        list($u, $v, $inverse) = $xform;
+        $map = idx(idx($maps, $u, array()), $v);
+        if (!$map) {
+          continue 2;
+        }
+
+        if ($inverse) {
+          $map = DifferentialLineAdjustmentMap::newInverseMap($map);
+        } else {
+          $map = clone $map;
+        }
+
+        if ($chain) {
+          $chain->addMapToChain($map);
+        } else {
+          $chain = $map;
+        }
+      }
+
+      foreach ($step['inlines'] as $inline) {
+        $head_line = $inline->getLineNumber();
+        $tail_line = ($head_line + $inline->getLineLength());
+
+        $head_info = $chain->mapLine($head_line, false);
+        $tail_info = $chain->mapLine($tail_line, true);
+
+        list($head_deleted, $head_offset, $head_line) = $head_info;
+        list($tail_deleted, $tail_offset, $tail_line) = $tail_info;
+
+        if ($head_offset !== false) {
+          $inline->setLineNumber($head_line + 1 + $head_offset);
+        } else {
+          $inline->setLineNumber($head_line);
+          $inline->setLineLength($tail_line - $head_line);
+        }
+      }
+    }
+
     return $results;
   }
 
 }
diff --git a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php
index a9eef41ac8..3eb0190664 100644
--- a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php
+++ b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php
@@ -1,372 +1,373 @@
 <?php
 
 final class DifferentialChangesetTwoUpRenderer
   extends DifferentialChangesetHTMLRenderer {
 
   public function isOneUpRenderer() {
     return false;
   }
 
   protected function getRendererTableClass() {
     return 'diff-2up';
   }
 
   public function getRendererKey() {
     return '2up';
   }
 
   protected function renderColgroup() {
     return phutil_tag('colgroup', array(), array(
       phutil_tag('col', array('class' => 'num')),
       phutil_tag('col', array('class' => 'left')),
       phutil_tag('col', array('class' => 'num')),
       phutil_tag('col', array('class' => 'copy')),
       phutil_tag('col', array('class' => 'right')),
       phutil_tag('col', array('class' => 'cov')),
     ));
   }
 
   public function renderTextChange(
     $range_start,
     $range_len,
     $rows) {
 
     $hunk_starts = $this->getHunkStartLines();
 
     $context_not_available = null;
     if ($hunk_starts) {
       $context_not_available = javelin_tag(
         'tr',
         array(
           'sigil' => 'context-target',
         ),
         phutil_tag(
           'td',
           array(
             'colspan' => 6,
             'class' => 'show-more',
           ),
           pht('Context not available.')));
     }
 
     $html = array();
 
     $old_lines = $this->getOldLines();
     $new_lines = $this->getNewLines();
     $gaps = $this->getGaps();
     $reference = $this->getRenderingReference();
 
     list($left_prefix, $right_prefix) = $this->getLineIDPrefixes();
 
     $changeset = $this->getChangeset();
     $copy_lines = idx($changeset->getMetadata(), 'copy:lines', array());
     $highlight_old = $this->getHighlightOld();
     $highlight_new = $this->getHighlightNew();
     $old_render = $this->getOldRender();
     $new_render = $this->getNewRender();
     $original_left = $this->getOriginalOld();
     $original_right = $this->getOriginalNew();
     $depths = $this->getDepths();
     $mask = $this->getMask();
 
     for ($ii = $range_start; $ii < $range_start + $range_len; $ii++) {
       if (empty($mask[$ii])) {
         // If we aren't going to show this line, we've just entered a gap.
         // Pop information about the next gap off the $gaps stack and render
         // an appropriate "Show more context" element. This branch eventually
         // increments $ii by the entire size of the gap and then continues
         // the loop.
         $gap = array_pop($gaps);
         $top = $gap[0];
         $len = $gap[1];
 
         $contents = $this->renderShowContextLinks($top, $len, $rows);
 
         $is_last_block = false;
         if ($ii + $len >= $rows) {
           $is_last_block = true;
         }
 
         $context = null;
         $context_line = null;
         if (!$is_last_block && $depths[$ii + $len]) {
           for ($l = $ii + $len - 1; $l >= $ii; $l--) {
             $line = $new_lines[$l]['text'];
             if ($depths[$l] < $depths[$ii + $len] && trim($line) != '') {
               $context = $new_render[$l];
               $context_line = $new_lines[$l]['line'];
               break;
             }
           }
         }
 
         $container = javelin_tag(
           'tr',
           array(
             'sigil' => 'context-target',
           ),
           array(
             phutil_tag(
               'td',
               array(
                 'colspan' => 2,
                 'class' => 'show-more',
               ),
               $contents),
             phutil_tag(
               'th',
               array(
                 'class' => 'show-context-line',
               ),
               $context_line ? (int)$context_line : null),
             phutil_tag(
               'td',
               array(
                 'colspan' => 3,
                 'class' => 'show-context',
               ),
               // TODO: [HTML] Escaping model here isn't ideal.
               phutil_safe_html($context)),
           ));
 
         $html[] = $container;
 
         $ii += ($len - 1);
         continue;
       }
 
       $o_num = null;
       $o_classes = '';
       $o_text = null;
       if (isset($old_lines[$ii])) {
         $o_num  = $old_lines[$ii]['line'];
         $o_text = isset($old_render[$ii]) ? $old_render[$ii] : null;
         if ($old_lines[$ii]['type']) {
           if ($old_lines[$ii]['type'] == '\\') {
             $o_text = $old_lines[$ii]['text'];
             $o_class = 'comment';
           } else if ($original_left && !isset($highlight_old[$o_num])) {
             $o_class = 'old-rebase';
           } else if (empty($new_lines[$ii])) {
             $o_class = 'old old-full';
           } else {
             $o_class = 'old';
           }
           $o_classes = $o_class;
         }
       }
 
       $n_copy = hsprintf('<td class="copy" />');
       $n_cov = null;
       $n_colspan = 2;
       $n_classes = '';
       $n_num  = null;
       $n_text = null;
 
       if (isset($new_lines[$ii])) {
         $n_num  = $new_lines[$ii]['line'];
         $n_text = isset($new_render[$ii]) ? $new_render[$ii] : null;
         $coverage = $this->getCodeCoverage();
 
         if ($coverage !== null) {
           if (empty($coverage[$n_num - 1])) {
             $cov_class = 'N';
           } else {
             $cov_class = $coverage[$n_num - 1];
           }
           $cov_class = 'cov-'.$cov_class;
           $n_cov = phutil_tag('td', array('class' => "cov {$cov_class}"));
           $n_colspan--;
         }
 
         if ($new_lines[$ii]['type']) {
           if ($new_lines[$ii]['type'] == '\\') {
             $n_text = $new_lines[$ii]['text'];
             $n_class = 'comment';
           } else if ($original_right && !isset($highlight_new[$n_num])) {
             $n_class = 'new-rebase';
           } else if (empty($old_lines[$ii])) {
             $n_class = 'new new-full';
           } else {
             $n_class = 'new';
           }
           $n_classes = $n_class;
 
           if ($new_lines[$ii]['type'] == '\\' || !isset($copy_lines[$n_num])) {
             $n_copy = phutil_tag('td', array('class' => "copy {$n_class}"));
           } else {
             list($orig_file, $orig_line, $orig_type) = $copy_lines[$n_num];
             $title = ($orig_type == '-' ? 'Moved' : 'Copied').' from ';
             if ($orig_file == '') {
               $title .= "line {$orig_line}";
             } else {
               $title .=
                 basename($orig_file).
                 ":{$orig_line} in dir ".
                 dirname('/'.$orig_file);
             }
             $class = ($orig_type == '-' ? 'new-move' : 'new-copy');
             $n_copy = javelin_tag(
               'td',
               array(
                 'meta' => array(
                   'msg' => $title,
                 ),
                 'class' => 'copy '.$class,
               ),
               '');
           }
         }
       }
 
       if (isset($hunk_starts[$o_num])) {
         $html[] = $context_not_available;
       }
 
       if ($o_num && $left_prefix) {
         $o_id = $left_prefix.$o_num;
       } else {
         $o_id = null;
       }
 
       if ($n_num && $right_prefix) {
         $n_id = $right_prefix.$n_num;
       } else {
         $n_id = null;
       }
 
       // NOTE: This is a unicode zero-width space, which we use as a hint when
       // intercepting 'copy' events to make sure sensible text ends up on the
       // clipboard. See the 'phabricator-oncopy' behavior.
       $zero_space = "\xE2\x80\x8B";
 
       $html[] = phutil_tag('tr', array(), array(
         phutil_tag('th', array('id' => $o_id), $o_num),
         phutil_tag('td', array('class' => $o_classes), $o_text),
         phutil_tag('th', array('id' => $n_id), $n_num),
         $n_copy,
         phutil_tag(
           'td',
           array('class' => $n_classes, 'colspan' => $n_colspan),
           array(
             phutil_tag('span', array('class' => 'zwsp'), $zero_space),
             $n_text,
           )),
         $n_cov,
       ));
 
       if ($context_not_available && ($ii == $rows - 1)) {
         $html[] = $context_not_available;
       }
 
       $old_comments = $this->getOldComments();
       $new_comments = $this->getNewComments();
 
       if ($o_num && isset($old_comments[$o_num])) {
         foreach ($old_comments[$o_num] as $comment) {
           $inline = $this->buildInlineComment(
             $comment,
             $on_right = false);
           $scaffold = $this->getRowScaffoldForInline($inline);
 
           if ($n_num && isset($new_comments[$n_num])) {
             foreach ($new_comments[$n_num] as $key => $new_comment) {
               if ($comment->isCompatible($new_comment)) {
                 $companion = $this->buildInlineComment(
                   $new_comment,
                   $on_right = true);
 
                 $scaffold->addInlineView($companion);
                 unset($new_comments[$n_num][$key]);
+                break;
               }
             }
           }
 
           $html[] = $scaffold;
         }
       }
       if ($n_num && isset($new_comments[$n_num])) {
         foreach ($new_comments[$n_num] as $comment) {
           $inline = $this->buildInlineComment(
             $comment,
             $on_right = true);
           $html[] = $this->getRowScaffoldForInline($inline);
         }
       }
     }
 
     return $this->wrapChangeInTable(phutil_implode_html('', $html));
   }
 
   public function renderFileChange($old_file = null,
                                    $new_file = null,
                                    $id = 0,
                                    $vs = 0) {
     $old = null;
     if ($old_file) {
       $old = $this->renderImageStage($old_file);
     }
 
     $new = null;
     if ($new_file) {
       $new = $this->renderImageStage($new_file);
     }
 
     $html_old = array();
     $html_new = array();
     foreach ($this->getOldComments() as $on_line => $comment_group) {
       foreach ($comment_group as $comment) {
         $inline = $this->buildInlineComment(
           $comment,
           $on_right = false);
         $html_old[] = $this->getRowScaffoldForInline($inline);
       }
     }
     foreach ($this->getNewComments() as $lin_line => $comment_group) {
       foreach ($comment_group as $comment) {
         $inline = $this->buildInlineComment(
           $comment,
           $on_right = true);
         $html_new[] = $this->getRowScaffoldForInline($inline);
       }
     }
 
     if (!$old) {
       $th_old = phutil_tag('th', array());
     } else {
       $th_old = phutil_tag('th', array('id' => "C{$vs}OL1"), 1);
     }
 
     if (!$new) {
       $th_new = phutil_tag('th', array());
     } else {
       $th_new = phutil_tag('th', array('id' => "C{$id}OL1"), 1);
     }
 
     $output = hsprintf(
       '<tr class="differential-image-diff">'.
         '%s'.
         '<td class="differential-old-image">%s</td>'.
         '%s'.
         '<td class="differential-new-image" colspan="3">%s</td>'.
       '</tr>'.
       '%s'.
       '%s',
       $th_old,
       $old,
       $th_new,
       $new,
       phutil_implode_html('', $html_old),
       phutil_implode_html('', $html_new));
 
     $output = $this->wrapChangeInTable($output);
 
     return $this->renderChangesetTable($output);
   }
 
   public function getRowScaffoldForInline(PHUIDiffInlineCommentView $view) {
     return id(new PHUIDiffTwoUpInlineCommentRowScaffold())
       ->addInlineView($view);
   }
 
 }
diff --git a/src/applications/differential/storage/DifferentialHunk.php b/src/applications/differential/storage/DifferentialHunk.php
index f22ba59d2b..bd13cadfec 100644
--- a/src/applications/differential/storage/DifferentialHunk.php
+++ b/src/applications/differential/storage/DifferentialHunk.php
@@ -1,235 +1,235 @@
 <?php
 
 abstract class DifferentialHunk extends DifferentialDAO
   implements PhabricatorPolicyInterface {
 
   protected $changesetID;
   protected $oldOffset;
   protected $oldLen;
   protected $newOffset;
   protected $newLen;
 
   private $changeset;
   private $splitLines;
   private $structuredLines;
   private $structuredFiles = array();
 
   const FLAG_LINES_ADDED     = 1;
   const FLAG_LINES_REMOVED   = 2;
   const FLAG_LINES_STABLE    = 4;
 
   public function getAddedLines() {
     return $this->makeContent($include = '+');
   }
 
   public function getRemovedLines() {
     return $this->makeContent($include = '-');
   }
 
   public function makeNewFile() {
     return implode('', $this->makeContent($include = ' +'));
   }
 
   public function makeOldFile() {
     return implode('', $this->makeContent($include = ' -'));
   }
 
   public function makeChanges() {
     return implode('', $this->makeContent($include = '-+'));
   }
 
   public function getStructuredOldFile() {
     return $this->getStructuredFile('-');
   }
 
   public function getStructuredNewFile() {
     return $this->getStructuredFile('+');
   }
 
   private function getStructuredFile($kind) {
     if ($kind !== '+' && $kind !== '-') {
       throw new Exception(
         pht(
           'Structured file kind should be "+" or "-", got "%s".',
           $kind));
     }
 
     if (!isset($this->structuredFiles[$kind])) {
       if ($kind == '+') {
         $number = $this->newOffset;
       } else {
         $number = $this->oldOffset;
       }
 
       $lines = $this->getStructuredLines();
 
       // NOTE: We keep the "\ No newline at end of file" line if it appears
       // after a line which is not excluded. For example, if we're constructing
       // the "+" side of the diff, we want to ignore this one since it's
       // relevant only to the "-" side of the diff:
       //
       //    - x
       //    \ No newline at end of file
       //    + x
       //
       // ...but we want to keep this one:
       //
       //    - x
       //    + x
       //    \ No newline at end of file
 
       $file = array();
       $keep = true;
       foreach ($lines as $line) {
         switch ($line['type']) {
           case ' ':
           case $kind:
             $file[$number++] = $line;
             $keep = true;
             break;
           case '\\':
             if ($keep) {
               // Strip the actual newline off the line's text.
               $text = $file[$number - 1]['text'];
               $text = rtrim($text, "\r\n");
               $file[$number - 1]['text'] = $text;
 
               $file[$number++] = $line;
               $keep = false;
             }
             break;
           default:
             $keep = false;
             break;
         }
       }
 
       $this->structuredFiles[$kind] = $file;
     }
 
     return $this->structuredFiles[$kind];
   }
 
   public function getSplitLines() {
     if ($this->splitLines === null) {
       $this->splitLines = phutil_split_lines($this->getChanges());
     }
     return $this->splitLines;
   }
 
-  private function getStructuredLines() {
+  public function getStructuredLines() {
     if ($this->structuredLines === null) {
       $lines = $this->getSplitLines();
 
       $structured = array();
       foreach ($lines as $line) {
         if (empty($line[0])) {
           // TODO: Can we just get rid of this?
           continue;
         }
 
         $structured[] = array(
           'type' => $line[0],
           'text' => substr($line, 1),
         );
       }
 
       $this->structuredLines = $structured;
     }
 
     return $this->structuredLines;
   }
 
 
   public function getContentWithMask($mask) {
     $include = array();
 
     if (($mask & self::FLAG_LINES_ADDED)) {
       $include[] = '+';
     }
 
     if (($mask & self::FLAG_LINES_REMOVED)) {
       $include[] = '-';
     }
 
     if (($mask & self::FLAG_LINES_STABLE)) {
       $include[] = ' ';
     }
 
     $include = implode('', $include);
 
     return implode('', $this->makeContent($include));
   }
 
   final private function makeContent($include) {
     $lines = $this->getSplitLines();
     $results = array();
 
     $include_map = array();
     for ($ii = 0; $ii < strlen($include); $ii++) {
       $include_map[$include[$ii]] = true;
     }
 
     if (isset($include_map['+'])) {
       $n = $this->newOffset;
     } else {
       $n = $this->oldOffset;
     }
 
     $use_next_newline = false;
     foreach ($lines as $line) {
       if (!isset($line[0])) {
         continue;
       }
 
       if ($line[0] == '\\') {
         if ($use_next_newline) {
           $results[last_key($results)] = rtrim(end($results), "\n");
         }
       } else if (empty($include_map[$line[0]])) {
         $use_next_newline = false;
       } else {
         $use_next_newline = true;
         $results[$n] = substr($line, 1);
       }
 
       if ($line[0] == ' ' || isset($include_map[$line[0]])) {
         $n++;
       }
     }
 
     return $results;
   }
 
   public function getChangeset() {
     return $this->assertAttached($this->changeset);
   }
 
   public function attachChangeset(DifferentialChangeset $changeset) {
     $this->changeset = $changeset;
     return $this;
   }
 
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
     );
   }
 
   public function getPolicy($capability) {
     return $this->getChangeset()->getPolicy($capability);
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     return $this->getChangeset()->hasAutomaticCapability($capability, $viewer);
   }
 
   public function describeAutomaticCapability($capability) {
     return null;
   }
 
 }
diff --git a/src/applications/differential/storage/DifferentialTransaction.php b/src/applications/differential/storage/DifferentialTransaction.php
index 5cb6fd7e87..2857d24272 100644
--- a/src/applications/differential/storage/DifferentialTransaction.php
+++ b/src/applications/differential/storage/DifferentialTransaction.php
@@ -1,665 +1,665 @@
 <?php
 
 final class DifferentialTransaction extends PhabricatorApplicationTransaction {
 
   private $isCommandeerSideEffect;
 
 
   public function setIsCommandeerSideEffect($is_side_effect) {
     $this->isCommandeerSideEffect = $is_side_effect;
     return $this;
   }
 
   public function getIsCommandeerSideEffect() {
     return $this->isCommandeerSideEffect;
   }
 
   const TYPE_INLINE = 'differential:inline';
   const TYPE_UPDATE = 'differential:update';
   const TYPE_ACTION = 'differential:action';
   const TYPE_STATUS = 'differential:status';
 
   public function getApplicationName() {
     return 'differential';
   }
 
   public function getApplicationTransactionType() {
     return DifferentialRevisionPHIDType::TYPECONST;
   }
 
   public function getApplicationTransactionCommentObject() {
     return new DifferentialTransactionComment();
   }
 
   public function getApplicationTransactionViewObject() {
     return new DifferentialTransactionView();
   }
 
   public function shouldHide() {
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
       case self::TYPE_UPDATE:
         // Older versions of this transaction have an ID for the new value,
         // and/or do not record the old value. Only hide the transaction if
         // the new value is a PHID, indicating that this is a newer style
         // transaction.
         if ($old === null) {
           if (phid_get_type($new) == DifferentialDiffPHIDType::TYPECONST) {
             return true;
           }
         }
         break;
 
       case PhabricatorTransactions::TYPE_EDGE:
         $add = array_diff_key($new, $old);
         $rem = array_diff_key($old, $new);
 
         // Hide metadata-only edge transactions. These correspond to users
         // accepting or rejecting revisions, but the change is always explicit
         // because of the TYPE_ACTION transaction. Rendering these transactions
         // just creates clutter.
 
         if (!$add && !$rem) {
           return true;
         }
         break;
     }
 
     return parent::shouldHide();
   }
 
   public function isInlineCommentTransaction() {
     switch ($this->getTransactionType()) {
       case self::TYPE_INLINE:
         return true;
     }
 
     return parent::isInlineCommentTransaction();
   }
 
   public function getRequiredHandlePHIDs() {
     $phids = parent::getRequiredHandlePHIDs();
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
       case self::TYPE_ACTION:
         if ($new == DifferentialAction::ACTION_CLOSE &&
             $this->getMetadataValue('isCommitClose')) {
           $phids[] = $this->getMetadataValue('commitPHID');
           if ($this->getMetadataValue('committerPHID')) {
             $phids[] = $this->getMetadataValue('committerPHID');
           }
           if ($this->getMetadataValue('authorPHID')) {
             $phids[] = $this->getMetadataValue('authorPHID');
           }
         }
         break;
       case self::TYPE_UPDATE:
         if ($new) {
           $phids[] = $new;
         }
         break;
     }
 
     return $phids;
   }
 
   public function getActionStrength() {
 
     switch ($this->getTransactionType()) {
       case self::TYPE_ACTION:
         return 3;
       case self::TYPE_UPDATE:
         return 2;
     }
 
     return parent::getActionStrength();
   }
 
 
   public function getActionName() {
     switch ($this->getTransactionType()) {
       case self::TYPE_INLINE:
         return pht('Commented On');
       case self::TYPE_UPDATE:
         $old = $this->getOldValue();
         if ($old === null) {
           return pht('Request');
         } else {
           return pht('Updated');
         }
       case self::TYPE_ACTION:
         $map = array(
           DifferentialAction::ACTION_ACCEPT => pht('Accepted'),
           DifferentialAction::ACTION_REJECT => pht('Requested Changes To'),
           DifferentialAction::ACTION_RETHINK => pht('Planned Changes To'),
           DifferentialAction::ACTION_ABANDON => pht('Abandoned'),
           DifferentialAction::ACTION_CLOSE => pht('Closed'),
           DifferentialAction::ACTION_REQUEST => pht('Requested A Review Of'),
           DifferentialAction::ACTION_RESIGN => pht('Resigned From'),
           DifferentialAction::ACTION_ADDREVIEWERS => pht('Added Reviewers'),
           DifferentialAction::ACTION_CLAIM => pht('Commandeered'),
           DifferentialAction::ACTION_REOPEN => pht('Reopened'),
         );
         $name = idx($map, $this->getNewValue());
         if ($name !== null) {
           return $name;
         }
         break;
     }
 
     return parent::getActionName();
   }
 
   public function getMailTags() {
     $tags = array();
 
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_SUBSCRIBERS;
         $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_CC;
         break;
       case self::TYPE_ACTION:
         switch ($this->getNewValue()) {
           case DifferentialAction::ACTION_CLOSE:
             $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_CLOSED;
             break;
         }
         break;
       case self::TYPE_UPDATE:
         $old = $this->getOldValue();
         if ($old === null) {
           $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_REVIEW_REQUEST;
         } else {
           $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_UPDATED;
         }
         break;
       case PhabricatorTransactions::TYPE_EDGE:
         switch ($this->getMetadataValue('edge:type')) {
           case DifferentialRevisionHasReviewerEdgeType::EDGECONST:
             $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_REVIEWERS;
             break;
         }
         break;
       case PhabricatorTransactions::TYPE_COMMENT:
       case self::TYPE_INLINE:
         $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_COMMENT;
         break;
     }
 
     if (!$tags) {
       $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_OTHER;
     }
 
     return $tags;
   }
 
   public function getTitle() {
     $author_phid = $this->getAuthorPHID();
     $author_handle = $this->renderHandleLink($author_phid);
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
       case self::TYPE_INLINE:
         return pht(
           '%s added inline comments.',
           $author_handle);
       case self::TYPE_UPDATE:
         if ($this->getMetadataValue('isCommitUpdate')) {
           return pht(
             'This revision was automatically updated to reflect the '.
             'committed changes.');
         } else if ($new) {
           // TODO: Migrate to PHIDs and use handles here?
           if (phid_get_type($new) == DifferentialDiffPHIDType::TYPECONST) {
             return pht(
               '%s updated this revision to %s.',
               $author_handle,
               $this->renderHandleLink($new));
           } else {
             return pht(
               '%s updated this revision.',
               $author_handle);
           }
         } else {
           return pht(
             '%s updated this revision.',
             $author_handle);
         }
       case self::TYPE_ACTION:
         switch ($new) {
           case DifferentialAction::ACTION_CLOSE:
             if (!$this->getMetadataValue('isCommitClose')) {
               return DifferentialAction::getBasicStoryText(
                 $new,
                 $author_handle);
             }
             $commit_name = $this->renderHandleLink(
               $this->getMetadataValue('commitPHID'));
             $committer_phid = $this->getMetadataValue('committerPHID');
             $author_phid = $this->getMetadataValue('authorPHID');
             if ($this->getHandleIfExists($committer_phid)) {
               $committer_name = $this->renderHandleLink($committer_phid);
             } else {
               $committer_name = $this->getMetadataValue('committerName');
             }
             if ($this->getHandleIfExists($author_phid)) {
               $author_name = $this->renderHandleLink($author_phid);
             } else {
               $author_name = $this->getMetadataValue('authorName');
             }
 
             if ($committer_name && ($committer_name != $author_name)) {
               return pht(
                 'Closed by commit %s (authored by %s, committed by %s).',
                 $commit_name,
                 $author_name,
                 $committer_name);
             } else {
               return pht(
                 'Closed by commit %s (authored by %s).',
                 $commit_name,
                 $author_name);
             }
             break;
           default:
             return DifferentialAction::getBasicStoryText($new, $author_handle);
         }
         break;
       case self::TYPE_STATUS:
         switch ($this->getNewValue()) {
           case ArcanistDifferentialRevisionStatus::ACCEPTED:
             return pht(
               'This revision is now accepted and ready to land.');
           case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
             return pht(
               'This revision now requires changes to proceed.');
           case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
             return pht(
               'This revision now requires review to proceed.');
         }
     }
 
     return parent::getTitle();
   }
 
   public function renderExtraInformationLink() {
     if ($this->getMetadataValue('revisionMatchData')) {
       $details_href =
         '/differential/revision/closedetails/'.$this->getPHID().'/';
       $details_link = javelin_tag(
         'a',
         array(
           'href' => $details_href,
           'sigil' => 'workflow',
         ),
         pht('Explain Why'));
       return $details_link;
     }
     return parent::renderExtraInformationLink();
   }
 
   public function getTitleForFeed() {
     $author_phid = $this->getAuthorPHID();
     $object_phid = $this->getObjectPHID();
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     $author_link = $this->renderHandleLink($author_phid);
     $object_link = $this->renderHandleLink($object_phid);
 
     switch ($this->getTransactionType()) {
       case self::TYPE_INLINE:
         return pht(
           '%s added inline comments to %s.',
           $author_link,
           $object_link);
       case self::TYPE_UPDATE:
         return pht(
           '%s updated the diff for %s.',
           $author_link,
           $object_link);
       case self::TYPE_ACTION:
         switch ($new) {
           case DifferentialAction::ACTION_ACCEPT:
             return pht(
               '%s accepted %s.',
               $author_link,
               $object_link);
           case DifferentialAction::ACTION_REJECT:
             return pht(
               '%s requested changes to %s.',
               $author_link,
               $object_link);
           case DifferentialAction::ACTION_RETHINK:
             return pht(
               '%s planned changes to %s.',
               $author_link,
               $object_link);
           case DifferentialAction::ACTION_ABANDON:
             return pht(
               '%s abandoned %s.',
               $author_link,
               $object_link);
           case DifferentialAction::ACTION_CLOSE:
             if (!$this->getMetadataValue('isCommitClose')) {
               return pht(
                 '%s closed %s.',
                 $author_link,
                 $object_link);
             } else {
               $commit_name = $this->renderHandleLink(
                 $this->getMetadataValue('commitPHID'));
               $committer_phid = $this->getMetadataValue('committerPHID');
               $author_phid = $this->getMetadataValue('authorPHID');
 
               if ($this->getHandleIfExists($committer_phid)) {
                 $committer_name = $this->renderHandleLink($committer_phid);
               } else {
                 $committer_name = $this->getMetadataValue('committerName');
               }
 
               if ($this->getHandleIfExists($author_phid)) {
                 $author_name = $this->renderHandleLink($author_phid);
               } else {
                 $author_name = $this->getMetadataValue('authorName');
               }
 
               // Check if the committer and author are the same. They're the
               // same if both resolved and are the same user, or if neither
               // resolved and the text is identical.
               if ($committer_phid && $author_phid) {
                 $same_author = ($committer_phid == $author_phid);
               } else if (!$committer_phid && !$author_phid) {
                 $same_author = ($committer_name == $author_name);
               } else {
                 $same_author = false;
               }
 
               if ($committer_name && !$same_author) {
                 return pht(
                   '%s closed %s by committing %s (authored by %s).',
                   $author_link,
                   $object_link,
                   $commit_name,
                   $author_name);
               } else {
                 return pht(
                   '%s closed %s by committing %s.',
                   $author_link,
                   $object_link,
                   $commit_name);
               }
             }
             break;
 
           case DifferentialAction::ACTION_REQUEST:
             return pht(
               '%s requested review of %s.',
               $author_link,
               $object_link);
           case DifferentialAction::ACTION_RECLAIM:
             return pht(
               '%s reclaimed %s.',
               $author_link,
               $object_link);
           case DifferentialAction::ACTION_RESIGN:
             return pht(
               '%s resigned from %s.',
               $author_link,
               $object_link);
           case DifferentialAction::ACTION_CLAIM:
             return pht(
               '%s commandeered %s.',
               $author_link,
               $object_link);
           case DifferentialAction::ACTION_REOPEN:
             return pht(
               '%s reopened %s.',
               $author_link,
               $object_link);
         }
         break;
       case self::TYPE_STATUS:
         switch ($this->getNewValue()) {
           case ArcanistDifferentialRevisionStatus::ACCEPTED:
             return pht(
               '%s is now accepted and ready to land.',
               $object_link);
           case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
             return pht(
               '%s now requires changes to proceed.',
               $object_link);
           case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
             return pht(
               '%s now requires review to proceed.',
               $object_link);
         }
     }
 
     return parent::getTitleForFeed();
   }
 
   public function getIcon() {
     switch ($this->getTransactionType()) {
       case self::TYPE_INLINE:
         return 'fa-comment';
       case self::TYPE_UPDATE:
         return 'fa-refresh';
       case self::TYPE_STATUS:
         switch ($this->getNewValue()) {
           case ArcanistDifferentialRevisionStatus::ACCEPTED:
             return 'fa-check';
           case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
             return 'fa-times';
           case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
             return 'fa-undo';
         }
         break;
       case self::TYPE_ACTION:
         switch ($this->getNewValue()) {
           case DifferentialAction::ACTION_CLOSE:
             return 'fa-check';
           case DifferentialAction::ACTION_ACCEPT:
             return 'fa-check-circle-o';
           case DifferentialAction::ACTION_REJECT:
             return 'fa-times-circle-o';
           case DifferentialAction::ACTION_ABANDON:
             return 'fa-plane';
           case DifferentialAction::ACTION_RETHINK:
             return 'fa-headphones';
           case DifferentialAction::ACTION_REQUEST:
             return 'fa-refresh';
           case DifferentialAction::ACTION_RECLAIM:
           case DifferentialAction::ACTION_REOPEN:
             return 'fa-bullhorn';
           case DifferentialAction::ACTION_RESIGN:
             return 'fa-flag';
           case DifferentialAction::ACTION_CLAIM:
             return 'fa-flag';
         }
       case PhabricatorTransactions::TYPE_EDGE:
         switch ($this->getMetadataValue('edge:type')) {
           case DifferentialRevisionHasReviewerEdgeType::EDGECONST:
             return 'fa-user';
         }
     }
 
     return parent::getIcon();
   }
 
   public function shouldDisplayGroupWith(array $group) {
 
     // Never group status changes with other types of actions, they're indirect
     // and don't make sense when combined with direct actions.
 
     $type_status = self::TYPE_STATUS;
 
     if ($this->getTransactionType() == $type_status) {
       return false;
     }
 
     foreach ($group as $xaction) {
       if ($xaction->getTransactionType() == $type_status) {
         return false;
       }
     }
 
     return parent::shouldDisplayGroupWith($group);
   }
 
 
   public function getColor() {
     switch ($this->getTransactionType()) {
       case self::TYPE_UPDATE:
         return PhabricatorTransactions::COLOR_SKY;
       case self::TYPE_STATUS:
         switch ($this->getNewValue()) {
           case ArcanistDifferentialRevisionStatus::ACCEPTED:
             return PhabricatorTransactions::COLOR_GREEN;
           case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
             return PhabricatorTransactions::COLOR_RED;
           case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
             return PhabricatorTransactions::COLOR_ORANGE;
         }
         break;
       case self::TYPE_ACTION:
         switch ($this->getNewValue()) {
           case DifferentialAction::ACTION_CLOSE:
             return PhabricatorTransactions::COLOR_INDIGO;
           case DifferentialAction::ACTION_ACCEPT:
             return PhabricatorTransactions::COLOR_GREEN;
           case DifferentialAction::ACTION_REJECT:
             return PhabricatorTransactions::COLOR_RED;
           case DifferentialAction::ACTION_ABANDON:
             return PhabricatorTransactions::COLOR_INDIGO;
           case DifferentialAction::ACTION_RETHINK:
             return PhabricatorTransactions::COLOR_RED;
           case DifferentialAction::ACTION_REQUEST:
             return PhabricatorTransactions::COLOR_SKY;
           case DifferentialAction::ACTION_RECLAIM:
             return PhabricatorTransactions::COLOR_SKY;
           case DifferentialAction::ACTION_REOPEN:
             return PhabricatorTransactions::COLOR_SKY;
           case DifferentialAction::ACTION_RESIGN:
             return PhabricatorTransactions::COLOR_ORANGE;
           case DifferentialAction::ACTION_CLAIM:
             return PhabricatorTransactions::COLOR_YELLOW;
         }
     }
 
 
     return parent::getColor();
   }
 
   public function getNoEffectDescription() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_EDGE:
         switch ($this->getMetadataValue('edge:type')) {
           case DifferentialRevisionHasReviewerEdgeType::EDGECONST:
             return pht(
               'The reviewers you are trying to add are already reviewing '.
               'this revision.');
         }
         break;
-      case DifferentialTransaction::TYPE_ACTION:
+      case self::TYPE_ACTION:
         switch ($this->getNewValue()) {
           case DifferentialAction::ACTION_CLOSE:
             return pht('This revision is already closed.');
           case DifferentialAction::ACTION_ABANDON:
             return pht('This revision has already been abandoned.');
           case DifferentialAction::ACTION_RECLAIM:
             return pht(
               'You can not reclaim this revision because his revision is '.
               'not abandoned.');
           case DifferentialAction::ACTION_REOPEN:
             return pht(
               'You can not reopen this revision because this revision is '.
               'not closed.');
           case DifferentialAction::ACTION_RETHINK:
             return pht('This revision already requires changes.');
           case DifferentialAction::ACTION_REQUEST:
             return pht('Review is already requested for this revision.');
           case DifferentialAction::ACTION_RESIGN:
             return pht(
               'You can not resign from this revision because you are not '.
               'a reviewer.');
           case DifferentialAction::ACTION_CLAIM:
             return pht(
               'You can not commandeer this revision because you already own '.
               'it.');
           case DifferentialAction::ACTION_ACCEPT:
             return pht(
               'You have already accepted this revision.');
           case DifferentialAction::ACTION_REJECT:
             return pht(
               'You have already requested changes to this revision.');
         }
         break;
     }
 
     return parent::getNoEffectDescription();
   }
 
   public function renderAsTextForDoorkeeper(
     DoorkeeperFeedStoryPublisher $publisher,
     PhabricatorFeedStory $story,
     array $xactions) {
 
     $body = parent::renderAsTextForDoorkeeper($publisher, $story, $xactions);
 
     $inlines = array();
     foreach ($xactions as $xaction) {
       if ($xaction->getTransactionType() == self::TYPE_INLINE) {
         $inlines[] = $xaction;
       }
     }
 
     // TODO: This is a bit gross, but far less bad than it used to be. It
     // could be further cleaned up at some point.
 
     if ($inlines) {
       $engine = PhabricatorMarkupEngine::newMarkupEngine(array())
         ->setConfig('viewer', new PhabricatorUser())
         ->setMode(PhutilRemarkupEngine::MODE_TEXT);
 
       $body .= "\n\n";
       $body .= pht('Inline Comments');
       $body .= "\n";
 
       $changeset_ids = array();
       foreach ($inlines as $inline) {
         $changeset_ids[] = $inline->getComment()->getChangesetID();
       }
 
       $changesets = id(new DifferentialChangeset())->loadAllWhere(
         'id IN (%Ld)',
         $changeset_ids);
 
       foreach ($inlines as $inline) {
         $comment = $inline->getComment();
         $changeset = idx($changesets, $comment->getChangesetID());
         if (!$changeset) {
           continue;
         }
 
         $filename = $changeset->getDisplayFilename();
         $linenumber = $comment->getLineNumber();
         $inline_text = $engine->markupText($comment->getContent());
         $inline_text = rtrim($inline_text);
 
         $body .= "{$filename}:{$linenumber} {$inline_text}\n";
       }
     }
 
     return $body;
   }
 
 
 }
diff --git a/src/applications/differential/storage/__tests__/DifferentialAdjustmentMapTestCase.php b/src/applications/differential/storage/__tests__/DifferentialAdjustmentMapTestCase.php
new file mode 100644
index 0000000000..8fc28953fc
--- /dev/null
+++ b/src/applications/differential/storage/__tests__/DifferentialAdjustmentMapTestCase.php
@@ -0,0 +1,294 @@
+<?php
+
+final class DifferentialAdjustmentMapTestCase extends ArcanistPhutilTestCase {
+
+  public function testBasicMaps() {
+    $change_map = array(
+      1 => array(1),
+      2 => array(2),
+      3 => array(3),
+      4 => array(),
+      5 => array(),
+      6 => array(),
+      7 => array(4),
+      8 => array(5),
+      9 => array(6),
+      10 => array(7),
+      11 => array(8),
+      12 => array(9),
+      13 => array(10),
+      14 => array(11),
+      15 => array(12),
+      16 => array(13),
+      17 => array(14),
+      18 => array(15),
+      19 => array(16),
+      20 => array(17, 20),
+      21 => array(21),
+      22 => array(22),
+      23 => array(23),
+      24 => array(24),
+      25 => array(25),
+      26 => array(26),
+    );
+
+    $hunks = $this->loadHunks('add.diff');
+    $this->assertEqual(
+      array(
+        0 => array(1, 26),
+      ),
+      DifferentialLineAdjustmentMap::newFromHunks($hunks)->getMap());
+
+    $hunks = $this->loadHunks('change.diff');
+    $this->assertEqual(
+      $change_map,
+      DifferentialLineAdjustmentMap::newFromHunks($hunks)->getMap());
+
+    $hunks = $this->loadHunks('remove.diff');
+    $this->assertEqual(
+      array_fill_keys(range(1, 26), array()),
+      DifferentialLineAdjustmentMap::newFromHunks($hunks)->getMap());
+
+    // With the contextless diff, we don't get the last few similar lines
+    // in the map.
+    $reduced_map = $change_map;
+    unset($reduced_map[24]);
+    unset($reduced_map[25]);
+    unset($reduced_map[26]);
+
+    $hunks = $this->loadHunks('context.diff');
+    $this->assertEqual(
+      $reduced_map,
+      DifferentialLineAdjustmentMap::newFromHunks($hunks)->getMap());
+  }
+
+
+  public function testInverseMaps() {
+    $change_map = array(
+      1 => array(1),
+      2 => array(2),
+      3 => array(3, 6),
+      4 => array(7),
+      5 => array(8),
+      6 => array(9),
+      7 => array(10),
+      8 => array(11),
+      9 => array(12),
+      10 => array(13),
+      11 => array(14),
+      12 => array(15),
+      13 => array(16),
+      14 => array(17),
+      15 => array(18),
+      16 => array(19),
+      17 => array(20),
+      18 => array(),
+      19 => array(),
+      20 => array(),
+      21 => array(21),
+      22 => array(22),
+      23 => array(23),
+      24 => array(24),
+      25 => array(25),
+      26 => array(26),
+    );
+
+    $hunks = $this->loadHunks('add.diff');
+    $this->assertEqual(
+      array_fill_keys(range(1, 26), array()),
+      DifferentialLineAdjustmentMap::newInverseMap(
+        DifferentialLineAdjustmentMap::newFromHunks($hunks))->getMap());
+
+    $hunks = $this->loadHunks('change.diff');
+    $this->assertEqual(
+      $change_map,
+      DifferentialLineAdjustmentMap::newInverseMap(
+        DifferentialLineAdjustmentMap::newFromHunks($hunks))->getMap());
+
+    $hunks = $this->loadHunks('remove.diff');
+    $this->assertEqual(
+      array(
+        0 => array(1, 26),
+      ),
+      DifferentialLineAdjustmentMap::newInverseMap(
+        DifferentialLineAdjustmentMap::newFromHunks($hunks))->getMap());
+
+    // With the contextless diff, we don't get the last few similar lines
+    // in the map.
+    $reduced_map = $change_map;
+    unset($reduced_map[24]);
+    unset($reduced_map[25]);
+    unset($reduced_map[26]);
+
+    $hunks = $this->loadHunks('context.diff');
+    $this->assertEqual(
+      $reduced_map,
+      DifferentialLineAdjustmentMap::newInverseMap(
+        DifferentialLineAdjustmentMap::newFromHunks($hunks))->getMap());
+  }
+
+
+  public function testNearestMaps() {
+    $change_map = array(
+      1 => array(1),
+      2 => array(2),
+      3 => array(3),
+      4 => array(-3, -4),
+      5 => array(-3, -4),
+      6 => array(-3, -4),
+      7 => array(4),
+      8 => array(5),
+      9 => array(6),
+      10 => array(7),
+      11 => array(8),
+      12 => array(9),
+      13 => array(10),
+      14 => array(11),
+      15 => array(12),
+      16 => array(13),
+      17 => array(14),
+      18 => array(15),
+      19 => array(16),
+      20 => array(17, 20),
+      21 => array(21),
+      22 => array(22),
+      23 => array(23),
+      24 => array(24),
+      25 => array(25),
+      26 => array(26),
+    );
+
+    $hunks = $this->loadHunks('add.diff');
+    $map = DifferentialLineAdjustmentMap::newFromHunks($hunks);
+    $this->assertEqual(
+      array(
+        0 => array(1, 26),
+      ),
+      $map->getNearestMap());
+    $this->assertEqual(26, $map->getFinalOffset());
+
+
+    $hunks = $this->loadHunks('change.diff');
+    $map = DifferentialLineAdjustmentMap::newFromHunks($hunks);
+    $this->assertEqual(
+      $change_map,
+      $map->getNearestMap());
+    $this->assertEqual(0, $map->getFinalOffset());
+
+
+    $hunks = $this->loadHunks('remove.diff');
+    $map = DifferentialLineAdjustmentMap::newFromHunks($hunks);
+    $this->assertEqual(
+      array_fill_keys(
+        range(1, 26),
+        array(0, 0)),
+      $map->getNearestMap());
+    $this->assertEqual(-26, $map->getFinalOffset());
+
+
+    $reduced_map = $change_map;
+    unset($reduced_map[24]);
+    unset($reduced_map[25]);
+    unset($reduced_map[26]);
+
+    $hunks = $this->loadHunks('context.diff');
+    $map = DifferentialLineAdjustmentMap::newFromHunks($hunks);
+    $this->assertEqual(
+      $reduced_map,
+      $map->getNearestMap());
+    $this->assertEqual(0, $map->getFinalOffset());
+
+
+    $hunks = $this->loadHunks('insert.diff');
+    $map = DifferentialLineAdjustmentMap::newFromHunks($hunks);
+    $this->assertEqual(
+      array(
+        1 => array(1),
+        2 => array(2),
+        3 => array(3),
+        4 => array(4),
+        5 => array(5),
+        6 => array(6),
+        7 => array(7),
+        8 => array(8),
+        9 => array(9),
+        10 => array(10, 13),
+        11 => array(14),
+        12 => array(15),
+        13 => array(16),
+      ),
+      $map->getNearestMap());
+    $this->assertEqual(3, $map->getFinalOffset());
+  }
+
+
+  public function testChainMaps() {
+    // This test simulates porting inlines forward across a rebase.
+    // Part 1 is the original diff.
+    // Part 2 is the rebase, which we would normally compute synthetically.
+    // Part 3 is the updated diff against the rebased changes.
+
+    $diff1 = $this->loadHunks('chain.adjust.1.diff');
+    $diff2 = $this->loadHunks('chain.adjust.2.diff');
+    $diff3 = $this->loadHunks('chain.adjust.3.diff');
+
+    $map = DifferentialLineAdjustmentMap::newInverseMap(
+      DifferentialLineAdjustmentMap::newFromHunks($diff1));
+
+    $map->addMapToChain(
+        DifferentialLineAdjustmentMap::newFromHunks($diff2));
+
+    $map->addMapToChain(
+      DifferentialLineAdjustmentMap::newFromHunks($diff3));
+
+    $actual = array();
+    for ($ii = 1; $ii <= 13; $ii++) {
+      $actual[$ii] = array(
+        $map->mapLine($ii, false),
+        $map->mapLine($ii, true),
+      );
+    }
+
+    $this->assertEqual(
+      array(
+        1 => array(array(false, false, 1), array(false, false, 1)),
+        2 => array(array(true, false, 1), array(true, false, 2)),
+        3 => array(array(true, false, 1), array(true, false, 2)),
+        4 => array(array(false, false, 2), array(false, false, 2)),
+        5 => array(array(false, false, 3), array(false, false, 3)),
+        6 => array(array(false, false, 4), array(false, false, 4)),
+        7 => array(array(false, false, 5), array(false, false, 8)),
+        8 => array(array(false, 0, 5), array(false, false, 9)),
+        9 => array(array(false, 1, 5), array(false, false, 9)),
+        10 => array(array(false, 2, 5), array(false, false, 9)),
+        11 => array(array(false, false, 9), array(false, false, 9)),
+        12 => array(array(false, false, 10), array(false, false, 10)),
+        13 => array(array(false, false, 11), array(false, false, 11)),
+      ),
+      $actual);
+  }
+
+
+  private function loadHunks($name) {
+    $root = dirname(__FILE__).'/map/';
+    $data = Filesystem::readFile($root.$name);
+
+    $parser = new ArcanistDiffParser();
+    $changes = $parser->parseDiff($data);
+
+    $viewer = PhabricatorUser::getOmnipotentUser();
+    $diff = DifferentialDiff::newFromRawChanges($viewer, $changes);
+
+    $changesets = $diff->getChangesets();
+    if (count($changesets) !== 1) {
+      throw new Exception(
+        pht(
+          'Expected exactly one changeset from "%s".',
+          $name));
+    }
+    $changeset = head($changesets);
+
+    return $changeset->getHunks();
+  }
+
+}
diff --git a/src/applications/differential/storage/__tests__/map/add.diff b/src/applications/differential/storage/__tests__/map/add.diff
new file mode 100644
index 0000000000..97e60a8b7c
--- /dev/null
+++ b/src/applications/differential/storage/__tests__/map/add.diff
@@ -0,0 +1,32 @@
+diff --git a/alphabet b/alphabet
+new file mode 100644
+index 0000000..0edb856
+--- /dev/null
++++ b/alphabet
+@@ -0,0 +1,26 @@
++a
++b
++c
++d
++e
++f
++g
++h
++i
++j
++k
++l
++m
++n
++o
++p
++q
++r
++s
++t
++u
++v
++w
++x
++y
++z
diff --git a/src/applications/differential/storage/__tests__/map/chain.adjust.1.diff b/src/applications/differential/storage/__tests__/map/chain.adjust.1.diff
new file mode 100644
index 0000000000..8370a66e1a
--- /dev/null
+++ b/src/applications/differential/storage/__tests__/map/chain.adjust.1.diff
@@ -0,0 +1,14 @@
+diff --git a/alphabet b/alphabet
+index 92dfa21..292798b 100644
+--- a/alphabet
++++ b/alphabet
+@@ -5,6 +5,9 @@ d
+ e
+ f
+ g
++G1
++G2
++G3
+ h
+ i
+ j
diff --git a/src/applications/differential/storage/__tests__/map/chain.adjust.2.diff b/src/applications/differential/storage/__tests__/map/chain.adjust.2.diff
new file mode 100644
index 0000000000..ac6f8c854a
--- /dev/null
+++ b/src/applications/differential/storage/__tests__/map/chain.adjust.2.diff
@@ -0,0 +1,11 @@
+diff --git a/alphabet b/alphabet
+index 92dfa21..e3344af 100644
+--- a/alphabet
++++ b/alphabet
+@@ -1,6 +1,4 @@
+ a
+-b
+-c
+ d
+ e
+ f
diff --git a/src/applications/differential/storage/__tests__/map/chain.adjust.3.diff b/src/applications/differential/storage/__tests__/map/chain.adjust.3.diff
new file mode 100644
index 0000000000..4d23f185fd
--- /dev/null
+++ b/src/applications/differential/storage/__tests__/map/chain.adjust.3.diff
@@ -0,0 +1,14 @@
+diff --git a/alphabet b/alphabet
+index e3344af..febfe3e 100644
+--- a/alphabet
++++ b/alphabet
+@@ -3,6 +3,9 @@ d
+ e
+ f
+ g
++G1x
++G2x
++G3x
+ h
+ i
+ j
diff --git a/src/applications/differential/storage/__tests__/map/change.diff b/src/applications/differential/storage/__tests__/map/change.diff
new file mode 100644
index 0000000000..7ef945267f
--- /dev/null
+++ b/src/applications/differential/storage/__tests__/map/change.diff
@@ -0,0 +1,34 @@
+diff --git a/alphabet b/alphabet
+index 0edb856..2449de2 100644
+--- a/alphabet
++++ b/alphabet
+@@ -1,26 +1,26 @@
+ a
+ b
+ c
+-d
+-e
+-f
+ g
+ h
+ i
+ j
+ k
+ l
+ m
+ n
+ o
+ p
+ q
+ r
+ s
+ t
++tx
++ty
++tz
+ u
+ v
+ w
+ x
+ y
+ z
diff --git a/src/applications/differential/storage/__tests__/map/context.diff b/src/applications/differential/storage/__tests__/map/context.diff
new file mode 100644
index 0000000000..ab77e4a9ba
--- /dev/null
+++ b/src/applications/differential/storage/__tests__/map/context.diff
@@ -0,0 +1,24 @@
+diff --git a/alphabet b/alphabet
+index 0edb856..2449de2 100644
+--- a/alphabet
++++ b/alphabet
+@@ -1,9 +1,6 @@
+ a
+ b
+ c
+-d
+-e
+-f
+ g
+ h
+ i
+@@ -18,6 +15,9 @@ q
+ r
+ s
+ t
++tx
++ty
++tz
+ u
+ v
+ w
diff --git a/src/applications/differential/storage/__tests__/map/insert.diff b/src/applications/differential/storage/__tests__/map/insert.diff
new file mode 100644
index 0000000000..9a726955e7
--- /dev/null
+++ b/src/applications/differential/storage/__tests__/map/insert.diff
@@ -0,0 +1,14 @@
+diff --git a/alphabet b/alphabet
+index f2b41ef..755b349 100644
+--- a/alphabet
++++ b/alphabet
+@@ -8,6 +8,9 @@ g
+ h
+ i
+ j
++j1
++j2
++j3
+ k
+ l
+ n
diff --git a/src/applications/differential/storage/__tests__/map/remove.diff b/src/applications/differential/storage/__tests__/map/remove.diff
new file mode 100644
index 0000000000..0feafbbfc3
--- /dev/null
+++ b/src/applications/differential/storage/__tests__/map/remove.diff
@@ -0,0 +1,32 @@
+diff --git a/alphabet b/alphabet
+deleted file mode 100644
+index 2449de2..0000000
+--- a/alphabet
++++ /dev/null
+@@ -1,26 +0,0 @@
+-a
+-b
+-c
+-g
+-h
+-i
+-j
+-k
+-l
+-m
+-n
+-o
+-p
+-q
+-r
+-s
+-t
+-tx
+-ty
+-tz
+-u
+-v
+-w
+-x
+-y
+-z
diff --git a/src/applications/differential/view/DifferentialLocalCommitsView.php b/src/applications/differential/view/DifferentialLocalCommitsView.php
index 489674b0ab..ab83016e2a 100644
--- a/src/applications/differential/view/DifferentialLocalCommitsView.php
+++ b/src/applications/differential/view/DifferentialLocalCommitsView.php
@@ -1,156 +1,156 @@
 <?php
 
 final class DifferentialLocalCommitsView extends AphrontView {
 
   private $localCommits;
   private $commitsForLinks = array();
 
   public function setLocalCommits($local_commits) {
     $this->localCommits = $local_commits;
     return $this;
   }
 
   public function setCommitsForLinks(array $commits) {
     assert_instances_of($commits, 'PhabricatorRepositoryCommit');
     $this->commitsForLinks = $commits;
     return $this;
   }
 
   public function render() {
     $user = $this->user;
     if (!$user) {
-      throw new Exception('Call setUser() before render()-ing this view.');
+      throw new PhutilInvalidStateException('setUser');
     }
 
     $local = $this->localCommits;
     if (!$local) {
       return null;
     }
 
     $has_tree = false;
     $has_local = false;
 
     foreach ($local as $commit) {
       if (idx($commit, 'tree')) {
         $has_tree = true;
       }
       if (idx($commit, 'local')) {
         $has_local = true;
       }
     }
 
     $rows = array();
     foreach ($local as $commit) {
       $row = array();
       if (idx($commit, 'commit')) {
         $commit_link = $this->buildCommitLink($commit['commit']);
       } else if (isset($commit['rev'])) {
         $commit_link = $this->buildCommitLink($commit['rev']);
       } else {
         $commit_link = null;
       }
       $row[] = $commit_link;
 
       if ($has_tree) {
         $row[] = $this->buildCommitLink($commit['tree']);
       }
 
       if ($has_local) {
         $row[] = $this->buildCommitLink($commit['local']);
       }
 
       $parents = idx($commit, 'parents', array());
       foreach ($parents as $k => $parent) {
         if (is_array($parent)) {
           $parent = idx($parent, 'rev');
         }
         $parents[$k] = $this->buildCommitLink($parent);
       }
       $parents = phutil_implode_html(phutil_tag('br'), $parents);
       $row[] = $parents;
 
       $author = nonempty(
         idx($commit, 'user'),
         idx($commit, 'author'));
       $row[] = $author;
 
       $message = idx($commit, 'message');
 
       $summary = idx($commit, 'summary');
       $summary = id(new PhutilUTF8StringTruncator())
         ->setMaximumGlyphs(80)
         ->truncateString($summary);
 
       $view = new AphrontMoreView();
       $view->setSome($summary);
 
       if ($message && (trim($summary) != trim($message))) {
         $view->setMore(phutil_escape_html_newlines($message));
       }
 
       $row[] = $view->render();
 
       $date = nonempty(
         idx($commit, 'date'),
         idx($commit, 'time'));
       if ($date) {
         $date = phabricator_datetime($date, $user);
       }
       $row[] = $date;
 
       $rows[] = $row;
     }
 
     $column_classes = array('');
     if ($has_tree) {
       $column_classes[] = '';
     }
     if ($has_local) {
       $column_classes[] = '';
     }
     $column_classes[] = '';
     $column_classes[] = '';
     $column_classes[] = 'wide';
     $column_classes[] = 'date';
     $table = id(new AphrontTableView($rows))
       ->setColumnClasses($column_classes);
     $headers = array();
     $headers[] = pht('Commit');
     if ($has_tree) {
       $headers[] = pht('Tree');
     }
     if ($has_local) {
       $headers[] = pht('Local');
     }
     $headers[] = pht('Parents');
     $headers[] = pht('Author');
     $headers[] = pht('Summary');
     $headers[] = pht('Date');
     $table->setHeaders($headers);
 
     return id(new PHUIObjectBoxView())
       ->setHeaderText(pht('Local Commits'))
       ->appendChild($table);
   }
 
   private static function formatCommit($commit) {
     return substr($commit, 0, 12);
   }
 
   private function buildCommitLink($hash) {
     $commit_for_link = idx($this->commitsForLinks, $hash);
     $commit_hash = self::formatCommit($hash);
     if ($commit_for_link) {
       $link = phutil_tag(
         'a',
         array(
           'href' => $commit_for_link->getURI(),
         ),
         $commit_hash);
     } else {
       $link = $commit_hash;
     }
     return $link;
   }
 
 }
diff --git a/src/applications/differential/view/DifferentialRevisionListView.php b/src/applications/differential/view/DifferentialRevisionListView.php
index 1dd5a5cc57..121792585d 100644
--- a/src/applications/differential/view/DifferentialRevisionListView.php
+++ b/src/applications/differential/view/DifferentialRevisionListView.php
@@ -1,208 +1,207 @@
 <?php
 
 /**
  * Render a table of Differential revisions.
  */
 final class DifferentialRevisionListView extends AphrontView {
 
   private $revisions;
   private $handles;
   private $highlightAge;
   private $header;
   private $noDataString;
   private $noBox;
 
   public function setNoDataString($no_data_string) {
     $this->noDataString = $no_data_string;
     return $this;
   }
 
   public function setHeader($header) {
     $this->header = $header;
     return $this;
   }
 
   public function setRevisions(array $revisions) {
     assert_instances_of($revisions, 'DifferentialRevision');
     $this->revisions = $revisions;
     return $this;
   }
 
   public function setHighlightAge($bool) {
     $this->highlightAge = $bool;
     return $this;
   }
 
   public function setNoBox($box) {
     $this->noBox = $box;
     return $this;
   }
 
   public function getRequiredHandlePHIDs() {
     $phids = array();
     foreach ($this->revisions as $revision) {
       $phids[] = array($revision->getAuthorPHID());
 
       // TODO: Switch to getReviewerStatus(), but not all callers pass us
       // revisions with this data loaded.
       $phids[] = $revision->getReviewers();
     }
     return array_mergev($phids);
   }
 
   public function setHandles(array $handles) {
     assert_instances_of($handles, 'PhabricatorObjectHandle');
     $this->handles = $handles;
     return $this;
   }
 
   public function render() {
-
     $user = $this->user;
     if (!$user) {
-      throw new Exception('Call setUser() before render()!');
+      throw new PhutilInvalidStateException('setUser');
     }
 
     $fresh = PhabricatorEnv::getEnvConfig('differential.days-fresh');
     if ($fresh) {
       $fresh = PhabricatorCalendarHoliday::getNthBusinessDay(
         time(),
         -$fresh);
     }
 
     $stale = PhabricatorEnv::getEnvConfig('differential.days-stale');
     if ($stale) {
       $stale = PhabricatorCalendarHoliday::getNthBusinessDay(
         time(),
         -$stale);
     }
 
     $this->initBehavior('phabricator-tooltips', array());
     $this->requireResource('aphront-tooltip-css');
 
     $list = new PHUIObjectItemListView();
 
     foreach ($this->revisions as $revision) {
       $item = id(new PHUIObjectItemView())
         ->setUser($user);
 
       $icons = array();
 
       $phid = $revision->getPHID();
       $flag = $revision->getFlag($user);
       if ($flag) {
         $flag_class = PhabricatorFlagColor::getCSSClass($flag->getColor());
         $icons['flag'] = phutil_tag(
           'div',
           array(
             'class' => 'phabricator-flag-icon '.$flag_class,
           ),
           '');
       }
 
       if ($revision->getDrafts($user)) {
         $icons['draft'] = true;
       }
 
       $modified = $revision->getDateModified();
 
       $status = $revision->getStatus();
       $show_age = ($fresh || $stale) &&
                   $this->highlightAge &&
                   !$revision->isClosed();
 
       if ($stale && $modified < $stale) {
         $object_age = PHUIObjectItemView::AGE_OLD;
       } else if ($fresh && $modified < $fresh) {
         $object_age = PHUIObjectItemView::AGE_STALE;
       } else {
         $object_age = PHUIObjectItemView::AGE_FRESH;
       }
 
       $status_name =
         ArcanistDifferentialRevisionStatus::getNameForRevisionStatus($status);
 
       if (isset($icons['flag'])) {
         $item->addHeadIcon($icons['flag']);
       }
 
       $item->setObjectName('D'.$revision->getID());
       $item->setHeader($revision->getTitle());
       $item->setHref('/D'.$revision->getID());
 
       if (isset($icons['draft'])) {
         $draft = id(new PHUIIconView())
           ->setIconFont('fa-comment yellow')
           ->addSigil('has-tooltip')
           ->setMetadata(
             array(
               'tip' => pht('Unsubmitted Comments'),
             ));
         $item->addAttribute($draft);
       }
 
       /* Most things 'Need Review', so accept it's the default */
       if ($status != ArcanistDifferentialRevisionStatus::NEEDS_REVIEW) {
         $item->addAttribute($status_name);
       }
 
       // Author
       $author_handle = $this->handles[$revision->getAuthorPHID()];
       $item->addByline(pht('Author: %s', $author_handle->renderLink()));
 
       $reviewers = array();
       // TODO: As above, this should be based on `getReviewerStatus()`.
       foreach ($revision->getReviewers() as $reviewer) {
         $reviewers[] = $this->handles[$reviewer]->renderLink();
       }
       if (!$reviewers) {
         $reviewers = phutil_tag('em', array(), pht('None'));
       } else {
         $reviewers = phutil_implode_html(', ', $reviewers);
       }
 
       $item->addAttribute(pht('Reviewers: %s', $reviewers));
       $item->setEpoch($revision->getDateModified(), $object_age);
 
       switch ($status) {
         case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
           break;
         case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
         case ArcanistDifferentialRevisionStatus::CHANGES_PLANNED:
           $item->setBarColor('red');
           break;
         case ArcanistDifferentialRevisionStatus::ACCEPTED:
           $item->setBarColor('green');
           break;
         case ArcanistDifferentialRevisionStatus::CLOSED:
           $item->setDisabled(true);
           break;
         case ArcanistDifferentialRevisionStatus::ABANDONED:
           $item->setBarColor('black');
           break;
       }
 
       $list->addItem($item);
     }
 
     $list->setNoDataString($this->noDataString);
 
 
     if ($this->header && !$this->noBox) {
       $list->setFlush(true);
       $list = id(new PHUIObjectBoxView())
         ->appendChild($list);
 
       if ($this->header instanceof PHUIHeaderView) {
         $list->setHeader($this->header);
       } else {
         $list->setHeaderText($this->header);
       }
     } else {
       $list->setHeader($this->header);
     }
 
     return $list;
   }
 
 }
diff --git a/src/applications/diffusion/data/DiffusionPathChange.php b/src/applications/diffusion/data/DiffusionPathChange.php
index b1d7286fdd..6f96057014 100644
--- a/src/applications/diffusion/data/DiffusionPathChange.php
+++ b/src/applications/diffusion/data/DiffusionPathChange.php
@@ -1,202 +1,202 @@
 <?php
 
 final class DiffusionPathChange {
 
   private $path;
   private $commitIdentifier;
   private $commit;
   private $commitData;
 
   private $changeType;
   private $fileType;
   private $targetPath;
   private $targetCommitIdentifier;
   private $awayPaths = array();
 
   final public function setPath($path) {
     $this->path = $path;
     return $this;
   }
 
   final public function getPath() {
     return $this->path;
   }
 
   public function setChangeType($change_type) {
     $this->changeType = $change_type;
     return $this;
   }
 
   public function getChangeType() {
     return $this->changeType;
   }
 
   public function setFileType($file_type) {
     $this->fileType = $file_type;
     return $this;
   }
 
   public function getFileType() {
     return $this->fileType;
   }
 
   public function setTargetPath($target_path) {
     $this->targetPath = $target_path;
     return $this;
   }
 
   public function getTargetPath() {
     return $this->targetPath;
   }
 
   public function setAwayPaths(array $away_paths) {
     $this->awayPaths = $away_paths;
     return $this;
   }
 
   public function getAwayPaths() {
     return $this->awayPaths;
   }
 
   final public function setCommitIdentifier($commit) {
     $this->commitIdentifier = $commit;
     return $this;
   }
 
   final public function getCommitIdentifier() {
     return $this->commitIdentifier;
   }
 
   final public function setTargetCommitIdentifier($target_commit_identifier) {
     $this->targetCommitIdentifier = $target_commit_identifier;
     return $this;
   }
 
   final public function getTargetCommitIdentifier() {
     return $this->targetCommitIdentifier;
   }
 
   final public function setCommit($commit) {
     $this->commit = $commit;
     return $this;
   }
 
   final public function getCommit() {
     return $this->commit;
   }
 
   final public function setCommitData($commit_data) {
     $this->commitData = $commit_data;
     return $this;
   }
 
   final public function getCommitData() {
     return $this->commitData;
   }
 
 
   final public function getEpoch() {
     if ($this->getCommit()) {
       return $this->getCommit()->getEpoch();
     }
     return null;
   }
 
   final public function getAuthorName() {
     if ($this->getCommitData()) {
       return $this->getCommitData()->getAuthorName();
     }
     return null;
   }
 
   final public function getSummary() {
     if (!$this->getCommitData()) {
       return null;
     }
     $message = $this->getCommitData()->getCommitMessage();
     $first = idx(explode("\n", $message), 0);
     return substr($first, 0, 80);
   }
 
   final public static function convertToArcanistChanges(array $changes) {
-    assert_instances_of($changes, 'DiffusionPathChange');
+    assert_instances_of($changes, __CLASS__);
     $direct = array();
     $result = array();
     foreach ($changes as $path) {
       $change = new ArcanistDiffChange();
       $change->setCurrentPath($path->getPath());
       $direct[] = $path->getPath();
       $change->setType($path->getChangeType());
       $file_type = $path->getFileType();
       if ($file_type == DifferentialChangeType::FILE_NORMAL) {
         $file_type = DifferentialChangeType::FILE_TEXT;
       }
       $change->setFileType($file_type);
       $change->setOldPath($path->getTargetPath());
       foreach ($path->getAwayPaths() as $away_path) {
         $change->addAwayPath($away_path);
       }
       $result[$path->getPath()] = $change;
     }
 
     return array_select_keys($result, $direct);
   }
 
   final public static function convertToDifferentialChangesets(
     PhabricatorUser $user,
     array $changes) {
-    assert_instances_of($changes, 'DiffusionPathChange');
+    assert_instances_of($changes, __CLASS__);
     $arcanist_changes = self::convertToArcanistChanges($changes);
     $diff = DifferentialDiff::newEphemeralFromRawChanges(
       $arcanist_changes);
     return $diff->getChangesets();
   }
 
   public function toDictionary() {
     $commit = $this->getCommit();
     if ($commit) {
       $commit_dict = $commit->toDictionary();
     } else {
       $commit_dict = array();
     }
     $commit_data = $this->getCommitData();
     if ($commit_data) {
       $commit_data_dict = $commit_data->toDictionary();
     } else {
       $commit_data_dict = array();
     }
     return array(
       'path' => $this->getPath(),
       'commitIdentifier' => $this->getCommitIdentifier(),
       'commit' => $commit_dict,
       'commitData' => $commit_data_dict,
       'fileType' => $this->getFileType(),
       'changeType' => $this->getChangeType(),
       'targetPath' =>  $this->getTargetPath(),
       'targetCommitIdentifier' => $this->getTargetCommitIdentifier(),
       'awayPaths' => $this->getAwayPaths(),
     );
   }
 
   public static function newFromConduit(array $dicts) {
     $results = array();
     foreach ($dicts as $dict) {
       $commit = PhabricatorRepositoryCommit::newFromDictionary($dict['commit']);
       $commit_data =
         PhabricatorRepositoryCommitData::newFromDictionary(
           $dict['commitData']);
       $results[] = id(new DiffusionPathChange())
         ->setPath($dict['path'])
         ->setCommitIdentifier($dict['commitIdentifier'])
         ->setCommit($commit)
         ->setCommitData($commit_data)
         ->setFileType($dict['fileType'])
         ->setChangeType($dict['changeType'])
         ->setTargetPath($dict['targetPath'])
         ->setTargetCommitIdentifier($dict['targetCommitIdentifier'])
         ->setAwayPaths($dict['awayPaths']);
     }
     return $results;
   }
 
 }
diff --git a/src/applications/diffusion/query/DiffusionLintCountQuery.php b/src/applications/diffusion/query/DiffusionLintCountQuery.php
index f85505af76..4441ccf3ef 100644
--- a/src/applications/diffusion/query/DiffusionLintCountQuery.php
+++ b/src/applications/diffusion/query/DiffusionLintCountQuery.php
@@ -1,123 +1,123 @@
 <?php
 
 final class DiffusionLintCountQuery extends PhabricatorQuery {
 
   private $branchIDs;
   private $paths;
   private $codes;
 
   public function withBranchIDs(array $branch_ids) {
     $this->branchIDs = $branch_ids;
     return $this;
   }
 
   public function withPaths(array $paths) {
     $this->paths = $paths;
     return $this;
   }
 
   public function withCodes(array $codes) {
     $this->codes = $codes;
     return $this;
   }
 
   public function execute() {
     if (!$this->paths) {
-      throw new Exception(pht('Call withPaths() before execute()!'));
+      throw new PhutilInvalidStateException('withPaths');
     }
 
     if (!$this->branchIDs) {
-      throw new Exception(pht('Call withBranchIDs() before execute()!'));
+      throw new PhutilInvalidStateException('withBranchIDs');
     }
 
     $conn_r = id(new PhabricatorRepositoryCommit())->establishConnection('r');
 
     $this->paths = array_unique($this->paths);
     list($dirs, $paths) = $this->processPaths();
 
     $parts = array();
     foreach ($dirs as $dir) {
       $parts[$dir] = qsprintf(
         $conn_r,
         'path LIKE %>',
         $dir);
     }
     foreach ($paths as $path) {
       $parts[$path] = qsprintf(
         $conn_r,
         'path = %s',
         $path);
     }
 
     $queries = array();
     foreach ($parts as $key => $part) {
       $queries[] = qsprintf(
         $conn_r,
         'SELECT %s path_prefix, COUNT(*) N FROM %T %Q',
         $key,
         PhabricatorRepository::TABLE_LINTMESSAGE,
         $this->buildCustomWhereClause($conn_r, $part));
     }
 
     $huge_union_query = '('.implode(') UNION ALL (', $queries).')';
 
     $data = queryfx_all(
       $conn_r,
       '%Q',
       $huge_union_query);
 
     return $this->processResults($data);
   }
 
   protected function buildCustomWhereClause(
     AphrontDatabaseConnection $conn_r,
     $part) {
 
     $where = array();
 
     $where[] = $part;
 
     if ($this->codes !== null) {
       $where[] = qsprintf(
         $conn_r,
         'code IN (%Ls)',
         $this->codes);
     }
 
     if ($this->branchIDs !== null) {
       $where[] = qsprintf(
         $conn_r,
         'branchID IN (%Ld)',
         $this->branchIDs);
     }
 
     return $this->formatWhereClause($where);
   }
 
   private function processPaths() {
     $dirs = array();
     $paths = array();
     foreach ($this->paths as $path) {
       $path = '/'.$path;
       if (substr($path, -1) == '/') {
         $dirs[] = $path;
       } else {
         $paths[] = $path;
       }
     }
     return array($dirs, $paths);
   }
 
   private function processResults(array $data) {
     $data = ipull($data, 'N', 'path_prefix');
 
     // Strip the leading "/" back off each path.
     $output = array();
     foreach ($data as $path => $count) {
       $output[substr($path, 1)] = $count;
     }
 
     return $output;
   }
 
 }
diff --git a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelQuery.php b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelQuery.php
index 0e8472c24d..e14198371c 100644
--- a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelQuery.php
+++ b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelQuery.php
@@ -1,43 +1,43 @@
 <?php
 
 abstract class DiffusionLowLevelQuery extends Phobject {
 
   private $repository;
 
   abstract protected function executeQuery();
 
   public function setRepository(PhabricatorRepository $repository) {
     $this->repository = $repository;
     return $this;
   }
 
   public function getRepository() {
     return $this->repository;
   }
 
   public function execute() {
     if (!$this->getRepository()) {
-      throw new Exception('Call setRepository() before execute()!');
+      throw new PhutilInvalidStateException('setRepository');
     }
 
     return $this->executeQuery();
   }
 
   protected function filterRefsByType(array $refs, array $types) {
     $type_map = array_fuse($types);
 
     foreach ($refs as $name => $ref_list) {
       foreach ($ref_list as $key => $ref) {
         if (empty($type_map[$ref['type']])) {
           unset($refs[$name][$key]);
         }
       }
       if (!$refs[$name]) {
         unset($refs[$name]);
       }
     }
 
     return $refs;
   }
 
 }
diff --git a/src/applications/diviner/atom/DivinerAtom.php b/src/applications/diviner/atom/DivinerAtom.php
index 46c1c70a9e..ef1a079a41 100644
--- a/src/applications/diviner/atom/DivinerAtom.php
+++ b/src/applications/diviner/atom/DivinerAtom.php
@@ -1,437 +1,435 @@
 <?php
 
 final class DivinerAtom {
 
   const TYPE_ARTICLE   = 'article';
   const TYPE_CLASS     = 'class';
   const TYPE_FILE      = 'file';
   const TYPE_FUNCTION  = 'function';
   const TYPE_INTERFACE = 'interface';
   const TYPE_METHOD    = 'method';
 
   private $type;
   private $name;
   private $file;
   private $line;
   private $hash;
   private $contentRaw;
   private $length;
   private $language;
   private $docblockRaw;
   private $docblockText;
   private $docblockMeta;
   private $warnings = array();
   private $parent;
   private $parentHash;
   private $children = array();
   private $childHashes = array();
   private $context;
   private $extends = array();
   private $links = array();
   private $book;
   private $properties = array();
 
   /**
    * Returns a sorting key which imposes an unambiguous, stable order on atoms.
    */
   public function getSortKey() {
     return implode(
       "\0",
       array(
         $this->getBook(),
         $this->getType(),
         $this->getContext(),
         $this->getName(),
         $this->getFile(),
         sprintf('%08', $this->getLine()),
       ));
   }
 
   public function setBook($book) {
     $this->book = $book;
     return $this;
   }
 
   public function getBook() {
     return $this->book;
   }
 
   public function setContext($context) {
     $this->context = $context;
     return $this;
   }
 
   public function getContext() {
     return $this->context;
   }
 
   public static function getAtomSerializationVersion() {
     return 2;
   }
 
   public function addWarning($warning) {
     $this->warnings[] = $warning;
     return $this;
   }
 
   public function getWarnings() {
     return $this->warnings;
   }
 
   public function setDocblockRaw($docblock_raw) {
     $this->docblockRaw = $docblock_raw;
 
     $parser = new PhutilDocblockParser();
     list($text, $meta) = $parser->parse($docblock_raw);
     $this->docblockText = $text;
     $this->docblockMeta = $meta;
 
     return $this;
   }
 
   public function getDocblockRaw() {
     return $this->docblockRaw;
   }
 
   public function getDocblockText() {
     if ($this->docblockText === null) {
-      throw new Exception(
-        pht('Call %s before %s!', 'setDocblockRaw()', 'getDocblockText()'));
+      throw new PhutilInvalidStateException('setDocblockRaw');
     }
     return $this->docblockText;
   }
 
   public function getDocblockMeta() {
     if ($this->docblockMeta === null) {
-      throw new Exception(
-        pht('Call %s before %s!', 'setDocblockRaw()', 'getDocblockMeta()'));
+      throw new PhutilInvalidStateException('setDocblockRaw');
     }
     return $this->docblockMeta;
   }
 
   public function getDocblockMetaValue($key, $default = null) {
     $meta = $this->getDocblockMeta();
     return idx($meta, $key, $default);
   }
 
   public function setDocblockMetaValue($key, $value) {
     $meta = $this->getDocblockMeta();
     $meta[$key] = $value;
     $this->docblockMeta = $meta;
     return $this;
   }
 
   public function setType($type) {
     $this->type = $type;
     return $this;
   }
 
   public function getType() {
     return $this->type;
   }
 
   public function setName($name) {
     $this->name = $name;
     return $this;
   }
 
   public function getName() {
     return $this->name;
   }
 
   public function setFile($file) {
     $this->file = $file;
     return $this;
   }
 
   public function getFile() {
     return $this->file;
   }
 
   public function setLine($line) {
     $this->line = $line;
     return $this;
   }
 
   public function getLine() {
     return $this->line;
   }
 
   public function setContentRaw($content_raw) {
     $this->contentRaw = $content_raw;
     return $this;
   }
 
   public function getContentRaw() {
     return $this->contentRaw;
   }
 
   public function setHash($hash) {
     $this->hash = $hash;
     return $this;
   }
 
   public function addLink(DivinerAtomRef $ref) {
     $this->links[] = $ref;
     return $this;
   }
 
   public function addExtends(DivinerAtomRef $ref) {
     $this->extends[] = $ref;
     return $this;
   }
 
   public function getLinkDictionaries() {
     return mpull($this->links, 'toDictionary');
   }
 
   public function getExtendsDictionaries() {
     return mpull($this->extends, 'toDictionary');
   }
 
   public function getExtends() {
     return $this->extends;
   }
 
   public function getHash() {
     if ($this->hash) {
       return $this->hash;
     }
 
     $parts = array(
       $this->getBook(),
       $this->getType(),
       $this->getName(),
       $this->getFile(),
       $this->getLine(),
       $this->getLength(),
       $this->getLanguage(),
       $this->getContentRaw(),
       $this->getDocblockRaw(),
       $this->getProperties(),
       $this->getChildHashes(),
       mpull($this->extends, 'toHash'),
       mpull($this->links, 'toHash'),
     );
 
     $this->hash = md5(serialize($parts)).'N';
     return $this->hash;
   }
 
   public function setLength($length) {
     $this->length = $length;
     return $this;
   }
 
   public function getLength() {
     return $this->length;
   }
 
   public function setLanguage($language) {
     $this->language = $language;
     return $this;
   }
 
   public function getLanguage() {
     return $this->language;
   }
 
   public function addChildHash($child_hash) {
     $this->childHashes[] = $child_hash;
     return $this;
   }
 
   public function getChildHashes() {
     if (!$this->childHashes && $this->children) {
       $this->childHashes = mpull($this->children, 'getHash');
     }
     return $this->childHashes;
   }
 
   public function setParentHash($parent_hash) {
     if ($this->parentHash) {
       throw new Exception(pht('Atom already has a parent!'));
     }
     $this->parentHash = $parent_hash;
     return $this;
   }
 
   public function hasParent() {
     return $this->parent || $this->parentHash;
   }
 
   public function setParent(DivinerAtom $atom) {
     if ($this->parentHash) {
       throw new Exception(pht('Parent hash has already been computed!'));
     }
     $this->parent = $atom;
     return $this;
   }
 
   public function getParentHash() {
     if ($this->parent && !$this->parentHash) {
       $this->parentHash = $this->parent->getHash();
     }
     return $this->parentHash;
   }
 
   public function addChild(DivinerAtom $atom) {
     if ($this->childHashes) {
       throw new Exception(pht('Child hashes have already been computed!'));
     }
 
     $atom->setParent($this);
     $this->children[] = $atom;
     return $this;
   }
 
   public function getURI() {
     $parts = array();
     $parts[] = phutil_escape_uri_path_component($this->getType());
     if ($this->getContext()) {
       $parts[] = phutil_escape_uri_path_component($this->getContext());
     }
     $parts[] = phutil_escape_uri_path_component($this->getName());
     $parts[] = null;
     return implode('/', $parts);
   }
 
   public function toDictionary() {
     // NOTE: If you change this format, bump the format version in
     // @{method:getAtomSerializationVersion}.
     return array(
       'book'        => $this->getBook(),
       'type'        => $this->getType(),
       'name'        => $this->getName(),
       'file'        => $this->getFile(),
       'line'        => $this->getLine(),
       'hash'        => $this->getHash(),
       'uri'         => $this->getURI(),
       'length'      => $this->getLength(),
       'context'     => $this->getContext(),
       'language'    => $this->getLanguage(),
       'docblockRaw' => $this->getDocblockRaw(),
       'warnings'    => $this->getWarnings(),
       'parentHash'  => $this->getParentHash(),
       'childHashes' => $this->getChildHashes(),
       'extends'     => $this->getExtendsDictionaries(),
       'links'       => $this->getLinkDictionaries(),
       'ref'         => $this->getRef()->toDictionary(),
       'properties'  => $this->getProperties(),
     );
   }
 
   public function getRef() {
     $title = null;
     if ($this->docblockMeta) {
       $title = $this->getDocblockMetaValue('title');
     }
 
     return id(new DivinerAtomRef())
       ->setBook($this->getBook())
       ->setContext($this->getContext())
       ->setType($this->getType())
       ->setName($this->getName())
       ->setTitle($title)
       ->setGroup($this->getProperty('group'));
   }
 
   public static function newFromDictionary(array $dictionary) {
     $atom = id(new DivinerAtom())
       ->setBook(idx($dictionary, 'book'))
       ->setType(idx($dictionary, 'type'))
       ->setName(idx($dictionary, 'name'))
       ->setFile(idx($dictionary, 'file'))
       ->setLine(idx($dictionary, 'line'))
       ->setHash(idx($dictionary, 'hash'))
       ->setLength(idx($dictionary, 'length'))
       ->setContext(idx($dictionary, 'context'))
       ->setLanguage(idx($dictionary, 'language'))
       ->setParentHash(idx($dictionary, 'parentHash'))
       ->setDocblockRaw(idx($dictionary, 'docblockRaw'))
       ->setProperties(idx($dictionary, 'properties'));
 
     foreach (idx($dictionary, 'warnings', array()) as $warning) {
       $atom->addWarning($warning);
     }
 
     foreach (idx($dictionary, 'childHashes', array()) as $child) {
       $atom->addChildHash($child);
     }
 
     foreach (idx($dictionary, 'extends', array()) as $extends) {
       $atom->addExtends(DivinerAtomRef::newFromDictionary($extends));
     }
 
     return $atom;
   }
 
   public function getProperty($key, $default = null) {
     return idx($this->properties, $key, $default);
   }
 
   public function setProperty($key, $value) {
     $this->properties[$key] = $value;
   }
 
   public function getProperties() {
     return $this->properties;
   }
 
   public function setProperties(array $properties) {
     $this->properties = $properties;
     return $this;
   }
 
   public static function getThisAtomIsNotDocumentedString($type) {
     switch ($type) {
       case self::TYPE_ARTICLE:
         return pht('This article is not documented.');
       case self::TYPE_CLASS:
         return pht('This class is not documented.');
       case self::TYPE_FILE:
         return pht('This file is not documented.');
       case self::TYPE_FUNCTION:
         return pht('This function is not documented.');
       case self::TYPE_INTERFACE:
         return pht('This interface is not documented.');
       case self::TYPE_METHOD:
         return pht('This method is not documented.');
       default:
         phlog("Need translation for '{$type}'.");
         return pht('This %s is not documented.', $type);
     }
   }
 
   public static function getAllTypes() {
     return array(
       self::TYPE_ARTICLE,
       self::TYPE_CLASS,
       self::TYPE_FILE,
       self::TYPE_FUNCTION,
       self::TYPE_INTERFACE,
       self::TYPE_METHOD,
     );
   }
 
   public static function getAtomTypeNameString($type) {
     switch ($type) {
       case self::TYPE_ARTICLE:
         return pht('Article');
       case self::TYPE_CLASS:
         return pht('Class');
       case self::TYPE_FILE:
         return pht('File');
       case self::TYPE_FUNCTION:
         return pht('Function');
       case self::TYPE_INTERFACE:
         return pht('Interface');
       case self::TYPE_METHOD:
         return pht('Method');
       default:
         phlog("Need translation for '{$type}'.");
         return ucwords($type);
     }
   }
 
 }
diff --git a/src/applications/diviner/storage/DivinerLiveSymbol.php b/src/applications/diviner/storage/DivinerLiveSymbol.php
index b6a7cde226..ba35eed255 100644
--- a/src/applications/diviner/storage/DivinerLiveSymbol.php
+++ b/src/applications/diviner/storage/DivinerLiveSymbol.php
@@ -1,266 +1,266 @@
 <?php
 
 final class DivinerLiveSymbol extends DivinerDAO
   implements
     PhabricatorPolicyInterface,
     PhabricatorMarkupInterface,
     PhabricatorDestructibleInterface {
 
   protected $bookPHID;
   protected $context;
   protected $type;
   protected $name;
   protected $atomIndex;
   protected $graphHash;
   protected $identityHash;
   protected $nodeHash;
 
   protected $title;
   protected $titleSlugHash;
   protected $groupName;
   protected $summary;
   protected $isDocumentable = 0;
 
   private $book = self::ATTACHABLE;
   private $atom = self::ATTACHABLE;
   private $extends = self::ATTACHABLE;
   private $children = self::ATTACHABLE;
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_TIMESTAMPS => false,
       self::CONFIG_COLUMN_SCHEMA => array(
         'context' => 'text255?',
         'type' => 'text32',
         'name' => 'text255',
         'atomIndex' => 'uint32',
         'identityHash' => 'bytes12',
         'graphHash' => 'text64?',
         'title' => 'text?',
         'titleSlugHash' => 'bytes12?',
         'groupName' => 'text255?',
         'summary' => 'text?',
         'isDocumentable' => 'bool',
         'nodeHash' => 'text64?',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_phid' => null,
         'identityHash' => array(
           'columns' => array('identityHash'),
           'unique' => true,
         ),
         'phid' => array(
           'columns' => array('phid'),
           'unique' => true,
         ),
         'graphHash' => array(
           'columns' => array('graphHash'),
           'unique' => true,
         ),
         'nodeHash' => array(
           'columns' => array('nodeHash'),
           'unique' => true,
         ),
         'bookPHID' => array(
           'columns' => array(
             'bookPHID',
             'type',
             'name(64)',
             'context(64)',
             'atomIndex',
           ),
         ),
         'name' => array(
           'columns' => array('name(64)'),
         ),
         'key_slug' => array(
           'columns' => array('titleSlugHash'),
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(DivinerAtomPHIDType::TYPECONST);
   }
 
   public function getBook() {
     return $this->assertAttached($this->book);
   }
 
   public function attachBook(DivinerLiveBook $book) {
     $this->book = $book;
     return $this;
   }
 
   public function getAtom() {
     return $this->assertAttached($this->atom);
   }
 
   public function attachAtom(DivinerLiveAtom $atom) {
     $this->atom = DivinerAtom::newFromDictionary($atom->getAtomData());
     return $this;
   }
 
   public function getURI() {
     $parts = array(
       'book',
       $this->getBook()->getName(),
       $this->getType(),
     );
 
     if ($this->getContext()) {
       $parts[] = $this->getContext();
     }
 
     $parts[] = $this->getName();
 
     if ($this->getAtomIndex()) {
       $parts[] = $this->getAtomIndex();
     }
 
     return '/'.implode('/', $parts).'/';
   }
 
   public function getSortKey() {
     // Sort articles before other types of content. Then, sort atoms in a
     // case-insensitive way.
     return sprintf(
       '%c:%s',
       ($this->getType() == DivinerAtom::TYPE_ARTICLE ? '0' : '1'),
       phutil_utf8_strtolower($this->getTitle()));
   }
 
   public function save() {
 
     // NOTE: The identity hash is just a sanity check because the unique tuple
     // on this table is way way too long to fit into a normal UNIQUE KEY. We
     // don't use it directly, but its existence prevents duplicate records.
 
     if (!$this->identityHash) {
       $this->identityHash = PhabricatorHash::digestForIndex(
         serialize(
           array(
             'bookPHID' => $this->getBookPHID(),
             'context'  => $this->getContext(),
             'type'     => $this->getType(),
             'name'     => $this->getName(),
             'index'    => $this->getAtomIndex(),
           )));
     }
 
     return parent::save();
   }
 
   public function getTitle() {
     $title = parent::getTitle();
     if (!strlen($title)) {
       $title = $this->getName();
     }
     return $title;
   }
 
   public function setTitle($value) {
     $this->writeField('title', $value);
     if (strlen($value)) {
       $slug = DivinerAtomRef::normalizeTitleString($value);
       $hash = PhabricatorHash::digestForIndex($slug);
       $this->titleSlugHash = $hash;
     } else {
       $this->titleSlugHash = null;
     }
     return $this;
   }
 
   public function attachExtends(array $extends) {
-    assert_instances_of($extends, 'DivinerLiveSymbol');
+    assert_instances_of($extends, __CLASS__);
     $this->extends = $extends;
     return $this;
   }
 
   public function getExtends() {
     return $this->assertAttached($this->extends);
   }
 
   public function attachChildren(array $children) {
-    assert_instances_of($children, 'DivinerLiveSymbol');
+    assert_instances_of($children, __CLASS__);
     $this->children = $children;
     return $this;
   }
 
   public function getChildren() {
     return $this->assertAttached($this->children);
   }
 
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
   public function getCapabilities() {
     return $this->getBook()->getCapabilities();
   }
 
 
   public function getPolicy($capability) {
     return $this->getBook()->getPolicy($capability);
   }
 
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     return $this->getBook()->hasAutomaticCapability($capability, $viewer);
   }
 
   public function describeAutomaticCapability($capability) {
     return pht('Atoms inherit the policies of the books they are part of.');
   }
 
 
 /* -(  Markup Interface  )--------------------------------------------------- */
 
 
   public function getMarkupFieldKey($field) {
     return $this->getPHID().':'.$field.':'.$this->getGraphHash();
   }
 
 
   public function newMarkupEngine($field) {
     return PhabricatorMarkupEngine::getEngine('diviner');
   }
 
 
   public function getMarkupText($field) {
     return $this->getAtom()->getDocblockText();
   }
 
 
   public function didMarkupText(
     $field,
     $output,
     PhutilMarkupEngine $engine) {
     return $output;
   }
 
 
   public function shouldUseMarkupCache($field) {
     return true;
   }
 
 /* -(  PhabricatorDestructibleInterface  )----------------------------------- */
 
   public function destroyObjectPermanently(
     PhabricatorDestructionEngine $engine) {
 
     $this->openTransaction();
       $conn_w = $this->establishConnection('w');
 
       queryfx(
         $conn_w,
         'DELETE FROM %T WHERE symbolPHID = %s',
         id(new DivinerLiveAtom())->getTableName(),
         $this->getPHID());
 
       $this->delete();
     $this->saveTransaction();
   }
 
 }
diff --git a/src/applications/doorkeeper/view/DoorkeeperTagView.php b/src/applications/doorkeeper/view/DoorkeeperTagView.php
index 66f759f43c..8dee79d72b 100644
--- a/src/applications/doorkeeper/view/DoorkeeperTagView.php
+++ b/src/applications/doorkeeper/view/DoorkeeperTagView.php
@@ -1,42 +1,42 @@
 <?php
 
 final class DoorkeeperTagView extends AphrontView {
 
   private $xobj;
 
   public function setExternalObject(DoorkeeperExternalObject $xobj) {
     $this->xobj = $xobj;
     return $this;
   }
 
   public function render() {
     $xobj = $this->xobj;
     if (!$xobj) {
-      throw new Exception('Call setExternalObject() before render()!');
+      throw new PhutilInvalidStateException('setExternalObject');
     }
 
     $tag_id = celerity_generate_unique_node_id();
 
     $href = $xobj->getObjectURI();
 
     $spec = array(
       'id' => $tag_id,
       'ref' => array(
         $xobj->getApplicationType(),
         $xobj->getApplicationDomain(),
         $xobj->getObjectType(),
         $xobj->getObjectID(),
       ),
     );
 
     Javelin::initBehavior('doorkeeper-tag', array('tags' => array($spec)));
 
     return id(new PHUITagView())
       ->setID($tag_id)
       ->setHref($href)
       ->setName($href)
       ->setType(PHUITagView::TYPE_OBJECT)
       ->setExternal(true);
   }
 
 }
diff --git a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php
index 18ca597a90..43891d0d07 100644
--- a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php
+++ b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php
@@ -1,469 +1,469 @@
 <?php
 
 /**
  * @task lease      Lease Acquisition
  * @task resource   Resource Allocation
  * @task log        Logging
  */
 abstract class DrydockBlueprintImplementation {
 
   private $activeResource;
   private $activeLease;
   private $instance;
 
   abstract public function getType();
   abstract public function getInterface(
     DrydockResource $resource,
     DrydockLease $lease,
     $type);
 
   abstract public function isEnabled();
 
   abstract public function getBlueprintName();
   abstract public function getDescription();
 
   public function getBlueprintClass() {
     return get_class($this);
   }
 
   protected function loadLease($lease_id) {
     // TODO: Get rid of this?
     $query = id(new DrydockLeaseQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withIDs(array($lease_id))
       ->execute();
 
     $lease = idx($query, $lease_id);
 
     if (!$lease) {
       throw new Exception("No such lease '{$lease_id}'!");
     }
 
     return $lease;
   }
 
   protected function getInstance() {
     if (!$this->instance) {
       throw new Exception(
         'Attach the blueprint instance to the implementation.');
     }
 
     return $this->instance;
   }
 
   public function attachInstance(DrydockBlueprint $instance) {
     $this->instance = $instance;
     return $this;
   }
 
   public function getFieldSpecifications() {
     return array();
   }
 
   public function getDetail($key, $default = null) {
     return $this->getInstance()->getDetail($key, $default);
   }
 
 
 /* -(  Lease Acquisition  )-------------------------------------------------- */
 
 
   /**
    * @task lease
    */
   final public function filterResource(
     DrydockResource $resource,
     DrydockLease $lease) {
 
     $scope = $this->pushActiveScope($resource, $lease);
 
     return $this->canAllocateLease($resource, $lease);
   }
 
 
   /**
    * Enforce basic checks on lease/resource compatibility. Allows resources to
    * reject leases if they are incompatible, even if the resource types match.
    *
    * For example, if a resource represents a 32-bit host, this method might
    * reject leases that need a 64-bit host. If a resource represents a working
    * copy of repository "X", this method might reject leases which need a
    * working copy of repository "Y". Generally, although the main types of
    * a lease and resource may match (e.g., both "host"), it may not actually be
    * possible to satisfy the lease with a specific resource.
    *
    * This method generally should not enforce limits or perform capacity
    * checks. Perform those in @{method:shouldAllocateLease} instead. It also
    * should not perform actual acquisition of the lease; perform that in
    * @{method:executeAcquireLease} instead.
    *
    * @param   DrydockResource   Candidiate resource to allocate the lease on.
    * @param   DrydockLease      Pending lease that wants to allocate here.
    * @return  bool              True if the resource and lease are compatible.
    * @task lease
    */
   abstract protected function canAllocateLease(
     DrydockResource $resource,
     DrydockLease $lease);
 
 
   /**
    * @task lease
    */
   final public function allocateLease(
     DrydockResource $resource,
     DrydockLease $lease) {
 
     $scope = $this->pushActiveScope($resource, $lease);
 
     $this->log('Trying to Allocate Lease');
 
     $lease->setStatus(DrydockLeaseStatus::STATUS_ACQUIRING);
     $lease->setResourceID($resource->getID());
     $lease->attachResource($resource);
 
     $ephemeral_lease = id(clone $lease)->makeEphemeral();
 
     $allocated = false;
     $allocation_exception = null;
 
     $resource->openTransaction();
       $resource->beginReadLocking();
         $resource->reload();
 
         // TODO: Policy stuff.
         $other_leases = id(new DrydockLease())->loadAllWhere(
           'status IN (%Ld) AND resourceID = %d',
           array(
             DrydockLeaseStatus::STATUS_ACQUIRING,
             DrydockLeaseStatus::STATUS_ACTIVE,
           ),
           $resource->getID());
 
         try {
           $allocated = $this->shouldAllocateLease(
             $resource,
             $ephemeral_lease,
             $other_leases);
         } catch (Exception $ex) {
           $allocation_exception = $ex;
         }
 
         if ($allocated) {
           $lease->save();
         }
       $resource->endReadLocking();
     if ($allocated) {
       $resource->saveTransaction();
       $this->log('Allocated Lease');
     } else {
       $resource->killTransaction();
       $this->log('Failed to Allocate Lease');
     }
 
     if ($allocation_exception) {
       $this->logException($allocation_exception);
     }
 
     return $allocated;
   }
 
 
   /**
    * Enforce lease limits on resources. Allows resources to reject leases if
    * they would become over-allocated by accepting them.
    *
    * For example, if a resource represents disk space, this method might check
    * how much space the lease is asking for (say, 200MB) and how much space is
    * left unallocated on the resource. It could grant the lease (return true)
    * if it has enough remaining space (more than 200MB), and reject the lease
    * (return false) if it does not (less than 200MB).
    *
    * A resource might also allow only exclusive leases. In this case it could
    * accept a new lease (return true) if there are no active leases, or reject
    * the new lease (return false) if there any other leases.
    *
    * A lock is held on the resource while this method executes to prevent
    * multiple processes from allocating leases on the resource simultaneously.
    * However, this means you should implement the method as cheaply as possible.
    * In particular, do not perform any actual acquisition or setup in this
    * method.
    *
    * If allocation is permitted, the lease will be moved to `ACQUIRING` status
    * and @{method:executeAcquireLease} will be called to actually perform
    * acquisition.
    *
    * General compatibility checks unrelated to resource limits and capacity are
    * better implemented in @{method:canAllocateLease}, which serves as a
    * cheap filter before lock acquisition.
    *
    * @param   DrydockResource     Candidate resource to allocate the lease on.
    * @param   DrydockLease        Pending lease that wants to allocate here.
    * @param   list<DrydockLease>  Other allocated and acquired leases on the
    *                              resource. The implementation can inspect them
    *                              to verify it can safely add the new lease.
    * @return  bool                True to allocate the lease on the resource;
    *                              false to reject it.
    * @task lease
    */
   abstract protected function shouldAllocateLease(
     DrydockResource $resource,
     DrydockLease $lease,
     array $other_leases);
 
 
   /**
    * @task lease
    */
   final public function acquireLease(
     DrydockResource $resource,
     DrydockLease $lease) {
 
     $scope = $this->pushActiveScope($resource, $lease);
 
     $this->log('Acquiring Lease');
     $lease->setStatus(DrydockLeaseStatus::STATUS_ACTIVE);
     $lease->setResourceID($resource->getID());
     $lease->attachResource($resource);
 
     $ephemeral_lease = id(clone $lease)->makeEphemeral();
 
     try {
       $this->executeAcquireLease($resource, $ephemeral_lease);
     } catch (Exception $ex) {
       $this->logException($ex);
       throw $ex;
     }
 
     $lease->setAttributes($ephemeral_lease->getAttributes());
     $lease->save();
     $this->log('Acquired Lease');
   }
 
 
   /**
    * Acquire and activate an allocated lease. Allows resources to peform setup
    * as leases are brought online.
    *
    * Following a successful call to @{method:canAllocateLease}, a lease is moved
    * to `ACQUIRING` status and this method is called after resource locks are
    * released. Nothing is locked while this method executes; the implementation
    * is free to perform expensive operations like writing files and directories,
    * executing commands, etc.
    *
    * After this method executes, the lease status is moved to `ACTIVE` and the
    * original leasee may access it.
    *
    * If acquisition fails, throw an exception.
    *
    * @param   DrydockResource   Resource to acquire a lease on.
    * @param   DrydockLease      Lease to acquire.
    * @return  void
    */
   abstract protected function executeAcquireLease(
     DrydockResource $resource,
     DrydockLease $lease);
 
 
 
   final public function releaseLease(
     DrydockResource $resource,
     DrydockLease $lease) {
     $scope = $this->pushActiveScope(null, $lease);
 
     $released = false;
 
     $lease->openTransaction();
       $lease->beginReadLocking();
         $lease->reload();
 
         if ($lease->getStatus() == DrydockLeaseStatus::STATUS_ACTIVE) {
           $lease->setStatus(DrydockLeaseStatus::STATUS_RELEASED);
           $lease->save();
           $released = true;
         }
 
       $lease->endReadLocking();
     $lease->saveTransaction();
 
     if (!$released) {
       throw new Exception('Unable to release lease: lease not active!');
     }
 
   }
 
 
 
 /* -(  Resource Allocation  )------------------------------------------------ */
 
 
   public function canAllocateMoreResources(array $pool) {
     return true;
   }
 
   abstract protected function executeAllocateResource(DrydockLease $lease);
 
 
   final public function allocateResource(DrydockLease $lease) {
     $scope = $this->pushActiveScope(null, $lease);
 
     $this->log(
       pht(
         "Blueprint '%s': Allocating Resource for '%s'",
         $this->getBlueprintClass(),
         $lease->getLeaseName()));
 
     try {
       $resource = $this->executeAllocateResource($lease);
       $this->validateAllocatedResource($resource);
     } catch (Exception $ex) {
       $this->logException($ex);
       throw $ex;
     }
 
     return $resource;
   }
 
 
 /* -(  Logging  )------------------------------------------------------------ */
 
 
   /**
    * @task log
    */
   protected function logException(Exception $ex) {
     $this->log($ex->getMessage());
   }
 
 
   /**
    * @task log
    */
   protected function log($message) {
     self::writeLog(
       $this->activeResource,
       $this->activeLease,
       $message);
   }
 
 
   /**
    * @task log
    */
   public static function writeLog(
     DrydockResource $resource = null,
     DrydockLease $lease = null,
     $message) {
 
     $log = id(new DrydockLog())
       ->setEpoch(time())
       ->setMessage($message);
 
     if ($resource) {
       $log->setResourceID($resource->getID());
     }
 
     if ($lease) {
       $log->setLeaseID($lease->getID());
     }
 
     $log->save();
   }
 
 
   public static function getAllBlueprintImplementations() {
     static $list = null;
 
     if ($list === null) {
       $blueprints = id(new PhutilSymbolLoader())
         ->setType('class')
-        ->setAncestorClass('DrydockBlueprintImplementation')
+        ->setAncestorClass(__CLASS__)
         ->setConcreteOnly(true)
         ->selectAndLoadSymbols();
       $list = ipull($blueprints, 'name', 'name');
       foreach ($list as $class_name => $ignored) {
         $list[$class_name] = newv($class_name, array());
       }
     }
 
     return $list;
   }
 
   public static function getAllBlueprintImplementationsForResource($type) {
     static $groups = null;
     if ($groups === null) {
       $groups = mgroup(self::getAllBlueprintImplementations(), 'getType');
     }
     return idx($groups, $type, array());
   }
 
   public static function getNamedImplementation($class) {
     return idx(self::getAllBlueprintImplementations(), $class);
   }
 
   protected function newResourceTemplate($name) {
     $resource = id(new DrydockResource())
       ->setBlueprintPHID($this->getInstance()->getPHID())
       ->setBlueprintClass($this->getBlueprintClass())
       ->setType($this->getType())
       ->setStatus(DrydockResourceStatus::STATUS_PENDING)
       ->setName($name)
       ->save();
 
     $this->activeResource = $resource;
 
     $this->log(
       pht(
         "Blueprint '%s': Created New Template",
         $this->getBlueprintClass()));
 
     return $resource;
   }
 
   /**
    * Sanity checks that the blueprint is implemented properly.
    */
   private function validateAllocatedResource($resource) {
     $blueprint = $this->getBlueprintClass();
 
     if (!($resource instanceof DrydockResource)) {
       throw new Exception(
         "Blueprint '{$blueprint}' is not properly implemented: ".
         "executeAllocateResource() must return an object of type ".
         "DrydockResource or throw, but returned something else.");
     }
 
     $current_status = $resource->getStatus();
     $req_status = DrydockResourceStatus::STATUS_OPEN;
     if ($current_status != $req_status) {
       $current_name = DrydockResourceStatus::getNameForStatus($current_status);
       $req_name = DrydockResourceStatus::getNameForStatus($req_status);
       throw new Exception(
         "Blueprint '{$blueprint}' is not properly implemented: ".
         "executeAllocateResource() must return a DrydockResource with ".
         "status '{$req_name}', but returned one with status ".
         "'{$current_name}'.");
     }
   }
 
   private function pushActiveScope(
     DrydockResource $resource = null,
     DrydockLease $lease = null) {
 
     if (($this->activeResource !== null) ||
         ($this->activeLease !== null)) {
       throw new Exception('There is already an active resource or lease!');
     }
 
     $this->activeResource = $resource;
     $this->activeLease = $lease;
 
     return new DrydockBlueprintScopeGuard($this);
   }
 
   public function popActiveScope() {
     $this->activeResource = null;
     $this->activeLease = null;
   }
 
 }
diff --git a/src/applications/drydock/storage/DrydockLease.php b/src/applications/drydock/storage/DrydockLease.php
index cb61e54490..252c922065 100644
--- a/src/applications/drydock/storage/DrydockLease.php
+++ b/src/applications/drydock/storage/DrydockLease.php
@@ -1,229 +1,229 @@
 <?php
 
 final class DrydockLease extends DrydockDAO
   implements PhabricatorPolicyInterface {
 
   protected $resourceID;
   protected $resourceType;
   protected $until;
   protected $ownerPHID;
   protected $attributes = array();
   protected $status = DrydockLeaseStatus::STATUS_PENDING;
   protected $taskID;
 
   private $resource = self::ATTACHABLE;
   private $releaseOnDestruction;
 
   /**
    * Flag this lease to be released when its destructor is called. This is
    * mostly useful if you have a script which acquires, uses, and then releases
    * a lease, as you don't need to explicitly handle exceptions to properly
    * release the lease.
    */
   public function releaseOnDestruction() {
     $this->releaseOnDestruction = true;
     return $this;
   }
 
   public function __destruct() {
     if ($this->releaseOnDestruction) {
       if ($this->isActive()) {
         $this->release();
       }
     }
   }
 
   public function getLeaseName() {
     return pht('Lease %d', $this->getID());
   }
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_SERIALIZATION => array(
         'attributes'    => self::SERIALIZATION_JSON,
       ),
       self::CONFIG_COLUMN_SCHEMA => array(
         'status' => 'uint32',
         'until' => 'epoch?',
         'resourceType' => 'text128',
         'taskID' => 'id?',
         'ownerPHID' => 'phid?',
         'resourceID' => 'id?',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_phid' => null,
         'phid' => array(
           'columns' => array('phid'),
           'unique' => true,
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function setAttribute($key, $value) {
     $this->attributes[$key] = $value;
     return $this;
   }
 
   public function getAttribute($key, $default = null) {
     return idx($this->attributes, $key, $default);
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(DrydockLeasePHIDType::TYPECONST);
   }
 
   public function getInterface($type) {
     return $this->getResource()->getInterface($this, $type);
   }
 
   public function getResource() {
     return $this->assertAttached($this->resource);
   }
 
   public function attachResource(DrydockResource $resource = null) {
     $this->resource = $resource;
     return $this;
   }
 
   public function hasAttachedResource() {
     return ($this->resource !== null);
   }
 
   public function loadResource() {
     return id(new DrydockResource())->loadOneWhere(
       'id = %d',
       $this->getResourceID());
   }
 
   public function queueForActivation() {
     if ($this->getID()) {
       throw new Exception(
         'Only new leases may be queued for activation!');
     }
 
     $this->setStatus(DrydockLeaseStatus::STATUS_PENDING);
     $this->save();
 
     $task = PhabricatorWorker::scheduleTask(
       'DrydockAllocatorWorker',
       $this->getID());
 
     // NOTE: Scheduling the task might execute it in-process, if we're running
     // from a CLI script. Reload the lease to make sure we have the most
     // up-to-date information. Normally, this has no effect.
     $this->reload();
 
     $this->setTaskID($task->getID());
     $this->save();
 
     return $this;
   }
 
   public function release() {
     $this->assertActive();
     $this->setStatus(DrydockLeaseStatus::STATUS_RELEASED);
     $this->save();
 
     $this->resource = null;
 
     return $this;
   }
 
   public function isActive() {
     switch ($this->status) {
       case DrydockLeaseStatus::STATUS_ACTIVE:
       case DrydockLeaseStatus::STATUS_ACQUIRING:
         return true;
     }
     return false;
   }
 
   private function assertActive() {
     if (!$this->isActive()) {
       throw new Exception(
         'Lease is not active! You can not interact with resources through '.
         'an inactive lease.');
     }
   }
 
   public static function waitForLeases(array $leases) {
-    assert_instances_of($leases, 'DrydockLease');
+    assert_instances_of($leases, __CLASS__);
 
     $task_ids = array_filter(mpull($leases, 'getTaskID'));
 
     PhabricatorWorker::waitForTasks($task_ids);
 
     $unresolved = $leases;
     while (true) {
       foreach ($unresolved as $key => $lease) {
         $lease->reload();
         switch ($lease->getStatus()) {
           case DrydockLeaseStatus::STATUS_ACTIVE:
             unset($unresolved[$key]);
             break;
           case DrydockLeaseStatus::STATUS_RELEASED:
             throw new Exception('Lease has already been released!');
           case DrydockLeaseStatus::STATUS_EXPIRED:
             throw new Exception('Lease has already expired!');
           case DrydockLeaseStatus::STATUS_BROKEN:
             throw new Exception('Lease has been broken!');
           case DrydockLeaseStatus::STATUS_PENDING:
           case DrydockLeaseStatus::STATUS_ACQUIRING:
             break;
           default:
             throw new Exception('Unknown status??');
         }
       }
 
       if ($unresolved) {
         sleep(1);
       } else {
         break;
       }
     }
 
     foreach ($leases as $lease) {
       $lease->attachResource($lease->loadResource());
     }
   }
 
   public function waitUntilActive() {
     if (!$this->getID()) {
       $this->queueForActivation();
     }
 
     self::waitForLeases(array($this));
     return $this;
   }
 
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
     );
   }
 
   public function getPolicy($capability) {
     if ($this->getResource()) {
       return $this->getResource()->getPolicy($capability);
     }
     return PhabricatorPolicies::getMostOpenPolicy();
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     if ($this->getResource()) {
       return $this->getResource()->hasAutomaticCapability($capability, $viewer);
     }
     return false;
   }
 
   public function describeAutomaticCapability($capability) {
     return pht('Leases inherit policies from the resources they lease.');
   }
 
 }
diff --git a/src/applications/feed/story/PhabricatorFeedStory.php b/src/applications/feed/story/PhabricatorFeedStory.php
index a115985704..f94c2601e8 100644
--- a/src/applications/feed/story/PhabricatorFeedStory.php
+++ b/src/applications/feed/story/PhabricatorFeedStory.php
@@ -1,513 +1,532 @@
 <?php
 
 /**
  * Manages rendering and aggregation of a story. A story is an event (like a
  * user adding a comment) which may be represented in different forms on
  * different channels (like feed, notifications and realtime alerts).
  *
  * @task load     Loading Stories
  * @task policy   Policy Implementation
  */
 abstract class PhabricatorFeedStory
   implements
     PhabricatorPolicyInterface,
     PhabricatorMarkupInterface {
 
   private $data;
   private $hasViewed;
   private $framed;
   private $hovercard = false;
   private $renderingTarget = PhabricatorApplicationTransaction::TARGET_HTML;
 
   private $handles = array();
   private $objects = array();
   private $projectPHIDs = array();
   private $markupFieldOutput = array();
 
 /* -(  Loading Stories  )---------------------------------------------------- */
 
 
   /**
    * Given @{class:PhabricatorFeedStoryData} rows, load them into objects and
    * construct appropriate @{class:PhabricatorFeedStory} wrappers for each
    * data row.
    *
    * @param list<dict>  List of @{class:PhabricatorFeedStoryData} rows from the
    *                    database.
    * @return list<PhabricatorFeedStory>   List of @{class:PhabricatorFeedStory}
    *                                      objects.
    * @task load
    */
   public static function loadAllFromRows(array $rows, PhabricatorUser $viewer) {
     $stories = array();
 
     $data = id(new PhabricatorFeedStoryData())->loadAllFromArray($rows);
     foreach ($data as $story_data) {
       $class = $story_data->getStoryType();
 
       try {
         $ok =
           class_exists($class) &&
-          is_subclass_of($class, 'PhabricatorFeedStory');
+          is_subclass_of($class, __CLASS__);
       } catch (PhutilMissingSymbolException $ex) {
         $ok = false;
       }
 
       // If the story type isn't a valid class or isn't a subclass of
       // PhabricatorFeedStory, decline to load it.
       if (!$ok) {
         continue;
       }
 
       $key = $story_data->getChronologicalKey();
       $stories[$key] = newv($class, array($story_data));
     }
 
     $object_phids = array();
     $key_phids = array();
     foreach ($stories as $key => $story) {
       $phids = array();
       foreach ($story->getRequiredObjectPHIDs() as $phid) {
         $phids[$phid] = true;
       }
       if ($story->getPrimaryObjectPHID()) {
         $phids[$story->getPrimaryObjectPHID()] = true;
       }
       $key_phids[$key] = $phids;
       $object_phids += $phids;
     }
 
     $objects = id(new PhabricatorObjectQuery())
       ->setViewer($viewer)
       ->withPHIDs(array_keys($object_phids))
       ->execute();
 
     foreach ($key_phids as $key => $phids) {
       if (!$phids) {
         continue;
       }
       $story_objects = array_select_keys($objects, array_keys($phids));
       if (count($story_objects) != count($phids)) {
         // An object this story requires either does not exist or is not visible
         // to the user. Decline to render the story.
         unset($stories[$key]);
         unset($key_phids[$key]);
         continue;
       }
 
       $stories[$key]->setObjects($story_objects);
     }
 
     // If stories are about PhabricatorProjectInterface objects, load the
     // projects the objects are a part of so we can render project tags
     // on the stories.
 
     $project_phids = array();
     foreach ($objects as $object) {
       if ($object instanceof PhabricatorProjectInterface) {
         $project_phids[$object->getPHID()] = array();
       }
     }
 
     if ($project_phids) {
       $edge_query = id(new PhabricatorEdgeQuery())
         ->withSourcePHIDs(array_keys($project_phids))
         ->withEdgeTypes(
           array(
             PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
           ));
       $edge_query->execute();
       foreach ($project_phids as $phid => $ignored) {
         $project_phids[$phid] = $edge_query->getDestinationPHIDs(array($phid));
       }
     }
 
     $handle_phids = array();
     foreach ($stories as $key => $story) {
       foreach ($story->getRequiredHandlePHIDs() as $phid) {
         $key_phids[$key][$phid] = true;
       }
       if ($story->getAuthorPHID()) {
         $key_phids[$key][$story->getAuthorPHID()] = true;
       }
 
       $object_phid = $story->getPrimaryObjectPHID();
       $object_project_phids = idx($project_phids, $object_phid, array());
       $story->setProjectPHIDs($object_project_phids);
       foreach ($object_project_phids as $dst) {
         $key_phids[$key][$dst] = true;
       }
 
       $handle_phids += $key_phids[$key];
     }
 
     $handles = id(new PhabricatorHandleQuery())
       ->setViewer($viewer)
       ->withPHIDs(array_keys($handle_phids))
       ->execute();
 
     foreach ($key_phids as $key => $phids) {
       if (!$phids) {
         continue;
       }
       $story_handles = array_select_keys($handles, array_keys($phids));
       $stories[$key]->setHandles($story_handles);
     }
 
     // Load and process story markup blocks.
 
     $engine = new PhabricatorMarkupEngine();
     $engine->setViewer($viewer);
     foreach ($stories as $story) {
       foreach ($story->getFieldStoryMarkupFields() as $field) {
         $engine->addObject($story, $field);
       }
     }
 
     $engine->process();
 
     foreach ($stories as $story) {
       foreach ($story->getFieldStoryMarkupFields() as $field) {
         $story->setMarkupFieldOutput(
           $field,
           $engine->getOutput($story, $field));
       }
     }
 
     return $stories;
   }
 
   public function setMarkupFieldOutput($field, $output) {
     $this->markupFieldOutput[$field] = $output;
     return $this;
   }
 
   public function getMarkupFieldOutput($field) {
     if (!array_key_exists($field, $this->markupFieldOutput)) {
       throw new Exception(
         pht(
           'Trying to retrieve markup field key "%s", but this feed story '.
           'did not request it be rendered.',
           $field));
     }
 
     return $this->markupFieldOutput[$field];
   }
 
   public function setHovercard($hover) {
     $this->hovercard = $hover;
     return $this;
   }
 
   public function setRenderingTarget($target) {
     $this->validateRenderingTarget($target);
     $this->renderingTarget = $target;
     return $this;
   }
 
   public function getRenderingTarget() {
     return $this->renderingTarget;
   }
 
   private function validateRenderingTarget($target) {
     switch ($target) {
       case PhabricatorApplicationTransaction::TARGET_HTML:
       case PhabricatorApplicationTransaction::TARGET_TEXT:
         break;
       default:
         throw new Exception('Unknown rendering target: '.$target);
         break;
     }
   }
 
   public function setObjects(array $objects) {
     $this->objects = $objects;
     return $this;
   }
 
   public function getObject($phid) {
     $object = idx($this->objects, $phid);
     if (!$object) {
       throw new Exception(
         "Story is asking for an object it did not request ('{$phid}')!");
     }
     return $object;
   }
 
   public function getPrimaryObject() {
     $phid = $this->getPrimaryObjectPHID();
     if (!$phid) {
       throw new Exception('Story has no primary object!');
     }
     return $this->getObject($phid);
   }
 
   public function getPrimaryObjectPHID() {
     return null;
   }
 
   final public function __construct(PhabricatorFeedStoryData $data) {
     $this->data = $data;
   }
 
   abstract public function renderView();
   public function renderAsTextForDoorkeeper(
     DoorkeeperFeedStoryPublisher $publisher) {
 
     // TODO: This (and text rendering) should be properly abstract and
     // universal. However, this is far less bad than it used to be, and we
     // need to clean up more old feed code to really make this reasonable.
 
     return pht(
       '(Unable to render story of class %s for Doorkeeper.)',
       get_class($this));
   }
 
   public function getRequiredHandlePHIDs() {
     return array();
   }
 
   public function getRequiredObjectPHIDs() {
     return array();
   }
 
   public function setHasViewed($has_viewed) {
     $this->hasViewed = $has_viewed;
     return $this;
   }
 
   public function getHasViewed() {
     return $this->hasViewed;
   }
 
   final public function setFramed($framed) {
     $this->framed = $framed;
     return $this;
   }
 
   final public function setHandles(array $handles) {
     assert_instances_of($handles, 'PhabricatorObjectHandle');
     $this->handles = $handles;
     return $this;
   }
 
   final protected function getObjects() {
     return $this->objects;
   }
 
   final protected function getHandles() {
     return $this->handles;
   }
 
   final protected function getHandle($phid) {
     if (isset($this->handles[$phid])) {
       if ($this->handles[$phid] instanceof PhabricatorObjectHandle) {
         return $this->handles[$phid];
       }
     }
 
     $handle = new PhabricatorObjectHandle();
     $handle->setPHID($phid);
     $handle->setName("Unloaded Object '{$phid}'");
 
     return $handle;
   }
 
   final public function getStoryData() {
     return $this->data;
   }
 
   final public function getEpoch() {
     return $this->getStoryData()->getEpoch();
   }
 
   final public function getChronologicalKey() {
     return $this->getStoryData()->getChronologicalKey();
   }
 
   final public function getValue($key, $default = null) {
     return $this->getStoryData()->getValue($key, $default);
   }
 
   final public function getAuthorPHID() {
     return $this->getStoryData()->getAuthorPHID();
   }
 
   final protected function renderHandleList(array $phids) {
     $items = array();
     foreach ($phids as $phid) {
       $items[] = $this->linkTo($phid);
     }
     $list = null;
     switch ($this->getRenderingTarget()) {
       case PhabricatorApplicationTransaction::TARGET_TEXT:
         $list = implode(', ', $items);
         break;
       case PhabricatorApplicationTransaction::TARGET_HTML:
         $list = phutil_implode_html(', ', $items);
         break;
     }
     return $list;
   }
 
   final protected function linkTo($phid) {
     $handle = $this->getHandle($phid);
 
     switch ($this->getRenderingTarget()) {
       case PhabricatorApplicationTransaction::TARGET_TEXT:
         return $handle->getLinkName();
     }
 
     // NOTE: We render our own link here to customize the styling and add
     // the '_top' target for framed feeds.
 
     $class = null;
     if ($handle->getType() == PhabricatorPeopleUserPHIDType::TYPECONST) {
       $class = 'phui-link-person';
     }
 
     return javelin_tag(
       'a',
       array(
         'href'    => $handle->getURI(),
         'target'  => $this->framed ? '_top' : null,
         'sigil'   => $this->hovercard ? 'hovercard' : null,
         'meta'    => $this->hovercard ? array('hoverPHID' => $phid) : null,
         'class'   => $class,
       ),
       $handle->getLinkName());
   }
 
   final protected function renderString($str) {
     switch ($this->getRenderingTarget()) {
       case PhabricatorApplicationTransaction::TARGET_TEXT:
         return $str;
       case PhabricatorApplicationTransaction::TARGET_HTML:
         return phutil_tag('strong', array(), $str);
     }
   }
 
   final public function renderSummary($text, $len = 128) {
     if ($len) {
       $text = id(new PhutilUTF8StringTruncator())
         ->setMaximumGlyphs($len)
         ->truncateString($text);
     }
     switch ($this->getRenderingTarget()) {
       case PhabricatorApplicationTransaction::TARGET_HTML:
         $text = phutil_escape_html_newlines($text);
         break;
     }
     return $text;
   }
 
   public function getNotificationAggregations() {
     return array();
   }
 
   protected function newStoryView() {
     $view = id(new PHUIFeedStoryView())
       ->setChronologicalKey($this->getChronologicalKey())
       ->setEpoch($this->getEpoch())
       ->setViewed($this->getHasViewed());
 
     $project_phids = $this->getProjectPHIDs();
     if ($project_phids) {
       $view->setTags($this->renderHandleList($project_phids));
     }
 
     return $view;
   }
 
   public function setProjectPHIDs(array $phids) {
     $this->projectPHIDs = $phids;
     return $this;
   }
 
   public function getProjectPHIDs() {
     return $this->projectPHIDs;
   }
 
   public function getFieldStoryMarkupFields() {
     return array();
   }
 
 
 /* -(  PhabricatorPolicyInterface Implementation  )-------------------------- */
 
   public function getPHID() {
     return null;
   }
 
   /**
    * @task policy
    */
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
     );
   }
 
 
   /**
    * @task policy
    */
   public function getPolicy($capability) {
-    // If this story's primary object is a policy-aware object, use its policy
-    // to control story visiblity.
-
-    $primary_phid = $this->getPrimaryObjectPHID();
-    if (isset($this->objects[$primary_phid])) {
-      $object = $this->objects[$primary_phid];
-      if ($object instanceof PhabricatorPolicyInterface) {
-        return $object->getPolicy($capability);
-      }
+    $policy_object = $this->getPrimaryPolicyObject();
+    if ($policy_object) {
+      return $policy_object->getPolicy($capability);
     }
 
     // TODO: Remove this once all objects are policy-aware. For now, keep
     // respecting the `feed.public` setting.
     return PhabricatorEnv::getEnvConfig('feed.public')
       ? PhabricatorPolicies::POLICY_PUBLIC
       : PhabricatorPolicies::POLICY_USER;
   }
 
 
   /**
    * @task policy
    */
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
+    $policy_object = $this->getPrimaryPolicyObject();
+    if ($policy_object) {
+      return $policy_object->hasAutomaticCapability($capability, $viewer);
+    }
+
     return false;
   }
 
   public function describeAutomaticCapability($capability) {
     return null;
   }
 
 
+  /**
+   * Get the policy object this story is about, if such a policy object
+   * exists.
+   *
+   * @return PhabricatorPolicyInterface|null Policy object, if available.
+   * @task policy
+   */
+  private function getPrimaryPolicyObject() {
+    $primary_phid = $this->getPrimaryObjectPHID();
+    if (empty($this->objects[$primary_phid])) {
+      $object = $this->objects[$primary_phid];
+      if ($object instanceof PhabricatorPolicyInterface) {
+        return $object;
+      }
+    }
+
+    return null;
+  }
+
+
 /* -(  PhabricatorMarkupInterface Implementation )--------------------------- */
 
 
   public function getMarkupFieldKey($field) {
     return 'feed:'.$this->getChronologicalKey().':'.$field;
   }
 
   public function newMarkupEngine($field) {
     return PhabricatorMarkupEngine::newMarkupEngine(array());
   }
 
   public function getMarkupText($field) {
     throw new PhutilMethodNotImplementedException();
   }
 
   public function didMarkupText(
     $field,
     $output,
     PhutilMarkupEngine $engine) {
     return $output;
   }
 
   public function shouldUseMarkupCache($field) {
     return true;
   }
 
 }
diff --git a/src/applications/files/PhabricatorImageTransformer.php b/src/applications/files/PhabricatorImageTransformer.php
index 95cf1fac7a..829f5d9f2d 100644
--- a/src/applications/files/PhabricatorImageTransformer.php
+++ b/src/applications/files/PhabricatorImageTransformer.php
@@ -1,661 +1,396 @@
 <?php
 
 /**
  * @task enormous Detecting Enormous Images
  * @task save     Saving Image Data
  */
 final class PhabricatorImageTransformer {
 
   public function executeMemeTransform(
     PhabricatorFile $file,
     $upper_text,
     $lower_text) {
     $image = $this->applyMemeToFile($file, $upper_text, $lower_text);
     return PhabricatorFile::newFromFileData(
       $image,
       array(
         'name' => 'meme-'.$file->getName(),
         'ttl' => time() + 60 * 60 * 24,
         'canCDN' => true,
       ));
   }
 
-  public function executeThumbTransform(
-    PhabricatorFile $file,
-    $x,
-    $y) {
-
-    $image = $this->crudelyScaleTo($file, $x, $y);
-
-    return PhabricatorFile::newFromFileData(
-      $image,
-      array(
-        'name' => 'thumb-'.$file->getName(),
-        'canCDN' => true,
-      ));
-  }
-
-  public function executeProfileTransform(
-    PhabricatorFile $file,
-    $x,
-    $min_y,
-    $max_y) {
-
-    $image = $this->crudelyCropTo($file, $x, $min_y, $max_y);
-
-    return PhabricatorFile::newFromFileData(
-      $image,
-      array(
-        'name' => 'profile-'.$file->getName(),
-        'canCDN' => true,
-      ));
-  }
-
-  public function executePreviewTransform(
-    PhabricatorFile $file,
-    $size) {
-
-    $image = $this->generatePreview($file, $size);
-
-    return PhabricatorFile::newFromFileData(
-      $image,
-      array(
-        'name' => 'preview-'.$file->getName(),
-        'canCDN' => true,
-      ));
-  }
-
   public function executeConpherenceTransform(
     PhabricatorFile $file,
     $top,
     $left,
     $width,
     $height) {
 
     $image = $this->crasslyCropTo(
       $file,
       $top,
       $left,
       $width,
       $height);
 
     return PhabricatorFile::newFromFileData(
       $image,
       array(
         'name' => 'conpherence-'.$file->getName(),
+        'profile' => true,
         'canCDN' => true,
       ));
   }
 
-  private function crudelyCropTo(PhabricatorFile $file, $x, $min_y, $max_y) {
-    $data = $file->loadFileData();
-    $img = imagecreatefromstring($data);
-    $sx = imagesx($img);
-    $sy = imagesy($img);
-
-    $scaled_y = ($x / $sx) * $sy;
-    if ($scaled_y > $max_y) {
-      // This image is very tall and thin.
-      $scaled_y = $max_y;
-    } else if ($scaled_y < $min_y) {
-      // This image is very short and wide.
-      $scaled_y = $min_y;
-    }
-
-    $cropped = $this->applyScaleWithImagemagick($file, $x, $scaled_y);
-    if ($cropped != null) {
-      return $cropped;
-    }
-
-    $img = $this->applyScaleTo(
-      $file,
-      $x,
-      $scaled_y);
-
-    return self::saveImageDataInAnyFormat($img, $file->getMimeType());
-  }
-
   private function crasslyCropTo(PhabricatorFile $file, $top, $left, $w, $h) {
     $data = $file->loadFileData();
     $src = imagecreatefromstring($data);
     $dst = $this->getBlankDestinationFile($w, $h);
 
     $scale = self::getScaleForCrop($file, $w, $h);
     $orig_x = $left / $scale;
     $orig_y = $top / $scale;
     $orig_w = $w / $scale;
     $orig_h = $h / $scale;
 
     imagecopyresampled(
       $dst,
       $src,
       0, 0,
       $orig_x, $orig_y,
       $w, $h,
       $orig_w, $orig_h);
 
     return self::saveImageDataInAnyFormat($dst, $file->getMimeType());
   }
 
-
-  /**
-   * Very crudely scale an image up or down to an exact size.
-   */
-  private function crudelyScaleTo(PhabricatorFile $file, $dx, $dy) {
-    $scaled = $this->applyScaleWithImagemagick($file, $dx, $dy);
-
-    if ($scaled != null) {
-      return $scaled;
-    }
-
-    $dst = $this->applyScaleTo($file, $dx, $dy);
-    return self::saveImageDataInAnyFormat($dst, $file->getMimeType());
-  }
-
   private function getBlankDestinationFile($dx, $dy) {
     $dst = imagecreatetruecolor($dx, $dy);
     imagesavealpha($dst, true);
     imagefill($dst, 0, 0, imagecolorallocatealpha($dst, 255, 255, 255, 127));
 
     return $dst;
   }
 
-  private function applyScaleTo(PhabricatorFile $file, $dx, $dy) {
-    $data = $file->loadFileData();
-    $src = imagecreatefromstring($data);
-
-    $x = imagesx($src);
-    $y = imagesy($src);
-
-    $scale = min(($dx / $x), ($dy / $y), 1);
-
-    $sdx = $scale * $x;
-    $sdy = $scale * $y;
-
-    $dst = $this->getBlankDestinationFile($dx, $dy);
-    imagesavealpha($dst, true);
-    imagefill($dst, 0, 0, imagecolorallocatealpha($dst, 255, 255, 255, 127));
-
-    imagecopyresampled(
-      $dst,
-      $src,
-      ($dx - $sdx) / 2,  ($dy - $sdy) / 2,
-      0, 0,
-      $sdx, $sdy,
-      $x, $y);
-
-    return $dst;
-
-  }
-
-  public static function getPreviewDimensions(PhabricatorFile $file, $size) {
-    $metadata = $file->getMetadata();
-    $x = idx($metadata, PhabricatorFile::METADATA_IMAGE_WIDTH);
-    $y = idx($metadata, PhabricatorFile::METADATA_IMAGE_HEIGHT);
-
-    if (!$x || !$y) {
-      $data = $file->loadFileData();
-      $src = imagecreatefromstring($data);
-
-      $x = imagesx($src);
-      $y = imagesy($src);
-    }
-
-    $scale = min($size / $x, $size / $y, 1);
-
-    $dx = max($size / 4, $scale * $x);
-    $dy = max($size / 4, $scale * $y);
-
-    $sdx = $scale * $x;
-    $sdy = $scale * $y;
-
-    return array(
-      'x' => $x,
-      'y' => $y,
-      'dx' => $dx,
-      'dy' => $dy,
-      'sdx' => $sdx,
-      'sdy' => $sdy,
-    );
-  }
-
   public static function getScaleForCrop(
     PhabricatorFile $file,
     $des_width,
     $des_height) {
 
     $metadata = $file->getMetadata();
     $width = $metadata[PhabricatorFile::METADATA_IMAGE_WIDTH];
     $height = $metadata[PhabricatorFile::METADATA_IMAGE_HEIGHT];
 
     if ($height < $des_height) {
       $scale = $height / $des_height;
     } else if ($width < $des_width) {
       $scale = $width / $des_width;
     } else {
       $scale_x = $des_width / $width;
       $scale_y = $des_height / $height;
       $scale = max($scale_x, $scale_y);
     }
 
     return $scale;
   }
 
-  private function generatePreview(PhabricatorFile $file, $size) {
-    $data = $file->loadFileData();
-    $src = imagecreatefromstring($data);
-
-    $dimensions = self::getPreviewDimensions($file, $size);
-    $x = $dimensions['x'];
-    $y = $dimensions['y'];
-    $dx = $dimensions['dx'];
-    $dy = $dimensions['dy'];
-    $sdx = $dimensions['sdx'];
-    $sdy = $dimensions['sdy'];
-
-    $dst = $this->getBlankDestinationFile($dx, $dy);
-
-    imagecopyresampled(
-      $dst,
-      $src,
-      ($dx - $sdx) / 2, ($dy - $sdy) / 2,
-      0, 0,
-      $sdx, $sdy,
-      $x, $y);
-
-    return self::saveImageDataInAnyFormat($dst, $file->getMimeType());
-  }
-
   private function applyMemeToFile(
     PhabricatorFile $file,
     $upper_text,
     $lower_text) {
     $data = $file->loadFileData();
 
     $img_type = $file->getMimeType();
     $imagemagick = PhabricatorEnv::getEnvConfig('files.enable-imagemagick');
 
     if ($img_type != 'image/gif' || $imagemagick == false) {
       return $this->applyMemeTo(
         $data, $upper_text, $lower_text, $img_type);
     }
 
     $data = $file->loadFileData();
     $input = new TempFile();
     Filesystem::writeFile($input, $data);
 
     list($out) = execx('convert %s info:', $input);
     $split = phutil_split_lines($out);
     if (count($split) > 1) {
       return $this->applyMemeWithImagemagick(
         $input,
         $upper_text,
         $lower_text,
         count($split),
         $img_type);
     } else {
       return $this->applyMemeTo($data, $upper_text, $lower_text, $img_type);
     }
   }
 
   private function applyMemeTo(
     $data,
     $upper_text,
     $lower_text,
     $mime_type) {
     $img = imagecreatefromstring($data);
 
     // Some PNGs have color palettes, and allocating the dark border color
     // fails and gives us whatever's first in the color table. Copy the image
     // to a fresh truecolor canvas before working with it.
 
     $truecolor = imagecreatetruecolor(imagesx($img), imagesy($img));
     imagecopy($truecolor, $img, 0, 0, 0, 0, imagesx($img), imagesy($img));
     $img = $truecolor;
 
     $phabricator_root = dirname(phutil_get_library_root('phabricator'));
     $font_root = $phabricator_root.'/resources/font/';
     $font_path = $font_root.'tuffy.ttf';
     if (Filesystem::pathExists($font_root.'impact.ttf')) {
       $font_path = $font_root.'impact.ttf';
     }
     $text_color = imagecolorallocate($img, 255, 255, 255);
     $border_color = imagecolorallocatealpha($img, 0, 0, 0, 110);
     $border_width = 4;
     $font_max = 200;
     $font_min = 5;
     for ($i = $font_max; $i > $font_min; $i--) {
       $fit = $this->doesTextBoundingBoxFitInImage(
         $img,
         $upper_text,
         $i,
         $font_path);
       if ($fit['doesfit']) {
         $x = ($fit['imgwidth'] - $fit['txtwidth']) / 2;
         $y = $fit['txtheight'] + 10;
         $this->makeImageWithTextBorder($img,
           $i,
           $x,
           $y,
           $text_color,
           $border_color,
           $border_width,
           $font_path,
           $upper_text);
         break;
       }
     }
     for ($i = $font_max; $i > $font_min; $i--) {
       $fit = $this->doesTextBoundingBoxFitInImage($img,
         $lower_text, $i, $font_path);
       if ($fit['doesfit']) {
         $x = ($fit['imgwidth'] - $fit['txtwidth']) / 2;
         $y = $fit['imgheight'] - 10;
         $this->makeImageWithTextBorder(
           $img,
           $i,
           $x,
           $y,
           $text_color,
           $border_color,
           $border_width,
           $font_path,
           $lower_text);
         break;
       }
     }
     return self::saveImageDataInAnyFormat($img, $mime_type);
   }
 
   private function makeImageWithTextBorder($img, $font_size, $x, $y,
     $color, $stroke_color, $bw, $font, $text) {
     $angle = 0;
     $bw = abs($bw);
     for ($c1 = $x - $bw; $c1 <= $x + $bw; $c1++) {
       for ($c2 = $y - $bw; $c2 <= $y + $bw; $c2++) {
         if (!(($c1 == $x - $bw || $x + $bw) &&
           $c2 == $y - $bw || $c2 == $y + $bw)) {
           $bg = imagettftext($img, $font_size,
             $angle, $c1, $c2, $stroke_color, $font, $text);
           }
         }
       }
     imagettftext($img, $font_size, $angle,
             $x , $y, $color , $font, $text);
   }
 
   private function doesTextBoundingBoxFitInImage($img,
     $text, $font_size, $font_path) {
     // Default Angle = 0
     $angle = 0;
 
     $bbox = imagettfbbox($font_size, $angle, $font_path, $text);
     $text_height = abs($bbox[3] - $bbox[5]);
     $text_width = abs($bbox[0] - $bbox[2]);
     return array(
       'doesfit' => ($text_height * 1.05 <= imagesy($img) / 2
         && $text_width * 1.05 <= imagesx($img)),
       'txtwidth' => $text_width,
       'txtheight' => $text_height,
       'imgwidth' => imagesx($img),
       'imgheight' => imagesy($img),
     );
   }
 
-  private function applyScaleWithImagemagick(PhabricatorFile $file, $dx, $dy) {
-    $img_type = $file->getMimeType();
-    $imagemagick = PhabricatorEnv::getEnvConfig('files.enable-imagemagick');
-
-    if ($img_type != 'image/gif' || $imagemagick == false) {
-      return null;
-    }
-
-    $data = $file->loadFileData();
-    $src = imagecreatefromstring($data);
-
-    $x = imagesx($src);
-    $y = imagesy($src);
-
-    if (self::isEnormousGIF($x, $y)) {
-      return null;
-    }
-
-    $scale = min(($dx / $x), ($dy / $y), 1);
-
-    $sdx = $scale * $x;
-    $sdy = $scale * $y;
-
-    $input = new TempFile();
-    Filesystem::writeFile($input, $data);
-
-    $resized = new TempFile();
-
-    $future = new ExecFuture(
-      'convert %s -coalesce -resize %sX%s%s %s',
-      $input,
-      $sdx,
-      $sdy,
-      '!',
-      $resized);
-
-    // Don't spend more than 10 seconds resizing; just fail if it takes longer
-    // than that.
-    $future->setTimeout(10)->resolvex();
-
-    return Filesystem::readFile($resized);
-  }
-
   private function applyMemeWithImagemagick(
     $input,
     $above,
     $below,
     $count,
     $img_type) {
 
     $output = new TempFile();
     $future = new ExecFuture(
       'convert %s -coalesce +adjoin %s_%s',
       $input,
       $input,
       '%09d');
     $future->setTimeout(10)->resolvex();
 
     $output_files = array();
     for ($ii = 0; $ii < $count; $ii++) {
       $frame_name = sprintf('%s_%09d', $input, $ii);
       $output_name = sprintf('%s_%09d', $output, $ii);
 
       $output_files[] = $output_name;
 
       $frame_data = Filesystem::readFile($frame_name);
       $memed_frame_data = $this->applyMemeTo(
         $frame_data,
         $above,
         $below,
         $img_type);
       Filesystem::writeFile($output_name, $memed_frame_data);
     }
 
     $future = new ExecFuture('convert -loop 0 %Ls %s', $output_files, $output);
     $future->setTimeout(10)->resolvex();
 
     return Filesystem::readFile($output);
   }
 
-/* -(  Detecting Enormous Files  )------------------------------------------- */
-
-
-  /**
-   * Determine if an image is enormous (too large to transform).
-   *
-   * Attackers can perform a denial of service attack by uploading highly
-   * compressible images with enormous dimensions but a very small filesize.
-   * Transforming them (e.g., into thumbnails) may consume huge quantities of
-   * memory and CPU relative to the resources required to transmit the file.
-   *
-   * In general, we respond to these images by declining to transform them, and
-   * using a default thumbnail instead.
-   *
-   * @param int Width of the image, in pixels.
-   * @param int Height of the image, in pixels.
-   * @return bool True if this image is enormous (too large to transform).
-   * @task enormous
-   */
-  public static function isEnormousImage($x, $y) {
-    // This is just a sanity check, but if we don't have valid dimensions we
-    // shouldn't be trying to transform the file.
-    if (($x <= 0) || ($y <= 0)) {
-      return true;
-    }
-
-    return ($x * $y) > (4096 * 4096);
-  }
-
-
-  /**
-   * Determine if a GIF is enormous (too large to transform).
-   *
-   * For discussion, see @{method:isEnormousImage}. We need to be more
-   * careful about GIFs, because they can also have a large number of frames
-   * despite having a very small filesize. We're more conservative about
-   * calling GIFs enormous than about calling images in general enormous.
-   *
-   * @param int Width of the GIF, in pixels.
-   * @param int Height of the GIF, in pixels.
-   * @return bool True if this image is enormous (too large to transform).
-   * @task enormous
-   */
-  public static function isEnormousGIF($x, $y) {
-    if (self::isEnormousImage($x, $y)) {
-      return true;
-    }
-
-    return ($x * $y) > (800 * 800);
-  }
-
 
 /* -(  Saving Image Data  )-------------------------------------------------- */
 
 
   /**
    * Save an image resource to a string representation suitable for storage or
    * transmission as an image file.
    *
    * Optionally, you can specify a preferred MIME type like `"image/png"`.
    * Generally, you should specify the MIME type of the original file if you're
    * applying file transformations. The MIME type may not be honored if
    * Phabricator can not encode images in the given format (based on available
    * extensions), but can save images in another format.
    *
    * @param   resource  GD image resource.
    * @param   string?   Optionally, preferred mime type.
    * @return  string    Bytes of an image file.
    * @task save
    */
   public static function saveImageDataInAnyFormat($data, $preferred_mime = '') {
     $preferred = null;
     switch ($preferred_mime) {
       case 'image/gif':
         $preferred = self::saveImageDataAsGIF($data);
         break;
       case 'image/png':
         $preferred = self::saveImageDataAsPNG($data);
         break;
     }
 
     if ($preferred !== null) {
       return $preferred;
     }
 
     $data = self::saveImageDataAsJPG($data);
     if ($data !== null) {
       return $data;
     }
 
     $data = self::saveImageDataAsPNG($data);
     if ($data !== null) {
       return $data;
     }
 
     $data = self::saveImageDataAsGIF($data);
     if ($data !== null) {
       return $data;
     }
 
     throw new Exception(pht('Failed to save image data into any format.'));
   }
 
 
   /**
    * Save an image in PNG format, returning the file data as a string.
    *
    * @param resource      GD image resource.
    * @return string|null  PNG file as a string, or null on failure.
    * @task save
    */
   private static function saveImageDataAsPNG($image) {
     if (!function_exists('imagepng')) {
       return null;
     }
 
     ob_start();
     $result = imagepng($image, null, 9);
     $output = ob_get_clean();
 
     if (!$result) {
       return null;
     }
 
     return $output;
   }
 
 
   /**
    * Save an image in GIF format, returning the file data as a string.
    *
    * @param resource      GD image resource.
    * @return string|null  GIF file as a string, or null on failure.
    * @task save
    */
   private static function saveImageDataAsGIF($image) {
     if (!function_exists('imagegif')) {
       return null;
     }
 
     ob_start();
     $result = imagegif($image);
     $output = ob_get_clean();
 
     if (!$result) {
       return null;
     }
 
     return $output;
   }
 
 
   /**
    * Save an image in JPG format, returning the file data as a string.
    *
    * @param resource      GD image resource.
    * @return string|null  JPG file as a string, or null on failure.
    * @task save
    */
   private static function saveImageDataAsJPG($image) {
     if (!function_exists('imagejpeg')) {
       return null;
     }
 
     ob_start();
     $result = imagejpeg($image);
     $output = ob_get_clean();
 
     if (!$result) {
       return null;
     }
 
     return $output;
   }
 
 
 }
diff --git a/src/applications/files/application/PhabricatorFilesApplication.php b/src/applications/files/application/PhabricatorFilesApplication.php
index 1187b0545b..ed4801ace6 100644
--- a/src/applications/files/application/PhabricatorFilesApplication.php
+++ b/src/applications/files/application/PhabricatorFilesApplication.php
@@ -1,113 +1,115 @@
 <?php
 
 final class PhabricatorFilesApplication extends PhabricatorApplication {
 
   public function getBaseURI() {
     return '/file/';
   }
 
   public function getName() {
     return pht('Files');
   }
 
   public function getShortDescription() {
     return pht('Store and Share Files');
   }
 
   public function getFontIcon() {
     return 'fa-file';
   }
 
   public function getTitleGlyph() {
     return "\xE2\x87\xAA";
   }
 
   public function getFlavorText() {
     return pht('Blob store for Pokemon pictures.');
   }
 
   public function getApplicationGroup() {
     return self::GROUP_UTILITIES;
   }
 
   public function canUninstall() {
     return false;
   }
 
   public function getRemarkupRules() {
     return array(
       new PhabricatorEmbedFileRemarkupRule(),
     );
   }
 
   public function supportsEmailIntegration() {
     return true;
   }
 
   public function getAppEmailBlurb() {
     return pht(
       'Send emails with file attachments to these addresses to upload '.
       'files. %s',
       phutil_tag(
         'a',
         array(
           'href' => $this->getInboundEmailSupportLink(),
         ),
         pht('Learn More')));
   }
 
   protected function getCustomCapabilities() {
     return array(
       FilesDefaultViewCapability::CAPABILITY => array(
         'caption' => pht(
           'Default view policy for newly created files.'),
       ),
     );
   }
 
   public function getRoutes() {
     return array(
       '/F(?P<id>[1-9]\d*)' => 'PhabricatorFileInfoController',
       '/file/' => array(
         '(query/(?P<key>[^/]+)/)?' => 'PhabricatorFileListController',
         'upload/' => 'PhabricatorFileUploadController',
         'dropupload/' => 'PhabricatorFileDropUploadController',
         'compose/' => 'PhabricatorFileComposeController',
         'comment/(?P<id>[1-9]\d*)/' => 'PhabricatorFileCommentController',
         'delete/(?P<id>[1-9]\d*)/' => 'PhabricatorFileDeleteController',
         'edit/(?P<id>[1-9]\d*)/' => 'PhabricatorFileEditController',
         'info/(?P<phid>[^/]+)/' => 'PhabricatorFileInfoController',
         'data/'.
           '(?:@(?P<instance>[^/]+)/)?'.
           '(?P<key>[^/]+)/'.
           '(?P<phid>[^/]+)/'.
           '(?:(?P<token>[^/]+)/)?'.
           '.*'
           => 'PhabricatorFileDataController',
         'proxy/' => 'PhabricatorFileProxyController',
         'xform/'.
           '(?:@(?P<instance>[^/]+)/)?'.
           '(?P<transform>[^/]+)/'.
           '(?P<phid>[^/]+)/'.
           '(?P<key>[^/]+)/'
           => 'PhabricatorFileTransformController',
+        'transforms/(?P<id>[1-9]\d*)/' =>
+          'PhabricatorFileTransformListController',
         'uploaddialog/' => 'PhabricatorFileUploadDialogController',
         'download/(?P<phid>[^/]+)/' => 'PhabricatorFileDialogController',
       ),
     );
   }
 
   public function getMailCommandObjects() {
     return array(
       'file' => array(
         'name' => pht('Email Commands: Files'),
         'header' => pht('Interacting with Files'),
         'object' => new PhabricatorFile(),
         'summary' => pht(
           'This page documents the commands you can use to interact with '.
           'files.'),
       ),
     );
   }
 
 }
diff --git a/src/applications/files/controller/PhabricatorFileComposeController.php b/src/applications/files/controller/PhabricatorFileComposeController.php
index 6903cff5d3..d1e6041131 100644
--- a/src/applications/files/controller/PhabricatorFileComposeController.php
+++ b/src/applications/files/controller/PhabricatorFileComposeController.php
@@ -1,338 +1,339 @@
 <?php
 
 final class PhabricatorFileComposeController
   extends PhabricatorFileController {
 
   public function processRequest() {
     $request = $this->getRequest();
     $viewer = $request->getUser();
 
     $colors = array(
       'red' => pht('Verbillion'),
       'orange' => pht('Navel Orange'),
       'yellow' => pht('Prim Goldenrod'),
       'green' => pht('Lustrous Verdant'),
       'blue' => pht('Tropical Deep'),
       'sky' => pht('Wide Open Sky'),
       'indigo' => pht('Pleated Khaki'),
       'violet' => pht('Aged Merlot'),
       'pink' => pht('Easter Bunny'),
       'charcoal' => pht('Gemstone'),
       'backdrop' => pht('Driven Snow'),
     );
 
     $manifest = PHUIIconView::getSheetManifest(PHUIIconView::SPRITE_PROJECTS);
 
     if ($request->isFormPost()) {
       $project_phid = $request->getStr('projectPHID');
       if ($project_phid) {
         $project = id(new PhabricatorProjectQuery())
           ->setViewer($viewer)
           ->withPHIDs(array($project_phid))
           ->requireCapabilities(
             array(
               PhabricatorPolicyCapability::CAN_VIEW,
               PhabricatorPolicyCapability::CAN_EDIT,
             ))
           ->executeOne();
         if (!$project) {
           return new Aphront404Response();
         }
         $icon = $project->getIcon();
         $color = $project->getColor();
         switch ($color) {
           case 'grey':
             $color = 'charcoal';
             break;
           case 'checkered':
             $color = 'backdrop';
             break;
         }
       } else {
         $icon = $request->getStr('icon');
         $color = $request->getStr('color');
       }
 
       if (!isset($colors[$color]) || !isset($manifest['projects-'.$icon])) {
         return new Aphront404Response();
       }
 
       $root = dirname(phutil_get_library_root('phabricator'));
-      $icon_file = $root.'/resources/sprite/projects_1x/'.$icon.'.png';
+      $icon_file = $root.'/resources/sprite/projects_2x/'.$icon.'.png';
       $icon_data = Filesystem::readFile($icon_file);
 
 
       $data = $this->composeImage($color, $icon_data);
 
       $file = PhabricatorFile::buildFromFileDataOrHash(
         $data,
         array(
           'name' => 'project.png',
+          'profile' => true,
           'canCDN' => true,
         ));
 
       if ($project_phid) {
         $edit_uri = '/project/profile/'.$project->getID().'/';
 
         $xactions = array();
         $xactions[] = id(new PhabricatorProjectTransaction())
           ->setTransactionType(PhabricatorProjectTransaction::TYPE_IMAGE)
           ->setNewValue($file->getPHID());
 
         $editor = id(new PhabricatorProjectTransactionEditor())
           ->setActor($viewer)
           ->setContentSourceFromRequest($request)
           ->setContinueOnMissingFields(true)
           ->setContinueOnNoEffect(true);
 
         $editor->applyTransactions($project, $xactions);
 
         return id(new AphrontRedirectResponse())->setURI($edit_uri);
       } else {
         $content = array(
           'phid' => $file->getPHID(),
         );
 
         return id(new AphrontAjaxResponse())->setContent($content);
       }
     }
 
     $value_color = head_key($colors);
     $value_icon = head_key($manifest);
     $value_icon = substr($value_icon, strlen('projects-'));
 
     require_celerity_resource('people-profile-css');
 
     $buttons = array();
     foreach ($colors as $color => $name) {
       $buttons[] = javelin_tag(
         'button',
         array(
           'class' => 'grey profile-image-button',
           'sigil' => 'has-tooltip compose-select-color',
           'style' => 'margin: 0 8px 8px 0',
           'meta' => array(
             'color' => $color,
             'tip' => $name,
           ),
         ),
         id(new PHUIIconView())
           ->addClass('compose-background-'.$color));
     }
 
      $sort_these_first = array(
       'projects-fa-briefcase',
       'projects-fa-tags',
       'projects-fa-folder',
       'projects-fa-group',
       'projects-fa-bug',
       'projects-fa-trash-o',
       'projects-fa-calendar',
       'projects-fa-flag-checkered',
       'projects-fa-envelope',
       'projects-fa-truck',
       'projects-fa-lock',
       'projects-fa-umbrella',
       'projects-fa-cloud',
       'projects-fa-building',
       'projects-fa-credit-card',
       'projects-fa-flask',
     );
 
     $manifest = array_select_keys(
       $manifest,
       $sort_these_first)
     + $manifest;
 
     $icons = array();
 
     $icon_quips = array(
       '8ball' => pht('Take a Risk'),
       'alien' => pht('Foreign Interface'),
       'announce' => pht('Louder is Better'),
       'art' => pht('Unique Snowflake'),
       'award' => pht('Shooting Star'),
       'bacon' => pht('Healthy Vegetables'),
       'bandaid' => pht('Durable Infrastructure'),
       'beer' => pht('Healthy Vegetable Juice'),
       'bomb' => pht('Imminent Success'),
       'briefcase' => pht('Adventure Pack'),
       'bug' => pht('Costumed Egg'),
       'calendar' => pht('Everyone Loves Meetings'),
       'cloud' => pht('Water Cycle'),
       'coffee' => pht('Half-Whip Nonfat Soy Latte'),
       'creditcard' => pht('Expense It'),
       'death' => pht('Calcium Promotes Bone Health'),
       'desktop' => pht('Magical Portal'),
       'dropbox' => pht('Cardboard Box'),
       'education' => pht('Debt'),
       'experimental' => pht('CAUTION: Dangerous Chemicals'),
       'facebook' => pht('Popular Social Network'),
       'facility' => pht('Pollution Solves Problems'),
       'film' => pht('Actual Physical Film'),
       'forked' => pht('You Can\'t Eat Soup'),
       'games' => pht('Serious Business'),
       'ghost' => pht('Haunted'),
       'gift' => pht('Surprise!'),
       'globe' => pht('Scanner Sweep'),
       'golf' => pht('Business Meeting'),
       'heart' => pht('Undergoing a Major Surgery'),
       'intergalactic' => pht('Jupiter'),
       'lock' => pht('Extremely Secret'),
       'mail' => pht('Oragami'),
       'martini' => pht('Healthy Olive Drink'),
       'medical' => pht('Medic!'),
       'mobile' => pht('Cellular Telephone'),
       'music' => pht("\xE2\x99\xAB"),
       'news' => pht('Actual Physical Newspaper'),
       'orgchart' => pht('It\'s Good to be King'),
       'peoples' => pht('Angel and Devil'),
       'piechart' => pht('Actual Physical Pie'),
       'poison' => pht('Healthy Bone Juice'),
       'putabirdonit' => pht('Put a Bird On It'),
       'radiate' => pht('Radiant Beauty'),
       'savings' => pht('Oink Oink'),
       'search' => pht('Sleuthing'),
       'shield' => pht('Royal Crest'),
       'speed' => pht('Slow and Steady'),
       'sprint' => pht('Fire Exit'),
       'star' => pht('The More You Know'),
       'storage' => pht('Stack of Pancakes'),
       'tablet' => pht('Cellular Telephone For Giants'),
       'travel' => pht('Pretty Clearly an Airplane'),
       'twitter' => pht('Bird Stencil'),
       'warning' => pht('No Caution Required, Everything Looks Safe'),
       'whale' => pht('Friendly Walrus'),
       'fa-flask' => pht('Experimental'),
       'fa-briefcase' => pht('Briefcase'),
       'fa-bug' => pht('Bug'),
       'fa-building' => pht('Company'),
       'fa-calendar' => pht('Deadline'),
       'fa-cloud' => pht('The Cloud'),
       'fa-credit-card' => pht('Accounting'),
       'fa-envelope' => pht('Communication'),
       'fa-flag-checkered' => pht('Goal'),
       'fa-folder' => pht('Folder'),
       'fa-group' => pht('Team'),
       'fa-lock' => pht('Policy'),
       'fa-tags' => pht('Tag'),
       'fa-trash-o' => pht('Garbage'),
       'fa-truck' => pht('Release'),
       'fa-umbrella' => pht('An Umbrella'),
     );
 
     foreach ($manifest as $icon => $spec) {
       $icon = substr($icon, strlen('projects-'));
 
       $icons[] = javelin_tag(
         'button',
         array(
           'class' => 'grey profile-image-button',
           'sigil' => 'has-tooltip compose-select-icon',
           'style' => 'margin: 0 8px 8px 0',
           'meta' => array(
             'icon' => $icon,
             'tip' => idx($icon_quips, $icon, $icon),
           ),
         ),
         id(new PHUIIconView())
           ->setSpriteIcon($icon)
           ->setSpriteSheet(PHUIIconView::SPRITE_PROJECTS));
     }
 
     $dialog_id = celerity_generate_unique_node_id();
-    $color_input_id = celerity_generate_unique_node_id();;
+    $color_input_id = celerity_generate_unique_node_id();
     $icon_input_id = celerity_generate_unique_node_id();
     $preview_id = celerity_generate_unique_node_id();
 
     $preview = id(new PHUIIconView())
       ->setID($preview_id)
       ->addClass('compose-background-'.$value_color)
       ->setSpriteIcon($value_icon)
       ->setSpriteSheet(PHUIIconView::SPRITE_PROJECTS);
 
     $color_input = javelin_tag(
       'input',
       array(
         'type' => 'hidden',
         'name' => 'color',
         'value' => $value_color,
         'id' => $color_input_id,
       ));
 
     $icon_input = javelin_tag(
       'input',
       array(
         'type' => 'hidden',
         'name' => 'icon',
         'value' => $value_icon,
         'id' => $icon_input_id,
       ));
 
     Javelin::initBehavior('phabricator-tooltips');
     Javelin::initBehavior(
       'icon-composer',
       array(
         'dialogID' => $dialog_id,
         'colorInputID' => $color_input_id,
         'iconInputID' => $icon_input_id,
         'previewID' => $preview_id,
         'defaultColor' => $value_color,
         'defaultIcon' => $value_icon,
       ));
 
     $dialog = id(new AphrontDialogView())
       ->setUser($viewer)
       ->setFormID($dialog_id)
       ->setClass('compose-dialog')
       ->setTitle(pht('Compose Image'))
       ->appendChild(
         phutil_tag(
           'div',
           array(
             'class' => 'compose-header',
           ),
           pht('Choose Background Color')))
       ->appendChild($buttons)
       ->appendChild(
         phutil_tag(
           'div',
           array(
             'class' => 'compose-header',
           ),
           pht('Choose Icon')))
       ->appendChild($icons)
       ->appendChild(
         phutil_tag(
           'div',
           array(
             'class' => 'compose-header',
           ),
           pht('Preview')))
       ->appendChild($preview)
       ->appendChild($color_input)
       ->appendChild($icon_input)
       ->addCancelButton('/')
       ->addSubmitButton(pht('Save Image'));
 
     return id(new AphrontDialogResponse())->setDialog($dialog);
   }
 
   private function composeImage($color, $icon_data) {
     $icon_img = imagecreatefromstring($icon_data);
 
     $map = CelerityResourceTransformer::getCSSVariableMap();
     $color_string = idx($map, $color, '#ff00ff');
     $color_const = hexdec(trim($color_string, '#'));
 
-    $canvas = imagecreatetruecolor(50, 50);
+    $canvas = imagecreatetruecolor(100, 100);
     imagefill($canvas, 0, 0, $color_const);
 
-    imagecopy($canvas, $icon_img, 0, 0, 0, 0, 50, 50);
+    imagecopy($canvas, $icon_img, 0, 0, 0, 0, 100, 100);
 
     return PhabricatorImageTransformer::saveImageDataInAnyFormat(
       $canvas,
       'image/png');
   }
 
 }
diff --git a/src/applications/files/controller/PhabricatorFileDataController.php b/src/applications/files/controller/PhabricatorFileDataController.php
index 2a66a3807e..e528eb15f2 100644
--- a/src/applications/files/controller/PhabricatorFileDataController.php
+++ b/src/applications/files/controller/PhabricatorFileDataController.php
@@ -1,235 +1,235 @@
 <?php
 
 final class PhabricatorFileDataController extends PhabricatorFileController {
 
   private $phid;
   private $key;
   private $token;
   private $file;
 
   public function willProcessRequest(array $data) {
     $this->phid = $data['phid'];
     $this->key  = $data['key'];
     $this->token = idx($data, 'token');
   }
 
   public function shouldRequireLogin() {
     return false;
   }
 
   public function processRequest() {
     $request = $this->getRequest();
     $viewer = $this->getViewer();
 
     $alt = PhabricatorEnv::getEnvConfig('security.alternate-file-domain');
     $base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
     $alt_uri = new PhutilURI($alt);
     $alt_domain = $alt_uri->getDomain();
     $req_domain = $request->getHost();
     $main_domain = id(new PhutilURI($base_uri))->getDomain();
 
     $cache_response = true;
 
     if (empty($alt) || $main_domain == $alt_domain) {
       // Alternate files domain isn't configured or it's set
       // to the same as the default domain
 
       $response = $this->loadFile($viewer);
       if ($response) {
         return $response;
       }
       $file = $this->getFile();
 
       // when the file is not CDNable, don't allow cache
       $cache_response = $file->getCanCDN();
     } else if ($req_domain != $alt_domain) {
       // Alternate domain is configured but this request isn't using it
 
       $response = $this->loadFile($viewer);
       if ($response) {
         return $response;
       }
       $file = $this->getFile();
 
       // if the user can see the file, generate a token;
       // redirect to the alt domain with the token;
       $token_uri = $file->getCDNURIWithToken();
       $token_uri = new PhutilURI($token_uri);
       $token_uri = $this->addURIParameters($token_uri);
 
       return id(new AphrontRedirectResponse())
         ->setIsExternal(true)
         ->setURI($token_uri);
 
     } else {
       // We are using the alternate domain. We don't have authentication
       // on this domain, so we bypass policy checks when loading the file.
 
       $bypass_policies = PhabricatorUser::getOmnipotentUser();
       $response = $this->loadFile($bypass_policies);
       if ($response) {
         return $response;
       }
       $file = $this->getFile();
 
       $acquire_token_uri = id(new PhutilURI($file->getViewURI()))
         ->setDomain($main_domain);
       $acquire_token_uri = $this->addURIParameters($acquire_token_uri);
 
       if ($this->token) {
         // validate the token, if it is valid, continue
         $validated_token = $file->validateOneTimeToken($this->token);
 
         if (!$validated_token) {
           $dialog = $this->newDialog()
             ->setShortTitle(pht('Expired File'))
             ->setTitle(pht('File Link Has Expired'))
             ->appendParagraph(
               pht(
                 'The link you followed to view this file is invalid or '.
                 'expired.'))
             ->appendParagraph(
               pht(
                 'Continue to generate a new link to the file. You may be '.
                 'required to log in.'))
             ->addCancelButton(
               $acquire_token_uri,
               pht('Continue'));
 
           // Build an explicit response so we can respond with HTTP/403 instead
           // of HTTP/200.
           $response = id(new AphrontDialogResponse())
             ->setDialog($dialog)
             ->setHTTPResponseCode(403);
 
           return $response;
         }
         // return the file data without cache headers
         $cache_response = false;
       } else if (!$file->getCanCDN()) {
         // file cannot be served via cdn, and no token given
         // redirect to the main domain to aquire a token
 
         // This is marked as an "external" URI because it is fully qualified.
         return id(new AphrontRedirectResponse())
           ->setIsExternal(true)
           ->setURI($acquire_token_uri);
       }
     }
 
     $response = new AphrontFileResponse();
     if ($cache_response) {
       $response->setCacheDurationInSeconds(60 * 60 * 24 * 30);
     }
 
     $begin = null;
     $end = null;
 
     // NOTE: It's important to accept "Range" requests when playing audio.
     // If we don't, Safari has difficulty figuring out how long sounds are
     // and glitches when trying to loop them. In particular, Safari sends
     // an initial request for bytes 0-1 of the audio file, and things go south
     // if we can't respond with a 206 Partial Content.
     $range = $request->getHTTPHeader('range');
     if ($range) {
       $matches = null;
       if (preg_match('/^bytes=(\d+)-(\d+)$/', $range, $matches)) {
         // Note that the "Range" header specifies bytes differently than
         // we do internally: the range 0-1 has 2 bytes (byte 0 and byte 1).
         $begin = (int)$matches[1];
         $end = (int)$matches[2] + 1;
 
         $response->setHTTPResponseCode(206);
         $response->setRange($begin, ($end - 1));
       }
     } else if (isset($validated_token)) {
       // We set this on the response, and the response deletes it after the
       // transfer completes. This allows transfers to be resumed, in theory.
       $response->setTemporaryFileToken($validated_token);
     }
 
     $is_viewable = $file->isViewableInBrowser();
     $force_download = $request->getExists('download');
 
     if ($is_viewable && !$force_download) {
       $response->setMimeType($file->getViewableMimeType());
     } else {
       if (!$request->isHTTPPost() && !$alt_domain) {
         // NOTE: Require POST to download files from the primary domain. We'd
         // rather go full-bore and do a real CSRF check, but can't currently
         // authenticate users on the file domain. This should blunt any
         // attacks based on iframes, script tags, applet tags, etc., at least.
         // Send the user to the "info" page if they're using some other method.
 
         // This is marked as "external" because it is fully qualified.
         return id(new AphrontRedirectResponse())
           ->setIsExternal(true)
           ->setURI(PhabricatorEnv::getProductionURI($file->getBestURI()));
       }
       $response->setMimeType($file->getMimeType());
       $response->setDownload($file->getName());
     }
 
     $iterator = $file->getFileDataIterator($begin, $end);
 
     $response->setContentLength($file->getByteSize());
     $response->setContentIterator($iterator);
 
     return $response;
   }
 
   /**
    * Add passthrough parameters to the URI so they aren't lost when we
    * redirect to acquire tokens.
    */
   private function addURIParameters(PhutilURI $uri) {
     $request = $this->getRequest();
 
     if ($request->getBool('download')) {
       $uri->setQueryParam('download', 1);
     }
 
     return $uri;
   }
 
   private function loadFile(PhabricatorUser $viewer) {
     $file = id(new PhabricatorFileQuery())
       ->setViewer($viewer)
       ->withPHIDs(array($this->phid))
       ->executeOne();
 
     if (!$file) {
       return new Aphront404Response();
     }
 
     if (!$file->validateSecretKey($this->key)) {
       return new Aphront403Response();
     }
 
     if ($file->getIsPartial()) {
       // We may be on the CDN domain, so we need to use a fully-qualified URI
       // here to make sure we end up back on the main domain.
       $info_uri = PhabricatorEnv::getURI($file->getInfoURI());
 
       return $this->newDialog()
         ->setTitle(pht('Partial Upload'))
         ->appendParagraph(
           pht(
             'This file has only been partially uploaded. It must be '.
             'uploaded completely before you can download it.'))
         ->addCancelButton($info_uri);
     }
 
     $this->file = $file;
 
     return null;
   }
 
   private function getFile() {
     if (!$this->file) {
-      throw new Exception(pht('Call loadFile() before getFile()!'));
+      throw new PhutilInvalidStateException('loadFile');
     }
     return $this->file;
   }
 
 }
diff --git a/src/applications/files/controller/PhabricatorFileInfoController.php b/src/applications/files/controller/PhabricatorFileInfoController.php
index 3d9731f6b4..0e3d041eac 100644
--- a/src/applications/files/controller/PhabricatorFileInfoController.php
+++ b/src/applications/files/controller/PhabricatorFileInfoController.php
@@ -1,371 +1,382 @@
 <?php
 
 final class PhabricatorFileInfoController extends PhabricatorFileController {
 
   private $phid;
   private $id;
 
   public function shouldAllowPublic() {
     return true;
   }
 
   public function willProcessRequest(array $data) {
     $this->phid = idx($data, 'phid');
     $this->id = idx($data, 'id');
   }
 
   public function processRequest() {
     $request = $this->getRequest();
     $user = $request->getUser();
 
     if ($this->phid) {
       $file = id(new PhabricatorFileQuery())
         ->setViewer($user)
         ->withPHIDs(array($this->phid))
         ->executeOne();
 
       if (!$file) {
         return new Aphront404Response();
       }
       return id(new AphrontRedirectResponse())->setURI($file->getInfoURI());
     }
     $file = id(new PhabricatorFileQuery())
       ->setViewer($user)
       ->withIDs(array($this->id))
       ->executeOne();
     if (!$file) {
       return new Aphront404Response();
     }
 
     $phid = $file->getPHID();
 
     $header = id(new PHUIHeaderView())
       ->setUser($user)
       ->setPolicyObject($file)
       ->setHeader($file->getName());
 
     $ttl = $file->getTTL();
     if ($ttl !== null) {
       $ttl_tag = id(new PHUITagView())
         ->setType(PHUITagView::TYPE_STATE)
         ->setBackgroundColor(PHUITagView::COLOR_YELLOW)
         ->setName(pht('Temporary'));
       $header->addTag($ttl_tag);
     }
 
     $partial = $file->getIsPartial();
     if ($partial) {
       $partial_tag = id(new PHUITagView())
         ->setType(PHUITagView::TYPE_STATE)
         ->setBackgroundColor(PHUITagView::COLOR_ORANGE)
         ->setName(pht('Partial Upload'));
       $header->addTag($partial_tag);
     }
 
     $actions = $this->buildActionView($file);
     $timeline = $this->buildTransactionView($file);
     $crumbs = $this->buildApplicationCrumbs();
     $crumbs->addTextCrumb(
       'F'.$file->getID(),
       $this->getApplicationURI("/info/{$phid}/"));
 
     $object_box = id(new PHUIObjectBoxView())
       ->setHeader($header);
 
     $this->buildPropertyViews($object_box, $file, $actions);
 
     return $this->buildApplicationPage(
       array(
         $crumbs,
         $object_box,
         $timeline,
       ),
       array(
         'title' => $file->getName(),
         'pageObjects' => array($file->getPHID()),
       ));
   }
 
   private function buildTransactionView(PhabricatorFile $file) {
     $user = $this->getRequest()->getUser();
 
     $timeline = $this->buildTransactionTimeline(
       $file,
       new PhabricatorFileTransactionQuery());
 
     $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
 
     $add_comment_header = $is_serious
       ? pht('Add Comment')
       : pht('Question File Integrity');
 
     $draft = PhabricatorDraft::newFromUserAndKey($user, $file->getPHID());
 
     $add_comment_form = id(new PhabricatorApplicationTransactionCommentView())
       ->setUser($user)
       ->setObjectPHID($file->getPHID())
       ->setDraft($draft)
       ->setHeaderText($add_comment_header)
       ->setAction($this->getApplicationURI('/comment/'.$file->getID().'/'))
       ->setSubmitButtonName(pht('Add Comment'));
 
     return array(
       $timeline,
       $add_comment_form,
     );
   }
 
   private function buildActionView(PhabricatorFile $file) {
     $request = $this->getRequest();
     $viewer = $request->getUser();
 
     $id = $file->getID();
 
     $can_edit = PhabricatorPolicyFilter::hasCapability(
       $viewer,
       $file,
       PhabricatorPolicyCapability::CAN_EDIT);
 
     $view = id(new PhabricatorActionListView())
       ->setUser($viewer)
       ->setObjectURI($this->getRequest()->getRequestURI())
       ->setObject($file);
 
     $can_download = !$file->getIsPartial();
 
     if ($file->isViewableInBrowser()) {
       $view->addAction(
         id(new PhabricatorActionView())
           ->setName(pht('View File'))
           ->setIcon('fa-file-o')
           ->setHref($file->getViewURI())
           ->setDisabled(!$can_download)
           ->setWorkflow(!$can_download));
     } else {
       $view->addAction(
         id(new PhabricatorActionView())
           ->setUser($viewer)
           ->setRenderAsForm($can_download)
           ->setDownload($can_download)
           ->setName(pht('Download File'))
           ->setIcon('fa-download')
           ->setHref($file->getViewURI())
           ->setDisabled(!$can_download)
           ->setWorkflow(!$can_download));
     }
 
     $view->addAction(
       id(new PhabricatorActionView())
         ->setName(pht('Edit File'))
         ->setIcon('fa-pencil')
         ->setHref($this->getApplicationURI("/edit/{$id}/"))
         ->setWorkflow(!$can_edit)
         ->setDisabled(!$can_edit));
 
     $view->addAction(
       id(new PhabricatorActionView())
         ->setName(pht('Delete File'))
         ->setIcon('fa-times')
         ->setHref($this->getApplicationURI("/delete/{$id}/"))
         ->setWorkflow(true)
         ->setDisabled(!$can_edit));
 
+    $view->addAction(
+      id(new PhabricatorActionView())
+        ->setName(pht('View Transforms'))
+        ->setIcon('fa-crop')
+        ->setHref($this->getApplicationURI("/transforms/{$id}/")));
+
     return $view;
   }
 
   private function buildPropertyViews(
     PHUIObjectBoxView $box,
     PhabricatorFile $file,
     PhabricatorActionListView $actions) {
     $request = $this->getRequest();
     $user = $request->getUser();
 
     $properties = id(new PHUIPropertyListView());
     $properties->setActionList($actions);
     $box->addPropertyList($properties, pht('Details'));
 
     if ($file->getAuthorPHID()) {
       $properties->addProperty(
         pht('Author'),
         $user->renderHandle($file->getAuthorPHID()));
     }
 
     $properties->addProperty(
       pht('Created'),
       phabricator_datetime($file->getDateCreated(), $user));
 
 
     $finfo = id(new PHUIPropertyListView());
     $box->addPropertyList($finfo, pht('File Info'));
 
     $finfo->addProperty(
       pht('Size'),
       phutil_format_bytes($file->getByteSize()));
 
     $finfo->addProperty(
       pht('Mime Type'),
       $file->getMimeType());
 
     $width = $file->getImageWidth();
     if ($width) {
       $finfo->addProperty(
         pht('Width'),
         pht('%s px', new PhutilNumber($width)));
     }
 
     $height = $file->getImageHeight();
     if ($height) {
       $finfo->addProperty(
         pht('Height'),
         pht('%s px', new PhutilNumber($height)));
     }
 
     $is_image = $file->isViewableImage();
     if ($is_image) {
       $image_string = pht('Yes');
       $cache_string = $file->getCanCDN() ? pht('Yes') : pht('No');
     } else {
       $image_string = pht('No');
       $cache_string = pht('Not Applicable');
     }
 
     $finfo->addProperty(pht('Viewable Image'), $image_string);
     $finfo->addProperty(pht('Cacheable'), $cache_string);
 
     $builtin = $file->getBuiltinName();
     if ($builtin === null) {
       $builtin_string = pht('No');
     } else {
       $builtin_string = $builtin;
     }
 
     $finfo->addProperty(pht('Builtin'), $builtin_string);
 
+    $is_profile = $file->getIsProfileImage()
+      ? pht('Yes')
+      : pht('No');
+
+    $finfo->addProperty(pht('Profile'), $is_profile);
+
     $storage_properties = new PHUIPropertyListView();
     $box->addPropertyList($storage_properties, pht('Storage'));
 
     $storage_properties->addProperty(
       pht('Engine'),
       $file->getStorageEngine());
 
     $storage_properties->addProperty(
       pht('Format'),
       $file->getStorageFormat());
 
     $storage_properties->addProperty(
       pht('Handle'),
       $file->getStorageHandle());
 
 
     $phids = $file->getObjectPHIDs();
     if ($phids) {
       $attached = new PHUIPropertyListView();
       $box->addPropertyList($attached, pht('Attached'));
 
       $attached->addProperty(
         pht('Attached To'),
         $user->renderHandleList($phids));
     }
 
-
     if ($file->isViewableImage()) {
       $image = phutil_tag(
         'img',
         array(
           'src' => $file->getViewURI(),
           'class' => 'phui-property-list-image',
         ));
 
       $linked_image = phutil_tag(
         'a',
         array(
           'href' => $file->getViewURI(),
         ),
         $image);
 
       $media = id(new PHUIPropertyListView())
         ->addImageContent($linked_image);
 
       $box->addPropertyList($media);
     } else if ($file->isAudio()) {
       $audio = phutil_tag(
         'audio',
         array(
           'controls' => 'controls',
           'class' => 'phui-property-list-audio',
         ),
         phutil_tag(
           'source',
           array(
             'src' => $file->getViewURI(),
             'type' => $file->getMimeType(),
           )));
       $media = id(new PHUIPropertyListView())
         ->addImageContent($audio);
 
       $box->addPropertyList($media);
     }
 
     $engine = null;
     try {
       $engine = $file->instantiateStorageEngine();
     } catch (Exception $ex) {
       // Don't bother raising this anywhere for now.
     }
 
     if ($engine) {
       if ($engine->isChunkEngine()) {
         $chunkinfo = new PHUIPropertyListView();
         $box->addPropertyList($chunkinfo, pht('Chunks'));
 
         $chunks = id(new PhabricatorFileChunkQuery())
           ->setViewer($user)
           ->withChunkHandles(array($file->getStorageHandle()))
           ->execute();
         $chunks = msort($chunks, 'getByteStart');
 
         $rows = array();
         $completed = array();
         foreach ($chunks as $chunk) {
           $is_complete = $chunk->getDataFilePHID();
 
           $rows[] = array(
             $chunk->getByteStart(),
             $chunk->getByteEnd(),
             ($is_complete ? pht('Yes') : pht('No')),
           );
 
           if ($is_complete) {
             $completed[] = $chunk;
           }
         }
 
         $table = id(new AphrontTableView($rows))
           ->setHeaders(
             array(
               pht('Offset'),
               pht('End'),
               pht('Complete'),
             ))
           ->setColumnClasses(
             array(
               '',
               '',
               'wide',
             ));
 
         $chunkinfo->addProperty(
           pht('Total Chunks'),
           count($chunks));
 
         $chunkinfo->addProperty(
           pht('Completed Chunks'),
           count($completed));
 
         $chunkinfo->addRawContent($table);
       }
     }
 
   }
 
 }
diff --git a/src/applications/files/controller/PhabricatorFileTransformController.php b/src/applications/files/controller/PhabricatorFileTransformController.php
index 248a6dddd2..5062fe76fc 100644
--- a/src/applications/files/controller/PhabricatorFileTransformController.php
+++ b/src/applications/files/controller/PhabricatorFileTransformController.php
@@ -1,165 +1,124 @@
 <?php
 
 final class PhabricatorFileTransformController
   extends PhabricatorFileController {
 
-  private $transform;
-  private $phid;
-  private $key;
-
   public function shouldRequireLogin() {
     return false;
   }
 
-  public function willProcessRequest(array $data) {
-    $this->transform = $data['transform'];
-    $this->phid      = $data['phid'];
-    $this->key       = $data['key'];
-  }
-
-  public function processRequest() {
-    $viewer = $this->getRequest()->getUser();
+  public function handleRequest(AphrontRequest $request) {
+    $viewer = $this->getViewer();
 
     // NOTE: This is a public/CDN endpoint, and permission to see files is
     // controlled by knowing the secret key, not by authentication.
 
+    $is_regenerate = $request->getBool('regenerate');
+
+    $source_phid = $request->getURIData('phid');
     $file = id(new PhabricatorFileQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
-      ->withPHIDs(array($this->phid))
+      ->withPHIDs(array($source_phid))
       ->executeOne();
     if (!$file) {
       return new Aphront404Response();
     }
 
-    if (!$file->validateSecretKey($this->key)) {
+    $secret_key = $request->getURIData('key');
+    if (!$file->validateSecretKey($secret_key)) {
       return new Aphront403Response();
     }
 
+    $transform = $request->getURIData('transform');
     $xform = id(new PhabricatorTransformedFile())
       ->loadOneWhere(
         'originalPHID = %s AND transform = %s',
-        $this->phid,
-        $this->transform);
+        $source_phid,
+        $transform);
 
     if ($xform) {
-      return $this->buildTransformedFileResponse($xform);
+      if ($is_regenerate) {
+        $this->destroyTransform($xform);
+      } else {
+        return $this->buildTransformedFileResponse($xform);
+      }
     }
 
-    $type = $file->getMimeType();
-
-    if (!$file->isViewableInBrowser() || !$file->isTransformableImage()) {
-      return $this->buildDefaultTransformation($file);
+    $xforms = PhabricatorFileTransform::getAllTransforms();
+    if (!isset($xforms[$transform])) {
+      return new Aphront404Response();
     }
 
+    $xform = $xforms[$transform];
+
     // We're essentially just building a cache here and don't need CSRF
     // protection.
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
 
-    switch ($this->transform) {
-      case 'thumb-profile':
-        $xformed_file = $this->executeThumbTransform($file, 50, 50);
-        break;
-      case 'thumb-280x210':
-        $xformed_file = $this->executeThumbTransform($file, 280, 210);
-        break;
-      case 'thumb-220x165':
-        $xformed_file = $this->executeThumbTransform($file, 220, 165);
-        break;
-      case 'preview-100':
-        $xformed_file = $this->executePreviewTransform($file, 100);
-        break;
-      case 'preview-220':
-        $xformed_file = $this->executePreviewTransform($file, 220);
-        break;
-      case 'thumb-160x120':
-        $xformed_file = $this->executeThumbTransform($file, 160, 120);
-        break;
-      case 'thumb-60x45':
-        $xformed_file = $this->executeThumbTransform($file, 60, 45);
-        break;
-      default:
-        return new Aphront400Response();
+    $xformed_file = null;
+    if ($xform->canApplyTransform($file)) {
+      try {
+        $xformed_file = $xforms[$transform]->applyTransform($file);
+      } catch (Exception $ex) {
+        // In normal transform mode, we ignore failures and generate a
+        // default transform below. If we're explicitly regenerating the
+        // thumbnail, rethrow the exception.
+        if ($is_regenerate) {
+          throw $ex;
+        }
+      }
     }
 
     if (!$xformed_file) {
-      return new Aphront400Response();
+      $xformed_file = $xform->getDefaultTransform($file);
     }
 
-    $xform = new PhabricatorTransformedFile();
-    $xform->setOriginalPHID($this->phid);
-    $xform->setTransform($this->transform);
-    $xform->setTransformedPHID($xformed_file->getPHID());
-    $xform->save();
-
-    return $this->buildTransformedFileResponse($xform);
-  }
-
-  private function buildDefaultTransformation(PhabricatorFile $file) {
-    static $regexps = array(
-      '@application/zip@'     => 'zip',
-      '@image/@'              => 'image',
-      '@application/pdf@'     => 'pdf',
-      '@.*@'                  => 'default',
-    );
-
-    $type = $file->getMimeType();
-    $prefix = 'default';
-    foreach ($regexps as $regexp => $implied_prefix) {
-      if (preg_match($regexp, $type)) {
-        $prefix = $implied_prefix;
-        break;
-      }
-    }
-
-    switch ($this->transform) {
-      case 'thumb-280x210':
-        $suffix = '280x210';
-        break;
-      case 'thumb-160x120':
-        $suffix = '160x120';
-        break;
-      case 'thumb-60x45':
-        $suffix = '60x45';
-        break;
-      case 'preview-100':
-        $suffix = '.p100';
-        break;
-      default:
-        throw new Exception('Unsupported transformation type!');
+    if (!$xformed_file) {
+      return new Aphront400Response();
     }
 
-    $path = celerity_get_resource_uri(
-      "rsrc/image/icon/fatcow/thumbnails/{$prefix}{$suffix}.png");
+    $xform = id(new PhabricatorTransformedFile())
+      ->setOriginalPHID($source_phid)
+      ->setTransform($transform)
+      ->setTransformedPHID($xformed_file->getPHID())
+      ->save();
 
-    return id(new AphrontRedirectResponse())
-      ->setURI($path);
+    return $this->buildTransformedFileResponse($xform);
   }
 
   private function buildTransformedFileResponse(
     PhabricatorTransformedFile $xform) {
 
     $file = id(new PhabricatorFileQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withPHIDs(array($xform->getTransformedPHID()))
       ->executeOne();
     if (!$file) {
       return new Aphront404Response();
     }
 
     // TODO: We could just delegate to the file view controller instead,
     // which would save the client a roundtrip, but is slightly more complex.
 
     return $file->getRedirectResponse();
   }
 
-  private function executePreviewTransform(PhabricatorFile $file, $size) {
-    $xformer = new PhabricatorImageTransformer();
-    return $xformer->executePreviewTransform($file, $size);
-  }
+  private function destroyTransform(PhabricatorTransformedFile $xform) {
+    $file = id(new PhabricatorFileQuery())
+      ->setViewer(PhabricatorUser::getOmnipotentUser())
+      ->withPHIDs(array($xform->getTransformedPHID()))
+      ->executeOne();
+
+    $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
+
+    if (!$file) {
+      $xform->delete();
+    } else {
+      $engine = new PhabricatorDestructionEngine();
+      $engine->destroyObject($file);
+    }
 
-  private function executeThumbTransform(PhabricatorFile $file, $x, $y) {
-    $xformer = new PhabricatorImageTransformer();
-    return $xformer->executeThumbTransform($file, $x, $y);
+    unset($unguarded);
   }
 
 }
diff --git a/src/applications/files/controller/PhabricatorFileTransformListController.php b/src/applications/files/controller/PhabricatorFileTransformListController.php
new file mode 100644
index 0000000000..6ef1818962
--- /dev/null
+++ b/src/applications/files/controller/PhabricatorFileTransformListController.php
@@ -0,0 +1,138 @@
+<?php
+
+final class PhabricatorFileTransformListController
+  extends PhabricatorFileController {
+
+  public function shouldAllowPublic() {
+    return true;
+  }
+
+  public function handleRequest(AphrontRequest $request) {
+    $viewer = $this->getViewer();
+
+    $file = id(new PhabricatorFileQuery())
+      ->setViewer($viewer)
+      ->withIDs(array($request->getURIData('id')))
+      ->executeOne();
+    if (!$file) {
+      return new Aphront404Response();
+    }
+
+    $monogram = $file->getMonogram();
+
+    $xdst = id(new PhabricatorTransformedFile())->loadAllWhere(
+      'transformedPHID = %s',
+      $file->getPHID());
+
+    $dst_rows = array();
+    foreach ($xdst as $source) {
+      $dst_rows[] = array(
+        $source->getTransform(),
+        $viewer->renderHandle($source->getOriginalPHID()),
+      );
+    }
+    $dst_table = id(new AphrontTableView($dst_rows))
+      ->setHeaders(
+        array(
+          pht('Key'),
+          pht('Source'),
+        ))
+      ->setColumnClasses(
+        array(
+          '',
+          'wide',
+        ))
+      ->setNoDataString(
+        pht(
+          'This file was not created by transforming another file.'));
+
+    $xsrc = id(new PhabricatorTransformedFile())->loadAllWhere(
+      'originalPHID = %s',
+      $file->getPHID());
+    $xsrc = mpull($xsrc, 'getTransformedPHID', 'getTransform');
+
+    $src_rows = array();
+    $xforms = PhabricatorFileTransform::getAllTransforms();
+    foreach ($xforms as $xform) {
+      $dst_phid = idx($xsrc, $xform->getTransformKey());
+
+      if ($xform->canApplyTransform($file)) {
+        $can_apply = pht('Yes');
+
+        $view_href = $file->getURIForTransform($xform);
+        $view_href = new PhutilURI($view_href);
+        $view_href->setQueryParam('regenerate', 'true');
+
+        $view_text = pht('Regenerate');
+
+        $view_link = phutil_tag(
+          'a',
+          array(
+            'class' => 'small grey button',
+            'href' => $view_href,
+          ),
+          $view_text);
+      } else {
+        $can_apply = phutil_tag('em', array(), pht('No'));
+        $view_link = phutil_tag('em', array(), pht('None'));
+      }
+
+      if ($dst_phid) {
+        $dst_link = $viewer->renderHandle($dst_phid);
+      } else {
+        $dst_link = phutil_tag('em', array(), pht('None'));
+      }
+
+      $src_rows[] = array(
+        $xform->getTransformName(),
+        $xform->getTransformKey(),
+        $can_apply,
+        $dst_link,
+        $view_link,
+      );
+    }
+
+    $src_table = id(new AphrontTableView($src_rows))
+      ->setHeaders(
+        array(
+          pht('Name'),
+          pht('Key'),
+          pht('Supported'),
+          pht('Transform'),
+          pht('View'),
+        ))
+      ->setColumnClasses(
+        array(
+          'wide',
+          '',
+          '',
+          '',
+          'action',
+        ));
+
+    $crumbs = $this->buildApplicationCrumbs();
+    $crumbs->addTextCrumb($monogram, '/'.$monogram);
+    $crumbs->addTextCrumb(pht('Transforms'));
+
+    $dst_box = id(new PHUIObjectBoxView())
+      ->setHeaderText(pht('File Sources'))
+      ->appendChild($dst_table);
+
+    $src_box = id(new PHUIObjectBoxView())
+      ->setHeaderText(pht('Available Transforms'))
+      ->appendChild($src_table);
+
+    return $this->buildApplicationPage(
+      array(
+        $crumbs,
+        $dst_box,
+        $src_box,
+      ),
+      array(
+        'title' => array(
+          pht('%s %s', $monogram, $file->getName()),
+          pht('Tranforms'),
+        ),
+      ));
+  }
+}
diff --git a/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php b/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php
index fe0bb1da7a..5ffb70be7f 100644
--- a/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php
+++ b/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php
@@ -1,228 +1,234 @@
 <?php
 
 final class PhabricatorEmbedFileRemarkupRule
   extends PhabricatorObjectRemarkupRule {
 
   const KEY_EMBED_FILE_PHIDS = 'phabricator.embedded-file-phids';
 
   protected function getObjectNamePrefix() {
     return 'F';
   }
 
   protected function loadObjects(array $ids) {
     $engine = $this->getEngine();
 
     $viewer = $engine->getConfig('viewer');
     $objects = id(new PhabricatorFileQuery())
       ->setViewer($viewer)
       ->withIDs($ids)
       ->execute();
 
     $phids_key = self::KEY_EMBED_FILE_PHIDS;
     $phids = $engine->getTextMetadata($phids_key, array());
     foreach (mpull($objects, 'getPHID') as $phid) {
       $phids[] = $phid;
     }
     $engine->setTextMetadata($phids_key, $phids);
 
     return $objects;
   }
 
   protected function renderObjectEmbed(
     $object,
     PhabricatorObjectHandle $handle,
     $options) {
 
     $options = $this->getFileOptions($options) + array(
       'name' => $object->getName(),
     );
 
     $is_viewable_image = $object->isViewableImage();
     $is_audio = $object->isAudio();
     $force_link = ($options['layout'] == 'link');
 
     $options['viewable'] = ($is_viewable_image || $is_audio);
 
     if ($is_viewable_image && !$force_link) {
       return $this->renderImageFile($object, $handle, $options);
     } else if ($is_audio && !$force_link) {
       return $this->renderAudioFile($object, $handle, $options);
     } else {
       return $this->renderFileLink($object, $handle, $options);
     }
   }
 
   private function getFileOptions($option_string) {
     $options = array(
       'size'    => null,
       'layout'  => 'left',
       'float'   => false,
       'width'   => null,
       'height'  => null,
       'alt' => null,
     );
 
     if ($option_string) {
       $option_string = trim($option_string, ', ');
       $parser = new PhutilSimpleOptions();
       $options = $parser->parse($option_string) + $options;
     }
 
     return $options;
   }
 
   private function renderImageFile(
     PhabricatorFile $file,
     PhabricatorObjectHandle $handle,
     array $options) {
 
     require_celerity_resource('lightbox-attachment-css');
 
     $attrs = array();
     $image_class = null;
 
     $use_size = true;
     if (!$options['size']) {
       $width = $this->parseDimension($options['width']);
       $height = $this->parseDimension($options['height']);
       if ($width || $height) {
         $use_size = false;
         $attrs += array(
           'src' => $file->getBestURI(),
           'width' => $width,
           'height' => $height,
         );
       }
     }
 
     if ($use_size) {
       switch ((string)$options['size']) {
         case 'full':
           $attrs += array(
             'src' => $file->getBestURI(),
             'height' => $file->getImageHeight(),
             'width' => $file->getImageWidth(),
           );
           $image_class = 'phabricator-remarkup-embed-image-full';
           break;
         case 'thumb':
         default:
-          $attrs['src'] = $file->getPreview220URI();
-          $dimensions =
-            PhabricatorImageTransformer::getPreviewDimensions($file, 220);
-          $attrs['width'] = $dimensions['sdx'];
-          $attrs['height'] = $dimensions['sdy'];
+          $preview_key = PhabricatorFileThumbnailTransform::TRANSFORM_PREVIEW;
+          $xform = PhabricatorFileTransform::getTransformByKey($preview_key);
+          $attrs['src'] = $file->getURIForTransform($xform);
+
+          $dimensions = $xform->getTransformedDimensions($file);
+          if ($dimensions) {
+            list($x, $y) = $dimensions;
+            $attrs['width'] = $x;
+            $attrs['height'] = $y;
+          }
+
           $image_class = 'phabricator-remarkup-embed-image';
           break;
       }
     }
 
     if (isset($options['alt'])) {
       $attrs['alt'] = $options['alt'];
     }
 
     $img = phutil_tag('img', $attrs);
 
     $embed = javelin_tag(
       'a',
       array(
         'href'        => $file->getBestURI(),
         'class'       => $image_class,
         'sigil'       => 'lightboxable',
         'meta'        => array(
           'phid' => $file->getPHID(),
           'uri' => $file->getBestURI(),
           'dUri' => $file->getDownloadURI(),
           'viewable' => true,
         ),
       ),
       $img);
 
     switch ($options['layout']) {
       case 'right':
       case 'center':
       case 'inline':
       case 'left':
         $layout_class = 'phabricator-remarkup-embed-layout-'.$options['layout'];
         break;
       default:
         $layout_class = 'phabricator-remarkup-embed-layout-left';
         break;
     }
 
     if ($options['float']) {
       switch ($options['layout']) {
         case 'center':
         case 'inline':
           break;
         case 'right':
           $layout_class .= ' phabricator-remarkup-embed-float-right';
           break;
         case 'left':
         default:
           $layout_class .= ' phabricator-remarkup-embed-float-left';
           break;
       }
     }
 
     return phutil_tag(
       ($options['layout'] == 'inline' ? 'span' : 'div'),
       array(
         'class' => $layout_class,
       ),
       $embed);
   }
 
   private function renderAudioFile(
     PhabricatorFile $file,
     PhabricatorObjectHandle $handle,
     array $options) {
 
     if (idx($options, 'autoplay')) {
       $preload = 'auto';
       $autoplay = 'autoplay';
     } else {
       $preload = 'none';
       $autoplay = null;
     }
 
     return $this->newTag(
       'audio',
       array(
         'controls' => 'controls',
         'preload' => $preload,
         'autoplay' => $autoplay,
         'loop' => idx($options, 'loop') ? 'loop' : null,
       ),
       $this->newTag(
         'source',
         array(
           'src' => $file->getBestURI(),
           'type' => $file->getMimeType(),
         )));
   }
 
   private function renderFileLink(
     PhabricatorFile $file,
     PhabricatorObjectHandle $handle,
     array $options) {
 
     return id(new PhabricatorFileLinkView())
       ->setFilePHID($file->getPHID())
       ->setFileName($this->assertFlatText($options['name']))
       ->setFileDownloadURI($file->getDownloadURI())
       ->setFileViewURI($file->getBestURI())
       ->setFileViewable((bool)$options['viewable']);
   }
 
   private function parseDimension($string) {
     $string = trim($string);
 
     if (preg_match('/^(?:\d*\\.)?\d+%?$/', $string)) {
       return $string;
     }
 
     return null;
   }
 
 }
diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php
index 5bc0eef0e6..f8e99bc965 100644
--- a/src/applications/files/storage/PhabricatorFile.php
+++ b/src/applications/files/storage/PhabricatorFile.php
@@ -1,1392 +1,1386 @@
 <?php
 
 /**
  * Parameters
  * ==========
  *
  * When creating a new file using a method like @{method:newFromFileData}, these
  * parameters are supported:
  *
  *   | name | Human readable filename.
  *   | authorPHID | User PHID of uploader.
  *   | ttl | Temporary file lifetime, in seconds.
  *   | viewPolicy | File visibility policy.
  *   | isExplicitUpload | Used to show users files they explicitly uploaded.
  *   | canCDN | Allows the file to be cached and delivered over a CDN.
  *   | mime-type | Optional, explicit file MIME type.
  *   | builtin | Optional filename, identifies this as a builtin.
  *
  */
 final class PhabricatorFile extends PhabricatorFileDAO
   implements
     PhabricatorApplicationTransactionInterface,
     PhabricatorTokenReceiverInterface,
     PhabricatorSubscribableInterface,
     PhabricatorFlaggableInterface,
     PhabricatorPolicyInterface,
     PhabricatorDestructibleInterface {
 
   const ONETIME_TEMPORARY_TOKEN_TYPE = 'file:onetime';
   const STORAGE_FORMAT_RAW  = 'raw';
 
   const METADATA_IMAGE_WIDTH  = 'width';
   const METADATA_IMAGE_HEIGHT = 'height';
   const METADATA_CAN_CDN = 'canCDN';
   const METADATA_BUILTIN = 'builtin';
   const METADATA_PARTIAL = 'partial';
+  const METADATA_PROFILE = 'profile';
 
   protected $name;
   protected $mimeType;
   protected $byteSize;
   protected $authorPHID;
   protected $secretKey;
   protected $contentHash;
   protected $metadata = array();
   protected $mailKey;
 
   protected $storageEngine;
   protected $storageFormat;
   protected $storageHandle;
 
   protected $ttl;
   protected $isExplicitUpload = 1;
   protected $viewPolicy = PhabricatorPolicies::POLICY_USER;
   protected $isPartial = 0;
 
   private $objects = self::ATTACHABLE;
   private $objectPHIDs = self::ATTACHABLE;
   private $originalFile = self::ATTACHABLE;
 
   public static function initializeNewFile() {
     $app = id(new PhabricatorApplicationQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withClasses(array('PhabricatorFilesApplication'))
       ->executeOne();
 
     $view_policy = $app->getPolicy(
       FilesDefaultViewCapability::CAPABILITY);
 
     return id(new PhabricatorFile())
       ->setViewPolicy($view_policy)
       ->setIsPartial(0)
       ->attachOriginalFile(null)
       ->attachObjects(array())
       ->attachObjectPHIDs(array());
   }
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_SERIALIZATION => array(
         'metadata' => self::SERIALIZATION_JSON,
       ),
       self::CONFIG_COLUMN_SCHEMA => array(
         'name' => 'text255?',
         'mimeType' => 'text255?',
         'byteSize' => 'uint64',
         'storageEngine' => 'text32',
         'storageFormat' => 'text32',
         'storageHandle' => 'text255',
         'authorPHID' => 'phid?',
         'secretKey' => 'bytes20?',
         'contentHash' => 'bytes40?',
         'ttl' => 'epoch?',
         'isExplicitUpload' => 'bool?',
         'mailKey' => 'bytes20',
         'isPartial' => 'bool',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_phid' => null,
         'phid' => array(
           'columns' => array('phid'),
           'unique' => true,
         ),
         'authorPHID' => array(
           'columns' => array('authorPHID'),
         ),
         'contentHash' => array(
           'columns' => array('contentHash'),
         ),
         'key_ttl' => array(
           'columns' => array('ttl'),
         ),
         'key_dateCreated' => array(
           'columns' => array('dateCreated'),
         ),
         'key_partial' => array(
           'columns' => array('authorPHID', 'isPartial'),
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       PhabricatorFileFilePHIDType::TYPECONST);
   }
 
   public function save() {
     if (!$this->getSecretKey()) {
       $this->setSecretKey($this->generateSecretKey());
     }
     if (!$this->getMailKey()) {
       $this->setMailKey(Filesystem::readRandomCharacters(20));
     }
     return parent::save();
   }
 
   public function getMonogram() {
     return 'F'.$this->getID();
   }
 
   public static function readUploadedFileData($spec) {
     if (!$spec) {
       throw new Exception('No file was uploaded!');
     }
 
     $err = idx($spec, 'error');
     if ($err) {
       throw new PhabricatorFileUploadException($err);
     }
 
     $tmp_name = idx($spec, 'tmp_name');
     $is_valid = @is_uploaded_file($tmp_name);
     if (!$is_valid) {
       throw new Exception('File is not an uploaded file.');
     }
 
     $file_data = Filesystem::readFile($tmp_name);
     $file_size = idx($spec, 'size');
 
     if (strlen($file_data) != $file_size) {
       throw new Exception('File size disagrees with uploaded size.');
     }
 
     return $file_data;
   }
 
   public static function newFromPHPUpload($spec, array $params = array()) {
     $file_data = self::readUploadedFileData($spec);
 
     $file_name = nonempty(
       idx($params, 'name'),
       idx($spec,   'name'));
     $params = array(
       'name' => $file_name,
     ) + $params;
 
     return self::newFromFileData($file_data, $params);
   }
 
   public static function newFromXHRUpload($data, array $params = array()) {
     return self::newFromFileData($data, $params);
   }
 
 
   /**
    * Given a block of data, try to load an existing file with the same content
    * if one exists. If it does not, build a new file.
    *
    * This method is generally used when we have some piece of semi-trusted data
    * like a diff or a file from a repository that we want to show to the user.
    * We can't just dump it out because it may be dangerous for any number of
    * reasons; instead, we need to serve it through the File abstraction so it
    * ends up on the CDN domain if one is configured and so on. However, if we
    * simply wrote a new file every time we'd potentially end up with a lot
    * of redundant data in file storage.
    *
    * To solve these problems, we use file storage as a cache and reuse the
    * same file again if we've previously written it.
    *
    * NOTE: This method unguards writes.
    *
    * @param string  Raw file data.
    * @param dict    Dictionary of file information.
    */
   public static function buildFromFileDataOrHash(
     $data,
     array $params = array()) {
 
     $file = id(new PhabricatorFile())->loadOneWhere(
       'name = %s AND contentHash = %s LIMIT 1',
       idx($params, 'name'),
       self::hashFileContent($data));
 
     if (!$file) {
       $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
-      $file = PhabricatorFile::newFromFileData($data, $params);
+      $file = self::newFromFileData($data, $params);
       unset($unguarded);
     }
 
     return $file;
   }
 
   public static function newFileFromContentHash($hash, array $params) {
     // Check to see if a file with same contentHash exist
     $file = id(new PhabricatorFile())->loadOneWhere(
       'contentHash = %s LIMIT 1',
       $hash);
 
     if ($file) {
       // copy storageEngine, storageHandle, storageFormat
       $copy_of_storage_engine = $file->getStorageEngine();
       $copy_of_storage_handle = $file->getStorageHandle();
       $copy_of_storage_format = $file->getStorageFormat();
       $copy_of_byte_size = $file->getByteSize();
       $copy_of_mime_type = $file->getMimeType();
 
-      $new_file = PhabricatorFile::initializeNewFile();
+      $new_file = self::initializeNewFile();
 
       $new_file->setByteSize($copy_of_byte_size);
 
       $new_file->setContentHash($hash);
       $new_file->setStorageEngine($copy_of_storage_engine);
       $new_file->setStorageHandle($copy_of_storage_handle);
       $new_file->setStorageFormat($copy_of_storage_format);
       $new_file->setMimeType($copy_of_mime_type);
       $new_file->copyDimensions($file);
 
       $new_file->readPropertiesFromParameters($params);
 
       $new_file->save();
 
       return $new_file;
     }
 
     return $file;
   }
 
   public static function newChunkedFile(
     PhabricatorFileStorageEngine $engine,
     $length,
     array $params) {
 
-    $file = PhabricatorFile::initializeNewFile();
+    $file = self::initializeNewFile();
 
     $file->setByteSize($length);
 
     // TODO: We might be able to test the first chunk in order to figure
     // this out more reliably, since MIME detection usually examines headers.
     // However, enormous files are probably always either actually raw data
     // or reasonable to treat like raw data.
     $file->setMimeType('application/octet-stream');
 
     $chunked_hash = idx($params, 'chunkedHash');
     if ($chunked_hash) {
       $file->setContentHash($chunked_hash);
     } else {
       // See PhabricatorChunkedFileStorageEngine::getChunkedHash() for some
       // discussion of this.
       $seed = Filesystem::readRandomBytes(64);
       $hash = PhabricatorChunkedFileStorageEngine::getChunkedHashForInput(
         $seed);
       $file->setContentHash($hash);
     }
 
     $file->setStorageEngine($engine->getEngineIdentifier());
     $file->setStorageHandle(PhabricatorFileChunk::newChunkHandle());
     $file->setStorageFormat(self::STORAGE_FORMAT_RAW);
     $file->setIsPartial(1);
 
     $file->readPropertiesFromParameters($params);
 
     return $file;
   }
 
   private static function buildFromFileData($data, array $params = array()) {
 
     if (isset($params['storageEngines'])) {
       $engines = $params['storageEngines'];
     } else {
       $size = strlen($data);
       $engines = PhabricatorFileStorageEngine::loadStorageEngines($size);
 
       if (!$engines) {
         throw new Exception(
           pht(
             'No configured storage engine can store this file. See '.
             '"Configuring File Storage" in the documentation for '.
             'information on configuring storage engines.'));
       }
     }
 
     assert_instances_of($engines, 'PhabricatorFileStorageEngine');
     if (!$engines) {
       throw new Exception(pht('No valid storage engines are available!'));
     }
 
-    $file = PhabricatorFile::initializeNewFile();
+    $file = self::initializeNewFile();
 
     $data_handle = null;
     $engine_identifier = null;
     $exceptions = array();
     foreach ($engines as $engine) {
       $engine_class = get_class($engine);
       try {
         list($engine_identifier, $data_handle) = $file->writeToEngine(
           $engine,
           $data,
           $params);
 
         // We stored the file somewhere so stop trying to write it to other
         // places.
         break;
       } catch (PhabricatorFileStorageConfigurationException $ex) {
         // If an engine is outright misconfigured (or misimplemented), raise
         // that immediately since it probably needs attention.
         throw $ex;
       } catch (Exception $ex) {
         phlog($ex);
 
         // If an engine doesn't work, keep trying all the other valid engines
         // in case something else works.
         $exceptions[$engine_class] = $ex;
       }
     }
 
     if (!$data_handle) {
       throw new PhutilAggregateException(
         'All storage engines failed to write file:',
         $exceptions);
     }
 
     $file->setByteSize(strlen($data));
     $file->setContentHash(self::hashFileContent($data));
 
     $file->setStorageEngine($engine_identifier);
     $file->setStorageHandle($data_handle);
 
     // TODO: This is probably YAGNI, but allows for us to do encryption or
     // compression later if we want.
     $file->setStorageFormat(self::STORAGE_FORMAT_RAW);
 
     $file->readPropertiesFromParameters($params);
 
     if (!$file->getMimeType()) {
       $tmp = new TempFile();
       Filesystem::writeFile($tmp, $data);
       $file->setMimeType(Filesystem::getMimeType($tmp));
     }
 
     try {
       $file->updateDimensions(false);
     } catch (Exception $ex) {
       // Do nothing
     }
 
     $file->save();
 
     return $file;
   }
 
   public static function newFromFileData($data, array $params = array()) {
     $hash = self::hashFileContent($data);
     $file = self::newFileFromContentHash($hash, $params);
 
     if ($file) {
       return $file;
     }
 
     return self::buildFromFileData($data, $params);
   }
 
   public function migrateToEngine(PhabricatorFileStorageEngine $engine) {
     if (!$this->getID() || !$this->getStorageHandle()) {
       throw new Exception(
         "You can not migrate a file which hasn't yet been saved.");
     }
 
     $data = $this->loadFileData();
     $params = array(
       'name' => $this->getName(),
     );
 
     list($new_identifier, $new_handle) = $this->writeToEngine(
       $engine,
       $data,
       $params);
 
     $old_engine = $this->instantiateStorageEngine();
     $old_identifier = $this->getStorageEngine();
     $old_handle = $this->getStorageHandle();
 
     $this->setStorageEngine($new_identifier);
     $this->setStorageHandle($new_handle);
     $this->save();
 
     $this->deleteFileDataIfUnused(
       $old_engine,
       $old_identifier,
       $old_handle);
 
     return $this;
   }
 
   private function writeToEngine(
     PhabricatorFileStorageEngine $engine,
     $data,
     array $params) {
 
     $engine_class = get_class($engine);
 
     $data_handle = $engine->writeFile($data, $params);
 
     if (!$data_handle || strlen($data_handle) > 255) {
       // This indicates an improperly implemented storage engine.
       throw new PhabricatorFileStorageConfigurationException(
         "Storage engine '{$engine_class}' executed writeFile() but did ".
         "not return a valid handle ('{$data_handle}') to the data: it ".
         "must be nonempty and no longer than 255 characters.");
     }
 
     $engine_identifier = $engine->getEngineIdentifier();
     if (!$engine_identifier || strlen($engine_identifier) > 32) {
       throw new PhabricatorFileStorageConfigurationException(
         "Storage engine '{$engine_class}' returned an improper engine ".
         "identifier '{$engine_identifier}': it must be nonempty ".
         "and no longer than 32 characters.");
     }
 
     return array($engine_identifier, $data_handle);
   }
 
 
   /**
    * Download a remote resource over HTTP and save the response body as a file.
    *
    * This method respects `security.outbound-blacklist`, and protects against
    * HTTP redirection (by manually following "Location" headers and verifying
    * each destination). It does not protect against DNS rebinding. See
    * discussion in T6755.
    */
   public static function newFromFileDownload($uri, array $params = array()) {
     $timeout = 5;
 
     $redirects = array();
     $current = $uri;
     while (true) {
       try {
         if (count($redirects) > 10) {
           throw new Exception(
             pht('Too many redirects trying to fetch remote URI.'));
         }
 
         $resolved = PhabricatorEnv::requireValidRemoteURIForFetch(
           $current,
           array(
             'http',
             'https',
           ));
 
         list($resolved_uri, $resolved_domain) = $resolved;
 
         $current = new PhutilURI($current);
         if ($current->getProtocol() == 'http') {
           // For HTTP, we can use a pre-resolved URI to defuse DNS rebinding.
           $fetch_uri = $resolved_uri;
           $fetch_host = $resolved_domain;
         } else {
           // For HTTPS, we can't: cURL won't verify the SSL certificate if
           // the domain has been replaced with an IP. But internal services
           // presumably will not have valid certificates for rebindable
           // domain names on attacker-controlled domains, so the DNS rebinding
           // attack should generally not be possible anyway.
           $fetch_uri = $current;
           $fetch_host = null;
         }
 
         $future = id(new HTTPSFuture($fetch_uri))
           ->setFollowLocation(false)
           ->setTimeout($timeout);
 
         if ($fetch_host !== null) {
           $future->addHeader('Host', $fetch_host);
         }
 
         list($status, $body, $headers) = $future->resolve();
 
         if ($status->isRedirect()) {
           // This is an HTTP 3XX status, so look for a "Location" header.
           $location = null;
           foreach ($headers as $header) {
             list($name, $value) = $header;
             if (phutil_utf8_strtolower($name) == 'location') {
               $location = $value;
               break;
             }
           }
 
           // HTTP 3XX status with no "Location" header, just treat this like
           // a normal HTTP error.
           if ($location === null) {
             throw $status;
           }
 
           if (isset($redirects[$location])) {
             throw new Exception(
               pht(
                 'Encountered loop while following redirects.'));
           }
 
           $redirects[$location] = $location;
           $current = $location;
           // We'll fall off the bottom and go try this URI now.
         } else if ($status->isError()) {
           // This is something other than an HTTP 2XX or HTTP 3XX status, so
           // just bail out.
           throw $status;
         } else {
           // This is HTTP 2XX, so use the the response body to save the
           // file data.
           $params = $params + array(
             'name' => basename($uri),
           );
 
           return self::newFromFileData($body, $params);
         }
       } catch (Exception $ex) {
         if ($redirects) {
           throw new PhutilProxyException(
             pht(
               'Failed to fetch remote URI "%s" after following %s redirect(s) '.
               '(%s): %s',
               $uri,
               new PhutilNumber(count($redirects)),
               implode(' > ', array_keys($redirects)),
               $ex->getMessage()),
             $ex);
         } else {
           throw $ex;
         }
       }
     }
   }
 
   public static function normalizeFileName($file_name) {
     $pattern = "@[\\x00-\\x19#%&+!~'\$\"\/=\\\\?<> ]+@";
     $file_name = preg_replace($pattern, '_', $file_name);
     $file_name = preg_replace('@_+@', '_', $file_name);
     $file_name = trim($file_name, '_');
 
     $disallowed_filenames = array(
       '.'  => 'dot',
       '..' => 'dotdot',
       ''   => 'file',
     );
     $file_name = idx($disallowed_filenames, $file_name, $file_name);
 
     return $file_name;
   }
 
   public function delete() {
     // We want to delete all the rows which mark this file as the transformation
     // of some other file (since we're getting rid of it). We also delete all
     // the transformations of this file, so that a user who deletes an image
     // doesn't need to separately hunt down and delete a bunch of thumbnails and
     // resizes of it.
 
     $outbound_xforms = id(new PhabricatorFileQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withTransforms(
         array(
           array(
             'originalPHID' => $this->getPHID(),
             'transform'    => true,
           ),
         ))
       ->execute();
 
     foreach ($outbound_xforms as $outbound_xform) {
       $outbound_xform->delete();
     }
 
     $inbound_xforms = id(new PhabricatorTransformedFile())->loadAllWhere(
       'transformedPHID = %s',
       $this->getPHID());
 
     $this->openTransaction();
       foreach ($inbound_xforms as $inbound_xform) {
         $inbound_xform->delete();
       }
       $ret = parent::delete();
     $this->saveTransaction();
 
     $this->deleteFileDataIfUnused(
       $this->instantiateStorageEngine(),
       $this->getStorageEngine(),
       $this->getStorageHandle());
 
     return $ret;
   }
 
 
   /**
    * Destroy stored file data if there are no remaining files which reference
    * it.
    */
   public function deleteFileDataIfUnused(
     PhabricatorFileStorageEngine $engine,
     $engine_identifier,
     $handle) {
 
     // Check to see if any files are using storage.
     $usage = id(new PhabricatorFile())->loadAllWhere(
       'storageEngine = %s AND storageHandle = %s LIMIT 1',
       $engine_identifier,
       $handle);
 
     // If there are no files using the storage, destroy the actual storage.
     if (!$usage) {
       try {
         $engine->deleteFile($handle);
       } catch (Exception $ex) {
         // In the worst case, we're leaving some data stranded in a storage
         // engine, which is not a big deal.
         phlog($ex);
       }
     }
   }
 
 
   public static function hashFileContent($data) {
     return sha1($data);
   }
 
   public function loadFileData() {
 
     $engine = $this->instantiateStorageEngine();
     $data = $engine->readFile($this->getStorageHandle());
 
     switch ($this->getStorageFormat()) {
       case self::STORAGE_FORMAT_RAW:
         $data = $data;
         break;
       default:
         throw new Exception('Unknown storage format.');
     }
 
     return $data;
   }
 
 
   /**
    * Return an iterable which emits file content bytes.
    *
    * @param int Offset for the start of data.
    * @param int Offset for the end of data.
    * @return Iterable Iterable object which emits requested data.
    */
   public function getFileDataIterator($begin = null, $end = null) {
     $engine = $this->instantiateStorageEngine();
     return $engine->getFileDataIterator($this, $begin, $end);
   }
 
 
   public function getViewURI() {
     if (!$this->getPHID()) {
       throw new Exception(
         'You must save a file before you can generate a view URI.');
     }
 
     return $this->getCDNURI(null);
   }
 
   private function getCDNURI($token) {
     $name = self::normalizeFileName($this->getName());
     $name = phutil_escape_uri($name);
 
     $parts = array();
     $parts[] = 'file';
     $parts[] = 'data';
 
     // If this is an instanced install, add the instance identifier to the URI.
     // Instanced configurations behind a CDN may not be able to control the
     // request domain used by the CDN (as with AWS CloudFront). Embedding the
     // instance identity in the path allows us to distinguish between requests
     // originating from different instances but served through the same CDN.
     $instance = PhabricatorEnv::getEnvConfig('cluster.instance');
     if (strlen($instance)) {
       $parts[] = '@'.$instance;
     }
 
     $parts[] = $this->getSecretKey();
     $parts[] = $this->getPHID();
     if ($token) {
       $parts[] = $token;
     }
     $parts[] = $name;
 
     $path = '/'.implode('/', $parts);
 
     // If this file is only partially uploaded, we're just going to return a
     // local URI to make sure that Ajax works, since the page is inevitably
     // going to give us an error back.
     if ($this->getIsPartial()) {
       return PhabricatorEnv::getURI($path);
     } else {
       return PhabricatorEnv::getCDNURI($path);
     }
   }
 
   /**
    * Get the CDN URI for this file, including a one-time-use security token.
    *
    */
   public function getCDNURIWithToken() {
     if (!$this->getPHID()) {
       throw new Exception(
         'You must save a file before you can generate a CDN URI.');
     }
 
     return $this->getCDNURI($this->generateOneTimeToken());
   }
 
 
   public function getInfoURI() {
     return '/'.$this->getMonogram();
   }
 
   public function getBestURI() {
     if ($this->isViewableInBrowser()) {
       return $this->getViewURI();
     } else {
       return $this->getInfoURI();
     }
   }
 
   public function getDownloadURI() {
     $uri = id(new PhutilURI($this->getViewURI()))
       ->setQueryParam('download', true);
     return (string) $uri;
   }
 
+  public function getURIForTransform(PhabricatorFileTransform $transform) {
+    return $this->getTransformedURI($transform->getTransformKey());
+  }
+
   private function getTransformedURI($transform) {
     $parts = array();
     $parts[] = 'file';
     $parts[] = 'xform';
 
     $instance = PhabricatorEnv::getEnvConfig('cluster.instance');
     if (strlen($instance)) {
       $parts[] = '@'.$instance;
     }
 
     $parts[] = $transform;
     $parts[] = $this->getPHID();
     $parts[] = $this->getSecretKey();
 
     $path = implode('/', $parts);
     $path = $path.'/';
 
     return PhabricatorEnv::getCDNURI($path);
   }
 
-  public function getProfileThumbURI() {
-    return $this->getTransformedURI('thumb-profile');
-  }
-
-  public function getThumb60x45URI() {
-    return $this->getTransformedURI('thumb-60x45');
-  }
-
-  public function getThumb160x120URI() {
-    return $this->getTransformedURI('thumb-160x120');
-  }
-
-  public function getPreview100URI() {
-    return $this->getTransformedURI('preview-100');
-  }
-
-  public function getPreview220URI() {
-    return $this->getTransformedURI('preview-220');
-  }
-
-  public function getThumb220x165URI() {
-    return $this->getTransfomredURI('thumb-220x165');
-  }
-
-  public function getThumb280x210URI() {
-    return $this->getTransformedURI('thumb-280x210');
-  }
-
   public function isViewableInBrowser() {
     return ($this->getViewableMimeType() !== null);
   }
 
   public function isViewableImage() {
     if (!$this->isViewableInBrowser()) {
       return false;
     }
 
     $mime_map = PhabricatorEnv::getEnvConfig('files.image-mime-types');
     $mime_type = $this->getMimeType();
     return idx($mime_map, $mime_type);
   }
 
   public function isAudio() {
     if (!$this->isViewableInBrowser()) {
       return false;
     }
 
     $mime_map = PhabricatorEnv::getEnvConfig('files.audio-mime-types');
     $mime_type = $this->getMimeType();
     return idx($mime_map, $mime_type);
   }
 
   public function isTransformableImage() {
     // NOTE: The way the 'gd' extension works in PHP is that you can install it
     // with support for only some file types, so it might be able to handle
     // PNG but not JPEG. Try to generate thumbnails for whatever we can. Setup
     // warns you if you don't have complete support.
 
     $matches = null;
     $ok = preg_match(
       '@^image/(gif|png|jpe?g)@',
       $this->getViewableMimeType(),
       $matches);
     if (!$ok) {
       return false;
     }
 
     switch ($matches[1]) {
       case 'jpg';
       case 'jpeg':
         return function_exists('imagejpeg');
         break;
       case 'png':
         return function_exists('imagepng');
         break;
       case 'gif':
         return function_exists('imagegif');
         break;
       default:
         throw new Exception('Unknown type matched as image MIME type.');
     }
   }
 
   public static function getTransformableImageFormats() {
     $supported = array();
 
     if (function_exists('imagejpeg')) {
       $supported[] = 'jpg';
     }
 
     if (function_exists('imagepng')) {
       $supported[] = 'png';
     }
 
     if (function_exists('imagegif')) {
       $supported[] = 'gif';
     }
 
     return $supported;
   }
 
   public function instantiateStorageEngine() {
     return self::buildEngine($this->getStorageEngine());
   }
 
   public static function buildEngine($engine_identifier) {
     $engines = self::buildAllEngines();
     foreach ($engines as $engine) {
       if ($engine->getEngineIdentifier() == $engine_identifier) {
         return $engine;
       }
     }
 
     throw new Exception(
       "Storage engine '{$engine_identifier}' could not be located!");
   }
 
   public static function buildAllEngines() {
     $engines = id(new PhutilSymbolLoader())
       ->setType('class')
       ->setConcreteOnly(true)
       ->setAncestorClass('PhabricatorFileStorageEngine')
       ->selectAndLoadSymbols();
 
     $results = array();
     foreach ($engines as $engine_class) {
       $results[] = newv($engine_class['name'], array());
     }
 
     return $results;
   }
 
   public function getViewableMimeType() {
     $mime_map = PhabricatorEnv::getEnvConfig('files.viewable-mime-types');
 
     $mime_type = $this->getMimeType();
     $mime_parts = explode(';', $mime_type);
     $mime_type = trim(reset($mime_parts));
 
     return idx($mime_map, $mime_type);
   }
 
   public function getDisplayIconForMimeType() {
     $mime_map = PhabricatorEnv::getEnvConfig('files.icon-mime-types');
     $mime_type = $this->getMimeType();
     return idx($mime_map, $mime_type, 'fa-file-o');
   }
 
   public function validateSecretKey($key) {
     return ($key == $this->getSecretKey());
   }
 
   public function generateSecretKey() {
     return Filesystem::readRandomCharacters(20);
   }
 
   public function updateDimensions($save = true) {
     if (!$this->isViewableImage()) {
       throw new Exception(
         'This file is not a viewable image.');
     }
 
     if (!function_exists('imagecreatefromstring')) {
       throw new Exception(
         'Cannot retrieve image information.');
     }
 
     $data = $this->loadFileData();
 
     $img = imagecreatefromstring($data);
     if ($img === false) {
       throw new Exception(
         'Error when decoding image.');
     }
 
     $this->metadata[self::METADATA_IMAGE_WIDTH] = imagesx($img);
     $this->metadata[self::METADATA_IMAGE_HEIGHT] = imagesy($img);
 
     if ($save) {
       $this->save();
     }
 
     return $this;
   }
 
   public function copyDimensions(PhabricatorFile $file) {
     $metadata = $file->getMetadata();
     $width = idx($metadata, self::METADATA_IMAGE_WIDTH);
     if ($width) {
       $this->metadata[self::METADATA_IMAGE_WIDTH] = $width;
     }
     $height = idx($metadata, self::METADATA_IMAGE_HEIGHT);
     if ($height) {
       $this->metadata[self::METADATA_IMAGE_HEIGHT] = $height;
     }
 
     return $this;
   }
 
 
   /**
    * Load (or build) the {@class:PhabricatorFile} objects for builtin file
    * resources. The builtin mechanism allows files shipped with Phabricator
    * to be treated like normal files so that APIs do not need to special case
    * things like default images or deleted files.
    *
    * Builtins are located in `resources/builtin/` and identified by their
    * name.
    *
    * @param  PhabricatorUser                Viewing user.
    * @param  list<string>                   List of builtin file names.
    * @return dict<string, PhabricatorFile>  Dictionary of named builtins.
    */
   public static function loadBuiltins(PhabricatorUser $user, array $names) {
     $specs = array();
     foreach ($names as $name) {
       $specs[] = array(
         'originalPHID' => PhabricatorPHIDConstants::PHID_VOID,
         'transform'    => 'builtin:'.$name,
       );
     }
 
     // NOTE: Anyone is allowed to access builtin files.
 
     $files = id(new PhabricatorFileQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withTransforms($specs)
       ->execute();
 
     $files = mpull($files, null, 'getName');
 
     $root = dirname(phutil_get_library_root('phabricator'));
     $root = $root.'/resources/builtin/';
 
     $build = array();
     foreach ($names as $name) {
       if (isset($files[$name])) {
         continue;
       }
 
       // This is just a sanity check to prevent loading arbitrary files.
       if (basename($name) != $name) {
         throw new Exception("Invalid builtin name '{$name}'!");
       }
 
       $path = $root.$name;
 
       if (!Filesystem::pathExists($path)) {
         throw new Exception("Builtin '{$path}' does not exist!");
       }
 
       $data = Filesystem::readFile($path);
       $params = array(
         'name' => $name,
         'ttl'  => time() + (60 * 60 * 24 * 7),
         'canCDN' => true,
         'builtin' => $name,
       );
 
       $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
-        $file = PhabricatorFile::newFromFileData($data, $params);
+        $file = self::newFromFileData($data, $params);
         $xform = id(new PhabricatorTransformedFile())
           ->setOriginalPHID(PhabricatorPHIDConstants::PHID_VOID)
           ->setTransform('builtin:'.$name)
           ->setTransformedPHID($file->getPHID())
           ->save();
       unset($unguarded);
 
       $file->attachObjectPHIDs(array());
       $file->attachObjects(array());
 
       $files[$name] = $file;
     }
 
     return $files;
   }
 
 
   /**
    * Convenience wrapper for @{method:loadBuiltins}.
    *
    * @param PhabricatorUser   Viewing user.
    * @param string            Single builtin name to load.
    * @return PhabricatorFile  Corresponding builtin file.
    */
   public static function loadBuiltin(PhabricatorUser $user, $name) {
     return idx(self::loadBuiltins($user, array($name)), $name);
   }
 
   public function getObjects() {
     return $this->assertAttached($this->objects);
   }
 
   public function attachObjects(array $objects) {
     $this->objects = $objects;
     return $this;
   }
 
   public function getObjectPHIDs() {
     return $this->assertAttached($this->objectPHIDs);
   }
 
   public function attachObjectPHIDs(array $object_phids) {
     $this->objectPHIDs = $object_phids;
     return $this;
   }
 
   public function getOriginalFile() {
     return $this->assertAttached($this->originalFile);
   }
 
   public function attachOriginalFile(PhabricatorFile $file = null) {
     $this->originalFile = $file;
     return $this;
   }
 
   public function getImageHeight() {
     if (!$this->isViewableImage()) {
       return null;
     }
     return idx($this->metadata, self::METADATA_IMAGE_HEIGHT);
   }
 
   public function getImageWidth() {
     if (!$this->isViewableImage()) {
       return null;
     }
     return idx($this->metadata, self::METADATA_IMAGE_WIDTH);
   }
 
   public function getCanCDN() {
     if (!$this->isViewableImage()) {
       return false;
     }
 
     return idx($this->metadata, self::METADATA_CAN_CDN);
   }
 
   public function setCanCDN($can_cdn) {
     $this->metadata[self::METADATA_CAN_CDN] = $can_cdn ? 1 : 0;
     return $this;
   }
 
   public function isBuiltin() {
     return ($this->getBuiltinName() !== null);
   }
 
   public function getBuiltinName() {
     return idx($this->metadata, self::METADATA_BUILTIN);
   }
 
   public function setBuiltinName($name) {
     $this->metadata[self::METADATA_BUILTIN] = $name;
     return $this;
   }
 
+  public function getIsProfileImage() {
+    return idx($this->metadata, self::METADATA_PROFILE);
+  }
+
+  public function setIsProfileImage($value) {
+    $this->metadata[self::METADATA_PROFILE] = $value;
+    return $this;
+  }
+
   protected function generateOneTimeToken() {
     $key = Filesystem::readRandomCharacters(16);
 
     // Save the new secret.
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
       $token = id(new PhabricatorAuthTemporaryToken())
         ->setObjectPHID($this->getPHID())
         ->setTokenType(self::ONETIME_TEMPORARY_TOKEN_TYPE)
         ->setTokenExpires(time() + phutil_units('1 hour in seconds'))
         ->setTokenCode(PhabricatorHash::digest($key))
         ->save();
     unset($unguarded);
 
     return $key;
   }
 
   public function validateOneTimeToken($token_code) {
     $token = id(new PhabricatorAuthTemporaryTokenQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withObjectPHIDs(array($this->getPHID()))
       ->withTokenTypes(array(self::ONETIME_TEMPORARY_TOKEN_TYPE))
       ->withExpired(false)
       ->withTokenCodes(array(PhabricatorHash::digest($token_code)))
       ->executeOne();
 
     return $token;
   }
 
 
   /**
    * Write the policy edge between this file and some object.
    *
    * @param phid Object PHID to attach to.
    * @return this
    */
   public function attachToObject($phid) {
     $edge_type = PhabricatorObjectHasFileEdgeType::EDGECONST;
 
     id(new PhabricatorEdgeEditor())
       ->addEdge($phid, $edge_type, $this->getPHID())
       ->save();
 
     return $this;
   }
 
 
   /**
    * Remove the policy edge between this file and some object.
    *
    * @param phid Object PHID to detach from.
    * @return this
    */
   public function detachFromObject($phid) {
     $edge_type = PhabricatorObjectHasFileEdgeType::EDGECONST;
 
     id(new PhabricatorEdgeEditor())
       ->removeEdge($phid, $edge_type, $this->getPHID())
       ->save();
 
     return $this;
   }
 
 
   /**
    * Configure a newly created file object according to specified parameters.
    *
    * This method is called both when creating a file from fresh data, and
    * when creating a new file which reuses existing storage.
    *
    * @param map<string, wild>   Bag of parameters, see @{class:PhabricatorFile}
    *  for documentation.
    * @return this
    */
   private function readPropertiesFromParameters(array $params) {
     $file_name = idx($params, 'name');
     $this->setName($file_name);
 
     $author_phid = idx($params, 'authorPHID');
     $this->setAuthorPHID($author_phid);
 
     $file_ttl = idx($params, 'ttl');
     $this->setTtl($file_ttl);
 
     $view_policy = idx($params, 'viewPolicy');
     if ($view_policy) {
       $this->setViewPolicy($params['viewPolicy']);
     }
 
     $is_explicit = (idx($params, 'isExplicitUpload') ? 1 : 0);
     $this->setIsExplicitUpload($is_explicit);
 
     $can_cdn = idx($params, 'canCDN');
     if ($can_cdn) {
       $this->setCanCDN(true);
     }
 
     $builtin = idx($params, 'builtin');
     if ($builtin) {
       $this->setBuiltinName($builtin);
     }
 
+    $profile = idx($params, 'profile');
+    if ($profile) {
+      $this->setIsProfileImage(true);
+    }
+
     $mime_type = idx($params, 'mime-type');
     if ($mime_type) {
       $this->setMimeType($mime_type);
     }
 
     return $this;
   }
 
   public function getRedirectResponse() {
     $uri = $this->getBestURI();
 
     // TODO: This is a bit iffy. Sometimes, getBestURI() returns a CDN URI
     // (if the file is a viewable image) and sometimes a local URI (if not).
     // For now, just detect which one we got and configure the response
     // appropriately. In the long run, if this endpoint is served from a CDN
     // domain, we can't issue a local redirect to an info URI (which is not
     // present on the CDN domain). We probably never actually issue local
     // redirects here anyway, since we only ever transform viewable images
     // right now.
 
     $is_external = strlen(id(new PhutilURI($uri))->getDomain());
 
     return id(new AphrontRedirectResponse())
       ->setIsExternal($is_external)
       ->setURI($uri);
   }
 
 
 /* -(  PhabricatorApplicationTransactionInterface  )------------------------- */
 
 
   public function getApplicationTransactionEditor() {
     return new PhabricatorFileEditor();
   }
 
   public function getApplicationTransactionObject() {
     return $this;
   }
 
   public function getApplicationTransactionTemplate() {
     return new PhabricatorFileTransaction();
   }
 
   public function willRenderTimeline(
     PhabricatorApplicationTransactionView $timeline,
     AphrontRequest $request) {
 
     return $timeline;
   }
 
 
 /* -(  PhabricatorPolicyInterface Implementation  )-------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
       PhabricatorPolicyCapability::CAN_EDIT,
     );
   }
 
   public function getPolicy($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         if ($this->isBuiltin()) {
           return PhabricatorPolicies::getMostOpenPolicy();
         }
+        if ($this->getIsProfileImage()) {
+          return PhabricatorPolicies::getMostOpenPolicy();
+        }
         return $this->getViewPolicy();
       case PhabricatorPolicyCapability::CAN_EDIT:
         return PhabricatorPolicies::POLICY_NOONE;
     }
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     $viewer_phid = $viewer->getPHID();
     if ($viewer_phid) {
       if ($this->getAuthorPHID() == $viewer_phid) {
         return true;
       }
     }
 
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         // If you can see the file this file is a transform of, you can see
         // this file.
         if ($this->getOriginalFile()) {
           return true;
         }
 
         // If you can see any object this file is attached to, you can see
         // the file.
         return (count($this->getObjects()) > 0);
     }
 
     return false;
   }
 
   public function describeAutomaticCapability($capability) {
     $out = array();
     $out[] = pht('The user who uploaded a file can always view and edit it.');
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         $out[] = pht(
           'Files attached to objects are visible to users who can view '.
           'those objects.');
         $out[] = pht(
           'Thumbnails are visible only to users who can view the original '.
           'file.');
         break;
     }
 
     return $out;
   }
 
 
 /* -(  PhabricatorSubscribableInterface Implementation  )-------------------- */
 
 
   public function isAutomaticallySubscribed($phid) {
     return ($this->authorPHID == $phid);
   }
 
   public function shouldShowSubscribersProperty() {
     return true;
   }
 
   public function shouldAllowSubscription($phid) {
     return true;
   }
 
 
 /* -(  PhabricatorTokenReceiverInterface  )---------------------------------- */
 
 
   public function getUsersToNotifyOfTokenGiven() {
     return array(
       $this->getAuthorPHID(),
     );
   }
 
 
 /* -(  PhabricatorDestructibleInterface  )----------------------------------- */
 
 
   public function destroyObjectPermanently(
     PhabricatorDestructionEngine $engine) {
 
     $this->openTransaction();
       $this->delete();
     $this->saveTransaction();
   }
 
 }
diff --git a/src/applications/files/transform/PhabricatorFileImageTransform.php b/src/applications/files/transform/PhabricatorFileImageTransform.php
new file mode 100644
index 0000000000..ff8bd14683
--- /dev/null
+++ b/src/applications/files/transform/PhabricatorFileImageTransform.php
@@ -0,0 +1,382 @@
+<?php
+
+abstract class PhabricatorFileImageTransform extends PhabricatorFileTransform {
+
+  private $file;
+  private $data;
+  private $image;
+  private $imageX;
+  private $imageY;
+
+  /**
+   * Get an estimate of the transformed dimensions of a file.
+   *
+   * @param PhabricatorFile File to transform.
+   * @return list<int, int>|null Width and height, if available.
+   */
+  public function getTransformedDimensions(PhabricatorFile $file) {
+    return null;
+  }
+
+  public function canApplyTransform(PhabricatorFile $file) {
+    if (!$file->isViewableImage()) {
+      return false;
+    }
+
+    if (!$file->isTransformableImage()) {
+      return false;
+    }
+
+    return true;
+  }
+
+  protected function willTransformFile(PhabricatorFile $file) {
+    $this->file = $file;
+    $this->data = null;
+    $this->image = null;
+    $this->imageX = null;
+    $this->imageY = null;
+  }
+
+  protected function getFileProperties() {
+    return array();
+  }
+
+  protected function applyCropAndScale(
+    $dst_w, $dst_h,
+    $src_x, $src_y,
+    $src_w, $src_h,
+    $use_w, $use_h,
+    $scale_up) {
+
+    // Figure out the effective destination width, height, and offsets.
+    $cpy_w = min($dst_w, $use_w);
+    $cpy_h = min($dst_h, $use_h);
+
+    // If we aren't scaling up, and are copying a very small source image,
+    // we're just going to center it in the destination image.
+    if (!$scale_up) {
+      $cpy_w = min($cpy_w, $src_w);
+      $cpy_h = min($cpy_h, $src_h);
+    }
+
+    $off_x = ($dst_w - $cpy_w) / 2;
+    $off_y = ($dst_h - $cpy_h) / 2;
+
+    if ($this->shouldUseImagemagick()) {
+      $argv = array();
+      $argv[] = '-coalesce';
+      $argv[] = '-shave';
+      $argv[] = $src_x.'x'.$src_y;
+      $argv[] = '-resize';
+
+      if ($scale_up) {
+        $argv[] = $dst_w.'x'.$dst_h;
+      } else {
+        $argv[] = $dst_w.'x'.$dst_h.'>';
+      }
+
+      $argv[] = '-bordercolor';
+      $argv[] = 'rgba(255, 255, 255, 0)';
+      $argv[] = '-border';
+      $argv[] = $off_x.'x'.$off_y;
+
+      return $this->applyImagemagick($argv);
+    }
+
+    $src = $this->getImage();
+    $dst = $this->newEmptyImage($dst_w, $dst_h);
+
+    $trap = new PhutilErrorTrap();
+    $ok = @imagecopyresampled(
+      $dst,
+      $src,
+      $off_x, $off_y,
+      $src_x, $src_y,
+      $cpy_w, $cpy_h,
+      $src_w, $src_h);
+    $errors = $trap->getErrorsAsString();
+    $trap->destroy();
+
+    if ($ok === false) {
+      throw new Exception(
+        pht(
+          'Failed to imagecopyresampled() image: %s',
+          $errors));
+    }
+
+    $data = PhabricatorImageTransformer::saveImageDataInAnyFormat(
+      $dst,
+      $this->file->getMimeType());
+
+    return $this->newFileFromData($data);
+  }
+
+  protected function applyImagemagick(array $argv) {
+    $tmp = new TempFile();
+    Filesystem::writeFile($tmp, $this->getData());
+
+    $out = new TempFile();
+
+    $future = new ExecFuture('convert %s %Ls %s', $tmp, $argv, $out);
+    // Don't spend more than 10 seconds resizing; just fail if it takes longer
+    // than that.
+    $future->setTimeout(10)->resolvex();
+
+    $data = Filesystem::readFile($out);
+
+    return $this->newFileFromData($data);
+  }
+
+
+  /**
+   * Create a new @{class:PhabricatorFile} from raw data.
+   *
+   * @param string Raw file data.
+   */
+  protected function newFileFromData($data) {
+    if ($this->file) {
+      $name = $this->file->getName();
+    } else {
+      $name = 'default.png';
+    }
+
+    $defaults = array(
+      'canCDN' => true,
+      'name' => $this->getTransformKey().'-'.$name,
+    );
+
+    $properties = $this->getFileProperties() + $defaults;
+
+    return PhabricatorFile::newFromFileData($data, $properties);
+  }
+
+
+  /**
+   * Create a new image filled with transparent pixels.
+   *
+   * @param int Desired image width.
+   * @param int Desired image height.
+   * @return resource New image resource.
+   */
+  protected function newEmptyImage($w, $h) {
+    $w = (int)$w;
+    $h = (int)$h;
+
+    if (($w <= 0) || ($h <= 0)) {
+      throw new Exception(
+        pht('Can not create an image with nonpositive dimensions.'));
+    }
+
+    $trap = new PhutilErrorTrap();
+    $img = @imagecreatetruecolor($w, $h);
+    $errors = $trap->getErrorsAsString();
+    $trap->destroy();
+    if ($img === false) {
+      throw new Exception(
+        pht(
+          'Unable to imagecreatetruecolor() a new empty image: %s',
+          $errors));
+    }
+
+    $trap = new PhutilErrorTrap();
+    $ok = @imagesavealpha($img, true);
+    $errors = $trap->getErrorsAsString();
+    $trap->destroy();
+    if ($ok === false) {
+      throw new Exception(
+        pht(
+          'Unable to imagesavealpha() a new empty image: %s',
+          $errors));
+    }
+
+    $trap = new PhutilErrorTrap();
+    $color = @imagecolorallocatealpha($img, 255, 255, 255, 127);
+    $errors = $trap->getErrorsAsString();
+    $trap->destroy();
+    if ($color === false) {
+      throw new Exception(
+        pht(
+          'Unable to imagecolorallocatealpha() a new empty image: %s',
+          $errors));
+    }
+
+    $trap = new PhutilErrorTrap();
+    $ok = @imagefill($img, 0, 0, $color);
+    $errors = $trap->getErrorsAsString();
+    $trap->destroy();
+    if ($ok === false) {
+      throw new Exception(
+        pht(
+          'Unable to imagefill() a new empty image: %s',
+          $errors));
+    }
+
+    return $img;
+  }
+
+
+  /**
+   * Get the pixel dimensions of the image being transformed.
+   *
+   * @return list<int, int> Width and height of the image.
+   */
+  protected function getImageDimensions() {
+    if ($this->imageX === null) {
+      $image = $this->getImage();
+
+      $trap = new PhutilErrorTrap();
+      $x = @imagesx($image);
+      $y = @imagesy($image);
+      $errors = $trap->getErrorsAsString();
+      $trap->destroy();
+
+      if (($x === false) || ($y === false) || ($x <= 0) || ($y <= 0)) {
+        throw new Exception(
+          pht(
+            'Unable to determine image dimensions with '.
+            'imagesx()/imagesy(): %s',
+            $errors));
+      }
+
+      $this->imageX = $x;
+      $this->imageY = $y;
+    }
+
+    return array($this->imageX, $this->imageY);
+  }
+
+
+  /**
+   * Get the raw file data for the image being transformed.
+   *
+   * @return string Raw file data.
+   */
+  protected function getData() {
+    if ($this->data !== null) {
+      return $this->data;
+    }
+
+    $file = $this->file;
+
+    $max_size = (1024 * 1024 * 4);
+    $img_size = $file->getByteSize();
+    if ($img_size > $max_size) {
+      throw new Exception(
+        pht(
+          'This image is too large to transform. The transform limit is %s '.
+          'bytes, but the image size is %s bytes.',
+          new PhutilNumber($max_size),
+          new PhutilNumber($img_size)));
+    }
+
+    $data = $file->loadFileData();
+    $this->data = $data;
+    return $this->data;
+  }
+
+
+  /**
+   * Get the GD image resource for the image being transformed.
+   *
+   * @return resource GD image resource.
+   */
+  protected function getImage() {
+    if ($this->image !== null) {
+      return $this->image;
+    }
+
+    if (!function_exists('imagecreatefromstring')) {
+      throw new Exception(
+        pht(
+          'Unable to transform image: the imagecreatefromstring() function '.
+          'is not available. Install or enable the "gd" extension for PHP.'));
+    }
+
+    $data = $this->getData();
+    $data = (string)$data;
+
+    // First, we're going to write the file to disk and use getimagesize()
+    // to determine its dimensions without actually loading the pixel data
+    // into memory. For very large images, we'll bail out.
+
+    // In particular, this defuses a resource exhaustion attack where the
+    // attacker uploads a 40,000 x 40,000 pixel PNGs of solid white. These
+    // kinds of files compress extremely well, but require a huge amount
+    // of memory and CPU to process.
+
+    $tmp = new TempFile();
+    Filesystem::writeFile($tmp, $data);
+    $tmp_path = (string)$tmp;
+
+    $trap = new PhutilErrorTrap();
+    $info = @getimagesize($tmp_path);
+    $errors = $trap->getErrorsAsString();
+    $trap->destroy();
+
+    unset($tmp);
+
+    if ($info === false) {
+      throw new Exception(
+        pht(
+          'Unable to get image information with getimagesize(): %s',
+          $errors));
+    }
+
+    list($width, $height) = $info;
+    if (($width <= 0) || ($height <= 0)) {
+      throw new Exception(
+        pht(
+          'Unable to determine image width and height with getimagesize().'));
+    }
+
+    $max_pixels = (4096 * 4096);
+    $img_pixels = ($width * $height);
+
+    if ($img_pixels > $max_pixels) {
+      throw new Exception(
+        pht(
+          'This image (with dimensions %spx x %spx) is too large to '.
+          'transform. The image has %s pixels, but transforms are limited '.
+          'to images with %s or fewer pixels.',
+          new PhutilNumber($width),
+          new PhutilNumber($height),
+          new PhutilNumber($img_pixels),
+          new PhutilNumber($max_pixels)));
+    }
+
+    $trap = new PhutilErrorTrap();
+    $image = @imagecreatefromstring($data);
+    $errors = $trap->getErrorsAsString();
+    $trap->destroy();
+
+    if ($image === false) {
+      throw new Exception(
+        pht(
+          'Unable to load image data with imagecreatefromstring(): %s',
+          $errors));
+    }
+
+    $this->image = $image;
+    return $this->image;
+  }
+
+  private function shouldUseImagemagick() {
+    if (!PhabricatorEnv::getEnvConfig('files.enable-imagemagick')) {
+      return false;
+    }
+
+    if ($this->file->getMimeType() != 'image/gif') {
+      return false;
+    }
+
+    // Don't try to preserve the animation in huge GIFs.
+    list($x, $y) = $this->getImageDimensions();
+    if (($x * $y) > (512 * 512)) {
+      return false;
+    }
+
+    return true;
+  }
+
+}
diff --git a/src/applications/files/transform/PhabricatorFileThumbnailTransform.php b/src/applications/files/transform/PhabricatorFileThumbnailTransform.php
new file mode 100644
index 0000000000..af97f8a9fa
--- /dev/null
+++ b/src/applications/files/transform/PhabricatorFileThumbnailTransform.php
@@ -0,0 +1,225 @@
+<?php
+
+final class PhabricatorFileThumbnailTransform
+  extends PhabricatorFileImageTransform {
+
+  const TRANSFORM_PROFILE = 'profile';
+  const TRANSFORM_PINBOARD = 'pinboard';
+  const TRANSFORM_THUMBGRID = 'thumbgrid';
+  const TRANSFORM_PREVIEW = 'preview';
+
+  private $name;
+  private $key;
+  private $dstX;
+  private $dstY;
+  private $scaleUp;
+
+  public function setName($name) {
+    $this->name = $name;
+    return $this;
+  }
+
+  public function setKey($key) {
+    $this->key = $key;
+    return $this;
+  }
+
+  public function setDimensions($x, $y) {
+    $this->dstX = $x;
+    $this->dstY = $y;
+    return $this;
+  }
+
+  public function setScaleUp($scale) {
+    $this->scaleUp = $scale;
+    return $this;
+  }
+
+  public function getTransformName() {
+    return $this->name;
+  }
+
+  public function getTransformKey() {
+    return $this->key;
+  }
+
+  protected function getFileProperties() {
+    $properties = array();
+    switch ($this->key) {
+      case self::TRANSFORM_PROFILE:
+        $properties['profile'] = true;
+        $properties['name'] = 'profile';
+        break;
+    }
+    return $properties;
+  }
+
+  public function generateTransforms() {
+    return array(
+      id(new PhabricatorFileThumbnailTransform())
+        ->setName(pht("Profile (100px \xC3\x97 100px)"))
+        ->setKey(self::TRANSFORM_PROFILE)
+        ->setDimensions(100, 100)
+        ->setScaleUp(true),
+      id(new PhabricatorFileThumbnailTransform())
+        ->setName(pht("Pinboard (280px \xC3\x97 210px)"))
+        ->setKey(self::TRANSFORM_PINBOARD)
+        ->setDimensions(280, 210),
+      id(new PhabricatorFileThumbnailTransform())
+        ->setName(pht('Thumbgrid (100px)'))
+        ->setKey(self::TRANSFORM_THUMBGRID)
+        ->setDimensions(100, null),
+      id(new PhabricatorFileThumbnailTransform())
+        ->setName(pht('Preview (220px)'))
+        ->setKey(self::TRANSFORM_PREVIEW)
+        ->setDimensions(220, null),
+    );
+  }
+
+  public function applyTransform(PhabricatorFile $file) {
+    $this->willTransformFile($file);
+
+    list($src_x, $src_y) = $this->getImageDimensions();
+    $dst_x = $this->dstX;
+    $dst_y = $this->dstY;
+
+    $dimensions = $this->computeDimensions(
+      $src_x,
+      $src_y,
+      $dst_x,
+      $dst_y);
+
+    $copy_x = $dimensions['copy_x'];
+    $copy_y = $dimensions['copy_y'];
+    $use_x = $dimensions['use_x'];
+    $use_y = $dimensions['use_y'];
+    $dst_x = $dimensions['dst_x'];
+    $dst_y = $dimensions['dst_y'];
+
+    return $this->applyCropAndScale(
+      $dst_x,
+      $dst_y,
+      ($src_x - $copy_x) / 2,
+      ($src_y - $copy_y) / 2,
+      $copy_x,
+      $copy_y,
+      $use_x,
+      $use_y,
+      $this->scaleUp);
+  }
+
+
+  public function getTransformedDimensions(PhabricatorFile $file) {
+    $dst_x = $this->dstX;
+    $dst_y = $this->dstY;
+
+    // If this is transform has fixed dimensions, we can trivially predict
+    // the dimensions of the transformed file.
+    if ($dst_y !== null) {
+      return array($dst_x, $dst_y);
+    }
+
+    $src_x = $file->getImageWidth();
+    $src_y = $file->getImageHeight();
+
+    if (!$src_x || !$src_y) {
+      return null;
+    }
+
+    $dimensions = $this->computeDimensions(
+      $src_x,
+      $src_y,
+      $dst_x,
+      $dst_y);
+
+    return array($dimensions['dst_x'], $dimensions['dst_y']);
+  }
+
+
+  private function computeDimensions($src_x, $src_y, $dst_x, $dst_y) {
+    if ($dst_y === null) {
+      // If we only have one dimension, it represents a maximum dimension.
+      // The other dimension of the transform is scaled appropriately, except
+      // that we never generate images with crazily extreme aspect ratios.
+      if ($src_x < $src_y) {
+        // This is a tall, narrow image. Use the maximum dimension for the
+        // height and scale the width.
+        $use_y = $dst_x;
+        $dst_y = $dst_x;
+
+        $use_x = $dst_y * ($src_x / $src_y);
+        $dst_x = max($dst_y / 4, $use_x);
+      } else {
+        // This is a short, wide image. Use the maximum dimension for the width
+        // and scale the height.
+        $use_x = $dst_x;
+
+        $use_y = $dst_x * ($src_y / $src_x);
+        $dst_y = max($dst_x / 4, $use_y);
+      }
+
+      // In this mode, we always copy the entire source image. We may generate
+      // margins in the output.
+      $copy_x = $src_x;
+      $copy_y = $src_y;
+    } else {
+      $scale_up = $this->scaleUp;
+
+      // Otherwise, both dimensions are fixed. Figure out how much we'd have to
+      // scale the image down along each dimension to get the entire thing to
+      // fit.
+      $scale_x = ($dst_x / $src_x);
+      $scale_y = ($dst_y / $src_y);
+
+      if (!$scale_up) {
+        $scale_x = min($scale_x, 1);
+        $scale_y = min($scale_y, 1);
+      }
+
+      if ($scale_x > $scale_y) {
+        // This image is relatively tall and narrow. We're going to crop off the
+        // top and bottom.
+        $scale = $scale_x;
+      } else {
+        // This image is relatively short and wide. We're going to crop off the
+        // left and right.
+        $scale = $scale_y;
+      }
+
+      $copy_x = $dst_x / $scale;
+      $copy_y = $dst_y / $scale;
+
+      if (!$scale_up) {
+        $copy_x = min($src_x, $copy_x);
+        $copy_y = min($src_y, $copy_y);
+      }
+
+      // In this mode, we always use the entire destination image. We may
+      // crop the source input.
+      $use_x = $dst_x;
+      $use_y = $dst_y;
+    }
+
+    return array(
+      'copy_x' => $copy_x,
+      'copy_y' => $copy_y,
+      'use_x' => $use_x,
+      'use_y' => $use_y,
+      'dst_x' => $dst_x,
+      'dst_y' => $dst_y,
+    );
+  }
+
+
+  public function getDefaultTransform(PhabricatorFile $file) {
+    $x = (int)$this->dstX;
+    $y = (int)$this->dstY;
+    $name = 'image-'.$x.'x'.nonempty($y, $x).'.png';
+
+    $root = dirname(phutil_get_library_root('phabricator'));
+    $data = Filesystem::readFile($root.'/resources/builtin/'.$name);
+
+    return $this->newFileFromData($data);
+  }
+
+}
diff --git a/src/applications/files/transform/PhabricatorFileTransform.php b/src/applications/files/transform/PhabricatorFileTransform.php
new file mode 100644
index 0000000000..caaf46920d
--- /dev/null
+++ b/src/applications/files/transform/PhabricatorFileTransform.php
@@ -0,0 +1,74 @@
+<?php
+
+abstract class PhabricatorFileTransform extends Phobject {
+
+  abstract public function getTransformName();
+  abstract public function getTransformKey();
+  abstract public function canApplyTransform(PhabricatorFile $file);
+  abstract public function applyTransform(PhabricatorFile $file);
+
+  public function getDefaultTransform(PhabricatorFile $file) {
+    return null;
+  }
+
+  public function generateTransforms() {
+    return array($this);
+  }
+
+  public function executeTransform(PhabricatorFile $file) {
+    if ($this->canApplyTransform($file)) {
+      try {
+        return $this->applyTransform($file);
+      } catch (Exception $ex) {
+        // Ignore.
+      }
+    }
+
+    return $this->getDefaultTransform($file);
+  }
+
+  public static function getAllTransforms() {
+    static $map;
+
+    if ($map === null) {
+      $xforms = id(new PhutilSymbolLoader())
+        ->setAncestorClass(__CLASS__)
+        ->loadObjects();
+
+      $result = array();
+      foreach ($xforms as $xform_template) {
+        foreach ($xform_template->generateTransforms() as $xform) {
+          $key = $xform->getTransformKey();
+          if (isset($result[$key])) {
+            throw new Exception(
+              pht(
+                'Two %s objects define the same transform key ("%s"), but '.
+                'each transform must have a unique key.',
+                __CLASS__,
+                $key));
+          }
+          $result[$key] = $xform;
+        }
+      }
+
+      $map = $result;
+    }
+
+    return $map;
+  }
+
+  public static function getTransformByKey($key) {
+    $all = self::getAllTransforms();
+
+    $xform = idx($all, $key);
+    if (!$xform) {
+      throw new Exception(
+        pht(
+          'No file transform with key "%s" exists.',
+          $key));
+    }
+
+    return $xform;
+  }
+
+}
diff --git a/src/applications/fund/phortune/FundBackerCart.php b/src/applications/fund/phortune/FundBackerCart.php
index 3dc25d4bbf..9cf530f23e 100644
--- a/src/applications/fund/phortune/FundBackerCart.php
+++ b/src/applications/fund/phortune/FundBackerCart.php
@@ -1,88 +1,87 @@
 <?php
 
 final class FundBackerCart extends PhortuneCartImplementation {
 
   private $initiativePHID;
   private $initiative;
 
   public function setInitiativePHID($initiative_phid) {
     $this->initiativePHID = $initiative_phid;
     return $this;
   }
 
   public function getInitiativePHID() {
     return $this->initiativePHID;
   }
 
   public function setInitiative(FundInitiative $initiative) {
     $this->initiative = $initiative;
     return $this;
   }
 
   public function getInitiative() {
     return $this->initiative;
   }
 
   public function getName(PhortuneCart $cart) {
     return pht('Fund Initiative');
   }
 
   public function willCreateCart(
     PhabricatorUser $viewer,
     PhortuneCart $cart) {
 
     $initiative = $this->getInitiative();
     if (!$initiative) {
-      throw new Exception(
-        pht('Call setInitiative() before building a cart!'));
+      throw new PhutilInvalidStateException('setInitiative');
     }
 
     $cart->setMetadataValue('initiativePHID', $initiative->getPHID());
   }
 
   public function loadImplementationsForCarts(
     PhabricatorUser $viewer,
     array $carts) {
 
     $phids = array();
     foreach ($carts as $cart) {
       $phids[] = $cart->getMetadataValue('initiativePHID');
     }
 
     $initiatives = id(new FundInitiativeQuery())
       ->setViewer($viewer)
       ->withPHIDs($phids)
       ->execute();
     $initiatives = mpull($initiatives, null, 'getPHID');
 
     $objects = array();
     foreach ($carts as $key => $cart) {
       $initiative_phid = $cart->getMetadataValue('initiativePHID');
 
       $object = id(new FundBackerCart())
         ->setInitiativePHID($initiative_phid);
 
       $initiative = idx($initiatives, $initiative_phid);
       if ($initiative) {
         $object->setInitiative($initiative);
       }
 
       $objects[$key] = $object;
     }
 
     return $objects;
   }
 
   public function getCancelURI(PhortuneCart $cart) {
     return '/'.$this->getInitiative()->getMonogram();
   }
 
   public function getDoneURI(PhortuneCart $cart) {
     return '/'.$this->getInitiative()->getMonogram();
   }
 
   public function getDoneActionName(PhortuneCart $cart) {
     return pht('Return to Initiative');
   }
 
 }
diff --git a/src/applications/fund/storage/FundInitiativeTransaction.php b/src/applications/fund/storage/FundInitiativeTransaction.php
index 3531ff3e5e..c06d62e229 100644
--- a/src/applications/fund/storage/FundInitiativeTransaction.php
+++ b/src/applications/fund/storage/FundInitiativeTransaction.php
@@ -1,249 +1,249 @@
 <?php
 
 final class FundInitiativeTransaction
   extends PhabricatorApplicationTransaction {
 
   const TYPE_NAME = 'fund:name';
   const TYPE_DESCRIPTION = 'fund:description';
   const TYPE_RISKS = 'fund:risks';
   const TYPE_STATUS = 'fund:status';
   const TYPE_BACKER = 'fund:backer';
   const TYPE_REFUND = 'fund:refund';
   const TYPE_MERCHANT = 'fund:merchant';
 
   const MAILTAG_BACKER = 'fund.backer';
   const MAILTAG_STATUS = 'fund.status';
   const MAILTAG_OTHER  = 'fund.other';
 
   const PROPERTY_AMOUNT = 'fund.amount';
   const PROPERTY_BACKER = 'fund.backer';
 
   public function getApplicationName() {
     return 'fund';
   }
 
   public function getApplicationTransactionType() {
     return FundInitiativePHIDType::TYPECONST;
   }
 
   public function getApplicationTransactionCommentObject() {
     return null;
   }
 
   public function getRequiredHandlePHIDs() {
     $phids = parent::getRequiredHandlePHIDs();
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     $type = $this->getTransactionType();
     switch ($type) {
-      case FundInitiativeTransaction::TYPE_MERCHANT:
+      case self::TYPE_MERCHANT:
         if ($old) {
           $phids[] = $old;
         }
         if ($new) {
           $phids[] = $new;
         }
         break;
-      case FundInitiativeTransaction::TYPE_REFUND:
+      case self::TYPE_REFUND:
         $phids[] = $this->getMetadataValue(self::PROPERTY_BACKER);
         break;
     }
 
     return $phids;
   }
 
   public function getTitle() {
     $author_phid = $this->getAuthorPHID();
     $object_phid = $this->getObjectPHID();
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     $type = $this->getTransactionType();
     switch ($type) {
-      case FundInitiativeTransaction::TYPE_NAME:
+      case self::TYPE_NAME:
         if ($old === null) {
           return pht(
             '%s created this initiative.',
             $this->renderHandleLink($author_phid));
         } else {
           return pht(
             '%s renamed this initiative from "%s" to "%s".',
             $this->renderHandleLink($author_phid),
             $old,
             $new);
         }
         break;
-      case FundInitiativeTransaction::TYPE_RISKS:
+      case self::TYPE_RISKS:
         return pht(
           '%s edited the risks for this initiative.',
           $this->renderHandleLink($author_phid));
-      case FundInitiativeTransaction::TYPE_DESCRIPTION:
+      case self::TYPE_DESCRIPTION:
         return pht(
           '%s edited the description of this initiative.',
           $this->renderHandleLink($author_phid));
-      case FundInitiativeTransaction::TYPE_STATUS:
+      case self::TYPE_STATUS:
         switch ($new) {
           case FundInitiative::STATUS_OPEN:
             return pht(
               '%s reopened this initiative.',
               $this->renderHandleLink($author_phid));
           case FundInitiative::STATUS_CLOSED:
             return pht(
               '%s closed this initiative.',
               $this->renderHandleLink($author_phid));
         }
         break;
-      case FundInitiativeTransaction::TYPE_BACKER:
+      case self::TYPE_BACKER:
         $amount = $this->getMetadataValue(self::PROPERTY_AMOUNT);
         $amount = PhortuneCurrency::newFromString($amount);
         return pht(
           '%s backed this initiative with %s.',
           $this->renderHandleLink($author_phid),
           $amount->formatForDisplay());
-      case FundInitiativeTransaction::TYPE_REFUND:
+      case self::TYPE_REFUND:
         $amount = $this->getMetadataValue(self::PROPERTY_AMOUNT);
         $amount = PhortuneCurrency::newFromString($amount);
 
         $backer_phid = $this->getMetadataValue(self::PROPERTY_BACKER);
 
         return pht(
           '%s refunded %s to %s.',
           $this->renderHandleLink($author_phid),
           $amount->formatForDisplay(),
           $this->renderHandleLink($backer_phid));
-      case FundInitiativeTransaction::TYPE_MERCHANT:
+      case self::TYPE_MERCHANT:
         if ($old === null) {
           return pht(
             '%s set this initiative to pay to %s.',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($new));
         } else {
           return pht(
             '%s changed the merchant receiving funds from this '.
             'initiative from %s to %s.',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($old),
             $this->renderHandleLink($new));
         }
     }
 
     return parent::getTitle();
   }
 
   public function getTitleForFeed() {
     $author_phid = $this->getAuthorPHID();
     $object_phid = $this->getObjectPHID();
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     $type = $this->getTransactionType();
     switch ($type) {
-      case FundInitiativeTransaction::TYPE_NAME:
+      case self::TYPE_NAME:
         if ($old === null) {
           return pht(
             '%s created %s.',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid));
 
         } else {
           return pht(
             '%s renamed %s.',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid));
         }
         break;
-      case FundInitiativeTransaction::TYPE_DESCRIPTION:
+      case self::TYPE_DESCRIPTION:
         return pht(
           '%s updated the description for %s.',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($object_phid));
-      case FundInitiativeTransaction::TYPE_STATUS:
+      case self::TYPE_STATUS:
         switch ($new) {
           case FundInitiative::STATUS_OPEN:
             return pht(
               '%s reopened %s.',
               $this->renderHandleLink($author_phid),
               $this->renderHandleLink($object_phid));
           case FundInitiative::STATUS_CLOSED:
             return pht(
               '%s closed %s.',
               $this->renderHandleLink($author_phid),
               $this->renderHandleLink($object_phid));
         }
         break;
-      case FundInitiativeTransaction::TYPE_BACKER:
+      case self::TYPE_BACKER:
         $amount = $this->getMetadataValue(self::PROPERTY_AMOUNT);
         $amount = PhortuneCurrency::newFromString($amount);
         return pht(
           '%s backed %s with %s.',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($object_phid),
           $amount->formatForDisplay());
-      case FundInitiativeTransaction::TYPE_REFUND:
+      case self::TYPE_REFUND:
         $amount = $this->getMetadataValue(self::PROPERTY_AMOUNT);
         $amount = PhortuneCurrency::newFromString($amount);
 
         $backer_phid = $this->getMetadataValue(self::PROPERTY_BACKER);
 
         return pht(
           '%s refunded %s to %s for %s.',
           $this->renderHandleLink($author_phid),
           $amount->formatForDisplay(),
           $this->renderHandleLink($backer_phid),
           $this->renderHandleLink($object_phid));
     }
 
     return parent::getTitleForFeed();
   }
 
   public function getMailTags() {
     $tags = parent::getMailTags();
 
     switch ($this->getTransactionType()) {
       case self::TYPE_STATUS:
         $tags[] = self::MAILTAG_STATUS;
         break;
       case self::TYPE_BACKER:
       case self::TYPE_REFUND:
         $tags[] = self::MAILTAG_BACKER;
         break;
       default:
         $tags[] = self::MAILTAG_OTHER;
         break;
     }
 
     return $tags;
   }
 
 
   public function shouldHide() {
     $old = $this->getOldValue();
     switch ($this->getTransactionType()) {
-      case FundInitiativeTransaction::TYPE_DESCRIPTION:
-      case FundInitiativeTransaction::TYPE_RISKS:
+      case self::TYPE_DESCRIPTION:
+      case self::TYPE_RISKS:
         return ($old === null);
     }
     return parent::shouldHide();
   }
 
   public function hasChangeDetails() {
     switch ($this->getTransactionType()) {
-      case FundInitiativeTransaction::TYPE_DESCRIPTION:
-      case FundInitiativeTransaction::TYPE_RISKS:
+      case self::TYPE_DESCRIPTION:
+      case self::TYPE_RISKS:
         return ($this->getOldValue() !== null);
     }
 
     return parent::hasChangeDetails();
   }
 
   public function renderChangeDetails(PhabricatorUser $viewer) {
     return $this->renderTextCorpusChangeDetails(
       $viewer,
       $this->getOldValue(),
       $this->getNewValue());
   }
 }
diff --git a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php
index 5057097fac..4fe1128ac0 100644
--- a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php
+++ b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php
@@ -1,250 +1,250 @@
 <?php
 
 abstract class HarbormasterBuildStepImplementation {
 
   public static function getImplementations() {
     return id(new PhutilSymbolLoader())
-      ->setAncestorClass('HarbormasterBuildStepImplementation')
+      ->setAncestorClass(__CLASS__)
       ->loadObjects();
   }
 
   public static function getImplementation($class) {
     $base = idx(self::getImplementations(), $class);
 
     if ($base) {
       return (clone $base);
     }
 
     return null;
   }
 
   public static function requireImplementation($class) {
     if (!$class) {
       throw new Exception(pht('No implementation is specified!'));
     }
 
     $implementation = self::getImplementation($class);
     if (!$implementation) {
       throw new Exception(pht('No such implementation "%s" exists!', $class));
     }
 
     return $implementation;
   }
 
   /**
    * The name of the implementation.
    */
   abstract public function getName();
 
   /**
    * The generic description of the implementation.
    */
   public function getGenericDescription() {
     return '';
   }
 
   /**
    * The description of the implementation, based on the current settings.
    */
   public function getDescription() {
     return $this->getGenericDescription();
   }
 
   /**
    * Run the build target against the specified build.
    */
   abstract public function execute(
     HarbormasterBuild $build,
     HarbormasterBuildTarget $build_target);
 
   /**
    * Gets the settings for this build step.
    */
   public function getSettings() {
     return $this->settings;
   }
 
   public function getSetting($key, $default = null) {
     return idx($this->settings, $key, $default);
   }
 
   /**
    * Loads the settings for this build step implementation from a build
    * step or target.
    */
   public final function loadSettings($build_object) {
     $this->settings = $build_object->getDetails();
     return $this;
   }
 
   /**
    * Return the name of artifacts produced by this command.
    *
    * Something like:
    *
    *   return array(
    *     'some_name_input_by_user' => 'host');
    *
    * Future steps will calculate all available artifact mappings
    * before them and filter on the type.
    *
    * @return array The mappings of artifact names to their types.
    */
   public function getArtifactInputs() {
     return array();
   }
 
   public function getArtifactOutputs() {
     return array();
   }
 
   public function getDependencies(HarbormasterBuildStep $build_step) {
     $dependencies = $build_step->getDetail('dependsOn', array());
 
     $inputs = $build_step->getStepImplementation()->getArtifactInputs();
     $inputs = ipull($inputs, null, 'key');
 
     $artifacts = $this->getAvailableArtifacts(
       $build_step->getBuildPlan(),
       $build_step,
       null);
 
     foreach ($artifacts as $key => $type) {
       if (!array_key_exists($key, $inputs)) {
         unset($artifacts[$key]);
       }
     }
 
     $artifact_steps = ipull($artifacts, 'step');
     $artifact_steps = mpull($artifact_steps, 'getPHID');
 
     $dependencies = array_merge($dependencies, $artifact_steps);
 
     return $dependencies;
   }
 
   /**
    * Returns a list of all artifacts made available in the build plan.
    */
   public static function getAvailableArtifacts(
     HarbormasterBuildPlan $build_plan,
     $current_build_step,
     $artifact_type) {
 
     $steps = id(new HarbormasterBuildStepQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withBuildPlanPHIDs(array($build_plan->getPHID()))
       ->execute();
 
     $artifacts = array();
 
     $artifact_arrays = array();
     foreach ($steps as $step) {
       if ($current_build_step !== null &&
         $step->getPHID() === $current_build_step->getPHID()) {
 
         continue;
       }
 
       $implementation = $step->getStepImplementation();
       $array = $implementation->getArtifactOutputs();
       $array = ipull($array, 'type', 'key');
       foreach ($array as $name => $type) {
         if ($type !== $artifact_type && $artifact_type !== null) {
           continue;
         }
         $artifacts[$name] = array('type' => $type, 'step' => $step);
       }
     }
 
     return $artifacts;
   }
 
   /**
    * Convert a user-provided string with variables in it, like:
    *
    *   ls ${dirname}
    *
    * ...into a string with variables merged into it safely:
    *
    *   ls 'dir with spaces'
    *
    * @param string Name of a `vxsprintf` function, like @{function:vcsprintf}.
    * @param string User-provided pattern string containing `${variables}`.
    * @param dict   List of available replacement variables.
    * @return string String with variables replaced safely into it.
    */
   protected function mergeVariables($function, $pattern, array $variables) {
     $regexp = '/\\$\\{(?P<name>[a-z\\.]+)\\}/';
 
     $matches = null;
     preg_match_all($regexp, $pattern, $matches);
 
     $argv = array();
     foreach ($matches['name'] as $name) {
       if (!array_key_exists($name, $variables)) {
         throw new Exception(pht("No such variable '%s'!", $name));
       }
       $argv[] = $variables[$name];
     }
 
     $pattern = str_replace('%', '%%', $pattern);
     $pattern = preg_replace($regexp, '%s', $pattern);
 
     return call_user_func($function, $pattern, $argv);
   }
 
   public function getFieldSpecifications() {
     return array();
   }
 
   protected function formatSettingForDescription($key, $default = null) {
     return $this->formatValueForDescription($this->getSetting($key, $default));
   }
 
   protected function formatValueForDescription($value) {
     if (strlen($value)) {
       return phutil_tag('strong', array(), $value);
     } else {
       return phutil_tag('em', array(), pht('(null)'));
     }
   }
 
   public function supportsWaitForMessage() {
     return false;
   }
 
   public function shouldWaitForMessage(HarbormasterBuildTarget $target) {
     if (!$this->supportsWaitForMessage()) {
       return false;
     }
 
     return (bool)$target->getDetail('builtin.wait-for-message');
   }
 
   protected function shouldAbort(
     HarbormasterBuild $build,
     HarbormasterBuildTarget $target) {
 
     return $build->getBuildGeneration() !== $target->getBuildGeneration();
   }
 
   protected function resolveFuture(
     HarbormasterBuild $build,
     HarbormasterBuildTarget $target,
     Future $future) {
 
     $futures = new FutureIterator(array($future));
     foreach ($futures->setUpdateInterval(5) as $key => $future) {
       if ($future === null) {
         $build->reload();
         if ($this->shouldAbort($build, $target)) {
           throw new HarbormasterBuildAbortedException();
         }
       } else {
         return $future->resolve();
       }
     }
   }
 
 }
diff --git a/src/applications/harbormaster/storage/HarbormasterBuildable.php b/src/applications/harbormaster/storage/HarbormasterBuildable.php
index b53e2bc2d3..81f8e66d07 100644
--- a/src/applications/harbormaster/storage/HarbormasterBuildable.php
+++ b/src/applications/harbormaster/storage/HarbormasterBuildable.php
@@ -1,302 +1,302 @@
 <?php
 
 final class HarbormasterBuildable extends HarbormasterDAO
   implements
     PhabricatorApplicationTransactionInterface,
     PhabricatorPolicyInterface,
     HarbormasterBuildableInterface {
 
   protected $buildablePHID;
   protected $containerPHID;
   protected $buildableStatus;
   protected $isManualBuildable;
 
   private $buildableObject = self::ATTACHABLE;
   private $containerObject = self::ATTACHABLE;
   private $buildableHandle = self::ATTACHABLE;
   private $containerHandle = self::ATTACHABLE;
   private $builds = self::ATTACHABLE;
 
   const STATUS_BUILDING = 'building';
   const STATUS_PASSED = 'passed';
   const STATUS_FAILED = 'failed';
 
   public static function getBuildableStatusName($status) {
     switch ($status) {
       case self::STATUS_BUILDING:
         return pht('Building');
       case self::STATUS_PASSED:
         return pht('Passed');
       case self::STATUS_FAILED:
         return pht('Failed');
       default:
         return pht('Unknown');
     }
   }
 
   public static function getBuildableStatusIcon($status) {
     switch ($status) {
       case self::STATUS_BUILDING:
         return PHUIStatusItemView::ICON_RIGHT;
       case self::STATUS_PASSED:
         return PHUIStatusItemView::ICON_ACCEPT;
       case self::STATUS_FAILED:
         return PHUIStatusItemView::ICON_REJECT;
       default:
         return PHUIStatusItemView::ICON_QUESTION;
     }
   }
 
   public static function getBuildableStatusColor($status) {
     switch ($status) {
       case self::STATUS_BUILDING:
         return 'blue';
       case self::STATUS_PASSED:
         return 'green';
       case self::STATUS_FAILED:
         return 'red';
       default:
         return 'bluegrey';
     }
   }
 
   public static function initializeNewBuildable(PhabricatorUser $actor) {
     return id(new HarbormasterBuildable())
       ->setIsManualBuildable(0)
       ->setBuildableStatus(self::STATUS_BUILDING);
   }
 
   public function getMonogram() {
     return 'B'.$this->getID();
   }
 
   /**
    * Returns an existing buildable for the object's PHID or creates a
    * new buildable implicitly if needed.
    */
   public static function createOrLoadExisting(
     PhabricatorUser $actor,
     $buildable_object_phid,
     $container_object_phid) {
 
     $buildable = id(new HarbormasterBuildableQuery())
       ->setViewer($actor)
       ->withBuildablePHIDs(array($buildable_object_phid))
       ->withManualBuildables(false)
       ->setLimit(1)
       ->executeOne();
     if ($buildable) {
       return $buildable;
     }
-    $buildable = HarbormasterBuildable::initializeNewBuildable($actor)
+    $buildable = self::initializeNewBuildable($actor)
       ->setBuildablePHID($buildable_object_phid)
       ->setContainerPHID($container_object_phid);
     $buildable->save();
     return $buildable;
   }
 
   /**
    * Looks up the plan PHIDs and applies the plans to the specified
    * object identified by it's PHID.
    */
   public static function applyBuildPlans(
     $phid,
     $container_phid,
     array $plan_phids) {
 
     if (count($plan_phids) === 0) {
       return;
     }
 
     // Skip all of this logic if the Harbormaster application
     // isn't currently installed.
 
     $harbormaster_app = 'PhabricatorHarbormasterApplication';
     if (!PhabricatorApplication::isClassInstalled($harbormaster_app)) {
       return;
     }
 
-    $buildable = HarbormasterBuildable::createOrLoadExisting(
+    $buildable = self::createOrLoadExisting(
       PhabricatorUser::getOmnipotentUser(),
       $phid,
       $container_phid);
 
     $plans = id(new HarbormasterBuildPlanQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withPHIDs($plan_phids)
       ->execute();
     foreach ($plans as $plan) {
       if ($plan->isDisabled()) {
         // TODO: This should be communicated more clearly -- maybe we should
         // create the build but set the status to "disabled" or "derelict".
         continue;
       }
 
       $buildable->applyPlan($plan);
     }
   }
 
   public function applyPlan(HarbormasterBuildPlan $plan) {
     $viewer = PhabricatorUser::getOmnipotentUser();
     $build = HarbormasterBuild::initializeNewBuild($viewer)
       ->setBuildablePHID($this->getPHID())
       ->setBuildPlanPHID($plan->getPHID())
       ->setBuildStatus(HarbormasterBuild::STATUS_PENDING)
       ->save();
 
     PhabricatorWorker::scheduleTask(
       'HarbormasterBuildWorker',
       array(
         'buildID' => $build->getID(),
       ));
 
     return $build;
   }
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_COLUMN_SCHEMA => array(
         'containerPHID' => 'phid?',
         'buildableStatus' => 'text32',
         'isManualBuildable' => 'bool',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_buildable' => array(
           'columns' => array('buildablePHID'),
         ),
         'key_container' => array(
           'columns' => array('containerPHID'),
         ),
         'key_manual' => array(
           'columns' => array('isManualBuildable'),
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       HarbormasterBuildablePHIDType::TYPECONST);
   }
 
   public function attachBuildableObject($buildable_object) {
     $this->buildableObject = $buildable_object;
     return $this;
   }
 
   public function getBuildableObject() {
     return $this->assertAttached($this->buildableObject);
   }
 
   public function attachContainerObject($container_object) {
     $this->containerObject = $container_object;
     return $this;
   }
 
   public function getContainerObject() {
     return $this->assertAttached($this->containerObject);
   }
 
   public function attachContainerHandle($container_handle) {
     $this->containerHandle = $container_handle;
     return $this;
   }
 
   public function getContainerHandle() {
     return $this->assertAttached($this->containerHandle);
   }
 
   public function attachBuildableHandle($buildable_handle) {
     $this->buildableHandle = $buildable_handle;
     return $this;
   }
 
   public function getBuildableHandle() {
     return $this->assertAttached($this->buildableHandle);
   }
 
   public function attachBuilds(array $builds) {
     assert_instances_of($builds, 'HarbormasterBuild');
     $this->builds = $builds;
     return $this;
   }
 
   public function getBuilds() {
     return $this->assertAttached($this->builds);
   }
 
 
 /* -(  PhabricatorApplicationTransactionInterface  )------------------------- */
 
 
   public function getApplicationTransactionEditor() {
     return new HarbormasterBuildableTransactionEditor();
   }
 
   public function getApplicationTransactionObject() {
     return $this;
   }
 
   public function getApplicationTransactionTemplate() {
     return new HarbormasterBuildableTransaction();
   }
 
   public function willRenderTimeline(
     PhabricatorApplicationTransactionView $timeline,
     AphrontRequest $request) {
 
     return $timeline;
   }
 
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
       PhabricatorPolicyCapability::CAN_EDIT,
     );
   }
 
   public function getPolicy($capability) {
     return $this->getBuildableObject()->getPolicy($capability);
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     return $this->getBuildableObject()->hasAutomaticCapability(
       $capability,
       $viewer);
   }
 
   public function describeAutomaticCapability($capability) {
     return pht('A buildable inherits policies from the underlying object.');
   }
 
 
 
 /* -(  HarbormasterBuildableInterface  )------------------------------------- */
 
 
   public function getHarbormasterBuildablePHID() {
     // NOTE: This is essentially just for convenience, as it allows you create
     // a copy of a buildable by specifying `B123` without bothering to go
     // look up the underlying object.
     return $this->getBuildablePHID();
   }
 
   public function getHarbormasterContainerPHID() {
     return $this->getContainerPHID();
   }
 
   public function getBuildVariables() {
     return array();
   }
 
   public function getAvailableBuildVariables() {
     return array();
   }
 
 
 }
diff --git a/src/applications/help/application/PhabricatorHelpApplication.php b/src/applications/help/application/PhabricatorHelpApplication.php
index c86d5cd0d0..b1f66b02cd 100644
--- a/src/applications/help/application/PhabricatorHelpApplication.php
+++ b/src/applications/help/application/PhabricatorHelpApplication.php
@@ -1,108 +1,108 @@
 <?php
 
 final class PhabricatorHelpApplication extends PhabricatorApplication {
 
   public function getName() {
     return pht('Help');
   }
 
   public function canUninstall() {
     return false;
   }
 
   public function isUnlisted() {
     return true;
   }
 
   public function getRoutes() {
     return array(
       '/help/' => array(
         'keyboardshortcut/' => 'PhabricatorHelpKeyboardShortcutController',
         'editorprotocol/' => 'PhabricatorHelpEditorProtocolController',
         'documentation/(?P<application>\w+)/'
           => 'PhabricatorHelpDocumentationController',
       ),
     );
   }
 
   public function buildMainMenuItems(
     PhabricatorUser $user,
     PhabricatorController $controller = null) {
 
     $application = null;
     if ($controller) {
       $application = $controller->getCurrentApplication();
     }
 
     $items = array();
 
     $help_id = celerity_generate_unique_node_id();
 
     Javelin::initBehavior(
       'aphlict-dropdown',
       array(
         'bubbleID' => $help_id,
         'dropdownID' => 'phabricator-help-menu',
-        'applicationClass' => 'PhabricatorHelpApplication',
+        'applicationClass' => __CLASS__,
         'local' => true,
         'desktop' => true,
         'right' => true,
       ));
 
     $item = id(new PHUIListItemView())
       ->setIcon('fa-life-ring')
       ->addClass('core-menu-item')
       ->setID($help_id)
       ->setOrder(200);
 
     $hide = true;
     if ($application) {
       $help_name = pht('%s Help', $application->getName());
         $item
           ->setName($help_name)
           ->setHref('/help/documentation/'.get_class($application).'/')
           ->setAural($help_name);
       $help_items = $application->getHelpMenuItems($user);
       if ($help_items) {
         $hide = false;
       }
     }
     if ($hide) {
       $item->setStyle('display: none');
     }
     $items[] = $item;
 
     return $items;
   }
 
   public function buildMainMenuExtraNodes(
     PhabricatorUser $viewer,
     PhabricatorController $controller = null) {
 
     $application = null;
     if ($controller) {
       $application = $controller->getCurrentApplication();
     }
 
     $view = null;
     if ($application) {
       $help_items = $application->getHelpMenuItems($viewer);
       if ($help_items) {
         $view = new PHUIListView();
         foreach ($help_items as $item) {
           $view->addMenuItem($item);
         }
       }
     }
 
     return phutil_tag(
       'div',
       array(
         'id' => 'phabricator-help-menu',
         'class' => 'phabricator-main-menu-dropdown phui-list-sidenav',
         'style' => 'display: none',
       ),
       $view);
   }
 
 }
diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php
index 6a51ade55f..f56572a398 100644
--- a/src/applications/herald/adapter/HeraldAdapter.php
+++ b/src/applications/herald/adapter/HeraldAdapter.php
@@ -1,1651 +1,1651 @@
 <?php
 
 /**
  * @task customfield Custom Field Integration
  */
 abstract class HeraldAdapter {
 
   const FIELD_TITLE                  = 'title';
   const FIELD_BODY                   = 'body';
   const FIELD_AUTHOR                 = 'author';
   const FIELD_ASSIGNEE               = 'assignee';
   const FIELD_REVIEWER               = 'reviewer';
   const FIELD_REVIEWERS              = 'reviewers';
   const FIELD_COMMITTER              = 'committer';
   const FIELD_CC                     = 'cc';
   const FIELD_TAGS                   = 'tags';
   const FIELD_DIFF_FILE              = 'diff-file';
   const FIELD_DIFF_CONTENT           = 'diff-content';
   const FIELD_DIFF_ADDED_CONTENT     = 'diff-added-content';
   const FIELD_DIFF_REMOVED_CONTENT   = 'diff-removed-content';
   const FIELD_DIFF_ENORMOUS          = 'diff-enormous';
   const FIELD_REPOSITORY             = 'repository';
   const FIELD_REPOSITORY_PROJECTS    = 'repository-projects';
   const FIELD_RULE                   = 'rule';
   const FIELD_AFFECTED_PACKAGE       = 'affected-package';
   const FIELD_AFFECTED_PACKAGE_OWNER = 'affected-package-owner';
   const FIELD_CONTENT_SOURCE         = 'contentsource';
   const FIELD_ALWAYS                 = 'always';
   const FIELD_AUTHOR_PROJECTS        = 'authorprojects';
   const FIELD_PROJECTS               = 'projects';
   const FIELD_PUSHER                 = 'pusher';
   const FIELD_PUSHER_PROJECTS        = 'pusher-projects';
   const FIELD_DIFFERENTIAL_REVISION  = 'differential-revision';
   const FIELD_DIFFERENTIAL_REVIEWERS = 'differential-reviewers';
   const FIELD_DIFFERENTIAL_CCS       = 'differential-ccs';
   const FIELD_DIFFERENTIAL_ACCEPTED  = 'differential-accepted';
   const FIELD_IS_MERGE_COMMIT        = 'is-merge-commit';
   const FIELD_BRANCHES               = 'branches';
   const FIELD_AUTHOR_RAW             = 'author-raw';
   const FIELD_COMMITTER_RAW          = 'committer-raw';
   const FIELD_IS_NEW_OBJECT          = 'new-object';
   const FIELD_APPLICATION_EMAIL      = 'applicaton-email';
   const FIELD_TASK_PRIORITY          = 'taskpriority';
   const FIELD_TASK_STATUS            = 'taskstatus';
   const FIELD_ARCANIST_PROJECT       = 'arcanist-project';
   const FIELD_PUSHER_IS_COMMITTER    = 'pusher-is-committer';
   const FIELD_PATH                   = 'path';
 
   const CONDITION_CONTAINS        = 'contains';
   const CONDITION_NOT_CONTAINS    = '!contains';
   const CONDITION_IS              = 'is';
   const CONDITION_IS_NOT          = '!is';
   const CONDITION_IS_ANY          = 'isany';
   const CONDITION_IS_NOT_ANY      = '!isany';
   const CONDITION_INCLUDE_ALL     = 'all';
   const CONDITION_INCLUDE_ANY     = 'any';
   const CONDITION_INCLUDE_NONE    = 'none';
   const CONDITION_IS_ME           = 'me';
   const CONDITION_IS_NOT_ME       = '!me';
   const CONDITION_REGEXP          = 'regexp';
   const CONDITION_RULE            = 'conditions';
   const CONDITION_NOT_RULE        = '!conditions';
   const CONDITION_EXISTS          = 'exists';
   const CONDITION_NOT_EXISTS      = '!exists';
   const CONDITION_UNCONDITIONALLY = 'unconditionally';
   const CONDITION_NEVER           = 'never';
   const CONDITION_REGEXP_PAIR     = 'regexp-pair';
   const CONDITION_HAS_BIT         = 'bit';
   const CONDITION_NOT_BIT         = '!bit';
   const CONDITION_IS_TRUE         = 'true';
   const CONDITION_IS_FALSE        = 'false';
 
   const ACTION_ADD_CC       = 'addcc';
   const ACTION_REMOVE_CC    = 'remcc';
   const ACTION_EMAIL        = 'email';
   const ACTION_NOTHING      = 'nothing';
   const ACTION_AUDIT        = 'audit';
   const ACTION_FLAG         = 'flag';
   const ACTION_ASSIGN_TASK  = 'assigntask';
   const ACTION_ADD_PROJECTS = 'addprojects';
   const ACTION_REMOVE_PROJECTS = 'removeprojects';
   const ACTION_ADD_REVIEWERS = 'addreviewers';
   const ACTION_ADD_BLOCKING_REVIEWERS = 'addblockingreviewers';
   const ACTION_APPLY_BUILD_PLANS = 'applybuildplans';
   const ACTION_BLOCK = 'block';
   const ACTION_REQUIRE_SIGNATURE = 'signature';
 
   const VALUE_TEXT            = 'text';
   const VALUE_NONE            = 'none';
   const VALUE_EMAIL           = 'email';
   const VALUE_USER            = 'user';
   const VALUE_TAG             = 'tag';
   const VALUE_RULE            = 'rule';
   const VALUE_REPOSITORY      = 'repository';
   const VALUE_OWNERS_PACKAGE  = 'package';
   const VALUE_PROJECT         = 'project';
   const VALUE_FLAG_COLOR      = 'flagcolor';
   const VALUE_CONTENT_SOURCE  = 'contentsource';
   const VALUE_USER_OR_PROJECT = 'userorproject';
   const VALUE_BUILD_PLAN      = 'buildplan';
   const VALUE_TASK_PRIORITY   = 'taskpriority';
   const VALUE_TASK_STATUS     = 'taskstatus';
   const VALUE_ARCANIST_PROJECT  = 'arcanistprojects';
   const VALUE_LEGAL_DOCUMENTS   = 'legaldocuments';
   const VALUE_APPLICATION_EMAIL = 'applicationemail';
 
   private $contentSource;
   private $isNewObject;
   private $applicationEmail;
   private $customFields = false;
   private $customActions = null;
   private $queuedTransactions = array();
   private $emailPHIDs = array();
   private $forcedEmailPHIDs = array();
 
   public function getEmailPHIDs() {
     return array_values($this->emailPHIDs);
   }
 
   public function getForcedEmailPHIDs() {
     return array_values($this->forcedEmailPHIDs);
   }
 
   public function getCustomActions() {
     if ($this->customActions === null) {
       $custom_actions = id(new PhutilSymbolLoader())
         ->setAncestorClass('HeraldCustomAction')
         ->loadObjects();
 
       foreach ($custom_actions as $key => $object) {
         if (!$object->appliesToAdapter($this)) {
           unset($custom_actions[$key]);
         }
       }
 
       $this->customActions = array();
       foreach ($custom_actions as $action) {
         $key = $action->getActionKey();
 
         if (array_key_exists($key, $this->customActions)) {
           throw new Exception(
             'More than one Herald custom action implementation '.
             'handles the action key: \''.$key.'\'.');
         }
 
         $this->customActions[$key] = $action;
       }
     }
 
     return $this->customActions;
   }
 
   public function setContentSource(PhabricatorContentSource $content_source) {
     $this->contentSource = $content_source;
     return $this;
   }
   public function getContentSource() {
     return $this->contentSource;
   }
 
   public function getIsNewObject() {
     if (is_bool($this->isNewObject)) {
       return $this->isNewObject;
     }
 
     throw new Exception(pht('You must setIsNewObject to a boolean first!'));
   }
   public function setIsNewObject($new) {
     $this->isNewObject = (bool) $new;
     return $this;
   }
 
   public function setApplicationEmail(
     PhabricatorMetaMTAApplicationEmail $email) {
     $this->applicationEmail = $email;
     return $this;
   }
 
   public function getApplicationEmail() {
     return $this->applicationEmail;
   }
 
   abstract public function getPHID();
   abstract public function getHeraldName();
 
   public function getHeraldField($field_name) {
     switch ($field_name) {
       case self::FIELD_RULE:
         return null;
       case self::FIELD_CONTENT_SOURCE:
         return $this->getContentSource()->getSource();
       case self::FIELD_ALWAYS:
         return true;
       case self::FIELD_IS_NEW_OBJECT:
         return $this->getIsNewObject();
       case self::FIELD_APPLICATION_EMAIL:
         $value = array();
         // while there is only one match by implementation, we do set
         // comparisons on phids, so return an array with just the phid
         if ($this->getApplicationEmail()) {
           $value[] = $this->getApplicationEmail()->getPHID();
         }
         return $value;
       default:
         if ($this->isHeraldCustomKey($field_name)) {
           return $this->getCustomFieldValue($field_name);
         }
 
         throw new Exception(
           "Unknown field '{$field_name}'!");
     }
   }
 
   abstract public function applyHeraldEffects(array $effects);
 
   protected function handleCustomHeraldEffect(HeraldEffect $effect) {
     $custom_action = idx($this->getCustomActions(), $effect->getAction());
 
     if ($custom_action !== null) {
       return $custom_action->applyEffect(
         $this,
         $this->getObject(),
         $effect);
     }
 
     return null;
   }
 
   public function isAvailableToUser(PhabricatorUser $viewer) {
     $applications = id(new PhabricatorApplicationQuery())
       ->setViewer($viewer)
       ->withInstalled(true)
       ->withClasses(array($this->getAdapterApplicationClass()))
       ->execute();
 
     return !empty($applications);
   }
 
   public function queueTransaction($transaction) {
     $this->queuedTransactions[] = $transaction;
   }
 
   public function getQueuedTransactions() {
     return $this->queuedTransactions;
   }
 
   protected function newTransaction() {
     $object = $this->newObject();
 
     if (!($object instanceof PhabricatorApplicationTransactionInterface)) {
       throw new Exception(
         pht(
           'Unable to build a new transaction for adapter object; it does '.
           'not implement "%s".',
           'PhabricatorApplicationTransactionInterface'));
     }
 
     return $object->getApplicationTransactionTemplate();
   }
 
 
   /**
    * NOTE: You generally should not override this; it exists to support legacy
    * adapters which had hard-coded content types.
    */
   public function getAdapterContentType() {
     return get_class($this);
   }
 
   abstract public function getAdapterContentName();
   abstract public function getAdapterContentDescription();
   abstract public function getAdapterApplicationClass();
   abstract public function getObject();
 
 
   /**
    * Return a new characteristic object for this adapter.
    *
    * The adapter will use this object to test for interfaces, generate
    * transactions, and interact with custom fields.
    *
    * Adapters must return an object from this method to enable custom
    * field rules and various implicit actions.
    *
    * Normally, you'll return an empty version of the adapted object:
    *
    *   return new ApplicationObject();
    *
    * @return null|object Template object.
    */
   protected function newObject() {
     return null;
   }
 
   public function supportsRuleType($rule_type) {
     return false;
   }
 
   public function canTriggerOnObject($object) {
     return false;
   }
 
   public function explainValidTriggerObjects() {
     return pht('This adapter can not trigger on objects.');
   }
 
   public function getTriggerObjectPHIDs() {
     return array($this->getPHID());
   }
 
   public function getAdapterSortKey() {
     return sprintf(
       '%08d%s',
       $this->getAdapterSortOrder(),
       $this->getAdapterContentName());
   }
 
   public function getAdapterSortOrder() {
     return 1000;
   }
 
 
 /* -(  Fields  )------------------------------------------------------------- */
 
 
   public function getFields() {
     $fields = array();
 
     $fields[] = self::FIELD_ALWAYS;
     $fields[] = self::FIELD_RULE;
 
     $custom_fields = $this->getCustomFields();
     if ($custom_fields) {
       foreach ($custom_fields->getFields() as $custom_field) {
         $key = $custom_field->getFieldKey();
         $fields[] = $this->getHeraldKeyFromCustomKey($key);
       }
     }
 
     return $fields;
   }
 
   public function getFieldNameMap() {
     return array(
       self::FIELD_TITLE => pht('Title'),
       self::FIELD_BODY => pht('Body'),
       self::FIELD_AUTHOR => pht('Author'),
       self::FIELD_ASSIGNEE => pht('Assignee'),
       self::FIELD_COMMITTER => pht('Committer'),
       self::FIELD_REVIEWER => pht('Reviewer'),
       self::FIELD_REVIEWERS => pht('Reviewers'),
       self::FIELD_CC => pht('CCs'),
       self::FIELD_TAGS => pht('Tags'),
       self::FIELD_DIFF_FILE => pht('Any changed filename'),
       self::FIELD_DIFF_CONTENT => pht('Any changed file content'),
       self::FIELD_DIFF_ADDED_CONTENT => pht('Any added file content'),
       self::FIELD_DIFF_REMOVED_CONTENT => pht('Any removed file content'),
       self::FIELD_DIFF_ENORMOUS => pht('Change is enormous'),
       self::FIELD_REPOSITORY => pht('Repository'),
       self::FIELD_REPOSITORY_PROJECTS => pht('Repository\'s projects'),
       self::FIELD_RULE => pht('Another Herald rule'),
       self::FIELD_AFFECTED_PACKAGE => pht('Any affected package'),
       self::FIELD_AFFECTED_PACKAGE_OWNER =>
         pht("Any affected package's owner"),
       self::FIELD_CONTENT_SOURCE => pht('Content Source'),
       self::FIELD_ALWAYS => pht('Always'),
       self::FIELD_AUTHOR_PROJECTS => pht("Author's projects"),
       self::FIELD_PROJECTS => pht('Projects'),
       self::FIELD_PUSHER => pht('Pusher'),
       self::FIELD_PUSHER_PROJECTS => pht("Pusher's projects"),
       self::FIELD_DIFFERENTIAL_REVISION => pht('Differential revision'),
       self::FIELD_DIFFERENTIAL_REVIEWERS => pht('Differential reviewers'),
       self::FIELD_DIFFERENTIAL_CCS => pht('Differential CCs'),
       self::FIELD_DIFFERENTIAL_ACCEPTED
         => pht('Accepted Differential revision'),
       self::FIELD_IS_MERGE_COMMIT => pht('Commit is a merge'),
       self::FIELD_BRANCHES => pht('Commit\'s branches'),
       self::FIELD_AUTHOR_RAW => pht('Raw author name'),
       self::FIELD_COMMITTER_RAW => pht('Raw committer name'),
       self::FIELD_IS_NEW_OBJECT => pht('Is newly created?'),
       self::FIELD_APPLICATION_EMAIL => pht('Receiving email address'),
       self::FIELD_TASK_PRIORITY => pht('Task priority'),
       self::FIELD_TASK_STATUS => pht('Task status'),
       self::FIELD_ARCANIST_PROJECT => pht('Arcanist Project'),
       self::FIELD_PUSHER_IS_COMMITTER => pht('Pusher same as committer'),
       self::FIELD_PATH => pht('Path'),
     ) + $this->getCustomFieldNameMap();
   }
 
 
 /* -(  Conditions  )--------------------------------------------------------- */
 
 
   public function getConditionNameMap() {
     return array(
       self::CONDITION_CONTAINS        => pht('contains'),
       self::CONDITION_NOT_CONTAINS    => pht('does not contain'),
       self::CONDITION_IS              => pht('is'),
       self::CONDITION_IS_NOT          => pht('is not'),
       self::CONDITION_IS_ANY          => pht('is any of'),
       self::CONDITION_IS_TRUE         => pht('is true'),
       self::CONDITION_IS_FALSE        => pht('is false'),
       self::CONDITION_IS_NOT_ANY      => pht('is not any of'),
       self::CONDITION_INCLUDE_ALL     => pht('include all of'),
       self::CONDITION_INCLUDE_ANY     => pht('include any of'),
       self::CONDITION_INCLUDE_NONE    => pht('do not include'),
       self::CONDITION_IS_ME           => pht('is myself'),
       self::CONDITION_IS_NOT_ME       => pht('is not myself'),
       self::CONDITION_REGEXP          => pht('matches regexp'),
       self::CONDITION_RULE            => pht('matches:'),
       self::CONDITION_NOT_RULE        => pht('does not match:'),
       self::CONDITION_EXISTS          => pht('exists'),
       self::CONDITION_NOT_EXISTS      => pht('does not exist'),
       self::CONDITION_UNCONDITIONALLY => '',  // don't show anything!
       self::CONDITION_NEVER           => '',  // don't show anything!
       self::CONDITION_REGEXP_PAIR     => pht('matches regexp pair'),
       self::CONDITION_HAS_BIT         => pht('has bit'),
       self::CONDITION_NOT_BIT         => pht('lacks bit'),
     );
   }
 
   public function getConditionsForField($field) {
     switch ($field) {
       case self::FIELD_TITLE:
       case self::FIELD_BODY:
       case self::FIELD_COMMITTER_RAW:
       case self::FIELD_AUTHOR_RAW:
       case self::FIELD_PATH:
         return array(
           self::CONDITION_CONTAINS,
           self::CONDITION_NOT_CONTAINS,
           self::CONDITION_IS,
           self::CONDITION_IS_NOT,
           self::CONDITION_REGEXP,
         );
       case self::FIELD_REVIEWER:
       case self::FIELD_PUSHER:
       case self::FIELD_TASK_PRIORITY:
       case self::FIELD_TASK_STATUS:
       case self::FIELD_ARCANIST_PROJECT:
         return array(
           self::CONDITION_IS_ANY,
           self::CONDITION_IS_NOT_ANY,
         );
       case self::FIELD_REPOSITORY:
       case self::FIELD_ASSIGNEE:
       case self::FIELD_AUTHOR:
       case self::FIELD_COMMITTER:
         return array(
           self::CONDITION_IS_ANY,
           self::CONDITION_IS_NOT_ANY,
           self::CONDITION_EXISTS,
           self::CONDITION_NOT_EXISTS,
         );
       case self::FIELD_TAGS:
       case self::FIELD_REVIEWERS:
       case self::FIELD_CC:
       case self::FIELD_AUTHOR_PROJECTS:
       case self::FIELD_PROJECTS:
       case self::FIELD_AFFECTED_PACKAGE:
       case self::FIELD_AFFECTED_PACKAGE_OWNER:
       case self::FIELD_PUSHER_PROJECTS:
       case self::FIELD_REPOSITORY_PROJECTS:
         return array(
           self::CONDITION_INCLUDE_ALL,
           self::CONDITION_INCLUDE_ANY,
           self::CONDITION_INCLUDE_NONE,
           self::CONDITION_EXISTS,
           self::CONDITION_NOT_EXISTS,
         );
       case self::FIELD_APPLICATION_EMAIL:
         return array(
           self::CONDITION_INCLUDE_ANY,
           self::CONDITION_INCLUDE_NONE,
           self::CONDITION_EXISTS,
           self::CONDITION_NOT_EXISTS,
         );
       case self::FIELD_DIFF_FILE:
       case self::FIELD_BRANCHES:
         return array(
           self::CONDITION_CONTAINS,
           self::CONDITION_REGEXP,
         );
       case self::FIELD_DIFF_CONTENT:
       case self::FIELD_DIFF_ADDED_CONTENT:
       case self::FIELD_DIFF_REMOVED_CONTENT:
         return array(
           self::CONDITION_CONTAINS,
           self::CONDITION_REGEXP,
           self::CONDITION_REGEXP_PAIR,
         );
       case self::FIELD_RULE:
         return array(
           self::CONDITION_RULE,
           self::CONDITION_NOT_RULE,
         );
       case self::FIELD_CONTENT_SOURCE:
         return array(
           self::CONDITION_IS,
           self::CONDITION_IS_NOT,
         );
       case self::FIELD_ALWAYS:
         return array(
           self::CONDITION_UNCONDITIONALLY,
         );
       case self::FIELD_DIFFERENTIAL_REVIEWERS:
         return array(
           self::CONDITION_EXISTS,
           self::CONDITION_NOT_EXISTS,
           self::CONDITION_INCLUDE_ALL,
           self::CONDITION_INCLUDE_ANY,
           self::CONDITION_INCLUDE_NONE,
         );
       case self::FIELD_DIFFERENTIAL_CCS:
         return array(
           self::CONDITION_INCLUDE_ALL,
           self::CONDITION_INCLUDE_ANY,
           self::CONDITION_INCLUDE_NONE,
         );
       case self::FIELD_DIFFERENTIAL_REVISION:
       case self::FIELD_DIFFERENTIAL_ACCEPTED:
         return array(
           self::CONDITION_EXISTS,
           self::CONDITION_NOT_EXISTS,
         );
       case self::FIELD_IS_MERGE_COMMIT:
       case self::FIELD_DIFF_ENORMOUS:
       case self::FIELD_IS_NEW_OBJECT:
       case self::FIELD_PUSHER_IS_COMMITTER:
         return array(
           self::CONDITION_IS_TRUE,
           self::CONDITION_IS_FALSE,
         );
       default:
         if ($this->isHeraldCustomKey($field)) {
           return $this->getCustomFieldConditions($field);
         }
         throw new Exception(
           "This adapter does not define conditions for field '{$field}'!");
     }
   }
 
   public function doesConditionMatch(
     HeraldEngine $engine,
     HeraldRule $rule,
     HeraldCondition $condition,
     $field_value) {
 
     $condition_type = $condition->getFieldCondition();
     $condition_value = $condition->getValue();
 
     switch ($condition_type) {
       case self::CONDITION_CONTAINS:
         // "Contains" can take an array of strings, as in "Any changed
         // filename" for diffs.
         foreach ((array)$field_value as $value) {
           if (stripos($value, $condition_value) !== false) {
             return true;
           }
         }
         return false;
       case self::CONDITION_NOT_CONTAINS:
         return (stripos($field_value, $condition_value) === false);
       case self::CONDITION_IS:
         return ($field_value == $condition_value);
       case self::CONDITION_IS_NOT:
         return ($field_value != $condition_value);
       case self::CONDITION_IS_ME:
         return ($field_value == $rule->getAuthorPHID());
       case self::CONDITION_IS_NOT_ME:
         return ($field_value != $rule->getAuthorPHID());
       case self::CONDITION_IS_ANY:
         if (!is_array($condition_value)) {
           throw new HeraldInvalidConditionException(
             'Expected condition value to be an array.');
         }
         $condition_value = array_fuse($condition_value);
         return isset($condition_value[$field_value]);
       case self::CONDITION_IS_NOT_ANY:
         if (!is_array($condition_value)) {
           throw new HeraldInvalidConditionException(
             'Expected condition value to be an array.');
         }
         $condition_value = array_fuse($condition_value);
         return !isset($condition_value[$field_value]);
       case self::CONDITION_INCLUDE_ALL:
         if (!is_array($field_value)) {
           throw new HeraldInvalidConditionException(
             'Object produced non-array value!');
         }
         if (!is_array($condition_value)) {
           throw new HeraldInvalidConditionException(
             'Expected condition value to be an array.');
         }
 
         $have = array_select_keys(array_fuse($field_value), $condition_value);
         return (count($have) == count($condition_value));
       case self::CONDITION_INCLUDE_ANY:
         return (bool)array_select_keys(
           array_fuse($field_value),
           $condition_value);
       case self::CONDITION_INCLUDE_NONE:
         return !array_select_keys(
           array_fuse($field_value),
           $condition_value);
       case self::CONDITION_EXISTS:
       case self::CONDITION_IS_TRUE:
         return (bool)$field_value;
       case self::CONDITION_NOT_EXISTS:
       case self::CONDITION_IS_FALSE:
         return !$field_value;
       case self::CONDITION_UNCONDITIONALLY:
         return (bool)$field_value;
       case self::CONDITION_NEVER:
         return false;
       case self::CONDITION_REGEXP:
         foreach ((array)$field_value as $value) {
           // We add the 'S' flag because we use the regexp multiple times.
           // It shouldn't cause any troubles if the flag is already there
           // - /.*/S is evaluated same as /.*/SS.
           $result = @preg_match($condition_value.'S', $value);
           if ($result === false) {
             throw new HeraldInvalidConditionException(
               'Regular expression is not valid!');
           }
           if ($result) {
             return true;
           }
         }
         return false;
       case self::CONDITION_REGEXP_PAIR:
         // Match a JSON-encoded pair of regular expressions against a
         // dictionary. The first regexp must match the dictionary key, and the
         // second regexp must match the dictionary value. If any key/value pair
         // in the dictionary matches both regexps, the condition is satisfied.
         $regexp_pair = null;
         try {
           $regexp_pair = phutil_json_decode($condition_value);
         } catch (PhutilJSONParserException $ex) {
           throw new HeraldInvalidConditionException(
             pht('Regular expression pair is not valid JSON!'));
         }
         if (count($regexp_pair) != 2) {
           throw new HeraldInvalidConditionException(
             pht('Regular expression pair is not a pair!'));
         }
 
         $key_regexp   = array_shift($regexp_pair);
         $value_regexp = array_shift($regexp_pair);
 
         foreach ((array)$field_value as $key => $value) {
           $key_matches = @preg_match($key_regexp, $key);
           if ($key_matches === false) {
             throw new HeraldInvalidConditionException(
               'First regular expression is invalid!');
           }
           if ($key_matches) {
             $value_matches = @preg_match($value_regexp, $value);
             if ($value_matches === false) {
               throw new HeraldInvalidConditionException(
                 'Second regular expression is invalid!');
             }
             if ($value_matches) {
               return true;
             }
           }
         }
         return false;
       case self::CONDITION_RULE:
       case self::CONDITION_NOT_RULE:
         $rule = $engine->getRule($condition_value);
         if (!$rule) {
           throw new HeraldInvalidConditionException(
             'Condition references a rule which does not exist!');
         }
 
         $is_not = ($condition_type == self::CONDITION_NOT_RULE);
         $result = $engine->doesRuleMatch($rule, $this);
         if ($is_not) {
           $result = !$result;
         }
         return $result;
       case self::CONDITION_HAS_BIT:
         return (($condition_value & $field_value) === (int) $condition_value);
       case self::CONDITION_NOT_BIT:
         return (($condition_value & $field_value) !== (int) $condition_value);
       default:
         throw new HeraldInvalidConditionException(
           "Unknown condition '{$condition_type}'.");
     }
   }
 
   public function willSaveCondition(HeraldCondition $condition) {
     $condition_type = $condition->getFieldCondition();
     $condition_value = $condition->getValue();
 
     switch ($condition_type) {
       case self::CONDITION_REGEXP:
         $ok = @preg_match($condition_value, '');
         if ($ok === false) {
           throw new HeraldInvalidConditionException(
             pht(
               'The regular expression "%s" is not valid. Regular expressions '.
               'must have enclosing characters (e.g. "@/path/to/file@", not '.
               '"/path/to/file") and be syntactically correct.',
               $condition_value));
         }
         break;
       case self::CONDITION_REGEXP_PAIR:
         $json = null;
         try {
           $json = phutil_json_decode($condition_value);
         } catch (PhutilJSONParserException $ex) {
           throw new HeraldInvalidConditionException(
             pht(
               'The regular expression pair "%s" is not valid JSON. Enter a '.
               'valid JSON array with two elements.',
               $condition_value));
         }
 
         if (count($json) != 2) {
           throw new HeraldInvalidConditionException(
             pht(
               'The regular expression pair "%s" must have exactly two '.
               'elements.',
               $condition_value));
         }
 
         $key_regexp = array_shift($json);
         $val_regexp = array_shift($json);
 
         $key_ok = @preg_match($key_regexp, '');
         if ($key_ok === false) {
           throw new HeraldInvalidConditionException(
             pht(
               'The first regexp in the regexp pair, "%s", is not a valid '.
               'regexp.',
               $key_regexp));
         }
 
         $val_ok = @preg_match($val_regexp, '');
         if ($val_ok === false) {
           throw new HeraldInvalidConditionException(
             pht(
               'The second regexp in the regexp pair, "%s", is not a valid '.
               'regexp.',
               $val_regexp));
         }
         break;
       case self::CONDITION_CONTAINS:
       case self::CONDITION_NOT_CONTAINS:
       case self::CONDITION_IS:
       case self::CONDITION_IS_NOT:
       case self::CONDITION_IS_ANY:
       case self::CONDITION_IS_NOT_ANY:
       case self::CONDITION_INCLUDE_ALL:
       case self::CONDITION_INCLUDE_ANY:
       case self::CONDITION_INCLUDE_NONE:
       case self::CONDITION_IS_ME:
       case self::CONDITION_IS_NOT_ME:
       case self::CONDITION_RULE:
       case self::CONDITION_NOT_RULE:
       case self::CONDITION_EXISTS:
       case self::CONDITION_NOT_EXISTS:
       case self::CONDITION_UNCONDITIONALLY:
       case self::CONDITION_NEVER:
       case self::CONDITION_HAS_BIT:
       case self::CONDITION_NOT_BIT:
       case self::CONDITION_IS_TRUE:
       case self::CONDITION_IS_FALSE:
         // No explicit validation for these types, although there probably
         // should be in some cases.
         break;
       default:
         throw new HeraldInvalidConditionException(
           pht(
             'Unknown condition "%s"!',
             $condition_type));
     }
   }
 
 
 /* -(  Actions  )------------------------------------------------------------ */
 
   public function getCustomActionsForRuleType($rule_type) {
     $results = array();
     foreach ($this->getCustomActions() as $custom_action) {
       if ($custom_action->appliesToRuleType($rule_type)) {
         $results[] = $custom_action;
       }
     }
     return $results;
   }
 
   public function getActions($rule_type) {
     $custom_actions = $this->getCustomActionsForRuleType($rule_type);
     $custom_actions = mpull($custom_actions, 'getActionKey');
 
     $actions = $custom_actions;
 
     $object = $this->newObject();
 
     if (($object instanceof PhabricatorProjectInterface)) {
       if ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_GLOBAL) {
         $actions[] = self::ACTION_ADD_PROJECTS;
         $actions[] = self::ACTION_REMOVE_PROJECTS;
       }
     }
 
     return $actions;
   }
 
   public function getActionNameMap($rule_type) {
     switch ($rule_type) {
       case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
       case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
         $standard = array(
           self::ACTION_NOTHING      => pht('Do nothing'),
           self::ACTION_ADD_CC       => pht('Add emails to CC'),
           self::ACTION_REMOVE_CC    => pht('Remove emails from CC'),
           self::ACTION_EMAIL        => pht('Send an email to'),
           self::ACTION_AUDIT        => pht('Trigger an Audit by'),
           self::ACTION_FLAG         => pht('Mark with flag'),
           self::ACTION_ASSIGN_TASK  => pht('Assign task to'),
           self::ACTION_ADD_PROJECTS => pht('Add projects'),
           self::ACTION_REMOVE_PROJECTS => pht('Remove projects'),
           self::ACTION_ADD_REVIEWERS => pht('Add reviewers'),
           self::ACTION_ADD_BLOCKING_REVIEWERS => pht('Add blocking reviewers'),
           self::ACTION_APPLY_BUILD_PLANS => pht('Run build plans'),
           self::ACTION_REQUIRE_SIGNATURE => pht('Require legal signatures'),
           self::ACTION_BLOCK => pht('Block change with message'),
         );
         break;
       case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
         $standard = array(
           self::ACTION_NOTHING      => pht('Do nothing'),
           self::ACTION_ADD_CC       => pht('Add me to CC'),
           self::ACTION_REMOVE_CC    => pht('Remove me from CC'),
           self::ACTION_EMAIL        => pht('Send me an email'),
           self::ACTION_AUDIT        => pht('Trigger an Audit by me'),
           self::ACTION_FLAG         => pht('Mark with flag'),
           self::ACTION_ASSIGN_TASK  => pht('Assign task to me'),
           self::ACTION_ADD_REVIEWERS => pht('Add me as a reviewer'),
           self::ACTION_ADD_BLOCKING_REVIEWERS =>
             pht('Add me as a blocking reviewer'),
         );
         break;
       default:
         throw new Exception("Unknown rule type '{$rule_type}'!");
     }
 
     $custom_actions = $this->getCustomActionsForRuleType($rule_type);
     $standard += mpull($custom_actions, 'getActionName', 'getActionKey');
 
     return $standard;
   }
 
   public function willSaveAction(
     HeraldRule $rule,
     HeraldAction $action) {
 
     $target = $action->getTarget();
     if (is_array($target)) {
       $target = array_keys($target);
     }
 
     $author_phid = $rule->getAuthorPHID();
 
     $rule_type = $rule->getRuleType();
     if ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) {
       switch ($action->getAction()) {
         case self::ACTION_EMAIL:
         case self::ACTION_ADD_CC:
         case self::ACTION_REMOVE_CC:
         case self::ACTION_AUDIT:
         case self::ACTION_ASSIGN_TASK:
         case self::ACTION_ADD_REVIEWERS:
         case self::ACTION_ADD_BLOCKING_REVIEWERS:
           // For personal rules, force these actions to target the rule owner.
           $target = array($author_phid);
           break;
         case self::ACTION_FLAG:
           // Make sure flag color is valid; set to blue if not.
           $color_map = PhabricatorFlagColor::getColorNameMap();
           if (empty($color_map[$target])) {
             $target = PhabricatorFlagColor::COLOR_BLUE;
           }
           break;
         case self::ACTION_BLOCK:
         case self::ACTION_NOTHING:
           break;
         default:
           throw new HeraldInvalidActionException(
             pht(
               'Unrecognized action type "%s"!',
               $action->getAction()));
       }
     }
 
     $action->setTarget($target);
   }
 
 
 
 /* -(  Values  )------------------------------------------------------------- */
 
 
   public function getValueTypeForFieldAndCondition($field, $condition) {
 
     if ($this->isHeraldCustomKey($field)) {
       $value_type = $this->getCustomFieldValueTypeForFieldAndCondition(
         $field,
         $condition);
       if ($value_type !== null) {
         return $value_type;
       }
     }
 
     switch ($condition) {
       case self::CONDITION_CONTAINS:
       case self::CONDITION_NOT_CONTAINS:
       case self::CONDITION_REGEXP:
       case self::CONDITION_REGEXP_PAIR:
         return self::VALUE_TEXT;
       case self::CONDITION_IS:
       case self::CONDITION_IS_NOT:
         switch ($field) {
           case self::FIELD_CONTENT_SOURCE:
             return self::VALUE_CONTENT_SOURCE;
           default:
             return self::VALUE_TEXT;
         }
         break;
       case self::CONDITION_IS_ANY:
       case self::CONDITION_IS_NOT_ANY:
         switch ($field) {
           case self::FIELD_REPOSITORY:
             return self::VALUE_REPOSITORY;
           case self::FIELD_TASK_PRIORITY:
             return self::VALUE_TASK_PRIORITY;
           case self::FIELD_TASK_STATUS:
             return self::VALUE_TASK_STATUS;
           case self::FIELD_ARCANIST_PROJECT:
             return self::VALUE_ARCANIST_PROJECT;
           default:
             return self::VALUE_USER;
         }
         break;
       case self::CONDITION_INCLUDE_ALL:
       case self::CONDITION_INCLUDE_ANY:
       case self::CONDITION_INCLUDE_NONE:
         switch ($field) {
           case self::FIELD_REPOSITORY:
             return self::VALUE_REPOSITORY;
           case self::FIELD_CC:
             return self::VALUE_EMAIL;
           case self::FIELD_TAGS:
             return self::VALUE_TAG;
           case self::FIELD_AFFECTED_PACKAGE:
             return self::VALUE_OWNERS_PACKAGE;
           case self::FIELD_AUTHOR_PROJECTS:
           case self::FIELD_PUSHER_PROJECTS:
           case self::FIELD_PROJECTS:
           case self::FIELD_REPOSITORY_PROJECTS:
             return self::VALUE_PROJECT;
           case self::FIELD_REVIEWERS:
             return self::VALUE_USER_OR_PROJECT;
           case self::FIELD_APPLICATION_EMAIL:
             return self::VALUE_APPLICATION_EMAIL;
           default:
             return self::VALUE_USER;
         }
         break;
       case self::CONDITION_IS_ME:
       case self::CONDITION_IS_NOT_ME:
       case self::CONDITION_EXISTS:
       case self::CONDITION_NOT_EXISTS:
       case self::CONDITION_UNCONDITIONALLY:
       case self::CONDITION_NEVER:
       case self::CONDITION_IS_TRUE:
       case self::CONDITION_IS_FALSE:
         return self::VALUE_NONE;
       case self::CONDITION_RULE:
       case self::CONDITION_NOT_RULE:
         return self::VALUE_RULE;
       default:
         throw new Exception("Unknown condition '{$condition}'.");
     }
   }
 
   public function getValueTypeForAction($action, $rule_type) {
     $is_personal = ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
 
     if ($is_personal) {
       switch ($action) {
         case self::ACTION_ADD_CC:
         case self::ACTION_REMOVE_CC:
         case self::ACTION_EMAIL:
         case self::ACTION_NOTHING:
         case self::ACTION_AUDIT:
         case self::ACTION_ASSIGN_TASK:
         case self::ACTION_ADD_REVIEWERS:
         case self::ACTION_ADD_BLOCKING_REVIEWERS:
           return self::VALUE_NONE;
         case self::ACTION_FLAG:
           return self::VALUE_FLAG_COLOR;
         case self::ACTION_ADD_PROJECTS:
         case self::ACTION_REMOVE_PROJECTS:
           return self::VALUE_PROJECT;
       }
     } else {
       switch ($action) {
         case self::ACTION_ADD_CC:
         case self::ACTION_REMOVE_CC:
         case self::ACTION_EMAIL:
           return self::VALUE_EMAIL;
         case self::ACTION_NOTHING:
           return self::VALUE_NONE;
         case self::ACTION_ADD_PROJECTS:
         case self::ACTION_REMOVE_PROJECTS:
           return self::VALUE_PROJECT;
         case self::ACTION_FLAG:
           return self::VALUE_FLAG_COLOR;
         case self::ACTION_ASSIGN_TASK:
           return self::VALUE_USER;
         case self::ACTION_AUDIT:
         case self::ACTION_ADD_REVIEWERS:
         case self::ACTION_ADD_BLOCKING_REVIEWERS:
           return self::VALUE_USER_OR_PROJECT;
         case self::ACTION_APPLY_BUILD_PLANS:
           return self::VALUE_BUILD_PLAN;
         case self::ACTION_REQUIRE_SIGNATURE:
           return self::VALUE_LEGAL_DOCUMENTS;
         case self::ACTION_BLOCK:
           return self::VALUE_TEXT;
       }
     }
 
     $custom_action = idx($this->getCustomActions(), $action);
     if ($custom_action !== null) {
       return $custom_action->getActionType();
     }
 
     throw new Exception("Unknown or invalid action '".$action."'.");
   }
 
 
 /* -(  Repetition  )--------------------------------------------------------- */
 
 
   public function getRepetitionOptions() {
     return array(
       HeraldRepetitionPolicyConfig::EVERY,
     );
   }
 
   public static function getAllAdapters() {
     static $adapters;
     if (!$adapters) {
       $adapters = id(new PhutilSymbolLoader())
         ->setAncestorClass(__CLASS__)
         ->loadObjects();
       $adapters = msort($adapters, 'getAdapterSortKey');
     }
     return $adapters;
   }
 
   public static function getAdapterForContentType($content_type) {
     $adapters = self::getAllAdapters();
 
     foreach ($adapters as $adapter) {
       if ($adapter->getAdapterContentType() == $content_type) {
         return $adapter;
       }
     }
 
     throw new Exception(
       pht(
         'No adapter exists for Herald content type "%s".',
         $content_type));
   }
 
   public static function getEnabledAdapterMap(PhabricatorUser $viewer) {
     $map = array();
 
-    $adapters = HeraldAdapter::getAllAdapters();
+    $adapters = self::getAllAdapters();
     foreach ($adapters as $adapter) {
       if (!$adapter->isAvailableToUser($viewer)) {
         continue;
       }
       $type = $adapter->getAdapterContentType();
       $name = $adapter->getAdapterContentName();
       $map[$type] = $name;
     }
 
     return $map;
   }
 
   public function renderRuleAsText(
     HeraldRule $rule,
     PhabricatorHandleList $handles) {
 
     require_celerity_resource('herald-css');
 
     $icon = id(new PHUIIconView())
       ->setIconFont('fa-chevron-circle-right lightgreytext')
       ->addClass('herald-list-icon');
 
     if ($rule->getMustMatchAll()) {
       $match_text = pht('When all of these conditions are met:');
     } else {
       $match_text = pht('When any of these conditions are met:');
     }
 
     $match_title = phutil_tag(
       'p',
       array(
         'class' => 'herald-list-description',
       ),
       $match_text);
 
     $match_list = array();
     foreach ($rule->getConditions() as $condition) {
       $match_list[] = phutil_tag(
         'div',
         array(
           'class' => 'herald-list-item',
         ),
         array(
           $icon,
           $this->renderConditionAsText($condition, $handles),
         ));
     }
 
     $integer_code_for_every = HeraldRepetitionPolicyConfig::toInt(
       HeraldRepetitionPolicyConfig::EVERY);
 
     if ($rule->getRepetitionPolicy() == $integer_code_for_every) {
       $action_text =
         pht('Take these actions every time this rule matches:');
     } else {
       $action_text =
         pht('Take these actions the first time this rule matches:');
     }
 
     $action_title = phutil_tag(
       'p',
       array(
         'class' => 'herald-list-description',
       ),
       $action_text);
 
     $action_list = array();
     foreach ($rule->getActions() as $action) {
       $action_list[] = phutil_tag(
         'div',
         array(
           'class' => 'herald-list-item',
         ),
         array(
           $icon,
           $this->renderActionAsText($action, $handles),
         ));
     }
 
     return array(
       $match_title,
       $match_list,
       $action_title,
       $action_list,
     );
   }
 
   private function renderConditionAsText(
     HeraldCondition $condition,
     PhabricatorHandleList $handles) {
 
     $field_type = $condition->getFieldName();
 
     $default = $this->isHeraldCustomKey($field_type)
       ? pht('(Unknown Custom Field "%s")', $field_type)
       : pht('(Unknown Field "%s")', $field_type);
 
     $field_name = idx($this->getFieldNameMap(), $field_type, $default);
 
     $condition_type = $condition->getFieldCondition();
     $condition_name = idx($this->getConditionNameMap(), $condition_type);
 
     $value = $this->renderConditionValueAsText($condition, $handles);
 
     return hsprintf('    %s %s %s', $field_name, $condition_name, $value);
   }
 
   private function renderActionAsText(
     HeraldAction $action,
     PhabricatorHandleList $handles) {
     $rule_global = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL;
 
     $action_type = $action->getAction();
     $action_name = idx($this->getActionNameMap($rule_global), $action_type);
 
     $target = $this->renderActionTargetAsText($action, $handles);
 
     return hsprintf('    %s %s', $action_name, $target);
   }
 
   private function renderConditionValueAsText(
     HeraldCondition $condition,
     PhabricatorHandleList $handles) {
 
     $value = $condition->getValue();
     if (!is_array($value)) {
       $value = array($value);
     }
     switch ($condition->getFieldName()) {
       case self::FIELD_TASK_PRIORITY:
         $priority_map = ManiphestTaskPriority::getTaskPriorityMap();
         foreach ($value as $index => $val) {
           $name = idx($priority_map, $val);
           if ($name) {
             $value[$index] = $name;
           }
         }
         break;
       case self::FIELD_TASK_STATUS:
         $status_map = ManiphestTaskStatus::getTaskStatusMap();
         foreach ($value as $index => $val) {
           $name = idx($status_map, $val);
           if ($name) {
             $value[$index] = $name;
           }
         }
         break;
       case HeraldPreCommitRefAdapter::FIELD_REF_CHANGE:
         $change_map =
           PhabricatorRepositoryPushLog::getHeraldChangeFlagConditionOptions();
         foreach ($value as $index => $val) {
           $name = idx($change_map, $val);
           if ($name) {
             $value[$index] = $name;
           }
         }
         break;
       default:
         foreach ($value as $index => $val) {
           $handle = $handles->getHandleIfExists($val);
           if ($handle) {
             $value[$index] = $handle->renderLink();
           }
         }
         break;
     }
     $value = phutil_implode_html(', ', $value);
     return $value;
   }
 
   private function renderActionTargetAsText(
     HeraldAction $action,
     PhabricatorHandleList $handles) {
 
     $target = $action->getTarget();
     if (!is_array($target)) {
       $target = array($target);
     }
     foreach ($target as $index => $val) {
       switch ($action->getAction()) {
         case self::ACTION_FLAG:
           $target[$index] = PhabricatorFlagColor::getColorName($val);
           break;
         default:
           $handle = $handles->getHandleIfExists($val);
           if ($handle) {
             $target[$index] = $handle->renderLink();
           }
           break;
       }
     }
     $target = phutil_implode_html(', ', $target);
     return $target;
   }
 
   /**
    * Given a @{class:HeraldRule}, this function extracts all the phids that
    * we'll want to load as handles later.
    *
    * This function performs a somewhat hacky approach to figuring out what
    * is and is not a phid - try to get the phid type and if the type is
    * *not* unknown assume its a valid phid.
    *
    * Don't try this at home. Use more strongly typed data at home.
    *
    * Think of the children.
    */
   public static function getHandlePHIDs(HeraldRule $rule) {
     $phids = array($rule->getAuthorPHID());
     foreach ($rule->getConditions() as $condition) {
       $value = $condition->getValue();
       if (!is_array($value)) {
         $value = array($value);
       }
       foreach ($value as $val) {
         if (phid_get_type($val) !=
             PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
           $phids[] = $val;
         }
       }
     }
 
     foreach ($rule->getActions() as $action) {
       $target = $action->getTarget();
       if (!is_array($target)) {
         $target = array($target);
       }
       foreach ($target as $val) {
         if (phid_get_type($val) !=
             PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
           $phids[] = $val;
         }
       }
     }
 
     if ($rule->isObjectRule()) {
       $phids[] = $rule->getTriggerObjectPHID();
     }
 
     return $phids;
   }
 
 /* -(  Custom Field Integration  )------------------------------------------- */
 
 
   /**
    * Returns the prefix used to namespace Herald fields which are based on
    * custom fields.
    *
    * @return string Key prefix.
    * @task customfield
    */
   private function getCustomKeyPrefix() {
     return 'herald.custom/';
   }
 
 
   /**
    * Determine if a field key is based on a custom field or a regular internal
    * field.
    *
    * @param string Field key.
    * @return bool True if the field key is based on a custom field.
    * @task customfield
    */
   private function isHeraldCustomKey($key) {
     $prefix = $this->getCustomKeyPrefix();
     return (strncmp($key, $prefix, strlen($prefix)) == 0);
   }
 
 
   /**
    * Convert a custom field key into a Herald field key.
    *
    * @param string Custom field key.
    * @return string Herald field key.
    * @task customfield
    */
   private function getHeraldKeyFromCustomKey($key) {
     return $this->getCustomKeyPrefix().$key;
   }
 
 
   /**
    * Get custom fields for this adapter, if appliable. This will either return
    * a field list or `null` if the adapted object does not implement custom
    * fields or the adapter does not support them.
    *
    * @return PhabricatorCustomFieldList|null List of fields, or `null`.
    * @task customfield
    */
   private function getCustomFields() {
     if ($this->customFields === false) {
       $this->customFields = null;
 
 
       $template_object = $this->newObject();
       if ($template_object instanceof PhabricatorCustomFieldInterface) {
         $object = $this->getObject();
         if (!$object) {
           $object = $template_object;
         }
 
         $fields = PhabricatorCustomField::getObjectFields(
           $object,
           PhabricatorCustomField::ROLE_HERALD);
         $fields->setViewer(PhabricatorUser::getOmnipotentUser());
         $fields->readFieldsFromStorage($object);
 
         $this->customFields = $fields;
       }
     }
 
     return $this->customFields;
   }
 
 
   /**
    * Get a custom field by Herald field key, or `null` if it does not exist
    * or custom fields are not supported.
    *
    * @param string Herald field key.
    * @return PhabricatorCustomField|null Matching field, if it exists.
    * @task customfield
    */
   private function getCustomField($herald_field_key) {
     $fields = $this->getCustomFields();
     if (!$fields) {
       return null;
     }
 
     foreach ($fields->getFields() as $custom_field) {
       $key = $custom_field->getFieldKey();
       if ($this->getHeraldKeyFromCustomKey($key) == $herald_field_key) {
         return $custom_field;
       }
     }
 
     return null;
   }
 
 
   /**
    * Get the field map for custom fields.
    *
    * @return map<string, string> Map of Herald field keys to field names.
    * @task customfield
    */
   private function getCustomFieldNameMap() {
     $fields = $this->getCustomFields();
     if (!$fields) {
       return array();
     }
 
     $map = array();
     foreach ($fields->getFields() as $field) {
       $key = $field->getFieldKey();
       $name = $field->getHeraldFieldName();
       $map[$this->getHeraldKeyFromCustomKey($key)] = $name;
     }
 
     return $map;
   }
 
 
   /**
    * Get the value for a custom field.
    *
    * @param string Herald field key.
    * @return wild Custom field value.
    * @task customfield
    */
   private function getCustomFieldValue($field_key) {
     $field = $this->getCustomField($field_key);
     if (!$field) {
       return null;
     }
 
     return $field->getHeraldFieldValue();
   }
 
 
   /**
    * Get the Herald conditions for a custom field.
    *
    * @param string Herald field key.
    * @return list<const> List of Herald conditions.
    * @task customfield
    */
   private function getCustomFieldConditions($field_key) {
     $field = $this->getCustomField($field_key);
     if (!$field) {
       return array(
         self::CONDITION_NEVER,
       );
     }
 
     return $field->getHeraldFieldConditions();
   }
 
 
   /**
    * Get the Herald value type for a custom field and condition.
    *
    * @param string Herald field key.
    * @param const Herald condition constant.
    * @return const|null Herald value type constant, or null to use the default.
    * @task customfield
    */
   private function getCustomFieldValueTypeForFieldAndCondition(
     $field_key,
     $condition) {
 
     $field = $this->getCustomField($field_key);
     if (!$field) {
       return self::VALUE_NONE;
     }
 
     return $field->getHeraldFieldValueType($condition);
   }
 
 
 /* -(  Applying Effects  )--------------------------------------------------- */
 
 
   /**
    * @task apply
    */
   protected function applyStandardEffect(HeraldEffect $effect) {
     $action = $effect->getAction();
 
     $rule_type = $effect->getRule()->getRuleType();
     $supported = $this->getActions($rule_type);
     $supported = array_fuse($supported);
     if (empty($supported[$action])) {
       throw new Exception(
         pht(
           'Adapter "%s" does not support action "%s" for rule type "%s".',
           get_class($this),
           $action,
           $rule_type));
     }
 
     switch ($action) {
       case self::ACTION_ADD_PROJECTS:
       case self::ACTION_REMOVE_PROJECTS:
         return $this->applyProjectsEffect($effect);
       case self::ACTION_FLAG:
         return $this->applyFlagEffect($effect);
       case self::ACTION_EMAIL:
         return $this->applyEmailEffect($effect);
       default:
         break;
     }
 
     $result = $this->handleCustomHeraldEffect($effect);
 
     if (!$result) {
       throw new Exception(
         pht(
           'No custom action exists to handle rule action "%s".',
           $action));
     }
 
     return $result;
   }
 
 
   /**
    * @task apply
    */
   private function applyProjectsEffect(HeraldEffect $effect) {
 
     if ($effect->getAction() == self::ACTION_ADD_PROJECTS) {
       $kind = '+';
     } else {
       $kind = '-';
     }
 
     $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
     $project_phids = $effect->getTarget();
     $xaction = $this->newTransaction()
       ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
       ->setMetadataValue('edge:type', $project_type)
       ->setNewValue(
         array(
           $kind => array_fuse($project_phids),
         ));
 
     $this->queueTransaction($xaction);
 
     return new HeraldApplyTranscript(
       $effect,
       true,
       pht('Added projects.'));
   }
 
 
   /**
    * @task apply
    */
   private function applyFlagEffect(HeraldEffect $effect) {
     $phid = $this->getPHID();
     $color = $effect->getTarget();
 
     $rule = $effect->getRule();
     $user = $rule->getAuthor();
 
     $flag = PhabricatorFlagQuery::loadUserFlag($user, $phid);
     if ($flag) {
       return new HeraldApplyTranscript(
         $effect,
         false,
         pht('Object already flagged.'));
     }
 
     $handle = id(new PhabricatorHandleQuery())
       ->setViewer($user)
       ->withPHIDs(array($phid))
       ->executeOne();
 
     $flag = new PhabricatorFlag();
     $flag->setOwnerPHID($user->getPHID());
     $flag->setType($handle->getType());
     $flag->setObjectPHID($handle->getPHID());
 
     // TOOD: Should really be transcript PHID, but it doesn't exist yet.
     $flag->setReasonPHID($user->getPHID());
 
     $flag->setColor($color);
     $flag->setNote(
       pht('Flagged by Herald Rule "%s".', $rule->getName()));
     $flag->save();
 
     return new HeraldApplyTranscript(
       $effect,
       true,
       pht('Added flag.'));
   }
 
 
   /**
    * @task apply
    */
   private function applyEmailEffect(HeraldEffect $effect) {
     foreach ($effect->getTarget() as $phid) {
       $this->emailPHIDs[$phid] = $phid;
 
       // If this is a personal rule, we'll force delivery of a real email. This
       // effect is stronger than notification preferences, so you get an actual
       // email even if your preferences are set to "Notify" or "Ignore".
       $rule = $effect->getRule();
       if ($rule->isPersonalRule()) {
         $this->forcedEmailPHIDs[$phid] = $phid;
       }
     }
 
     return new HeraldApplyTranscript(
       $effect,
       true,
       pht('Added mailable to mail targets.'));
   }
 
 
 }
diff --git a/src/applications/macro/controller/PhabricatorMacroMemeController.php b/src/applications/macro/controller/PhabricatorMacroMemeController.php
index badb1759ad..ff25a8a8a4 100644
--- a/src/applications/macro/controller/PhabricatorMacroMemeController.php
+++ b/src/applications/macro/controller/PhabricatorMacroMemeController.php
@@ -1,69 +1,69 @@
 <?php
 
 final class PhabricatorMacroMemeController
   extends PhabricatorMacroController {
 
   public function shouldAllowPublic() {
     return true;
   }
 
   public function processRequest() {
     $request = $this->getRequest();
     $macro_name = $request->getStr('macro');
     $upper_text = $request->getStr('uppertext');
     $lower_text = $request->getStr('lowertext');
     $user = $request->getUser();
 
-    $uri = PhabricatorMacroMemeController::generateMacro($user, $macro_name,
+    $uri = self::generateMacro($user, $macro_name,
       $upper_text, $lower_text);
     if ($uri === false) {
       return new Aphront404Response();
     }
     return id(new AphrontRedirectResponse())
       ->setIsExternal(true)
       ->setURI($uri);
   }
 
   public static function generateMacro($user, $macro_name, $upper_text,
       $lower_text) {
     $macro = id(new PhabricatorMacroQuery())
       ->setViewer($user)
       ->withNames(array($macro_name))
       ->needFiles(true)
       ->executeOne();
     if (!$macro) {
       return false;
     }
     $file = $macro->getFile();
 
     $upper_text = strtoupper($upper_text);
     $lower_text = strtoupper($lower_text);
     $mixed_text = md5($upper_text).':'.md5($lower_text);
     $hash = 'meme'.hash('sha256', $mixed_text);
     $xform = id(new PhabricatorTransformedFile())
       ->loadOneWhere('originalphid=%s and transform=%s',
         $file->getPHID(), $hash);
 
     if ($xform) {
       $memefile = id(new PhabricatorFileQuery())
         ->setViewer($user)
         ->withPHIDs(array($xform->getTransformedPHID()))
         ->executeOne();
       if ($memefile) {
         return $memefile->getBestURI();
       }
     }
 
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
     $transformers = (new PhabricatorImageTransformer());
     $newfile = $transformers
       ->executeMemeTransform($file, $upper_text, $lower_text);
     $xfile = new PhabricatorTransformedFile();
     $xfile->setOriginalPHID($file->getPHID());
     $xfile->setTransformedPHID($newfile->getPHID());
     $xfile->setTransform($hash);
     $xfile->save();
 
     return $newfile->getBestURI();
   }
 }
diff --git a/src/applications/macro/markup/PhabricatorMemeRemarkupRule.php b/src/applications/macro/markup/PhabricatorMemeRemarkupRule.php
index 0c949e130c..004e415450 100644
--- a/src/applications/macro/markup/PhabricatorMemeRemarkupRule.php
+++ b/src/applications/macro/markup/PhabricatorMemeRemarkupRule.php
@@ -1,65 +1,65 @@
 <?php
 
 final class PhabricatorMemeRemarkupRule extends PhutilRemarkupRule {
 
   private $images;
 
   public function getPriority() {
     return 200.0;
   }
 
   public function apply($text) {
     return preg_replace_callback(
-      '@{meme,((?:[^}\\\\]+|\\\\.)+)}$@m',
+      '@{meme,((?:[^}\\\\]+|\\\\.)+)}@m',
       array($this, 'markupMeme'),
       $text);
   }
 
   public function markupMeme(array $matches) {
     if (!$this->isFlatText($matches[0])) {
       return $matches[0];
     }
 
     $options = array(
       'src' => null,
       'above' => null,
       'below' => null,
     );
 
     $parser = new PhutilSimpleOptions();
     $options = $parser->parse($matches[1]) + $options;
 
     $uri = id(new PhutilURI('/macro/meme/'))
       ->alter('macro', $options['src'])
       ->alter('uppertext', $options['above'])
       ->alter('lowertext', $options['below']);
 
     if ($this->getEngine()->isHTMLMailMode()) {
       $uri = PhabricatorEnv::getProductionURI($uri);
     }
 
     if ($this->getEngine()->isTextMode()) {
       $img =
         ($options['above'] != '' ? "\"{$options['above']}\"\n" : '').
         $options['src'].' <'.PhabricatorEnv::getProductionURI($uri).'>'.
         ($options['below'] != '' ? "\n\"{$options['below']}\"" : '');
     } else {
       $alt_text = pht(
         'Macro %s: %s %s',
         $options['src'],
         $options['above'],
         $options['below']);
 
       $img = $this->newTag(
         'img',
         array(
           'src' => $uri,
           'alt' => $alt_text,
           'class' => 'phabricator-remarkup-macro',
         ));
     }
 
     return $this->getEngine()->storeText($img);
   }
 
 }
diff --git a/src/applications/macro/query/PhabricatorMacroSearchEngine.php b/src/applications/macro/query/PhabricatorMacroSearchEngine.php
index 8a810c886f..8d632e818f 100644
--- a/src/applications/macro/query/PhabricatorMacroSearchEngine.php
+++ b/src/applications/macro/query/PhabricatorMacroSearchEngine.php
@@ -1,225 +1,229 @@
 <?php
 
 final class PhabricatorMacroSearchEngine
   extends PhabricatorApplicationSearchEngine {
 
   public function getResultTypeDescription() {
     return pht('Macros');
   }
 
   public function getApplicationClassName() {
     return 'PhabricatorMacroApplication';
   }
 
   public function buildSavedQueryFromRequest(AphrontRequest $request) {
     $saved = new PhabricatorSavedQuery();
     $saved->setParameter(
       'authorPHIDs',
       $this->readUsersFromRequest($request, 'authors'));
 
     $saved->setParameter('status', $request->getStr('status'));
     $saved->setParameter('names', $request->getStrList('names'));
     $saved->setParameter('nameLike', $request->getStr('nameLike'));
     $saved->setParameter('createdStart', $request->getStr('createdStart'));
     $saved->setParameter('createdEnd', $request->getStr('createdEnd'));
     $saved->setParameter('flagColor', $request->getStr('flagColor', '-1'));
 
     $this->saveQueryOrder($saved, $request);
 
     return $saved;
   }
 
   public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
     $query = id(new PhabricatorMacroQuery())
       ->needFiles(true)
       ->withIDs($saved->getParameter('ids', array()))
       ->withPHIDs($saved->getParameter('phids', array()))
       ->withAuthorPHIDs($saved->getParameter('authorPHIDs', array()));
 
     $this->setQueryOrder($query, $saved);
 
     $status = $saved->getParameter('status');
     $options = PhabricatorMacroQuery::getStatusOptions();
     if (empty($options[$status])) {
       $status = head_key($options);
     }
     $query->withStatus($status);
 
     $names = $saved->getParameter('names', array());
     if ($names) {
       $query->withNames($names);
     }
 
     $like = $saved->getParameter('nameLike');
     if (strlen($like)) {
       $query->withNameLike($like);
     }
 
     $start = $this->parseDateTime($saved->getParameter('createdStart'));
     $end = $this->parseDateTime($saved->getParameter('createdEnd'));
 
     if ($start) {
       $query->withDateCreatedAfter($start);
     }
 
     if ($end) {
       $query->withDateCreatedBefore($end);
     }
 
     $color = $saved->getParameter('flagColor');
     if (strlen($color)) {
       $query->withFlagColor($color);
     }
 
     return $query;
   }
 
   public function buildSearchForm(
     AphrontFormView $form,
     PhabricatorSavedQuery $saved) {
 
     $author_phids = $saved->getParameter('authorPHIDs', array());
     $status = $saved->getParameter('status');
     $names = implode(', ', $saved->getParameter('names', array()));
     $like = $saved->getParameter('nameLike');
     $color = $saved->getParameter('flagColor', '-1');
 
     $form
       ->appendChild(
         id(new AphrontFormSelectControl())
           ->setName('status')
           ->setLabel(pht('Status'))
           ->setOptions(PhabricatorMacroQuery::getStatusOptions())
           ->setValue($status))
       ->appendControl(
         id(new AphrontFormTokenizerControl())
           ->setDatasource(new PhabricatorPeopleDatasource())
           ->setName('authors')
           ->setLabel(pht('Authors'))
           ->setValue($author_phids))
       ->appendChild(
         id(new AphrontFormTextControl())
           ->setName('nameLike')
           ->setLabel(pht('Name Contains'))
           ->setValue($like))
       ->appendChild(
         id(new AphrontFormTextControl())
           ->setName('names')
           ->setLabel(pht('Exact Names'))
           ->setValue($names))
       ->appendChild(
         id(new AphrontFormSelectControl())
           ->setName('flagColor')
           ->setLabel(pht('Marked with Flag'))
           ->setOptions(PhabricatorMacroQuery::getFlagColorsOptions())
           ->setValue($color));
 
     $this->buildDateRange(
       $form,
       $saved,
       'createdStart',
       pht('Created After'),
       'createdEnd',
       pht('Created Before'));
 
     $this->appendOrderFieldsToForm(
       $form,
       $saved,
       new PhabricatorMacroQuery());
   }
 
   protected function getURI($path) {
     return '/macro/'.$path;
   }
 
   protected function getBuiltinQueryNames() {
     $names = array(
       'active'  => pht('Active'),
       'all'     => pht('All'),
     );
 
     if ($this->requireViewer()->isLoggedIn()) {
       $names['authored'] = pht('Authored');
     }
 
     return $names;
   }
 
   public function buildSavedQueryFromBuiltin($query_key) {
     $query = $this->newSavedQuery();
     $query->setQueryKey($query_key);
 
     switch ($query_key) {
       case 'active':
         return $query;
       case 'all':
         return $query->setParameter(
           'status',
           PhabricatorMacroQuery::STATUS_ANY);
       case 'authored':
         return $query->setParameter(
           'authorPHIDs',
           array($this->requireViewer()->getPHID()));
     }
 
     return parent::buildSavedQueryFromBuiltin($query_key);
   }
 
   protected function getRequiredHandlePHIDsForResultList(
     array $macros,
     PhabricatorSavedQuery $query) {
     return mpull($macros, 'getAuthorPHID');
   }
 
   protected function renderResultList(
     array $macros,
     PhabricatorSavedQuery $query,
     array $handles) {
 
     assert_instances_of($macros, 'PhabricatorFileImageMacro');
     $viewer = $this->requireViewer();
 
+    $xform = PhabricatorFileTransform::getTransformByKey(
+      PhabricatorFileThumbnailTransform::TRANSFORM_PINBOARD);
+
     $pinboard = new PHUIPinboardView();
     foreach ($macros as $macro) {
       $file = $macro->getFile();
 
       $item = new PHUIPinboardItemView();
       if ($file) {
-        $item->setImageURI($file->getThumb280x210URI());
-        $item->setImageSize(280, 210);
+        $item->setImageURI($file->getURIForTransform($xform));
+        list($x, $y) = $xform->getTransformedDimensions($file);
+        $item->setImageSize($x, $y);
       }
 
       if ($macro->getDateCreated()) {
         $datetime = phabricator_date($macro->getDateCreated(), $viewer);
         $item->appendChild(
           phutil_tag(
             'div',
             array(),
             pht('Created on %s', $datetime)));
       } else {
         // Very old macros don't have a creation date. Rendering something
         // keeps all the pins at the same height and avoids flow issues.
         $item->appendChild(
           phutil_tag(
             'div',
             array(),
             pht('Created in ages long past')));
       }
 
       if ($macro->getAuthorPHID()) {
         $author_handle = $handles[$macro->getAuthorPHID()];
         $item->appendChild(
           pht('Created by %s', $author_handle->renderLink()));
       }
 
       $item->setURI($this->getApplicationURI('/view/'.$macro->getID().'/'));
       $item->setDisabled($macro->getisDisabled());
       $item->setHeader($macro->getName());
 
       $pinboard->addItem($item);
     }
 
     return $pinboard;
   }
 
 }
diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php
index 60e3425af6..4a6381581b 100644
--- a/src/applications/maniphest/query/ManiphestTaskQuery.php
+++ b/src/applications/maniphest/query/ManiphestTaskQuery.php
@@ -1,862 +1,862 @@
 <?php
 
 /**
  * Query tasks by specific criteria. This class uses the higher-performance
  * but less-general Maniphest indexes to satisfy queries.
  */
 final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
 
   private $taskIDs             = array();
   private $taskPHIDs           = array();
   private $authorPHIDs         = array();
   private $ownerPHIDs          = array();
   private $noOwner;
   private $anyOwner;
   private $subscriberPHIDs     = array();
   private $dateCreatedAfter;
   private $dateCreatedBefore;
   private $dateModifiedAfter;
   private $dateModifiedBefore;
   private $subpriorityMin;
   private $subpriorityMax;
 
   private $fullTextSearch   = '';
 
   private $status           = 'status-any';
   const STATUS_ANY          = 'status-any';
   const STATUS_OPEN         = 'status-open';
   const STATUS_CLOSED       = 'status-closed';
   const STATUS_RESOLVED     = 'status-resolved';
   const STATUS_WONTFIX      = 'status-wontfix';
   const STATUS_INVALID      = 'status-invalid';
   const STATUS_SPITE        = 'status-spite';
   const STATUS_DUPLICATE    = 'status-duplicate';
 
   private $statuses;
   private $priorities;
   private $subpriorities;
 
   private $groupBy          = 'group-none';
   const GROUP_NONE          = 'group-none';
   const GROUP_PRIORITY      = 'group-priority';
   const GROUP_OWNER         = 'group-owner';
   const GROUP_STATUS        = 'group-status';
   const GROUP_PROJECT       = 'group-project';
 
   private $orderBy          = 'order-modified';
   const ORDER_PRIORITY      = 'order-priority';
   const ORDER_CREATED       = 'order-created';
   const ORDER_MODIFIED      = 'order-modified';
   const ORDER_TITLE         = 'order-title';
 
   private $needSubscriberPHIDs;
   private $needProjectPHIDs;
   private $blockingTasks;
   private $blockedTasks;
 
   public function withAuthors(array $authors) {
     $this->authorPHIDs = $authors;
     return $this;
   }
 
   public function withIDs(array $ids) {
     $this->taskIDs = $ids;
     return $this;
   }
 
   public function withPHIDs(array $phids) {
     $this->taskPHIDs = $phids;
     return $this;
   }
 
   public function withOwners(array $owners) {
     $no_owner = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN;
     $any_owner = PhabricatorPeopleAnyOwnerDatasource::FUNCTION_TOKEN;
 
     foreach ($owners as $k => $phid) {
       if ($phid === $no_owner || $phid === null) {
         $this->noOwner = true;
         unset($owners[$k]);
         break;
       }
       if ($phid === $any_owner) {
         $this->anyOwner = true;
         unset($owners[$k]);
         break;
       }
     }
     $this->ownerPHIDs = $owners;
     return $this;
   }
 
   public function withStatus($status) {
     $this->status = $status;
     return $this;
   }
 
   public function withStatuses(array $statuses) {
     $this->statuses = $statuses;
     return $this;
   }
 
   public function withPriorities(array $priorities) {
     $this->priorities = $priorities;
     return $this;
   }
 
   public function withSubpriorities(array $subpriorities) {
     $this->subpriorities = $subpriorities;
     return $this;
   }
 
   public function withSubpriorityBetween($min, $max) {
     $this->subpriorityMin = $min;
     $this->subpriorityMax = $max;
     return $this;
   }
 
   public function withSubscribers(array $subscribers) {
     $this->subscriberPHIDs = $subscribers;
     return $this;
   }
 
   public function withFullTextSearch($fulltext_search) {
     $this->fullTextSearch = $fulltext_search;
     return $this;
   }
 
   public function setGroupBy($group) {
     $this->groupBy = $group;
     return $this;
   }
 
   public function setOrderBy($order) {
     $this->orderBy = $order;
     return $this;
   }
 
   /**
    * True returns tasks that are blocking other tasks only.
    * False returns tasks that are not blocking other tasks only.
    * Null returns tasks regardless of blocking status.
    */
   public function withBlockingTasks($mode) {
     $this->blockingTasks = $mode;
     return $this;
   }
 
   public function shouldJoinBlockingTasks() {
     return $this->blockingTasks !== null;
   }
 
   /**
    * True returns tasks that are blocked by other tasks only.
    * False returns tasks that are not blocked by other tasks only.
    * Null returns tasks regardless of blocked by status.
    */
   public function withBlockedTasks($mode) {
     $this->blockedTasks = $mode;
     return $this;
   }
 
   public function shouldJoinBlockedTasks() {
     return $this->blockedTasks !== null;
   }
 
   public function withDateCreatedBefore($date_created_before) {
     $this->dateCreatedBefore = $date_created_before;
     return $this;
   }
 
   public function withDateCreatedAfter($date_created_after) {
     $this->dateCreatedAfter = $date_created_after;
     return $this;
   }
 
   public function withDateModifiedBefore($date_modified_before) {
     $this->dateModifiedBefore = $date_modified_before;
     return $this;
   }
 
   public function withDateModifiedAfter($date_modified_after) {
     $this->dateModifiedAfter = $date_modified_after;
     return $this;
   }
 
   public function needSubscriberPHIDs($bool) {
     $this->needSubscriberPHIDs = $bool;
     return $this;
   }
 
   public function needProjectPHIDs($bool) {
     $this->needProjectPHIDs = $bool;
     return $this;
   }
 
   protected function newResultObject() {
     return new ManiphestTask();
   }
 
   protected function willExecute() {
     // If we already have an order vector, use it as provided.
     // TODO: This is a messy hack to make setOrderVector() stronger than
     // setPriority().
     $vector = $this->getOrderVector();
     $keys = mpull(iterator_to_array($vector), 'getOrderKey');
     if (array_values($keys) !== array('id')) {
       return;
     }
 
     $parts = array();
     switch ($this->groupBy) {
       case self::GROUP_NONE:
         break;
       case self::GROUP_PRIORITY:
         $parts[] = array('priority');
         break;
       case self::GROUP_OWNER:
         $parts[] = array('owner');
         break;
       case self::GROUP_STATUS:
         $parts[] = array('status');
         break;
       case self::GROUP_PROJECT:
         $parts[] = array('project');
         break;
     }
 
     if ($this->applicationSearchOrders) {
       $columns = array();
       foreach ($this->applicationSearchOrders as $order) {
         $part = 'custom:'.$order['key'];
         if ($order['ascending']) {
           $part = '-'.$part;
         }
         $columns[] = $part;
       }
       $columns[] = 'id';
       $parts[] = $columns;
     } else {
       switch ($this->orderBy) {
         case self::ORDER_PRIORITY:
           $parts[] = array('priority', 'subpriority', 'id');
           break;
         case self::ORDER_CREATED:
           $parts[] = array('id');
           break;
         case self::ORDER_MODIFIED:
           $parts[] = array('updated', 'id');
           break;
         case self::ORDER_TITLE:
           $parts[] = array('title', 'id');
           break;
       }
     }
 
     $parts = array_mergev($parts);
     // We may have a duplicate column if we are both ordering and grouping
     // by priority.
     $parts = array_unique($parts);
     $this->setOrderVector($parts);
   }
 
   protected function loadPage() {
     $task_dao = new ManiphestTask();
     $conn = $task_dao->establishConnection('r');
 
     $where = array();
     $where[] = $this->buildTaskIDsWhereClause($conn);
     $where[] = $this->buildTaskPHIDsWhereClause($conn);
     $where[] = $this->buildStatusWhereClause($conn);
     $where[] = $this->buildStatusesWhereClause($conn);
     $where[] = $this->buildDependenciesWhereClause($conn);
     $where[] = $this->buildAuthorWhereClause($conn);
     $where[] = $this->buildOwnerWhereClause($conn);
     $where[] = $this->buildFullTextWhereClause($conn);
 
     if ($this->dateCreatedAfter) {
       $where[] = qsprintf(
         $conn,
         'task.dateCreated >= %d',
         $this->dateCreatedAfter);
     }
 
     if ($this->dateCreatedBefore) {
       $where[] = qsprintf(
         $conn,
         'task.dateCreated <= %d',
         $this->dateCreatedBefore);
     }
 
     if ($this->dateModifiedAfter) {
       $where[] = qsprintf(
         $conn,
         'task.dateModified >= %d',
         $this->dateModifiedAfter);
     }
 
     if ($this->dateModifiedBefore) {
       $where[] = qsprintf(
         $conn,
         'task.dateModified <= %d',
         $this->dateModifiedBefore);
     }
 
     if ($this->priorities) {
       $where[] = qsprintf(
         $conn,
         'task.priority IN (%Ld)',
         $this->priorities);
     }
 
     if ($this->subpriorities) {
       $where[] = qsprintf(
         $conn,
         'task.subpriority IN (%Lf)',
         $this->subpriorities);
     }
 
     if ($this->subpriorityMin) {
       $where[] = qsprintf(
         $conn,
         'task.subpriority >= %f',
         $this->subpriorityMin);
     }
 
     if ($this->subpriorityMax) {
       $where[] = qsprintf(
         $conn,
         'task.subpriority <= %f',
         $this->subpriorityMax);
     }
 
     $where[] = $this->buildWhereClauseParts($conn);
 
     $where = $this->formatWhereClause($where);
 
     $group_column = '';
     switch ($this->groupBy) {
       case self::GROUP_PROJECT:
         $group_column = qsprintf(
           $conn,
           ', projectGroupName.indexedObjectPHID projectGroupPHID');
         break;
     }
 
     $rows = queryfx_all(
       $conn,
       '%Q %Q FROM %T task %Q %Q %Q %Q %Q %Q',
       $this->buildSelectClause($conn),
       $group_column,
       $task_dao->getTableName(),
       $this->buildJoinClause($conn),
       $where,
       $this->buildGroupClause($conn),
       $this->buildHavingClause($conn),
       $this->buildOrderClause($conn),
       $this->buildLimitClause($conn));
 
     switch ($this->groupBy) {
       case self::GROUP_PROJECT:
         $data = ipull($rows, null, 'id');
         break;
       default:
         $data = $rows;
         break;
     }
 
     $tasks = $task_dao->loadAllFromArray($data);
 
     switch ($this->groupBy) {
       case self::GROUP_PROJECT:
         $results = array();
         foreach ($rows as $row) {
           $task = clone $tasks[$row['id']];
           $task->attachGroupByProjectPHID($row['projectGroupPHID']);
           $results[] = $task;
         }
         $tasks = $results;
         break;
     }
 
     return $tasks;
   }
 
   protected function willFilterPage(array $tasks) {
     if ($this->groupBy == self::GROUP_PROJECT) {
       // We should only return project groups which the user can actually see.
       $project_phids = mpull($tasks, 'getGroupByProjectPHID');
       $projects = id(new PhabricatorProjectQuery())
         ->setViewer($this->getViewer())
         ->withPHIDs($project_phids)
         ->execute();
       $projects = mpull($projects, null, 'getPHID');
 
       foreach ($tasks as $key => $task) {
         if (!$task->getGroupByProjectPHID()) {
           // This task is either not in any projects, or only in projects
           // which we're ignoring because they're being queried for explicitly.
           continue;
         }
 
         if (empty($projects[$task->getGroupByProjectPHID()])) {
           unset($tasks[$key]);
         }
       }
     }
 
     return $tasks;
   }
 
   protected function didFilterPage(array $tasks) {
     $phids = mpull($tasks, 'getPHID');
 
     if ($this->needProjectPHIDs) {
       $edge_query = id(new PhabricatorEdgeQuery())
         ->withSourcePHIDs($phids)
         ->withEdgeTypes(
           array(
             PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
           ));
       $edge_query->execute();
 
       foreach ($tasks as $task) {
         $project_phids = $edge_query->getDestinationPHIDs(
           array($task->getPHID()));
         $task->attachProjectPHIDs($project_phids);
       }
     }
 
     if ($this->needSubscriberPHIDs) {
       $subscriber_sets = id(new PhabricatorSubscribersQuery())
         ->withObjectPHIDs($phids)
         ->execute();
       foreach ($tasks as $task) {
         $subscribers = idx($subscriber_sets, $task->getPHID(), array());
         $task->attachSubscriberPHIDs($subscribers);
       }
     }
 
     return $tasks;
   }
 
   private function buildTaskIDsWhereClause(AphrontDatabaseConnection $conn) {
     if (!$this->taskIDs) {
       return null;
     }
 
     return qsprintf(
       $conn,
       'task.id in (%Ld)',
       $this->taskIDs);
   }
 
   private function buildTaskPHIDsWhereClause(AphrontDatabaseConnection $conn) {
     if (!$this->taskPHIDs) {
       return null;
     }
 
     return qsprintf(
       $conn,
       'task.phid in (%Ls)',
       $this->taskPHIDs);
   }
 
   private function buildStatusWhereClause(AphrontDatabaseConnection $conn) {
     static $map = array(
       self::STATUS_RESOLVED   => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
       self::STATUS_WONTFIX    => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
       self::STATUS_INVALID    => ManiphestTaskStatus::STATUS_CLOSED_INVALID,
       self::STATUS_SPITE      => ManiphestTaskStatus::STATUS_CLOSED_SPITE,
       self::STATUS_DUPLICATE  => ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE,
     );
 
     switch ($this->status) {
       case self::STATUS_ANY:
         return null;
       case self::STATUS_OPEN:
         return qsprintf(
           $conn,
           'task.status IN (%Ls)',
           ManiphestTaskStatus::getOpenStatusConstants());
       case self::STATUS_CLOSED:
         return qsprintf(
           $conn,
           'task.status IN (%Ls)',
           ManiphestTaskStatus::getClosedStatusConstants());
       default:
         $constant = idx($map, $this->status);
         if (!$constant) {
           throw new Exception("Unknown status query '{$this->status}'!");
         }
         return qsprintf(
           $conn,
           'task.status = %s',
           $constant);
     }
   }
 
   private function buildStatusesWhereClause(AphrontDatabaseConnection $conn) {
     if ($this->statuses) {
       return qsprintf(
         $conn,
         'task.status IN (%Ls)',
         $this->statuses);
     }
     return null;
   }
 
   private function buildAuthorWhereClause(AphrontDatabaseConnection $conn) {
     if (!$this->authorPHIDs) {
       return null;
     }
 
     return qsprintf(
       $conn,
       'task.authorPHID in (%Ls)',
       $this->authorPHIDs);
   }
 
   private function buildOwnerWhereClause(AphrontDatabaseConnection $conn) {
     $subclause = array();
 
     if ($this->noOwner) {
       $subclause[] = qsprintf(
         $conn,
         'task.ownerPHID IS NULL');
     }
 
     if ($this->anyOwner) {
       $subclause[] = qsprintf(
         $conn,
         'task.ownerPHID IS NOT NULL');
     }
 
     if ($this->ownerPHIDs) {
       $subclause[] = qsprintf(
         $conn,
         'task.ownerPHID IN (%Ls)',
         $this->ownerPHIDs);
     }
 
     if (!$subclause) {
       return '';
     }
 
     return '('.implode(') OR (', $subclause).')';
   }
 
   private function buildFullTextWhereClause(AphrontDatabaseConnection $conn) {
     if (!strlen($this->fullTextSearch)) {
       return null;
     }
 
     // In doing a fulltext search, we first find all the PHIDs that match the
     // fulltext search, and then use that to limit the rest of the search
     $fulltext_query = id(new PhabricatorSavedQuery())
       ->setEngineClassName('PhabricatorSearchApplicationSearchEngine')
       ->setParameter('query', $this->fullTextSearch);
 
     // NOTE: Setting this to something larger than 2^53 will raise errors in
     // ElasticSearch, and billions of results won't fit in memory anyway.
     $fulltext_query->setParameter('limit', 100000);
     $fulltext_query->setParameter('types',
       array(ManiphestTaskPHIDType::TYPECONST));
 
     $engine = PhabricatorSearchEngineSelector::newSelector()->newEngine();
     $fulltext_results = $engine->executeSearch($fulltext_query);
 
     if (empty($fulltext_results)) {
       $fulltext_results = array(null);
     }
 
     return qsprintf(
       $conn,
       'task.phid IN (%Ls)',
       $fulltext_results);
   }
 
   private function buildDependenciesWhereClause(
     AphrontDatabaseConnection $conn) {
 
     if (!$this->shouldJoinBlockedTasks() &&
         !$this->shouldJoinBlockingTasks()) {
       return null;
     }
 
     $parts = array();
     if ($this->blockingTasks === true) {
       $parts[] = qsprintf(
         $conn,
         'blocking.dst IS NOT NULL AND blockingtask.status IN (%Ls)',
         ManiphestTaskStatus::getOpenStatusConstants());
     } else if ($this->blockingTasks === false) {
       $parts[] = qsprintf(
         $conn,
         'blocking.dst IS NULL OR blockingtask.status NOT IN (%Ls)',
         ManiphestTaskStatus::getOpenStatusConstants());
     }
 
     if ($this->blockedTasks === true) {
       $parts[] = qsprintf(
         $conn,
         'blocked.dst IS NOT NULL AND blockedtask.status IN (%Ls)',
         ManiphestTaskStatus::getOpenStatusConstants());
     } else if ($this->blockedTasks === false) {
       $parts[] = qsprintf(
         $conn,
         'blocked.dst IS NULL OR blockedtask.status NOT IN (%Ls)',
         ManiphestTaskStatus::getOpenStatusConstants());
     }
 
     return '('.implode(') OR (', $parts).')';
   }
 
   protected function buildJoinClauseParts(AphrontDatabaseConnection $conn_r) {
     $edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE;
 
     $joins = array();
 
     if ($this->shouldJoinBlockingTasks()) {
       $joins[] = qsprintf(
         $conn_r,
         'LEFT JOIN %T blocking ON blocking.src = task.phid '.
         'AND blocking.type = %d '.
         'LEFT JOIN %T blockingtask ON blocking.dst = blockingtask.phid',
         $edge_table,
         ManiphestTaskDependedOnByTaskEdgeType::EDGECONST,
         id(new ManiphestTask())->getTableName());
     }
     if ($this->shouldJoinBlockedTasks()) {
       $joins[] = qsprintf(
         $conn_r,
         'LEFT JOIN %T blocked ON blocked.src = task.phid '.
         'AND blocked.type = %d '.
         'LEFT JOIN %T blockedtask ON blocked.dst = blockedtask.phid',
         $edge_table,
         ManiphestTaskDependsOnTaskEdgeType::EDGECONST,
         id(new ManiphestTask())->getTableName());
     }
 
     if ($this->subscriberPHIDs) {
       $joins[] = qsprintf(
         $conn_r,
         'JOIN %T e_ccs ON e_ccs.src = task.phid '.
         'AND e_ccs.type = %s '.
         'AND e_ccs.dst in (%Ls)',
         PhabricatorEdgeConfig::TABLE_NAME_EDGE,
         PhabricatorObjectHasSubscriberEdgeType::EDGECONST,
         $this->subscriberPHIDs);
     }
 
     switch ($this->groupBy) {
       case self::GROUP_PROJECT:
         $ignore_group_phids = $this->getIgnoreGroupedProjectPHIDs();
         if ($ignore_group_phids) {
           $joins[] = qsprintf(
             $conn_r,
             'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src
               AND projectGroup.type = %d
               AND projectGroup.dst NOT IN (%Ls)',
             $edge_table,
             PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
             $ignore_group_phids);
         } else {
           $joins[] = qsprintf(
             $conn_r,
             'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src
               AND projectGroup.type = %d',
             $edge_table,
             PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
         }
         $joins[] = qsprintf(
           $conn_r,
           'LEFT JOIN %T projectGroupName
             ON projectGroup.dst = projectGroupName.indexedObjectPHID',
           id(new ManiphestNameIndex())->getTableName());
         break;
     }
 
     $joins[] = parent::buildJoinClauseParts($conn_r);
 
     return $joins;
   }
 
   protected function buildGroupClause(AphrontDatabaseConnection $conn_r) {
     $joined_multiple_rows = $this->shouldJoinBlockingTasks() ||
                             $this->shouldJoinBlockedTasks() ||
                             ($this->shouldGroupQueryResultRows());
 
     $joined_project_name = ($this->groupBy == self::GROUP_PROJECT);
 
     // If we're joining multiple rows, we need to group the results by the
     // task IDs.
     if ($joined_multiple_rows) {
       if ($joined_project_name) {
         return 'GROUP BY task.phid, projectGroup.dst';
       } else {
         return 'GROUP BY task.phid';
       }
     } else {
       return '';
     }
   }
 
   /**
    * Return project PHIDs which we should ignore when grouping tasks by
    * project. For example, if a user issues a query like:
    *
    *   Tasks in all projects: Frontend, Bugs
    *
    * ...then we don't show "Frontend" or "Bugs" groups in the result set, since
    * they're meaningless as all results are in both groups.
    *
    * Similarly, for queries like:
    *
    *   Tasks in any projects: Public Relations
    *
    * ...we ignore the single project, as every result is in that project. (In
    * the case that there are several "any" projects, we do not ignore them.)
    *
    * @return list<phid> Project PHIDs which should be ignored in query
    *                    construction.
    */
   private function getIgnoreGroupedProjectPHIDs() {
     // Maybe we should also exclude the "OPERATOR_NOT" PHIDs? It won't
     // impact the results, but we might end up with a better query plan.
     // Investigate this on real data? This is likely very rare.
 
     $edge_types = array(
       PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
     );
 
     $phids = array();
 
     $phids[] = $this->getEdgeLogicValues(
       $edge_types,
       array(
         PhabricatorQueryConstraint::OPERATOR_AND,
       ));
 
     $any = $this->getEdgeLogicValues(
       $edge_types,
       array(
         PhabricatorQueryConstraint::OPERATOR_OR,
       ));
     if (count($any) == 1) {
       $phids[] = $any;
     }
 
     return array_mergev($phids);
   }
 
   protected function getResultCursor($result) {
     $id = $result->getID();
 
     if ($this->groupBy == self::GROUP_PROJECT) {
-      return rtrim($id.'.'.$result->getGroupByProjectPHID(), '.');;
+      return rtrim($id.'.'.$result->getGroupByProjectPHID(), '.');
     }
 
     return $id;
   }
 
   public function getOrderableColumns() {
     return parent::getOrderableColumns() + array(
       'priority' => array(
         'table' => 'task',
         'column' => 'priority',
         'type' => 'int',
       ),
       'owner' => array(
         'table' => 'task',
         'column' => 'ownerOrdering',
         'null' => 'head',
         'reverse' => true,
         'type' => 'string',
       ),
       'status' => array(
         'table' => 'task',
         'column' => 'status',
         'type' => 'string',
         'reverse' => true,
       ),
       'project' => array(
         'table' => 'projectGroupName',
         'column' => 'indexedObjectName',
         'type' => 'string',
         'null' => 'head',
         'reverse' => true,
       ),
       'title' => array(
         'table' => 'task',
         'column' => 'title',
         'type' => 'string',
         'reverse' => true,
       ),
       'subpriority' => array(
         'table' => 'task',
         'column' => 'subpriority',
         'type' => 'float',
       ),
       'updated' => array(
         'table' => 'task',
         'column' => 'dateModified',
         'type' => 'int',
       ),
     );
   }
 
   protected function getPagingValueMap($cursor, array $keys) {
     $cursor_parts = explode('.', $cursor, 2);
     $task_id = $cursor_parts[0];
     $group_id = idx($cursor_parts, 1);
 
     $task = $this->loadCursorObject($task_id);
 
     $map = array(
       'id' => $task->getID(),
       'priority' => $task->getPriority(),
       'subpriority' => $task->getSubpriority(),
       'owner' => $task->getOwnerOrdering(),
       'status' => $task->getStatus(),
       'title' => $task->getTitle(),
       'updated' => $task->getDateModified(),
     );
 
     foreach ($keys as $key) {
       switch ($key) {
         case 'project':
           $value = null;
           if ($group_id) {
             $paging_projects = id(new PhabricatorProjectQuery())
               ->setViewer($this->getViewer())
               ->withPHIDs(array($group_id))
               ->execute();
             if ($paging_projects) {
               $value = head($paging_projects)->getName();
             }
           }
           $map[$key] = $value;
           break;
       }
     }
 
     foreach ($keys as $key) {
       if ($this->isCustomFieldOrderKey($key)) {
         $map += $this->getPagingValueMapForCustomFields($task);
         break;
       }
     }
 
     return $map;
   }
 
   protected function getPrimaryTableAlias() {
     return 'task';
   }
 
   public function getQueryApplicationClass() {
     return 'PhabricatorManiphestApplication';
   }
 
 }
diff --git a/src/applications/metamta/command/MetaMTAEmailTransactionCommand.php b/src/applications/metamta/command/MetaMTAEmailTransactionCommand.php
index e6011768df..cc78df3492 100644
--- a/src/applications/metamta/command/MetaMTAEmailTransactionCommand.php
+++ b/src/applications/metamta/command/MetaMTAEmailTransactionCommand.php
@@ -1,120 +1,120 @@
 <?php
 
 /**
  * @task docs Command Documentation
  */
 abstract class MetaMTAEmailTransactionCommand extends Phobject {
 
   abstract public function getCommand();
 
   /**
    * Return a brief human-readable description of the command effect.
    *
    * This should normally be one or two sentences briefly describing the
    * command behavior.
    *
    * @return string Brief human-readable remarkup.
    * @task docs
    */
   abstract public function getCommandSummary();
 
 
   /**
    * Return a one-line Remarkup description of command syntax for documentation.
    *
    * @return string Brief human-readable remarkup.
    * @task docs
    */
   public function getCommandSyntax() {
     return '**!'.$this->getCommand().'**';
   }
 
   /**
    * Return a longer human-readable description of the command effect.
    *
    * This can be as long as necessary to explain the command.
    *
    * @return string Human-readable remarkup of whatever length is desired.
    * @task docs
    */
   public function getCommandDescription() {
     return null;
   }
 
   abstract public function isCommandSupportedForObject(
     PhabricatorApplicationTransactionInterface $object);
 
   abstract public function buildTransactions(
     PhabricatorUser $viewer,
     PhabricatorApplicationTransactionInterface $object,
     PhabricatorMetaMTAReceivedMail $mail,
     $command,
     array $argv);
 
   public function getCommandAliases() {
     return array();
   }
 
   public function getCommandObjects() {
     return array($this);
   }
 
   public static function getAllCommands() {
     static $commands;
 
     if ($commands === null) {
       $kinds = id(new PhutilSymbolLoader())
         ->setAncestorClass(__CLASS__)
         ->loadObjects();
       $commands = array();
       foreach ($kinds as $kind) {
         foreach ($kind->getCommandObjects() as $command) {
           $commands[] = $command;
         }
       }
     }
 
     return $commands;
   }
 
   public static function getAllCommandsForObject(
     PhabricatorApplicationTransactionInterface $object) {
 
     $commands = self::getAllCommands();
     foreach ($commands as $key => $command) {
       if (!$command->isCommandSupportedForObject($object)) {
         unset($commands[$key]);
       }
     }
 
     return $commands;
   }
 
   public static function getCommandMap(array $commands) {
-    assert_instances_of($commands, 'MetaMTAEmailTransactionCommand');
+    assert_instances_of($commands, __CLASS__);
 
     $map = array();
     foreach ($commands as $command) {
       $keywords = $command->getCommandAliases();
       $keywords[] = $command->getCommand();
 
       foreach ($keywords as $keyword) {
         $keyword = phutil_utf8_strtolower($keyword);
         if (empty($map[$keyword])) {
           $map[$keyword] = $command;
         } else {
           throw new Exception(
             pht(
               'Mail commands "%s" and "%s" both respond to keyword "%s". '.
               'Keywords must be uniquely associated with commands.',
               get_class($command),
               get_class($map[$keyword]),
               $keyword));
         }
       }
     }
 
     return $map;
   }
 
 }
diff --git a/src/applications/metamta/contentsource/PhabricatorContentSource.php b/src/applications/metamta/contentsource/PhabricatorContentSource.php
index a1cbd061f2..e5a29572c1 100644
--- a/src/applications/metamta/contentsource/PhabricatorContentSource.php
+++ b/src/applications/metamta/contentsource/PhabricatorContentSource.php
@@ -1,104 +1,104 @@
 <?php
 
 final class PhabricatorContentSource {
 
   const SOURCE_UNKNOWN  = 'unknown';
   const SOURCE_WEB      = 'web';
   const SOURCE_EMAIL    = 'email';
   const SOURCE_CONDUIT  = 'conduit';
   const SOURCE_MOBILE   = 'mobile';
   const SOURCE_TABLET   = 'tablet';
   const SOURCE_FAX      = 'fax';
   const SOURCE_CONSOLE  = 'console';
   const SOURCE_HERALD   = 'herald';
   const SOURCE_LEGACY   = 'legacy';
   const SOURCE_DAEMON   = 'daemon';
   const SOURCE_LIPSUM   = 'lipsum';
   const SOURCE_PHORTUNE = 'phortune';
 
   private $source;
   private $params = array();
 
   private function __construct() {
     // <empty>
   }
 
   public static function newForSource($source, array $params) {
     $obj = new PhabricatorContentSource();
     $obj->source = $source;
     $obj->params = $params;
 
     return $obj;
   }
 
   public static function newFromSerialized($serialized) {
     $dict = json_decode($serialized, true);
     if (!is_array($dict)) {
       $dict = array();
     }
 
     $obj = new PhabricatorContentSource();
     $obj->source = idx($dict, 'source', self::SOURCE_UNKNOWN);
     $obj->params = idx($dict, 'params', array());
 
     return $obj;
   }
 
   public static function newConsoleSource() {
     return self::newForSource(
-      PhabricatorContentSource::SOURCE_CONSOLE,
+      self::SOURCE_CONSOLE,
       array());
   }
 
   public static function newFromRequest(AphrontRequest $request) {
     return self::newForSource(
-      PhabricatorContentSource::SOURCE_WEB,
+      self::SOURCE_WEB,
       array(
         'ip' => $request->getRemoteAddr(),
       ));
   }
 
   public static function newFromConduitRequest(ConduitAPIRequest $request) {
     return self::newForSource(
-      PhabricatorContentSource::SOURCE_CONDUIT,
+      self::SOURCE_CONDUIT,
       array());
   }
 
   public static function getSourceNameMap() {
     return array(
       self::SOURCE_WEB      => pht('Web'),
       self::SOURCE_EMAIL    => pht('Email'),
       self::SOURCE_CONDUIT  => pht('Conduit'),
       self::SOURCE_MOBILE   => pht('Mobile'),
       self::SOURCE_TABLET   => pht('Tablet'),
       self::SOURCE_FAX      => pht('Fax'),
       self::SOURCE_CONSOLE  => pht('Console'),
       self::SOURCE_LEGACY   => pht('Legacy'),
       self::SOURCE_HERALD   => pht('Herald'),
       self::SOURCE_DAEMON   => pht('Daemons'),
       self::SOURCE_LIPSUM   => pht('Lipsum'),
       self::SOURCE_UNKNOWN  => pht('Old World'),
       self::SOURCE_PHORTUNE => pht('Phortune'),
     );
   }
 
   public function serialize() {
     return json_encode(array(
       'source' => $this->getSource(),
       'params' => $this->getParams(),
     ));
   }
 
   public function getSource() {
     return $this->source;
   }
 
   public function getParams() {
     return $this->params;
   }
 
   public function getParam($key, $default = null) {
     return idx($this->params, $key, $default);
   }
 
 }
diff --git a/src/applications/multimeter/data/MultimeterControl.php b/src/applications/multimeter/data/MultimeterControl.php
index 2bc849c185..892e9e56cd 100644
--- a/src/applications/multimeter/data/MultimeterControl.php
+++ b/src/applications/multimeter/data/MultimeterControl.php
@@ -1,292 +1,292 @@
 <?php
 
 final class MultimeterControl {
 
   private static $instance;
 
   private $events = array();
   private $sampleRate;
   private $pauseDepth;
 
   private $eventViewer;
   private $eventContext;
 
   private function __construct() {
     // Private.
   }
 
   public static function newInstance() {
     $instance = new MultimeterControl();
 
     // NOTE: We don't set the sample rate yet. This allows the multimeter to
     // be initialized and begin recording events, then make a decision about
     // whether the page will be sampled or not later on (once we've loaded
     // enough configuration).
 
     self::$instance = $instance;
     return self::getInstance();
   }
 
   public static function getInstance() {
     return self::$instance;
   }
 
   public function isActive() {
     return ($this->sampleRate !== 0) && ($this->pauseDepth == 0);
   }
 
   public function setSampleRate($rate) {
     if ($rate && (mt_rand(1, $rate) == $rate)) {
       $sample_rate = $rate;
     } else {
       $sample_rate = 0;
     }
 
     $this->sampleRate = $sample_rate;
 
     return;
   }
 
   public function pauseMultimeter() {
     $this->pauseDepth++;
     return $this;
   }
 
   public function unpauseMultimeter() {
     if (!$this->pauseDepth) {
       throw new Exception(pht('Trying to unpause an active multimeter!'));
     }
     $this->pauseDepth--;
     return $this;
   }
 
 
   public function newEvent($type, $label, $cost) {
     if (!$this->isActive()) {
       return null;
     }
 
     $event = id(new MultimeterEvent())
       ->setEventType($type)
       ->setEventLabel($label)
       ->setResourceCost($cost)
       ->setEpoch(PhabricatorTime::getNow());
 
     $this->events[] = $event;
 
     return $event;
   }
 
   public function saveEvents() {
     if (!$this->isActive()) {
       return;
     }
 
     $events = $this->events;
     if (!$events) {
       return;
     }
 
     if ($this->sampleRate === null) {
-      throw new Exception(pht('Call setSampleRate() before saving events!'));
+      throw new PhutilInvalidStateException('setSampleRate');
     }
 
     $this->addServiceEvents();
 
     // Don't sample any of this stuff.
     $this->pauseMultimeter();
 
     $use_scope = AphrontWriteGuard::isGuardActive();
     if ($use_scope) {
       $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
     } else {
       AphrontWriteGuard::allowDangerousUnguardedWrites(true);
     }
 
     $caught = null;
     try {
       $this->writeEvents();
     } catch (Exception $ex) {
       $caught = $ex;
     }
 
     if ($use_scope) {
       unset($unguarded);
     } else {
       AphrontWriteGuard::allowDangerousUnguardedWrites(false);
     }
 
     $this->unpauseMultimeter();
 
     if ($caught) {
       throw $caught;
     }
   }
 
   private function writeEvents() {
     $events = $this->events;
 
     $random = Filesystem::readRandomBytes(32);
     $request_key = PhabricatorHash::digestForIndex($random);
 
     $host_id = $this->loadHostID(php_uname('n'));
     $context_id = $this->loadEventContextID($this->eventContext);
     $viewer_id = $this->loadEventViewerID($this->eventViewer);
     $label_map = $this->loadEventLabelIDs(mpull($events, 'getEventLabel'));
 
     foreach ($events as $event) {
       $event
         ->setRequestKey($request_key)
         ->setSampleRate($this->sampleRate)
         ->setEventHostID($host_id)
         ->setEventContextID($context_id)
         ->setEventViewerID($viewer_id)
         ->setEventLabelID($label_map[$event->getEventLabel()])
         ->save();
     }
   }
 
   public function setEventContext($event_context) {
     $this->eventContext = $event_context;
     return $this;
   }
 
   public function getEventContext() {
     return $this->eventContext;
   }
 
   public function setEventViewer($viewer) {
     $this->eventViewer = $viewer;
     return $this;
   }
 
   private function loadHostID($host) {
     $map = $this->loadDimensionMap(new MultimeterHost(), array($host));
     return idx($map, $host);
   }
 
   private function loadEventViewerID($viewer) {
     $map = $this->loadDimensionMap(new MultimeterViewer(), array($viewer));
     return idx($map, $viewer);
   }
 
   private function loadEventContextID($context) {
     $map = $this->loadDimensionMap(new MultimeterContext(), array($context));
     return idx($map, $context);
   }
 
   private function loadEventLabelIDs(array $labels) {
     return $this->loadDimensionMap(new MultimeterLabel(), $labels);
   }
 
   private function loadDimensionMap(MultimeterDimension $table, array $names) {
     $hashes = array();
     foreach ($names as $name) {
       $hashes[] = PhabricatorHash::digestForIndex($name);
     }
 
     $objects = $table->loadAllWhere('nameHash IN (%Ls)', $hashes);
     $map = mpull($objects, 'getID', 'getName');
 
     $need = array();
     foreach ($names as $name) {
       if (isset($map[$name])) {
         continue;
       }
       $need[$name] = $name;
     }
 
     foreach ($need as $name) {
       $object = id(clone $table)
         ->setName($name)
         ->save();
       $map[$name] = $object->getID();
     }
 
     return $map;
   }
 
   private function addServiceEvents() {
     $events = PhutilServiceProfiler::getInstance()->getServiceCallLog();
     foreach ($events as $event) {
       $type = idx($event, 'type');
       switch ($type) {
         case 'exec':
           $this->newEvent(
             MultimeterEvent::TYPE_EXEC_TIME,
             $label = $this->getLabelForCommandEvent($event['command']),
             (1000000 * $event['duration']));
           break;
       }
     }
   }
 
   private function getLabelForCommandEvent($command) {
     $argv = preg_split('/\s+/', $command);
 
     $bin = array_shift($argv);
     $bin = basename($bin);
     $bin = trim($bin, '"\'');
 
     // It's important to avoid leaking details about command parameters,
     // because some may be sensitive. Given this, it's not trivial to
     // determine which parts of a command are arguments and which parts are
     // flags.
 
     // Rather than try too hard for now, just whitelist some workflows that we
     // know about and record everything else generically. Overall, this will
     // produce labels like "pygmentize" or "git log", discarding all flags and
     // arguments.
 
     $workflows = array(
       'git' => array(
         'log' => true,
         'for-each-ref' => true,
         'pull' => true,
         'clone' => true,
         'fetch' => true,
         'cat-file' => true,
         'init' => true,
         'config' => true,
         'remote' => true,
         'rev-parse' => true,
         'diff' => true,
         'ls-tree' => true,
       ),
       'svn' => array(
         'log' => true,
         'diff' => true,
       ),
       'hg' => array(
         'log' => true,
         'locate' => true,
         'pull' => true,
         'clone' => true,
         'init' => true,
         'diff' => true,
         'cat' => true,
       ),
       'svnadmin' => array(
         'create' => true,
       ),
     );
 
     $workflow = null;
     $candidates = idx($workflows, $bin);
     if ($candidates) {
       foreach ($argv as $arg) {
         if (isset($candidates[$arg])) {
           $workflow = $arg;
           break;
         }
       }
     }
 
     if ($workflow) {
       return 'bin.'.$bin.' '.$workflow;
     } else {
       return 'bin.'.$bin;
     }
   }
 
 }
diff --git a/src/applications/nuance/storage/NuanceItem.php b/src/applications/nuance/storage/NuanceItem.php
index 2db403cd2e..50d7ee72ef 100644
--- a/src/applications/nuance/storage/NuanceItem.php
+++ b/src/applications/nuance/storage/NuanceItem.php
@@ -1,137 +1,137 @@
 <?php
 
 final class NuanceItem
   extends NuanceDAO
   implements PhabricatorPolicyInterface {
 
   const STATUS_OPEN     = 0;
   const STATUS_ASSIGNED = 10;
   const STATUS_CLOSED   = 20;
 
   protected $status;
   protected $ownerPHID;
   protected $requestorPHID;
   protected $sourcePHID;
   protected $sourceLabel;
   protected $data;
   protected $mailKey;
   protected $dateNuanced;
 
   public static function initializeNewItem(PhabricatorUser $user) {
     return id(new NuanceItem())
       ->setDateNuanced(time())
-      ->setStatus(NuanceItem::STATUS_OPEN);
+      ->setStatus(self::STATUS_OPEN);
   }
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_SERIALIZATION => array(
         'data' => self::SERIALIZATION_JSON,
       ),
       self::CONFIG_COLUMN_SCHEMA => array(
         'ownerPHID' => 'phid?',
         'sourceLabel' => 'text255?',
         'status' => 'uint32',
         'mailKey' => 'bytes20',
         'dateNuanced' => 'epoch',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_source' => array(
           'columns' => array('sourcePHID', 'status', 'dateNuanced', 'id'),
         ),
         'key_owner' => array(
           'columns' => array('ownerPHID', 'status', 'dateNuanced', 'id'),
         ),
         'key_contacter' => array(
           'columns' => array('requestorPHID', 'status', 'dateNuanced', 'id'),
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       NuanceItemPHIDType::TYPECONST);
   }
 
   public function save() {
     if (!$this->getMailKey()) {
       $this->setMailKey(Filesystem::readRandomCharacters(20));
     }
     return parent::save();
   }
 
   public function getURI() {
     return '/nuance/item/view/'.$this->getID().'/';
   }
 
   public function getLabel(PhabricatorUser $viewer) {
     // this is generated at the time the item is created based on
     // the configuration from the item source. It is typically
     // something like 'Twitter'.
     $source_label = $this->getSourceLabel();
 
     return pht(
       'Item via %s @ %s.',
       $source_label,
       phabricator_datetime($this->getDateCreated(), $viewer));
   }
 
   public function getRequestor() {
     return $this->assertAttached($this->requestor);
   }
 
   public function attachRequestor(NuanceRequestor $requestor) {
     return $this->requestor = $requestor;
   }
 
   public function getSource() {
     return $this->assertAttached($this->source);
   }
 
   public function attachSource(NuanceSource $source) {
     $this->source = $source;
   }
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
       PhabricatorPolicyCapability::CAN_EDIT,
     );
   }
 
   public function getPolicy($capability) {
     // TODO - this should be based on the queues the item currently resides in
     return PhabricatorPolicies::POLICY_USER;
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     // TODO - requestors should get auto access too!
     return $viewer->getPHID() == $this->ownerPHID;
   }
 
   public function describeAutomaticCapability($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         return pht('Owners of an item can always view it.');
       case PhabricatorPolicyCapability::CAN_EDIT:
         return pht('Owners of an item can always edit it.');
     }
     return null;
   }
 
   public function toDictionary() {
     return array(
       'id' => $this->getID(),
       'phid' => $this->getPHID(),
       'ownerPHID' => $this->getOwnerPHID(),
       'requestorPHID' => $this->getRequestorPHID(),
       'sourcePHID' => $this->getSourcePHID(),
       'sourceLabel' => $this->getSourceLabel(),
       'dateCreated' => $this->getDateCreated(),
       'dateModified' => $this->getDateModified(),
       'dateNuanced' => $this->getDateNuanced(),
     );
   }
 }
diff --git a/src/applications/paste/storage/PhabricatorPasteTransaction.php b/src/applications/paste/storage/PhabricatorPasteTransaction.php
index cc4e36d2ff..34cc6bd92d 100644
--- a/src/applications/paste/storage/PhabricatorPasteTransaction.php
+++ b/src/applications/paste/storage/PhabricatorPasteTransaction.php
@@ -1,207 +1,207 @@
 <?php
 
 final class PhabricatorPasteTransaction
   extends PhabricatorApplicationTransaction {
 
   const TYPE_CONTENT = 'paste.create';
   const TYPE_TITLE = 'paste.title';
   const TYPE_LANGUAGE = 'paste.language';
 
   const MAILTAG_CONTENT = 'paste-content';
   const MAILTAG_OTHER = 'paste-other';
   const MAILTAG_COMMENT = 'paste-comment';
 
   public function getApplicationName() {
     return 'pastebin';
   }
 
   public function getApplicationTransactionType() {
     return PhabricatorPastePastePHIDType::TYPECONST;
   }
 
   public function getApplicationTransactionCommentObject() {
     return new PhabricatorPasteTransactionComment();
   }
 
   public function getRequiredHandlePHIDs() {
     $phids = parent::getRequiredHandlePHIDs();
 
     switch ($this->getTransactionType()) {
       case self::TYPE_CONTENT:
         $phids[] = $this->getObjectPHID();
         break;
     }
 
     return $phids;
   }
 
   public function shouldHide() {
     $old = $this->getOldValue();
     switch ($this->getTransactionType()) {
       case self::TYPE_TITLE:
       case self::TYPE_LANGUAGE:
         return ($old === null);
     }
     return parent::shouldHide();
   }
 
   public function getIcon() {
     switch ($this->getTransactionType()) {
       case self::TYPE_CONTENT:
         return 'fa-plus';
         break;
       case self::TYPE_TITLE:
       case self::TYPE_LANGUAGE:
         return 'fa-pencil';
         break;
     }
     return parent::getIcon();
   }
 
   public function getTitle() {
     $author_phid = $this->getAuthorPHID();
     $object_phid = $this->getObjectPHID();
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     $type = $this->getTransactionType();
     switch ($type) {
-      case PhabricatorPasteTransaction::TYPE_CONTENT:
+      case self::TYPE_CONTENT:
         if ($old === null) {
           return pht(
             '%s created this paste.',
             $this->renderHandleLink($author_phid));
         } else {
           return pht(
             '%s edited the content of this paste.',
             $this->renderHandleLink($author_phid));
         }
         break;
-      case PhabricatorPasteTransaction::TYPE_TITLE:
+      case self::TYPE_TITLE:
         return pht(
           '%s updated the paste\'s title to "%s".',
           $this->renderHandleLink($author_phid),
           $new);
         break;
-      case PhabricatorPasteTransaction::TYPE_LANGUAGE:
+      case self::TYPE_LANGUAGE:
         return pht(
           "%s updated the paste's language.",
           $this->renderHandleLink($author_phid));
         break;
     }
 
     return parent::getTitle();
   }
 
   public function getTitleForFeed() {
     $author_phid = $this->getAuthorPHID();
     $object_phid = $this->getObjectPHID();
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     $type = $this->getTransactionType();
     switch ($type) {
-      case PhabricatorPasteTransaction::TYPE_CONTENT:
+      case self::TYPE_CONTENT:
         if ($old === null) {
           return pht(
             '%s created %s.',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid));
         } else {
           return pht(
             '%s edited %s.',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid));
         }
         break;
-      case PhabricatorPasteTransaction::TYPE_TITLE:
+      case self::TYPE_TITLE:
         return pht(
           '%s updated the title for %s.',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($object_phid));
         break;
-      case PhabricatorPasteTransaction::TYPE_LANGUAGE:
+      case self::TYPE_LANGUAGE:
         return pht(
           '%s updated the language for %s.',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($object_phid));
         break;
     }
 
     return parent::getTitleForFeed();
   }
 
   public function getColor() {
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
       case self::TYPE_CONTENT:
         return PhabricatorTransactions::COLOR_GREEN;
     }
 
     return parent::getColor();
   }
 
 
   public function hasChangeDetails() {
     switch ($this->getTransactionType()) {
       case self::TYPE_CONTENT:
         return ($this->getOldValue() !== null);
     }
 
     return parent::hasChangeDetails();
   }
 
   public function renderChangeDetails(PhabricatorUser $viewer) {
     switch ($this->getTransactionType()) {
       case self::TYPE_CONTENT:
         $old = $this->getOldValue();
         $new = $this->getNewValue();
 
         $files = id(new PhabricatorFileQuery())
           ->setViewer($viewer)
           ->withPHIDs(array_filter(array($old, $new)))
           ->execute();
         $files = mpull($files, null, 'getPHID');
 
         $old_text = '';
         if (idx($files, $old)) {
           $old_text = $files[$old]->loadFileData();
         }
 
         $new_text = '';
         if (idx($files, $new)) {
           $new_text = $files[$new]->loadFileData();
         }
 
         return $this->renderTextCorpusChangeDetails(
           $viewer,
           $old_text,
           $new_text);
     }
 
     return parent::renderChangeDetails($viewer);
   }
 
   public function getMailTags() {
     $tags = array();
     switch ($this->getTransactionType()) {
       case self::TYPE_TITLE:
       case self::TYPE_CONTENT:
       case self::TYPE_LANGUAGE:
         $tags[] = self::MAILTAG_CONTENT;
         break;
       case PhabricatorTransactions::TYPE_COMMENT:
         $tags[] = self::MAILTAG_COMMENT;
         break;
       default:
         $tags[] = self::MAILTAG_OTHER;
         break;
     }
     return $tags;
   }
 
 }
diff --git a/src/applications/paste/view/PasteEmbedView.php b/src/applications/paste/view/PasteEmbedView.php
index 46301e5a31..650905c46c 100644
--- a/src/applications/paste/view/PasteEmbedView.php
+++ b/src/applications/paste/view/PasteEmbedView.php
@@ -1,70 +1,70 @@
 <?php
 
 final class PasteEmbedView extends AphrontView {
 
   private $paste;
   private $handle;
   private $highlights = array();
   private $lines = 24;
 
   public function setPaste(PhabricatorPaste $paste) {
     $this->paste = $paste;
     return $this;
   }
 
   public function setHandle(PhabricatorObjectHandle $handle) {
     $this->handle = $handle;
     return $this;
   }
 
   public function setHighlights(array $highlights) {
     $this->highlights = $highlights;
     return $this;
   }
 
   public function setLines($lines) {
     $this->lines = $lines;
   }
 
   public function render() {
     if (!$this->paste) {
-      throw new Exception('Call setPaste() before render()!');
+      throw new PhutilInvalidStateException('setPaste');
     }
 
     $lines = phutil_split_lines($this->paste->getContent());
     require_celerity_resource('paste-css');
 
     $link = phutil_tag(
       'a',
       array(
         'href' => '/P'.$this->paste->getID(),
       ),
       $this->handle->getFullName());
 
     $head = phutil_tag(
       'div',
       array(
         'class' => 'paste-embed-head',
       ),
       $link);
 
     $body_attributes = array('class' => 'paste-embed-body');
     if ($this->lines != null) {
       $body_attributes['style'] = 'max-height: '.$this->lines * (1.15).'em;';
     }
 
     $body = phutil_tag(
       'div',
       $body_attributes,
       id(new PhabricatorSourceCodeView())
       ->setLines($lines)
       ->setHighlights($this->highlights)
       ->disableHighlightOnClick());
 
     return phutil_tag(
       'div',
       array('class' => 'paste-embed'),
       array($head, $body));
 
   }
 }
diff --git a/src/applications/people/controller/PhabricatorPeopleCalendarController.php b/src/applications/people/controller/PhabricatorPeopleCalendarController.php
index 06c7033fd7..168c1def70 100644
--- a/src/applications/people/controller/PhabricatorPeopleCalendarController.php
+++ b/src/applications/people/controller/PhabricatorPeopleCalendarController.php
@@ -1,88 +1,100 @@
 <?php
 
 final class PhabricatorPeopleCalendarController
   extends PhabricatorPeopleController {
 
   private $username;
 
   public function shouldRequireAdmin() {
     return false;
   }
 
   public function willProcessRequest(array $data) {
     $this->username = idx($data, 'username');
   }
 
   public function processRequest() {
     $viewer = $this->getRequest()->getUser();
     $user = id(new PhabricatorPeopleQuery())
       ->setViewer($viewer)
       ->withUsernames(array($this->username))
       ->needProfileImage(true)
       ->executeOne();
 
     if (!$user) {
       return new Aphront404Response();
     }
 
     $picture = $user->getProfileImageURI();
 
     $now     = time();
     $request = $this->getRequest();
     $year_d  = phabricator_format_local_time($now, $user, 'Y');
     $year    = $request->getInt('year', $year_d);
     $month_d = phabricator_format_local_time($now, $user, 'm');
     $month   = $request->getInt('month', $month_d);
     $day   = phabricator_format_local_time($now, $user, 'j');
 
-
-    $holidays = id(new PhabricatorCalendarHoliday())->loadAllWhere(
-      'day BETWEEN %s AND %s',
-      "{$year}-{$month}-01",
-      "{$year}-{$month}-31");
+    $start_epoch = strtotime("{$year}-{$month}-01");
+    $end_epoch = strtotime("{$year}-{$month}-01 next month");
 
     $statuses = id(new PhabricatorCalendarEventQuery())
       ->setViewer($user)
       ->withInvitedPHIDs(array($user->getPHID()))
       ->withDateRange(
-        strtotime("{$year}-{$month}-01"),
-        strtotime("{$year}-{$month}-01 next month"))
+        $start_epoch,
+        $end_epoch)
       ->execute();
 
+    $start_range_value = AphrontFormDateControlValue::newFromEpoch(
+      $user,
+      $start_epoch);
+    $end_range_value = AphrontFormDateControlValue::newFromEpoch(
+      $user,
+      $end_epoch);
+
     if ($month == $month_d && $year == $year_d) {
-      $month_view = new PHUICalendarMonthView($month, $year, $day);
+      $month_view = new PHUICalendarMonthView(
+        $start_range_value,
+        $end_range_value,
+        $month,
+        $year,
+        $day);
     } else {
-      $month_view = new PHUICalendarMonthView($month, $year);
+      $month_view = new PHUICalendarMonthView(
+        $start_range_value,
+        $end_range_value,
+        $month,
+        $year);
     }
 
     $month_view->setBrowseURI($request->getRequestURI());
     $month_view->setUser($user);
-    $month_view->setHolidays($holidays);
     $month_view->setImage($picture);
 
     $phids = mpull($statuses, 'getUserPHID');
     $handles = $this->loadViewerHandles($phids);
 
     foreach ($statuses as $status) {
       $event = new AphrontCalendarEventView();
       $event->setEpochRange($status->getDateFrom(), $status->getDateTo());
       $event->setUserPHID($status->getUserPHID());
-      $event->setName($status->getHumanStatus());
+      $event->setName($status->getName());
       $event->setDescription($status->getDescription());
       $event->setEventID($status->getID());
       $month_view->addEvent($event);
     }
 
     $name = $user->getUsername();
 
     $nav = $this->buildIconNavView($user);
     $nav->selectFilter("{$name}/calendar/");
     $nav->appendChild($month_view);
 
     return $this->buildApplicationPage(
       $nav,
       array(
         'title' => pht('Calendar'),
       ));
   }
 }
diff --git a/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php b/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php
index 231181614d..0f59e23286 100644
--- a/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php
+++ b/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php
@@ -1,254 +1,251 @@
 <?php
 
 final class PhabricatorPeopleProfilePictureController
   extends PhabricatorPeopleController {
 
   private $id;
 
   public function shouldRequireAdmin() {
     return false;
   }
 
   public function willProcessRequest(array $data) {
     $this->id = $data['id'];
   }
 
   public function processRequest() {
     $request = $this->getRequest();
     $viewer = $request->getUser();
 
     $user = id(new PhabricatorPeopleQuery())
       ->setViewer($viewer)
       ->withIDs(array($this->id))
       ->needProfileImage(true)
       ->requireCapabilities(
         array(
           PhabricatorPolicyCapability::CAN_VIEW,
           PhabricatorPolicyCapability::CAN_EDIT,
         ))
       ->executeOne();
     if (!$user) {
       return new Aphront404Response();
     }
 
     $profile_uri = '/p/'.$user->getUsername().'/';
 
     $supported_formats = PhabricatorFile::getTransformableImageFormats();
     $e_file = true;
     $errors = array();
 
     if ($request->isFormPost()) {
       $phid = $request->getStr('phid');
       $is_default = false;
       if ($phid == PhabricatorPHIDConstants::PHID_VOID) {
         $phid = null;
         $is_default = true;
       } else if ($phid) {
         $file = id(new PhabricatorFileQuery())
           ->setViewer($viewer)
           ->withPHIDs(array($phid))
           ->executeOne();
       } else {
         if ($request->getFileExists('picture')) {
           $file = PhabricatorFile::newFromPHPUpload(
             $_FILES['picture'],
             array(
               'authorPHID' => $viewer->getPHID(),
               'canCDN' => true,
             ));
         } else {
           $e_file = pht('Required');
           $errors[] = pht(
             'You must choose a file when uploading a new profile picture.');
         }
       }
 
       if (!$errors && !$is_default) {
         if (!$file->isTransformableImage()) {
           $e_file = pht('Not Supported');
           $errors[] = pht(
             'This server only supports these image formats: %s.',
             implode(', ', $supported_formats));
         } else {
-          $xformer = new PhabricatorImageTransformer();
-          $xformed = $xformer->executeProfileTransform(
-            $file,
-            $width = 50,
-            $min_height = 50,
-            $max_height = 50);
+          $xform = PhabricatorFileTransform::getTransformByKey(
+            PhabricatorFileThumbnailTransform::TRANSFORM_PROFILE);
+          $xformed = $xform->executeTransform($file);
         }
       }
 
       if (!$errors) {
         if ($is_default) {
           $user->setProfileImagePHID(null);
         } else {
           $user->setProfileImagePHID($xformed->getPHID());
           $xformed->attachToObject($user->getPHID());
         }
         $user->save();
         return id(new AphrontRedirectResponse())->setURI($profile_uri);
       }
     }
 
     $title = pht('Edit Profile Picture');
 
     $form = id(new PHUIFormLayoutView())
       ->setUser($viewer);
 
     $default_image = PhabricatorFile::loadBuiltin($viewer, 'profile.png');
 
     $images = array();
 
     $current = $user->getProfileImagePHID();
     $has_current = false;
     if ($current) {
       $files = id(new PhabricatorFileQuery())
         ->setViewer($viewer)
         ->withPHIDs(array($current))
         ->execute();
       if ($files) {
         $file = head($files);
         if ($file->isTransformableImage()) {
           $has_current = true;
           $images[$current] = array(
             'uri' => $file->getBestURI(),
             'tip' => pht('Current Picture'),
           );
         }
       }
     }
 
     // Try to add external account images for any associated external accounts.
     $accounts = id(new PhabricatorExternalAccountQuery())
       ->setViewer($viewer)
       ->withUserPHIDs(array($user->getPHID()))
       ->needImages(true)
       ->requireCapabilities(
         array(
           PhabricatorPolicyCapability::CAN_VIEW,
           PhabricatorPolicyCapability::CAN_EDIT,
         ))
       ->execute();
 
     foreach ($accounts as $account) {
       $file = $account->getProfileImageFile();
       if ($account->getProfileImagePHID() != $file->getPHID()) {
         // This is a default image, just skip it.
         continue;
       }
 
       $provider = PhabricatorAuthProvider::getEnabledProviderByKey(
         $account->getProviderKey());
       if ($provider) {
         $tip = pht('Picture From %s', $provider->getProviderName());
       } else {
         $tip = pht('Picture From External Account');
       }
 
       if ($file->isTransformableImage()) {
         $images[$file->getPHID()] = array(
           'uri' => $file->getBestURI(),
           'tip' => $tip,
         );
       }
     }
 
     $images[PhabricatorPHIDConstants::PHID_VOID] = array(
       'uri' => $default_image->getBestURI(),
       'tip' => pht('Default Picture'),
     );
 
     require_celerity_resource('people-profile-css');
     Javelin::initBehavior('phabricator-tooltips', array());
 
     $buttons = array();
     foreach ($images as $phid => $spec) {
       $button = javelin_tag(
         'button',
         array(
           'class' => 'grey profile-image-button',
           'sigil' => 'has-tooltip',
           'meta' => array(
             'tip' => $spec['tip'],
             'size' => 300,
           ),
         ),
         phutil_tag(
           'img',
           array(
             'height' => 50,
             'width' => 50,
             'src' => $spec['uri'],
           )));
 
       $button = array(
         phutil_tag(
           'input',
           array(
             'type'  => 'hidden',
             'name'  => 'phid',
             'value' => $phid,
           )),
         $button,
       );
 
       $button = phabricator_form(
         $viewer,
         array(
           'class' => 'profile-image-form',
           'method' => 'POST',
         ),
         $button);
 
       $buttons[] = $button;
     }
 
     if ($has_current) {
       $form->appendChild(
         id(new AphrontFormMarkupControl())
           ->setLabel(pht('Current Picture'))
           ->setValue(array_shift($buttons)));
     }
 
     $form->appendChild(
       id(new AphrontFormMarkupControl())
         ->setLabel(pht('Use Picture'))
         ->setValue($buttons));
 
     $form_box = id(new PHUIObjectBoxView())
       ->setHeaderText($title)
       ->setFormErrors($errors)
       ->setForm($form);
 
     $upload_form = id(new AphrontFormView())
       ->setUser($viewer)
       ->setEncType('multipart/form-data')
       ->appendChild(
         id(new AphrontFormFileControl())
           ->setName('picture')
           ->setLabel(pht('Upload Picture'))
           ->setError($e_file)
           ->setCaption(
             pht('Supported formats: %s', implode(', ', $supported_formats))))
       ->appendChild(
         id(new AphrontFormSubmitControl())
           ->addCancelButton($profile_uri)
           ->setValue(pht('Upload Picture')));
 
     $upload_box = id(new PHUIObjectBoxView())
       ->setHeaderText(pht('Upload New Picture'))
       ->setForm($upload_form);
 
     $nav = $this->buildIconNavView($user);
     $nav->selectFilter('/');
     $nav->appendChild($form_box);
     $nav->appendChild($upload_box);
 
     return $this->buildApplicationPage(
       $nav,
       array(
         'title' => $title,
       ));
   }
 }
diff --git a/src/applications/people/editor/__tests__/PhabricatorUserEditorTestCase.php b/src/applications/people/editor/__tests__/PhabricatorUserEditorTestCase.php
index f5c3c73a9a..6cc6abf37b 100644
--- a/src/applications/people/editor/__tests__/PhabricatorUserEditorTestCase.php
+++ b/src/applications/people/editor/__tests__/PhabricatorUserEditorTestCase.php
@@ -1,90 +1,90 @@
 <?php
 
 final class PhabricatorUserEditorTestCase extends PhabricatorTestCase {
 
   protected function getPhabricatorTestCaseConfiguration() {
     return array(
       self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
     );
   }
 
   public function testRegistrationEmailOK() {
     $env = PhabricatorEnv::beginScopedEnv();
     $env->overrideEnvConfig('auth.email-domains', array('example.com'));
 
     $this->registerUser(
       'PhabricatorUserEditorTestCaseOK',
-      'PhabricatorUserEditorTestCase@example.com');
+      'PhabricatorUserEditorTest@example.com');
 
     $this->assertTrue(true);
   }
 
   public function testRegistrationEmailInvalid() {
     $env = PhabricatorEnv::beginScopedEnv();
     $env->overrideEnvConfig('auth.email-domains', array('example.com'));
 
     $prefix = str_repeat('a', PhabricatorUserEmail::MAX_ADDRESS_LENGTH);
     $email = $prefix.'@evil.com@example.com';
 
     try {
       $this->registerUser(
         'PhabricatorUserEditorTestCaseInvalid',
         $email);
     } catch (Exception $ex) {
       $caught = $ex;
     }
 
     $this->assertTrue($caught instanceof Exception);
   }
 
   public function testRegistrationEmailDomain() {
     $env = PhabricatorEnv::beginScopedEnv();
     $env->overrideEnvConfig('auth.email-domains', array('example.com'));
 
     $caught = null;
     try {
       $this->registerUser(
         'PhabricatorUserEditorTestCaseDomain',
-        'PhabricatorUserEditorTestCase@whitehouse.gov');
+        'PhabricatorUserEditorTest@whitehouse.gov');
     } catch (Exception $ex) {
       $caught = $ex;
     }
 
     $this->assertTrue($caught instanceof Exception);
   }
 
   public function testRegistrationEmailApplicationEmailCollide() {
     $app_email = 'bugs@whitehouse.gov';
     $app_email_object =
       PhabricatorMetaMTAApplicationEmail::initializeNewAppEmail(
         $this->generateNewTestUser());
     $app_email_object->setAddress($app_email);
     $app_email_object->setApplicationPHID('test');
     $app_email_object->save();
 
     $caught = null;
     try {
       $this->registerUser(
         'PhabricatorUserEditorTestCaseDomain',
         $app_email);
     } catch (Exception $ex) {
       $caught = $ex;
     }
     $this->assertTrue($caught instanceof Exception);
   }
 
   private function registerUser($username, $email) {
     $user = id(new PhabricatorUser())
       ->setUsername($username)
       ->setRealname($username);
 
     $email = id(new PhabricatorUserEmail())
       ->setAddress($email)
       ->setIsVerified(0);
 
     id(new PhabricatorUserEditor())
       ->setActor($user)
       ->createNewUser($user, $email);
   }
 
 }
diff --git a/src/applications/people/query/PhabricatorPeopleQuery.php b/src/applications/people/query/PhabricatorPeopleQuery.php
index ac9d8b0200..b8b7ad1e29 100644
--- a/src/applications/people/query/PhabricatorPeopleQuery.php
+++ b/src/applications/people/query/PhabricatorPeopleQuery.php
@@ -1,350 +1,379 @@
 <?php
 
 final class PhabricatorPeopleQuery
   extends PhabricatorCursorPagedPolicyAwareQuery {
 
   private $usernames;
   private $realnames;
   private $emails;
   private $phids;
   private $ids;
   private $dateCreatedAfter;
   private $dateCreatedBefore;
   private $isAdmin;
   private $isSystemAgent;
   private $isDisabled;
   private $isApproved;
   private $nameLike;
   private $nameTokens;
 
   private $needPrimaryEmail;
   private $needProfile;
   private $needProfileImage;
   private $needStatus;
 
   public function withIDs(array $ids) {
     $this->ids = $ids;
     return $this;
   }
 
   public function withPHIDs(array $phids) {
     $this->phids = $phids;
     return $this;
   }
 
   public function withEmails(array $emails) {
     $this->emails = $emails;
     return $this;
   }
 
   public function withRealnames(array $realnames) {
     $this->realnames = $realnames;
     return $this;
   }
 
   public function withUsernames(array $usernames) {
     $this->usernames = $usernames;
     return $this;
   }
 
   public function withDateCreatedBefore($date_created_before) {
     $this->dateCreatedBefore = $date_created_before;
     return $this;
   }
 
   public function withDateCreatedAfter($date_created_after) {
     $this->dateCreatedAfter = $date_created_after;
     return $this;
   }
 
   public function withIsAdmin($admin) {
     $this->isAdmin = $admin;
     return $this;
   }
 
   public function withIsSystemAgent($system_agent) {
     $this->isSystemAgent = $system_agent;
     return $this;
   }
 
   public function withIsDisabled($disabled) {
     $this->isDisabled = $disabled;
     return $this;
   }
 
   public function withIsApproved($approved) {
     $this->isApproved = $approved;
     return $this;
   }
 
   public function withNameLike($like) {
     $this->nameLike = $like;
     return $this;
   }
 
   public function withNameTokens(array $tokens) {
     $this->nameTokens = array_values($tokens);
     return $this;
   }
 
   public function needPrimaryEmail($need) {
     $this->needPrimaryEmail = $need;
     return $this;
   }
 
   public function needProfile($need) {
     $this->needProfile = $need;
     return $this;
   }
 
   public function needProfileImage($need) {
     $this->needProfileImage = $need;
     return $this;
   }
 
   public function needStatus($need) {
     $this->needStatus = $need;
     return $this;
   }
 
   protected function loadPage() {
     $table  = new PhabricatorUser();
     $conn_r = $table->establishConnection('r');
 
     $data = queryfx_all(
       $conn_r,
       'SELECT * FROM %T user %Q %Q %Q %Q %Q',
       $table->getTableName(),
       $this->buildJoinsClause($conn_r),
       $this->buildWhereClause($conn_r),
       $this->buildGroupClause($conn_r),
       $this->buildOrderClause($conn_r),
       $this->buildLimitClause($conn_r));
 
     if ($this->needPrimaryEmail) {
       $table->putInSet(new LiskDAOSet());
     }
 
     return $table->loadAllFromArray($data);
   }
 
   protected function didFilterPage(array $users) {
     if ($this->needProfile) {
       $user_list = mpull($users, null, 'getPHID');
       $profiles = new PhabricatorUserProfile();
       $profiles = $profiles->loadAllWhere('userPHID IN (%Ls)',
         array_keys($user_list));
 
       $profiles = mpull($profiles, null, 'getUserPHID');
       foreach ($user_list as $user_phid => $user) {
         $profile = idx($profiles, $user_phid);
         if (!$profile) {
           $profile = new PhabricatorUserProfile();
           $profile->setUserPHID($user_phid);
         }
 
         $user->attachUserProfile($profile);
       }
     }
 
     if ($this->needProfileImage) {
-      $user_profile_file_phids = mpull($users, 'getProfileImagePHID');
-      $user_profile_file_phids = array_filter($user_profile_file_phids);
-      if ($user_profile_file_phids) {
-        $files = id(new PhabricatorFileQuery())
-          ->setParentQuery($this)
-          ->setViewer($this->getViewer())
-          ->withPHIDs($user_profile_file_phids)
-          ->execute();
-        $files = mpull($files, null, 'getPHID');
-      } else {
-        $files = array();
-      }
+      $rebuild = array();
       foreach ($users as $user) {
-        $image_phid = $user->getProfileImagePHID();
-        if (isset($files[$image_phid])) {
-          $profile_image_uri = $files[$image_phid]->getBestURI();
+        $image_uri = $user->getProfileImageCache();
+        if ($image_uri) {
+          // This user has a valid cache, so we don't need to fetch any
+          // data or rebuild anything.
+
+          $user->attachProfileImageURI($image_uri);
+          continue;
+        }
+
+        // This user's cache is invalid or missing, so we're going to rebuild
+        // it.
+        $rebuild[] = $user;
+      }
+
+      if ($rebuild) {
+        $file_phids = mpull($rebuild, 'getProfileImagePHID');
+        $file_phids = array_filter($file_phids);
+
+        if ($file_phids) {
+          // NOTE: We're using the omnipotent user here because older profile
+          // images do not have the 'profile' flag, so they may not be visible
+          // to the executing viewer. At some point, we could migrate to add
+          // this flag and then use the real viewer, or just use the real
+          // viewer after enough time has passed to limit the impact of old
+          // data. The consequence of missing here is that we cache a default
+          // image when a real image exists.
+          $files = id(new PhabricatorFileQuery())
+            ->setParentQuery($this)
+            ->setViewer(PhabricatorUser::getOmnipotentUser())
+            ->withPHIDs($file_phids)
+            ->execute();
+          $files = mpull($files, null, 'getPHID');
         } else {
-          $profile_image_uri = PhabricatorUser::getDefaultProfileImageURI();
+          $files = array();
+        }
+
+        foreach ($rebuild as $user) {
+          $image_phid = $user->getProfileImagePHID();
+          if (isset($files[$image_phid])) {
+            $image_uri = $files[$image_phid]->getBestURI();
+          } else {
+            $image_uri = PhabricatorUser::getDefaultProfileImageURI();
+          }
+
+          $user->writeProfileImageCache($image_uri);
+          $user->attachProfileImageURI($image_uri);
         }
-        $user->attachProfileImageURI($profile_image_uri);
       }
     }
 
     if ($this->needStatus) {
       $user_list = mpull($users, null, 'getPHID');
       $statuses = id(new PhabricatorCalendarEvent())->loadCurrentStatuses(
         array_keys($user_list));
       foreach ($user_list as $phid => $user) {
         $status = idx($statuses, $phid);
         if ($status) {
           $user->attachStatus($status);
         }
       }
     }
 
     return $users;
   }
 
   protected function buildGroupClause(AphrontDatabaseConnection $conn) {
     if ($this->nameTokens) {
       return qsprintf(
         $conn,
         'GROUP BY user.id');
     } else {
       return $this->buildApplicationSearchGroupClause($conn);
     }
   }
 
   private function buildJoinsClause($conn_r) {
     $joins = array();
 
     if ($this->emails) {
       $email_table = new PhabricatorUserEmail();
       $joins[] = qsprintf(
         $conn_r,
         'JOIN %T email ON email.userPHID = user.PHID',
         $email_table->getTableName());
     }
 
     if ($this->nameTokens) {
       foreach ($this->nameTokens as $key => $token) {
         $token_table = 'token_'.$key;
         $joins[] = qsprintf(
           $conn_r,
           'JOIN %T %T ON %T.userID = user.id AND %T.token LIKE %>',
           PhabricatorUser::NAMETOKEN_TABLE,
           $token_table,
           $token_table,
           $token_table,
           $token);
       }
     }
 
     $joins[] = $this->buildApplicationSearchJoinClause($conn_r);
 
     $joins = implode(' ', $joins);
     return  $joins;
   }
 
   protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
     $where = array();
 
     if ($this->usernames !== null) {
       $where[] = qsprintf(
         $conn_r,
         'user.userName IN (%Ls)',
         $this->usernames);
     }
 
     if ($this->emails !== null) {
       $where[] = qsprintf(
         $conn_r,
         'email.address IN (%Ls)',
         $this->emails);
     }
 
     if ($this->realnames !== null) {
       $where[] = qsprintf(
         $conn_r,
         'user.realName IN (%Ls)',
         $this->realnames);
     }
 
     if ($this->phids !== null) {
       $where[] = qsprintf(
         $conn_r,
         'user.phid IN (%Ls)',
         $this->phids);
     }
 
     if ($this->ids !== null) {
       $where[] = qsprintf(
         $conn_r,
         'user.id IN (%Ld)',
         $this->ids);
     }
 
     if ($this->dateCreatedAfter) {
       $where[] = qsprintf(
         $conn_r,
         'user.dateCreated >= %d',
         $this->dateCreatedAfter);
     }
 
     if ($this->dateCreatedBefore) {
       $where[] = qsprintf(
         $conn_r,
         'user.dateCreated <= %d',
         $this->dateCreatedBefore);
     }
 
     if ($this->isAdmin) {
       $where[] = qsprintf(
         $conn_r,
         'user.isAdmin = 1');
     }
 
     if ($this->isDisabled !== null) {
       $where[] = qsprintf(
         $conn_r,
         'user.isDisabled = %d',
         (int)$this->isDisabled);
     }
 
     if ($this->isApproved !== null) {
       $where[] = qsprintf(
         $conn_r,
         'user.isApproved = %d',
         (int)$this->isApproved);
     }
 
     if ($this->isSystemAgent) {
       $where[] = qsprintf(
         $conn_r,
         'user.isSystemAgent = 1');
     }
 
     if (strlen($this->nameLike)) {
       $where[] = qsprintf(
         $conn_r,
         'user.username LIKE %~ OR user.realname LIKE %~',
         $this->nameLike,
         $this->nameLike);
     }
 
     $where[] = $this->buildPagingClause($conn_r);
 
     return $this->formatWhereClause($where);
   }
 
   protected function getPrimaryTableAlias() {
     return 'user';
   }
 
   public function getQueryApplicationClass() {
     return 'PhabricatorPeopleApplication';
   }
 
   public function getOrderableColumns() {
     return parent::getOrderableColumns() + array(
       'username' => array(
         'table' => 'user',
         'column' => 'username',
         'type' => 'string',
         'reverse' => true,
         'unique' => true,
       ),
     );
   }
 
   protected function getPagingValueMap($cursor, array $keys) {
     $user = $this->loadCursorObject($cursor);
     return array(
       'id' => $user->getID(),
       'username' => $user->getUsername(),
     );
   }
 
 
 }
diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php
index ea138042b2..3fbfac062f 100644
--- a/src/applications/people/storage/PhabricatorUser.php
+++ b/src/applications/people/storage/PhabricatorUser.php
@@ -1,992 +1,1068 @@
 <?php
 
 /**
+ * @task image-cache Profile Image Cache
  * @task factors Multi-Factor Authentication
  * @task handles Managing Handles
  */
 final class PhabricatorUser
   extends PhabricatorUserDAO
   implements
     PhutilPerson,
     PhabricatorPolicyInterface,
     PhabricatorCustomFieldInterface,
     PhabricatorDestructibleInterface,
     PhabricatorSSHPublicKeyInterface {
 
   const SESSION_TABLE = 'phabricator_session';
   const NAMETOKEN_TABLE = 'user_nametoken';
   const MAXIMUM_USERNAME_LENGTH = 64;
 
   protected $userName;
   protected $realName;
   protected $sex;
   protected $translation;
   protected $passwordSalt;
   protected $passwordHash;
   protected $profileImagePHID;
+  protected $profileImageCache;
   protected $timezoneIdentifier = '';
 
   protected $consoleEnabled = 0;
   protected $consoleVisible = 0;
   protected $consoleTab = '';
 
   protected $conduitCertificate;
 
   protected $isSystemAgent = 0;
   protected $isAdmin = 0;
   protected $isDisabled = 0;
   protected $isEmailVerified = 0;
   protected $isApproved = 0;
   protected $isEnrolledInMultiFactor = 0;
 
   protected $accountSecret;
 
   private $profileImage = self::ATTACHABLE;
   private $profile = null;
   private $status = self::ATTACHABLE;
   private $preferences = null;
   private $omnipotent = false;
   private $customFields = self::ATTACHABLE;
 
   private $alternateCSRFString = self::ATTACHABLE;
   private $session = self::ATTACHABLE;
 
   private $authorities = array();
   private $handlePool;
 
   protected function readField($field) {
     switch ($field) {
       case 'timezoneIdentifier':
         // If the user hasn't set one, guess the server's time.
         return nonempty(
           $this->timezoneIdentifier,
           date_default_timezone_get());
       // Make sure these return booleans.
       case 'isAdmin':
         return (bool)$this->isAdmin;
       case 'isDisabled':
         return (bool)$this->isDisabled;
       case 'isSystemAgent':
         return (bool)$this->isSystemAgent;
       case 'isEmailVerified':
         return (bool)$this->isEmailVerified;
       case 'isApproved':
         return (bool)$this->isApproved;
       default:
         return parent::readField($field);
     }
   }
 
 
   /**
    * Is this a live account which has passed required approvals? Returns true
    * if this is an enabled, verified (if required), approved (if required)
    * account, and false otherwise.
    *
    * @return bool True if this is a standard, usable account.
    */
   public function isUserActivated() {
     if ($this->isOmnipotent()) {
       return true;
     }
 
     if ($this->getIsDisabled()) {
       return false;
     }
 
     if (!$this->getIsApproved()) {
       return false;
     }
 
     if (PhabricatorUserEmail::isEmailVerificationRequired()) {
       if (!$this->getIsEmailVerified()) {
         return false;
       }
     }
 
     return true;
   }
 
   /**
    * Returns `true` if this is a standard user who is logged in. Returns `false`
    * for logged out, anonymous, or external users.
    *
    * @return bool `true` if the user is a standard user who is logged in with
    *              a normal session.
    */
   public function getIsStandardUser() {
     $type_user = PhabricatorPeopleUserPHIDType::TYPECONST;
     return $this->getPHID() && (phid_get_type($this->getPHID()) == $type_user);
   }
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_COLUMN_SCHEMA => array(
         'userName' => 'sort64',
         'realName' => 'text128',
         'sex' => 'text4?',
         'translation' => 'text64?',
         'passwordSalt' => 'text32?',
         'passwordHash' => 'text128?',
         'profileImagePHID' => 'phid?',
         'consoleEnabled' => 'bool',
         'consoleVisible' => 'bool',
         'consoleTab' => 'text64',
         'conduitCertificate' => 'text255',
         'isSystemAgent' => 'bool',
         'isDisabled' => 'bool',
         'isAdmin' => 'bool',
         'timezoneIdentifier' => 'text255',
         'isEmailVerified' => 'uint32',
         'isApproved' => 'uint32',
         'accountSecret' => 'bytes64',
         'isEnrolledInMultiFactor' => 'bool',
+        'profileImageCache' => 'text255?',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_phid' => null,
         'phid' => array(
           'columns' => array('phid'),
           'unique' => true,
         ),
         'userName' => array(
           'columns' => array('userName'),
           'unique' => true,
         ),
         'realName' => array(
           'columns' => array('realName'),
         ),
         'key_approved' => array(
           'columns' => array('isApproved'),
         ),
       ),
+      self::CONFIG_NO_MUTATE => array(
+        'profileImageCache' => true,
+      ),
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       PhabricatorPeopleUserPHIDType::TYPECONST);
   }
 
   public function setPassword(PhutilOpaqueEnvelope $envelope) {
     if (!$this->getPHID()) {
       throw new Exception(
         'You can not set a password for an unsaved user because their PHID '.
         'is a salt component in the password hash.');
     }
 
     if (!strlen($envelope->openEnvelope())) {
       $this->setPasswordHash('');
     } else {
       $this->setPasswordSalt(md5(Filesystem::readRandomBytes(32)));
       $hash = $this->hashPassword($envelope);
       $this->setPasswordHash($hash->openEnvelope());
     }
     return $this;
   }
 
   // To satisfy PhutilPerson.
   public function getSex() {
     return $this->sex;
   }
 
   public function getMonogram() {
     return '@'.$this->getUsername();
   }
 
   public function isLoggedIn() {
     return !($this->getPHID() === null);
   }
 
   public function save() {
     if (!$this->getConduitCertificate()) {
       $this->setConduitCertificate($this->generateConduitCertificate());
     }
 
     if (!strlen($this->getAccountSecret())) {
       $this->setAccountSecret(Filesystem::readRandomCharacters(64));
     }
 
     $result = parent::save();
 
     if ($this->profile) {
       $this->profile->save();
     }
 
     $this->updateNameTokens();
 
     id(new PhabricatorSearchIndexer())
       ->queueDocumentForIndexing($this->getPHID());
 
     return $result;
   }
 
   public function attachSession(PhabricatorAuthSession $session) {
     $this->session = $session;
     return $this;
   }
 
   public function getSession() {
     return $this->assertAttached($this->session);
   }
 
   public function hasSession() {
     return ($this->session !== self::ATTACHABLE);
   }
 
   private function generateConduitCertificate() {
     return Filesystem::readRandomCharacters(255);
   }
 
   public function comparePassword(PhutilOpaqueEnvelope $envelope) {
     if (!strlen($envelope->openEnvelope())) {
       return false;
     }
     if (!strlen($this->getPasswordHash())) {
       return false;
     }
 
     return PhabricatorPasswordHasher::comparePassword(
       $this->getPasswordHashInput($envelope),
       new PhutilOpaqueEnvelope($this->getPasswordHash()));
   }
 
   private function getPasswordHashInput(PhutilOpaqueEnvelope $password) {
     $input =
       $this->getUsername().
       $password->openEnvelope().
       $this->getPHID().
       $this->getPasswordSalt();
 
     return new PhutilOpaqueEnvelope($input);
   }
 
   private function hashPassword(PhutilOpaqueEnvelope $password) {
     $hasher = PhabricatorPasswordHasher::getBestHasher();
 
     $input_envelope = $this->getPasswordHashInput($password);
     return $hasher->getPasswordHashForStorage($input_envelope);
   }
 
   const CSRF_CYCLE_FREQUENCY  = 3600;
   const CSRF_SALT_LENGTH      = 8;
   const CSRF_TOKEN_LENGTH     = 16;
   const CSRF_BREACH_PREFIX    = 'B@';
 
   const EMAIL_CYCLE_FREQUENCY = 86400;
   const EMAIL_TOKEN_LENGTH    = 24;
 
   private function getRawCSRFToken($offset = 0) {
     return $this->generateToken(
       time() + (self::CSRF_CYCLE_FREQUENCY * $offset),
       self::CSRF_CYCLE_FREQUENCY,
       PhabricatorEnv::getEnvConfig('phabricator.csrf-key'),
       self::CSRF_TOKEN_LENGTH);
   }
 
   /**
    * @phutil-external-symbol class PhabricatorStartup
    */
   public function getCSRFToken() {
     $salt = PhabricatorStartup::getGlobal('csrf.salt');
     if (!$salt) {
       $salt = Filesystem::readRandomCharacters(self::CSRF_SALT_LENGTH);
       PhabricatorStartup::setGlobal('csrf.salt', $salt);
     }
 
     // Generate a token hash to mitigate BREACH attacks against SSL. See
     // discussion in T3684.
     $token = $this->getRawCSRFToken();
     $hash = PhabricatorHash::digest($token, $salt);
     return 'B@'.$salt.substr($hash, 0, self::CSRF_TOKEN_LENGTH);
   }
 
   public function validateCSRFToken($token) {
     $salt = null;
     $version = 'plain';
 
     // This is a BREACH-mitigating token. See T3684.
     $breach_prefix = self::CSRF_BREACH_PREFIX;
     $breach_prelen = strlen($breach_prefix);
 
     if (!strncmp($token, $breach_prefix, $breach_prelen)) {
       $version = 'breach';
       $salt = substr($token, $breach_prelen, self::CSRF_SALT_LENGTH);
       $token = substr($token, $breach_prelen + self::CSRF_SALT_LENGTH);
     }
 
     // When the user posts a form, we check that it contains a valid CSRF token.
     // Tokens cycle each hour (every CSRF_CYLCE_FREQUENCY seconds) and we accept
     // either the current token, the next token (users can submit a "future"
     // token if you have two web frontends that have some clock skew) or any of
     // the last 6 tokens. This means that pages are valid for up to 7 hours.
     // There is also some Javascript which periodically refreshes the CSRF
     // tokens on each page, so theoretically pages should be valid indefinitely.
     // However, this code may fail to run (if the user loses their internet
     // connection, or there's a JS problem, or they don't have JS enabled).
     // Choosing the size of the window in which we accept old CSRF tokens is
     // an issue of balancing concerns between security and usability. We could
     // choose a very narrow (e.g., 1-hour) window to reduce vulnerability to
     // attacks using captured CSRF tokens, but it's also more likely that real
     // users will be affected by this, e.g. if they close their laptop for an
     // hour, open it back up, and try to submit a form before the CSRF refresh
     // can kick in. Since the user experience of submitting a form with expired
     // CSRF is often quite bad (you basically lose data, or it's a big pain to
     // recover at least) and I believe we gain little additional protection
     // by keeping the window very short (the overwhelming value here is in
     // preventing blind attacks, and most attacks which can capture CSRF tokens
     // can also just capture authentication information [sniffing networks]
     // or act as the user [xss]) the 7 hour default seems like a reasonable
     // balance. Other major platforms have much longer CSRF token lifetimes,
     // like Rails (session duration) and Django (forever), which suggests this
     // is a reasonable analysis.
     $csrf_window = 6;
 
     for ($ii = -$csrf_window; $ii <= 1; $ii++) {
       $valid = $this->getRawCSRFToken($ii);
       switch ($version) {
         // TODO: We can remove this after the BREACH version has been in the
         // wild for a while.
         case 'plain':
           if ($token == $valid) {
             return true;
           }
           break;
         case 'breach':
           $digest = PhabricatorHash::digest($valid, $salt);
           if (substr($digest, 0, self::CSRF_TOKEN_LENGTH) == $token) {
             return true;
           }
           break;
         default:
           throw new Exception('Unknown CSRF token format!');
       }
     }
 
     return false;
   }
 
   private function generateToken($epoch, $frequency, $key, $len) {
     if ($this->getPHID()) {
       $vec = $this->getPHID().$this->getAccountSecret();
     } else {
       $vec = $this->getAlternateCSRFString();
     }
 
     if ($this->hasSession()) {
       $vec = $vec.$this->getSession()->getSessionKey();
     }
 
     $time_block = floor($epoch / $frequency);
     $vec = $vec.$key.$time_block;
 
     return substr(PhabricatorHash::digest($vec), 0, $len);
   }
 
   public function attachUserProfile(PhabricatorUserProfile $profile) {
     $this->profile = $profile;
     return $this;
   }
 
   public function loadUserProfile() {
     if ($this->profile) {
       return $this->profile;
     }
 
     $profile_dao = new PhabricatorUserProfile();
     $this->profile = $profile_dao->loadOneWhere('userPHID = %s',
       $this->getPHID());
 
     if (!$this->profile) {
       $profile_dao->setUserPHID($this->getPHID());
       $this->profile = $profile_dao;
     }
 
     return $this->profile;
   }
 
   public function loadPrimaryEmailAddress() {
     $email = $this->loadPrimaryEmail();
     if (!$email) {
       throw new Exception('User has no primary email address!');
     }
     return $email->getAddress();
   }
 
   public function loadPrimaryEmail() {
     return $this->loadOneRelative(
       new PhabricatorUserEmail(),
       'userPHID',
       'getPHID',
       '(isPrimary = 1)');
   }
 
   public function loadPreferences() {
     if ($this->preferences) {
       return $this->preferences;
     }
 
     $preferences = null;
     if ($this->getPHID()) {
       $preferences = id(new PhabricatorUserPreferences())->loadOneWhere(
         'userPHID = %s',
         $this->getPHID());
     }
 
     if (!$preferences) {
       $preferences = new PhabricatorUserPreferences();
       $preferences->setUserPHID($this->getPHID());
 
       $default_dict = array(
         PhabricatorUserPreferences::PREFERENCE_TITLES => 'glyph',
         PhabricatorUserPreferences::PREFERENCE_EDITOR => '',
         PhabricatorUserPreferences::PREFERENCE_MONOSPACED => '',
         PhabricatorUserPreferences::PREFERENCE_DARK_CONSOLE => 0,
       );
 
       $preferences->setPreferences($default_dict);
     }
 
     $this->preferences = $preferences;
     return $preferences;
   }
 
   public function loadEditorLink($path, $line, $callsign) {
     $editor = $this->loadPreferences()->getPreference(
       PhabricatorUserPreferences::PREFERENCE_EDITOR);
 
     if (is_array($path)) {
       $multiedit = $this->loadPreferences()->getPreference(
         PhabricatorUserPreferences::PREFERENCE_MULTIEDIT);
       switch ($multiedit) {
         case '':
           $path = implode(' ', $path);
           break;
         case 'disable':
           return null;
       }
     }
 
     if (!strlen($editor)) {
       return null;
     }
 
     $uri = strtr($editor, array(
       '%%' => '%',
       '%f' => phutil_escape_uri($path),
       '%l' => phutil_escape_uri($line),
       '%r' => phutil_escape_uri($callsign),
     ));
 
     // The resulting URI must have an allowed protocol. Otherwise, we'll return
     // a link to an error page explaining the misconfiguration.
 
     $ok = PhabricatorHelpEditorProtocolController::hasAllowedProtocol($uri);
     if (!$ok) {
       return '/help/editorprotocol/';
     }
 
     return (string)$uri;
   }
 
   public function getAlternateCSRFString() {
     return $this->assertAttached($this->alternateCSRFString);
   }
 
   public function attachAlternateCSRFString($string) {
     $this->alternateCSRFString = $string;
     return $this;
   }
 
   /**
    * Populate the nametoken table, which used to fetch typeahead results. When
    * a user types "linc", we want to match "Abraham Lincoln" from on-demand
    * typeahead sources. To do this, we need a separate table of name fragments.
    */
   public function updateNameTokens() {
     $table  = self::NAMETOKEN_TABLE;
     $conn_w = $this->establishConnection('w');
 
     $tokens = PhabricatorTypeaheadDatasource::tokenizeString(
       $this->getUserName().' '.$this->getRealName());
 
     $sql = array();
     foreach ($tokens as $token) {
       $sql[] = qsprintf(
         $conn_w,
         '(%d, %s)',
         $this->getID(),
         $token);
     }
 
     queryfx(
       $conn_w,
       'DELETE FROM %T WHERE userID = %d',
       $table,
       $this->getID());
     if ($sql) {
       queryfx(
         $conn_w,
         'INSERT INTO %T (userID, token) VALUES %Q',
         $table,
         implode(', ', $sql));
     }
   }
 
   public function sendWelcomeEmail(PhabricatorUser $admin) {
     $admin_username = $admin->getUserName();
     $admin_realname = $admin->getRealName();
     $user_username = $this->getUserName();
     $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
 
     $base_uri = PhabricatorEnv::getProductionURI('/');
 
     $engine = new PhabricatorAuthSessionEngine();
     $uri = $engine->getOneTimeLoginURI(
       $this,
       $this->loadPrimaryEmail(),
       PhabricatorAuthSessionEngine::ONETIME_WELCOME);
 
     $body = <<<EOBODY
 Welcome to Phabricator!
 
 {$admin_username} ({$admin_realname}) has created an account for you.
 
   Username: {$user_username}
 
 To login to Phabricator, follow this link and set a password:
 
   {$uri}
 
 After you have set a password, you can login in the future by going here:
 
   {$base_uri}
 
 EOBODY;
 
     if (!$is_serious) {
       $body .= <<<EOBODY
 
 Love,
 Phabricator
 
 EOBODY;
     }
 
     $mail = id(new PhabricatorMetaMTAMail())
       ->addTos(array($this->getPHID()))
       ->setForceDelivery(true)
       ->setSubject('[Phabricator] Welcome to Phabricator')
       ->setBody($body)
       ->saveAndSend();
   }
 
   public function sendUsernameChangeEmail(
     PhabricatorUser $admin,
     $old_username) {
 
     $admin_username = $admin->getUserName();
     $admin_realname = $admin->getRealName();
     $new_username = $this->getUserName();
 
     $password_instructions = null;
     if (PhabricatorPasswordAuthProvider::getPasswordProvider()) {
       $engine = new PhabricatorAuthSessionEngine();
       $uri = $engine->getOneTimeLoginURI(
         $this,
         null,
         PhabricatorAuthSessionEngine::ONETIME_USERNAME);
       $password_instructions = <<<EOTXT
 If you use a password to login, you'll need to reset it before you can login
 again. You can reset your password by following this link:
 
   {$uri}
 
 And, of course, you'll need to use your new username to login from now on. If
 you use OAuth to login, nothing should change.
 
 EOTXT;
     }
 
     $body = <<<EOBODY
 {$admin_username} ({$admin_realname}) has changed your Phabricator username.
 
   Old Username: {$old_username}
   New Username: {$new_username}
 
 {$password_instructions}
 EOBODY;
 
     $mail = id(new PhabricatorMetaMTAMail())
       ->addTos(array($this->getPHID()))
       ->setForceDelivery(true)
       ->setSubject('[Phabricator] Username Changed')
       ->setBody($body)
       ->saveAndSend();
   }
 
   public static function describeValidUsername() {
     return pht(
       'Usernames must contain only numbers, letters, period, underscore and '.
       'hyphen, and can not end with a period. They must have no more than %d '.
       'characters.',
       new PhutilNumber(self::MAXIMUM_USERNAME_LENGTH));
   }
 
   public static function validateUsername($username) {
     // NOTE: If you update this, make sure to update:
     //
     //  - Remarkup rule for @mentions.
     //  - Routing rule for "/p/username/".
     //  - Unit tests, obviously.
     //  - describeValidUsername() method, above.
 
     if (strlen($username) > self::MAXIMUM_USERNAME_LENGTH) {
       return false;
     }
 
     return (bool)preg_match('/^[a-zA-Z0-9._-]*[a-zA-Z0-9_-]\z/', $username);
   }
 
   public static function getDefaultProfileImageURI() {
     return celerity_get_resource_uri('/rsrc/image/avatar.png');
   }
 
   public function attachStatus(PhabricatorCalendarEvent $status) {
     $this->status = $status;
     return $this;
   }
 
   public function getStatus() {
     return $this->assertAttached($this->status);
   }
 
   public function hasStatus() {
     return $this->status !== self::ATTACHABLE;
   }
 
   public function attachProfileImageURI($uri) {
     $this->profileImage = $uri;
     return $this;
   }
 
   public function getProfileImageURI() {
     return $this->assertAttached($this->profileImage);
   }
 
   public function getFullName() {
     if (strlen($this->getRealName())) {
       return $this->getUsername().' ('.$this->getRealName().')';
     } else {
       return $this->getUsername();
     }
   }
 
+  public function getTimeZone() {
+    return new DateTimeZone($this->getTimezoneIdentifier());
+  }
+
   public function __toString() {
     return $this->getUsername();
   }
 
   public static function loadOneWithEmailAddress($address) {
     $email = id(new PhabricatorUserEmail())->loadOneWhere(
       'address = %s',
       $address);
     if (!$email) {
       return null;
     }
     return id(new PhabricatorUser())->loadOneWhere(
       'phid = %s',
       $email->getUserPHID());
   }
 
 
   /**
    * Grant a user a source of authority, to let them bypass policy checks they
    * could not otherwise.
    */
   public function grantAuthority($authority) {
     $this->authorities[] = $authority;
     return $this;
   }
 
 
   /**
    * Get authorities granted to the user.
    */
   public function getAuthorities() {
     return $this->authorities;
   }
 
 
+/* -(  Profile Image Cache  )------------------------------------------------ */
+
+
+  /**
+   * Get this user's cached profile image URI.
+   *
+   * @return string|null Cached URI, if a URI is cached.
+   * @task image-cache
+   */
+  public function getProfileImageCache() {
+    $version = $this->getProfileImageVersion();
+
+    $parts = explode(',', $this->profileImageCache, 2);
+    if (count($parts) !== 2) {
+      return null;
+    }
+
+    if ($parts[0] !== $version) {
+      return null;
+    }
+
+    return $parts[1];
+  }
+
+
+  /**
+   * Generate a new cache value for this user's profile image.
+   *
+   * @return string New cache value.
+   * @task image-cache
+   */
+  public function writeProfileImageCache($uri) {
+    $version = $this->getProfileImageVersion();
+    $cache = "{$version},{$uri}";
+
+    $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
+    queryfx(
+      $this->establishConnection('w'),
+      'UPDATE %T SET profileImageCache = %s WHERE id = %d',
+      $this->getTableName(),
+      $cache,
+      $this->getID());
+    unset($unguarded);
+  }
+
+
+  /**
+   * Get a version identifier for a user's profile image.
+   *
+   * This version will change if the image changes, or if any of the
+   * environment configuration which goes into generating a URI changes.
+   *
+   * @return string Cache version.
+   * @task image-cache
+   */
+  private function getProfileImageVersion() {
+    $parts = array(
+      PhabricatorEnv::getCDNURI('/'),
+      PhabricatorEnv::getEnvConfig('cluster.instance'),
+      $this->getProfileImagePHID(),
+    );
+    $parts = serialize($parts);
+    return PhabricatorHash::digestForIndex($parts);
+  }
+
+
 /* -(  Multi-Factor Authentication  )---------------------------------------- */
 
 
   /**
    * Update the flag storing this user's enrollment in multi-factor auth.
    *
    * With certain settings, we need to check if a user has MFA on every page,
    * so we cache MFA enrollment on the user object for performance. Calling this
    * method synchronizes the cache by examining enrollment records. After
    * updating the cache, use @{method:getIsEnrolledInMultiFactor} to check if
    * the user is enrolled.
    *
    * This method should be called after any changes are made to a given user's
    * multi-factor configuration.
    *
    * @return void
    * @task factors
    */
   public function updateMultiFactorEnrollment() {
     $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
       'userPHID = %s',
       $this->getPHID());
 
     $enrolled = count($factors) ? 1 : 0;
     if ($enrolled !== $this->isEnrolledInMultiFactor) {
       $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
         queryfx(
           $this->establishConnection('w'),
           'UPDATE %T SET isEnrolledInMultiFactor = %d WHERE id = %d',
           $this->getTableName(),
           $enrolled,
           $this->getID());
       unset($unguarded);
 
       $this->isEnrolledInMultiFactor = $enrolled;
     }
   }
 
 
   /**
    * Check if the user is enrolled in multi-factor authentication.
    *
    * Enrolled users have one or more multi-factor authentication sources
    * attached to their account. For performance, this value is cached. You
    * can use @{method:updateMultiFactorEnrollment} to update the cache.
    *
    * @return bool True if the user is enrolled.
    * @task factors
    */
   public function getIsEnrolledInMultiFactor() {
     return $this->isEnrolledInMultiFactor;
   }
 
 
 /* -(  Omnipotence  )-------------------------------------------------------- */
 
 
   /**
    * Returns true if this user is omnipotent. Omnipotent users bypass all policy
    * checks.
    *
    * @return bool True if the user bypasses policy checks.
    */
   public function isOmnipotent() {
     return $this->omnipotent;
   }
 
 
   /**
    * Get an omnipotent user object for use in contexts where there is no acting
    * user, notably daemons.
    *
    * @return PhabricatorUser An omnipotent user.
    */
   public static function getOmnipotentUser() {
     static $user = null;
     if (!$user) {
       $user = new PhabricatorUser();
       $user->omnipotent = true;
       $user->makeEphemeral();
     }
     return $user;
   }
 
 
 /* -(  Managing Handles  )--------------------------------------------------- */
 
 
   /**
    * Get a @{class:PhabricatorHandleList} which benefits from this viewer's
    * internal handle pool.
    *
    * @param list<phid> List of PHIDs to load.
    * @return PhabricatorHandleList Handle list object.
    * @task handle
    */
   public function loadHandles(array $phids) {
     if ($this->handlePool === null) {
       $this->handlePool = id(new PhabricatorHandlePool())
         ->setViewer($this);
     }
 
     return $this->handlePool->newHandleList($phids);
   }
 
 
   /**
    * Get a @{class:PHUIHandleView} for a single handle.
    *
    * This benefits from the viewer's internal handle pool.
    *
    * @param phid PHID to render a handle for.
    * @return PHUIHandleView View of the handle.
    * @task handle
    */
   public function renderHandle($phid) {
     return $this->loadHandles(array($phid))->renderHandle($phid);
   }
 
 
   /**
    * Get a @{class:PHUIHandleListView} for a list of handles.
    *
    * This benefits from the viewer's internal handle pool.
    *
    * @param list<phid> List of PHIDs to render.
    * @return PHUIHandleListView View of the handles.
    * @task handle
    */
   public function renderHandleList(array $phids) {
     return $this->loadHandles($phids)->renderList();
   }
 
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
       PhabricatorPolicyCapability::CAN_EDIT,
     );
   }
 
   public function getPolicy($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         return PhabricatorPolicies::POLICY_PUBLIC;
       case PhabricatorPolicyCapability::CAN_EDIT:
         if ($this->getIsSystemAgent()) {
           return PhabricatorPolicies::POLICY_ADMIN;
         } else {
           return PhabricatorPolicies::POLICY_NOONE;
         }
     }
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     return $this->getPHID() && ($viewer->getPHID() === $this->getPHID());
   }
 
   public function describeAutomaticCapability($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_EDIT:
         return pht('Only you can edit your information.');
       default:
         return null;
     }
   }
 
 
 /* -(  PhabricatorCustomFieldInterface  )------------------------------------ */
 
 
   public function getCustomFieldSpecificationForRole($role) {
     return PhabricatorEnv::getEnvConfig('user.fields');
   }
 
   public function getCustomFieldBaseClass() {
     return 'PhabricatorUserCustomField';
   }
 
   public function getCustomFields() {
     return $this->assertAttached($this->customFields);
   }
 
   public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
     $this->customFields = $fields;
     return $this;
   }
 
 
 /* -(  PhabricatorDestructibleInterface  )----------------------------------- */
 
 
   public function destroyObjectPermanently(
     PhabricatorDestructionEngine $engine) {
 
     $this->openTransaction();
       $this->delete();
 
       $externals = id(new PhabricatorExternalAccount())->loadAllWhere(
         'userPHID = %s',
         $this->getPHID());
       foreach ($externals as $external) {
         $external->delete();
       }
 
       $prefs = id(new PhabricatorUserPreferences())->loadAllWhere(
         'userPHID = %s',
         $this->getPHID());
       foreach ($prefs as $pref) {
         $pref->delete();
       }
 
       $profiles = id(new PhabricatorUserProfile())->loadAllWhere(
         'userPHID = %s',
         $this->getPHID());
       foreach ($profiles as $profile) {
         $profile->delete();
       }
 
       $keys = id(new PhabricatorAuthSSHKey())->loadAllWhere(
         'objectPHID = %s',
         $this->getPHID());
       foreach ($keys as $key) {
         $key->delete();
       }
 
       $emails = id(new PhabricatorUserEmail())->loadAllWhere(
         'userPHID = %s',
         $this->getPHID());
       foreach ($emails as $email) {
         $email->delete();
       }
 
       $sessions = id(new PhabricatorAuthSession())->loadAllWhere(
         'userPHID = %s',
         $this->getPHID());
       foreach ($sessions as $session) {
         $session->delete();
       }
 
       $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
         'userPHID = %s',
         $this->getPHID());
       foreach ($factors as $factor) {
         $factor->delete();
       }
 
     $this->saveTransaction();
   }
 
 
 /* -(  PhabricatorSSHPublicKeyInterface  )----------------------------------- */
 
 
   public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer) {
     if ($viewer->getPHID() == $this->getPHID()) {
       // If the viewer is managing their own keys, take them to the normal
       // panel.
       return '/settings/panel/ssh/';
     } else {
       // Otherwise, take them to the administrative panel for this user.
       return '/settings/'.$this->getID().'/panel/ssh/';
     }
   }
 
   public function getSSHKeyDefaultName() {
     return 'id_rsa_phabricator';
   }
 
 }
diff --git a/src/applications/phame/application/PhabricatorPhameApplication.php b/src/applications/phame/application/PhabricatorPhameApplication.php
index b466ad5191..4d8cebffa0 100644
--- a/src/applications/phame/application/PhabricatorPhameApplication.php
+++ b/src/applications/phame/application/PhabricatorPhameApplication.php
@@ -1,72 +1,78 @@
 <?php
 
 final class PhabricatorPhameApplication extends PhabricatorApplication {
 
   public function getName() {
     return pht('Phame');
   }
 
   public function getBaseURI() {
     return '/phame/';
   }
 
   public function getFontIcon() {
     return 'fa-star';
   }
 
   public function getShortDescription() {
     return 'Blog';
   }
 
   public function getTitleGlyph() {
     return "\xe2\x9c\xa9";
   }
 
   public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
     return array(
       array(
         'name' => pht('Phame User Guide'),
         'href' => PhabricatorEnv::getDoclink('Phame User Guide'),
       ),
     );
   }
 
   public function isPrototype() {
     return true;
   }
 
   public function getRoutes() {
     return array(
      '/phame/' => array(
         '' => 'PhamePostListController',
         'r/(?P<id>\d+)/(?P<hash>[^/]+)/(?P<name>.*)'
           => 'PhameResourceController',
 
         'live/(?P<id>[^/]+)/(?P<more>.*)' => 'PhameBlogLiveController',
         'post/' => array(
           '(?:(?P<filter>draft|all)/)?' => 'PhamePostListController',
           'blogger/(?P<bloggername>[\w\.-_]+)/' => 'PhamePostListController',
           'delete/(?P<id>[^/]+)/' => 'PhamePostDeleteController',
           'edit/(?:(?P<id>[^/]+)/)?' => 'PhamePostEditController',
           'view/(?P<id>\d+)/' => 'PhamePostViewController',
           'publish/(?P<id>\d+)/' => 'PhamePostPublishController',
           'unpublish/(?P<id>\d+)/' => 'PhamePostUnpublishController',
           'notlive/(?P<id>\d+)/' => 'PhamePostNotLiveController',
           'preview/' => 'PhamePostPreviewController',
           'framed/(?P<id>\d+)/' => 'PhamePostFramedController',
           'new/' => 'PhamePostNewController',
           'move/(?P<id>\d+)/' => 'PhamePostNewController',
         ),
         'blog/' => array(
           '(?:(?P<filter>user|all)/)?' => 'PhameBlogListController',
           'delete/(?P<id>[^/]+)/' => 'PhameBlogDeleteController',
           'edit/(?P<id>[^/]+)/' => 'PhameBlogEditController',
           'view/(?P<id>[^/]+)/' => 'PhameBlogViewController',
           'feed/(?P<id>[^/]+)/' => 'PhameBlogFeedController',
           'new/' => 'PhameBlogEditController',
         ),
       ),
     );
   }
 
+  public function getQuicksandURIPatternBlacklist() {
+    return array(
+      '/phame/live/.*',
+    );
+  }
+
 }
diff --git a/src/applications/phid/handle/pool/PhabricatorHandleList.php b/src/applications/phid/handle/pool/PhabricatorHandleList.php
index d21e461871..7a8fed529f 100644
--- a/src/applications/phid/handle/pool/PhabricatorHandleList.php
+++ b/src/applications/phid/handle/pool/PhabricatorHandleList.php
@@ -1,171 +1,174 @@
 <?php
 
 /**
  * A list of object handles.
  *
  * This is a convenience class which behaves like an array but makes working
  * with handles more convenient, improves their caching and batching semantics,
  * and provides some utility behavior.
  *
  * Load a handle list by calling `loadHandles()` on a `$viewer`:
  *
  *   $handles = $viewer->loadHandles($phids);
  *
  * This creates a handle list object, which behaves like an array of handles.
  * However, it benefits from the viewer's internal handle cache and performs
  * just-in-time bulk loading.
  */
 final class PhabricatorHandleList
   extends Phobject
   implements
     Iterator,
     ArrayAccess,
     Countable {
 
   private $handlePool;
   private $phids;
+  private $count;
   private $handles;
   private $cursor;
   private $map;
 
   public function setHandlePool(PhabricatorHandlePool $pool) {
     $this->handlePool = $pool;
     return $this;
   }
 
   public function setPHIDs(array $phids) {
     $this->phids = $phids;
+    $this->count = count($phids);
     return $this;
   }
 
   private function loadHandles() {
     $this->handles = $this->handlePool->loadPHIDs($this->phids);
   }
 
   private function getHandle($phid) {
     if ($this->handles === null) {
       $this->loadHandles();
     }
 
     if (empty($this->handles[$phid])) {
       throw new Exception(
         pht(
           'Requested handle "%s" was not loaded.',
           $phid));
     }
 
     return $this->handles[$phid];
   }
 
 
   /**
    * Get a handle from this list if it exists.
    *
    * This has similar semantics to @{function:idx}.
    */
   public function getHandleIfExists($phid, $default = null) {
     if ($this->handles === null) {
       $this->loadHandles();
     }
 
     return idx($this->handles, $phid, $default);
   }
 
 
 /* -(  Rendering  )---------------------------------------------------------- */
 
 
   /**
    * Return a @{class:PHUIHandleListView} which can render the handles in
    * this list.
    */
   public function renderList() {
     return id(new PHUIHandleListView())
       ->setHandleList($this);
   }
 
 
   /**
    * Return a @{class:PHUIHandleView} which can render a specific handle.
    */
   public function renderHandle($phid) {
     if (!isset($this[$phid])) {
       throw new Exception(
         pht('Trying to render a handle which does not exist!'));
     }
 
     return id(new PHUIHandleView())
       ->setHandleList($this)
       ->setHandlePHID($phid);
   }
 
 /* -(  Iterator  )----------------------------------------------------------- */
 
 
   public function rewind() {
     $this->cursor = 0;
   }
 
   public function current() {
     return $this->getHandle($this->phids[$this->cursor]);
   }
 
   public function key() {
     return $this->phids[$this->cursor];
   }
 
   public function next() {
     ++$this->cursor;
   }
 
   public function valid() {
-    return isset($this->phids[$this->cursor]);
+    return ($this->cursor < $this->count);
   }
 
 
 /* -(  ArrayAccess  )-------------------------------------------------------- */
 
 
   public function offsetExists($offset) {
     // NOTE: We're intentionally not loading handles here so that isset()
     // checks do not trigger fetches. This gives us better bulk loading
     // behavior, particularly when invoked through methods like renderHandle().
 
     if ($this->map === null) {
       $this->map = array_fill_keys($this->phids, true);
     }
 
     return isset($this->map[$offset]);
   }
 
   public function offsetGet($offset) {
     if ($this->handles === null) {
       $this->loadHandles();
     }
     return $this->handles[$offset];
   }
 
   public function offsetSet($offset, $value) {
     $this->raiseImmutableException();
   }
 
   public function offsetUnset($offset) {
     $this->raiseImmutableException();
   }
 
   private function raiseImmutableException() {
     throw new Exception(
       pht(
-        'Trying to mutate a PhabricatorHandleList, but this is not permitted; '.
-        'handle lists are immutable.'));
+        'Trying to mutate a %s, but this is not permitted; '.
+        'handle lists are immutable.',
+        __CLASS__));
   }
 
 
 /* -(  Countable  )---------------------------------------------------------- */
 
 
   public function count() {
-    return count($this->phids);
+    return $this->count;
   }
 
 }
diff --git a/src/applications/phid/handle/pool/PhabricatorHandlePool.php b/src/applications/phid/handle/pool/PhabricatorHandlePool.php
index a1195225bb..eaa48828a4 100644
--- a/src/applications/phid/handle/pool/PhabricatorHandlePool.php
+++ b/src/applications/phid/handle/pool/PhabricatorHandlePool.php
@@ -1,75 +1,80 @@
 <?php
 
 /**
  * Coordinates loading object handles.
  *
  * This is a low-level piece of plumbing which code will not normally interact
  * with directly. For discussion of the handle pool mechanism, see
  * @{class:PhabricatorHandleList}.
  */
 final class PhabricatorHandlePool extends Phobject {
 
   private $viewer;
   private $handles = array();
   private $unloadedPHIDs = array();
 
   public function setViewer(PhabricatorUser $user) {
     $this->viewer = $user;
     return $this;
   }
 
   public function getViewer() {
     return $this->viewer;
   }
 
   public function newHandleList(array $phids) {
     // Mark any PHIDs we haven't loaded yet as unloaded. This will let us bulk
     // load them later.
     foreach ($phids as $phid) {
       if (empty($this->handles[$phid])) {
         $this->unloadedPHIDs[$phid] = true;
       }
     }
 
     $unique = array();
     foreach ($phids as $phid) {
       $unique[$phid] = $phid;
     }
 
     return id(new PhabricatorHandleList())
       ->setHandlePool($this)
       ->setPHIDs(array_values($unique));
   }
 
   public function loadPHIDs(array $phids) {
     $need = array();
     foreach ($phids as $phid) {
       if (empty($this->handles[$phid])) {
         $need[$phid] = true;
       }
     }
 
     foreach ($need as $phid => $ignored) {
       if (empty($this->unloadedPHIDs[$phid])) {
         throw new Exception(
           pht(
             'Attempting to load PHID "%s", but it was not requested by any '.
             'handle list.',
             $phid));
       }
     }
 
     // If we need any handles, bulk load everything in the queue.
     if ($need) {
+      // Clear the list of PHIDs that need to be loaded before performing the
+      // actual fetch. This prevents us from looping if we need to reenter the
+      // HandlePool while loading handles.
+      $fetch_phids = array_keys($this->unloadedPHIDs);
+      $this->unloadedPHIDs = array();
+
       $handles = id(new PhabricatorHandleQuery())
         ->setViewer($this->getViewer())
-        ->withPHIDs(array_keys($this->unloadedPHIDs))
+        ->withPHIDs($fetch_phids)
         ->execute();
       $this->handles += $handles;
-      $this->unloadedPHIDs = array();
     }
 
     return array_select_keys($this->handles, $phids);
   }
 
 }
diff --git a/src/applications/phid/query/PhabricatorObjectQuery.php b/src/applications/phid/query/PhabricatorObjectQuery.php
index 49d238c6a9..26d0668cc2 100644
--- a/src/applications/phid/query/PhabricatorObjectQuery.php
+++ b/src/applications/phid/query/PhabricatorObjectQuery.php
@@ -1,165 +1,176 @@
 <?php
 
 final class PhabricatorObjectQuery
   extends PhabricatorCursorPagedPolicyAwareQuery {
 
   private $phids = array();
   private $names = array();
   private $types;
 
   private $namedResults;
 
   public function withPHIDs(array $phids) {
     $this->phids = $phids;
     return $this;
   }
 
   public function withNames(array $names) {
     $this->names = $names;
     return $this;
   }
 
   public function withTypes(array $types) {
     $this->types = $types;
     return $this;
   }
 
   protected function loadPage() {
     if ($this->namedResults === null) {
       $this->namedResults = array();
     }
 
     $types = PhabricatorPHIDType::getAllTypes();
     if ($this->types) {
       $types = array_select_keys($types, $this->types);
     }
 
     $names = array_unique($this->names);
     $phids = $this->phids;
 
     // We allow objects to be named by their PHID in addition to their normal
     // name so that, e.g., CLI tools which accept object names can also accept
     // PHIDs and work as users expect.
     $actually_phids = array();
     if ($names) {
       foreach ($names as $key => $name) {
         if (!strncmp($name, 'PHID-', 5)) {
           $actually_phids[] = $name;
           $phids[] = $name;
           unset($names[$key]);
         }
       }
     }
 
     $phids = array_unique($phids);
 
     if ($names) {
       $name_results = $this->loadObjectsByName($types, $names);
     } else {
       $name_results = array();
     }
 
     if ($phids) {
       $phid_results = $this->loadObjectsByPHID($types, $phids);
     } else {
       $phid_results = array();
     }
 
     foreach ($actually_phids as $phid) {
       if (isset($phid_results[$phid])) {
         $name_results[$phid] = $phid_results[$phid];
       }
     }
 
     $this->namedResults += $name_results;
 
     return $phid_results + mpull($name_results, null, 'getPHID');
   }
 
   public function getNamedResults() {
     if ($this->namedResults === null) {
-      throw new Exception('Call execute() before getNamedResults()!');
+      throw new PhutilInvalidStateException('execute');
     }
     return $this->namedResults;
   }
 
   private function loadObjectsByName(array $types, array $names) {
     $groups = array();
     foreach ($names as $name) {
       foreach ($types as $type => $type_impl) {
         if (!$type_impl->canLoadNamedObject($name)) {
           continue;
         }
         $groups[$type][] = $name;
         break;
       }
     }
 
     $results = array();
     foreach ($groups as $type => $group) {
       $results += $types[$type]->loadNamedObjects($this, $group);
     }
 
     return $results;
   }
 
   private function loadObjectsByPHID(array $types, array $phids) {
     $results = array();
 
     $workspace = $this->getObjectsFromWorkspace($phids);
 
     foreach ($phids as $key => $phid) {
       if (isset($workspace[$phid])) {
         $results[$phid] = $workspace[$phid];
         unset($phids[$key]);
       }
     }
 
     if (!$phids) {
       return $results;
     }
 
     $groups = array();
     foreach ($phids as $phid) {
       $type = phid_get_type($phid);
       $groups[$type][] = $phid;
     }
 
+    $in_flight = $this->getPHIDsInFlight();
     foreach ($groups as $type => $group) {
-      if (isset($types[$type])) {
+      // Don't try to load PHIDs which are already "in flight"; this prevents
+      // us from recursing indefinitely if policy checks or edges form a loop.
+      // We will decline to load the corresponding objects.
+      foreach ($group as $key => $phid) {
+        if (isset($in_flight[$phid])) {
+          unset($group[$key]);
+        }
+      }
+
+      if ($group && isset($types[$type])) {
+        $this->putPHIDsInFlight($group);
         $objects = $types[$type]->loadObjects($this, $group);
         $results += mpull($objects, null, 'getPHID');
       }
     }
 
     return $results;
   }
 
   protected function didFilterResults(array $filtered) {
     foreach ($this->namedResults as $name => $result) {
       if (isset($filtered[$result->getPHID()])) {
         unset($this->namedResults[$name]);
       }
     }
   }
 
   /**
    * This query disables policy filtering if the only required capability is
    * the view capability.
    *
    * The view capability is always checked in the subqueries, so we do not need
    * to re-filter results. For any other set of required capabilities, we do.
    */
   protected function shouldDisablePolicyFiltering() {
     $view_capability = PhabricatorPolicyCapability::CAN_VIEW;
     if ($this->getRequiredCapabilities() === array($view_capability)) {
       return true;
     }
     return false;
   }
 
   public function getQueryApplicationClass() {
     return null;
   }
 
 }
diff --git a/src/applications/phid/type/PhabricatorPHIDType.php b/src/applications/phid/type/PhabricatorPHIDType.php
index 4445ab863d..fd64ab7bd1 100644
--- a/src/applications/phid/type/PhabricatorPHIDType.php
+++ b/src/applications/phid/type/PhabricatorPHIDType.php
@@ -1,231 +1,235 @@
 <?php
 
 abstract class PhabricatorPHIDType {
 
   final public function getTypeConstant() {
     $class = new ReflectionClass($this);
 
     $const = $class->getConstant('TYPECONST');
     if ($const === false) {
       throw new Exception(
         pht(
           'PHIDType class "%s" must define an TYPECONST property.',
           get_class($this)));
     }
 
     if (!is_string($const) || !preg_match('/^[A-Z]{4}$/', $const)) {
       throw new Exception(
         pht(
           'PHIDType class "%s" has an invalid TYPECONST property. PHID '.
           'constants must be a four character uppercase string.',
           get_class($this)));
     }
 
     return $const;
   }
 
   abstract public function getTypeName();
 
   public function newObject() {
     return null;
   }
 
   public function getTypeIcon() {
     // Default to the application icon if the type doesn't specify one.
     $application_class = $this->getPHIDTypeApplicationClass();
     if ($application_class) {
       $application = newv($application_class, array());
       return $application->getFontIcon();
     }
 
     return null;
   }
 
 
   /**
    * Get the class name for the application this type belongs to.
    *
    * @return string|null Class name of the corresponding application, or null
    *   if the type is not bound to an application.
    */
   public function getPHIDTypeApplicationClass() {
     // TODO: Some day this should probably be abstract, but for now it only
     // affects global search and there's no real burning need to go classify
     // every PHID type.
     return null;
   }
 
   /**
    * Build a @{class:PhabricatorPolicyAwareQuery} to load objects of this type
    * by PHID.
    *
    * If you can not build a single query which satisfies this requirement, you
    * can provide a dummy implementation for this method and overload
    * @{method:loadObjects} instead.
    *
    * @param PhabricatorObjectQuery Query being executed.
    * @param list<phid> PHIDs to load.
    * @return PhabricatorPolicyAwareQuery Query object which loads the
    *   specified PHIDs when executed.
    */
   abstract protected function buildQueryForObjects(
     PhabricatorObjectQuery $query,
     array $phids);
 
 
   /**
    * Load objects of this type, by PHID. For most PHID types, it is only
    * necessary to implement @{method:buildQueryForObjects} to get object
    * loading to work.
    *
    * @param PhabricatorObjectQuery Query being executed.
    * @param list<phid> PHIDs to load.
    * @return list<wild> Corresponding objects.
    */
   public function loadObjects(
     PhabricatorObjectQuery $query,
     array $phids) {
 
     $object_query = $this->buildQueryForObjects($query, $phids)
       ->setViewer($query->getViewer())
       ->setParentQuery($query);
 
     // If the user doesn't have permission to use the application at all,
     // just mark all the PHIDs as filtered. This primarily makes these
     // objects show up as "Restricted" instead of "Unknown" when loaded as
     // handles, which is technically true.
     if (!$object_query->canViewerUseQueryApplication()) {
       $object_query->addPolicyFilteredPHIDs(array_fuse($phids));
       return array();
     }
 
     return $object_query->execute();
   }
 
 
   /**
    * Populate provided handles with application-specific data, like titles and
    * URIs.
    *
    * NOTE: The `$handles` and `$objects` lists are guaranteed to be nonempty
    * and have the same keys: subclasses are expected to load information only
    * for handles with visible objects.
    *
    * Because of this guarantee, a safe implementation will typically look like*
    *
    *   foreach ($handles as $phid => $handle) {
    *     $object = $objects[$phid];
    *
    *     $handle->setStuff($object->getStuff());
    *     // ...
    *   }
    *
    * In general, an implementation should call `setName()` and `setURI()` on
    * each handle at a minimum. See @{class:PhabricatorObjectHandle} for other
    * handle properties.
    *
    * @param PhabricatorHandleQuery          Issuing query object.
    * @param list<PhabricatorObjectHandle>   Handles to populate with data.
    * @param list<Object>                    Objects for these PHIDs loaded by
    *                                        @{method:buildQueryForObjects()}.
    * @return void
    */
   abstract public function loadHandles(
     PhabricatorHandleQuery $query,
     array $handles,
     array $objects);
 
   public function canLoadNamedObject($name) {
     return false;
   }
 
   public function loadNamedObjects(
     PhabricatorObjectQuery $query,
     array $names) {
     throw new PhutilMethodNotImplementedException();
   }
 
 
   /**
    * Get all known PHID types.
    *
    * To get PHID types a given user has access to, see
    * @{method:getAllInstalledTypes}.
    *
    * @return dict<string, PhabricatorPHIDType> Map of type constants to types.
    */
   public static function getAllTypes() {
     static $types;
     if ($types === null) {
       $objects = id(new PhutilSymbolLoader())
         ->setAncestorClass(__CLASS__)
         ->loadObjects();
 
       $map = array();
       $original = array();
       foreach ($objects as $object) {
         $type = $object->getTypeConstant();
         if (isset($map[$type])) {
           $that_class = $original[$type];
           $this_class = get_class($object);
           throw new Exception(
-            "Two PhabricatorPHIDType classes ({$that_class}, {$this_class}) ".
-            "both handle PHID type '{$type}'. A type may be handled by only ".
-            "one class.");
+            pht(
+              "Two %s classes (%s, %s) both handle PHID type '%s'. ".
+              "A type may be handled by only one class.",
+              __CLASS__,
+              $that_class,
+              $this_class,
+              $type));
         }
 
         $original[$type] = get_class($object);
         $map[$type] = $object;
       }
 
       $types = $map;
     }
 
     return $types;
   }
 
 
   /**
    * Get all PHID types of applications installed for a given viewer.
    *
    * @param PhabricatorUser Viewing user.
    * @return dict<string, PhabricatorPHIDType> Map of constants to installed
    *  types.
    */
   public static function getAllInstalledTypes(PhabricatorUser $viewer) {
     $all_types = self::getAllTypes();
 
     $installed_types = array();
 
     $app_classes = array();
     foreach ($all_types as $key => $type) {
       $app_class = $type->getPHIDTypeApplicationClass();
 
       if ($app_class === null) {
         // If the PHID type isn't bound to an application, include it as
         // installed.
         $installed_types[$key] = $type;
         continue;
       }
 
       // Otherwise, we need to check if this application is installed before
       // including the PHID type.
       $app_classes[$app_class][$key] = $type;
     }
 
     if ($app_classes) {
       $apps = id(new PhabricatorApplicationQuery())
         ->setViewer($viewer)
         ->withInstalled(true)
         ->withClasses(array_keys($app_classes))
         ->execute();
 
       foreach ($apps as $app_class => $app) {
         $installed_types += $app_classes[$app_class];
       }
     }
 
     return $installed_types;
   }
 
 }
diff --git a/src/applications/pholio/application/PhabricatorPholioApplication.php b/src/applications/pholio/application/PhabricatorPholioApplication.php
index e56f989630..801a2de4e8 100644
--- a/src/applications/pholio/application/PhabricatorPholioApplication.php
+++ b/src/applications/pholio/application/PhabricatorPholioApplication.php
@@ -1,99 +1,98 @@
 <?php
 
 final class PhabricatorPholioApplication extends PhabricatorApplication {
 
   public function getName() {
     return pht('Pholio');
   }
 
   public function getBaseURI() {
     return '/pholio/';
   }
 
   public function getShortDescription() {
     return pht('Review Mocks and Design');
   }
 
   public function getFontIcon() {
     return 'fa-camera-retro';
   }
 
   public function getTitleGlyph() {
     return "\xE2\x9D\xA6";
   }
 
   public function getFlavorText() {
     return pht('Things before they were cool.');
   }
 
   public function getEventListeners() {
     return array(
       new PholioActionMenuEventListener(),
     );
   }
 
   public function getRemarkupRules() {
     return array(
       new PholioRemarkupRule(),
     );
   }
 
   public function getRoutes() {
     return array(
       '/M(?P<id>[1-9]\d*)(?:/(?P<imageID>\d+)/)?' => 'PholioMockViewController',
       '/pholio/' => array(
         '(?:query/(?P<queryKey>[^/]+)/)?' => 'PholioMockListController',
         'new/'                  => 'PholioMockEditController',
         'edit/(?P<id>\d+)/'     => 'PholioMockEditController',
         'comment/(?P<id>\d+)/'  => 'PholioMockCommentController',
         'inline/' => array(
           '(?:(?P<id>\d+)/)?' => 'PholioInlineController',
           'list/(?P<id>\d+)/' => 'PholioInlineListController',
-          'thumb/(?P<imageid>\d+)/' => 'PholioInlineThumbController',
         ),
         'image/' => array(
           'upload/' => 'PholioImageUploadController',
         ),
       ),
     );
   }
 
   public function getQuickCreateItems(PhabricatorUser $viewer) {
     $items = array();
 
     $item = id(new PHUIListItemView())
       ->setName(pht('Pholio Mock'))
       ->setIcon('fa-picture-o')
       ->setHref($this->getBaseURI().'new/');
     $items[] = $item;
 
     return $items;
   }
 
   protected function getCustomCapabilities() {
     return array(
       PholioDefaultViewCapability::CAPABILITY => array(),
       PholioDefaultEditCapability::CAPABILITY => array(),
     );
   }
 
   public function getMailCommandObjects() {
     return array(
       'mock' => array(
         'name' => pht('Email Commands: Mocks'),
         'header' => pht('Interacting with Pholio Mocks'),
         'object' => new PholioMock(),
         'summary' => pht(
           'This page documents the commands you can use to interact with '.
           'mocks in Pholio.'),
       ),
     );
   }
 
   public function getApplicationSearchDocumentTypes() {
     return array(
       PholioMockPHIDType::TYPECONST,
     );
   }
 
 }
diff --git a/src/applications/pholio/controller/PholioInlineThumbController.php b/src/applications/pholio/controller/PholioInlineThumbController.php
deleted file mode 100644
index 624ce3d3ea..0000000000
--- a/src/applications/pholio/controller/PholioInlineThumbController.php
+++ /dev/null
@@ -1,46 +0,0 @@
-<?php
-
-final class PholioInlineThumbController extends PholioController {
-
-  private $imageid;
-
-  public function shouldAllowPublic() {
-    return true;
-  }
-
-  public function willProcessRequest(array $data) {
-    $this->imageid = idx($data, 'imageid');
-  }
-
-  public function processRequest() {
-    $request = $this->getRequest();
-    $user = $request->getUser();
-
-    $image = id(new PholioImage())->load($this->imageid);
-
-    if ($image == null) {
-      return new Aphront404Response();
-    }
-
-    $mock = id(new PholioMockQuery())
-      ->setViewer($user)
-      ->withIDs(array($image->getMockID()))
-      ->executeOne();
-
-    if (!$mock) {
-      return new Aphront404Response();
-    }
-
-    $file = id(new PhabricatorFileQuery())
-      ->setViewer($user)
-      ->witHPHIDs(array($image->getFilePHID()))
-      ->executeOne();
-
-    if (!$file) {
-      return new Aphront404Response();
-    }
-
-    return id(new AphrontRedirectResponse())->setURI($file->getThumb60x45URI());
-  }
-
-}
diff --git a/src/applications/pholio/query/PholioMockSearchEngine.php b/src/applications/pholio/query/PholioMockSearchEngine.php
index 226f225e11..057a370649 100644
--- a/src/applications/pholio/query/PholioMockSearchEngine.php
+++ b/src/applications/pholio/query/PholioMockSearchEngine.php
@@ -1,170 +1,177 @@
 <?php
 
 final class PholioMockSearchEngine extends PhabricatorApplicationSearchEngine {
 
   public function getResultTypeDescription() {
     return pht('Pholio Mocks');
   }
 
   public function getApplicationClassName() {
     return 'PhabricatorPholioApplication';
   }
 
   public function buildSavedQueryFromRequest(AphrontRequest $request) {
     $saved = new PhabricatorSavedQuery();
 
     $saved->setParameter(
       'authorPHIDs',
       $this->readUsersFromRequest($request, 'authors'));
 
     $saved->setParameter(
       'projects',
       $this->readProjectsFromRequest($request, 'projects'));
 
     $saved->setParameter(
       'statuses',
       $request->getStrList('status'));
 
     return $saved;
   }
 
   public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
     $query = id(new PholioMockQuery())
       ->needCoverFiles(true)
       ->needImages(true)
       ->needTokenCounts(true);
 
     $datasource = id(new PhabricatorPeopleUserFunctionDatasource())
       ->setViewer($this->requireViewer());
 
     $author_phids = $saved->getParameter('authorPHIDs', array());
     $author_phids = $datasource->evaluateTokens($author_phids);
     if ($author_phids) {
       $query->withAuthorPHIDs($author_phids);
     }
 
     $statuses = $saved->getParameter('statuses', array());
     if ($statuses) {
       $query->withStatuses($statuses);
     }
 
     $this->setQueryProjects($query, $saved);
 
     return $query;
   }
 
   public function buildSearchForm(
     AphrontFormView $form,
     PhabricatorSavedQuery $saved_query) {
 
     $author_phids = $saved_query->getParameter('authorPHIDs', array());
     $projects = $saved_query->getParameter('projects', array());
 
     $statuses = array(
       '' => pht('Any Status'),
       'closed' => pht('Closed'),
       'open' => pht('Open'),
     );
 
     $status = $saved_query->getParameter('statuses', array());
     $status = head($status);
 
     $form
       ->appendControl(
         id(new AphrontFormTokenizerControl())
           ->setDatasource(new PhabricatorPeopleUserFunctionDatasource())
           ->setName('authors')
           ->setLabel(pht('Authors'))
           ->setValue($author_phids))
       ->appendControl(
         id(new AphrontFormTokenizerControl())
           ->setDatasource(new PhabricatorProjectLogicalDatasource())
           ->setName('projects')
           ->setLabel(pht('Projects'))
           ->setValue($projects))
       ->appendChild(
         id(new AphrontFormSelectControl())
           ->setLabel(pht('Status'))
           ->setName('status')
           ->setOptions($statuses)
           ->setValue($status));
   }
 
   protected function getURI($path) {
     return '/pholio/'.$path;
   }
 
   protected function getBuiltinQueryNames() {
     $names = array(
       'open' => pht('Open Mocks'),
       'all' => pht('All Mocks'),
     );
 
     if ($this->requireViewer()->isLoggedIn()) {
       $names['authored'] = pht('Authored');
     }
 
     return $names;
   }
 
   public function buildSavedQueryFromBuiltin($query_key) {
     $query = $this->newSavedQuery();
     $query->setQueryKey($query_key);
 
     switch ($query_key) {
       case 'open':
         return $query->setParameter(
           'statuses',
           array('open'));
       case 'all':
         return $query;
       case 'authored':
         return $query->setParameter(
           'authorPHIDs',
           array($this->requireViewer()->getPHID()));
     }
 
     return parent::buildSavedQueryFromBuiltin($query_key);
   }
 
   protected function getRequiredHandlePHIDsForResultList(
     array $mocks,
     PhabricatorSavedQuery $query) {
     return mpull($mocks, 'getAuthorPHID');
   }
 
   protected function renderResultList(
     array $mocks,
     PhabricatorSavedQuery $query,
     array $handles) {
     assert_instances_of($mocks, 'PholioMock');
 
     $viewer = $this->requireViewer();
 
+    $xform = PhabricatorFileTransform::getTransformByKey(
+      PhabricatorFileThumbnailTransform::TRANSFORM_PINBOARD);
+
     $board = new PHUIPinboardView();
     foreach ($mocks as $mock) {
 
+      $image = $mock->getCoverFile();
+      $image_uri = $image->getURIForTransform($xform);
+      list($x, $y) = $xform->getTransformedDimensions($image);
+
       $header = 'M'.$mock->getID().' '.$mock->getName();
       $item = id(new PHUIPinboardItemView())
         ->setHeader($header)
         ->setURI('/M'.$mock->getID())
-        ->setImageURI($mock->getCoverFile()->getThumb280x210URI())
-        ->setImageSize(280, 210)
+        ->setImageURI($image_uri)
+        ->setImageSize($x, $y)
         ->setDisabled($mock->isClosed())
         ->addIconCount('fa-picture-o', count($mock->getImages()))
         ->addIconCount('fa-trophy', $mock->getTokenCount());
 
       if ($mock->getAuthorPHID()) {
         $author_handle = $handles[$mock->getAuthorPHID()];
         $datetime = phabricator_date($mock->getDateCreated(), $viewer);
         $item->appendChild(
           pht('By %s on %s', $author_handle->renderLink(), $datetime));
       }
 
       $board->addItem($item);
     }
 
     return $board;
   }
 
 }
diff --git a/src/applications/pholio/remarkup/PholioRemarkupRule.php b/src/applications/pholio/remarkup/PholioRemarkupRule.php
index e8c8f00a17..2b1ca0f876 100644
--- a/src/applications/pholio/remarkup/PholioRemarkupRule.php
+++ b/src/applications/pholio/remarkup/PholioRemarkupRule.php
@@ -1,81 +1,85 @@
 <?php
 
 final class PholioRemarkupRule extends PhabricatorObjectRemarkupRule {
 
   protected function getObjectNamePrefix() {
     return 'M';
   }
 
   protected function getObjectIDPattern() {
     // Match "M123", "M123/456", and "M123/456/". Users can hit the latter
     // forms when clicking comment anchors on a mock page.
     return '[1-9]\d*(?:/[1-9]\d*/?)?';
   }
 
   protected function getObjectHref(
     $object,
     PhabricatorObjectHandle $handle,
     $id) {
 
     $href = $handle->getURI();
 
     // If the ID has a `M123/456` component, link to that specific image.
     $id = explode('/', $id);
     if (isset($id[1])) {
       $href = $href.'/'.$id[1].'/';
     }
 
+    if ($this->getEngine()->getConfig('uri.full')) {
+      $href = PhabricatorEnv::getURI($href);
+    }
+
     return $href;
   }
 
   protected function loadObjects(array $ids) {
     // Strip off any image ID components of the URI.
     $map = array();
     foreach ($ids as $id) {
       $map[head(explode('/', $id))][] = $id;
     }
 
     $viewer = $this->getEngine()->getConfig('viewer');
     $mocks = id(new PholioMockQuery())
       ->setViewer($viewer)
       ->needCoverFiles(true)
       ->needImages(true)
       ->needTokenCounts(true)
       ->withIDs(array_keys($map))
       ->execute();
 
     $results = array();
     foreach ($mocks as $mock) {
       $ids = idx($map, $mock->getID(), array());
       foreach ($ids as $id) {
         $results[$id] = $mock;
       }
     }
 
     return $results;
   }
 
   protected function renderObjectEmbed(
     $object,
     PhabricatorObjectHandle $handle,
     $options) {
 
     $embed_mock = id(new PholioMockEmbedView())
       ->setMock($object);
 
     if (strlen($options)) {
       $parser = new PhutilSimpleOptions();
       $opts = $parser->parse(substr($options, 1));
 
       if (isset($opts['image'])) {
         $images = array_unique(
           explode('&', preg_replace('/\s+/', '', $opts['image'])));
 
         $embed_mock->setImages($images);
       }
     }
 
     return $embed_mock->render();
   }
 
 }
diff --git a/src/applications/pholio/view/PholioMockEmbedView.php b/src/applications/pholio/view/PholioMockEmbedView.php
index 81dfa670a3..3429cfd569 100644
--- a/src/applications/pholio/view/PholioMockEmbedView.php
+++ b/src/applications/pholio/view/PholioMockEmbedView.php
@@ -1,57 +1,61 @@
 <?php
 
 final class PholioMockEmbedView extends AphrontView {
 
   private $mock;
   private $images = array();
 
   public function setMock(PholioMock $mock) {
     $this->mock = $mock;
     return $this;
   }
 
   public function setImages(array $images) {
     $this->images = $images;
     return $this;
   }
 
   public function render() {
     if (!$this->mock) {
-      throw new Exception('Call setMock() before render()!');
+      throw new PhutilInvalidStateException('setMock');
     }
     $mock = $this->mock;
 
     $images_to_show = array();
     $thumbnail = null;
     if (!empty($this->images)) {
       $images_to_show = array_intersect_key(
         $this->mock->getImages(), array_flip($this->images));
     }
 
+    $xform = PhabricatorFileTransform::getTransformByKey(
+      PhabricatorFileThumbnailTransform::TRANSFORM_PINBOARD);
+
     if ($images_to_show) {
-      foreach ($images_to_show as $image) {
-        $thumbfile = $image->getFile();
-        $thumbnail = $thumbfile->getThumb280x210URI();
-      }
+      $image = head($images_to_show);
+      $thumbfile = $image->getFile();
       $header = 'M'.$mock->getID().' '.$mock->getName().
         ' (#'.$image->getID().')';
       $uri = '/M'.$this->mock->getID().'/'.$image->getID().'/';
     } else {
-      $thumbnail = $mock->getCoverFile()->getThumb280x210URI();
+      $thumbfile = $mock->getCoverFile();
       $header = 'M'.$mock->getID().' '.$mock->getName();
       $uri = '/M'.$this->mock->getID();
     }
 
+    $thumbnail = $thumbfile->getURIForTransform($xform);
+    list($x, $y) = $xform->getTransformedDimensions($thumbfile);
+
     $item = id(new PHUIPinboardItemView())
       ->setHeader($header)
       ->setURI($uri)
       ->setImageURI($thumbnail)
-      ->setImageSize(280, 210)
+      ->setImageSize($x, $y)
       ->setDisabled($mock->isClosed())
       ->addIconCount('fa-picture-o', count($mock->getImages()))
       ->addIconCount('fa-trophy', $mock->getTokenCount());
 
     return $item;
   }
 
 }
diff --git a/src/applications/pholio/view/PholioMockImagesView.php b/src/applications/pholio/view/PholioMockImagesView.php
index f894c698f2..d59e701579 100644
--- a/src/applications/pholio/view/PholioMockImagesView.php
+++ b/src/applications/pholio/view/PholioMockImagesView.php
@@ -1,227 +1,230 @@
 <?php
 
 final class PholioMockImagesView extends AphrontView {
 
   private $mock;
   private $imageID;
   private $requestURI;
   private $commentFormID;
 
   private $panelID;
   private $viewportID;
   private $behaviorConfig;
 
   public function setCommentFormID($comment_form_id) {
     $this->commentFormID = $comment_form_id;
     return $this;
   }
 
   public function getCommentFormID() {
     return $this->commentFormID;
   }
 
   public function setRequestURI(PhutilURI $request_uri) {
     $this->requestURI = $request_uri;
     return $this;
   }
 
   public function getRequestURI() {
     return $this->requestURI;
   }
 
   public function setImageID($image_id) {
     $this->imageID = $image_id;
     return $this;
   }
 
   public function getImageID() {
     return $this->imageID;
   }
 
   public function setMock(PholioMock $mock) {
     $this->mock = $mock;
     return $this;
   }
 
   public function getMock() {
     return $this->mock;
   }
 
   public function __construct() {
     $this->panelID = celerity_generate_unique_node_id();
     $this->viewportID = celerity_generate_unique_node_id();
   }
 
   public function getBehaviorConfig() {
     if (!$this->getMock()) {
       throw new Exception('Call setMock() before getBehaviorConfig()!');
     }
 
     if ($this->behaviorConfig === null) {
       $this->behaviorConfig = $this->calculateBehaviorConfig();
     }
     return $this->behaviorConfig;
   }
 
   private function calculateBehaviorConfig() {
     $mock = $this->getMock();
 
     // TODO: We could maybe do a better job with tailoring this, which is the
     // image shown on the review stage.
-    $nonimage_uri = celerity_get_resource_uri(
-      'rsrc/image/icon/fatcow/thumbnails/default.p100.png');
+    $default_name = 'image-100x100.png';
+    $builtins = PhabricatorFile::loadBuiltins(
+      $this->getUser(),
+      array($default_name));
+    $default = $builtins[$default_name];
 
     $engine = id(new PhabricatorMarkupEngine())
       ->setViewer($this->getUser());
     foreach ($mock->getAllImages() as $image) {
       $engine->addObject($image, 'default');
     }
     $engine->process();
 
     $images = array();
     $current_set = 0;
     foreach ($mock->getAllImages() as $image) {
       $file = $image->getFile();
       $metadata = $file->getMetadata();
       $x = idx($metadata, PhabricatorFile::METADATA_IMAGE_WIDTH);
       $y = idx($metadata, PhabricatorFile::METADATA_IMAGE_HEIGHT);
 
       $is_obs = (bool)$image->getIsObsolete();
       if (!$is_obs) {
         $current_set++;
       }
 
       $history_uri = '/pholio/image/history/'.$image->getID().'/';
       $images[] = array(
         'id' => $image->getID(),
         'fullURI' => $file->getBestURI(),
         'stageURI' => ($file->isViewableImage()
           ? $file->getBestURI()
-          : $nonimage_uri),
+          : $default->getBestURI()),
         'pageURI' => $this->getImagePageURI($image, $mock),
         'downloadURI' => $file->getDownloadURI(),
         'historyURI' => $history_uri,
         'width' => $x,
         'height' => $y,
         'title' => $image->getName(),
         'descriptionMarkup' => $engine->getOutput($image, 'default'),
         'isObsolete' => (bool)$image->getIsObsolete(),
         'isImage' => $file->isViewableImage(),
         'isViewable' => $file->isViewableInBrowser(),
       );
     }
 
     $ids = mpull($mock->getImages(), 'getID');
     if ($this->imageID && isset($ids[$this->imageID])) {
       $selected_id = $this->imageID;
     } else {
       $selected_id = head_key($ids);
     }
 
     $navsequence = array();
     foreach ($mock->getImages() as $image) {
       $navsequence[] = $image->getID();
     }
 
     $full_icon = array(
       javelin_tag('span', array('aural' => true), pht('View Raw File')),
       id(new PHUIIconView())->setIconFont('fa-file-image-o'),
     );
 
     $download_icon = array(
       javelin_tag('span', array('aural' => true), pht('Download File')),
       id(new PHUIIconView())->setIconFont('fa-download'),
     );
 
     $login_uri = id(new PhutilURI('/login/'))
       ->setQueryParam('next', (string) $this->getRequestURI());
 
     $config = array(
       'mockID' => $mock->getID(),
       'panelID' => $this->panelID,
       'viewportID' => $this->viewportID,
       'commentFormID' => $this->getCommentFormID(),
       'images' => $images,
       'selectedID' => $selected_id,
       'loggedIn' => $this->getUser()->isLoggedIn(),
       'logInLink' => (string) $login_uri,
       'navsequence' => $navsequence,
       'fullIcon' => hsprintf('%s', $full_icon),
       'downloadIcon' => hsprintf('%s', $download_icon),
       'currentSetSize' => $current_set,
     );
     return $config;
   }
 
   public function render() {
     if (!$this->getMock()) {
       throw new Exception('Call setMock() before render()!');
     }
     $mock = $this->getMock();
 
     require_celerity_resource('javelin-behavior-pholio-mock-view');
 
     $panel_id = $this->panelID;
     $viewport_id = $this->viewportID;
 
     $config = $this->getBehaviorConfig();
     Javelin::initBehavior(
       'pholio-mock-view',
       $this->getBehaviorConfig());
 
     $mockview = '';
 
     $mock_wrapper = javelin_tag(
       'div',
       array(
         'id' => $this->viewportID,
         'sigil' => 'mock-viewport',
         'class' => 'pholio-mock-image-viewport',
       ),
       '');
 
     $image_header = javelin_tag(
       'div',
       array(
         'id' => 'mock-image-header',
         'class' => 'pholio-mock-image-header',
       ),
       '');
 
     $mock_wrapper = javelin_tag(
       'div',
       array(
         'id' => $this->panelID,
         'sigil' => 'mock-panel touchable',
         'class' => 'pholio-mock-image-panel',
       ),
       array(
         $image_header,
         $mock_wrapper,
       ));
 
     $inline_comments_holder = javelin_tag(
       'div',
       array(
         'id' => 'mock-image-description',
         'sigil' => 'mock-image-description',
         'class' => 'mock-image-description',
       ),
       '');
 
     $mockview[] = phutil_tag(
       'div',
         array(
           'class' => 'pholio-mock-image-container',
           'id' => 'pholio-mock-image-container',
         ),
       array($mock_wrapper, $inline_comments_holder));
 
     return $mockview;
   }
 
   private function getImagePageURI(PholioImage $image, PholioMock $mock) {
     $uri = '/M'.$mock->getID().'/'.$image->getID().'/';
     return $uri;
   }
 }
diff --git a/src/applications/pholio/view/PholioMockThumbGridView.php b/src/applications/pholio/view/PholioMockThumbGridView.php
index df9fe1aa06..8e9d3007c5 100644
--- a/src/applications/pholio/view/PholioMockThumbGridView.php
+++ b/src/applications/pholio/view/PholioMockThumbGridView.php
@@ -1,171 +1,177 @@
 <?php
 
 final class PholioMockThumbGridView extends AphrontView {
 
   private $mock;
 
   public function setMock(PholioMock $mock) {
     $this->mock = $mock;
     return $this;
   }
 
   public function render() {
     $mock = $this->mock;
 
     $all_images = $mock->getAllImages();
     $all_images = mpull($all_images, null, 'getPHID');
 
     $history = mpull($all_images, 'getReplacesImagePHID', 'getPHID');
 
     $replaced = array();
     foreach ($history as $phid => $replaces_phid) {
       if ($replaces_phid) {
         $replaced[$replaces_phid] = true;
       }
     }
 
     // Figure out the columns. Start with all the active images.
     $images = mpull($mock->getImages(), null, 'getPHID');
 
     // Now, find deleted images: obsolete images which were not replaced.
     foreach ($mock->getAllImages() as $image) {
       if (!$image->getIsObsolete()) {
         // Image is current.
         continue;
       }
 
       if (isset($replaced[$image->getPHID()])) {
         // Image was replaced.
         continue;
       }
 
       // This is an obsolete image which was not replaced, so it must be
       // a deleted image.
       $images[$image->getPHID()] = $image;
     }
 
     $cols = array();
     $depth = 0;
     foreach ($images as $image) {
       $phid = $image->getPHID();
 
       $col = array();
 
       // If this is a deleted image, null out the final column.
       if ($image->getIsObsolete()) {
         $col[] = null;
       }
 
       $col[] = $phid;
       while ($phid && isset($history[$phid])) {
         $col[] = $history[$phid];
         $phid = $history[$phid];
       }
 
       $cols[] = $col;
       $depth = max($depth, count($col));
     }
 
     $grid = array();
     $jj = $depth;
     for ($ii = 0; $ii < $depth; $ii++) {
       $row = array();
       if ($depth == $jj) {
         $row[] = phutil_tag(
           'th',
           array(
             'valign' => 'middle',
             'class' => 'pholio-history-header',
           ),
           pht('Current Revision'));
       } else {
         $row[] = phutil_tag('th', array(), null);
       }
       foreach ($cols as $col) {
         if (empty($col[$ii])) {
           $row[] = phutil_tag('td', array(), null);
         } else {
           $thumb = $this->renderThumbnail($all_images[$col[$ii]]);
           $row[] = phutil_tag('td', array(), $thumb);
         }
       }
       $grid[] = phutil_tag('tr', array(), $row);
       $jj--;
     }
 
     $grid = phutil_tag(
       'table',
       array(
         'id' => 'pholio-mock-thumb-grid',
         'class' => 'pholio-mock-thumb-grid',
       ),
       $grid);
 
     $grid = id(new PHUIBoxView())
       ->addClass('pholio-mock-thumb-grid-container')
       ->appendChild($grid);
 
     return id(new PHUIObjectBoxView())
       ->setHeaderText(pht('Mock History'))
       ->appendChild($grid);
   }
 
 
   private function renderThumbnail(PholioImage $image) {
     $thumbfile = $image->getFile();
 
+    $preview_key = PhabricatorFileThumbnailTransform::TRANSFORM_THUMBGRID;
+    $xform = PhabricatorFileTransform::getTransformByKey($preview_key);
+
+    $attributes = array(
+      'class' => 'pholio-mock-thumb-grid-image',
+      'src' => $thumbfile->getURIForTransform($xform),
+    );
+
     if ($image->getFile()->isViewableImage()) {
-      $dimensions = PhabricatorImageTransformer::getPreviewDimensions(
-        $thumbfile,
-        100);
+      $dimensions = $xform->getTransformedDimensions($thumbfile);
+      if ($dimensions) {
+        list($x, $y) = $dimensions;
+        $attributes += array(
+          'width' => $x,
+          'height' => $y,
+          'style' => 'top: '.floor((100 - $y) / 2).'px',
+        );
+      }
     } else {
       // If this is a PDF or a text file or something, we'll end up using a
       // generic thumbnail which is always sized correctly.
-      $dimensions = array(
-        'sdx' => 100,
-        'sdy' => 100,
+      $attributes += array(
+        'width' => 100,
+        'height' => 100,
       );
     }
 
-    $tag = phutil_tag(
-      'img',
-      array(
-        'width' => $dimensions['sdx'],
-        'height' => $dimensions['sdy'],
-        'src' => $thumbfile->getPreview100URI(),
-        'class' => 'pholio-mock-thumb-grid-image',
-        'style' => 'top: '.floor((100 - $dimensions['sdy'] ) / 2).'px',
-    ));
+    $tag = phutil_tag('img', $attributes);
 
     $classes = array('pholio-mock-thumb-grid-item');
     if ($image->getIsObsolete()) {
       $classes[] = 'pholio-mock-thumb-grid-item-obsolete';
     }
 
     $inline_count = null;
     if ($image->getInlineComments()) {
       $inline_count[] = phutil_tag(
         'span',
         array(
           'class' => 'pholio-mock-thumb-grid-comment-count',
         ),
         pht('%s', new PhutilNumber(count($image->getInlineComments()))));
     }
 
     return javelin_tag(
       'a',
       array(
         'sigil' => 'mock-thumbnail',
         'class' => implode(' ', $classes),
         'href' => '#',
         'meta' => array(
           'imageID' => $image->getID(),
         ),
       ),
       array(
         $tag,
         $inline_count,
       ));
   }
 
 }
diff --git a/src/applications/pholio/view/PholioUploadedImageView.php b/src/applications/pholio/view/PholioUploadedImageView.php
index fb8a82431e..2ff3ba0390 100644
--- a/src/applications/pholio/view/PholioUploadedImageView.php
+++ b/src/applications/pholio/view/PholioUploadedImageView.php
@@ -1,117 +1,121 @@
 <?php
 
 final class PholioUploadedImageView extends AphrontView {
 
   private $image;
   private $replacesPHID;
 
   public function setReplacesPHID($replaces_phid) {
     $this->replacesPHID = $replaces_phid;
     return $this;
   }
 
   public function setImage(PholioImage $image) {
     $this->image = $image;
     return $this;
   }
 
   public function render() {
     require_celerity_resource('pholio-edit-css');
 
     $image = $this->image;
     $file = $image->getFile();
     $phid = $file->getPHID();
     $replaces_phid = $this->replacesPHID;
 
     $remove = $this->renderRemoveElement();
 
     $title = id(new AphrontFormTextControl())
       ->setName('title_'.$phid)
       ->setValue($image->getName())
       ->setSigil('image-title')
       ->setLabel(pht('Title'));
 
     $description = id(new PhabricatorRemarkupControl())
       ->setUser($this->getUser())
       ->setName('description_'.$phid)
       ->setValue($image->getDescription())
       ->setSigil('image-description')
       ->setLabel(pht('Description'));
 
+    $xform = PhabricatorFileTransform::getTransformByKey(
+      PhabricatorFileThumbnailTransform::TRANSFORM_PINBOARD);
+    $thumbnail_uri = $file->getURIForTransform($xform);
+
     $thumb_frame = phutil_tag(
       'div',
       array(
         'class' => 'pholio-thumb-frame',
-        'style' => 'background-image: url('.$file->getThumb280x210URI().');',
+        'style' => 'background-image: url('.$thumbnail_uri.');',
       ));
 
     $handle = javelin_tag(
       'div',
       array(
         'class' => 'pholio-drag-handle',
         'sigil' => 'pholio-drag-handle',
       ));
 
     $content = hsprintf(
       '<div class="pholio-thumb-box">
         <div class="pholio-thumb-title">
           %s
           <div class="pholio-thumb-name">%s</div>
         </div>
         %s
       </div>
       <div class="pholio-image-details">
         %s
         %s
       </div>',
       $remove,
       $file->getName(),
       $thumb_frame,
       $title,
       $description);
 
     $input = phutil_tag(
       'input',
       array(
         'type' => 'hidden',
         'name' => 'file_phids[]',
         'value' => $phid,
       ));
 
     $replaces_input = phutil_tag(
       'input',
       array(
         'type' => 'hidden',
         'name' => 'replaces['.$replaces_phid.']',
         'value' => $phid,
       ));
 
     return javelin_tag(
       'div',
       array(
         'class' => 'pholio-uploaded-image',
         'sigil' => 'pholio-drop-image',
         'meta'  => array(
           'filePHID' => $file->getPHID(),
           'replacesPHID' => $replaces_phid,
         ),
       ),
       array(
         $handle,
         $content,
         $input,
         $replaces_input,
       ));
   }
 
   private function renderRemoveElement() {
     return javelin_tag(
       'a',
       array(
         'class' => 'button grey',
         'sigil' => 'pholio-drop-remove',
       ),
       'X');
   }
 
 }
diff --git a/src/applications/phortune/cart/PhortuneSubscriptionCart.php b/src/applications/phortune/cart/PhortuneSubscriptionCart.php
index 6c17e00331..ff71106932 100644
--- a/src/applications/phortune/cart/PhortuneSubscriptionCart.php
+++ b/src/applications/phortune/cart/PhortuneSubscriptionCart.php
@@ -1,89 +1,88 @@
 <?php
 
 final class PhortuneSubscriptionCart
    extends PhortuneCartImplementation {
 
   private $subscriptionPHID;
   private $subscription;
 
   public function setSubscriptionPHID($subscription_phid) {
     $this->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 getName(PhortuneCart $cart) {
     return $this->getSubscription()->getCartName($cart);
   }
 
   public function willCreateCart(
     PhabricatorUser $viewer,
     PhortuneCart $cart) {
 
     $subscription = $this->getSubscription();
     if (!$subscription) {
-      throw new Exception(
-        pht('Call setSubscription() before building a cart!'));
+      throw new PhutilInvalidStateException('setSubscription');
     }
 
     $cart->setMetadataValue('subscriptionPHID', $subscription->getPHID());
   }
 
   public function loadImplementationsForCarts(
     PhabricatorUser $viewer,
     array $carts) {
 
     $phids = array();
     foreach ($carts as $cart) {
       $phids[] = $cart->getMetadataValue('subscriptionPHID');
     }
 
     $subscriptions = id(new PhortuneSubscriptionQuery())
       ->setViewer($viewer)
       ->withPHIDs($phids)
       ->execute();
     $subscriptions = mpull($subscriptions, null, 'getPHID');
 
     $objects = array();
     foreach ($carts as $key => $cart) {
       $subscription_phid = $cart->getMetadataValue('subscriptionPHID');
       $subscription = idx($subscriptions, $subscription_phid);
       if (!$subscription) {
         continue;
       }
 
       $object = id(new PhortuneSubscriptionCart())
         ->setSubscriptionPHID($subscription_phid)
         ->setSubscription($subscription);
 
       $objects[$key] = $object;
     }
 
     return $objects;
   }
 
   public function getCancelURI(PhortuneCart $cart) {
     return $this->getSubscription()->getURI();
   }
 
   public function getDoneURI(PhortuneCart $cart) {
     return $this->getSubscription()->getURI();
   }
 
   public function getDoneActionName(PhortuneCart $cart) {
     return pht('Return to Subscription');
   }
 
 }
diff --git a/src/applications/phortune/currency/PhortuneCurrency.php b/src/applications/phortune/currency/PhortuneCurrency.php
index a473738ed6..b59d9b144a 100644
--- a/src/applications/phortune/currency/PhortuneCurrency.php
+++ b/src/applications/phortune/currency/PhortuneCurrency.php
@@ -1,239 +1,239 @@
 <?php
 
 final class PhortuneCurrency extends Phobject {
 
   private $value;
   private $currency;
 
   private function __construct() {
     // Intentionally private.
   }
 
   public static function getDefaultCurrency() {
     return 'USD';
   }
 
   public static function newEmptyCurrency() {
     return self::newFromString('0.00 USD');
   }
 
   public static function newFromUserInput(PhabricatorUser $user, $string) {
     // Eventually, this might select a default currency based on user settings.
     return self::newFromString($string, self::getDefaultCurrency());
   }
 
   public static function newFromString($string, $default = null) {
     $matches = null;
     $ok = preg_match(
       '/^([-$]*(?:\d+)?(?:[.]\d{0,2})?)(?:\s+([A-Z]+))?$/',
       trim($string),
       $matches);
 
     if (!$ok) {
       self::throwFormatException($string);
     }
 
     $value = $matches[1];
 
     if (substr_count($value, '-') > 1) {
       self::throwFormatException($string);
     }
 
     if (substr_count($value, '$') > 1) {
       self::throwFormatException($string);
     }
 
     $value = str_replace('$', '', $value);
     $value = (float)$value;
     $value = (int)round(100 * $value);
 
     $currency = idx($matches, 2, $default);
     switch ($currency) {
       case 'USD':
         break;
       default:
         throw new Exception("Unsupported currency '{$currency}'!");
     }
 
     return self::newFromValueAndCurrency($value, $currency);
   }
 
   public static function newFromValueAndCurrency($value, $currency) {
     $obj = new PhortuneCurrency();
 
     $obj->value = $value;
     $obj->currency = $currency;
 
     return $obj;
   }
 
   public static function newFromList(array $list) {
-    assert_instances_of($list, 'PhortuneCurrency');
+    assert_instances_of($list, __CLASS__);
 
     if (!$list) {
-      return PhortuneCurrency::newEmptyCurrency();
+      return self::newEmptyCurrency();
     }
 
     $total = null;
     foreach ($list as $item) {
       if ($total === null) {
         $total = $item;
       } else {
         $total = $total->add($item);
       }
     }
 
     return $total;
   }
 
   public function formatForDisplay() {
     $bare = $this->formatBareValue();
     return '$'.$bare.' '.$this->currency;
   }
 
   public function serializeForStorage() {
     return $this->formatBareValue().' '.$this->currency;
   }
 
   public function formatBareValue() {
     switch ($this->currency) {
       case 'USD':
         return sprintf('%.02f', $this->value / 100);
       default:
         throw new Exception(
           pht('Unsupported currency ("%s")!', $this->currency));
     }
   }
 
   public function getValue() {
     return $this->value;
   }
 
   public function getCurrency() {
     return $this->currency;
   }
 
   public function getValueInUSDCents() {
     if ($this->currency !== 'USD') {
       throw new Exception(pht('Unexpected currency!'));
     }
     return $this->value;
   }
 
   private static function throwFormatException($string) {
     throw new Exception("Invalid currency format ('{$string}').");
   }
 
   private function throwUnlikeCurrenciesException(PhortuneCurrency $other) {
     throw new Exception(
       pht(
         'Trying to operate on unlike currencies ("%s" and "%s")!',
         $this->currency,
         $other->currency));
   }
 
   public function add(PhortuneCurrency $other) {
     if ($this->currency !== $other->currency) {
       $this->throwUnlikeCurrenciesException($other);
     }
 
     $currency = new PhortuneCurrency();
 
     // TODO: This should check for integer overflows, etc.
     $currency->value = $this->value + $other->value;
     $currency->currency = $this->currency;
 
     return $currency;
   }
 
   public function subtract(PhortuneCurrency $other) {
     if ($this->currency !== $other->currency) {
       $this->throwUnlikeCurrenciesException($other);
     }
 
     $currency = new PhortuneCurrency();
 
     // TODO: This should check for integer overflows, etc.
     $currency->value = $this->value - $other->value;
     $currency->currency = $this->currency;
 
     return $currency;
   }
 
   public function isEqualTo(PhortuneCurrency $other) {
     if ($this->currency !== $other->currency) {
       $this->throwUnlikeCurrenciesException($other);
     }
 
     return ($this->value === $other->value);
   }
 
   public function negate() {
     $currency = new PhortuneCurrency();
     $currency->value = -$this->value;
     $currency->currency = $this->currency;
     return $currency;
   }
 
   public function isPositive() {
     return ($this->value > 0);
   }
 
   public function isGreaterThan(PhortuneCurrency $other) {
     if ($this->currency !== $other->currency) {
       $this->throwUnlikeCurrenciesException($other);
     }
     return $this->value > $other->value;
   }
 
   /**
    * Assert that a currency value lies within a range.
    *
    * Throws if the value is not between the minimum and maximum, inclusive.
    *
    * In particular, currency values can be negative (to represent a debt or
    * credit), so checking against zero may be useful to make sure a value
    * has the expected sign.
    *
    * @param string|null Currency string, or null to skip check.
    * @param string|null Currency string, or null to skip check.
    * @return this
    */
   public function assertInRange($minimum, $maximum) {
     if ($minimum !== null && $maximum !== null) {
-      $min = PhortuneCurrency::newFromString($minimum);
-      $max = PhortuneCurrency::newFromString($maximum);
+      $min = self::newFromString($minimum);
+      $max = self::newFromString($maximum);
       if ($min->value > $max->value) {
         throw new Exception(
           pht(
             'Range (%s - %s) is not valid!',
             $min->formatForDisplay(),
             $max->formatForDisplay()));
       }
     }
 
     if ($minimum !== null) {
-      $min = PhortuneCurrency::newFromString($minimum);
+      $min = self::newFromString($minimum);
       if ($min->value > $this->value) {
         throw new Exception(
           pht(
             'Minimum allowed amount is %s.',
             $min->formatForDisplay()));
       }
     }
 
     if ($maximum !== null) {
-      $max = PhortuneCurrency::newFromString($maximum);
+      $max = self::newFromString($maximum);
       if ($max->value < $this->value) {
         throw new Exception(
           pht(
             'Maximum allowed amount is %s.',
             $max->formatForDisplay()));
       }
     }
 
     return $this;
   }
 
 
 }
diff --git a/src/applications/phortune/provider/PhortunePaymentProvider.php b/src/applications/phortune/provider/PhortunePaymentProvider.php
index 49c80dcd72..da36779d06 100644
--- a/src/applications/phortune/provider/PhortunePaymentProvider.php
+++ b/src/applications/phortune/provider/PhortunePaymentProvider.php
@@ -1,296 +1,296 @@
 <?php
 
 /**
  * @task addmethod  Adding Payment Methods
  */
 abstract class PhortunePaymentProvider {
 
   private $providerConfig;
 
   public function setProviderConfig(
     PhortunePaymentProviderConfig $provider_config) {
     $this->providerConfig = $provider_config;
     return $this;
   }
 
   public function getProviderConfig() {
     return $this->providerConfig;
   }
 
   /**
    * Return a short name which identifies this provider.
    */
   abstract public function getName();
 
 
 /* -(  Configuring Providers  )---------------------------------------------- */
 
 
   /**
    * Return a human-readable provider name for use on the merchant workflow
    * where a merchant owner adds providers.
    */
   abstract public function getConfigureName();
 
 
   /**
    * Return a human-readable provider description for use on the merchant
    * workflow where a merchant owner adds providers.
    */
   abstract public function getConfigureDescription();
 
   abstract public function getConfigureInstructions();
 
   abstract public function getConfigureProvidesDescription();
 
   abstract public function getAllConfigurableProperties();
 
   abstract public function getAllConfigurableSecretProperties();
   /**
    * Read a dictionary of properties from the provider's configuration for
    * use when editing the provider.
    */
   public function readEditFormValuesFromProviderConfig() {
     $properties = $this->getAllConfigurableProperties();
     $config = $this->getProviderConfig();
 
     $secrets = $this->getAllConfigurableSecretProperties();
     $secrets = array_fuse($secrets);
 
     $map = array();
     foreach ($properties as $property) {
       $map[$property] = $config->getMetadataValue($property);
       if (isset($secrets[$property])) {
         $map[$property] = $this->renderConfigurationSecret($map[$property]);
       }
     }
 
     return $map;
   }
 
 
   /**
    * Read a dictionary of properties from a request for use when editing the
    * provider.
    */
   public function readEditFormValuesFromRequest(AphrontRequest $request) {
     $properties = $this->getAllConfigurableProperties();
 
     $map = array();
     foreach ($properties as $property) {
       $map[$property] = $request->getStr($property);
     }
 
     return $map;
   }
 
 
   abstract public function processEditForm(
     AphrontRequest $request,
     array $values);
 
   abstract public function extendEditForm(
     AphrontRequest $request,
     AphrontFormView $form,
     array $values,
     array $issues);
 
   protected function renderConfigurationSecret($value) {
     if (strlen($value)) {
       return str_repeat('*', strlen($value));
     }
     return '';
   }
 
   public function isConfigurationSecret($value) {
     return preg_match('/^\*+\z/', trim($value));
   }
 
   abstract public function canRunConfigurationTest();
 
   public function runConfigurationTest() {
     throw new PhortuneNotImplementedException($this);
   }
 
 
 /* -(  Selecting Providers  )------------------------------------------------ */
 
 
   public static function getAllProviders() {
     return id(new PhutilSymbolLoader())
-      ->setAncestorClass('PhortunePaymentProvider')
+      ->setAncestorClass(__CLASS__)
       ->loadObjects();
   }
 
   public function isEnabled() {
     return $this->getProviderConfig()->getIsEnabled();
   }
 
   abstract public function isAcceptingLivePayments();
   abstract public function getPaymentMethodDescription();
   abstract public function getPaymentMethodIcon();
   abstract public function getPaymentMethodProviderDescription();
 
   final public function applyCharge(
     PhortunePaymentMethod $payment_method,
     PhortuneCharge $charge) {
     $this->executeCharge($payment_method, $charge);
   }
 
   final public function refundCharge(
     PhortuneCharge $charge,
     PhortuneCharge $refund) {
     $this->executeRefund($charge, $refund);
   }
 
   abstract protected function executeCharge(
     PhortunePaymentMethod $payment_method,
     PhortuneCharge $charge);
 
   abstract protected function executeRefund(
     PhortuneCharge $charge,
     PhortuneCharge $refund);
 
   abstract public function updateCharge(PhortuneCharge $charge);
 
 
 /* -(  Adding Payment Methods  )--------------------------------------------- */
 
 
   /**
    * @task addmethod
    */
   public function canCreatePaymentMethods() {
     return false;
   }
 
 
   /**
    * @task addmethod
    */
   public function translateCreatePaymentMethodErrorCode($error_code) {
     throw new PhortuneNotImplementedException($this);
   }
 
 
   /**
    * @task addmethod
    */
   public function getCreatePaymentMethodErrorMessage($error_code) {
     throw new PhortuneNotImplementedException($this);
   }
 
 
   /**
    * @task addmethod
    */
   public function validateCreatePaymentMethodToken(array $token) {
     throw new PhortuneNotImplementedException($this);
   }
 
 
   /**
    * @task addmethod
    */
   public function createPaymentMethodFromRequest(
     AphrontRequest $request,
     PhortunePaymentMethod $method,
     array $token) {
     throw new PhortuneNotImplementedException($this);
   }
 
 
   /**
    * @task addmethod
    */
   public function renderCreatePaymentMethodForm(
     AphrontRequest $request,
     array $errors) {
     throw new PhortuneNotImplementedException($this);
   }
 
   public function getDefaultPaymentMethodDisplayName(
     PhortunePaymentMethod $method) {
     throw new PhortuneNotImplementedException($this);
   }
 
 
 /* -(  One-Time Payments  )-------------------------------------------------- */
 
 
   public function canProcessOneTimePayments() {
     return false;
   }
 
   public function renderOneTimePaymentButton(
     PhortuneAccount $account,
     PhortuneCart $cart,
     PhabricatorUser $user) {
 
     require_celerity_resource('phortune-css');
 
     $description = $this->getPaymentMethodProviderDescription();
     $details = $this->getPaymentMethodDescription();
 
     $icon = id(new PHUIIconView())
       ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN)
       ->setSpriteIcon($this->getPaymentMethodIcon());
 
     $button = id(new PHUIButtonView())
       ->setSize(PHUIButtonView::BIG)
       ->setColor(PHUIButtonView::GREY)
       ->setIcon($icon)
       ->setText($description)
       ->setSubtext($details);
 
     // NOTE: We generate a local URI to make sure the form picks up CSRF tokens.
     $uri = $this->getControllerURI(
       'checkout',
       array(
         'cartID' => $cart->getID(),
       ),
       $local = true);
 
     return phabricator_form(
       $user,
       array(
         'action' => $uri,
         'method' => 'POST',
       ),
       $button);
   }
 
 
 /* -(  Controllers  )-------------------------------------------------------- */
 
 
   final public function getControllerURI(
     $action,
     array $params = array(),
     $local = false) {
 
     $id = $this->getProviderConfig()->getID();
     $app = PhabricatorApplication::getByClass('PhabricatorPhortuneApplication');
     $path = $app->getBaseURI().'provider/'.$id.'/'.$action.'/';
 
     $uri = new PhutilURI($path);
     $uri->setQueryParams($params);
 
     if ($local) {
       return $uri;
     } else {
       return PhabricatorEnv::getURI((string)$uri);
     }
   }
 
   public function canRespondToControllerAction($action) {
     return false;
   }
 
   public function processControllerRequest(
     PhortuneProviderActionController $controller,
     AphrontRequest $request) {
     throw new PhortuneNotImplementedException($this);
   }
 
 }
diff --git a/src/applications/phortune/storage/PhortuneAccount.php b/src/applications/phortune/storage/PhortuneAccount.php
index facb9d5089..e86fd53df2 100644
--- a/src/applications/phortune/storage/PhortuneAccount.php
+++ b/src/applications/phortune/storage/PhortuneAccount.php
@@ -1,170 +1,170 @@
 <?php
 
 /**
  * An account represents a purchasing entity. An account may have multiple users
  * on it (e.g., several employees of a company have access to the company
  * account), and a user may have several accounts (e.g., a company account and
  * a personal account).
  */
 final class PhortuneAccount extends PhortuneDAO
   implements
     PhabricatorApplicationTransactionInterface,
     PhabricatorPolicyInterface {
 
   protected $name;
 
   private $memberPHIDs = self::ATTACHABLE;
 
   public static function initializeNewAccount(PhabricatorUser $actor) {
     $account = id(new PhortuneAccount());
 
     $account->memberPHIDs = array();
 
     return $account;
   }
 
   public static function createNewAccount(
     PhabricatorUser $actor,
     PhabricatorContentSource $content_source) {
 
-    $account = PhortuneAccount::initializeNewAccount($actor);
+    $account = self::initializeNewAccount($actor);
 
     $xactions = array();
     $xactions[] = id(new PhortuneAccountTransaction())
       ->setTransactionType(PhortuneAccountTransaction::TYPE_NAME)
       ->setNewValue(pht('Default Account'));
 
     $xactions[] = id(new PhortuneAccountTransaction())
       ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
       ->setMetadataValue(
         'edge:type',
         PhortuneAccountHasMemberEdgeType::EDGECONST)
       ->setNewValue(
         array(
           '=' => array($actor->getPHID() => $actor->getPHID()),
         ));
 
     $editor = id(new PhortuneAccountEditor())
       ->setActor($actor)
       ->setContentSource($content_source);
 
     // We create an account for you the first time you visit Phortune.
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
 
       $editor->applyTransactions($account, $xactions);
 
     unset($unguarded);
 
     return $account;
   }
 
   public function newCart(
     PhabricatorUser $actor,
     PhortuneCartImplementation $implementation,
     PhortuneMerchant $merchant) {
 
     $cart = PhortuneCart::initializeNewCart($actor, $this, $merchant);
 
     $cart->setCartClass(get_class($implementation));
     $cart->attachImplementation($implementation);
 
     $implementation->willCreateCart($actor, $cart);
 
     return $cart->save();
   }
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_COLUMN_SCHEMA => array(
         'name' => 'text255',
       ),
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       PhortuneAccountPHIDType::TYPECONST);
   }
 
   public function getMemberPHIDs() {
     return $this->assertAttached($this->memberPHIDs);
   }
 
   public function attachMemberPHIDs(array $phids) {
     $this->memberPHIDs = $phids;
     return $this;
   }
 
 
 /* -(  PhabricatorApplicationTransactionInterface  )------------------------- */
 
 
   public function getApplicationTransactionEditor() {
     return new PhortuneAccountEditor();
   }
 
   public function getApplicationTransactionObject() {
     return $this;
   }
 
   public function getApplicationTransactionTemplate() {
     return new PhortuneAccountTransaction();
   }
 
   public function willRenderTimeline(
     PhabricatorApplicationTransactionView $timeline,
     AphrontRequest $request) {
 
     return $timeline;
   }
 
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
       PhabricatorPolicyCapability::CAN_EDIT,
     );
   }
 
   public function getPolicy($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
       case PhabricatorPolicyCapability::CAN_EDIT:
         if ($this->getPHID() === null) {
           // Allow a user to create an account for themselves.
           return PhabricatorPolicies::POLICY_USER;
         } else {
           return PhabricatorPolicies::POLICY_NOONE;
         }
     }
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     $members = array_fuse($this->getMemberPHIDs());
     if (isset($members[$viewer->getPHID()])) {
       return true;
     }
 
     // If the viewer is acting on behalf of a merchant, they can see
     // payment accounts.
     if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
       foreach ($viewer->getAuthorities() as $authority) {
         if ($authority instanceof PhortuneMerchant) {
           return true;
         }
       }
     }
 
     return false;
   }
 
   public function describeAutomaticCapability($capability) {
     return pht('Members of an account can always view and edit it.');
   }
 
 
 }
diff --git a/src/applications/phortune/storage/PhortuneCart.php b/src/applications/phortune/storage/PhortuneCart.php
index 46d474cc93..70ecbb21c9 100644
--- a/src/applications/phortune/storage/PhortuneCart.php
+++ b/src/applications/phortune/storage/PhortuneCart.php
@@ -1,689 +1,689 @@
 <?php
 
 final class PhortuneCart extends PhortuneDAO
   implements
     PhabricatorApplicationTransactionInterface,
     PhabricatorPolicyInterface {
 
   const STATUS_BUILDING = 'cart:building';
   const STATUS_READY = 'cart:ready';
   const STATUS_PURCHASING = 'cart:purchasing';
   const STATUS_CHARGED = 'cart:charged';
   const STATUS_HOLD = 'cart:hold';
   const STATUS_REVIEW = 'cart:review';
   const STATUS_PURCHASED = 'cart:purchased';
 
   protected $accountPHID;
   protected $authorPHID;
   protected $merchantPHID;
   protected $subscriptionPHID;
   protected $cartClass;
   protected $status;
   protected $metadata = array();
   protected $mailKey;
   protected $isInvoice;
 
   private $account = self::ATTACHABLE;
   private $purchases = self::ATTACHABLE;
   private $implementation = self::ATTACHABLE;
   private $merchant = self::ATTACHABLE;
 
   public static function initializeNewCart(
     PhabricatorUser $actor,
     PhortuneAccount $account,
     PhortuneMerchant $merchant) {
     $cart = id(new PhortuneCart())
       ->setAuthorPHID($actor->getPHID())
       ->setStatus(self::STATUS_BUILDING)
       ->setAccountPHID($account->getPHID())
       ->setIsInvoice(0)
       ->attachAccount($account)
       ->setMerchantPHID($merchant->getPHID())
       ->attachMerchant($merchant);
 
     $cart->account = $account;
     $cart->purchases = array();
 
     return $cart;
   }
 
   public function newPurchase(
     PhabricatorUser $actor,
     PhortuneProduct $product) {
 
     $purchase = PhortunePurchase::initializeNewPurchase($actor, $product)
       ->setAccountPHID($this->getAccount()->getPHID())
       ->setCartPHID($this->getPHID())
       ->save();
 
     $this->purchases[] = $purchase;
 
     return $purchase;
   }
 
   public static function getStatusNameMap() {
     return array(
       self::STATUS_BUILDING => pht('Building'),
       self::STATUS_READY => pht('Ready'),
       self::STATUS_PURCHASING => pht('Purchasing'),
       self::STATUS_CHARGED => pht('Charged'),
       self::STATUS_HOLD => pht('Hold'),
       self::STATUS_REVIEW => pht('Review'),
       self::STATUS_PURCHASED => pht('Purchased'),
     );
   }
 
   public static function getNameForStatus($status) {
     return idx(self::getStatusNameMap(), $status, $status);
   }
 
   public function activateCart() {
     $this->openTransaction();
       $this->beginReadLocking();
 
         $copy = clone $this;
         $copy->reload();
 
         if ($copy->getStatus() !== self::STATUS_BUILDING) {
           throw new Exception(
             pht(
               'Cart has wrong status ("%s") to call willApplyCharge().',
               $copy->getStatus()));
         }
 
         $this->setStatus(self::STATUS_READY)->save();
 
       $this->endReadLocking();
     $this->saveTransaction();
 
     $this->recordCartTransaction(PhortuneCartTransaction::TYPE_CREATED);
 
     return $this;
   }
 
   public function willApplyCharge(
     PhabricatorUser $actor,
     PhortunePaymentProvider $provider,
     PhortunePaymentMethod $method = null) {
 
     $account = $this->getAccount();
 
     $charge = PhortuneCharge::initializeNewCharge()
       ->setAccountPHID($account->getPHID())
       ->setCartPHID($this->getPHID())
       ->setAuthorPHID($actor->getPHID())
       ->setMerchantPHID($this->getMerchant()->getPHID())
       ->setProviderPHID($provider->getProviderConfig()->getPHID())
       ->setAmountAsCurrency($this->getTotalPriceAsCurrency());
 
     if ($method) {
       $charge->setPaymentMethodPHID($method->getPHID());
     }
 
     $this->openTransaction();
       $this->beginReadLocking();
 
         $copy = clone $this;
         $copy->reload();
 
         if ($copy->getStatus() !== self::STATUS_READY) {
           throw new Exception(
             pht(
               'Cart has wrong status ("%s") to call willApplyCharge(), '.
               'expected "%s".',
               $copy->getStatus(),
               self::STATUS_READY));
         }
 
         $charge->save();
-        $this->setStatus(PhortuneCart::STATUS_PURCHASING)->save();
+        $this->setStatus(self::STATUS_PURCHASING)->save();
 
       $this->endReadLocking();
     $this->saveTransaction();
 
     return $charge;
   }
 
   public function didHoldCharge(PhortuneCharge $charge) {
     $charge->setStatus(PhortuneCharge::STATUS_HOLD);
 
     $this->openTransaction();
       $this->beginReadLocking();
 
         $copy = clone $this;
         $copy->reload();
 
         if ($copy->getStatus() !== self::STATUS_PURCHASING) {
           throw new Exception(
             pht(
               'Cart has wrong status ("%s") to call didHoldCharge(), '.
               'expected "%s".',
               $copy->getStatus(),
               self::STATUS_PURCHASING));
         }
 
         $charge->save();
         $this->setStatus(self::STATUS_HOLD)->save();
 
       $this->endReadLocking();
     $this->saveTransaction();
 
     $this->recordCartTransaction(PhortuneCartTransaction::TYPE_HOLD);
   }
 
   public function didApplyCharge(PhortuneCharge $charge) {
     $charge->setStatus(PhortuneCharge::STATUS_CHARGED);
 
     $this->openTransaction();
       $this->beginReadLocking();
 
         $copy = clone $this;
         $copy->reload();
 
         if (($copy->getStatus() !== self::STATUS_PURCHASING) &&
             ($copy->getStatus() !== self::STATUS_HOLD)) {
           throw new Exception(
             pht(
               'Cart has wrong status ("%s") to call didApplyCharge().',
               $copy->getStatus()));
         }
 
         $charge->save();
         $this->setStatus(self::STATUS_CHARGED)->save();
 
       $this->endReadLocking();
     $this->saveTransaction();
 
     // TODO: Perform purchase review. Here, we would apply rules to determine
     // whether the charge needs manual review (maybe making the decision via
     // Herald, configuration, or by examining provider fraud data). For now,
     // don't require review.
     $needs_review = false;
 
     if ($needs_review) {
       $this->willReviewCart();
     } else {
       $this->didReviewCart();
     }
 
     return $this;
   }
 
   public function willReviewCart() {
     $this->openTransaction();
       $this->beginReadLocking();
 
         $copy = clone $this;
         $copy->reload();
 
         if (($copy->getStatus() !== self::STATUS_CHARGED)) {
           throw new Exception(
             pht(
               'Cart has wrong status ("%s") to call willReviewCart()!',
               $copy->getStatus()));
         }
 
         $this->setStatus(self::STATUS_REVIEW)->save();
 
       $this->endReadLocking();
     $this->saveTransaction();
 
     $this->recordCartTransaction(PhortuneCartTransaction::TYPE_REVIEW);
 
     return $this;
   }
 
   public function didReviewCart() {
     $this->openTransaction();
       $this->beginReadLocking();
 
         $copy = clone $this;
         $copy->reload();
 
         if (($copy->getStatus() !== self::STATUS_CHARGED) &&
             ($copy->getStatus() !== self::STATUS_REVIEW)) {
           throw new Exception(
             pht(
               'Cart has wrong status ("%s") to call didReviewCart()!',
               $copy->getStatus()));
         }
 
         foreach ($this->purchases as $purchase) {
           $purchase->getProduct()->didPurchaseProduct($purchase);
         }
 
         $this->setStatus(self::STATUS_PURCHASED)->save();
 
       $this->endReadLocking();
     $this->saveTransaction();
 
     $this->recordCartTransaction(PhortuneCartTransaction::TYPE_PURCHASED);
 
     return $this;
   }
 
   public function didFailCharge(PhortuneCharge $charge) {
     $charge->setStatus(PhortuneCharge::STATUS_FAILED);
 
     $this->openTransaction();
       $this->beginReadLocking();
 
         $copy = clone $this;
         $copy->reload();
 
         if (($copy->getStatus() !== self::STATUS_PURCHASING) &&
             ($copy->getStatus() !== self::STATUS_HOLD)) {
           throw new Exception(
             pht(
               'Cart has wrong status ("%s") to call didFailCharge().',
               $copy->getStatus()));
         }
 
         $charge->save();
 
         // Move the cart back into STATUS_READY so the user can try
         // making the purchase again.
         $this->setStatus(self::STATUS_READY)->save();
 
       $this->endReadLocking();
     $this->saveTransaction();
 
     return $this;
   }
 
 
   public function willRefundCharge(
     PhabricatorUser $actor,
     PhortunePaymentProvider $provider,
     PhortuneCharge $charge,
     PhortuneCurrency $amount) {
 
     if (!$amount->isPositive()) {
       throw new Exception(
         pht('Trying to refund nonpositive amount of money!'));
     }
 
     if ($amount->isGreaterThan($charge->getAmountRefundableAsCurrency())) {
       throw new Exception(
         pht('Trying to refund more money than remaining on charge!'));
     }
 
     if ($charge->getRefundedChargePHID()) {
       throw new Exception(
         pht('Trying to refund a refund!'));
     }
 
     if (($charge->getStatus() !== PhortuneCharge::STATUS_CHARGED) &&
         ($charge->getStatus() !== PhortuneCharge::STATUS_HOLD)) {
       throw new Exception(
         pht('Trying to refund an uncharged charge!'));
     }
 
     $refund_charge = PhortuneCharge::initializeNewCharge()
       ->setAccountPHID($this->getAccount()->getPHID())
       ->setCartPHID($this->getPHID())
       ->setAuthorPHID($actor->getPHID())
       ->setMerchantPHID($this->getMerchant()->getPHID())
       ->setProviderPHID($provider->getProviderConfig()->getPHID())
       ->setPaymentMethodPHID($charge->getPaymentMethodPHID())
       ->setRefundedChargePHID($charge->getPHID())
       ->setAmountAsCurrency($amount->negate());
 
     $charge->openTransaction();
       $charge->beginReadLocking();
 
         $copy = clone $charge;
         $copy->reload();
 
         if ($copy->getRefundingPHID() !== null) {
           throw new Exception(
             pht('Trying to refund a charge which is already refunding!'));
         }
 
         $refund_charge->save();
         $charge->setRefundingPHID($refund_charge->getPHID());
         $charge->save();
 
       $charge->endReadLocking();
     $charge->saveTransaction();
 
     return $refund_charge;
   }
 
   public function didRefundCharge(
     PhortuneCharge $charge,
     PhortuneCharge $refund) {
 
     $refund->setStatus(PhortuneCharge::STATUS_CHARGED);
 
     $this->openTransaction();
       $this->beginReadLocking();
 
         $copy = clone $charge;
         $copy->reload();
 
         if ($charge->getRefundingPHID() !== $refund->getPHID()) {
           throw new Exception(
             pht('Charge is in the wrong refunding state!'));
         }
 
         $charge->setRefundingPHID(null);
 
         // NOTE: There's some trickiness here to get the signs right. Both
         // these values are positive but the refund has a negative value.
         $total_refunded = $charge
           ->getAmountRefundedAsCurrency()
           ->add($refund->getAmountAsCurrency()->negate());
 
         $charge->setAmountRefundedAsCurrency($total_refunded);
         $charge->save();
         $refund->save();
 
       $this->endReadLocking();
     $this->saveTransaction();
 
     $amount = $refund->getAmountAsCurrency()->negate();
     foreach ($this->purchases as $purchase) {
       $purchase->getProduct()->didRefundProduct($purchase, $amount);
     }
 
     return $this;
   }
 
   public function didFailRefund(
     PhortuneCharge $charge,
     PhortuneCharge $refund) {
 
     $refund->setStatus(PhortuneCharge::STATUS_FAILED);
 
     $this->openTransaction();
       $this->beginReadLocking();
 
         $copy = clone $charge;
         $copy->reload();
 
         if ($charge->getRefundingPHID() !== $refund->getPHID()) {
           throw new Exception(
             pht('Charge is in the wrong refunding state!'));
         }
 
         $charge->setRefundingPHID(null);
         $charge->save();
         $refund->save();
 
       $this->endReadLocking();
     $this->saveTransaction();
   }
 
   private function recordCartTransaction($type) {
     $omnipotent_user = PhabricatorUser::getOmnipotentUser();
     $phortune_phid = id(new PhabricatorPhortuneApplication())->getPHID();
 
     $xactions = array();
 
     $xactions[] = id(new PhortuneCartTransaction())
       ->setTransactionType($type)
       ->setNewValue(true);
 
     $content_source = PhabricatorContentSource::newForSource(
       PhabricatorContentSource::SOURCE_PHORTUNE,
       array());
 
     $editor = id(new PhortuneCartEditor())
       ->setActor($omnipotent_user)
       ->setActingAsPHID($phortune_phid)
       ->setContentSource($content_source)
       ->setContinueOnMissingFields(true)
       ->setContinueOnNoEffect(true);
 
     $editor->applyTransactions($this, $xactions);
   }
 
   public function getName() {
     return $this->getImplementation()->getName($this);
   }
 
   public function getDoneURI() {
     return $this->getImplementation()->getDoneURI($this);
   }
 
   public function getDoneActionName() {
     return $this->getImplementation()->getDoneActionName($this);
   }
 
   public function getCancelURI() {
     return $this->getImplementation()->getCancelURI($this);
   }
 
   public function getDescription() {
     return $this->getImplementation()->getDescription($this);
   }
 
   public function getDetailURI(PhortuneMerchant $authority = null) {
     if ($authority) {
       $prefix = 'merchant/'.$authority->getID().'/';
     } else {
       $prefix = '';
     }
     return '/phortune/'.$prefix.'cart/'.$this->getID().'/';
   }
 
   public function getCheckoutURI() {
     return '/phortune/cart/'.$this->getID().'/checkout/';
   }
 
   public function canCancelOrder() {
     try {
       $this->assertCanCancelOrder();
       return true;
     } catch (Exception $ex) {
       return false;
     }
   }
 
   public function canRefundOrder() {
     try {
       $this->assertCanRefundOrder();
       return true;
     } catch (Exception $ex) {
       return false;
     }
   }
 
   public function assertCanCancelOrder() {
     switch ($this->getStatus()) {
       case self::STATUS_BUILDING:
         throw new Exception(
           pht(
             'This order can not be cancelled because the application has not '.
             'finished building it yet.'));
       case self::STATUS_READY:
         throw new Exception(
           pht(
             'This order can not be cancelled because it has not been placed.'));
     }
 
     return $this->getImplementation()->assertCanCancelOrder($this);
   }
 
   public function assertCanRefundOrder() {
     switch ($this->getStatus()) {
       case self::STATUS_BUILDING:
         throw new Exception(
           pht(
             'This order can not be refunded because the application has not '.
             'finished building it yet.'));
       case self::STATUS_READY:
         throw new Exception(
           pht(
             'This order can not be refunded because it has not been placed.'));
     }
 
     return $this->getImplementation()->assertCanRefundOrder($this);
   }
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_SERIALIZATION => array(
         'metadata' => self::SERIALIZATION_JSON,
       ),
       self::CONFIG_COLUMN_SCHEMA => array(
         'status' => 'text32',
         'cartClass' => 'text128',
         'mailKey' => 'bytes20',
         'subscriptionPHID' => 'phid?',
         'isInvoice' => 'bool',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_account' => array(
           'columns' => array('accountPHID'),
         ),
         'key_merchant' => array(
           'columns' => array('merchantPHID'),
         ),
         'key_subscription' => array(
           'columns' => array('subscriptionPHID'),
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       PhortuneCartPHIDType::TYPECONST);
   }
 
   public function save() {
     if (!$this->getMailKey()) {
       $this->setMailKey(Filesystem::readRandomCharacters(20));
     }
     return parent::save();
   }
 
   public function attachPurchases(array $purchases) {
     assert_instances_of($purchases, 'PhortunePurchase');
     $this->purchases = $purchases;
     return $this;
   }
 
   public function getPurchases() {
     return $this->assertAttached($this->purchases);
   }
 
   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 attachImplementation(
     PhortuneCartImplementation $implementation) {
     $this->implementation = $implementation;
     return $this;
   }
 
   public function getImplementation() {
     return $this->assertAttached($this->implementation);
   }
 
   public function getTotalPriceAsCurrency() {
     $prices = array();
     foreach ($this->getPurchases() as $purchase) {
       $prices[] = $purchase->getTotalPriceAsCurrency();
     }
 
     return PhortuneCurrency::newFromList($prices);
   }
 
   public function setMetadataValue($key, $value) {
     $this->metadata[$key] = $value;
     return $this;
   }
 
   public function getMetadataValue($key, $default = null) {
     return idx($this->metadata, $key, $default);
   }
 
 
 /* -(  PhabricatorApplicationTransactionInterface  )------------------------- */
 
 
   public function getApplicationTransactionEditor() {
     return new PhortuneCartEditor();
   }
 
   public function getApplicationTransactionObject() {
     return $this;
   }
 
   public function getApplicationTransactionTemplate() {
     return new PhortuneCartTransaction();
   }
 
   public function willRenderTimeline(
     PhabricatorApplicationTransactionView $timeline,
     AphrontRequest $request) {
 
     return $timeline;
   }
 
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
       PhabricatorPolicyCapability::CAN_EDIT,
     );
   }
 
   public function getPolicy($capability) {
     // 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 order was placed with, they
     // can view the order.
     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('Orders inherit the policies of the associated account.'),
       pht('The merchant you placed an order with can review and manage it.'),
     );
   }
 
 }
diff --git a/src/applications/phragment/storage/PhragmentFragment.php b/src/applications/phragment/storage/PhragmentFragment.php
index 3f5719178b..574283d7a7 100644
--- a/src/applications/phragment/storage/PhragmentFragment.php
+++ b/src/applications/phragment/storage/PhragmentFragment.php
@@ -1,351 +1,351 @@
 <?php
 
 final class PhragmentFragment extends PhragmentDAO
   implements PhabricatorPolicyInterface {
 
   protected $path;
   protected $depth;
   protected $latestVersionPHID;
   protected $viewPolicy;
   protected $editPolicy;
 
   private $latestVersion = self::ATTACHABLE;
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_COLUMN_SCHEMA => array(
         'path' => 'text128',
         'depth' => 'uint32',
         'latestVersionPHID' => 'phid?',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_path' => array(
           'columns' => array('path'),
           'unique' => true,
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       PhragmentFragmentPHIDType::TYPECONST);
   }
 
   public function getURI() {
     return '/phragment/browse/'.$this->getPath();
   }
 
   public function getName() {
     return basename($this->path);
   }
 
   public function getFile() {
     return $this->assertAttached($this->file);
   }
 
   public function attachFile(PhabricatorFile $file) {
     return $this->file = $file;
   }
 
   public function isDirectory() {
     return $this->latestVersionPHID === null;
   }
 
   public function isDeleted() {
     return $this->getLatestVersion()->getFilePHID() === null;
   }
 
   public function getLatestVersion() {
     if ($this->latestVersionPHID === null) {
       return null;
     }
     return $this->assertAttached($this->latestVersion);
   }
 
   public function attachLatestVersion(PhragmentFragmentVersion $version) {
     return $this->latestVersion = $version;
   }
 
 
 /* -(  Updating  )  --------------------------------------------------------- */
 
 
   /**
    * Create a new fragment from a file.
    */
   public static function createFromFile(
     PhabricatorUser $viewer,
     PhabricatorFile $file = null,
     $path,
     $view_policy,
     $edit_policy) {
 
     $fragment = id(new PhragmentFragment());
     $fragment->setPath($path);
     $fragment->setDepth(count(explode('/', $path)));
     $fragment->setLatestVersionPHID(null);
     $fragment->setViewPolicy($view_policy);
     $fragment->setEditPolicy($edit_policy);
     $fragment->save();
 
     // Directory fragments have no versions associated with them, so we
     // just return the fragment at this point.
     if ($file === null) {
       return $fragment;
     }
 
     if ($file->getMimeType() === 'application/zip') {
       $fragment->updateFromZIP($viewer, $file);
     } else {
       $fragment->updateFromFile($viewer, $file);
     }
 
     return $fragment;
   }
 
 
   /**
    * Set the specified file as the next version for the fragment.
    */
   public function updateFromFile(
     PhabricatorUser $viewer,
     PhabricatorFile $file) {
 
     $existing = id(new PhragmentFragmentVersionQuery())
       ->setViewer($viewer)
       ->withFragmentPHIDs(array($this->getPHID()))
       ->execute();
     $sequence = count($existing);
 
     $this->openTransaction();
       $version = id(new PhragmentFragmentVersion());
       $version->setSequence($sequence);
       $version->setFragmentPHID($this->getPHID());
       $version->setFilePHID($file->getPHID());
       $version->save();
 
       $this->setLatestVersionPHID($version->getPHID());
       $this->save();
     $this->saveTransaction();
 
     $file->attachToObject($version->getPHID());
   }
 
   /**
    * Apply the specified ZIP archive onto the fragment, removing
    * and creating fragments as needed.
    */
   public function updateFromZIP(
     PhabricatorUser $viewer,
     PhabricatorFile $file) {
 
     if ($file->getMimeType() !== 'application/zip') {
       throw new Exception("File must have mimetype 'application/zip'");
     }
 
     // First apply the ZIP as normal.
     $this->updateFromFile($viewer, $file);
 
     // Ensure we have ZIP support.
     $zip = null;
     try {
       $zip = new ZipArchive();
     } catch (Exception $e) {
       // The server doesn't have php5-zip, so we can't do recursive updates.
       return;
     }
 
     $temp = new TempFile();
     Filesystem::writeFile($temp, $file->loadFileData());
     if (!$zip->open($temp)) {
       throw new Exception('Unable to open ZIP');
     }
 
     // Get all of the paths and their data from the ZIP.
     $mappings = array();
     for ($i = 0; $i < $zip->numFiles; $i++) {
       $path = trim($zip->getNameIndex($i), '/');
       $stream = $zip->getStream($path);
       $data = null;
       // If the stream is false, then it is a directory entry. We leave
       // $data set to null for directories so we know not to create a
       // version entry for them.
       if ($stream !== false) {
         $data = stream_get_contents($stream);
         fclose($stream);
       }
       $mappings[$path] = $data;
     }
 
     // We need to detect any directories that are in the ZIP folder that
     // aren't explicitly noted in the ZIP. This can happen if the file
     // entries in the ZIP look like:
     //
     //  * something/blah.png
     //  * something/other.png
     //  * test.png
     //
     // Where there is no explicit "something/" entry.
     foreach ($mappings as $path_key => $data) {
       if ($data === null) {
         continue;
       }
       $directory = dirname($path_key);
       while ($directory !== '.') {
         if (!array_key_exists($directory, $mappings)) {
           $mappings[$directory] = null;
         }
         if (dirname($directory) === $directory) {
           // dirname() will not reduce this directory any further; to
           // prevent infinite loop we just break out here.
           break;
         }
         $directory = dirname($directory);
       }
     }
 
     // Adjust the paths relative to this fragment so we can look existing
     // fragments up in the DB.
     $base_path = $this->getPath();
     $paths = array();
     foreach ($mappings as $p => $data) {
       $paths[] = $base_path.'/'.$p;
     }
 
     // FIXME: What happens when a child exists, but the current user
     // can't see it. We're going to create a new child with the exact
     // same path and then bad things will happen.
     $children = id(new PhragmentFragmentQuery())
       ->setViewer($viewer)
       ->needLatestVersion(true)
       ->withLeadingPath($this->getPath().'/')
       ->execute();
     $children = mpull($children, null, 'getPath');
 
     // Iterate over the existing fragments.
     foreach ($children as $full_path => $child) {
       $path = substr($full_path, strlen($base_path) + 1);
       if (array_key_exists($path, $mappings)) {
         if ($child->isDirectory() && $mappings[$path] === null) {
           // Don't create a version entry for a directory
           // (unless it's been converted into a file).
           continue;
         }
 
         // The file is being updated.
         $file = PhabricatorFile::newFromFileData(
           $mappings[$path],
           array('name' => basename($path)));
         $child->updateFromFile($viewer, $file);
       } else {
         // The file is being deleted.
         $child->deleteFile($viewer);
       }
     }
 
     // Iterate over the mappings to find new files.
     foreach ($mappings as $path => $data) {
       if (!array_key_exists($base_path.'/'.$path, $children)) {
         // The file is being created. If the data is null,
         // then this is explicitly a directory being created.
         $file = null;
         if ($mappings[$path] !== null) {
           $file = PhabricatorFile::newFromFileData(
             $mappings[$path],
             array('name' => basename($path)));
         }
-        PhragmentFragment::createFromFile(
+        self::createFromFile(
           $viewer,
           $file,
           $base_path.'/'.$path,
           $this->getViewPolicy(),
           $this->getEditPolicy());
       }
     }
   }
 
   /**
    * Delete the contents of the specified fragment.
    */
   public function deleteFile(PhabricatorUser $viewer) {
     $existing = id(new PhragmentFragmentVersionQuery())
       ->setViewer($viewer)
       ->withFragmentPHIDs(array($this->getPHID()))
       ->execute();
     $sequence = count($existing);
 
     $this->openTransaction();
       $version = id(new PhragmentFragmentVersion());
       $version->setSequence($sequence);
       $version->setFragmentPHID($this->getPHID());
       $version->setFilePHID(null);
       $version->save();
 
       $this->setLatestVersionPHID($version->getPHID());
       $this->save();
     $this->saveTransaction();
   }
 
 
 /* -(  Utility  )  ---------------------------------------------------------- */
 
 
   public function getFragmentMappings(
     PhabricatorUser $viewer,
     $base_path) {
 
     $children = id(new PhragmentFragmentQuery())
       ->setViewer($viewer)
       ->needLatestVersion(true)
       ->withLeadingPath($this->getPath().'/')
       ->withDepths(array($this->getDepth() + 1))
       ->execute();
 
     if (count($children) === 0) {
       $path = substr($this->getPath(), strlen($base_path) + 1);
       return array($path => $this);
     } else {
       $mappings = array();
       foreach ($children as $child) {
         $child_mappings = $child->getFragmentMappings(
           $viewer,
           $base_path);
         foreach ($child_mappings as $key => $value) {
           $mappings[$key] = $value;
         }
       }
       return $mappings;
     }
   }
 
 
 /* -(  Policy Interface  )--------------------------------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
       PhabricatorPolicyCapability::CAN_EDIT,
     );
   }
 
   public function getPolicy($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         return $this->getViewPolicy();
       case PhabricatorPolicyCapability::CAN_EDIT:
         return $this->getEditPolicy();
     }
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     return false;
   }
 
   public function describeAutomaticCapability($capability) {
     return null;
   }
 
 }
diff --git a/src/applications/policy/constants/PhabricatorPolicies.php b/src/applications/policy/constants/PhabricatorPolicies.php
index 859010eca2..c33b9bf909 100644
--- a/src/applications/policy/constants/PhabricatorPolicies.php
+++ b/src/applications/policy/constants/PhabricatorPolicies.php
@@ -1,25 +1,25 @@
 <?php
 
 final class PhabricatorPolicies extends PhabricatorPolicyConstants {
 
   const POLICY_PUBLIC   = 'public';
   const POLICY_USER     = 'users';
   const POLICY_ADMIN    = 'admin';
   const POLICY_NOONE    = 'no-one';
 
   /**
    * Returns the most public policy this install's configuration permits.
    * This is either "public" (if available) or "all users" (if not).
    *
    * @return const Most open working policy constant.
    */
   public static function getMostOpenPolicy() {
     if (PhabricatorEnv::getEnvConfig('policy.allow-public')) {
-      return PhabricatorPolicies::POLICY_PUBLIC;
+      return self::POLICY_PUBLIC;
     } else {
-      return PhabricatorPolicies::POLICY_USER;
+      return self::POLICY_USER;
     }
   }
 
 
 }
diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php
index 031427d058..b9c366fecc 100644
--- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php
+++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php
@@ -1,744 +1,745 @@
 <?php
 
 final class PhabricatorProjectBoardViewController
   extends PhabricatorProjectBoardController {
 
   const BATCH_EDIT_ALL = 'all';
 
   private $id;
   private $slug;
   private $handles;
   private $queryKey;
   private $filter;
   private $sortKey;
   private $showHidden;
 
   public function shouldAllowPublic() {
     return true;
   }
 
   public function handleRequest(AphrontRequest $request) {
     $viewer = $request->getUser();
     $id = $request->getURIData('id');
 
     $show_hidden = $request->getBool('hidden');
     $this->showHidden = $show_hidden;
 
     $project = id(new PhabricatorProjectQuery())
       ->setViewer($viewer)
       ->needImages(true);
     $id = $request->getURIData('id');
     $slug = $request->getURIData('slug');
     if ($slug) {
       $project->withSlugs(array($slug));
     } else {
       $project->withIDs(array($id));
     }
     $project = $project->executeOne();
     if (!$project) {
       return new Aphront404Response();
     }
 
     $this->setProject($project);
     $this->id = $project->getID();
 
     $sort_key = $request->getStr('order');
     switch ($sort_key) {
       case PhabricatorProjectColumn::ORDER_NATURAL:
       case PhabricatorProjectColumn::ORDER_PRIORITY:
         break;
       default:
         $sort_key = PhabricatorProjectColumn::DEFAULT_ORDER;
         break;
     }
     $this->sortKey = $sort_key;
 
     $column_query = id(new PhabricatorProjectColumnQuery())
       ->setViewer($viewer)
       ->withProjectPHIDs(array($project->getPHID()));
     if (!$show_hidden) {
       $column_query->withStatuses(
         array(PhabricatorProjectColumn::STATUS_ACTIVE));
     }
 
     $columns = $column_query->execute();
     $columns = mpull($columns, null, 'getSequence');
 
     // TODO: Expand the checks here if we add the ability
     // to hide the Backlog column
     if (!$columns) {
       switch ($request->getStr('initialize-type')) {
         case 'backlog-only':
           $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
             $column = PhabricatorProjectColumn::initializeNewColumn($viewer)
               ->setSequence(0)
               ->setProperty('isDefault', true)
               ->setProjectPHID($project->getPHID())
               ->save();
             $column->attachProject($project);
             $columns[0] = $column;
           unset($unguarded);
           break;
         case 'import':
           return id(new AphrontRedirectResponse())
             ->setURI(
               $this->getApplicationURI('board/'.$project->getID().'/import/'));
           break;
         default:
           return $this->initializeWorkboardDialog($project);
           break;
       }
     }
 
     ksort($columns);
 
     $board_uri = $this->getApplicationURI('board/'.$project->getID().'/');
 
     $engine = id(new ManiphestTaskSearchEngine())
       ->setViewer($viewer)
       ->setBaseURI($board_uri)
       ->setIsBoardView(true);
 
     if ($request->isFormPost()) {
       $saved = $engine->buildSavedQueryFromRequest($request);
       $engine->saveQuery($saved);
       $filter_form = id(new AphrontFormView())
         ->setUser($viewer);
       $engine->buildSearchForm($filter_form, $saved);
       if ($engine->getErrors()) {
         return $this->newDialog()
           ->setWidth(AphrontDialogView::WIDTH_FULL)
           ->setTitle(pht('Advanced Filter'))
           ->appendChild($filter_form->buildLayoutView())
           ->setErrors($engine->getErrors())
           ->setSubmitURI($board_uri)
           ->addSubmitButton(pht('Apply Filter'))
           ->addCancelButton($board_uri);
       }
       return id(new AphrontRedirectResponse())->setURI(
         $this->getURIWithState(
           $engine->getQueryResultsPageURI($saved->getQueryKey())));
     }
 
     $query_key = $request->getURIData('queryKey');
     if (!$query_key) {
       $query_key = 'open';
     }
     $this->queryKey = $query_key;
 
     $custom_query = null;
     if ($engine->isBuiltinQuery($query_key)) {
       $saved = $engine->buildSavedQueryFromBuiltin($query_key);
     } else {
       $saved = id(new PhabricatorSavedQueryQuery())
         ->setViewer($viewer)
         ->withQueryKeys(array($query_key))
         ->executeOne();
 
       if (!$saved) {
         return new Aphront404Response();
       }
 
       $custom_query = $saved;
     }
 
     if ($request->getURIData('filter')) {
       $filter_form = id(new AphrontFormView())
         ->setUser($viewer);
       $engine->buildSearchForm($filter_form, $saved);
 
       return $this->newDialog()
         ->setWidth(AphrontDialogView::WIDTH_FULL)
         ->setTitle(pht('Advanced Filter'))
         ->appendChild($filter_form->buildLayoutView())
         ->setSubmitURI($board_uri)
         ->addSubmitButton(pht('Apply Filter'))
         ->addCancelButton($board_uri);
     }
 
     $task_query = $engine->buildQueryFromSavedQuery($saved);
 
     $tasks = $task_query
       ->withEdgeLogicPHIDs(
         PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
         PhabricatorQueryConstraint::OPERATOR_AND,
         array($project->getPHID()))
       ->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY)
       ->setViewer($viewer)
       ->execute();
     $tasks = mpull($tasks, null, 'getPHID');
 
     if ($tasks) {
       $positions = id(new PhabricatorProjectColumnPositionQuery())
         ->setViewer($viewer)
         ->withObjectPHIDs(mpull($tasks, 'getPHID'))
         ->withColumns($columns)
         ->execute();
       $positions = mpull($positions, null, 'getObjectPHID');
     } else {
       $positions = array();
     }
 
     $task_map = array();
     foreach ($tasks as $task) {
       $task_phid = $task->getPHID();
       if (empty($positions[$task_phid])) {
         // This shouldn't normally be possible because we create positions on
         // demand, but we might have raced as an object was removed from the
         // board. Just drop the task if we don't have a position for it.
         continue;
       }
 
       $position = $positions[$task_phid];
       $task_map[$position->getColumnPHID()][] = $task_phid;
     }
 
     // If we're showing the board in "natural" order, sort columns by their
     // column positions.
     if ($this->sortKey == PhabricatorProjectColumn::ORDER_NATURAL) {
       foreach ($task_map as $column_phid => $task_phids) {
         $order = array();
         foreach ($task_phids as $task_phid) {
           if (isset($positions[$task_phid])) {
             $order[$task_phid] = $positions[$task_phid]->getOrderingKey();
           } else {
             $order[$task_phid] = 0;
           }
         }
         asort($order);
         $task_map[$column_phid] = array_keys($order);
       }
     }
 
     $task_can_edit_map = id(new PhabricatorPolicyFilter())
       ->setViewer($viewer)
       ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT))
       ->apply($tasks);
 
     // If this is a batch edit, select the editable tasks in the chosen column
     // and ship the user into the batch editor.
     $batch_edit = $request->getStr('batch');
     if ($batch_edit) {
       if ($batch_edit !== self::BATCH_EDIT_ALL) {
         $column_id_map = mpull($columns, null, 'getID');
         $batch_column = idx($column_id_map, $batch_edit);
         if (!$batch_column) {
           return new Aphront404Response();
         }
 
         $batch_task_phids = idx($task_map, $batch_column->getPHID(), array());
         foreach ($batch_task_phids as $key => $batch_task_phid) {
           if (empty($task_can_edit_map[$batch_task_phid])) {
             unset($batch_task_phids[$key]);
           }
         }
 
         $batch_tasks = array_select_keys($tasks, $batch_task_phids);
       } else {
         $batch_tasks = $task_can_edit_map;
       }
 
       if (!$batch_tasks) {
         $cancel_uri = $this->getURIWithState($board_uri);
         return $this->newDialog()
           ->setTitle(pht('No Editable Tasks'))
           ->appendParagraph(
             pht(
               'The selected column contains no visible tasks which you '.
               'have permission to edit.'))
           ->addCancelButton($board_uri);
       }
 
       $batch_ids = mpull($batch_tasks, 'getID');
       $batch_ids = implode(',', $batch_ids);
 
       $batch_uri = new PhutilURI('/maniphest/batch/');
       $batch_uri->setQueryParam('board', $this->id);
       $batch_uri->setQueryParam('batch', $batch_ids);
       return id(new AphrontRedirectResponse())
         ->setURI($batch_uri);
     }
 
     $board_id = celerity_generate_unique_node_id();
 
     $board = id(new PHUIWorkboardView())
       ->setUser($viewer)
       ->setID($board_id);
 
     $behavior_config = array(
       'boardID' => $board_id,
       'projectPHID' => $project->getPHID(),
       'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'),
       'createURI' => '/maniphest/task/create/',
       'order' => $this->sortKey,
     );
     $this->initBehavior(
       'project-boards',
       $behavior_config);
     $this->addExtraQuickSandConfig(array('boardConfig' => $behavior_config));
 
     $this->handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks);
 
     foreach ($columns as $column) {
       $task_phids = idx($task_map, $column->getPHID(), array());
       $column_tasks = array_select_keys($tasks, $task_phids);
 
       $panel = id(new PHUIWorkpanelView())
         ->setHeader($column->getDisplayName())
         ->setSubHeader($column->getDisplayType())
         ->addSigil('workpanel');
 
       $header_icon = $column->getHeaderIcon();
       if ($header_icon) {
         $panel->setHeaderIcon($header_icon);
       }
 
       if ($column->isHidden()) {
         $panel->addClass('project-panel-hidden');
       }
 
       $column_menu = $this->buildColumnMenu($project, $column);
       $panel->addHeaderAction($column_menu);
 
       $tag_id = celerity_generate_unique_node_id();
       $tag_content_id = celerity_generate_unique_node_id();
 
       $count_tag = id(new PHUITagView())
         ->setType(PHUITagView::TYPE_SHADE)
         ->setShade(PHUITagView::COLOR_BLUE)
         ->setID($tag_id)
         ->setName(phutil_tag('span', array('id' => $tag_content_id), '-'))
         ->setStyle('display: none');
 
       $panel->setHeaderTag($count_tag);
 
       $cards = id(new PHUIObjectItemListView())
         ->setUser($viewer)
         ->setFlush(true)
         ->setAllowEmptyList(true)
         ->addSigil('project-column')
         ->setMetadata(
           array(
             'columnPHID' => $column->getPHID(),
             'countTagID' => $tag_id,
             'countTagContentID' => $tag_content_id,
             'pointLimit' => $column->getPointLimit(),
           ));
 
       foreach ($column_tasks as $task) {
         $owner = null;
         if ($task->getOwnerPHID()) {
           $owner = $this->handles[$task->getOwnerPHID()];
         }
         $can_edit = idx($task_can_edit_map, $task->getPHID(), false);
         $cards->addItem(id(new ProjectBoardTaskCard())
           ->setViewer($viewer)
           ->setTask($task)
           ->setOwner($owner)
           ->setCanEdit($can_edit)
           ->getItem());
       }
       $panel->setCards($cards);
       $board->addPanel($panel);
     }
 
     $sort_menu = $this->buildSortMenu(
       $viewer,
       $sort_key);
 
     $filter_menu = $this->buildFilterMenu(
       $viewer,
       $custom_query,
       $engine,
       $query_key);
 
     $manage_menu = $this->buildManageMenu($project, $show_hidden);
 
     $header_link = phutil_tag(
       'a',
       array(
         'href' => $this->getApplicationURI('profile/'.$project->getID().'/'),
       ),
       $project->getName());
 
     $header = id(new PHUIHeaderView())
       ->setHeader(pht('%s Workboard', $header_link))
       ->setUser($viewer)
       ->setNoBackground(true)
       ->addActionLink($sort_menu)
       ->addActionLink($filter_menu)
       ->addActionLink($manage_menu)
       ->setPolicyObject($project);
 
     $board_box = id(new PHUIBoxView())
       ->appendChild($board)
       ->addClass('project-board-wrapper');
 
     $nav = $this->buildIconNavView($project);
     $nav->appendChild($header);
     $nav->appendChild($board_box);
 
     return $this->buildApplicationPage(
       $nav,
       array(
         'title' => pht('%s Board', $project->getName()),
         'showFooter' => false,
+        'pageObjects' => array($project->getPHID()),
       ));
   }
 
   private function buildSortMenu(
     PhabricatorUser $viewer,
     $sort_key) {
 
     $sort_icon = id(new PHUIIconView())
       ->setIconFont('fa-sort-amount-asc bluegrey');
 
     $named = array(
       PhabricatorProjectColumn::ORDER_NATURAL => pht('Natural'),
       PhabricatorProjectColumn::ORDER_PRIORITY => pht('Sort by Priority'),
     );
 
     $base_uri = $this->getURIWithState();
 
     $items = array();
     foreach ($named as $key => $name) {
       $is_selected = ($key == $sort_key);
       if ($is_selected) {
         $active_order = $name;
       }
 
       $item = id(new PhabricatorActionView())
         ->setIcon('fa-sort-amount-asc')
         ->setSelected($is_selected)
         ->setName($name);
 
       $uri = $base_uri->alter('order', $key);
       $item->setHref($uri);
 
       $items[] = $item;
     }
 
     $sort_menu = id(new PhabricatorActionListView())
       ->setUser($viewer);
     foreach ($items as $item) {
       $sort_menu->addAction($item);
     }
 
     $sort_button = id(new PHUIButtonView())
       ->setText(pht('Sort: %s', $active_order))
       ->setIcon($sort_icon)
       ->setTag('a')
       ->setHref('#')
       ->addSigil('boards-dropdown-menu')
       ->setMetadata(
         array(
           'items' => hsprintf('%s', $sort_menu),
         ));
 
     return $sort_button;
   }
   private function buildFilterMenu(
     PhabricatorUser $viewer,
     $custom_query,
     PhabricatorApplicationSearchEngine $engine,
     $query_key) {
 
     $filter_icon = id(new PHUIIconView())
       ->setIconFont('fa-search-plus bluegrey');
 
     $named = array(
       'open' => pht('Open Tasks'),
       'all' => pht('All Tasks'),
     );
 
     if ($viewer->isLoggedIn()) {
       $named['assigned'] = pht('Assigned to Me');
     }
 
     if ($custom_query) {
       $named[$custom_query->getQueryKey()] = pht('Custom Filter');
     }
 
     $items = array();
     foreach ($named as $key => $name) {
       $is_selected = ($key == $query_key);
       if ($is_selected) {
         $active_filter = $name;
       }
 
       $is_custom = false;
       if ($custom_query) {
         $is_custom = ($key == $custom_query->getQueryKey());
       }
 
       $item = id(new PhabricatorActionView())
         ->setIcon('fa-search')
         ->setSelected($is_selected)
         ->setName($name);
 
       if ($is_custom) {
         $uri = $this->getApplicationURI(
           'board/'.$this->id.'/filter/query/'.$key.'/');
         $item->setWorkflow(true);
       } else {
         $uri = $engine->getQueryResultsPageURI($key);
       }
 
       $uri = $this->getURIWithState($uri);
       $item->setHref($uri);
 
       $items[] = $item;
     }
 
     $items[] = id(new PhabricatorActionView())
       ->setIcon('fa-cog')
       ->setHref($this->getApplicationURI('board/'.$this->id.'/filter/'))
       ->setWorkflow(true)
       ->setName(pht('Advanced Filter...'));
 
     $filter_menu = id(new PhabricatorActionListView())
         ->setUser($viewer);
     foreach ($items as $item) {
       $filter_menu->addAction($item);
     }
 
     $filter_button = id(new PHUIButtonView())
       ->setText(pht('Filter: %s', $active_filter))
       ->setIcon($filter_icon)
       ->setTag('a')
       ->setHref('#')
       ->addSigil('boards-dropdown-menu')
       ->setMetadata(
         array(
           'items' => hsprintf('%s', $filter_menu),
         ));
 
     return $filter_button;
   }
 
   private function buildManageMenu(
     PhabricatorProject $project,
     $show_hidden) {
 
     $request = $this->getRequest();
     $viewer = $request->getUser();
 
     $can_edit = PhabricatorPolicyFilter::hasCapability(
       $viewer,
       $project,
       PhabricatorPolicyCapability::CAN_EDIT);
 
     $manage_icon = id(new PHUIIconView())
       ->setIconFont('fa-cog bluegrey');
 
     $manage_items = array();
 
     $manage_items[] = id(new PhabricatorActionView())
       ->setIcon('fa-plus')
       ->setName(pht('Add Column'))
       ->setHref($this->getApplicationURI('board/'.$this->id.'/edit/'))
       ->setDisabled(!$can_edit)
       ->setWorkflow(!$can_edit);
 
     $manage_items[] = id(new PhabricatorActionView())
       ->setIcon('fa-exchange')
       ->setName(pht('Reorder Columns'))
       ->setHref($this->getApplicationURI('board/'.$this->id.'/reorder/'))
       ->setDisabled(!$can_edit)
       ->setWorkflow(true);
 
     if ($show_hidden) {
       $hidden_uri = $this->getURIWithState()
         ->setQueryParam('hidden', null);
       $hidden_icon = 'fa-eye-slash';
       $hidden_text = pht('Hide Hidden Columns');
     } else {
       $hidden_uri = $this->getURIWithState()
         ->setQueryParam('hidden', 'true');
       $hidden_icon = 'fa-eye';
       $hidden_text = pht('Show Hidden Columns');
     }
 
     $manage_items[] = id(new PhabricatorActionView())
       ->setIcon($hidden_icon)
       ->setName($hidden_text)
       ->setHref($hidden_uri);
 
     $batch_edit_uri = $request->getRequestURI();
     $batch_edit_uri->setQueryParam('batch', self::BATCH_EDIT_ALL);
     $can_batch_edit = PhabricatorPolicyFilter::hasCapability(
       $viewer,
       PhabricatorApplication::getByClass('PhabricatorManiphestApplication'),
       ManiphestBulkEditCapability::CAPABILITY);
 
     $manage_items[] = id(new PhabricatorActionView())
       ->setIcon('fa-list-ul')
       ->setName(pht('Batch Edit Visible Tasks...'))
       ->setHref($batch_edit_uri)
       ->setDisabled(!$can_batch_edit);
 
     $manage_menu = id(new PhabricatorActionListView())
         ->setUser($viewer);
     foreach ($manage_items as $item) {
       $manage_menu->addAction($item);
     }
 
     $manage_button = id(new PHUIButtonView())
       ->setText(pht('Manage Board'))
       ->setIcon($manage_icon)
       ->setTag('a')
       ->setHref('#')
       ->addSigil('boards-dropdown-menu')
       ->setMetadata(
         array(
           'items' => hsprintf('%s', $manage_menu),
         ));
 
     return $manage_button;
   }
 
   private function buildColumnMenu(
     PhabricatorProject $project,
     PhabricatorProjectColumn $column) {
 
     $request = $this->getRequest();
     $viewer = $request->getUser();
 
     $can_edit = PhabricatorPolicyFilter::hasCapability(
       $viewer,
       $project,
       PhabricatorPolicyCapability::CAN_EDIT);
 
     $column_items = array();
 
     $column_items[] = id(new PhabricatorActionView())
       ->setIcon('fa-plus')
       ->setName(pht('Create Task...'))
       ->setHref('/maniphest/task/create/')
       ->addSigil('column-add-task')
       ->setMetadata(
         array(
           'columnPHID' => $column->getPHID(),
         ))
       ->setDisabled(!$can_edit);
 
     $batch_edit_uri = $request->getRequestURI();
     $batch_edit_uri->setQueryParam('batch', $column->getID());
     $can_batch_edit = PhabricatorPolicyFilter::hasCapability(
       $viewer,
       PhabricatorApplication::getByClass('PhabricatorManiphestApplication'),
       ManiphestBulkEditCapability::CAPABILITY);
 
     $column_items[] = id(new PhabricatorActionView())
       ->setIcon('fa-list-ul')
       ->setName(pht('Batch Edit Tasks...'))
       ->setHref($batch_edit_uri)
       ->setDisabled(!$can_batch_edit);
 
     $edit_uri = $this->getApplicationURI(
       'board/'.$this->id.'/column/'.$column->getID().'/');
 
     $column_items[] = id(new PhabricatorActionView())
       ->setIcon('fa-pencil')
       ->setName(pht('Edit Column'))
       ->setHref($edit_uri)
       ->setDisabled(!$can_edit)
       ->setWorkflow(!$can_edit);
 
     $can_hide = ($can_edit && !$column->isDefaultColumn());
     $hide_uri = 'board/'.$this->id.'/hide/'.$column->getID().'/';
     $hide_uri = $this->getApplicationURI($hide_uri);
     $hide_uri = $this->getURIWithState($hide_uri);
 
     if (!$column->isHidden()) {
       $column_items[] = id(new PhabricatorActionView())
         ->setName(pht('Hide Column'))
         ->setIcon('fa-eye-slash')
         ->setHref($hide_uri)
         ->setDisabled(!$can_hide)
         ->setWorkflow(true);
     } else {
       $column_items[] = id(new PhabricatorActionView())
         ->setName(pht('Show Column'))
         ->setIcon('fa-eye')
         ->setHref($hide_uri)
         ->setDisabled(!$can_hide)
         ->setWorkflow(true);
     }
 
     $column_menu = id(new PhabricatorActionListView())
       ->setUser($viewer);
     foreach ($column_items as $item) {
       $column_menu->addAction($item);
     }
 
     $column_button = id(new PHUIIconView())
       ->setIconFont('fa-caret-down')
       ->setHref('#')
       ->addSigil('boards-dropdown-menu')
       ->setMetadata(
         array(
           'items' => hsprintf('%s', $column_menu),
         ));
 
     return $column_button;
   }
 
   private function initializeWorkboardDialog(PhabricatorProject $project) {
 
     $instructions = pht('This workboard has not been setup yet.');
     $new_selector = id(new AphrontFormRadioButtonControl())
       ->setName('initialize-type')
       ->setValue('backlog-only')
       ->addButton(
         'backlog-only',
         pht('New Empty Board'),
         pht('Create a new board with just a backlog column.'))
       ->addButton(
         'import',
         pht('Import Columns'),
         pht('Import board columns from another project.'));
 
     $dialog = id(new AphrontDialogView())
       ->setUser($this->getRequest()->getUser())
       ->setTitle(pht('New Workboard'))
       ->addSubmitButton('Continue')
       ->addCancelButton($this->getApplicationURI('view/'.$project->getID().'/'))
       ->appendParagraph($instructions)
       ->appendChild($new_selector);
 
     return id(new AphrontDialogResponse())
       ->setDialog($dialog);
   }
 
 
   /**
    * Add current state parameters (like order and the visibility of hidden
    * columns) to a URI.
    *
    * This allows actions which toggle or adjust one piece of state to keep
    * the rest of the board state persistent. If no URI is provided, this method
    * starts with the request URI.
    *
    * @param string|null   URI to add state parameters to.
    * @return PhutilURI    URI with state parameters.
    */
   private function getURIWithState($base = null) {
     if ($base === null) {
       $base = $this->getRequest()->getRequestURI();
     }
 
     $base = new PhutilURI($base);
 
     if ($this->sortKey != PhabricatorProjectColumn::DEFAULT_ORDER) {
       $base->setQueryParam('order', $this->sortKey);
     } else {
       $base->setQueryParam('order', null);
     }
 
     $base->setQueryParam('hidden', $this->showHidden ? 'true' : null);
 
     return $base;
   }
 
 }
diff --git a/src/applications/project/controller/PhabricatorProjectEditPictureController.php b/src/applications/project/controller/PhabricatorProjectEditPictureController.php
index 58e345930e..ca99159718 100644
--- a/src/applications/project/controller/PhabricatorProjectEditPictureController.php
+++ b/src/applications/project/controller/PhabricatorProjectEditPictureController.php
@@ -1,304 +1,301 @@
 <?php
 
 final class PhabricatorProjectEditPictureController
   extends PhabricatorProjectController {
 
   private $id;
 
   public function willProcessRequest(array $data) {
     $this->id = $data['id'];
   }
 
   public function processRequest() {
     $request = $this->getRequest();
     $viewer = $request->getUser();
     $id = $request->getURIData('id');
 
     $project = id(new PhabricatorProjectQuery())
       ->setViewer($viewer)
       ->withIDs(array($this->id))
       ->needImages(true)
       ->requireCapabilities(
         array(
           PhabricatorPolicyCapability::CAN_VIEW,
           PhabricatorPolicyCapability::CAN_EDIT,
         ))
       ->executeOne();
     if (!$project) {
       return new Aphront404Response();
     }
 
     $edit_uri = $this->getApplicationURI('profile/'.$project->getID().'/');
     $view_uri = $this->getApplicationURI('profile/'.$project->getID().'/');
 
     $supported_formats = PhabricatorFile::getTransformableImageFormats();
     $e_file = true;
     $errors = array();
 
     if ($request->isFormPost()) {
       $phid = $request->getStr('phid');
       $is_default = false;
       if ($phid == PhabricatorPHIDConstants::PHID_VOID) {
         $phid = null;
         $is_default = true;
       } else if ($phid) {
         $file = id(new PhabricatorFileQuery())
           ->setViewer($viewer)
           ->withPHIDs(array($phid))
           ->executeOne();
       } else {
         if ($request->getFileExists('picture')) {
           $file = PhabricatorFile::newFromPHPUpload(
             $_FILES['picture'],
             array(
               'authorPHID' => $viewer->getPHID(),
               'canCDN' => true,
             ));
         } else {
           $e_file = pht('Required');
           $errors[] = pht(
             'You must choose a file when uploading a new project picture.');
         }
       }
 
       if (!$errors && !$is_default) {
         if (!$file->isTransformableImage()) {
           $e_file = pht('Not Supported');
           $errors[] = pht(
             'This server only supports these image formats: %s.',
             implode(', ', $supported_formats));
         } else {
-          $xformer = new PhabricatorImageTransformer();
-          $xformed = $xformer->executeProfileTransform(
-            $file,
-            $width = 50,
-            $min_height = 50,
-            $max_height = 50);
+          $xform = PhabricatorFileTransform::getTransformByKey(
+            PhabricatorFileThumbnailTransform::TRANSFORM_PROFILE);
+          $xformed = $xform->executeTransform($file);
         }
       }
 
       if (!$errors) {
         if ($is_default) {
           $new_value = null;
         } else {
           $new_value = $xformed->getPHID();
         }
 
         $xactions = array();
         $xactions[] = id(new PhabricatorProjectTransaction())
           ->setTransactionType(PhabricatorProjectTransaction::TYPE_IMAGE)
           ->setNewValue($new_value);
 
         $editor = id(new PhabricatorProjectTransactionEditor())
           ->setActor($viewer)
           ->setContentSourceFromRequest($request)
           ->setContinueOnMissingFields(true)
           ->setContinueOnNoEffect(true);
 
         $editor->applyTransactions($project, $xactions);
 
         return id(new AphrontRedirectResponse())->setURI($edit_uri);
       }
     }
 
     $title = pht('Edit Project Picture');
 
     $form = id(new PHUIFormLayoutView())
       ->setUser($viewer);
 
     $default_image = PhabricatorFile::loadBuiltin($viewer, 'project.png');
 
     $images = array();
 
     $current = $project->getProfileImagePHID();
     $has_current = false;
     if ($current) {
       $files = id(new PhabricatorFileQuery())
         ->setViewer($viewer)
         ->withPHIDs(array($current))
         ->execute();
       if ($files) {
         $file = head($files);
         if ($file->isTransformableImage()) {
           $has_current = true;
           $images[$current] = array(
             'uri' => $file->getBestURI(),
             'tip' => pht('Current Picture'),
           );
         }
       }
     }
 
     $images[PhabricatorPHIDConstants::PHID_VOID] = array(
       'uri' => $default_image->getBestURI(),
       'tip' => pht('Default Picture'),
     );
 
     require_celerity_resource('people-profile-css');
     Javelin::initBehavior('phabricator-tooltips', array());
 
     $buttons = array();
     foreach ($images as $phid => $spec) {
       $button = javelin_tag(
         'button',
         array(
           'class' => 'grey profile-image-button',
           'sigil' => 'has-tooltip',
           'meta' => array(
             'tip' => $spec['tip'],
             'size' => 300,
           ),
         ),
         phutil_tag(
           'img',
           array(
             'height' => 50,
             'width' => 50,
             'src' => $spec['uri'],
           )));
 
       $button = array(
         phutil_tag(
           'input',
           array(
             'type'  => 'hidden',
             'name'  => 'phid',
             'value' => $phid,
           )),
         $button,
       );
 
       $button = phabricator_form(
         $viewer,
         array(
           'class' => 'profile-image-form',
           'method' => 'POST',
         ),
         $button);
 
       $buttons[] = $button;
     }
 
     if ($has_current) {
       $form->appendChild(
         id(new AphrontFormMarkupControl())
           ->setLabel(pht('Current Picture'))
           ->setValue(array_shift($buttons)));
     }
 
     $form->appendChild(
       id(new AphrontFormMarkupControl())
         ->setLabel(pht('Use Picture'))
         ->setValue($buttons));
 
     $launch_id = celerity_generate_unique_node_id();
     $input_id = celerity_generate_unique_node_id();
 
     Javelin::initBehavior(
       'launch-icon-composer',
       array(
         'launchID' => $launch_id,
         'inputID' => $input_id,
       ));
 
     $compose_button = javelin_tag(
       'button',
       array(
         'class' => 'grey',
         'id' => $launch_id,
         'sigil' => 'icon-composer',
       ),
       pht('Choose Icon and Color...'));
 
     $compose_input = javelin_tag(
       'input',
       array(
         'type' => 'hidden',
         'id' => $input_id,
         'name' => 'phid',
       ));
 
     $compose_form = phabricator_form(
       $viewer,
       array(
         'class' => 'profile-image-form',
         'method' => 'POST',
       ),
       array(
         $compose_input,
         $compose_button,
       ));
 
     $form->appendChild(
       id(new AphrontFormMarkupControl())
         ->setLabel(pht('Quick Create'))
         ->setValue($compose_form));
 
     $default_button = javelin_tag(
       'button',
       array(
         'class' => 'grey',
       ),
       pht('Use Project Icon'));
 
     $default_input = javelin_tag(
       'input',
       array(
         'type' => 'hidden',
         'name' => 'projectPHID',
         'value' => $project->getPHID(),
       ));
 
     $default_form = phabricator_form(
       $viewer,
       array(
         'class' => 'profile-image-form',
         'method' => 'POST',
         'action' => '/file/compose/',
        ),
       array(
         $default_input,
         $default_button,
       ));
 
     $form->appendChild(
       id(new AphrontFormMarkupControl())
         ->setLabel(pht('Use Default'))
         ->setValue($default_form));
 
     $upload_form = id(new AphrontFormView())
       ->setUser($viewer)
       ->setEncType('multipart/form-data')
       ->appendChild(
         id(new AphrontFormFileControl())
           ->setName('picture')
           ->setLabel(pht('Upload Picture'))
           ->setError($e_file)
           ->setCaption(
             pht('Supported formats: %s', implode(', ', $supported_formats))))
       ->appendChild(
         id(new AphrontFormSubmitControl())
           ->addCancelButton($edit_uri)
           ->setValue(pht('Upload Picture')));
 
     $form_box = id(new PHUIObjectBoxView())
       ->setHeaderText($title)
       ->setFormErrors($errors)
       ->setForm($form);
 
     $upload_box = id(new PHUIObjectBoxView())
       ->setHeaderText(pht('Upload New Picture'))
       ->setForm($upload_form);
 
     $nav = $this->buildIconNavView($project);
     $nav->selectFilter("edit/{$id}/");
     $nav->appendChild($form_box);
     $nav->appendChild($upload_box);
 
     return $this->buildApplicationPage(
       $nav,
       array(
         'title' => $title,
       ));
   }
 }
diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php
index 9679ddab5c..cb8fe9e87f 100644
--- a/src/applications/project/controller/PhabricatorProjectProfileController.php
+++ b/src/applications/project/controller/PhabricatorProjectProfileController.php
@@ -1,221 +1,222 @@
 <?php
 
 final class PhabricatorProjectProfileController
   extends PhabricatorProjectController {
 
   public function shouldAllowPublic() {
     return true;
   }
 
   public function handleRequest(AphrontRequest $request) {
     $user = $request->getUser();
 
     $query = id(new PhabricatorProjectQuery())
       ->setViewer($user)
       ->needMembers(true)
       ->needWatchers(true)
       ->needImages(true)
       ->needSlugs(true);
     $id = $request->getURIData('id');
     $slug = $request->getURIData('slug');
     if ($slug) {
       $query->withSlugs(array($slug));
     } else {
       $query->withIDs(array($id));
     }
     $project = $query->executeOne();
     if (!$project) {
       return new Aphront404Response();
     }
     if ($slug && $slug != $project->getPrimarySlug()) {
       return id(new AphrontRedirectResponse())
         ->setURI('/tag/'.$project->getPrimarySlug().'/');
     }
 
     $picture = $project->getProfileImageURI();
 
     $header = id(new PHUIHeaderView())
       ->setHeader($project->getName())
       ->setUser($user)
       ->setPolicyObject($project)
       ->setImage($picture);
 
     if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ACTIVE) {
       $header->setStatus('fa-check', 'bluegrey', pht('Active'));
     } else {
       $header->setStatus('fa-ban', 'red', pht('Archived'));
     }
 
     $actions = $this->buildActionListView($project);
     $properties = $this->buildPropertyListView($project, $actions);
 
     $object_box = id(new PHUIObjectBoxView())
       ->setHeader($header)
       ->addPropertyList($properties);
 
     $timeline = $this->buildTransactionTimeline(
       $project,
       new PhabricatorProjectTransactionQuery());
     $timeline->setShouldTerminate(true);
 
     $nav = $this->buildIconNavView($project);
     $nav->selectFilter("profile/{$id}/");
     $nav->appendChild($object_box);
     $nav->appendChild($timeline);
 
     return $this->buildApplicationPage(
       $nav,
       array(
         'title' => $project->getName(),
+        'pageObjects' => array($project->getPHID()),
       ));
   }
 
   private function buildActionListView(PhabricatorProject $project) {
     $request = $this->getRequest();
     $viewer = $request->getUser();
 
     $id = $project->getID();
 
     $view = id(new PhabricatorActionListView())
       ->setUser($viewer)
       ->setObject($project)
       ->setObjectURI($request->getRequestURI());
 
     $can_edit = PhabricatorPolicyFilter::hasCapability(
       $viewer,
       $project,
       PhabricatorPolicyCapability::CAN_EDIT);
 
     $view->addAction(
       id(new PhabricatorActionView())
         ->setName(pht('Edit Details'))
         ->setIcon('fa-pencil')
         ->setHref($this->getApplicationURI("details/{$id}/")));
 
     $view->addAction(
       id(new PhabricatorActionView())
         ->setName(pht('Edit Picture'))
         ->setIcon('fa-picture-o')
         ->setHref($this->getApplicationURI("picture/{$id}/"))
         ->setDisabled(!$can_edit)
         ->setWorkflow(!$can_edit));
 
     if ($project->isArchived()) {
       $view->addAction(
         id(new PhabricatorActionView())
           ->setName(pht('Activate Project'))
           ->setIcon('fa-check')
           ->setHref($this->getApplicationURI("archive/{$id}/"))
           ->setDisabled(!$can_edit)
           ->setWorkflow(true));
     } else {
       $view->addAction(
         id(new PhabricatorActionView())
           ->setName(pht('Archive Project'))
           ->setIcon('fa-ban')
           ->setHref($this->getApplicationURI("archive/{$id}/"))
           ->setDisabled(!$can_edit)
           ->setWorkflow(true));
     }
 
     $action = null;
     if (!$project->isUserMember($viewer->getPHID())) {
       $can_join = PhabricatorPolicyFilter::hasCapability(
         $viewer,
         $project,
         PhabricatorPolicyCapability::CAN_JOIN);
 
       $action = id(new PhabricatorActionView())
         ->setUser($viewer)
         ->setRenderAsForm(true)
         ->setHref('/project/update/'.$project->getID().'/join/')
         ->setIcon('fa-plus')
         ->setDisabled(!$can_join)
         ->setName(pht('Join Project'));
       $view->addAction($action);
     } else {
       $action = id(new PhabricatorActionView())
         ->setWorkflow(true)
         ->setHref('/project/update/'.$project->getID().'/leave/')
         ->setIcon('fa-times')
         ->setName(pht('Leave Project...'));
       $view->addAction($action);
 
       if (!$project->isUserWatcher($viewer->getPHID())) {
         $action = id(new PhabricatorActionView())
           ->setWorkflow(true)
           ->setHref('/project/watch/'.$project->getID().'/')
           ->setIcon('fa-eye')
           ->setName(pht('Watch Project'));
         $view->addAction($action);
       } else {
         $action = id(new PhabricatorActionView())
           ->setWorkflow(true)
           ->setHref('/project/unwatch/'.$project->getID().'/')
           ->setIcon('fa-eye-slash')
           ->setName(pht('Unwatch Project'));
         $view->addAction($action);
       }
     }
 
     return $view;
   }
 
   private function buildPropertyListView(
     PhabricatorProject $project,
     PhabricatorActionListView $actions) {
     $request = $this->getRequest();
     $viewer = $request->getUser();
 
     $view = id(new PHUIPropertyListView())
       ->setUser($viewer)
       ->setObject($project)
       ->setActionList($actions);
 
     $hashtags = array();
     foreach ($project->getSlugs() as $slug) {
       $hashtags[] = id(new PHUITagView())
         ->setType(PHUITagView::TYPE_OBJECT)
         ->setName('#'.$slug->getSlug());
     }
 
     $view->addProperty(pht('Hashtags'), phutil_implode_html(' ', $hashtags));
 
     $view->addProperty(
       pht('Members'),
       $project->getMemberPHIDs()
         ? $viewer
           ->renderHandleList($project->getMemberPHIDs())
           ->setAsInline(true)
         : phutil_tag('em', array(), pht('None')));
 
     $view->addProperty(
       pht('Watchers'),
       $project->getWatcherPHIDs()
         ? $viewer
           ->renderHandleList($project->getWatcherPHIDs())
           ->setAsInline(true)
         : phutil_tag('em', array(), pht('None')));
 
     $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions(
       $viewer,
       $project);
 
     $view->addProperty(
       pht('Looks Like'),
       $viewer->renderHandle($project->getPHID())->setAsTag(true));
 
     $view->addProperty(
       pht('Joinable By'),
       $descriptions[PhabricatorPolicyCapability::CAN_JOIN]);
 
     $field_list = PhabricatorCustomField::getObjectFields(
       $project,
       PhabricatorCustomField::ROLE_VIEW);
     $field_list->appendFieldsToPropertyList($project, $viewer, $view);
 
     return $view;
   }
 
 
 }
diff --git a/src/applications/project/icon/PhabricatorProjectIcon.php b/src/applications/project/icon/PhabricatorProjectIcon.php
index ae6efa9b6e..9411baa6a9 100644
--- a/src/applications/project/icon/PhabricatorProjectIcon.php
+++ b/src/applications/project/icon/PhabricatorProjectIcon.php
@@ -1,59 +1,59 @@
 <?php
 
 final class PhabricatorProjectIcon extends Phobject {
 
   public static function getIconMap() {
     return
       array(
         'fa-briefcase' => pht('Briefcase'),
         'fa-tags' => pht('Tag'),
         'fa-folder' => pht('Folder'),
         'fa-users' => pht('Team'),
         'fa-bug' => pht('Bug'),
         'fa-trash-o' => pht('Garbage'),
         'fa-calendar' => pht('Deadline'),
         'fa-flag-checkered' => pht('Goal'),
         'fa-envelope' => pht('Communication'),
         'fa-truck' => pht('Release'),
         'fa-lock' => pht('Policy'),
         'fa-umbrella' => pht('An Umbrella'),
         'fa-cloud' => pht('The Cloud'),
         'fa-building' => pht('Company'),
         'fa-credit-card' => pht('Accounting'),
         'fa-flask' => pht('Experimental'),
       );
   }
 
   public static function getColorMap() {
     $shades = PHUITagView::getShadeMap();
     $shades = array_select_keys(
       $shades,
       array(PhabricatorProject::DEFAULT_COLOR)) + $shades;
     unset($shades[PHUITagView::COLOR_DISABLED]);
 
     return $shades;
   }
 
   public static function getLabel($key) {
     $map = self::getIconMap();
     return $map[$key];
   }
 
   public static function getAPIName($key) {
     return substr($key, 3);
   }
 
   public static function renderIconForChooser($icon) {
-    $project_icons = PhabricatorProjectIcon::getIconMap();
+    $project_icons = self::getIconMap();
 
     return phutil_tag(
       'span',
       array(),
       array(
         id(new PHUIIconView())->setIconFont($icon),
         ' ',
         idx($project_icons, $icon, pht('Unknown Icon')),
       ));
   }
 
 }
diff --git a/src/applications/project/storage/PhabricatorProjectColumn.php b/src/applications/project/storage/PhabricatorProjectColumn.php
index 8fcb6ade9c..63b8a8748f 100644
--- a/src/applications/project/storage/PhabricatorProjectColumn.php
+++ b/src/applications/project/storage/PhabricatorProjectColumn.php
@@ -1,197 +1,197 @@
 <?php
 
 final class PhabricatorProjectColumn
   extends PhabricatorProjectDAO
   implements
     PhabricatorApplicationTransactionInterface,
     PhabricatorPolicyInterface,
     PhabricatorDestructibleInterface {
 
   const STATUS_ACTIVE = 0;
   const STATUS_HIDDEN = 1;
 
   const DEFAULT_ORDER = 'natural';
   const ORDER_NATURAL = 'natural';
   const ORDER_PRIORITY = 'priority';
 
   protected $name;
   protected $status;
   protected $projectPHID;
   protected $sequence;
   protected $properties = array();
 
   private $project = self::ATTACHABLE;
 
   public static function initializeNewColumn(PhabricatorUser $user) {
     return id(new PhabricatorProjectColumn())
       ->setName('')
       ->setStatus(self::STATUS_ACTIVE);
   }
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_SERIALIZATION => array(
         'properties' => self::SERIALIZATION_JSON,
       ),
       self::CONFIG_COLUMN_SCHEMA => array(
         'name' => 'text255',
         'status' => 'uint32',
         'sequence' => 'uint32',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_status' => array(
           'columns' => array('projectPHID', 'status', 'sequence'),
         ),
         'key_sequence' => array(
           'columns' => array('projectPHID', 'sequence'),
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       PhabricatorProjectColumnPHIDType::TYPECONST);
   }
 
   public function attachProject(PhabricatorProject $project) {
     $this->project = $project;
     return $this;
   }
 
   public function getProject() {
     return $this->assertAttached($this->project);
   }
 
   public function isDefaultColumn() {
     return (bool)$this->getProperty('isDefault');
   }
 
   public function isHidden() {
     return ($this->getStatus() == self::STATUS_HIDDEN);
   }
 
   public function getDisplayName() {
     $name = $this->getName();
     if (strlen($name)) {
       return $name;
     }
 
     if ($this->isDefaultColumn()) {
       return pht('Backlog');
     }
 
     return pht('Unnamed Column');
   }
 
   public function getDisplayType() {
     if ($this->isDefaultColumn()) {
       return pht('(Default)');
     }
     if ($this->isHidden()) {
       return pht('(Hidden)');
     }
 
     return null;
   }
 
   public function getHeaderIcon() {
     $icon = null;
 
     if ($this->isHidden()) {
       $icon = 'fa-eye-slash';
       $text = pht('Hidden');
     }
 
     if ($icon) {
       return id(new PHUIIconView())
         ->setIconFont($icon)
         ->addSigil('has-tooltip')
         ->setMetadata(
           array(
             'tip' => $text,
-          ));;
+          ));
     }
 
     return null;
   }
 
   public function getProperty($key, $default = null) {
     return idx($this->properties, $key, $default);
   }
 
   public function setProperty($key, $value) {
     $this->properties[$key] = $value;
     return $this;
   }
 
   public function getPointLimit() {
     return $this->getProperty('pointLimit');
   }
 
   public function setPointLimit($limit) {
     $this->setProperty('pointLimit', $limit);
     return $this;
   }
 
 
 /* -(  PhabricatorApplicationTransactionInterface  )------------------------- */
 
 
   public function getApplicationTransactionEditor() {
     return new PhabricatorProjectColumnTransactionEditor();
   }
 
   public function getApplicationTransactionObject() {
     return $this;
   }
 
   public function getApplicationTransactionTemplate() {
     return new PhabricatorProjectColumnTransaction();
   }
 
   public function willRenderTimeline(
     PhabricatorApplicationTransactionView $timeline,
     AphrontRequest $request) {
 
     return $timeline;
   }
 
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
       PhabricatorPolicyCapability::CAN_EDIT,
     );
   }
 
   public function getPolicy($capability) {
     return $this->getProject()->getPolicy($capability);
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     return $this->getProject()->hasAutomaticCapability(
       $capability,
       $viewer);
   }
 
   public function describeAutomaticCapability($capability) {
     return pht('Users must be able to see a project to see its board.');
   }
 
 
 /* -(  PhabricatorDestructibleInterface  )----------------------------------- */
 
   public function destroyObjectPermanently(
     PhabricatorDestructionEngine $engine) {
 
     $this->openTransaction();
     $this->delete();
     $this->saveTransaction();
   }
 
 }
diff --git a/src/applications/project/storage/PhabricatorProjectColumnTransaction.php b/src/applications/project/storage/PhabricatorProjectColumnTransaction.php
index dd4f95d5f6..ed4bfed8a6 100644
--- a/src/applications/project/storage/PhabricatorProjectColumnTransaction.php
+++ b/src/applications/project/storage/PhabricatorProjectColumnTransaction.php
@@ -1,82 +1,82 @@
 <?php
 
 final class PhabricatorProjectColumnTransaction
   extends PhabricatorApplicationTransaction {
 
   const TYPE_NAME       = 'project:col:name';
   const TYPE_STATUS     = 'project:col:status';
   const TYPE_LIMIT      = 'project:col:limit';
 
   public function getApplicationName() {
     return 'project';
   }
 
   public function getApplicationTransactionType() {
     return PhabricatorProjectColumnPHIDType::TYPECONST;
   }
 
   public function getTitle() {
     $old = $this->getOldValue();
     $new = $this->getNewValue();
     $author_handle = $this->renderHandleLink($this->getAuthorPHID());
 
     switch ($this->getTransactionType()) {
-      case PhabricatorProjectColumnTransaction::TYPE_NAME:
+      case self::TYPE_NAME:
         if ($old === null) {
           return pht(
             '%s created this column.',
             $author_handle);
         } else {
           if (!strlen($old)) {
             return pht(
               '%s named this column "%s".',
               $author_handle,
               $new);
           } else if (strlen($new)) {
             return pht(
               '%s renamed this column from "%s" to "%s".',
               $author_handle,
               $old,
               $new);
           } else {
             return pht(
               '%s removed the custom name of this column.',
               $author_handle);
           }
         }
-      case PhabricatorProjectColumnTransaction::TYPE_LIMIT:
+      case self::TYPE_LIMIT:
         if (!$old) {
           return pht(
             '%s set the point limit for this column to %s.',
             $author_handle,
             $new);
         } else if (!$new) {
           return pht(
             '%s removed the point limit for this column.',
             $author_handle);
         } else {
           return pht(
             '%s changed point limit for this column from %s to %s.',
             $author_handle,
             $old,
             $new);
         }
 
-      case PhabricatorProjectColumnTransaction::TYPE_STATUS:
+      case self::TYPE_STATUS:
         switch ($new) {
           case PhabricatorProjectColumn::STATUS_ACTIVE:
             return pht(
               '%s marked this column visible.',
               $author_handle);
           case PhabricatorProjectColumn::STATUS_HIDDEN:
             return pht(
               '%s marked this column hidden.',
               $author_handle);
         }
         break;
     }
 
     return parent::getTitle();
   }
 
 }
diff --git a/src/applications/project/storage/PhabricatorProjectTransaction.php b/src/applications/project/storage/PhabricatorProjectTransaction.php
index 9b0ff0c340..3dceb0934c 100644
--- a/src/applications/project/storage/PhabricatorProjectTransaction.php
+++ b/src/applications/project/storage/PhabricatorProjectTransaction.php
@@ -1,352 +1,352 @@
 <?php
 
 final class PhabricatorProjectTransaction
   extends PhabricatorApplicationTransaction {
 
   const TYPE_NAME       = 'project:name';
   const TYPE_SLUGS      = 'project:slugs';
   const TYPE_STATUS     = 'project:status';
   const TYPE_IMAGE      = 'project:image';
   const TYPE_ICON       = 'project:icon';
   const TYPE_COLOR      = 'project:color';
   const TYPE_LOCKED     = 'project:locked';
 
   // NOTE: This is deprecated, members are just a normal edge now.
   const TYPE_MEMBERS    = 'project:members';
 
   public function getApplicationName() {
     return 'project';
   }
 
   public function getApplicationTransactionType() {
     return PhabricatorProjectProjectPHIDType::TYPECONST;
   }
 
   public function getRequiredHandlePHIDs() {
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     $req_phids = array();
     switch ($this->getTransactionType()) {
-      case PhabricatorProjectTransaction::TYPE_MEMBERS:
+      case self::TYPE_MEMBERS:
         $add = array_diff($new, $old);
         $rem = array_diff($old, $new);
         $req_phids = array_merge($add, $rem);
         break;
-      case PhabricatorProjectTransaction::TYPE_IMAGE:
+      case self::TYPE_IMAGE:
         $req_phids[] = $old;
         $req_phids[] = $new;
         break;
     }
 
     return array_merge($req_phids, parent::getRequiredHandlePHIDs());
   }
 
   public function getColor() {
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
-      case PhabricatorProjectTransaction::TYPE_STATUS:
+      case self::TYPE_STATUS:
         if ($old == 0) {
           return 'red';
         } else {
           return 'green';
         }
       }
     return parent::getColor();
   }
 
   public function getIcon() {
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
-      case PhabricatorProjectTransaction::TYPE_STATUS:
+      case self::TYPE_STATUS:
         if ($old == 0) {
           return 'fa-ban';
         } else {
           return 'fa-check';
         }
-      case PhabricatorProjectTransaction::TYPE_LOCKED:
+      case self::TYPE_LOCKED:
         if ($new) {
           return 'fa-lock';
         } else {
           return 'fa-unlock';
         }
-      case PhabricatorProjectTransaction::TYPE_ICON:
+      case self::TYPE_ICON:
         return $new;
-      case PhabricatorProjectTransaction::TYPE_IMAGE:
+      case self::TYPE_IMAGE:
         return 'fa-photo';
-      case PhabricatorProjectTransaction::TYPE_MEMBERS:
+      case self::TYPE_MEMBERS:
         return 'fa-user';
-      case PhabricatorProjectTransaction::TYPE_SLUGS:
+      case self::TYPE_SLUGS:
         return 'fa-tag';
     }
     return parent::getIcon();
   }
 
   public function getTitle() {
     $old = $this->getOldValue();
     $new = $this->getNewValue();
     $author_handle = $this->renderHandleLink($this->getAuthorPHID());
 
     switch ($this->getTransactionType()) {
-      case PhabricatorProjectTransaction::TYPE_NAME:
+      case self::TYPE_NAME:
         if ($old === null) {
           return pht(
             '%s created this project.',
             $author_handle);
         } else {
           return pht(
             '%s renamed this project from "%s" to "%s".',
             $author_handle,
             $old,
             $new);
         }
-      case PhabricatorProjectTransaction::TYPE_STATUS:
+      case self::TYPE_STATUS:
         if ($old == 0) {
           return pht(
             '%s archived this project.',
             $author_handle);
         } else {
           return pht(
             '%s activated this project.',
             $author_handle);
         }
-      case PhabricatorProjectTransaction::TYPE_IMAGE:
+      case self::TYPE_IMAGE:
         // TODO: Some day, it would be nice to show the images.
         if (!$old) {
           return pht(
             '%s set this project\'s image to %s.',
             $author_handle,
             $this->renderHandleLink($new));
         } else if (!$new) {
           return pht(
             '%s removed this project\'s image.',
             $author_handle);
         } else {
           return pht(
             '%s updated this project\'s image from %s to %s.',
             $author_handle,
             $this->renderHandleLink($old),
             $this->renderHandleLink($new));
         }
 
-      case PhabricatorProjectTransaction::TYPE_ICON:
+      case self::TYPE_ICON:
         return pht(
           '%s set this project\'s icon to %s.',
           $author_handle,
           PhabricatorProjectIcon::getLabel($new));
 
-      case PhabricatorProjectTransaction::TYPE_COLOR:
+      case self::TYPE_COLOR:
         return pht(
           '%s set this project\'s color to %s.',
           $author_handle,
           PHUITagView::getShadeName($new));
 
-      case PhabricatorProjectTransaction::TYPE_LOCKED:
+      case self::TYPE_LOCKED:
         if ($new) {
           return pht(
             '%s locked this project\'s membership.',
             $author_handle);
         } else {
           return pht(
             '%s unlocked this project\'s membership.',
             $author_handle);
         }
 
-      case PhabricatorProjectTransaction::TYPE_SLUGS:
+      case self::TYPE_SLUGS:
         $add = array_diff($new, $old);
         $rem = array_diff($old, $new);
 
         if ($add && $rem) {
           return pht(
             '%s changed project hashtag(s), added %d: %s; removed %d: %s.',
             $author_handle,
             count($add),
             $this->renderSlugList($add),
             count($rem),
             $this->renderSlugList($rem));
         } else if ($add) {
           return pht(
             '%s added %d project hashtag(s): %s.',
             $author_handle,
             count($add),
             $this->renderSlugList($add));
         } else if ($rem) {
             return pht(
               '%s removed %d project hashtag(s): %s.',
               $author_handle,
               count($rem),
               $this->renderSlugList($rem));
         }
 
-      case PhabricatorProjectTransaction::TYPE_MEMBERS:
+      case self::TYPE_MEMBERS:
         $add = array_diff($new, $old);
         $rem = array_diff($old, $new);
 
         if ($add && $rem) {
           return pht(
             '%s changed project member(s), added %d: %s; removed %d: %s.',
             $author_handle,
             count($add),
             $this->renderHandleList($add),
             count($rem),
             $this->renderHandleList($rem));
         } else if ($add) {
           if (count($add) == 1 && (head($add) == $this->getAuthorPHID())) {
             return pht(
               '%s joined this project.',
               $author_handle);
           } else {
             return pht(
               '%s added %d project member(s): %s.',
               $author_handle,
               count($add),
               $this->renderHandleList($add));
           }
         } else if ($rem) {
           if (count($rem) == 1 && (head($rem) == $this->getAuthorPHID())) {
             return pht(
               '%s left this project.',
               $author_handle);
           } else {
             return pht(
               '%s removed %d project member(s): %s.',
               $author_handle,
               count($rem),
               $this->renderHandleList($rem));
           }
         }
     }
 
     return parent::getTitle();
   }
 
   public function getTitleForFeed() {
     $author_phid = $this->getAuthorPHID();
     $object_phid = $this->getObjectPHID();
     $author_handle = $this->renderHandleLink($author_phid);
     $object_handle = $this->renderHandleLink($object_phid);
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
       case self::TYPE_NAME:
         if ($old === null) {
           return pht(
             '%s created %s.',
             $author_handle,
             $object_handle);
         } else {
           return pht(
             '%s renamed %s from "%s" to "%s".',
             $author_handle,
             $object_handle,
             $old,
             $new);
         }
       case self::TYPE_STATUS:
         if ($old == 0) {
           return pht(
             '%s archived %s.',
             $author_handle,
             $object_handle);
         } else {
           return pht(
             '%s activated %s.',
             $author_handle,
             $object_handle);
         }
       case self::TYPE_IMAGE:
         // TODO: Some day, it would be nice to show the images.
         if (!$old) {
           return pht(
             '%s set the image for %s to %s.',
             $author_handle,
             $object_handle,
             $this->renderHandleLink($new));
         } else if (!$new) {
           return pht(
             '%s removed the image for %s.',
             $author_handle,
             $object_handle);
         } else {
           return pht(
             '%s updated the image for %s from %s to %s.',
             $author_handle,
             $object_handle,
             $this->renderHandleLink($old),
             $this->renderHandleLink($new));
         }
 
       case self::TYPE_ICON:
         return pht(
           '%s set the icon for %s to %s.',
           $author_handle,
           $object_handle,
           PhabricatorProjectIcon::getLabel($new));
 
       case self::TYPE_COLOR:
         return pht(
           '%s set the color for %s to %s.',
           $author_handle,
           $object_handle,
           PHUITagView::getShadeName($new));
 
       case self::TYPE_LOCKED:
         if ($new) {
           return pht(
             '%s locked %s membership.',
             $author_handle,
             $object_handle);
         } else {
           return pht(
             '%s unlocked %s membership.',
             $author_handle,
             $object_handle);
         }
 
       case self::TYPE_SLUGS:
         $add = array_diff($new, $old);
         $rem = array_diff($old, $new);
 
         if ($add && $rem) {
           return pht(
             '%s changed %s hashtag(s), added %d: %s; removed %d: %s.',
             $author_handle,
             $object_handle,
             count($add),
             $this->renderSlugList($add),
             count($rem),
             $this->renderSlugList($rem));
         } else if ($add) {
           return pht(
             '%s added %d %s hashtag(s): %s.',
             $author_handle,
             count($add),
             $object_handle,
             $this->renderSlugList($add));
         } else if ($rem) {
           return pht(
             '%s removed %d %s hashtag(s): %s.',
             $author_handle,
             count($rem),
             $object_handle,
             $this->renderSlugList($rem));
         }
 
     }
 
     return parent::getTitleForFeed();
   }
 
   private function renderSlugList($slugs) {
     return implode(', ', $slugs);
   }
 
 }
diff --git a/src/applications/releeph/storage/ReleephRequest.php b/src/applications/releeph/storage/ReleephRequest.php
index 7f3f190a6d..7f2487107a 100644
--- a/src/applications/releeph/storage/ReleephRequest.php
+++ b/src/applications/releeph/storage/ReleephRequest.php
@@ -1,373 +1,373 @@
 <?php
 
 final class ReleephRequest extends ReleephDAO
   implements
     PhabricatorApplicationTransactionInterface,
     PhabricatorPolicyInterface,
     PhabricatorCustomFieldInterface {
 
   protected $branchID;
   protected $requestUserPHID;
   protected $details = array();
   protected $userIntents = array();
   protected $inBranch;
   protected $pickStatus;
   protected $mailKey;
 
   /**
    * The object which is being requested. Normally this is a commit, but it
    * might also be a revision. In the future, it could be a repository branch
    * or an external object (like a GitHub pull request).
    */
   protected $requestedObjectPHID;
 
   // Information about the thing being requested
   protected $requestCommitPHID;
 
   // Information about the last commit to the releeph branch
   protected $commitIdentifier;
   protected $commitPHID;
 
 
   private $customFields = self::ATTACHABLE;
   private $branch = self::ATTACHABLE;
   private $requestedObject = self::ATTACHABLE;
 
 
 /* -(  Constants and helper methods  )--------------------------------------- */
 
   const INTENT_WANT = 'want';
   const INTENT_PASS = 'pass';
 
   const PICK_PENDING  = 1; // old
   const PICK_FAILED   = 2;
   const PICK_OK       = 3;
   const PICK_MANUAL   = 4; // old
   const REVERT_OK     = 5;
   const REVERT_FAILED = 6;
 
   public function shouldBeInBranch() {
     return
       $this->getPusherIntent() == self::INTENT_WANT &&
       /**
        * We use "!= pass" instead of "== want" in case the requestor intent is
        * not present. In other words, only revert if the requestor explicitly
        * passed.
        */
       $this->getRequestorIntent() != self::INTENT_PASS;
   }
 
   /**
    * Will return INTENT_WANT if any pusher wants this request, and no pusher
    * passes on this request.
    */
   public function getPusherIntent() {
     $product = $this->getBranch()->getProduct();
 
     if (!$product->getPushers()) {
       return self::INTENT_WANT;
     }
 
     $found_pusher_want = false;
     foreach ($this->userIntents as $phid => $intent) {
       if ($product->isAuthoritativePHID($phid)) {
         if ($intent == self::INTENT_PASS) {
           return self::INTENT_PASS;
         }
 
         $found_pusher_want = true;
       }
     }
 
     if ($found_pusher_want) {
       return self::INTENT_WANT;
     } else {
       return null;
     }
   }
 
   public function getRequestorIntent() {
     return idx($this->userIntents, $this->requestUserPHID);
   }
 
   public function getStatus() {
     return $this->calculateStatus();
   }
 
   public function getMonogram() {
     return 'Y'.$this->getID();
   }
 
   public function getBranch() {
     return $this->assertAttached($this->branch);
   }
 
   public function attachBranch(ReleephBranch $branch) {
     $this->branch = $branch;
     return $this;
   }
 
   public function getRequestedObject() {
     return $this->assertAttached($this->requestedObject);
   }
 
   public function attachRequestedObject($object) {
     $this->requestedObject = $object;
     return $this;
   }
 
   private function calculateStatus() {
     if ($this->shouldBeInBranch()) {
       if ($this->getInBranch()) {
         return ReleephRequestStatus::STATUS_PICKED;
       } else {
         return ReleephRequestStatus::STATUS_NEEDS_PICK;
       }
     } else {
       if ($this->getInBranch()) {
         return ReleephRequestStatus::STATUS_NEEDS_REVERT;
       } else {
-        $intent_pass = ReleephRequest::INTENT_PASS;
-        $intent_want = ReleephRequest::INTENT_WANT;
+        $intent_pass = self::INTENT_PASS;
+        $intent_want = self::INTENT_WANT;
 
         $has_been_in_branch = $this->getCommitIdentifier();
         // Regardless of why we reverted something, always say reverted if it
         // was once in the branch.
         if ($has_been_in_branch) {
           return ReleephRequestStatus::STATUS_REVERTED;
         } else if ($this->getPusherIntent() === $intent_pass) {
           // Otherwise, if it has never been in the branch, explicitly say why:
           return ReleephRequestStatus::STATUS_REJECTED;
         } else if ($this->getRequestorIntent() === $intent_want) {
           return ReleephRequestStatus::STATUS_REQUESTED;
         } else {
           return ReleephRequestStatus::STATUS_ABANDONED;
         }
       }
     }
   }
 
 
 /* -(  Lisk mechanics  )----------------------------------------------------- */
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_SERIALIZATION => array(
         'details' => self::SERIALIZATION_JSON,
         'userIntents' => self::SERIALIZATION_JSON,
       ),
       self::CONFIG_COLUMN_SCHEMA => array(
         'requestCommitPHID' => 'phid?',
         'commitIdentifier' => 'text40?',
         'commitPHID' => 'phid?',
         'pickStatus' => 'uint32?',
         'inBranch' => 'bool',
         'mailKey' => 'bytes20',
         'userIntents' => 'text?',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_phid' => null,
         'phid' => array(
           'columns' => array('phid'),
           'unique' => true,
         ),
         'requestIdentifierBranch' => array(
           'columns' => array('requestCommitPHID', 'branchID'),
           'unique' => true,
         ),
         'branchID' => array(
           'columns' => array('branchID'),
         ),
         'key_requestedObject' => array(
           'columns' => array('requestedObjectPHID'),
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       ReleephRequestPHIDType::TYPECONST);
   }
 
   public function save() {
     if (!$this->getMailKey()) {
       $this->setMailKey(Filesystem::readRandomCharacters(20));
     }
     return parent::save();
   }
 
 
 /* -(  Helpful accessors )--------------------------------------------------- */
 
 
   public function getDetail($key, $default = null) {
     return idx($this->getDetails(), $key, $default);
   }
 
   public function setDetail($key, $value) {
     $this->details[$key] = $value;
     return $this;
   }
 
 
   /**
    * Get the commit PHIDs this request is requesting.
    *
    * NOTE: For now, this always returns one PHID.
    *
    * @return list<phid> Commit PHIDs requested by this request.
    */
   public function getCommitPHIDs() {
     return array(
       $this->requestCommitPHID,
     );
   }
 
   public function getReason() {
     // Backward compatibility: reason used to be called comments
     $reason = $this->getDetail('reason');
     if (!$reason) {
       return $this->getDetail('comments');
     }
     return $reason;
   }
 
   /**
    * Allow a null summary, and fall back to the title of the commit.
    */
   public function getSummaryForDisplay() {
     $summary = $this->getDetail('summary');
 
     if (!strlen($summary)) {
       $commit = $this->loadPhabricatorRepositoryCommit();
       if ($commit) {
         $summary = $commit->getSummary();
       }
     }
 
     if (!strlen($summary)) {
       $summary = pht('None');
     }
 
     return $summary;
   }
 
 /* -(  Loading external objects  )------------------------------------------- */
 
   public function loadPhabricatorRepositoryCommit() {
     return $this->loadOneRelative(
       new PhabricatorRepositoryCommit(),
       'phid',
       'getRequestCommitPHID');
   }
 
   public function loadPhabricatorRepositoryCommitData() {
     $commit = $this->loadPhabricatorRepositoryCommit();
     if ($commit) {
       return $commit->loadOneRelative(
         new PhabricatorRepositoryCommitData(),
         'commitID');
     }
   }
 
 
 /* -(  State change helpers  )----------------------------------------------- */
 
   public function setUserIntent(PhabricatorUser $user, $intent) {
     $this->userIntents[$user->getPHID()] = $intent;
     return $this;
   }
 
 
 /* -(  Migrating to status-less ReleephRequests  )--------------------------- */
 
   protected function didReadData() {
     if ($this->userIntents === null) {
       $this->userIntents = array();
     }
   }
 
   public function setStatus($value) {
     throw new Exception('`status` is now deprecated!');
   }
 
 /* -(  Make magic Lisk methods private  )------------------------------------ */
 
   private function setUserIntents(array $ar) {
     return parent::setUserIntents($ar);
   }
 
 
 /* -(  PhabricatorApplicationTransactionInterface  )------------------------- */
 
 
   public function getApplicationTransactionEditor() {
     return new ReleephRequestTransactionalEditor();
   }
 
   public function getApplicationTransactionObject() {
     return $this;
   }
 
   public function getApplicationTransactionTemplate() {
     return new ReleephRequestTransaction();
   }
 
   public function willRenderTimeline(
     PhabricatorApplicationTransactionView $timeline,
     AphrontRequest $request) {
 
     return $timeline;
   }
 
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
       PhabricatorPolicyCapability::CAN_EDIT,
     );
   }
 
   public function getPolicy($capability) {
     return $this->getBranch()->getPolicy($capability);
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     return $this->getBranch()->hasAutomaticCapability($capability, $viewer);
   }
 
   public function describeAutomaticCapability($capability) {
     return pht(
       'Pull requests have the same policies as the branches they are '.
       'requested against.');
   }
 
 
 
 /* -(  PhabricatorCustomFieldInterface  )------------------------------------ */
 
 
   public function getCustomFieldSpecificationForRole($role) {
     return PhabricatorEnv::getEnvConfig('releeph.fields');
   }
 
   public function getCustomFieldBaseClass() {
     return 'ReleephFieldSpecification';
   }
 
   public function getCustomFields() {
     return $this->assertAttached($this->customFields);
   }
 
   public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
     $this->customFields = $fields;
     return $this;
   }
 
 
 }
diff --git a/src/applications/releeph/storage/ReleephRequestTransaction.php b/src/applications/releeph/storage/ReleephRequestTransaction.php
index bd17ad43c9..f4a4720c78 100644
--- a/src/applications/releeph/storage/ReleephRequestTransaction.php
+++ b/src/applications/releeph/storage/ReleephRequestTransaction.php
@@ -1,279 +1,279 @@
 <?php
 
 final class ReleephRequestTransaction
   extends PhabricatorApplicationTransaction {
 
   const TYPE_REQUEST          = 'releeph:request';
   const TYPE_USER_INTENT      = 'releeph:user_intent';
   const TYPE_EDIT_FIELD       = 'releeph:edit_field';
   const TYPE_PICK_STATUS      = 'releeph:pick_status';
   const TYPE_COMMIT           = 'releeph:commit';
   const TYPE_DISCOVERY        = 'releeph:discovery';
   const TYPE_MANUAL_IN_BRANCH = 'releeph:manual';
 
   public function getApplicationName() {
     return 'releeph';
   }
 
   public function getApplicationTransactionType() {
     return ReleephRequestPHIDType::TYPECONST;
   }
 
   public function getApplicationTransactionCommentObject() {
     return new ReleephRequestTransactionComment();
   }
 
   public function hasChangeDetails() {
     switch ($this->getTransactionType()) {
       default;
         break;
     }
     return parent::hasChangeDetails();
   }
 
   public function getRequiredHandlePHIDs() {
     $phids = parent::getRequiredHandlePHIDs();
     $phids[] = $this->getObjectPHID();
 
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
-      case ReleephRequestTransaction::TYPE_REQUEST:
-      case ReleephRequestTransaction::TYPE_DISCOVERY:
+      case self::TYPE_REQUEST:
+      case self::TYPE_DISCOVERY:
         $phids[] = $new;
         break;
 
-      case ReleephRequestTransaction::TYPE_EDIT_FIELD:
+      case self::TYPE_EDIT_FIELD:
         self::searchForPHIDs($this->getOldValue(), $phids);
         self::searchForPHIDs($this->getNewValue(), $phids);
         break;
     }
 
     return $phids;
   }
 
   public function getTitle() {
     $author_phid = $this->getAuthorPHID();
     $object_phid = $this->getObjectPHID();
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
-      case ReleephRequestTransaction::TYPE_REQUEST:
+      case self::TYPE_REQUEST:
         return pht(
           '%s requested %s',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($new));
         break;
 
-      case ReleephRequestTransaction::TYPE_USER_INTENT:
+      case self::TYPE_USER_INTENT:
         return $this->getIntentTitle();
         break;
 
-      case ReleephRequestTransaction::TYPE_EDIT_FIELD:
+      case self::TYPE_EDIT_FIELD:
         $field = newv($this->getMetadataValue('fieldClass'), array());
         $name = $field->getName();
 
         $markup = $name;
         if ($this->getRenderingTarget() ===
           PhabricatorApplicationTransaction::TARGET_HTML) {
 
           $markup = hsprintf('<em>%s</em>', $name);
         }
 
         return pht(
           '%s changed the %s to "%s"',
           $this->renderHandleLink($author_phid),
           $markup,
           $field->normalizeForTransactionView($this, $new));
         break;
 
-      case ReleephRequestTransaction::TYPE_PICK_STATUS:
+      case self::TYPE_PICK_STATUS:
         switch ($new) {
           case ReleephRequest::PICK_OK:
             return pht('%s found this request picks without error',
               $this->renderHandleLink($author_phid));
 
           case ReleephRequest::REVERT_OK:
             return pht('%s found this request reverts without error',
               $this->renderHandleLink($author_phid));
 
           case ReleephRequest::PICK_FAILED:
             return pht("%s couldn't pick this request",
               $this->renderHandleLink($author_phid));
 
           case ReleephRequest::REVERT_FAILED:
             return pht("%s couldn't revert this request",
               $this->renderHandleLink($author_phid));
         }
         break;
 
-      case ReleephRequestTransaction::TYPE_COMMIT:
+      case self::TYPE_COMMIT:
         $action_type = $this->getMetadataValue('action');
         switch ($action_type) {
           case 'pick':
             return pht(
               '%s picked this request and committed the result upstream',
               $this->renderHandleLink($author_phid));
             break;
 
           case 'revert':
             return pht(
               '%s reverted this request and committed the result upstream',
               $this->renderHandleLink($author_phid));
             break;
         }
         break;
 
-      case ReleephRequestTransaction::TYPE_MANUAL_IN_BRANCH:
+      case self::TYPE_MANUAL_IN_BRANCH:
         $action = $new ? pht('picked') : pht('reverted');
         return pht(
           '%s marked this request as manually %s',
           $this->renderHandleLink($author_phid),
           $action);
         break;
 
-      case ReleephRequestTransaction::TYPE_DISCOVERY:
+      case self::TYPE_DISCOVERY:
         return pht('%s discovered this commit as %s',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($new));
         break;
 
       default:
         return parent::getTitle();
         break;
     }
   }
 
   public function getActionStrength() {
     return parent::getActionStrength();
   }
 
   public function getActionName() {
     switch ($this->getTransactionType()) {
       case self::TYPE_REQUEST:
         return pht('Requested');
 
       case self::TYPE_COMMIT:
         $action_type = $this->getMetadataValue('action');
         switch ($action_type) {
           case 'pick':
             return pht('Picked');
 
           case 'revert':
             return pht('Reverted');
         }
     }
 
     return parent::getActionName();
   }
 
   public function getColor() {
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
-      case ReleephRequestTransaction::TYPE_USER_INTENT:
+      case self::TYPE_USER_INTENT:
         switch ($new) {
           case ReleephRequest::INTENT_WANT:
             return PhabricatorTransactions::COLOR_GREEN;
           case ReleephRequest::INTENT_PASS:
             return PhabricatorTransactions::COLOR_RED;
         }
     }
     return parent::getColor();
   }
 
   private static function searchForPHIDs($thing, array &$phids) {
     /**
      * To implement something like getRequiredHandlePHIDs() in a
      * ReleephFieldSpecification, we'd have to provide the field with its
      * ReleephRequest (so that it could load the PHIDs from the
      * ReleephRequest's storage, and return them.)
      *
      * We don't have fields initialized with their ReleephRequests, but we can
      * make a good guess at what handles will be needed for rendering the field
      * in this transaction by inspecting the old and new values.
      */
     if (!is_array($thing)) {
       $thing = array($thing);
     }
 
     foreach ($thing as $value) {
       if (phid_get_type($value) !==
         PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
 
         $phids[] = $value;
       }
     }
   }
 
   private function getIntentTitle() {
     $author_phid = $this->getAuthorPHID();
     $object_phid = $this->getObjectPHID();
 
     $new = $this->getNewValue();
     $is_pusher = $this->getMetadataValue('isPusher');
 
     switch ($new) {
       case ReleephRequest::INTENT_WANT:
         if ($is_pusher) {
           return pht(
             '%s approved this request',
             $this->renderHandleLink($author_phid));
         } else {
           return pht(
             '%s wanted this request',
             $this->renderHandleLink($author_phid));
         }
 
       case ReleephRequest::INTENT_PASS:
         if ($is_pusher) {
           return pht(
             '%s rejected this request',
             $this->renderHandleLink($author_phid));
         } else {
           return pht(
             '%s passed on this request',
             $this->renderHandleLink($author_phid));
         }
     }
   }
 
   public function shouldHide() {
     $type = $this->getTransactionType();
 
-    if ($type === ReleephRequestTransaction::TYPE_USER_INTENT &&
+    if ($type === self::TYPE_USER_INTENT &&
         $this->getMetadataValue('isRQCreate')) {
 
       return true;
     }
 
     if ($this->isBoringPickStatus()) {
       return true;
     }
 
     // ReleephSummaryFieldSpecification is usually blank when an RQ is created,
     // creating a transaction change from null to "". Hide these!
-    if ($type === ReleephRequestTransaction::TYPE_EDIT_FIELD) {
+    if ($type === self::TYPE_EDIT_FIELD) {
       if ($this->getOldValue() === null && $this->getNewValue() === '') {
         return true;
       }
     }
     return parent::shouldHide();
   }
 
   public function isBoringPickStatus() {
     $type = $this->getTransactionType();
-    if ($type === ReleephRequestTransaction::TYPE_PICK_STATUS) {
+    if ($type === self::TYPE_PICK_STATUS) {
       $new = $this->getNewValue();
       if ($new === ReleephRequest::PICK_OK ||
           $new === ReleephRequest::REVERT_OK) {
 
         return true;
       }
     }
     return false;
   }
 
 }
diff --git a/src/applications/repository/query/PhabricatorRepositorySearchEngine.php b/src/applications/repository/query/PhabricatorRepositorySearchEngine.php
index 3131f9b9e6..21c7fe38ab 100644
--- a/src/applications/repository/query/PhabricatorRepositorySearchEngine.php
+++ b/src/applications/repository/query/PhabricatorRepositorySearchEngine.php
@@ -1,282 +1,282 @@
 <?php
 
 final class PhabricatorRepositorySearchEngine
   extends PhabricatorApplicationSearchEngine {
 
   public function getResultTypeDescription() {
     return pht('Repositories');
   }
 
   public function getApplicationClassName() {
     return 'PhabricatorDiffusionApplication';
   }
 
   public function buildSavedQueryFromRequest(AphrontRequest $request) {
     $saved = new PhabricatorSavedQuery();
 
     $saved->setParameter('callsigns', $request->getStrList('callsigns'));
     $saved->setParameter('status', $request->getStr('status'));
     $saved->setParameter('order', $request->getStr('order'));
     $saved->setParameter('hosted', $request->getStr('hosted'));
     $saved->setParameter('types', $request->getArr('types'));
     $saved->setParameter('name', $request->getStr('name'));
 
     $saved->setParameter(
       'projects',
       $this->readProjectsFromRequest($request, 'projects'));
 
     return $saved;
   }
 
   public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
     $query = id(new PhabricatorRepositoryQuery())
       ->needProjectPHIDs(true)
       ->needCommitCounts(true)
       ->needMostRecentCommits(true);
 
     $callsigns = $saved->getParameter('callsigns');
     if ($callsigns) {
       $query->withCallsigns($callsigns);
     }
 
     $status = $saved->getParameter('status');
     $status = idx($this->getStatusValues(), $status);
     if ($status) {
       $query->withStatus($status);
     }
 
     $this->setQueryOrder($query, $saved);
 
     $hosted = $saved->getParameter('hosted');
     $hosted = idx($this->getHostedValues(), $hosted);
     if ($hosted) {
       $query->withHosted($hosted);
     }
 
     $types = $saved->getParameter('types');
     if ($types) {
       $query->withTypes($types);
     }
 
     $name = $saved->getParameter('name');
     if (strlen($name)) {
       $query->withNameContains($name);
     }
 
     $adjusted = clone $saved;
     $adjusted->setParameter('projects', $this->readProjectTokens($saved));
     $this->setQueryProjects($query, $adjusted);
 
     return $query;
   }
 
   public function buildSearchForm(
     AphrontFormView $form,
     PhabricatorSavedQuery $saved_query) {
 
     $callsigns = $saved_query->getParameter('callsigns', array());
     $types = $saved_query->getParameter('types', array());
     $types = array_fuse($types);
     $name = $saved_query->getParameter('name');
     $projects = $this->readProjectTokens($saved_query);
 
     $form
       ->appendChild(
         id(new AphrontFormTextControl())
           ->setName('callsigns')
           ->setLabel(pht('Callsigns'))
           ->setValue(implode(', ', $callsigns)))
       ->appendChild(
         id(new AphrontFormTextControl())
           ->setName('name')
           ->setLabel(pht('Name Contains'))
           ->setValue($name))
       ->appendControl(
         id(new AphrontFormTokenizerControl())
           ->setDatasource(new PhabricatorProjectLogicalDatasource())
           ->setName('projects')
           ->setLabel(pht('Projects'))
           ->setValue($projects))
       ->appendChild(
         id(new AphrontFormSelectControl())
           ->setName('status')
           ->setLabel(pht('Status'))
           ->setValue($saved_query->getParameter('status'))
           ->setOptions($this->getStatusOptions()))
       ->appendChild(
         id(new AphrontFormSelectControl())
           ->setName('hosted')
           ->setLabel(pht('Hosted'))
           ->setValue($saved_query->getParameter('hosted'))
           ->setOptions($this->getHostedOptions()));
 
     $type_control = id(new AphrontFormCheckboxControl())
       ->setLabel(pht('Types'));
 
     $all_types = PhabricatorRepositoryType::getAllRepositoryTypes();
     foreach ($all_types as $key => $name) {
       $type_control->addCheckbox(
         'types[]',
         $key,
         $name,
         isset($types[$key]));
     }
     $form->appendChild($type_control);
 
     $this->appendOrderFieldsToForm(
       $form,
       $saved_query,
       new PhabricatorRepositoryQuery());
   }
 
   protected function getURI($path) {
     return '/diffusion/'.$path;
   }
 
   protected function getBuiltinQueryNames() {
     $names = array(
       'active' => pht('Active Repositories'),
       'all' => pht('All Repositories'),
     );
 
     return $names;
   }
 
   public function buildSavedQueryFromBuiltin($query_key) {
 
     $query = $this->newSavedQuery();
     $query->setQueryKey($query_key);
 
     switch ($query_key) {
       case 'active':
         return $query->setParameter('status', 'open');
       case 'all':
         return $query;
     }
 
     return parent::buildSavedQueryFromBuiltin($query_key);
   }
 
   private function getStatusOptions() {
     return array(
       '' => pht('Active and Inactive Repositories'),
       'open' => pht('Active Repositories'),
       'closed' => pht('Inactive Repositories'),
     );
   }
 
   private function getStatusValues() {
     return array(
       '' => PhabricatorRepositoryQuery::STATUS_ALL,
       'open' => PhabricatorRepositoryQuery::STATUS_OPEN,
       'closed' => PhabricatorRepositoryQuery::STATUS_CLOSED,
     );
   }
 
   private function getHostedOptions() {
     return array(
       '' => pht('Hosted and Remote Repositories'),
       'phabricator' => pht('Hosted Repositories'),
       'remote' => pht('Remote Repositories'),
     );
   }
 
   private function getHostedValues() {
     return array(
       '' => PhabricatorRepositoryQuery::HOSTED_ALL,
       'phabricator' => PhabricatorRepositoryQuery::HOSTED_PHABRICATOR,
       'remote' => PhabricatorRepositoryQuery::HOSTED_REMOTE,
     );
   }
 
   protected function getRequiredHandlePHIDsForResultList(
     array $repositories,
     PhabricatorSavedQuery $query) {
     return array_mergev(mpull($repositories, 'getProjectPHIDs'));
   }
 
   protected function renderResultList(
     array $repositories,
     PhabricatorSavedQuery $query,
     array $handles) {
     assert_instances_of($repositories, 'PhabricatorRepository');
 
-    $viewer = $this->requireViewer();;
+    $viewer = $this->requireViewer();
 
     $list = new PHUIObjectItemListView();
     foreach ($repositories as $repository) {
       $id = $repository->getID();
 
       $item = id(new PHUIObjectItemView())
         ->setUser($viewer)
         ->setHeader($repository->getName())
         ->setObjectName('r'.$repository->getCallsign())
         ->setHref($this->getApplicationURI($repository->getCallsign().'/'));
 
       $commit = $repository->getMostRecentCommit();
       if ($commit) {
         $commit_link = DiffusionView::linkCommit(
             $repository,
             $commit->getCommitIdentifier(),
             $commit->getSummary());
         $item->setSubhead($commit_link);
         $item->setEpoch($commit->getEpoch());
       }
 
       $item->addIcon(
         'none',
         PhabricatorRepositoryType::getNameForRepositoryType(
           $repository->getVersionControlSystem()));
 
       $size = $repository->getCommitCount();
       if ($size) {
         $history_uri = DiffusionRequest::generateDiffusionURI(
           array(
             'callsign' => $repository->getCallsign(),
             'action' => 'history',
           ));
 
         $item->addAttribute(
           phutil_tag(
             'a',
             array(
               'href' => $history_uri,
             ),
             pht('%s Commit(s)', new PhutilNumber($size))));
       } else {
         $item->addAttribute(pht('No Commits'));
       }
 
       $project_handles = array_select_keys(
         $handles,
         $repository->getProjectPHIDs());
       if ($project_handles) {
         $item->addAttribute(
           id(new PHUIHandleTagListView())
             ->setSlim(true)
             ->setHandles($project_handles));
       }
 
       if (!$repository->isTracked()) {
         $item->setDisabled(true);
         $item->addIcon('disable-grey', pht('Inactive'));
       }
 
       $list->addItem($item);
     }
 
     return $list;
   }
 
   private function readProjectTokens(PhabricatorSavedQuery $saved) {
     $projects = $saved->getParameter('projects', array());
 
     $any = $saved->getParameter('anyProjectPHIDs', array());
     foreach ($any as $project) {
       $projects[] = 'any('.$project.')';
     }
 
     return $projects;
   }
 
 }
diff --git a/src/applications/repository/storage/PhabricatorRepositoryPushLog.php b/src/applications/repository/storage/PhabricatorRepositoryPushLog.php
index d5fb87fa8d..f16784bbf1 100644
--- a/src/applications/repository/storage/PhabricatorRepositoryPushLog.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryPushLog.php
@@ -1,202 +1,202 @@
 <?php
 
 /**
  * Records a push to a hosted repository. This allows us to store metadata
  * about who pushed commits, when, and from where. We can also record the
  * history of branches and tags, which is not normally persisted outside of
  * the reflog.
  *
  * This log is written by commit hooks installed into hosted repositories.
  * See @{class:DiffusionCommitHookEngine}.
  */
 final class PhabricatorRepositoryPushLog
   extends PhabricatorRepositoryDAO
   implements PhabricatorPolicyInterface {
 
   const REFTYPE_BRANCH = 'branch';
   const REFTYPE_TAG = 'tag';
   const REFTYPE_BOOKMARK = 'bookmark';
   const REFTYPE_COMMIT = 'commit';
 
   const CHANGEFLAG_ADD = 1;
   const CHANGEFLAG_DELETE = 2;
   const CHANGEFLAG_APPEND = 4;
   const CHANGEFLAG_REWRITE = 8;
   const CHANGEFLAG_DANGEROUS = 16;
 
   const REJECT_ACCEPT = 0;
   const REJECT_DANGEROUS = 1;
   const REJECT_HERALD = 2;
   const REJECT_EXTERNAL = 3;
   const REJECT_BROKEN = 4;
 
   protected $repositoryPHID;
   protected $epoch;
   protected $pusherPHID;
   protected $pushEventPHID;
   protected $refType;
   protected $refNameHash;
   protected $refNameRaw;
   protected $refNameEncoding;
   protected $refOld;
   protected $refNew;
   protected $mergeBase;
   protected $changeFlags;
 
   private $dangerousChangeDescription = self::ATTACHABLE;
   private $pushEvent = self::ATTACHABLE;
   private $repository = self::ATTACHABLE;
 
   public static function initializeNewLog(PhabricatorUser $viewer) {
     return id(new PhabricatorRepositoryPushLog())
       ->setPusherPHID($viewer->getPHID());
   }
 
   public static function getHeraldChangeFlagConditionOptions() {
     return array(
-      PhabricatorRepositoryPushLog::CHANGEFLAG_ADD =>
+      self::CHANGEFLAG_ADD =>
         pht('change creates ref'),
-      PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE =>
+      self::CHANGEFLAG_DELETE =>
         pht('change deletes ref'),
-      PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE =>
+      self::CHANGEFLAG_REWRITE =>
         pht('change rewrites ref'),
-      PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS =>
+      self::CHANGEFLAG_DANGEROUS =>
         pht('dangerous change'),
     );
   }
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_TIMESTAMPS => false,
       self::CONFIG_BINARY => array(
         'refNameRaw' => true,
       ),
       self::CONFIG_COLUMN_SCHEMA => array(
         'refType' => 'text12',
         'refNameHash' => 'bytes12?',
         'refNameRaw' => 'bytes?',
         'refNameEncoding' => 'text16?',
         'refOld' => 'text40?',
         'refNew' => 'text40',
         'mergeBase' => 'text40?',
         'changeFlags' => 'uint32',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_repository' => array(
           'columns' => array('repositoryPHID'),
         ),
         'key_ref' => array(
           'columns' => array('repositoryPHID', 'refNew'),
         ),
         'key_name' => array(
           'columns' => array('repositoryPHID', 'refNameHash'),
         ),
         'key_event' => array(
           'columns' => array('pushEventPHID'),
         ),
         'key_pusher' => array(
           'columns' => array('pusherPHID'),
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       PhabricatorRepositoryPushLogPHIDType::TYPECONST);
   }
 
   public function attachPushEvent(PhabricatorRepositoryPushEvent $push_event) {
     $this->pushEvent = $push_event;
     return $this;
   }
 
   public function getPushEvent() {
     return $this->assertAttached($this->pushEvent);
   }
 
   public function getRefName() {
     return $this->getUTF8StringFromStorage(
       $this->getRefNameRaw(),
       $this->getRefNameEncoding());
   }
 
   public function setRefName($ref_raw) {
     $this->setRefNameRaw($ref_raw);
     $this->setRefNameHash(PhabricatorHash::digestForIndex($ref_raw));
     $this->setRefNameEncoding($this->detectEncodingForStorage($ref_raw));
 
     return $this;
   }
 
   public function getRefOldShort() {
     if ($this->getRepository()->isSVN()) {
       return $this->getRefOld();
     }
     return substr($this->getRefOld(), 0, 12);
   }
 
   public function getRefNewShort() {
     if ($this->getRepository()->isSVN()) {
       return $this->getRefNew();
     }
     return substr($this->getRefNew(), 0, 12);
   }
 
   public function hasChangeFlags($mask) {
     return ($this->changeFlags & $mask);
   }
 
   public function attachDangerousChangeDescription($description) {
     $this->dangerousChangeDescription = $description;
     return $this;
   }
 
   public function getDangerousChangeDescription() {
     return $this->assertAttached($this->dangerousChangeDescription);
   }
 
   public function attachRepository(PhabricatorRepository $repository) {
     // NOTE: Some gymnastics around this because of object construction order
     // in the hook engine. Particularly, web build the logs before we build
     // their push event.
     $this->repository = $repository;
     return $this;
   }
 
   public function getRepository() {
     if ($this->repository == self::ATTACHABLE) {
       return $this->getPushEvent()->getRepository();
     }
     return $this->assertAttached($this->repository);
   }
 
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
     );
   }
 
   public function getPolicy($capability) {
     // NOTE: We're passing through the repository rather than the push event
     // mostly because we need to do policy checks in Herald before we create
     // the event. The two approaches are equivalent in practice.
     return $this->getRepository()->getPolicy($capability);
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     return $this->getRepository()->hasAutomaticCapability($capability, $viewer);
   }
 
   public function describeAutomaticCapability($capability) {
     return pht(
       "A repository's push logs are visible to users who can see the ".
       "repository.");
   }
 
 }
diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php
index d3ba1cf9ae..ac67585268 100644
--- a/src/applications/search/controller/PhabricatorApplicationSearchController.php
+++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php
@@ -1,376 +1,370 @@
 <?php
 
 final class PhabricatorApplicationSearchController
   extends PhabricatorSearchBaseController {
 
   private $searchEngine;
   private $navigation;
   private $queryKey;
   private $preface;
 
   public function setPreface($preface) {
     $this->preface = $preface;
     return $this;
   }
 
   public function getPreface() {
     return $this->preface;
   }
 
   public function setQueryKey($query_key) {
     $this->queryKey = $query_key;
     return $this;
   }
 
   protected function getQueryKey() {
     return $this->queryKey;
   }
 
   public function setNavigation(AphrontSideNavFilterView $navigation) {
     $this->navigation = $navigation;
     return $this;
   }
 
   protected function getNavigation() {
     return $this->navigation;
   }
 
   public function setSearchEngine(
     PhabricatorApplicationSearchEngine $search_engine) {
     $this->searchEngine = $search_engine;
     return $this;
   }
 
   protected function getSearchEngine() {
     return $this->searchEngine;
   }
 
   protected function validateDelegatingController() {
     $parent = $this->getDelegatingController();
 
     if (!$parent) {
       throw new Exception(
         pht('You must delegate to this controller, not invoke it directly.'));
     }
 
     $engine = $this->getSearchEngine();
     if (!$engine) {
-      throw new Exception(
-        pht(
-          'Call %s before delegating to this controller!',
-          'setEngine()'));
+      throw new PhutilInvalidStateException('setEngine');
     }
 
     $nav = $this->getNavigation();
     if (!$nav) {
-      throw new Exception(
-        pht(
-          'Call %s before delegating to this controller!',
-          'setNavigation()'));
+      throw new PhutilInvalidStateException('setNavigation');
     }
 
     $engine->setViewer($this->getRequest()->getUser());
 
     $parent = $this->getDelegatingController();
   }
 
   public function processRequest() {
     $this->validateDelegatingController();
 
     $key = $this->getQueryKey();
     if ($key == 'edit') {
       return $this->processEditRequest();
     } else {
       return $this->processSearchRequest();
     }
   }
 
   private function processSearchRequest() {
     $parent = $this->getDelegatingController();
     $request = $this->getRequest();
     $user = $request->getUser();
     $engine = $this->getSearchEngine();
     $nav = $this->getNavigation();
 
     if ($request->isFormPost()) {
       $saved_query = $engine->buildSavedQueryFromRequest($request);
       $engine->saveQuery($saved_query);
       return id(new AphrontRedirectResponse())->setURI(
         $engine->getQueryResultsPageURI($saved_query->getQueryKey()).'#R');
     }
 
     $named_query = null;
     $run_query = true;
     $query_key = $this->queryKey;
     if ($this->queryKey == 'advanced') {
       $run_query = false;
       $query_key = $request->getStr('query');
     } else if (!strlen($this->queryKey)) {
       $found_query_data = false;
 
       if ($request->isHTTPGet()) {
         // If this is a GET request and it has some query data, don't
         // do anything unless it's only before= or after=. We'll build and
         // execute a query from it below. This allows external tools to build
         // URIs like "/query/?users=a,b".
         $pt_data = $request->getPassthroughRequestData();
 
         foreach ($pt_data as $pt_key => $pt_value) {
           if ($pt_key != 'before' && $pt_key != 'after') {
             $found_query_data = true;
             break;
           }
         }
       }
 
       if (!$found_query_data) {
         // Otherwise, there's no query data so just run the user's default
         // query for this application.
         $query_key = head_key($engine->loadEnabledNamedQueries());
       }
     }
 
     if ($engine->isBuiltinQuery($query_key)) {
       $saved_query = $engine->buildSavedQueryFromBuiltin($query_key);
       $named_query = idx($engine->loadEnabledNamedQueries(), $query_key);
     } else if ($query_key) {
       $saved_query = id(new PhabricatorSavedQueryQuery())
         ->setViewer($user)
         ->withQueryKeys(array($query_key))
         ->executeOne();
 
       if (!$saved_query) {
         return new Aphront404Response();
       }
 
       $named_query = idx($engine->loadEnabledNamedQueries(), $query_key);
     } else {
       $saved_query = $engine->buildSavedQueryFromRequest($request);
 
       // Save the query to generate a query key, so "Save Custom Query..." and
       // other features like Maniphest's "Export..." work correctly.
       $engine->saveQuery($saved_query);
     }
 
     $nav->selectFilter(
       'query/'.$saved_query->getQueryKey(),
       'query/advanced');
 
     $form = id(new AphrontFormView())
       ->setUser($user)
       ->setAction($request->getPath());
 
     $engine->buildSearchForm($form, $saved_query);
 
     $errors = $engine->getErrors();
     if ($errors) {
       $run_query = false;
     }
 
     $submit = id(new AphrontFormSubmitControl())
       ->setValue(pht('Execute Query'));
 
     if ($run_query && !$named_query && $user->isLoggedIn()) {
       $submit->addCancelButton(
         '/search/edit/'.$saved_query->getQueryKey().'/',
         pht('Save Custom Query...'));
     }
 
     // TODO: A "Create Dashboard Panel" action goes here somewhere once
     // we sort out T5307.
 
     $form->appendChild($submit);
     $filter_view = id(new AphrontListFilterView())->appendChild($form);
 
     if ($run_query && $named_query) {
       if ($named_query->getIsBuiltin()) {
         $description = pht(
           'Showing results for query "%s".',
           $named_query->getQueryName());
       } else {
         $description = pht(
           'Showing results for saved query "%s".',
           $named_query->getQueryName());
       }
 
       $filter_view->setCollapsed(
         pht('Edit Query...'),
         pht('Hide Query'),
         $description,
         $this->getApplicationURI('query/advanced/?query='.$query_key));
     }
 
     if ($this->getPreface()) {
       $nav->appendChild($this->getPreface());
     }
 
     $nav->appendChild($filter_view);
 
     if ($run_query) {
       $nav->appendChild(
         $anchor = id(new PhabricatorAnchorView())
           ->setAnchorName('R'));
 
       try {
         $query = $engine->buildQueryFromSavedQuery($saved_query);
 
         $pager = $engine->newPagerForSavedQuery($saved_query);
         $pager->readFromRequest($request);
 
         $objects = $engine->executeQuery($query, $pager);
 
         // TODO: To support Dashboard panels, rendering is moving into
         // SearchEngines. Move it all the way in and then get rid of this.
 
         $interface = 'PhabricatorApplicationSearchResultsControllerInterface';
         if ($parent instanceof $interface) {
           $list = $parent->renderResultsList($objects, $saved_query);
         } else {
           $engine->setRequest($request);
 
           $list = $engine->renderResults(
             $objects,
             $saved_query);
         }
 
         $nav->appendChild($list);
 
         // TODO: This is a bit hacky.
         if ($list instanceof PHUIObjectItemListView) {
           $list->setNoDataString(pht('No results found for this query.'));
           $list->setPager($pager);
         } else {
           if ($pager->willShowPagingControls()) {
             $pager_box = id(new PHUIBoxView())
               ->addPadding(PHUI::PADDING_MEDIUM)
               ->addMargin(PHUI::MARGIN_LARGE)
               ->setBorder(true)
               ->appendChild($pager);
             $nav->appendChild($pager_box);
           }
         }
       } catch (PhabricatorTypeaheadInvalidTokenException $ex) {
         $errors[] = pht(
           'This query specifies an invalid parameter. Review the '.
           'query parameters and correct errors.');
       }
     }
 
     if ($errors) {
       $errors = id(new PHUIInfoView())
         ->setTitle(pht('Query Errors'))
         ->setErrors($errors);
     }
 
     if ($errors) {
       $nav->appendChild($errors);
     }
 
     if ($named_query) {
       $title = $named_query->getQueryName();
     } else {
       $title = pht('Advanced Search');
     }
 
     $crumbs = $parent
       ->buildApplicationCrumbs()
       ->setBorder(true)
       ->addTextCrumb($title);
 
     $nav->setCrumbs($crumbs);
 
     return $this->buildApplicationPage(
       $nav,
       array(
         'title' => pht('Query: %s', $title),
       ));
   }
 
   private function processEditRequest() {
     $parent = $this->getDelegatingController();
     $request = $this->getRequest();
     $user = $request->getUser();
     $engine = $this->getSearchEngine();
     $nav = $this->getNavigation();
 
     $named_queries = $engine->loadAllNamedQueries();
 
     $list_id = celerity_generate_unique_node_id();
 
     $list = new PHUIObjectItemListView();
     $list->setUser($user);
     $list->setID($list_id);
 
     Javelin::initBehavior(
       'search-reorder-queries',
       array(
         'listID' => $list_id,
         'orderURI' => '/search/order/'.get_class($engine).'/',
       ));
 
     foreach ($named_queries as $named_query) {
       $class = get_class($engine);
       $key = $named_query->getQueryKey();
 
       $item = id(new PHUIObjectItemView())
         ->setHeader($named_query->getQueryName())
         ->setHref($engine->getQueryResultsPageURI($key));
 
       if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) {
         $icon = 'fa-plus';
       } else {
         $icon = 'fa-times';
       }
 
       $item->addAction(
         id(new PHUIListItemView())
           ->setIcon($icon)
           ->setHref('/search/delete/'.$key.'/'.$class.'/')
           ->setWorkflow(true));
 
       if ($named_query->getIsBuiltin()) {
         if ($named_query->getIsDisabled()) {
           $item->addIcon('fa-times lightgreytext', pht('Disabled'));
           $item->setDisabled(true);
         } else {
           $item->addIcon('fa-lock lightgreytext', pht('Builtin'));
         }
       } else {
         $item->addAction(
           id(new PHUIListItemView())
             ->setIcon('fa-pencil')
             ->setHref('/search/edit/'.$key.'/'));
       }
 
       $item->setGrippable(true);
       $item->addSigil('named-query');
       $item->setMetadata(
         array(
           'queryKey' => $named_query->getQueryKey(),
         ));
 
       $list->addItem($item);
     }
 
     $list->setNoDataString(pht('No saved queries.'));
 
     $crumbs = $parent
       ->buildApplicationCrumbs()
       ->addTextCrumb(pht('Saved Queries'), $engine->getQueryManagementURI());
 
     $nav->selectFilter('query/edit');
     $nav->setCrumbs($crumbs);
     $nav->appendChild($list);
 
     return $parent->buildApplicationPage(
       $nav,
       array(
         'title' => pht('Saved Queries'),
       ));
   }
 
   public function buildApplicationMenu() {
     return $this->getDelegatingController()->buildApplicationMenu();
   }
 
 }
diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
index a0e7245a76..cd0a9655fa 100644
--- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
+++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
@@ -1,1031 +1,1031 @@
 <?php
 
 /**
  * Represents an abstract search engine for an application. It supports
  * creating and storing saved queries.
  *
  * @task construct  Constructing Engines
  * @task app        Applications
  * @task builtin    Builtin Queries
  * @task uri        Query URIs
  * @task dates      Date Filters
  * @task order      Result Ordering
  * @task read       Reading Utilities
  * @task exec       Paging and Executing Queries
  * @task render     Rendering Results
  */
 abstract class PhabricatorApplicationSearchEngine {
 
   private $application;
   private $viewer;
   private $errors = array();
   private $customFields = false;
   private $request;
   private $context;
 
   const CONTEXT_LIST  = 'list';
   const CONTEXT_PANEL = 'panel';
 
   public function setViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
   protected function requireViewer() {
     if (!$this->viewer) {
-      throw new Exception('Call setViewer() before using an engine!');
+      throw new PhutilInvalidStateException('setViewer');
     }
     return $this->viewer;
   }
 
   public function setContext($context) {
     $this->context = $context;
     return $this;
   }
 
   public function isPanelContext() {
     return ($this->context == self::CONTEXT_PANEL);
   }
 
   public function canUseInPanelContext() {
     return true;
   }
 
   public function saveQuery(PhabricatorSavedQuery $query) {
     $query->setEngineClassName(get_class($this));
 
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
     try {
       $query->save();
     } catch (AphrontDuplicateKeyQueryException $ex) {
       // Ignore, this is just a repeated search.
     }
     unset($unguarded);
   }
 
   /**
    * Create a saved query object from the request.
    *
    * @param AphrontRequest The search request.
    * @return PhabricatorSavedQuery
    */
   abstract public function buildSavedQueryFromRequest(
     AphrontRequest $request);
 
   /**
    * Executes the saved query.
    *
    * @param PhabricatorSavedQuery The saved query to operate on.
    * @return The result of the query.
    */
   abstract public function buildQueryFromSavedQuery(
     PhabricatorSavedQuery $saved);
 
   /**
    * Builds the search form using the request.
    *
    * @param AphrontFormView       Form to populate.
    * @param PhabricatorSavedQuery The query from which to build the form.
    * @return void
    */
   abstract public function buildSearchForm(
     AphrontFormView $form,
     PhabricatorSavedQuery $query);
 
   public function getErrors() {
     return $this->errors;
   }
 
   public function addError($error) {
     $this->errors[] = $error;
     return $this;
   }
 
   /**
    * Return an application URI corresponding to the results page of a query.
    * Normally, this is something like `/application/query/QUERYKEY/`.
    *
    * @param   string  The query key to build a URI for.
    * @return  string  URI where the query can be executed.
    * @task uri
    */
   public function getQueryResultsPageURI($query_key) {
     return $this->getURI('query/'.$query_key.'/');
   }
 
 
   /**
    * Return an application URI for query management. This is used when, e.g.,
    * a query deletion operation is cancelled.
    *
    * @return  string  URI where queries can be managed.
    * @task uri
    */
   public function getQueryManagementURI() {
     return $this->getURI('query/edit/');
   }
 
 
   /**
    * Return the URI to a path within the application. Used to construct default
    * URIs for management and results.
    *
    * @return string URI to path.
    * @task uri
    */
   abstract protected function getURI($path);
 
 
   /**
    * Return a human readable description of the type of objects this query
    * searches for.
    *
    * For example, "Tasks" or "Commits".
    *
    * @return string Human-readable description of what this engine is used to
    *   find.
    */
   abstract public function getResultTypeDescription();
 
 
   public function newSavedQuery() {
     return id(new PhabricatorSavedQuery())
       ->setEngineClassName(get_class($this));
   }
 
   public function addNavigationItems(PHUIListView $menu) {
     $viewer = $this->requireViewer();
 
     $menu->newLabel(pht('Queries'));
 
     $named_queries = $this->loadEnabledNamedQueries();
 
     foreach ($named_queries as $query) {
       $key = $query->getQueryKey();
       $uri = $this->getQueryResultsPageURI($key);
       $menu->newLink($query->getQueryName(), $uri, 'query/'.$key);
     }
 
     if ($viewer->isLoggedIn()) {
       $manage_uri = $this->getQueryManagementURI();
       $menu->newLink(pht('Edit Queries...'), $manage_uri, 'query/edit');
     }
 
     $menu->newLabel(pht('Search'));
     $advanced_uri = $this->getQueryResultsPageURI('advanced');
     $menu->newLink(pht('Advanced Search'), $advanced_uri, 'query/advanced');
 
     return $this;
   }
 
   public function loadAllNamedQueries() {
     $viewer = $this->requireViewer();
 
     $named_queries = id(new PhabricatorNamedQueryQuery())
       ->setViewer($viewer)
       ->withUserPHIDs(array($viewer->getPHID()))
       ->withEngineClassNames(array(get_class($this)))
       ->execute();
     $named_queries = mpull($named_queries, null, 'getQueryKey');
 
     $builtin = $this->getBuiltinQueries($viewer);
     $builtin = mpull($builtin, null, 'getQueryKey');
 
     foreach ($named_queries as $key => $named_query) {
       if ($named_query->getIsBuiltin()) {
         if (isset($builtin[$key])) {
           $named_queries[$key]->setQueryName($builtin[$key]->getQueryName());
           unset($builtin[$key]);
         } else {
           unset($named_queries[$key]);
         }
       }
 
       unset($builtin[$key]);
     }
 
     $named_queries = msort($named_queries, 'getSortKey');
 
     return $named_queries + $builtin;
   }
 
   public function loadEnabledNamedQueries() {
     $named_queries = $this->loadAllNamedQueries();
     foreach ($named_queries as $key => $named_query) {
       if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) {
         unset($named_queries[$key]);
       }
     }
     return $named_queries;
   }
 
   protected function setQueryProjects(
     PhabricatorCursorPagedPolicyAwareQuery $query,
     PhabricatorSavedQuery $saved) {
 
     $datasource = id(new PhabricatorProjectLogicalDatasource())
       ->setViewer($this->requireViewer());
 
     $projects = $saved->getParameter('projects', array());
     $constraints = $datasource->evaluateTokens($projects);
 
     if ($constraints) {
       $query->withEdgeLogicConstraints(
         PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
         $constraints);
     }
   }
 
 
 /* -(  Applications  )------------------------------------------------------- */
 
 
   protected function getApplicationURI($path = '') {
     return $this->getApplication()->getApplicationURI($path);
   }
 
   protected function getApplication() {
     if (!$this->application) {
       $class = $this->getApplicationClassName();
 
       $this->application = id(new PhabricatorApplicationQuery())
         ->setViewer($this->requireViewer())
         ->withClasses(array($class))
         ->withInstalled(true)
         ->executeOne();
 
       if (!$this->application) {
         throw new Exception(
           pht(
             'Application "%s" is not installed!',
             $class));
       }
     }
 
     return $this->application;
   }
 
   abstract public function getApplicationClassName();
 
 
 /* -(  Constructing Engines  )----------------------------------------------- */
 
 
   /**
    * Load all available application search engines.
    *
    * @return list<PhabricatorApplicationSearchEngine> All available engines.
    * @task construct
    */
   public static function getAllEngines() {
     $engines = id(new PhutilSymbolLoader())
       ->setAncestorClass(__CLASS__)
       ->loadObjects();
 
     return $engines;
   }
 
 
   /**
    * Get an engine by class name, if it exists.
    *
    * @return PhabricatorApplicationSearchEngine|null Engine, or null if it does
    *   not exist.
    * @task construct
    */
   public static function getEngineByClassName($class_name) {
     return idx(self::getAllEngines(), $class_name);
   }
 
 
 /* -(  Builtin Queries  )---------------------------------------------------- */
 
 
   /**
    * @task builtin
    */
   public function getBuiltinQueries() {
     $names = $this->getBuiltinQueryNames();
 
     $queries = array();
     $sequence = 0;
     foreach ($names as $key => $name) {
       $queries[$key] = id(new PhabricatorNamedQuery())
         ->setUserPHID($this->requireViewer()->getPHID())
         ->setEngineClassName(get_class($this))
         ->setQueryName($name)
         ->setQueryKey($key)
         ->setSequence((1 << 24) + $sequence++)
         ->setIsBuiltin(true);
     }
 
     return $queries;
   }
 
 
   /**
    * @task builtin
    */
   public function getBuiltinQuery($query_key) {
     if (!$this->isBuiltinQuery($query_key)) {
       throw new Exception("'{$query_key}' is not a builtin!");
     }
     return idx($this->getBuiltinQueries(), $query_key);
   }
 
 
   /**
    * @task builtin
    */
   protected function getBuiltinQueryNames() {
     return array();
   }
 
 
   /**
    * @task builtin
    */
   public function isBuiltinQuery($query_key) {
     $builtins = $this->getBuiltinQueries();
     return isset($builtins[$query_key]);
   }
 
 
   /**
    * @task builtin
    */
   public function buildSavedQueryFromBuiltin($query_key) {
     throw new Exception("Builtin '{$query_key}' is not supported!");
   }
 
 
 /* -(  Reading Utilities )--------------------------------------------------- */
 
 
   /**
    * Read a list of user PHIDs from a request in a flexible way. This method
    * supports either of these forms:
    *
    *   users[]=alincoln&users[]=htaft
    *   users=alincoln,htaft
    *
    * Additionally, users can be specified either by PHID or by name.
    *
    * The main goal of this flexibility is to allow external programs to generate
    * links to pages (like "alincoln's open revisions") without needing to make
    * API calls.
    *
    * @param AphrontRequest  Request to read user PHIDs from.
    * @param string          Key to read in the request.
    * @param list<const>     Other permitted PHID types.
    * @return list<phid>     List of user PHIDs and selector functions.
    * @task read
    */
   protected function readUsersFromRequest(
     AphrontRequest $request,
     $key,
     array $allow_types = array()) {
 
     $list = $this->readListFromRequest($request, $key);
 
     $phids = array();
     $names = array();
     $allow_types = array_fuse($allow_types);
     $user_type = PhabricatorPeopleUserPHIDType::TYPECONST;
     foreach ($list as $item) {
       $type = phid_get_type($item);
       if ($type == $user_type) {
         $phids[] = $item;
       } else if (isset($allow_types[$type])) {
         $phids[] = $item;
       } else {
         if (PhabricatorTypeaheadDatasource::isFunctionToken($item)) {
           // If this is a function, pass it through unchanged; we'll evaluate
           // it later.
           $phids[] = $item;
         } else {
           $names[] = $item;
         }
       }
     }
 
     if ($names) {
       $users = id(new PhabricatorPeopleQuery())
         ->setViewer($this->requireViewer())
         ->withUsernames($names)
         ->execute();
       foreach ($users as $user) {
         $phids[] = $user->getPHID();
       }
       $phids = array_unique($phids);
     }
 
     return $phids;
   }
 
 
   /**
    * Read a list of project PHIDs from a request in a flexible way.
    *
    * @param AphrontRequest  Request to read user PHIDs from.
    * @param string          Key to read in the request.
    * @return list<phid>     List of projet PHIDs and selector functions.
    * @task read
    */
   protected function readProjectsFromRequest(AphrontRequest $request, $key) {
     $list = $this->readListFromRequest($request, $key);
 
     $phids = array();
     $slugs = array();
     $project_type = PhabricatorProjectProjectPHIDType::TYPECONST;
     foreach ($list as $item) {
       $type = phid_get_type($item);
       if ($type == $project_type) {
         $phids[] = $item;
       } else {
         if (PhabricatorTypeaheadDatasource::isFunctionToken($item)) {
           // If this is a function, pass it through unchanged; we'll evaluate
           // it later.
           $phids[] = $item;
         } else {
           $slugs[] = $item;
         }
       }
     }
 
     if ($slugs) {
       $projects = id(new PhabricatorProjectQuery())
         ->setViewer($this->requireViewer())
         ->withSlugs($slugs)
         ->execute();
       foreach ($projects as $project) {
         $phids[] = $project->getPHID();
       }
       $phids = array_unique($phids);
     }
 
     return $phids;
   }
 
 
   /**
    * Read a list of subscribers from a request in a flexible way.
    *
    * @param AphrontRequest  Request to read PHIDs from.
    * @param string          Key to read in the request.
    * @return list<phid>     List of object PHIDs.
    * @task read
    */
   protected function readSubscribersFromRequest(
     AphrontRequest $request,
     $key) {
     return $this->readUsersFromRequest(
       $request,
       $key,
       array(
         PhabricatorProjectProjectPHIDType::TYPECONST,
         PhabricatorMailingListListPHIDType::TYPECONST,
       ));
   }
 
 
   /**
    * Read a list of generic PHIDs from a request in a flexible way. Like
    * @{method:readUsersFromRequest}, this method supports either array or
    * comma-delimited forms. Objects can be specified either by PHID or by
    * object name.
    *
    * @param AphrontRequest  Request to read PHIDs from.
    * @param string          Key to read in the request.
    * @param list<const>     Optional, list of permitted PHID types.
    * @return list<phid>     List of object PHIDs.
    *
    * @task read
    */
   protected function readPHIDsFromRequest(
     AphrontRequest $request,
     $key,
     array $allow_types = array()) {
 
     $list = $this->readListFromRequest($request, $key);
 
     $objects = id(new PhabricatorObjectQuery())
       ->setViewer($this->requireViewer())
       ->withNames($list)
       ->execute();
     $list = mpull($objects, 'getPHID');
 
     if (!$list) {
       return array();
     }
 
     // If only certain PHID types are allowed, filter out all the others.
     if ($allow_types) {
       $allow_types = array_fuse($allow_types);
       foreach ($list as $key => $phid) {
         if (empty($allow_types[phid_get_type($phid)])) {
           unset($list[$key]);
         }
       }
     }
 
     return $list;
   }
 
 
   /**
    * Read a list of items from the request, in either array format or string
    * format:
    *
    *   list[]=item1&list[]=item2
    *   list=item1,item2
    *
    * This provides flexibility when constructing URIs, especially from external
    * sources.
    *
    * @param AphrontRequest  Request to read strings from.
    * @param string          Key to read in the request.
    * @return list<string>   List of values.
    */
   protected function readListFromRequest(
     AphrontRequest $request,
     $key) {
     $list = $request->getArr($key, null);
     if ($list === null) {
       $list = $request->getStrList($key);
     }
 
     if (!$list) {
       return array();
     }
 
     return $list;
   }
 
   protected function readDateFromRequest(
     AphrontRequest $request,
     $key) {
 
     $value = AphrontFormDateControlValue::newFromRequest($request, $key);
 
     if ($value->isEmpty()) {
       return null;
     }
 
     return $value->getDictionary();
   }
 
   protected function readBoolFromRequest(
     AphrontRequest $request,
     $key) {
     if (!strlen($request->getStr($key))) {
       return null;
     }
     return $request->getBool($key);
   }
 
 
   protected function getBoolFromQuery(PhabricatorSavedQuery $query, $key) {
     $value = $query->getParameter($key);
     if ($value === null) {
       return $value;
     }
     return $value ? 'true' : 'false';
   }
 
 
 /* -(  Dates  )-------------------------------------------------------------- */
 
 
   /**
    * @task dates
    */
   protected function parseDateTime($date_time) {
     if (!strlen($date_time)) {
       return null;
     }
 
     return PhabricatorTime::parseLocalTime($date_time, $this->requireViewer());
   }
 
 
   /**
    * @task dates
    */
   protected function buildDateRange(
     AphrontFormView $form,
     PhabricatorSavedQuery $saved_query,
     $start_key,
     $start_name,
     $end_key,
     $end_name) {
 
     $start_str = $saved_query->getParameter($start_key);
     $start = null;
     if (strlen($start_str)) {
       $start = $this->parseDateTime($start_str);
       if (!$start) {
         $this->addError(
           pht(
             '"%s" date can not be parsed.',
             $start_name));
       }
     }
 
 
     $end_str = $saved_query->getParameter($end_key);
     $end = null;
     if (strlen($end_str)) {
       $end = $this->parseDateTime($end_str);
       if (!$end) {
         $this->addError(
           pht(
             '"%s" date can not be parsed.',
             $end_name));
       }
     }
 
     if ($start && $end && ($start >= $end)) {
       $this->addError(
         pht(
           '"%s" must be a date before "%s".',
           $start_name,
           $end_name));
     }
 
     $form
       ->appendChild(
         id(new PHUIFormFreeformDateControl())
           ->setName($start_key)
           ->setLabel($start_name)
           ->setValue($start_str))
       ->appendChild(
         id(new AphrontFormTextControl())
           ->setName($end_key)
           ->setLabel($end_name)
           ->setValue($end_str));
   }
 
 
 /* -(  Result Ordering  )---------------------------------------------------- */
 
 
   /**
    * Save order selection to a @{class:PhabricatorSavedQuery}.
    */
   protected function saveQueryOrder(
     PhabricatorSavedQuery $saved,
     AphrontRequest $request) {
 
     $saved->setParameter('order', $request->getStr('order'));
 
     return $this;
   }
 
 
   /**
    * Set query ordering from a saved value.
    */
   protected function setQueryOrder(
     PhabricatorCursorPagedPolicyAwareQuery $query,
     PhabricatorSavedQuery $saved) {
 
     $order = $saved->getParameter('order');
     $builtin = $query->getBuiltinOrders();
     if (strlen($order) && isset($builtin[$order])) {
       $query->setOrder($order);
     } else {
       // If the order is invalid or not available, we choose the first
       // builtin order. This isn't always the default order for the query,
       // but is the first value in the "Order" dropdown, and makes the query
       // behavior more consistent with the UI. In queries where the two
       // orders differ, this order is the preferred order for humans.
       $query->setOrder(head_key($builtin));
     }
 
     return $this;
   }
 
 
 
   protected function appendOrderFieldsToForm(
     AphrontFormView $form,
     PhabricatorSavedQuery $saved,
     PhabricatorCursorPagedPolicyAwareQuery $query) {
 
     $orders = $query->getBuiltinOrders();
     $orders = ipull($orders, 'name');
 
     $form->appendControl(
       id(new AphrontFormSelectControl())
         ->setLabel(pht('Order'))
         ->setName('order')
         ->setOptions($orders)
         ->setValue($saved->getParameter('order')));
   }
 
 /* -(  Paging and Executing Queries  )--------------------------------------- */
 
 
   public function getPageSize(PhabricatorSavedQuery $saved) {
     return $saved->getParameter('limit', 100);
   }
 
 
   public function shouldUseOffsetPaging() {
     return false;
   }
 
 
   public function newPagerForSavedQuery(PhabricatorSavedQuery $saved) {
     if ($this->shouldUseOffsetPaging()) {
       $pager = new AphrontPagerView();
     } else {
       $pager = new AphrontCursorPagerView();
     }
 
     $page_size = $this->getPageSize($saved);
     if (is_finite($page_size)) {
       $pager->setPageSize($page_size);
     } else {
       // Consider an INF pagesize to mean a large finite pagesize.
 
       // TODO: It would be nice to handle this more gracefully, but math
       // with INF seems to vary across PHP versions, systems, and runtimes.
       $pager->setPageSize(0xFFFF);
     }
 
     return $pager;
   }
 
 
   public function executeQuery(
     PhabricatorPolicyAwareQuery $query,
     AphrontView $pager) {
 
     $query->setViewer($this->requireViewer());
 
     if ($this->shouldUseOffsetPaging()) {
       $objects = $query->executeWithOffsetPager($pager);
     } else {
       $objects = $query->executeWithCursorPager($pager);
     }
 
     return $objects;
   }
 
 
 /* -(  Rendering  )---------------------------------------------------------- */
 
 
   public function setRequest(AphrontRequest $request) {
     $this->request = $request;
     return $this;
   }
 
   public function getRequest() {
     return $this->request;
   }
 
   public function renderResults(
     array $objects,
     PhabricatorSavedQuery $query) {
 
     $phids = $this->getRequiredHandlePHIDsForResultList($objects, $query);
 
     if ($phids) {
       $handles = id(new PhabricatorHandleQuery())
         ->setViewer($this->requireViewer())
         ->witHPHIDs($phids)
         ->execute();
     } else {
       $handles = array();
     }
 
     return $this->renderResultList($objects, $query, $handles);
   }
 
   protected function getRequiredHandlePHIDsForResultList(
     array $objects,
     PhabricatorSavedQuery $query) {
     return array();
   }
 
   protected function renderResultList(
     array $objects,
     PhabricatorSavedQuery $query,
     array $handles) {
     throw new Exception(pht('Not supported here yet!'));
   }
 
 
 /* -(  Application Search  )------------------------------------------------- */
 
 
   /**
    * Retrieve an object to use to define custom fields for this search.
    *
    * To integrate with custom fields, subclasses should override this method
    * and return an instance of the application object which implements
    * @{interface:PhabricatorCustomFieldInterface}.
    *
    * @return PhabricatorCustomFieldInterface|null Object with custom fields.
    * @task appsearch
    */
   public function getCustomFieldObject() {
     return null;
   }
 
 
   /**
    * Get the custom fields for this search.
    *
    * @return PhabricatorCustomFieldList|null Custom fields, if this search
    *   supports custom fields.
    * @task appsearch
    */
   public function getCustomFieldList() {
     if ($this->customFields === false) {
       $object = $this->getCustomFieldObject();
       if ($object) {
         $fields = PhabricatorCustomField::getObjectFields(
           $object,
           PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
         $fields->setViewer($this->requireViewer());
       } else {
         $fields = null;
       }
       $this->customFields = $fields;
     }
     return $this->customFields;
   }
 
 
   /**
    * Moves data from the request into a saved query.
    *
    * @param AphrontRequest Request to read.
    * @param PhabricatorSavedQuery Query to write to.
    * @return void
    * @task appsearch
    */
   protected function readCustomFieldsFromRequest(
     AphrontRequest $request,
     PhabricatorSavedQuery $saved) {
 
     $list = $this->getCustomFieldList();
     if (!$list) {
       return;
     }
 
     foreach ($list->getFields() as $field) {
       $key = $this->getKeyForCustomField($field);
       $value = $field->readApplicationSearchValueFromRequest(
         $this,
         $request);
       $saved->setParameter($key, $value);
     }
   }
 
 
   /**
    * Applies data from a saved query to an executable query.
    *
    * @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain.
    * @param PhabricatorSavedQuery Saved query to read.
    * @return void
    */
   protected function applyCustomFieldsToQuery(
     PhabricatorCursorPagedPolicyAwareQuery $query,
     PhabricatorSavedQuery $saved) {
 
     $list = $this->getCustomFieldList();
     if (!$list) {
       return;
     }
 
     foreach ($list->getFields() as $field) {
       $key = $this->getKeyForCustomField($field);
       $value = $field->applyApplicationSearchConstraintToQuery(
         $this,
         $query,
         $saved->getParameter($key));
     }
   }
 
   protected function applyOrderByToQuery(
     PhabricatorCursorPagedPolicyAwareQuery $query,
     array $standard_values,
     $order) {
 
     if (substr($order, 0, 7) === 'custom:') {
       $list = $this->getCustomFieldList();
       if (!$list) {
         $query->setOrderBy(head($standard_values));
         return;
       }
 
       foreach ($list->getFields() as $field) {
         $key = $this->getKeyForCustomField($field);
 
         if ($key === $order) {
           $index = $field->buildOrderIndex();
 
           if ($index === null) {
             $query->setOrderBy(head($standard_values));
             return;
           }
 
           $query->withApplicationSearchOrder(
             $field,
             $index,
             false);
           break;
         }
       }
     } else {
       $order = idx($standard_values, $order);
       if ($order) {
         $query->setOrderBy($order);
       } else {
         $query->setOrderBy(head($standard_values));
       }
     }
   }
 
 
   protected function getCustomFieldOrderOptions() {
     $list = $this->getCustomFieldList();
     if (!$list) {
       return;
     }
 
     $custom_order = array();
     foreach ($list->getFields() as $field) {
       if ($field->shouldAppearInApplicationSearch()) {
         if ($field->buildOrderIndex() !== null) {
           $key = $this->getKeyForCustomField($field);
           $custom_order[$key] = $field->getFieldName();
         }
       }
     }
 
     return $custom_order;
   }
 
   /**
    * Get a unique key identifying a field.
    *
    * @param PhabricatorCustomField Field to identify.
    * @return string Unique identifier, suitable for use as an input name.
    */
   public function getKeyForCustomField(PhabricatorCustomField $field) {
     return 'custom:'.$field->getFieldIndex();
   }
 
 
   /**
    * Add inputs to an application search form so the user can query on custom
    * fields.
    *
    * @param AphrontFormView Form to update.
    * @param PhabricatorSavedQuery Values to prefill.
    * @return void
    */
   protected function appendCustomFieldsToForm(
     AphrontFormView $form,
     PhabricatorSavedQuery $saved) {
 
     $list = $this->getCustomFieldList();
     if (!$list) {
       return;
     }
 
     $phids = array();
     foreach ($list->getFields() as $field) {
       $key = $this->getKeyForCustomField($field);
       $value = $saved->getParameter($key);
       $phids[$key] = $field->getRequiredHandlePHIDsForApplicationSearch($value);
     }
     $all_phids = array_mergev($phids);
 
     $handles = array();
     if ($all_phids) {
       $handles = id(new PhabricatorHandleQuery())
         ->setViewer($this->requireViewer())
         ->withPHIDs($all_phids)
         ->execute();
     }
 
     foreach ($list->getFields() as $field) {
       $key = $this->getKeyForCustomField($field);
       $value = $saved->getParameter($key);
       $field->appendToApplicationSearchForm(
         $this,
         $form,
         $value,
         array_select_keys($handles, $phids[$key]));
     }
   }
 
 }
diff --git a/src/applications/settings/storage/PhabricatorUserPreferences.php b/src/applications/settings/storage/PhabricatorUserPreferences.php
index e07a045dc2..3248dc8b2a 100644
--- a/src/applications/settings/storage/PhabricatorUserPreferences.php
+++ b/src/applications/settings/storage/PhabricatorUserPreferences.php
@@ -1,111 +1,111 @@
 <?php
 
 final class PhabricatorUserPreferences extends PhabricatorUserDAO {
 
   const PREFERENCE_MONOSPACED           = 'monospaced';
   const PREFERENCE_DARK_CONSOLE         = 'dark_console';
   const PREFERENCE_EDITOR               = 'editor';
   const PREFERENCE_MULTIEDIT            = 'multiedit';
   const PREFERENCE_TITLES               = 'titles';
   const PREFERENCE_MONOSPACED_TEXTAREAS = 'monospaced-textareas';
   const PREFERENCE_TIME_FORMAT          = 'time-format';
 
   const PREFERENCE_RE_PREFIX            = 're-prefix';
   const PREFERENCE_NO_SELF_MAIL         = 'self-mail';
   const PREFERENCE_NO_MAIL              = 'no-mail';
   const PREFERENCE_MAILTAGS             = 'mailtags';
   const PREFERENCE_VARY_SUBJECT         = 'vary-subject';
   const PREFERENCE_HTML_EMAILS          = 'html-emails';
 
   const PREFERENCE_SEARCHBAR_JUMP       = 'searchbar-jump';
   const PREFERENCE_SEARCH_SHORTCUT      = 'search-shortcut';
   const PREFERENCE_SEARCH_SCOPE = 'search-scope';
 
   const PREFERENCE_DIFFUSION_BLAME      = 'diffusion-blame';
   const PREFERENCE_DIFFUSION_COLOR      = 'diffusion-color';
 
   const PREFERENCE_NAV_COLLAPSED        = 'nav-collapsed';
   const PREFERENCE_NAV_WIDTH            = 'nav-width';
   const PREFERENCE_APP_TILES            = 'app-tiles';
   const PREFERENCE_APP_PINNED           = 'app-pinned';
 
   const PREFERENCE_DIFF_UNIFIED         = 'diff-unified';
   const PREFERENCE_DIFF_FILETREE        = 'diff-filetree';
   const PREFERENCE_DIFF_GHOSTS = 'diff-ghosts';
 
   const PREFERENCE_CONPH_NOTIFICATIONS = 'conph-notifications';
   const PREFERENCE_CONPHERENCE_COLUMN = 'conpherence-column';
 
   // These are in an unusual order for historic reasons.
   const MAILTAG_PREFERENCE_NOTIFY       = 0;
   const MAILTAG_PREFERENCE_EMAIL        = 1;
   const MAILTAG_PREFERENCE_IGNORE       = 2;
 
   protected $userPHID;
   protected $preferences = array();
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_SERIALIZATION => array(
         'preferences' => self::SERIALIZATION_JSON,
       ),
       self::CONFIG_TIMESTAMPS => false,
       self::CONFIG_KEY_SCHEMA => array(
         'userPHID' => array(
           'columns' => array('userPHID'),
           'unique' => true,
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function getPreference($key, $default = null) {
     return idx($this->preferences, $key, $default);
   }
 
   public function setPreference($key, $value) {
     $this->preferences[$key] = $value;
     return $this;
   }
 
   public function unsetPreference($key) {
     unset($this->preferences[$key]);
     return $this;
   }
 
   public function getPinnedApplications(array $apps, PhabricatorUser $viewer) {
-    $pref_pinned = PhabricatorUserPreferences::PREFERENCE_APP_PINNED;
+    $pref_pinned = self::PREFERENCE_APP_PINNED;
     $pinned = $this->getPreference($pref_pinned);
 
     if ($pinned) {
       return $pinned;
     }
 
-    $pref_tiles = PhabricatorUserPreferences::PREFERENCE_APP_TILES;
+    $pref_tiles = self::PREFERENCE_APP_TILES;
     $tiles = $this->getPreference($pref_tiles, array());
     $full_tile = 'full';
 
     $large = array();
     foreach ($apps as $app) {
       $show = $app->isPinnedByDefault($viewer);
 
       // TODO: This is legacy stuff, clean it up eventually. This approximately
       // retains the old "tiles" preference.
       if (isset($tiles[get_class($app)])) {
         $show = ($tiles[get_class($app)] == $full_tile);
       }
 
       if ($show) {
         $large[] = get_class($app);
       }
     }
 
     return $large;
   }
 
   public static function filterMonospacedCSSRule($monospaced) {
     // Prevent the user from doing dangerous things.
     return preg_replace('/[^a-z0-9 ,".]+/i', '', $monospaced);
   }
 
 }
diff --git a/src/applications/slowvote/storage/PhabricatorSlowvoteTransaction.php b/src/applications/slowvote/storage/PhabricatorSlowvoteTransaction.php
index abb73fd588..9abf1f5801 100644
--- a/src/applications/slowvote/storage/PhabricatorSlowvoteTransaction.php
+++ b/src/applications/slowvote/storage/PhabricatorSlowvoteTransaction.php
@@ -1,156 +1,156 @@
 <?php
 
 final class PhabricatorSlowvoteTransaction
   extends PhabricatorApplicationTransaction {
 
   const TYPE_QUESTION     = 'vote:question';
   const TYPE_DESCRIPTION  = 'vote:description';
   const TYPE_RESPONSES    = 'vote:responses';
   const TYPE_SHUFFLE      = 'vote:shuffle';
   const TYPE_CLOSE        = 'vote:close';
 
   public function getApplicationName() {
     return 'slowvote';
   }
 
   public function getApplicationTransactionType() {
     return PhabricatorSlowvotePollPHIDType::TYPECONST;
   }
 
   public function getApplicationTransactionCommentObject() {
     return new PhabricatorSlowvoteTransactionComment();
   }
 
   public function shouldHide() {
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
-      case PhabricatorSlowvoteTransaction::TYPE_DESCRIPTION:
-      case PhabricatorSlowvoteTransaction::TYPE_RESPONSES:
-      case PhabricatorSlowvoteTransaction::TYPE_SHUFFLE:
-      case PhabricatorSlowvoteTransaction::TYPE_CLOSE:
+      case self::TYPE_DESCRIPTION:
+      case self::TYPE_RESPONSES:
+      case self::TYPE_SHUFFLE:
+      case self::TYPE_CLOSE:
         return ($old === null);
     }
 
     return parent::shouldHide();
   }
 
   public function getTitle() {
     $author_phid = $this->getAuthorPHID();
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
-      case PhabricatorSlowvoteTransaction::TYPE_QUESTION:
+      case self::TYPE_QUESTION:
         if ($old === null) {
           return pht(
             '%s created this poll.',
             $this->renderHandleLink($author_phid));
         } else {
           return pht(
             '%s changed the poll question from "%s" to "%s".',
             $this->renderHandleLink($author_phid),
             $old,
             $new);
         }
         break;
-      case PhabricatorSlowvoteTransaction::TYPE_DESCRIPTION:
+      case self::TYPE_DESCRIPTION:
         return pht(
           '%s updated the description for this poll.',
           $this->renderHandleLink($author_phid));
-      case PhabricatorSlowvoteTransaction::TYPE_RESPONSES:
+      case self::TYPE_RESPONSES:
         // TODO: This could be more detailed
         return pht(
           '%s changed who can see the responses.',
           $this->renderHandleLink($author_phid));
-      case PhabricatorSlowvoteTransaction::TYPE_SHUFFLE:
+      case self::TYPE_SHUFFLE:
         if ($new) {
           return pht(
             '%s made poll responses appear in a random order.',
             $this->renderHandleLink($author_phid));
         } else {
           return pht(
             '%s made poll responses appear in a fixed order.',
             $this->renderHandleLink($author_phid));
         }
         break;
-      case PhabricatorSlowvoteTransaction::TYPE_CLOSE:
+      case self::TYPE_CLOSE:
         if ($new) {
           return pht(
             '%s closed this poll.',
             $this->renderHandleLink($author_phid));
         } else {
           return pht(
             '%s reopened this poll.',
             $this->renderHandleLink($author_phid));
         }
 
         break;
     }
 
     return parent::getTitle();
   }
 
   public function getIcon() {
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
-      case PhabricatorSlowvoteTransaction::TYPE_QUESTION:
+      case self::TYPE_QUESTION:
         if ($old === null) {
           return 'fa-plus';
         } else {
           return 'fa-pencil';
         }
-      case PhabricatorSlowvoteTransaction::TYPE_DESCRIPTION:
-      case PhabricatorSlowvoteTransaction::TYPE_RESPONSES:
+      case self::TYPE_DESCRIPTION:
+      case self::TYPE_RESPONSES:
         return 'fa-pencil';
-      case PhabricatorSlowvoteTransaction::TYPE_SHUFFLE:
+      case self::TYPE_SHUFFLE:
         return 'fa-refresh';
-      case PhabricatorSlowvoteTransaction::TYPE_CLOSE:
+      case self::TYPE_CLOSE:
         if ($new) {
           return 'fa-ban';
         } else {
           return 'fa-pencil';
         }
     }
 
     return parent::getIcon();
   }
 
 
   public function getColor() {
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
-      case PhabricatorSlowvoteTransaction::TYPE_QUESTION:
-      case PhabricatorSlowvoteTransaction::TYPE_DESCRIPTION:
-      case PhabricatorSlowvoteTransaction::TYPE_RESPONSES:
-      case PhabricatorSlowvoteTransaction::TYPE_SHUFFLE:
-      case PhabricatorSlowvoteTransaction::TYPE_CLOSE:
+      case self::TYPE_QUESTION:
+      case self::TYPE_DESCRIPTION:
+      case self::TYPE_RESPONSES:
+      case self::TYPE_SHUFFLE:
+      case self::TYPE_CLOSE:
         return PhabricatorTransactions::COLOR_BLUE;
     }
 
     return parent::getColor();
   }
 
   public function hasChangeDetails() {
     switch ($this->getTransactionType()) {
-      case PhabricatorSlowvoteTransaction::TYPE_DESCRIPTION:
+      case self::TYPE_DESCRIPTION:
         return true;
     }
     return parent::hasChangeDetails();
   }
 
   public function renderChangeDetails(PhabricatorUser $viewer) {
     return $this->renderTextCorpusChangeDetails(
       $viewer,
       $this->getOldValue(),
       $this->getNewValue());
   }
 
 
 }
diff --git a/src/applications/slowvote/view/SlowvoteEmbedView.php b/src/applications/slowvote/view/SlowvoteEmbedView.php
index 79e4c0bb1d..4ff920bb7d 100644
--- a/src/applications/slowvote/view/SlowvoteEmbedView.php
+++ b/src/applications/slowvote/view/SlowvoteEmbedView.php
@@ -1,362 +1,362 @@
 <?php
 
 final class SlowvoteEmbedView extends AphrontView {
 
   private $poll;
   private $handles;
   private $headless;
 
   public function setHeadless($headless) {
     $this->headless = $headless;
     return $this;
   }
 
   public function setPoll(PhabricatorSlowvotePoll $poll) {
     $this->poll = $poll;
     return $this;
   }
 
   public function getPoll() {
     return $this->poll;
   }
 
   public function render() {
     if (!$this->poll) {
-      throw new Exception('Call setPoll() before render()!');
+      throw new PhutilInvalidStateException('setPoll');
     }
 
     $poll = $this->poll;
 
     $phids = array();
     foreach ($poll->getChoices() as $choice) {
       $phids[] = $choice->getAuthorPHID();
     }
     $phids[] = $poll->getAuthorPHID();
 
     $this->handles = id(new PhabricatorHandleQuery())
       ->setViewer($this->getUser())
       ->withPHIDs($phids)
       ->execute();
 
     $options = $poll->getOptions();
 
     if ($poll->getShuffle()) {
       shuffle($options);
     }
 
     require_celerity_resource('phabricator-slowvote-css');
     require_celerity_resource('javelin-behavior-slowvote-embed');
 
     $config = array(
       'pollID' => $poll->getID(),
     );
     Javelin::initBehavior('slowvote-embed', $config);
 
     $user_choices = $poll->getViewerChoices($this->getUser());
     $user_choices = mpull($user_choices, 'getOptionID', 'getOptionID');
 
     $out = array();
     foreach ($options as $option) {
       $is_selected = isset($user_choices[$option->getID()]);
       $out[] = $this->renderLabel($option, $is_selected);
     }
 
     $link_to_slowvote = phutil_tag(
       'a',
       array(
         'href' => '/V'.$poll->getID(),
       ),
       $poll->getQuestion());
 
     if ($this->headless) {
       $header = null;
     } else {
       $header = phutil_tag(
         'div',
         array(
           'class' => 'slowvote-header',
         ),
         phutil_tag(
           'div',
           array(
             'class' => 'slowvote-header-content',
           ),
           array(
             'V'.$poll->getID(),
             ' ',
             $link_to_slowvote,
           )));
 
       $description = null;
       if ($poll->getDescription()) {
         $description = PhabricatorMarkupEngine::renderOneObject(
           id(new PhabricatorMarkupOneOff())->setContent(
             $poll->getDescription()),
           'default',
           $this->getUser());
         $description = phutil_tag(
           'div',
           array(
             'class' => 'slowvote-description',
           ),
           $description);
       }
 
       $header = array(
         $header,
         $description,
       );
     }
 
     $vis = $poll->getResponseVisibility();
     if ($this->areResultsVisible()) {
       if ($vis == PhabricatorSlowvotePoll::RESPONSES_OWNER) {
         $quip = pht('Only you can see the results.');
       } else {
         $quip = pht('Voting improves cardiovascular endurance.');
       }
     } else if ($vis == PhabricatorSlowvotePoll::RESPONSES_VOTERS) {
       $quip = pht('You must vote to see the results.');
     } else if ($vis == PhabricatorSlowvotePoll::RESPONSES_OWNER) {
       $quip = pht('Only the author can see the results.');
     }
 
     $hint = phutil_tag(
       'span',
       array(
         'class' => 'slowvote-hint',
       ),
       $quip);
 
     if ($poll->getIsClosed()) {
       $submit = null;
     } else {
       $submit = phutil_tag(
         'div',
         array(
           'class' => 'slowvote-footer',
         ),
         phutil_tag(
           'div',
           array(
             'class' => 'slowvote-footer-content',
           ),
           array(
             $hint,
             phutil_tag(
               'button',
               array(
               ),
               pht('Engage in Deliberations')),
           )));
     }
 
     $body = phabricator_form(
       $this->getUser(),
       array(
         'action'  => '/vote/'.$poll->getID().'/',
         'method'  => 'POST',
         'class'   => 'slowvote-body',
       ),
       array(
         phutil_tag(
           'div',
           array(
             'class' => 'slowvote-body-content',
           ),
           $out),
         $submit,
       ));
 
     return javelin_tag(
       'div',
       array(
         'class' => 'slowvote-embed',
         'sigil' => 'slowvote-embed',
         'meta' => array(
           'pollID' => $poll->getID(),
         ),
       ),
       array($header, $body));
   }
 
   private function renderLabel(PhabricatorSlowvoteOption $option, $selected) {
     $classes = array();
     $classes[] = 'slowvote-option-label';
 
     $status = $this->renderStatus($option);
     $voters = $this->renderVoters($option);
 
     return phutil_tag(
       'div',
       array(
         'class' => 'slowvote-option-label-group',
       ),
       array(
         phutil_tag(
           'label',
           array(
             'class' => implode(' ', $classes),
           ),
           array(
             phutil_tag(
               'div',
               array(
                 'class' => 'slowvote-control-offset',
               ),
               $option->getName()),
             $this->renderBar($option),
             phutil_tag(
               'div',
               array(
                 'class' => 'slowvote-above-the-bar',
               ),
               array(
                 $this->renderControl($option, $selected),
               )),
           )),
         $status,
         $voters,
       ));
   }
 
   private function renderBar(PhabricatorSlowvoteOption $option) {
     if (!$this->areResultsVisible()) {
       return null;
     }
 
     $poll = $this->getPoll();
 
     $choices = mgroup($poll->getChoices(), 'getOptionID');
     $choices = count(idx($choices, $option->getID(), array()));
     $count = count(mgroup($poll->getChoices(), 'getAuthorPHID'));
 
     return phutil_tag(
       'div',
       array(
         'class' => 'slowvote-bar',
         'style' => sprintf(
           'width: %.1f%%;',
           $count ? 100 * ($choices / $count) : 0),
       ),
       array(
         phutil_tag(
           'div',
           array(
             'class' => 'slowvote-control-offset',
           ),
           $option->getName()),
       ));
   }
 
   private function renderControl(PhabricatorSlowvoteOption $option, $selected) {
     $types = array(
       PhabricatorSlowvotePoll::METHOD_PLURALITY => 'radio',
       PhabricatorSlowvotePoll::METHOD_APPROVAL => 'checkbox',
     );
 
     $closed = $this->getPoll()->getIsClosed();
 
     return phutil_tag(
       'input',
       array(
         'type' => idx($types, $this->getPoll()->getMethod()),
         'name' => 'vote[]',
         'value' => $option->getID(),
         'checked' => ($selected ? 'checked' : null),
         'disabled' => ($closed ? 'disabled' : null),
       ));
   }
 
   private function renderVoters(PhabricatorSlowvoteOption $option) {
     if (!$this->areResultsVisible()) {
       return null;
     }
 
     $poll = $this->getPoll();
 
     $choices = mgroup($poll->getChoices(), 'getOptionID');
     $choices = idx($choices, $option->getID(), array());
 
     if (!$choices) {
       return null;
     }
 
     $handles = $this->handles;
     $authors = mpull($choices, 'getAuthorPHID', 'getAuthorPHID');
 
     $viewer_phid = $this->getUser()->getPHID();
 
     // Put the viewer first if they've voted for this option.
     $authors = array_select_keys($authors, array($viewer_phid))
              + $authors;
 
     $voters = array();
     foreach ($authors as $author_phid) {
       $handle = $handles[$author_phid];
 
       $voters[] = javelin_tag(
         'div',
         array(
           'class' => 'slowvote-voter',
           'style' => 'background-image: url('.$handle->getImageURI().')',
           'sigil' => 'has-tooltip',
           'meta' => array(
             'tip' => $handle->getName(),
           ),
         ));
     }
 
     return phutil_tag(
       'div',
       array(
         'class' => 'slowvote-voters',
       ),
       $voters);
   }
 
   private function renderStatus(PhabricatorSlowvoteOption $option) {
     if (!$this->areResultsVisible()) {
       return null;
     }
 
     $poll = $this->getPoll();
 
     $choices = mgroup($poll->getChoices(), 'getOptionID');
     $choices = count(idx($choices, $option->getID(), array()));
     $count = count(mgroup($poll->getChoices(), 'getAuthorPHID'));
 
     $percent = sprintf('%d%%', $count ? 100 * $choices / $count : 0);
 
     switch ($poll->getMethod()) {
       case PhabricatorSlowvotePoll::METHOD_PLURALITY:
         $status = pht('%s (%d / %d)', $percent, $choices, $count);
         break;
       case PhabricatorSlowvotePoll::METHOD_APPROVAL:
         $status = pht('%s Approval (%d / %d)', $percent, $choices, $count);
         break;
     }
 
     return phutil_tag(
       'div',
       array(
         'class' => 'slowvote-status',
       ),
       $status);
   }
 
   private function areResultsVisible() {
     $poll = $this->getPoll();
 
     $vis = $poll->getResponseVisibility();
     if ($vis == PhabricatorSlowvotePoll::RESPONSES_VISIBLE) {
       return true;
     } else if ($vis == PhabricatorSlowvotePoll::RESPONSES_OWNER) {
       return ($poll->getAuthorPHID() == $this->getUser()->getPHID());
     } else {
       $choices = mgroup($poll->getChoices(), 'getAuthorPHID');
       return (bool)idx($choices, $this->getUser()->getPHID());
     }
   }
 
 }
diff --git a/src/applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php b/src/applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php
index 498b01a04f..3ee4b5f6e8 100644
--- a/src/applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php
+++ b/src/applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php
@@ -1,102 +1,102 @@
 <?php
 
 final class PhabricatorSubscriptionsEditor extends PhabricatorEditor {
 
   private $object;
 
   private $explicitSubscribePHIDs = array();
   private $implicitSubscribePHIDs = array();
   private $unsubscribePHIDs       = array();
 
   public function setObject(PhabricatorSubscribableInterface $object) {
     $this->object = $object;
     return $this;
   }
 
   /**
    * Add explicit subscribers. These subscribers have explicitly subscribed
    * (or been subscribed) to the object, and will be added even if they
    * had previously unsubscribed.
    *
    * @param list<phid>  List of PHIDs to explicitly subscribe.
    * @return this
    */
   public function subscribeExplicit(array $phids) {
     $this->explicitSubscribePHIDs += array_fill_keys($phids, true);
     return $this;
   }
 
 
   /**
    * Add implicit subscribers. These subscribers have taken some action which
    * implicitly subscribes them (e.g., adding a comment) but it will be
    * suppressed if they've previously unsubscribed from the object.
    *
    * @param list<phid>  List of PHIDs to implicitly subscribe.
    * @return this
    */
   public function subscribeImplicit(array $phids) {
     $this->implicitSubscribePHIDs += array_fill_keys($phids, true);
     return $this;
   }
 
 
   /**
    * Unsubscribe PHIDs and mark them as unsubscribed, so implicit subscriptions
    * will not resubscribe them.
    *
    * @param list<phid>  List of PHIDs to unsubscribe.
    * @return this
    */
   public function unsubscribe(array $phids) {
     $this->unsubscribePHIDs += array_fill_keys($phids, true);
     return $this;
   }
 
 
   public function save() {
     if (!$this->object) {
-      throw new Exception('Call setObject() before save()!');
+      throw new PhutilInvalidStateException('setObject');
     }
     $actor = $this->requireActor();
 
     $src = $this->object->getPHID();
 
     if ($this->implicitSubscribePHIDs) {
       $unsub = PhabricatorEdgeQuery::loadDestinationPHIDs(
         $src,
         PhabricatorObjectHasUnsubscriberEdgeType::EDGECONST);
       $unsub = array_fill_keys($unsub, true);
       $this->implicitSubscribePHIDs = array_diff_key(
         $this->implicitSubscribePHIDs,
         $unsub);
     }
 
     $add = $this->implicitSubscribePHIDs + $this->explicitSubscribePHIDs;
     $del = $this->unsubscribePHIDs;
 
     // If a PHID is marked for both subscription and unsubscription, treat
     // unsubscription as the stronger action.
     $add = array_diff_key($add, $del);
 
     if ($add || $del) {
       $u_type = PhabricatorObjectHasUnsubscriberEdgeType::EDGECONST;
       $s_type = PhabricatorObjectHasSubscriberEdgeType::EDGECONST;
 
       $editor = new PhabricatorEdgeEditor();
 
       foreach ($add as $phid => $ignored) {
         $editor->removeEdge($src, $u_type, $phid);
         $editor->addEdge($src, $s_type, $phid);
       }
 
       foreach ($del as $phid => $ignored) {
         $editor->removeEdge($src, $s_type, $phid);
         $editor->addEdge($src, $u_type, $phid);
       }
 
       $editor->save();
     }
   }
 
 }
diff --git a/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php b/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php
index 380c8380ce..fb7c32892f 100644
--- a/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php
+++ b/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php
@@ -1,128 +1,128 @@
 <?php
 
 final class PhabricatorSubscriptionsUIEventListener
   extends PhabricatorEventListener {
 
   public function register() {
     $this->listen(PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS);
     $this->listen(PhabricatorEventType::TYPE_UI_WILLRENDERPROPERTIES);
   }
 
   public function handleEvent(PhutilEvent $event) {
     switch ($event->getType()) {
       case PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS:
         $this->handleActionEvent($event);
         break;
       case PhabricatorEventType::TYPE_UI_WILLRENDERPROPERTIES:
         $this->handlePropertyEvent($event);
         break;
     }
   }
 
   private function handleActionEvent($event) {
     $user = $event->getUser();
+    $user_phid = $user->getPHID();
     $object = $event->getValue('object');
 
     if (!$object || !$object->getPHID()) {
       // No object, or the object has no PHID yet. No way to subscribe.
       return;
     }
 
     if (!($object instanceof PhabricatorSubscribableInterface)) {
       // This object isn't subscribable.
       return;
     }
 
-    if (!$object->shouldAllowSubscription($user->getPHID())) {
+    if (!$object->shouldAllowSubscription($user_phid)) {
       // This object doesn't allow the viewer to subscribe.
       return;
     }
 
-    if ($object->isAutomaticallySubscribed($user->getPHID())) {
+    if ($user_phid && $object->isAutomaticallySubscribed($user_phid)) {
       $sub_action = id(new PhabricatorActionView())
         ->setWorkflow(true)
         ->setDisabled(true)
         ->setRenderAsForm(true)
         ->setHref('/subscriptions/add/'.$object->getPHID().'/')
         ->setName(pht('Automatically Subscribed'))
         ->setIcon('fa-check-circle lightgreytext');
     } else {
       $subscribed = false;
       if ($user->isLoggedIn()) {
         $src_phid = $object->getPHID();
-        $dst_phid = $user->getPHID();
         $edge_type = PhabricatorObjectHasSubscriberEdgeType::EDGECONST;
 
         $edges = id(new PhabricatorEdgeQuery())
           ->withSourcePHIDs(array($src_phid))
           ->withEdgeTypes(array($edge_type))
-          ->withDestinationPHIDs(array($user->getPHID()))
+          ->withDestinationPHIDs(array($user_phid))
           ->execute();
-        $subscribed = isset($edges[$src_phid][$edge_type][$dst_phid]);
+        $subscribed = isset($edges[$src_phid][$edge_type][$user_phid]);
       }
 
       if ($subscribed) {
         $sub_action = id(new PhabricatorActionView())
           ->setWorkflow(true)
           ->setRenderAsForm(true)
           ->setHref('/subscriptions/delete/'.$object->getPHID().'/')
           ->setName(pht('Unsubscribe'))
           ->setIcon('fa-minus-circle');
       } else {
         $sub_action = id(new PhabricatorActionView())
           ->setWorkflow(true)
           ->setRenderAsForm(true)
           ->setHref('/subscriptions/add/'.$object->getPHID().'/')
           ->setName(pht('Subscribe'))
           ->setIcon('fa-plus-circle');
       }
 
       if (!$user->isLoggedIn()) {
         $sub_action->setDisabled(true);
       }
     }
 
     $actions = $event->getValue('actions');
     $actions[] = $sub_action;
     $event->setValue('actions', $actions);
   }
 
   private function handlePropertyEvent($event) {
     $user = $event->getUser();
     $object = $event->getValue('object');
 
     if (!$object || !$object->getPHID()) {
       // No object, or the object has no PHID yet..
       return;
     }
 
     if (!($object instanceof PhabricatorSubscribableInterface)) {
       // This object isn't subscribable.
       return;
     }
 
     if (!$object->shouldShowSubscribersProperty()) {
       // This object doesn't render subscribers in its property list.
       return;
     }
 
     $subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID(
       $object->getPHID());
     if ($subscribers) {
       $handles = id(new PhabricatorHandleQuery())
         ->setViewer($user)
         ->withPHIDs($subscribers)
         ->execute();
     } else {
       $handles = array();
     }
     $sub_view = id(new SubscriptionListStringBuilder())
       ->setObjectPHID($object->getPHID())
       ->setHandles($handles)
       ->buildPropertyString();
 
     $view = $event->getValue('view');
     $view->addProperty(pht('Subscribers'), $sub_view);
   }
 
 }
diff --git a/src/applications/system/engine/PhabricatorDestructionEngine.php b/src/applications/system/engine/PhabricatorDestructionEngine.php
index e1f1045132..b55b336052 100644
--- a/src/applications/system/engine/PhabricatorDestructionEngine.php
+++ b/src/applications/system/engine/PhabricatorDestructionEngine.php
@@ -1,100 +1,124 @@
 <?php
 
 final class PhabricatorDestructionEngine extends Phobject {
 
   private $rootLogID;
 
   public function destroyObject(PhabricatorDestructibleInterface $object) {
     $log = id(new PhabricatorSystemDestructionLog())
       ->setEpoch(time())
       ->setObjectClass(get_class($object));
 
     if ($this->rootLogID) {
       $log->setRootLogID($this->rootLogID);
     }
 
     $object_phid = null;
     if (method_exists($object, 'getPHID')) {
       try {
         $object_phid = $object->getPHID();
         $log->setObjectPHID($object_phid);
       } catch (Exception $ex) {
         // Ignore.
       }
     }
 
     if (method_exists($object, 'getMonogram')) {
       try {
         $log->setObjectMonogram($object->getMonogram());
       } catch (Exception $ex) {
         // Ignore.
       }
     }
 
     $log->save();
 
     if (!$this->rootLogID) {
       $this->rootLogID = $log->getID();
     }
 
     $object->destroyObjectPermanently($this);
 
     if ($object_phid) {
       $this->destroyEdges($object_phid);
 
       if ($object instanceof PhabricatorApplicationTransactionInterface) {
         $template = $object->getApplicationTransactionTemplate();
         $this->destroyTransactions($template, $object_phid);
       }
+
+      $this->destroyWorkerTasks($object_phid);
+      $this->destroyNotifications($object_phid);
     }
 
     // Nuke any Herald transcripts of the object, because they may contain
     // field data.
 
     // TODO: Define an interface so we don't have to do this for transactions
     // and other objects with no Herald adapters?
     $transcripts = id(new HeraldTranscript())->loadAllWhere(
       'objectPHID = %s',
       $object_phid);
     foreach ($transcripts as $transcript) {
       $transcript->destroyObjectPermanently($this);
     }
 
     // TODO: Remove stuff from search indexes?
     // TODO: PhabricatorFlaggableInterface
     // TODO: PhabricatorTokenReceiverInterface
   }
 
   private function destroyEdges($src_phid) {
     try {
       $edges = id(new PhabricatorEdgeQuery())
         ->withSourcePHIDs(array($src_phid))
         ->execute();
     } catch (Exception $ex) {
       // This is (presumably) a "no edges for this PHID type" exception.
       return;
     }
 
     $editor = new PhabricatorEdgeEditor();
     foreach ($edges as $type => $type_edges) {
       foreach ($type_edges as $src => $src_edges) {
         foreach ($src_edges as $dst => $edge) {
           $editor->removeEdge($edge['src'], $edge['type'], $edge['dst']);
         }
       }
     }
     $editor->save();
   }
 
   private function destroyTransactions(
     PhabricatorApplicationTransaction $template,
     $object_phid) {
 
     $xactions = $template->loadAllWhere('objectPHID = %s', $object_phid);
     foreach ($xactions as $xaction) {
       $this->destroyObject($xaction);
     }
+  }
+
+  private function destroyWorkerTasks($object_phid) {
+    $tasks = id(new PhabricatorWorkerActiveTask())->loadAllWhere(
+      'objectPHID = %s',
+      $object_phid);
 
+    foreach ($tasks as $task) {
+      $task->archiveTask(
+        PhabricatorWorkerArchiveTask::RESULT_CANCELLED,
+        0);
+    }
+  }
+
+  private function destroyNotifications($object_phid) {
+    $notifications = id(new PhabricatorFeedStoryNotification())->loadAllWhere(
+      'primaryObjectPHID = %s',
+      $object_phid);
+
+    foreach ($notifications as $notification) {
+      $notification->delete();
+    }
   }
 
 }
diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php
index ee09a76eda..23b77fbd1b 100644
--- a/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php
+++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php
@@ -1,159 +1,158 @@
 <?php
 
 final class PhabricatorApplicationTransactionCommentEditor
   extends PhabricatorEditor {
 
   private $contentSource;
   private $actingAsPHID;
 
   public function setActingAsPHID($acting_as_phid) {
     $this->actingAsPHID = $acting_as_phid;
     return $this;
   }
 
   public function getActingAsPHID() {
     if ($this->actingAsPHID) {
       return $this->actingAsPHID;
     }
     return $this->getActor()->getPHID();
   }
 
   public function setContentSource(PhabricatorContentSource $content_source) {
     $this->contentSource = $content_source;
     return $this;
   }
 
   public function getContentSource() {
     return $this->contentSource;
   }
 
   /**
    * Edit a transaction's comment. This method effects the required create,
    * update or delete to set the transaction's comment to the provided comment.
    */
   public function applyEdit(
     PhabricatorApplicationTransaction $xaction,
     PhabricatorApplicationTransactionComment $comment) {
 
     $this->validateEdit($xaction, $comment);
 
     $actor = $this->requireActor();
 
     $comment->setContentSource($this->getContentSource());
     $comment->setAuthorPHID($this->getActingAsPHID());
 
     // TODO: This needs to be more sophisticated once we have meta-policies.
     $comment->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC);
     $comment->setEditPolicy($this->getActingAsPHID());
 
     $file_phids = PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles(
       $actor,
       array(
         $comment->getContent(),
       ));
 
     $xaction->openTransaction();
       $xaction->beginReadLocking();
         if ($xaction->getID()) {
           $xaction->reload();
         }
 
         $new_version = $xaction->getCommentVersion() + 1;
 
         $comment->setCommentVersion($new_version);
         $comment->setTransactionPHID($xaction->getPHID());
         $comment->save();
 
         $xaction->setCommentVersion($new_version);
         $xaction->setCommentPHID($comment->getPHID());
         $xaction->setViewPolicy($comment->getViewPolicy());
         $xaction->setEditPolicy($comment->getEditPolicy());
         $xaction->save();
         $xaction->attachComment($comment);
 
         // For comment edits, we need to make sure there are no automagical
         // transactions like adding mentions or projects.
         if ($new_version > 1) {
           $object = id(new PhabricatorObjectQuery())
             ->withPHIDs(array($xaction->getObjectPHID()))
             ->setViewer($this->getActor())
             ->executeOne();
           if ($object &&
               $object instanceof PhabricatorApplicationTransactionInterface) {
             $editor = $object->getApplicationTransactionEditor();
             $editor->setActor($this->getActor());
             $support_xactions = $editor->getExpandedSupportTransactions(
               $object,
               $xaction);
             if ($support_xactions) {
               $editor
                 ->setContentSource($this->getContentSource())
                 ->setContinueOnNoEffect(true)
                 ->applyTransactions($object, $support_xactions);
             }
           }
         }
       $xaction->endReadLocking();
     $xaction->saveTransaction();
 
     // Add links to any files newly referenced by the edit.
     if ($file_phids) {
       $editor = new PhabricatorEdgeEditor();
       foreach ($file_phids as $file_phid) {
         $editor->addEdge(
           $xaction->getObjectPHID(),
           PhabricatorObjectHasFileEdgeType::EDGECONST ,
           $file_phid);
       }
       $editor->save();
     }
 
     return $this;
   }
 
   /**
    * Validate that the edit is permissible, and the actor has permission to
    * perform it.
    */
   private function validateEdit(
     PhabricatorApplicationTransaction $xaction,
     PhabricatorApplicationTransactionComment $comment) {
 
     if (!$xaction->getPHID()) {
       throw new Exception(
         'Transaction must have a PHID before calling applyEdit()!');
     }
 
     $type_comment = PhabricatorTransactions::TYPE_COMMENT;
     if ($xaction->getTransactionType() == $type_comment) {
       if ($comment->getPHID()) {
         throw new Exception(
           'Transaction comment must not yet have a PHID!');
       }
     }
 
     if (!$this->getContentSource()) {
-      throw new Exception(
-        'Call setContentSource() before applyEdit()!');
+      throw new PhutilInvalidStateException('applyEdit');
     }
 
     $actor = $this->requireActor();
 
     PhabricatorPolicyFilter::requireCapability(
       $actor,
       $xaction,
       PhabricatorPolicyCapability::CAN_VIEW);
 
     if ($comment->getIsRemoved() && $actor->getIsAdmin()) {
       // NOTE: Administrators can remove comments by any user, and don't need
       // to pass the edit check.
     } else {
       PhabricatorPolicyFilter::requireCapability(
         $actor,
         $xaction,
         PhabricatorPolicyCapability::CAN_EDIT);
     }
   }
 
 
 }
diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
index c28fb135ee..dfd4bba286 100644
--- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
+++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
@@ -1,2676 +1,2682 @@
 <?php
 
 /**
  * @task mail   Sending Mail
  * @task feed   Publishing Feed Stories
  * @task search Search Index
  * @task files  Integration with Files
  */
 abstract class PhabricatorApplicationTransactionEditor
   extends PhabricatorEditor {
 
   private $contentSource;
   private $object;
   private $xactions;
 
   private $isNewObject;
   private $mentionedPHIDs;
   private $continueOnNoEffect;
   private $continueOnMissingFields;
   private $parentMessageID;
   private $heraldAdapter;
   private $heraldTranscript;
   private $subscribers;
   private $unmentionablePHIDMap = array();
   private $applicationEmail;
 
   private $isPreview;
   private $isHeraldEditor;
   private $isInverseEdgeEditor;
   private $actingAsPHID;
   private $disableEmail;
 
 
   /**
    * Get the class name for the application this editor is a part of.
    *
    * Uninstalling the application will disable the editor.
    *
    * @return string Editor's application class name.
    */
   abstract public function getEditorApplicationClass();
 
 
   /**
    * Get a description of the objects this editor edits, like "Differential
    * Revisions".
    *
    * @return string Human readable description of edited objects.
    */
   abstract public function getEditorObjectsDescription();
 
 
   public function setActingAsPHID($acting_as_phid) {
     $this->actingAsPHID = $acting_as_phid;
     return $this;
   }
 
   public function getActingAsPHID() {
     if ($this->actingAsPHID) {
       return $this->actingAsPHID;
     }
     return $this->getActor()->getPHID();
   }
 
 
   /**
    * When the editor tries to apply transactions that have no effect, should
    * it raise an exception (default) or drop them and continue?
    *
    * Generally, you will set this flag for edits coming from "Edit" interfaces,
    * and leave it cleared for edits coming from "Comment" interfaces, so the
    * user will get a useful error if they try to submit a comment that does
    * nothing (e.g., empty comment with a status change that has already been
    * performed by another user).
    *
    * @param bool  True to drop transactions without effect and continue.
    * @return this
    */
   public function setContinueOnNoEffect($continue) {
     $this->continueOnNoEffect = $continue;
     return $this;
   }
 
   public function getContinueOnNoEffect() {
     return $this->continueOnNoEffect;
   }
 
 
   /**
    * When the editor tries to apply transactions which don't populate all of
    * an object's required fields, should it raise an exception (default) or
    * drop them and continue?
    *
    * For example, if a user adds a new required custom field (like "Severity")
    * to a task, all existing tasks won't have it populated. When users
    * manually edit existing tasks, it's usually desirable to have them provide
    * a severity. However, other operations (like batch editing just the
    * owner of a task) will fail by default.
    *
    * By setting this flag for edit operations which apply to specific fields
    * (like the priority, batch, and merge editors in Maniphest), these
    * operations can continue to function even if an object is outdated.
    *
    * @param bool  True to continue when transactions don't completely satisfy
    *              all required fields.
    * @return this
    */
   public function setContinueOnMissingFields($continue_on_missing_fields) {
     $this->continueOnMissingFields = $continue_on_missing_fields;
     return $this;
   }
 
   public function getContinueOnMissingFields() {
     return $this->continueOnMissingFields;
   }
 
 
   /**
    * Not strictly necessary, but reply handlers ideally set this value to
    * make email threading work better.
    */
   public function setParentMessageID($parent_message_id) {
     $this->parentMessageID = $parent_message_id;
     return $this;
   }
   public function getParentMessageID() {
     return $this->parentMessageID;
   }
 
   public function getIsNewObject() {
     return $this->isNewObject;
   }
 
   protected function getMentionedPHIDs() {
     return $this->mentionedPHIDs;
   }
 
   public function setIsPreview($is_preview) {
     $this->isPreview = $is_preview;
     return $this;
   }
 
   public function getIsPreview() {
     return $this->isPreview;
   }
 
   public function setIsInverseEdgeEditor($is_inverse_edge_editor) {
     $this->isInverseEdgeEditor = $is_inverse_edge_editor;
     return $this;
   }
 
   public function getIsInverseEdgeEditor() {
     return $this->isInverseEdgeEditor;
   }
 
   public function setIsHeraldEditor($is_herald_editor) {
     $this->isHeraldEditor = $is_herald_editor;
     return $this;
   }
 
   public function getIsHeraldEditor() {
     return $this->isHeraldEditor;
   }
 
   /**
    * Prevent this editor from generating email when applying transactions.
    *
    * @param bool  True to disable email.
    * @return this
    */
   public function setDisableEmail($disable_email) {
     $this->disableEmail = $disable_email;
     return $this;
   }
 
   public function getDisableEmail() {
     return $this->disableEmail;
   }
 
   public function setUnmentionablePHIDMap(array $map) {
     $this->unmentionablePHIDMap = $map;
     return $this;
   }
 
   public function getUnmentionablePHIDMap() {
     return $this->unmentionablePHIDMap;
   }
 
   protected function shouldEnableMentions(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return true;
   }
 
   public function setApplicationEmail(
     PhabricatorMetaMTAApplicationEmail $email) {
     $this->applicationEmail = $email;
     return $this;
   }
 
   public function getApplicationEmail() {
     return $this->applicationEmail;
   }
 
   public function getTransactionTypes() {
     $types = array();
 
     if ($this->object instanceof PhabricatorSubscribableInterface) {
       $types[] = PhabricatorTransactions::TYPE_SUBSCRIBERS;
     }
 
     if ($this->object instanceof PhabricatorCustomFieldInterface) {
       $types[] = PhabricatorTransactions::TYPE_CUSTOMFIELD;
     }
 
     if ($this->object instanceof HarbormasterBuildableInterface) {
       $types[] = PhabricatorTransactions::TYPE_BUILDABLE;
     }
 
     if ($this->object instanceof PhabricatorTokenReceiverInterface) {
       $types[] = PhabricatorTransactions::TYPE_TOKEN;
     }
 
     if ($this->object instanceof PhabricatorProjectInterface ||
         $this->object instanceof PhabricatorMentionableInterface) {
       $types[] = PhabricatorTransactions::TYPE_EDGE;
     }
 
     return $types;
   }
 
   private function adjustTransactionValues(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     if ($xaction->shouldGenerateOldValue()) {
       $old = $this->getTransactionOldValue($object, $xaction);
       $xaction->setOldValue($old);
     }
 
     $new = $this->getTransactionNewValue($object, $xaction);
     $xaction->setNewValue($new);
   }
 
   private function getTransactionOldValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         return array_values($this->subscribers);
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
         return $object->getViewPolicy();
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         return $object->getEditPolicy();
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
         return $object->getJoinPolicy();
       case PhabricatorTransactions::TYPE_EDGE:
         $edge_type = $xaction->getMetadataValue('edge:type');
         if (!$edge_type) {
           throw new Exception("Edge transaction has no 'edge:type'!");
         }
 
         $old_edges = array();
         if ($object->getPHID()) {
           $edge_src = $object->getPHID();
 
           $old_edges = id(new PhabricatorEdgeQuery())
             ->withSourcePHIDs(array($edge_src))
             ->withEdgeTypes(array($edge_type))
             ->needEdgeData(true)
             ->execute();
 
           $old_edges = $old_edges[$edge_src][$edge_type];
         }
         return $old_edges;
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         // NOTE: Custom fields have their old value pre-populated when they are
         // built by PhabricatorCustomFieldList.
         return $xaction->getOldValue();
       case PhabricatorTransactions::TYPE_COMMENT:
         return null;
       default:
         return $this->getCustomTransactionOldValue($object, $xaction);
     }
   }
 
   private function getTransactionNewValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         return $this->getPHIDTransactionNewValue($xaction);
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
       case PhabricatorTransactions::TYPE_BUILDABLE:
       case PhabricatorTransactions::TYPE_TOKEN:
       case PhabricatorTransactions::TYPE_INLINESTATE:
         return $xaction->getNewValue();
       case PhabricatorTransactions::TYPE_EDGE:
         return $this->getEdgeTransactionNewValue($xaction);
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getCustomFieldForTransaction($object, $xaction);
         return $field->getNewValueFromApplicationTransactions($xaction);
       case PhabricatorTransactions::TYPE_COMMENT:
         return null;
       default:
         return $this->getCustomTransactionNewValue($object, $xaction);
     }
   }
 
   protected function getCustomTransactionOldValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     throw new Exception('Capability not supported!');
   }
 
   protected function getCustomTransactionNewValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     throw new Exception('Capability not supported!');
   }
 
   protected function transactionHasEffect(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         return $xaction->hasComment();
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getCustomFieldForTransaction($object, $xaction);
         return $field->getApplicationTransactionHasEffect($xaction);
       case PhabricatorTransactions::TYPE_EDGE:
         // A straight value comparison here doesn't always get the right
         // result, because newly added edges aren't fully populated. Instead,
         // compare the changes in a more granular way.
         $old = $xaction->getOldValue();
         $new = $xaction->getNewValue();
 
         $old_dst = array_keys($old);
         $new_dst = array_keys($new);
 
         // NOTE: For now, we don't consider edge reordering to be a change.
         // We have very few order-dependent edges and effectively no order
         // oriented UI. This might change in the future.
         sort($old_dst);
         sort($new_dst);
 
         if ($old_dst !== $new_dst) {
           // We've added or removed edges, so this transaction definitely
           // has an effect.
           return true;
         }
 
         // We haven't added or removed edges, but we might have changed
         // edge data.
         foreach ($old as $key => $old_value) {
           $new_value = $new[$key];
           if ($old_value['data'] !== $new_value['data']) {
             return true;
           }
         }
 
         return false;
     }
 
     return ($xaction->getOldValue() !== $xaction->getNewValue());
   }
 
   protected function shouldApplyInitialEffects(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return false;
   }
 
   protected function applyInitialEffects(
     PhabricatorLiskDAO $object,
     array $xactions) {
     throw new PhutilMethodNotImplementedException();
   }
 
   private function applyInternalEffects(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_BUILDABLE:
       case PhabricatorTransactions::TYPE_TOKEN:
         return;
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
         $object->setViewPolicy($xaction->getNewValue());
         break;
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         $object->setEditPolicy($xaction->getNewValue());
         break;
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
         $object->setJoinPolicy($xaction->getNewValue());
         break;
 
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getCustomFieldForTransaction($object, $xaction);
         return $field->applyApplicationTransactionInternalEffects($xaction);
       case PhabricatorTransactions::TYPE_INLINESTATE:
         return $this->applyBuiltinInternalTransaction($object, $xaction);
     }
 
     return $this->applyCustomInternalTransaction($object, $xaction);
   }
 
   private function applyExternalEffects(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_BUILDABLE:
       case PhabricatorTransactions::TYPE_TOKEN:
         return;
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         $subeditor = id(new PhabricatorSubscriptionsEditor())
           ->setObject($object)
           ->setActor($this->requireActor());
 
         $old_map = array_fuse($xaction->getOldValue());
         $new_map = array_fuse($xaction->getNewValue());
 
         $subeditor->unsubscribe(
           array_keys(
             array_diff_key($old_map, $new_map)));
 
         $subeditor->subscribeExplicit(
           array_keys(
             array_diff_key($new_map, $old_map)));
 
         $subeditor->save();
 
         // for the rest of these edits, subscribers should include those just
         // added as well as those just removed.
         $subscribers = array_unique(array_merge(
           $this->subscribers,
           $xaction->getOldValue(),
           $xaction->getNewValue()));
         $this->subscribers = $subscribers;
 
         break;
       case PhabricatorTransactions::TYPE_EDGE:
         if ($this->getIsInverseEdgeEditor()) {
           // If we're writing an inverse edge transaction, don't actually
           // do anything. The initiating editor on the other side of the
           // transaction will take care of the edge writes.
           break;
         }
 
         $old = $xaction->getOldValue();
         $new = $xaction->getNewValue();
         $src = $object->getPHID();
         $const = $xaction->getMetadataValue('edge:type');
 
         $type = PhabricatorEdgeType::getByConstant($const);
         if ($type->shouldWriteInverseTransactions()) {
           $this->applyInverseEdgeTransactions(
             $object,
             $xaction,
             $type->getInverseEdgeConstant());
         }
 
         foreach ($new as $dst_phid => $edge) {
           $new[$dst_phid]['src'] = $src;
         }
 
         $editor = new PhabricatorEdgeEditor();
 
         foreach ($old as $dst_phid => $edge) {
           if (!empty($new[$dst_phid])) {
             if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
               continue;
             }
           }
           $editor->removeEdge($src, $const, $dst_phid);
         }
 
         foreach ($new as $dst_phid => $edge) {
           if (!empty($old[$dst_phid])) {
             if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
               continue;
             }
           }
 
           $data = array(
             'data' => $edge['data'],
           );
 
           $editor->addEdge($src, $const, $dst_phid, $data);
         }
 
         $editor->save();
         break;
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getCustomFieldForTransaction($object, $xaction);
         return $field->applyApplicationTransactionExternalEffects($xaction);
       case PhabricatorTransactions::TYPE_INLINESTATE:
         return $this->applyBuiltinExternalTransaction($object, $xaction);
     }
 
     return $this->applyCustomExternalTransaction($object, $xaction);
   }
 
   protected function applyCustomInternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     $type = $xaction->getTransactionType();
     throw new Exception(
       "Transaction type '{$type}' is missing an internal apply ".
       "implementation!");
   }
 
   protected function applyCustomExternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     $type = $xaction->getTransactionType();
     throw new Exception(
       "Transaction type '{$type}' is missing an external apply ".
       "implementation!");
   }
 
   // TODO: Write proper documentation for these hooks. These are like the
   // "applyCustom" hooks, except that implementation is optional, so you do
   // not need to handle all of the builtin transaction types. See T6403. These
   // are not completely implemented.
 
   protected function applyBuiltinInternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     return;
   }
 
   protected function applyBuiltinExternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     return;
   }
 
   /**
    * Fill in a transaction's common values, like author and content source.
    */
   protected function populateTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     $actor = $this->getActor();
 
     // TODO: This needs to be more sophisticated once we have meta-policies.
     $xaction->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC);
 
     if ($actor->isOmnipotent()) {
       $xaction->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
     } else {
       $xaction->setEditPolicy($this->getActingAsPHID());
     }
 
     $xaction->setAuthorPHID($this->getActingAsPHID());
     $xaction->setContentSource($this->getContentSource());
     $xaction->attachViewer($actor);
     $xaction->attachObject($object);
 
     if ($object->getPHID()) {
       $xaction->setObjectPHID($object->getPHID());
     }
 
     return $xaction;
   }
 
+  protected function didApplyInternalEffects(
+    PhabricatorLiskDAO $object,
+    array $xactions) {
+    return $xactions;
+  }
 
   protected function applyFinalEffects(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return $xactions;
   }
 
   public function setContentSource(PhabricatorContentSource $content_source) {
     $this->contentSource = $content_source;
     return $this;
   }
 
   public function setContentSourceFromRequest(AphrontRequest $request) {
     return $this->setContentSource(
       PhabricatorContentSource::newFromRequest($request));
   }
 
   public function setContentSourceFromConduitRequest(
     ConduitAPIRequest $request) {
 
     $content_source = PhabricatorContentSource::newForSource(
       PhabricatorContentSource::SOURCE_CONDUIT,
       array());
 
     return $this->setContentSource($content_source);
   }
 
   public function getContentSource() {
     return $this->contentSource;
   }
 
   final public function applyTransactions(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $this->object = $object;
     $this->xactions = $xactions;
     $this->isNewObject = ($object->getPHID() === null);
 
     $this->validateEditParameters($object, $xactions);
 
     $actor = $this->requireActor();
 
     // NOTE: Some transaction expansion requires that the edited object be
     // attached.
     foreach ($xactions as $xaction) {
       $xaction->attachObject($object);
       $xaction->attachViewer($actor);
     }
 
     $xactions = $this->expandTransactions($object, $xactions);
     $xactions = $this->expandSupportTransactions($object, $xactions);
     $xactions = $this->combineTransactions($xactions);
 
     foreach ($xactions as $xaction) {
       $xaction = $this->populateTransaction($object, $xaction);
     }
 
     $is_preview = $this->getIsPreview();
     $read_locking = false;
     $transaction_open = false;
 
     if (!$is_preview) {
       $errors = array();
       $type_map = mgroup($xactions, 'getTransactionType');
       foreach ($this->getTransactionTypes() as $type) {
         $type_xactions = idx($type_map, $type, array());
         $errors[] = $this->validateTransaction($object, $type, $type_xactions);
       }
 
       $errors[] = $this->validateAllTransactions($object, $xactions);
       $errors = array_mergev($errors);
 
       $continue_on_missing = $this->getContinueOnMissingFields();
       foreach ($errors as $key => $error) {
         if ($continue_on_missing && $error->getIsMissingFieldError()) {
           unset($errors[$key]);
         }
       }
 
       if ($errors) {
         throw new PhabricatorApplicationTransactionValidationException($errors);
       }
 
       $file_phids = $this->extractFilePHIDs($object, $xactions);
 
       if ($object->getID()) {
         foreach ($xactions as $xaction) {
 
           // If any of the transactions require a read lock, hold one and
           // reload the object. We need to do this fairly early so that the
           // call to `adjustTransactionValues()` (which populates old values)
           // is based on the synchronized state of the object, which may differ
           // from the state when it was originally loaded.
 
           if ($this->shouldReadLock($object, $xaction)) {
             $object->openTransaction();
             $object->beginReadLocking();
             $transaction_open = true;
             $read_locking = true;
             $object->reload();
             break;
           }
         }
       }
 
       if ($this->shouldApplyInitialEffects($object, $xactions)) {
         if (!$transaction_open) {
           $object->openTransaction();
           $transaction_open = true;
         }
       }
     }
 
     if ($this->shouldApplyInitialEffects($object, $xactions)) {
       $this->applyInitialEffects($object, $xactions);
     }
 
     foreach ($xactions as $xaction) {
       $this->adjustTransactionValues($object, $xaction);
     }
 
     $xactions = $this->filterTransactions($object, $xactions);
 
     if (!$xactions) {
       if ($read_locking) {
         $object->endReadLocking();
         $read_locking = false;
       }
       if ($transaction_open) {
         $object->killTransaction();
         $transaction_open = false;
       }
       return array();
     }
 
     // Now that we've merged, filtered, and combined transactions, check for
     // required capabilities.
     foreach ($xactions as $xaction) {
       $this->requireCapabilities($object, $xaction);
     }
 
     $xactions = $this->sortTransactions($xactions);
 
     if ($is_preview) {
       $this->loadHandles($xactions);
       return $xactions;
     }
 
     $comment_editor = id(new PhabricatorApplicationTransactionCommentEditor())
       ->setActor($actor)
       ->setActingAsPHID($this->getActingAsPHID())
       ->setContentSource($this->getContentSource());
 
     if (!$transaction_open) {
       $object->openTransaction();
     }
 
       foreach ($xactions as $xaction) {
         $this->applyInternalEffects($object, $xaction);
       }
 
+      $xactions = $this->didApplyInternalEffects($object, $xactions);
+
       $object->save();
 
       foreach ($xactions as $xaction) {
         $xaction->setObjectPHID($object->getPHID());
         if ($xaction->getComment()) {
           $xaction->setPHID($xaction->generatePHID());
           $comment_editor->applyEdit($xaction, $xaction->getComment());
         } else {
           $xaction->save();
         }
       }
 
       if ($file_phids) {
         $this->attachFiles($object, $file_phids);
       }
 
       foreach ($xactions as $xaction) {
         $this->applyExternalEffects($object, $xaction);
       }
 
       $xactions = $this->applyFinalEffects($object, $xactions);
 
       if ($read_locking) {
         $object->endReadLocking();
         $read_locking = false;
       }
 
     $object->saveTransaction();
 
     // Now that we've completely applied the core transaction set, try to apply
     // Herald rules. Herald rules are allowed to either take direct actions on
     // the database (like writing flags), or take indirect actions (like saving
     // some targets for CC when we generate mail a little later), or return
     // transactions which we'll apply normally using another Editor.
 
     // First, check if *this* is a sub-editor which is itself applying Herald
     // rules: if it is, stop working and return so we don't descend into
     // madness.
 
     // Otherwise, we're not a Herald editor, so process Herald rules (possibly
     // using a Herald editor to apply resulting transactions) and then send out
     // mail, notifications, and feed updates about everything.
 
     if ($this->getIsHeraldEditor()) {
       // We are the Herald editor, so stop work here and return the updated
       // transactions.
       return $xactions;
     } else if ($this->getIsInverseEdgeEditor()) {
       // If we're applying inverse edge transactions, don't trigger Herald.
       // From a product perspective, the current set of inverse edges (most
       // often, mentions) aren't things users would expect to trigger Herald.
       // From a technical perspective, objects loaded by the inverse editor may
       // not have enough data to execute rules. At least for now, just stop
       // Herald from executing when applying inverse edges.
     } else if ($this->shouldApplyHeraldRules($object, $xactions)) {
       // We are not the Herald editor, so try to apply Herald rules.
       $herald_xactions = $this->applyHeraldRules($object, $xactions);
 
       if ($herald_xactions) {
         $xscript_id = $this->getHeraldTranscript()->getID();
         foreach ($herald_xactions as $herald_xaction) {
           $herald_xaction->setMetadataValue('herald:transcriptID', $xscript_id);
         }
 
         // NOTE: We're acting as the omnipotent user because rules deal with
         // their own policy issues. We use a synthetic author PHID (the
         // Herald application) as the author of record, so that transactions
         // will render in a reasonable way ("Herald assigned this task ...").
         $herald_actor = PhabricatorUser::getOmnipotentUser();
         $herald_phid = id(new PhabricatorHeraldApplication())->getPHID();
 
         // TODO: It would be nice to give transactions a more specific source
         // which points at the rule which generated them. You can figure this
         // out from transcripts, but it would be cleaner if you didn't have to.
 
         $herald_source = PhabricatorContentSource::newForSource(
           PhabricatorContentSource::SOURCE_HERALD,
           array());
 
         $herald_editor = newv(get_class($this), array())
           ->setContinueOnNoEffect(true)
           ->setContinueOnMissingFields(true)
           ->setParentMessageID($this->getParentMessageID())
           ->setIsHeraldEditor(true)
           ->setActor($herald_actor)
           ->setActingAsPHID($herald_phid)
           ->setContentSource($herald_source);
 
         $herald_xactions = $herald_editor->applyTransactions(
           $object,
           $herald_xactions);
 
         // Merge the new transactions into the transaction list: we want to
         // send email and publish feed stories about them, too.
         $xactions = array_merge($xactions, $herald_xactions);
       }
     }
 
     // Before sending mail or publishing feed stories, reload the object
     // subscribers to pick up changes caused by Herald (or by other side effects
     // in various transaction phases).
     $this->loadSubscribers($object);
 
     $this->loadHandles($xactions);
 
     $mail = null;
     if (!$this->getDisableEmail()) {
       if ($this->shouldSendMail($object, $xactions)) {
         $mail = $this->sendMail($object, $xactions);
       }
     }
 
     if ($this->supportsSearch()) {
       id(new PhabricatorSearchIndexer())
         ->queueDocumentForIndexing(
           $object->getPHID(),
           $this->getSearchContextParameter($object, $xactions));
     }
 
     if ($this->shouldPublishFeedStory($object, $xactions)) {
       $mailed = array();
       if ($mail) {
         $mailed = $mail->buildRecipientList();
       }
       $this->publishFeedStory(
         $object,
         $xactions,
         $mailed);
     }
 
     $this->didApplyTransactions($xactions);
 
     if ($object instanceof PhabricatorCustomFieldInterface) {
       // Maybe this makes more sense to move into the search index itself? For
       // now I'm putting it here since I think we might end up with things that
       // need it to be up to date once the next page loads, but if we don't go
       // there we we could move it into search once search moves to the daemons.
 
       // It now happens in the search indexer as well, but the search indexer is
       // always daemonized, so the logic above still potentially holds. We could
       // possibly get rid of this. The major motivation for putting it in the
       // indexer was to enable reindexing to work.
 
       $fields = PhabricatorCustomField::getObjectFields(
         $object,
         PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
       $fields->readFieldsFromStorage($object);
       $fields->rebuildIndexes($object);
     }
 
     return $xactions;
   }
 
   protected function didApplyTransactions(array $xactions) {
     // Hook for subclasses.
     return;
   }
 
 
   /**
    * Determine if the editor should hold a read lock on the object while
    * applying a transaction.
    *
    * If the editor does not hold a lock, two editors may read an object at the
    * same time, then apply their changes without any synchronization. For most
    * transactions, this does not matter much. However, it is important for some
    * transactions. For example, if an object has a transaction count on it, both
    * editors may read the object with `count = 23`, then independently update it
    * and save the object with `count = 24` twice. This will produce the wrong
    * state: the object really has 25 transactions, but the count is only 24.
    *
    * Generally, transactions fall into one of four buckets:
    *
    *   - Append operations: Actions like adding a comment to an object purely
    *     add information to its state, and do not depend on the current object
    *     state in any way. These transactions never need to hold locks.
    *   - Overwrite operations: Actions like changing the title or description
    *     of an object replace the current value with a new value, so the end
    *     state is consistent without a lock. We currently do not lock these
    *     transactions, although we may in the future.
    *   - Edge operations: Edge and subscription operations have internal
    *     synchronization which limits the damage race conditions can cause.
    *     We do not currently lock these transactions, although we may in the
    *     future.
    *   - Update operations: Actions like incrementing a count on an object.
    *     These operations generally should use locks, unless it is not
    *     important that the state remain consistent in the presence of races.
    *
    * @param   PhabricatorLiskDAO  Object being updated.
    * @param   PhabricatorApplicationTransaction Transaction being applied.
    * @return  bool                True to synchronize the edit with a lock.
    */
   protected function shouldReadLock(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     return false;
   }
 
   private function loadHandles(array $xactions) {
     $phids = array();
     foreach ($xactions as $key => $xaction) {
       $phids[$key] = $xaction->getRequiredHandlePHIDs();
     }
     $handles = array();
     $merged = array_mergev($phids);
     if ($merged) {
       $handles = id(new PhabricatorHandleQuery())
         ->setViewer($this->requireActor())
         ->withPHIDs($merged)
         ->execute();
     }
     foreach ($xactions as $key => $xaction) {
       $xaction->setHandles(array_select_keys($handles, $phids[$key]));
     }
   }
 
   private function loadSubscribers(PhabricatorLiskDAO $object) {
     if ($object->getPHID() &&
         ($object instanceof PhabricatorSubscribableInterface)) {
       $subs = PhabricatorSubscribersQuery::loadSubscribersForPHID(
         $object->getPHID());
       $this->subscribers = array_fuse($subs);
     } else {
       $this->subscribers = array();
     }
   }
 
   private function validateEditParameters(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     if (!$this->getContentSource()) {
-      throw new Exception(
-        'Call setContentSource() before applyTransactions()!');
+      throw new PhutilInvalidStateException('setContentSource');
     }
 
     // Do a bunch of sanity checks that the incoming transactions are fresh.
     // They should be unsaved and have only "transactionType" and "newValue"
     // set.
 
     $types = array_fill_keys($this->getTransactionTypes(), true);
 
     assert_instances_of($xactions, 'PhabricatorApplicationTransaction');
     foreach ($xactions as $xaction) {
       if ($xaction->getPHID() || $xaction->getID()) {
         throw new PhabricatorApplicationTransactionStructureException(
           $xaction,
           pht(
             'You can not apply transactions which already have IDs/PHIDs!'));
       }
       if ($xaction->getObjectPHID()) {
         throw new PhabricatorApplicationTransactionStructureException(
           $xaction,
           pht(
             'You can not apply transactions which already have objectPHIDs!'));
       }
       if ($xaction->getAuthorPHID()) {
         throw new PhabricatorApplicationTransactionStructureException(
           $xaction,
           pht(
             'You can not apply transactions which already have authorPHIDs!'));
       }
       if ($xaction->getCommentPHID()) {
         throw new PhabricatorApplicationTransactionStructureException(
           $xaction,
           pht(
             'You can not apply transactions which already have '.
             'commentPHIDs!'));
       }
       if ($xaction->getCommentVersion() !== 0) {
         throw new PhabricatorApplicationTransactionStructureException(
           $xaction,
           pht(
             'You can not apply transactions which already have '.
             'commentVersions!'));
       }
 
       $expect_value = !$xaction->shouldGenerateOldValue();
       $has_value = $xaction->hasOldValue();
 
       if ($expect_value && !$has_value) {
         throw new PhabricatorApplicationTransactionStructureException(
           $xaction,
           pht(
             'This transaction is supposed to have an oldValue set, but '.
             'it does not!'));
       }
 
       if ($has_value && !$expect_value) {
         throw new PhabricatorApplicationTransactionStructureException(
           $xaction,
           pht(
             'This transaction should generate its oldValue automatically, '.
             'but has already had one set!'));
       }
 
       $type = $xaction->getTransactionType();
       if (empty($types[$type])) {
         throw new PhabricatorApplicationTransactionStructureException(
           $xaction,
           pht(
             'Transaction has type "%s", but that transaction type is not '.
             'supported by this editor (%s).',
             $type,
             get_class($this)));
       }
     }
   }
 
   protected function requireCapabilities(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     if ($this->getIsNewObject()) {
       return;
     }
 
     $actor = $this->requireActor();
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         PhabricatorPolicyFilter::requireCapability(
           $actor,
           $object,
           PhabricatorPolicyCapability::CAN_VIEW);
         break;
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
         PhabricatorPolicyFilter::requireCapability(
           $actor,
           $object,
           PhabricatorPolicyCapability::CAN_EDIT);
         break;
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         PhabricatorPolicyFilter::requireCapability(
           $actor,
           $object,
           PhabricatorPolicyCapability::CAN_EDIT);
         break;
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
         PhabricatorPolicyFilter::requireCapability(
           $actor,
           $object,
           PhabricatorPolicyCapability::CAN_EDIT);
         break;
     }
   }
 
   private function buildSubscribeTransaction(
     PhabricatorLiskDAO $object,
     array $xactions,
     array $blocks) {
 
     if (!($object instanceof PhabricatorSubscribableInterface)) {
       return null;
     }
 
     if ($this->shouldEnableMentions($object, $xactions)) {
       $texts = array_mergev($blocks);
       $phids = PhabricatorMarkupEngine::extractPHIDsFromMentions(
         $this->getActor(),
         $texts);
     } else {
       $phids = array();
     }
 
     $this->mentionedPHIDs = $phids;
 
     if ($object->getPHID()) {
       // Don't try to subscribe already-subscribed mentions: we want to generate
       // a dialog about an action having no effect if the user explicitly adds
       // existing CCs, but not if they merely mention existing subscribers.
       $phids = array_diff($phids, $this->subscribers);
     }
 
     if ($phids) {
       $users = id(new PhabricatorPeopleQuery())
         ->setViewer($this->getActor())
         ->withPHIDs($phids)
         ->execute();
       $users = mpull($users, null, 'getPHID');
 
       foreach ($phids as $key => $phid) {
         // Do not subscribe mentioned users
         // who do not have VIEW Permissions
         if ($object instanceof PhabricatorPolicyInterface
           && !PhabricatorPolicyFilter::hasCapability(
           $users[$phid],
           $object,
           PhabricatorPolicyCapability::CAN_VIEW)
         ) {
           unset($phids[$key]);
         } else {
           if ($object->isAutomaticallySubscribed($phid)) {
             unset($phids[$key]);
           }
         }
       }
       $phids = array_values($phids);
     }
     // No else here to properly return null should we unset all subscriber
     if (!$phids) {
       return null;
     }
 
     $xaction = newv(get_class(head($xactions)), array());
     $xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
     $xaction->setNewValue(array('+' => $phids));
 
     return $xaction;
   }
 
   protected function getRemarkupBlocksFromTransaction(
     PhabricatorApplicationTransaction $transaction) {
     return $transaction->getRemarkupBlocks();
   }
 
   protected function mergeTransactions(
     PhabricatorApplicationTransaction $u,
     PhabricatorApplicationTransaction $v) {
 
     $type = $u->getTransactionType();
 
     switch ($type) {
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         return $this->mergePHIDOrEdgeTransactions($u, $v);
       case PhabricatorTransactions::TYPE_EDGE:
         $u_type = $u->getMetadataValue('edge:type');
         $v_type = $v->getMetadataValue('edge:type');
         if ($u_type == $v_type) {
           return $this->mergePHIDOrEdgeTransactions($u, $v);
         }
         return null;
     }
 
     // By default, do not merge the transactions.
     return null;
   }
 
   /**
    * Optionally expand transactions which imply other effects. For example,
    * resigning from a revision in Differential implies removing yourself as
    * a reviewer.
    */
   private function expandTransactions(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $results = array();
     foreach ($xactions as $xaction) {
       foreach ($this->expandTransaction($object, $xaction) as $expanded) {
         $results[] = $expanded;
       }
     }
 
     return $results;
   }
 
   protected function expandTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     return array($xaction);
   }
 
 
   public function getExpandedSupportTransactions(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     $xactions = array($xaction);
     $xactions = $this->expandSupportTransactions(
       $object,
       $xactions);
 
     if (count($xactions) == 1) {
       return array();
     }
 
     foreach ($xactions as $index => $cxaction) {
       if ($cxaction === $xaction) {
         unset($xactions[$index]);
         break;
       }
     }
 
     return $xactions;
   }
 
   private function expandSupportTransactions(
     PhabricatorLiskDAO $object,
     array $xactions) {
     $this->loadSubscribers($object);
 
     $xactions = $this->applyImplicitCC($object, $xactions);
 
     $blocks = array();
     foreach ($xactions as $key => $xaction) {
       $blocks[$key] = $this->getRemarkupBlocksFromTransaction($xaction);
     }
 
     $subscribe_xaction = $this->buildSubscribeTransaction(
       $object,
       $xactions,
       $blocks);
     if ($subscribe_xaction) {
       $xactions[] = $subscribe_xaction;
     }
 
     // TODO: For now, this is just a placeholder.
     $engine = PhabricatorMarkupEngine::getEngine('extract');
     $engine->setConfig('viewer', $this->requireActor());
 
     $block_xactions = $this->expandRemarkupBlockTransactions(
       $object,
       $xactions,
       $blocks,
       $engine);
 
     foreach ($block_xactions as $xaction) {
       $xactions[] = $xaction;
     }
 
     return $xactions;
   }
 
   private function expandRemarkupBlockTransactions(
     PhabricatorLiskDAO $object,
     array $xactions,
     $blocks,
     PhutilMarkupEngine $engine) {
 
     $block_xactions = $this->expandCustomRemarkupBlockTransactions(
       $object,
       $xactions,
       $blocks,
       $engine);
 
     $mentioned_phids = array();
     if ($this->shouldEnableMentions($object, $xactions)) {
       foreach ($blocks as $key => $xaction_blocks) {
         foreach ($xaction_blocks as $block) {
           $engine->markupText($block);
           $mentioned_phids += $engine->getTextMetadata(
             PhabricatorObjectRemarkupRule::KEY_MENTIONED_OBJECTS,
             array());
         }
       }
     }
 
     if (!$mentioned_phids) {
       return $block_xactions;
     }
 
     $mentioned_objects = id(new PhabricatorObjectQuery())
       ->setViewer($this->getActor())
       ->withPHIDs($mentioned_phids)
       ->execute();
 
     $mentionable_phids = array();
     if ($this->shouldEnableMentions($object, $xactions)) {
       foreach ($mentioned_objects as $mentioned_object) {
         if ($mentioned_object instanceof PhabricatorMentionableInterface) {
           $mentioned_phid = $mentioned_object->getPHID();
           if (idx($this->getUnmentionablePHIDMap(), $mentioned_phid)) {
             continue;
           }
           // don't let objects mention themselves
           if ($object->getPHID() && $mentioned_phid == $object->getPHID()) {
             continue;
           }
           $mentionable_phids[$mentioned_phid] = $mentioned_phid;
         }
       }
     }
 
     if ($mentionable_phids) {
       $edge_type = PhabricatorObjectMentionsObjectEdgeType::EDGECONST;
       $block_xactions[] = newv(get_class(head($xactions)), array())
         ->setIgnoreOnNoEffect(true)
         ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
         ->setMetadataValue('edge:type', $edge_type)
         ->setNewValue(array('+' => $mentionable_phids));
     }
 
     return $block_xactions;
   }
 
   protected function expandCustomRemarkupBlockTransactions(
     PhabricatorLiskDAO $object,
     array $xactions,
     $blocks,
     PhutilMarkupEngine $engine) {
     return array();
   }
 
 
   /**
    * Attempt to combine similar transactions into a smaller number of total
    * transactions. For example, two transactions which edit the title of an
    * object can be merged into a single edit.
    */
   private function combineTransactions(array $xactions) {
     $stray_comments = array();
 
     $result = array();
     $types = array();
     foreach ($xactions as $key => $xaction) {
       $type = $xaction->getTransactionType();
       if (isset($types[$type])) {
         foreach ($types[$type] as $other_key) {
           $merged = $this->mergeTransactions($result[$other_key], $xaction);
           if ($merged) {
             $result[$other_key] = $merged;
 
             if ($xaction->getComment() &&
                 ($xaction->getComment() !== $merged->getComment())) {
               $stray_comments[] = $xaction->getComment();
             }
 
             if ($result[$other_key]->getComment() &&
                 ($result[$other_key]->getComment() !== $merged->getComment())) {
               $stray_comments[] = $result[$other_key]->getComment();
             }
 
             // Move on to the next transaction.
             continue 2;
           }
         }
       }
       $result[$key] = $xaction;
       $types[$type][] = $key;
     }
 
     // If we merged any comments away, restore them.
     foreach ($stray_comments as $comment) {
       $xaction = newv(get_class(head($result)), array());
       $xaction->setTransactionType(PhabricatorTransactions::TYPE_COMMENT);
       $xaction->setComment($comment);
       $result[] = $xaction;
     }
 
     return array_values($result);
   }
 
   protected function mergePHIDOrEdgeTransactions(
     PhabricatorApplicationTransaction $u,
     PhabricatorApplicationTransaction $v) {
 
     $result = $u->getNewValue();
     foreach ($v->getNewValue() as $key => $value) {
       if ($u->getTransactionType() == PhabricatorTransactions::TYPE_EDGE) {
         if (empty($result[$key])) {
           $result[$key] = $value;
         } else {
           // We're merging two lists of edge adds, sets, or removes. Merge
           // them by merging individual PHIDs within them.
           $merged = $result[$key];
 
           foreach ($value as $dst => $v_spec) {
             if (empty($merged[$dst])) {
               $merged[$dst] = $v_spec;
             } else {
               // Two transactions are trying to perform the same operation on
               // the same edge. Normalize the edge data and then merge it. This
               // allows transactions to specify how data merges execute in a
               // precise way.
 
               $u_spec = $merged[$dst];
 
               if (!is_array($u_spec)) {
                 $u_spec = array('dst' => $u_spec);
               }
               if (!is_array($v_spec)) {
                 $v_spec = array('dst' => $v_spec);
               }
 
               $ux_data = idx($u_spec, 'data', array());
               $vx_data = idx($v_spec, 'data', array());
 
               $merged_data = $this->mergeEdgeData(
                 $u->getMetadataValue('edge:type'),
                 $ux_data,
                 $vx_data);
 
               $u_spec['data'] = $merged_data;
               $merged[$dst] = $u_spec;
             }
           }
 
           $result[$key] = $merged;
         }
       } else {
         $result[$key] = array_merge($value, idx($result, $key, array()));
       }
     }
     $u->setNewValue($result);
 
     // When combining an "ignore" transaction with a normal transaction, make
     // sure we don't propagate the "ignore" flag.
     if (!$v->getIgnoreOnNoEffect()) {
       $u->setIgnoreOnNoEffect(false);
     }
 
     return $u;
   }
 
   protected function mergeEdgeData($type, array $u, array $v) {
     return $v + $u;
   }
 
   protected function getPHIDTransactionNewValue(
     PhabricatorApplicationTransaction $xaction,
     $old = null) {
 
     if ($old !== null) {
       $old = array_fuse($old);
     } else {
       $old = array_fuse($xaction->getOldValue());
     }
 
     $new = $xaction->getNewValue();
     $new_add = idx($new, '+', array());
     unset($new['+']);
     $new_rem = idx($new, '-', array());
     unset($new['-']);
     $new_set = idx($new, '=', null);
     if ($new_set !== null) {
       $new_set = array_fuse($new_set);
     }
     unset($new['=']);
 
     if ($new) {
       throw new Exception(
         "Invalid 'new' value for PHID transaction. Value should contain only ".
         "keys '+' (add PHIDs), '-' (remove PHIDs) and '=' (set PHIDS).");
     }
 
     $result = array();
 
     foreach ($old as $phid) {
       if ($new_set !== null && empty($new_set[$phid])) {
         continue;
       }
       $result[$phid] = $phid;
     }
 
     if ($new_set !== null) {
       foreach ($new_set as $phid) {
         $result[$phid] = $phid;
       }
     }
 
     foreach ($new_add as $phid) {
       $result[$phid] = $phid;
     }
 
     foreach ($new_rem as $phid) {
       unset($result[$phid]);
     }
 
     return array_values($result);
   }
 
   protected function getEdgeTransactionNewValue(
     PhabricatorApplicationTransaction $xaction) {
 
     $new = $xaction->getNewValue();
     $new_add = idx($new, '+', array());
     unset($new['+']);
     $new_rem = idx($new, '-', array());
     unset($new['-']);
     $new_set = idx($new, '=', null);
     unset($new['=']);
 
     if ($new) {
       throw new Exception(
         "Invalid 'new' value for Edge transaction. Value should contain only ".
         "keys '+' (add edges), '-' (remove edges) and '=' (set edges).");
     }
 
     $old = $xaction->getOldValue();
 
     $lists = array($new_set, $new_add, $new_rem);
     foreach ($lists as $list) {
       $this->checkEdgeList($list);
     }
 
     $result = array();
     foreach ($old as $dst_phid => $edge) {
       if ($new_set !== null && empty($new_set[$dst_phid])) {
         continue;
       }
       $result[$dst_phid] = $this->normalizeEdgeTransactionValue(
         $xaction,
         $edge,
         $dst_phid);
     }
 
     if ($new_set !== null) {
       foreach ($new_set as $dst_phid => $edge) {
         $result[$dst_phid] = $this->normalizeEdgeTransactionValue(
           $xaction,
           $edge,
           $dst_phid);
       }
     }
 
     foreach ($new_add as $dst_phid => $edge) {
       $result[$dst_phid] = $this->normalizeEdgeTransactionValue(
         $xaction,
         $edge,
         $dst_phid);
     }
 
     foreach ($new_rem as $dst_phid => $edge) {
       unset($result[$dst_phid]);
     }
 
     return $result;
   }
 
   private function checkEdgeList($list) {
     if (!$list) {
       return;
     }
     foreach ($list as $key => $item) {
       if (phid_get_type($key) === PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
         throw new Exception(
           "Edge transactions must have destination PHIDs as in edge ".
           "lists (found key '{$key}').");
       }
       if (!is_array($item) && $item !== $key) {
         throw new Exception(
           "Edge transactions must have PHIDs or edge specs as values ".
           "(found value '{$item}').");
       }
     }
   }
 
   private function normalizeEdgeTransactionValue(
     PhabricatorApplicationTransaction $xaction,
     $edge,
     $dst_phid) {
 
     if (!is_array($edge)) {
       if ($edge != $dst_phid) {
         throw new Exception(
           pht(
             'Transaction edge data must either be the edge PHID or an edge '.
             'specification dictionary.'));
       }
       $edge = array();
     } else {
       foreach ($edge as $key => $value) {
         switch ($key) {
           case 'src':
           case 'dst':
           case 'type':
           case 'data':
           case 'dateCreated':
           case 'dateModified':
           case 'seq':
           case 'dataID':
             break;
           default:
             throw new Exception(
               pht(
                 'Transaction edge specification contains unexpected key '.
                 '"%s".',
                 $key));
         }
       }
     }
 
     $edge['dst'] = $dst_phid;
 
     $edge_type = $xaction->getMetadataValue('edge:type');
     if (empty($edge['type'])) {
       $edge['type'] = $edge_type;
     } else {
       if ($edge['type'] != $edge_type) {
         $this_type = $edge['type'];
         throw new Exception(
           "Edge transaction includes edge of type '{$this_type}', but ".
           "transaction is of type '{$edge_type}'. Each edge transaction must ".
           "alter edges of only one type.");
       }
     }
 
     if (!isset($edge['data'])) {
       $edge['data'] = array();
     }
 
     return $edge;
   }
 
   protected function sortTransactions(array $xactions) {
     $head = array();
     $tail = array();
 
     // Move bare comments to the end, so the actions precede them.
     foreach ($xactions as $xaction) {
       $type = $xaction->getTransactionType();
       if ($type == PhabricatorTransactions::TYPE_COMMENT) {
         $tail[] = $xaction;
       } else {
         $head[] = $xaction;
       }
     }
 
     return array_values(array_merge($head, $tail));
   }
 
 
   protected function filterTransactions(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $type_comment = PhabricatorTransactions::TYPE_COMMENT;
 
     $no_effect = array();
     $has_comment = false;
     $any_effect = false;
     foreach ($xactions as $key => $xaction) {
       if ($this->transactionHasEffect($object, $xaction)) {
         if ($xaction->getTransactionType() != $type_comment) {
           $any_effect = true;
         }
       } else if ($xaction->getIgnoreOnNoEffect()) {
         unset($xactions[$key]);
       } else {
         $no_effect[$key] = $xaction;
       }
       if ($xaction->hasComment()) {
         $has_comment = true;
       }
     }
 
     if (!$no_effect) {
       return $xactions;
     }
 
     if (!$this->getContinueOnNoEffect() && !$this->getIsPreview()) {
       throw new PhabricatorApplicationTransactionNoEffectException(
         $no_effect,
         $any_effect,
         $has_comment);
     }
 
     if (!$any_effect && !$has_comment) {
       // If we only have empty comment transactions, just drop them all.
       return array();
     }
 
     foreach ($no_effect as $key => $xaction) {
       if ($xaction->getComment()) {
         $xaction->setTransactionType($type_comment);
         $xaction->setOldValue(null);
         $xaction->setNewValue(null);
       } else {
         unset($xactions[$key]);
       }
     }
 
     return $xactions;
   }
 
 
   /**
    * Hook for validating transactions. This callback will be invoked for each
    * available transaction type, even if an edit does not apply any transactions
    * of that type. This allows you to raise exceptions when required fields are
    * missing, by detecting that the object has no field value and there is no
    * transaction which sets one.
    *
    * @param PhabricatorLiskDAO Object being edited.
    * @param string Transaction type to validate.
    * @param list<PhabricatorApplicationTransaction> Transactions of given type,
    *   which may be empty if the edit does not apply any transactions of the
    *   given type.
    * @return list<PhabricatorApplicationTransactionValidationError> List of
    *   validation errors.
    */
   protected function validateTransaction(
     PhabricatorLiskDAO $object,
     $type,
     array $xactions) {
 
     $errors = array();
     switch ($type) {
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
         $errors[] = $this->validatePolicyTransaction(
           $object,
           $xactions,
           $type,
           PhabricatorPolicyCapability::CAN_VIEW);
         break;
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         $errors[] = $this->validatePolicyTransaction(
           $object,
           $xactions,
           $type,
           PhabricatorPolicyCapability::CAN_EDIT);
         break;
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $groups = array();
         foreach ($xactions as $xaction) {
           $groups[$xaction->getMetadataValue('customfield:key')][] = $xaction;
         }
 
         $field_list = PhabricatorCustomField::getObjectFields(
           $object,
           PhabricatorCustomField::ROLE_EDIT);
         $field_list->setViewer($this->getActor());
 
         $role_xactions = PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS;
         foreach ($field_list->getFields() as $field) {
           if (!$field->shouldEnableForRole($role_xactions)) {
             continue;
           }
           $errors[] = $field->validateApplicationTransactions(
             $this,
             $type,
             idx($groups, $field->getFieldKey(), array()));
         }
         break;
     }
 
     return array_mergev($errors);
   }
 
   private function validatePolicyTransaction(
     PhabricatorLiskDAO $object,
     array $xactions,
     $transaction_type,
     $capability) {
 
     $actor = $this->requireActor();
     $errors = array();
     // Note $this->xactions is necessary; $xactions is $this->xactions of
     // $transaction_type
     $policy_object = $this->adjustObjectForPolicyChecks(
       $object,
       $this->xactions);
 
     // Make sure the user isn't editing away their ability to $capability this
     // object.
     foreach ($xactions as $xaction) {
       try {
         PhabricatorPolicyFilter::requireCapabilityWithForcedPolicy(
           $actor,
           $policy_object,
           $capability,
           $xaction->getNewValue());
       } catch (PhabricatorPolicyException $ex) {
         $errors[] = new PhabricatorApplicationTransactionValidationError(
           $transaction_type,
           pht('Invalid'),
           pht(
             'You can not select this %s policy, because you would no longer '.
             'be able to %s the object.',
             $capability,
             $capability),
           $xaction);
       }
     }
 
     if ($this->getIsNewObject()) {
       if (!$xactions) {
         $has_capability = PhabricatorPolicyFilter::hasCapability(
           $actor,
           $policy_object,
           $capability);
         if (!$has_capability) {
           $errors[] = new PhabricatorApplicationTransactionValidationError(
             $transaction_type,
             pht('Invalid'),
             pht('The selected %s policy excludes you. Choose a %s policy '.
                 'which allows you to %s the object.',
             $capability,
             $capability,
             $capability));
         }
       }
     }
 
     return $errors;
   }
 
   protected function adjustObjectForPolicyChecks(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     return clone $object;
   }
 
   protected function validateAllTransactions(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return array();
   }
 
   /**
    * Check for a missing text field.
    *
    * A text field is missing if the object has no value and there are no
    * transactions which set a value, or if the transactions remove the value.
    * This method is intended to make implementing @{method:validateTransaction}
    * more convenient:
    *
    *   $missing = $this->validateIsEmptyTextField(
    *     $object->getName(),
    *     $xactions);
    *
    * This will return `true` if the net effect of the object and transactions
    * is an empty field.
    *
    * @param wild Current field value.
    * @param list<PhabricatorApplicationTransaction> Transactions editing the
    *          field.
    * @return bool True if the field will be an empty text field after edits.
    */
   protected function validateIsEmptyTextField($field_value, array $xactions) {
     if (strlen($field_value) && empty($xactions)) {
       return false;
     }
 
     if ($xactions && strlen(last($xactions)->getNewValue())) {
       return false;
     }
 
     return true;
   }
 
 
 /* -(  Implicit CCs  )------------------------------------------------------- */
 
 
   /**
    * When a user interacts with an object, we might want to add them to CC.
    */
   final public function applyImplicitCC(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     if (!($object instanceof PhabricatorSubscribableInterface)) {
       // If the object isn't subscribable, we can't CC them.
       return $xactions;
     }
 
     $actor_phid = $this->getActingAsPHID();
 
     $type_user = PhabricatorPeopleUserPHIDType::TYPECONST;
     if (phid_get_type($actor_phid) != $type_user) {
       // Transactions by application actors like Herald, Harbormaster and
       // Diffusion should not CC the applications.
       return $xactions;
     }
 
     if ($object->isAutomaticallySubscribed($actor_phid)) {
       // If they're auto-subscribed, don't CC them.
       return $xactions;
     }
 
     $should_cc = false;
     foreach ($xactions as $xaction) {
       if ($this->shouldImplyCC($object, $xaction)) {
         $should_cc = true;
         break;
       }
     }
 
     if (!$should_cc) {
       // Only some types of actions imply a CC (like adding a comment).
       return $xactions;
     }
 
     if ($object->getPHID()) {
       if (isset($this->subscribers[$actor_phid])) {
         // If the user is already subscribed, don't implicitly CC them.
         return $xactions;
       }
 
       $unsub = PhabricatorEdgeQuery::loadDestinationPHIDs(
         $object->getPHID(),
         PhabricatorObjectHasUnsubscriberEdgeType::EDGECONST);
       $unsub = array_fuse($unsub);
       if (isset($unsub[$actor_phid])) {
         // If the user has previously unsubscribed from this object explicitly,
         // don't implicitly CC them.
         return $xactions;
       }
     }
 
     $xaction = newv(get_class(head($xactions)), array());
     $xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
     $xaction->setNewValue(array('+' => array($actor_phid)));
 
     array_unshift($xactions, $xaction);
 
     return $xactions;
   }
 
   protected function shouldImplyCC(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     return $xaction->isCommentTransaction();
   }
 
 
 /* -(  Sending Mail  )------------------------------------------------------- */
 
 
   /**
    * @task mail
    */
   protected function shouldSendMail(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return false;
   }
 
 
   /**
    * @task mail
    */
   protected function sendMail(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     // Check if any of the transactions are visible. If we don't have any
     // visible transactions, don't send the mail.
 
     $any_visible = false;
     foreach ($xactions as $xaction) {
       if (!$xaction->shouldHideForMail($xactions)) {
         $any_visible = true;
         break;
       }
     }
 
     if (!$any_visible) {
       return;
     }
 
     $email_force = array();
     $email_to = $this->getMailTo($object);
     $email_cc = $this->getMailCC($object);
 
     $adapter = $this->getHeraldAdapter();
     if ($adapter) {
       $email_cc = array_merge($email_cc, $adapter->getEmailPHIDs());
       $email_force = $adapter->getForcedEmailPHIDs();
     }
 
     $phids = array_merge($email_to, $email_cc);
     $handles = id(new PhabricatorHandleQuery())
       ->setViewer($this->requireActor())
       ->withPHIDs($phids)
       ->execute();
 
     $template = $this->buildMailTemplate($object);
     $body = $this->buildMailBody($object, $xactions);
 
     $mail_tags = $this->getMailTags($object, $xactions);
     $action = $this->getMailAction($object, $xactions);
 
     $reply_handler = $this->buildReplyHandler($object);
 
     $body->addEmailPreferenceSection();
 
     $template
       ->setFrom($this->getActingAsPHID())
       ->setSubjectPrefix($this->getMailSubjectPrefix())
       ->setVarySubjectPrefix('['.$action.']')
       ->setThreadID($this->getMailThreadID($object), $this->getIsNewObject())
       ->setRelatedPHID($object->getPHID())
       ->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs())
       ->setForceHeraldMailRecipientPHIDs($email_force)
       ->setMailTags($mail_tags)
       ->setIsBulk(true)
       ->setBody($body->render())
       ->setHTMLBody($body->renderHTML());
 
     foreach ($body->getAttachments() as $attachment) {
       $template->addAttachment($attachment);
     }
 
     $herald_xscript = $this->getHeraldTranscript();
     if ($herald_xscript) {
       $herald_header = $herald_xscript->getXHeraldRulesHeader();
       $herald_header = HeraldTranscript::saveXHeraldRulesHeader(
         $object->getPHID(),
         $herald_header);
     } else {
       $herald_header = HeraldTranscript::loadXHeraldRulesHeader(
         $object->getPHID());
     }
 
     if ($herald_header) {
       $template->addHeader('X-Herald-Rules', $herald_header);
     }
 
     if ($object instanceof PhabricatorProjectInterface) {
       $this->addMailProjectMetadata($object, $template);
     }
 
     if ($this->getParentMessageID()) {
       $template->setParentMessageID($this->getParentMessageID());
     }
 
     $mails = $reply_handler->multiplexMail(
       $template,
       array_select_keys($handles, $email_to),
       array_select_keys($handles, $email_cc));
 
     foreach ($mails as $mail) {
       $mail->saveAndSend();
     }
 
     $template->addTos($email_to);
     $template->addCCs($email_cc);
 
     return $template;
   }
 
   private function addMailProjectMetadata(
     PhabricatorLiskDAO $object,
     PhabricatorMetaMTAMail $template) {
 
     $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
       $object->getPHID(),
       PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
 
     if (!$project_phids) {
       return;
     }
 
     // TODO: This viewer isn't quite right. It would be slightly better to use
     // the mail recipient, but that's not very easy given the way rendering
     // works today.
 
     $handles = id(new PhabricatorHandleQuery())
       ->setViewer($this->requireActor())
       ->withPHIDs($project_phids)
       ->execute();
 
     $project_tags = array();
     foreach ($handles as $handle) {
       if (!$handle->isComplete()) {
         continue;
       }
       $project_tags[] = '<'.$handle->getObjectName().'>';
     }
 
     if (!$project_tags) {
       return;
     }
 
     $project_tags = implode(', ', $project_tags);
     $template->addHeader('X-Phabricator-Projects', $project_tags);
   }
 
 
   protected function getMailThreadID(PhabricatorLiskDAO $object) {
     return $object->getPHID();
   }
 
 
   /**
    * @task mail
    */
   protected function getStrongestAction(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return last(msort($xactions, 'getActionStrength'));
   }
 
 
   /**
    * @task mail
    */
   protected function buildReplyHandler(PhabricatorLiskDAO $object) {
     throw new Exception('Capability not supported.');
   }
 
   /**
    * @task mail
    */
   protected function getMailSubjectPrefix() {
     throw new Exception('Capability not supported.');
   }
 
 
   /**
    * @task mail
    */
   protected function getMailTags(
     PhabricatorLiskDAO $object,
     array $xactions) {
     $tags = array();
 
     foreach ($xactions as $xaction) {
       $tags[] = $xaction->getMailTags();
     }
 
     return array_mergev($tags);
   }
 
   /**
    * @task mail
    */
   public function getMailTagsMap() {
     // TODO: We should move shared mail tags, like "comment", here.
     return array();
   }
 
 
   /**
    * @task mail
    */
   protected function getMailAction(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return $this->getStrongestAction($object, $xactions)->getActionName();
   }
 
 
   /**
    * @task mail
    */
   protected function buildMailTemplate(PhabricatorLiskDAO $object) {
     throw new Exception('Capability not supported.');
   }
 
 
   /**
    * @task mail
    */
   protected function getMailTo(PhabricatorLiskDAO $object) {
     throw new Exception('Capability not supported.');
   }
 
 
   /**
    * @task mail
    */
   protected function getMailCC(PhabricatorLiskDAO $object) {
     $phids = array();
     $has_support = false;
 
     if ($object instanceof PhabricatorSubscribableInterface) {
       $phids[] = $this->subscribers;
       $has_support = true;
     }
 
     if ($object instanceof PhabricatorProjectInterface) {
       $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
         $object->getPHID(),
         PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
 
       if ($project_phids) {
         $watcher_type = PhabricatorObjectHasWatcherEdgeType::EDGECONST;
 
         $query = id(new PhabricatorEdgeQuery())
           ->withSourcePHIDs($project_phids)
           ->withEdgeTypes(array($watcher_type));
         $query->execute();
 
         $watcher_phids = $query->getDestinationPHIDs();
         if ($watcher_phids) {
           // We need to do a visibility check for all the watchers, as
           // watching a project is not a guarantee that you can see objects
           // associated with it.
           $users = id(new PhabricatorPeopleQuery())
             ->setViewer($this->requireActor())
             ->withPHIDs($watcher_phids)
             ->execute();
 
           $watchers = array();
           foreach ($users as $user) {
             $can_see = PhabricatorPolicyFilter::hasCapability(
               $user,
               $object,
               PhabricatorPolicyCapability::CAN_VIEW);
             if ($can_see) {
               $watchers[] = $user->getPHID();
             }
           }
           $phids[] = $watchers;
         }
       }
 
       $has_support = true;
     }
 
     if (!$has_support) {
       throw new Exception('Capability not supported.');
     }
 
     return array_mergev($phids);
   }
 
 
   /**
    * @task mail
    */
   protected function buildMailBody(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $body = new PhabricatorMetaMTAMailBody();
     $body->setViewer($this->requireActor());
 
     $this->addHeadersAndCommentsToMailBody($body, $xactions);
     $this->addCustomFieldsToMailBody($body, $object, $xactions);
     return $body;
   }
 
   /**
    * @task mail
    */
   protected function addHeadersAndCommentsToMailBody(
     PhabricatorMetaMTAMailBody $body,
     array $xactions) {
 
     $headers = array();
     $comments = array();
 
     foreach ($xactions as $xaction) {
       if ($xaction->shouldHideForMail($xactions)) {
         continue;
       }
 
       $header = $xaction->getTitleForMail();
       if ($header !== null) {
         $headers[] = $header;
       }
 
       $comment = $xaction->getBodyForMail();
       if ($comment !== null) {
         $comments[] = $comment;
       }
     }
     $body->addRawSection(implode("\n", $headers));
 
     foreach ($comments as $comment) {
       $body->addRemarkupSection($comment);
     }
   }
 
   /**
    * @task mail
    */
   protected function addCustomFieldsToMailBody(
     PhabricatorMetaMTAMailBody $body,
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     if ($object instanceof PhabricatorCustomFieldInterface) {
       $field_list = PhabricatorCustomField::getObjectFields(
         $object,
         PhabricatorCustomField::ROLE_TRANSACTIONMAIL);
       $field_list->setViewer($this->getActor());
       $field_list->readFieldsFromStorage($object);
 
       foreach ($field_list->getFields() as $field) {
         $field->updateTransactionMailBody(
           $body,
           $this,
           $xactions);
       }
     }
   }
 
 
 
 /* -(  Publishing Feed Stories  )-------------------------------------------- */
 
 
   /**
    * @task feed
    */
   protected function shouldPublishFeedStory(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return false;
   }
 
 
   /**
    * @task feed
    */
   protected function getFeedStoryType() {
     return 'PhabricatorApplicationTransactionFeedStory';
   }
 
 
   /**
    * @task feed
    */
   protected function getFeedRelatedPHIDs(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $phids = array(
       $object->getPHID(),
       $this->getActingAsPHID(),
     );
 
     if ($object instanceof PhabricatorProjectInterface) {
       $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
         $object->getPHID(),
         PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
       foreach ($project_phids as $project_phid) {
         $phids[] = $project_phid;
       }
     }
 
     return $phids;
   }
 
 
   /**
    * @task feed
    */
   protected function getFeedNotifyPHIDs(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     return array_unique(array_merge(
       $this->getMailTo($object),
       $this->getMailCC($object)));
   }
 
 
   /**
    * @task feed
    */
   protected function getFeedStoryData(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $xactions = msort($xactions, 'getActionStrength');
     $xactions = array_reverse($xactions);
 
     return array(
       'objectPHID'        => $object->getPHID(),
       'transactionPHIDs'  => mpull($xactions, 'getPHID'),
     );
   }
 
 
   /**
    * @task feed
    */
   protected function publishFeedStory(
     PhabricatorLiskDAO $object,
     array $xactions,
     array $mailed_phids) {
 
     $xactions = mfilter($xactions, 'shouldHideForFeed', true);
 
     if (!$xactions) {
       return;
     }
 
     $related_phids = $this->getFeedRelatedPHIDs($object, $xactions);
     $subscribed_phids = $this->getFeedNotifyPHIDs($object, $xactions);
 
     $story_type = $this->getFeedStoryType();
     $story_data = $this->getFeedStoryData($object, $xactions);
 
     id(new PhabricatorFeedStoryPublisher())
       ->setStoryType($story_type)
       ->setStoryData($story_data)
       ->setStoryTime(time())
       ->setStoryAuthorPHID($this->getActingAsPHID())
       ->setRelatedPHIDs($related_phids)
       ->setPrimaryObjectPHID($object->getPHID())
       ->setSubscribedPHIDs($subscribed_phids)
       ->setMailRecipientPHIDs($mailed_phids)
       ->setMailTags($this->getMailTags($object, $xactions))
       ->publish();
   }
 
 
 /* -(  Search Index  )------------------------------------------------------- */
 
 
   /**
    * @task search
    */
   protected function supportsSearch() {
     return false;
   }
 
   /**
    * @task search
    */
   protected function getSearchContextParameter(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return null;
   }
 
 
 /* -(  Herald Integration )-------------------------------------------------- */
 
 
   protected function shouldApplyHeraldRules(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return false;
   }
 
   protected function buildHeraldAdapter(
     PhabricatorLiskDAO $object,
     array $xactions) {
     throw new Exception('No herald adapter specified.');
   }
 
   private function setHeraldAdapter(HeraldAdapter $adapter) {
     $this->heraldAdapter = $adapter;
     return $this;
   }
 
   protected function getHeraldAdapter() {
     return $this->heraldAdapter;
   }
 
   private function setHeraldTranscript(HeraldTranscript $transcript) {
     $this->heraldTranscript = $transcript;
     return $this;
   }
 
   protected function getHeraldTranscript() {
     return $this->heraldTranscript;
   }
 
   private function applyHeraldRules(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $adapter = $this->buildHeraldAdapter($object, $xactions);
     $adapter->setContentSource($this->getContentSource());
     $adapter->setIsNewObject($this->getIsNewObject());
     if ($this->getApplicationEmail()) {
       $adapter->setApplicationEmail($this->getApplicationEmail());
     }
     $xscript = HeraldEngine::loadAndApplyRules($adapter);
 
     $this->setHeraldAdapter($adapter);
     $this->setHeraldTranscript($xscript);
 
     return array_merge(
       $this->didApplyHeraldRules($object, $adapter, $xscript),
       $adapter->getQueuedTransactions());
   }
 
   protected function didApplyHeraldRules(
     PhabricatorLiskDAO $object,
     HeraldAdapter $adapter,
     HeraldTranscript $transcript) {
     return array();
   }
 
 
 /* -(  Custom Fields  )------------------------------------------------------ */
 
 
   /**
    * @task customfield
    */
   private function getCustomFieldForTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     $field_key = $xaction->getMetadataValue('customfield:key');
     if (!$field_key) {
       throw new Exception(
         "Custom field transaction has no 'customfield:key'!");
     }
 
     $field = PhabricatorCustomField::getObjectField(
       $object,
       PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS,
       $field_key);
 
     if (!$field) {
       throw new Exception(
         "Custom field transaction has invalid 'customfield:key'; field ".
         "'{$field_key}' is disabled or does not exist.");
     }
 
     if (!$field->shouldAppearInApplicationTransactions()) {
       throw new Exception(
         "Custom field transaction '{$field_key}' does not implement ".
         "integration for ApplicationTransactions.");
     }
 
     $field->setViewer($this->getActor());
 
     return $field;
   }
 
 
 /* -(  Files  )-------------------------------------------------------------- */
 
 
   /**
    * Extract the PHIDs of any files which these transactions attach.
    *
    * @task files
    */
   private function extractFilePHIDs(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $blocks = array();
     foreach ($xactions as $xaction) {
       $blocks[] = $this->getRemarkupBlocksFromTransaction($xaction);
     }
     $blocks = array_mergev($blocks);
 
     $phids = array();
     if ($blocks) {
       $phids[] = PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles(
         $this->getActor(),
         $blocks);
     }
 
     foreach ($xactions as $xaction) {
       $phids[] = $this->extractFilePHIDsFromCustomTransaction(
         $object,
         $xaction);
     }
 
     $phids = array_unique(array_filter(array_mergev($phids)));
     if (!$phids) {
       return array();
     }
 
     // Only let a user attach files they can actually see, since this would
     // otherwise let you access any file by attaching it to an object you have
     // view permission on.
 
     $files = id(new PhabricatorFileQuery())
       ->setViewer($this->getActor())
       ->withPHIDs($phids)
       ->execute();
 
     return mpull($files, 'getPHID');
   }
 
   /**
    * @task files
    */
   protected function extractFilePHIDsFromCustomTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     return array();
   }
 
 
   /**
    * @task files
    */
   private function attachFiles(
     PhabricatorLiskDAO $object,
     array $file_phids) {
 
     if (!$file_phids) {
       return;
     }
 
     $editor = new PhabricatorEdgeEditor();
 
     $src = $object->getPHID();
     $type = PhabricatorObjectHasFileEdgeType::EDGECONST;
     foreach ($file_phids as $dst) {
       $editor->addEdge($src, $type, $dst);
     }
 
     $editor->save();
   }
 
   private function applyInverseEdgeTransactions(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction,
     $inverse_type) {
 
     $old = $xaction->getOldValue();
     $new = $xaction->getNewValue();
 
     $add = array_keys(array_diff_key($new, $old));
     $rem = array_keys(array_diff_key($old, $new));
 
     $add = array_fuse($add);
     $rem = array_fuse($rem);
     $all = $add + $rem;
 
     $nodes = id(new PhabricatorObjectQuery())
       ->setViewer($this->requireActor())
       ->withPHIDs($all)
       ->execute();
 
     foreach ($nodes as $node) {
       if (!($node instanceof PhabricatorApplicationTransactionInterface)) {
         continue;
       }
 
       $editor = $node->getApplicationTransactionEditor();
       $template = $node->getApplicationTransactionTemplate();
       $target = $node->getApplicationTransactionObject();
 
       if (isset($add[$node->getPHID()])) {
         $edge_edit_type = '+';
       } else {
         $edge_edit_type = '-';
       }
 
       $template
         ->setTransactionType($xaction->getTransactionType())
         ->setMetadataValue('edge:type', $inverse_type)
         ->setNewValue(
           array(
             $edge_edit_type => array($object->getPHID() => $object->getPHID()),
           ));
 
       $editor
         ->setContinueOnNoEffect(true)
         ->setContinueOnMissingFields(true)
         ->setParentMessageID($this->getParentMessageID())
         ->setIsInverseEdgeEditor(true)
         ->setActor($this->requireActor())
         ->setActingAsPHID($this->getActingAsPHID())
         ->setContentSource($this->getContentSource());
 
       $editor->applyTransactions($target, array($template));
     }
   }
 
 }
diff --git a/src/applications/transactions/query/PhabricatorApplicationTransactionQuery.php b/src/applications/transactions/query/PhabricatorApplicationTransactionQuery.php
index ac6f713eeb..4c1e3ccd17 100644
--- a/src/applications/transactions/query/PhabricatorApplicationTransactionQuery.php
+++ b/src/applications/transactions/query/PhabricatorApplicationTransactionQuery.php
@@ -1,191 +1,189 @@
 <?php
 
 abstract class PhabricatorApplicationTransactionQuery
   extends PhabricatorCursorPagedPolicyAwareQuery {
 
   private $phids;
   private $objectPHIDs;
   private $authorPHIDs;
   private $transactionTypes;
 
   private $needComments = true;
   private $needHandles  = true;
 
   abstract public function getTemplateApplicationTransaction();
 
   protected function buildMoreWhereClauses(AphrontDatabaseConnection $conn_r) {
     return array();
   }
 
   public function withPHIDs(array $phids) {
     $this->phids = $phids;
     return $this;
   }
 
   public function withObjectPHIDs(array $object_phids) {
     $this->objectPHIDs = $object_phids;
     return $this;
   }
 
   public function withAuthorPHIDs(array $author_phids) {
     $this->authorPHIDs = $author_phids;
     return $this;
   }
 
   public function withTransactionTypes(array $transaction_types) {
     $this->transactionTypes = $transaction_types;
     return $this;
   }
 
   public function needComments($need) {
     $this->needComments = $need;
     return $this;
   }
 
   public function needHandles($need) {
     $this->needHandles = $need;
     return $this;
   }
 
   protected function loadPage() {
     $table = $this->getTemplateApplicationTransaction();
     $conn_r = $table->establishConnection('r');
 
     $data = queryfx_all(
       $conn_r,
       'SELECT * FROM %T x %Q %Q %Q',
       $table->getTableName(),
       $this->buildWhereClause($conn_r),
       $this->buildOrderClause($conn_r),
       $this->buildLimitClause($conn_r));
 
     $xactions = $table->loadAllFromArray($data);
 
     foreach ($xactions as $xaction) {
       $xaction->attachViewer($this->getViewer());
     }
 
     if ($this->needComments) {
       $comment_phids = array_filter(mpull($xactions, 'getCommentPHID'));
 
       $comments = array();
       if ($comment_phids) {
         $comments =
           id(new PhabricatorApplicationTransactionTemplatedCommentQuery())
             ->setTemplate($table->getApplicationTransactionCommentObject())
             ->setViewer($this->getViewer())
             ->withPHIDs($comment_phids)
             ->execute();
         $comments = mpull($comments, null, 'getPHID');
       }
 
       foreach ($xactions as $xaction) {
         if ($xaction->getCommentPHID()) {
           $comment = idx($comments, $xaction->getCommentPHID());
           if ($comment) {
             $xaction->attachComment($comment);
           }
         }
       }
     } else {
       foreach ($xactions as $xaction) {
         $xaction->setCommentNotLoaded(true);
       }
     }
 
     return $xactions;
   }
 
   protected function willFilterPage(array $xactions) {
     $object_phids = array_keys(mpull($xactions, null, 'getObjectPHID'));
 
     $objects = id(new PhabricatorObjectQuery())
       ->setViewer($this->getViewer())
       ->setParentQuery($this)
       ->withPHIDs($object_phids)
       ->execute();
 
     foreach ($xactions as $key => $xaction) {
       $object_phid = $xaction->getObjectPHID();
       if (empty($objects[$object_phid])) {
         unset($xactions[$key]);
         continue;
       }
       $xaction->attachObject($objects[$object_phid]);
     }
 
     // NOTE: We have to do this after loading objects, because the objects
     // may help determine which handles are required (for example, in the case
     // of custom fields).
 
     if ($this->needHandles) {
       $phids = array();
       foreach ($xactions as $xaction) {
         $phids[$xaction->getPHID()] = $xaction->getRequiredHandlePHIDs();
       }
       $handles = array();
       $merged = array_mergev($phids);
       if ($merged) {
-        $handles = id(new PhabricatorHandleQuery())
-          ->setViewer($this->getViewer())
-          ->withPHIDs($merged)
-          ->execute();
+        $handles = $this->getViewer()->loadHandles($merged);
+        $handles = iterator_to_array($handles);
       }
       foreach ($xactions as $xaction) {
         $xaction->setHandles(
           array_select_keys(
             $handles,
             $phids[$xaction->getPHID()]));
       }
     }
 
     return $xactions;
   }
 
   protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
     $where = array();
 
     if ($this->phids) {
       $where[] = qsprintf(
         $conn_r,
         'phid IN (%Ls)',
         $this->phids);
     }
 
     if ($this->objectPHIDs) {
       $where[] = qsprintf(
         $conn_r,
         'objectPHID IN (%Ls)',
         $this->objectPHIDs);
     }
 
     if ($this->authorPHIDs) {
       $where[] = qsprintf(
         $conn_r,
         'authorPHID IN (%Ls)',
         $this->authorPHIDs);
     }
 
     if ($this->transactionTypes) {
       $where[] = qsprintf(
         $conn_r,
         'transactionType IN (%Ls)',
         $this->transactionTypes);
     }
 
     foreach ($this->buildMoreWhereClauses($conn_r) as $clause) {
       $where[] = $clause;
     }
 
     $where[] = $this->buildPagingClause($conn_r);
 
     return $this->formatWhereClause($where);
   }
 
 
   public function getQueryApplicationClass() {
     // TODO: Sort this out?
     return null;
   }
 
 }
diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
index c6755ccfcc..355f15c2a7 100644
--- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
+++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
@@ -1,1247 +1,1247 @@
 <?php
 
 abstract class PhabricatorApplicationTransaction
   extends PhabricatorLiskDAO
   implements
     PhabricatorPolicyInterface,
     PhabricatorDestructibleInterface {
 
   const TARGET_TEXT = 'text';
   const TARGET_HTML = 'html';
 
   protected $phid;
   protected $objectPHID;
   protected $authorPHID;
   protected $viewPolicy;
   protected $editPolicy;
 
   protected $commentPHID;
   protected $commentVersion = 0;
   protected $transactionType;
   protected $oldValue;
   protected $newValue;
   protected $metadata = array();
 
   protected $contentSource;
 
   private $comment;
   private $commentNotLoaded;
 
   private $handles;
   private $renderingTarget = self::TARGET_HTML;
   private $transactionGroup = array();
   private $viewer = self::ATTACHABLE;
   private $object = self::ATTACHABLE;
   private $oldValueHasBeenSet = false;
 
   private $ignoreOnNoEffect;
 
 
   /**
    * Flag this transaction as a pure side-effect which should be ignored when
    * applying transactions if it has no effect, even if transaction application
    * would normally fail. This both provides users with better error messages
    * and allows transactions to perform optional side effects.
    */
   public function setIgnoreOnNoEffect($ignore) {
     $this->ignoreOnNoEffect = $ignore;
     return $this;
   }
 
   public function getIgnoreOnNoEffect() {
     return $this->ignoreOnNoEffect;
   }
 
   public function shouldGenerateOldValue() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_BUILDABLE:
       case PhabricatorTransactions::TYPE_TOKEN:
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
       case PhabricatorTransactions::TYPE_INLINESTATE:
         return false;
     }
     return true;
   }
 
   abstract public function getApplicationTransactionType();
 
   private function getApplicationObjectTypeName() {
     $types = PhabricatorPHIDType::getAllTypes();
 
     $type = idx($types, $this->getApplicationTransactionType());
     if ($type) {
       return $type->getTypeName();
     }
 
     return pht('Object');
   }
 
   public function getApplicationTransactionCommentObject() {
     throw new PhutilMethodNotImplementedException();
   }
 
   public function getApplicationTransactionViewObject() {
     return new PhabricatorApplicationTransactionView();
   }
 
   public function getMetadataValue($key, $default = null) {
     return idx($this->metadata, $key, $default);
   }
 
   public function setMetadataValue($key, $value) {
     $this->metadata[$key] = $value;
     return $this;
   }
 
   public function generatePHID() {
     $type = PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST;
     $subtype = $this->getApplicationTransactionType();
 
     return PhabricatorPHID::generateNewPHID($type, $subtype);
   }
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_SERIALIZATION => array(
         'oldValue' => self::SERIALIZATION_JSON,
         'newValue' => self::SERIALIZATION_JSON,
         'metadata' => self::SERIALIZATION_JSON,
       ),
       self::CONFIG_COLUMN_SCHEMA => array(
         'commentPHID' => 'phid?',
         'commentVersion' => 'uint32',
         'contentSource' => 'text',
         'transactionType' => 'text32',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_object' => array(
           'columns' => array('objectPHID'),
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function setContentSource(PhabricatorContentSource $content_source) {
     $this->contentSource = $content_source->serialize();
     return $this;
   }
 
   public function getContentSource() {
     return PhabricatorContentSource::newFromSerialized($this->contentSource);
   }
 
   public function hasComment() {
     return $this->getComment() && strlen($this->getComment()->getContent());
   }
 
   public function getComment() {
     if ($this->commentNotLoaded) {
       throw new Exception('Comment for this transaction was not loaded.');
     }
     return $this->comment;
   }
 
   public function attachComment(
     PhabricatorApplicationTransactionComment $comment) {
     $this->comment = $comment;
     $this->commentNotLoaded = false;
     return $this;
   }
 
   public function setCommentNotLoaded($not_loaded) {
     $this->commentNotLoaded = $not_loaded;
     return $this;
   }
 
   public function attachObject($object) {
     $this->object = $object;
     return $this;
   }
 
   public function getObject() {
     return $this->assertAttached($this->object);
   }
 
   public function getRemarkupBlocks() {
     $blocks = array();
 
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getTransactionCustomField();
         if ($field) {
           $custom_blocks = $field->getApplicationTransactionRemarkupBlocks(
             $this);
           foreach ($custom_blocks as $custom_block) {
             $blocks[] = $custom_block;
           }
         }
         break;
     }
 
     if ($this->getComment()) {
       $blocks[] = $this->getComment()->getContent();
     }
 
     return $blocks;
   }
 
   public function setOldValue($value) {
     $this->oldValueHasBeenSet = true;
     $this->writeField('oldValue', $value);
     return $this;
   }
 
   public function hasOldValue() {
     return $this->oldValueHasBeenSet;
   }
 
 
 /* -(  Rendering  )---------------------------------------------------------- */
 
   public function setRenderingTarget($rendering_target) {
     $this->renderingTarget = $rendering_target;
     return $this;
   }
 
   public function getRenderingTarget() {
     return $this->renderingTarget;
   }
 
   public function attachViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
   public function getViewer() {
     return $this->assertAttached($this->viewer);
   }
 
   public function getRequiredHandlePHIDs() {
     $phids = array();
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     $phids[] = array($this->getAuthorPHID());
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getTransactionCustomField();
         if ($field) {
           $phids[] = $field->getApplicationTransactionRequiredHandlePHIDs(
             $this);
         }
         break;
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         $phids[] = $old;
         $phids[] = $new;
         break;
       case PhabricatorTransactions::TYPE_EDGE:
         $phids[] = ipull($old, 'dst');
         $phids[] = ipull($new, 'dst');
         break;
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
         if (!PhabricatorPolicyQuery::isGlobalPolicy($old)) {
           $phids[] = array($old);
         }
         if (!PhabricatorPolicyQuery::isGlobalPolicy($new)) {
           $phids[] = array($new);
         }
         break;
       case PhabricatorTransactions::TYPE_TOKEN:
         break;
       case PhabricatorTransactions::TYPE_BUILDABLE:
         $phid = $this->getMetadataValue('harbormaster:buildablePHID');
         if ($phid) {
           $phids[] = array($phid);
         }
         break;
     }
 
     if ($this->getComment()) {
       $phids[] = array($this->getComment()->getAuthorPHID());
     }
 
     return array_mergev($phids);
   }
 
   public function setHandles(array $handles) {
     $this->handles = $handles;
     return $this;
   }
 
   public function getHandle($phid) {
     if (empty($this->handles[$phid])) {
       throw new Exception(
         pht(
           'Transaction ("%s", of type "%s") requires a handle ("%s") that it '.
           'did not load.',
           $this->getPHID(),
           $this->getTransactionType(),
           $phid));
     }
     return $this->handles[$phid];
   }
 
   public function getHandleIfExists($phid) {
     return idx($this->handles, $phid);
   }
 
   public function getHandles() {
     if ($this->handles === null) {
       throw new Exception(
         'Transaction requires handles and it did not load them.'
       );
     }
     return $this->handles;
   }
 
   public function renderHandleLink($phid) {
     if ($this->renderingTarget == self::TARGET_HTML) {
       return $this->getHandle($phid)->renderLink();
     } else {
       return $this->getHandle($phid)->getLinkName();
     }
   }
 
   public function renderHandleList(array $phids) {
     $links = array();
     foreach ($phids as $phid) {
       $links[] = $this->renderHandleLink($phid);
     }
     if ($this->renderingTarget == self::TARGET_HTML) {
       return phutil_implode_html(', ', $links);
     } else {
       return implode(', ', $links);
     }
   }
 
   private function renderSubscriberList(array $phids, $change_type) {
     if ($this->getRenderingTarget() == self::TARGET_TEXT) {
       return $this->renderHandleList($phids);
     } else {
       $handles = array_select_keys($this->getHandles(), $phids);
       return id(new SubscriptionListStringBuilder())
         ->setHandles($handles)
         ->setObjectPHID($this->getPHID())
         ->buildTransactionString($change_type);
     }
   }
 
   protected function renderPolicyName($phid, $state = 'old') {
     $policy = PhabricatorPolicy::newFromPolicyAndHandle(
       $phid,
       $this->getHandleIfExists($phid));
     if ($this->renderingTarget == self::TARGET_HTML) {
       switch ($policy->getType()) {
         case PhabricatorPolicyType::TYPE_CUSTOM:
           $policy->setHref('/transactions/'.$state.'/'.$this->getPHID().'/');
           $policy->setWorkflow(true);
           break;
         default:
           break;
       }
       $output = $policy->renderDescription();
     } else {
       $output = hsprintf('%s', $policy->getFullName());
     }
     return $output;
   }
 
   public function getIcon() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         $comment = $this->getComment();
         if ($comment && $comment->getIsRemoved()) {
           return 'fa-eraser';
         }
         return 'fa-comment';
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         return 'fa-envelope';
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
         return 'fa-lock';
       case PhabricatorTransactions::TYPE_EDGE:
         return 'fa-link';
       case PhabricatorTransactions::TYPE_BUILDABLE:
         return 'fa-wrench';
       case PhabricatorTransactions::TYPE_TOKEN:
         return 'fa-trophy';
     }
 
     return 'fa-pencil';
   }
 
   public function getToken() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_TOKEN:
         $old = $this->getOldValue();
         $new = $this->getNewValue();
         if ($new) {
           $icon = substr($new, 10);
         } else {
           $icon = substr($old, 10);
         }
         return array($icon, !$this->getNewValue());
     }
 
     return array(null, null);
   }
 
   public function getColor() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT;
         $comment = $this->getComment();
         if ($comment && $comment->getIsRemoved()) {
           return 'black';
         }
         break;
       case PhabricatorTransactions::TYPE_BUILDABLE:
         switch ($this->getNewValue()) {
           case HarbormasterBuildable::STATUS_PASSED:
             return 'green';
           case HarbormasterBuildable::STATUS_FAILED:
             return 'red';
         }
         break;
     }
     return null;
   }
 
   protected function getTransactionCustomField() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $key = $this->getMetadataValue('customfield:key');
         if (!$key) {
           return null;
         }
 
         $field = PhabricatorCustomField::getObjectField(
           $this->getObject(),
           PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS,
           $key);
         if (!$field) {
           return null;
         }
 
         $field->setViewer($this->getViewer());
         return $field;
     }
 
     return null;
   }
 
   public function shouldHide() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
         if ($this->getOldValue() === null) {
           return true;
         } else {
           return false;
         }
         break;
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getTransactionCustomField();
         if ($field) {
           return $field->shouldHideInApplicationTransactions($this);
         }
       case PhabricatorTransactions::TYPE_EDGE:
         $edge_type = $this->getMetadataValue('edge:type');
         switch ($edge_type) {
           case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
             return true;
             break;
           case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
             $new = ipull($this->getNewValue(), 'dst');
             $old = ipull($this->getOldValue(), 'dst');
             $add = array_diff($new, $old);
             $add_value = reset($add);
             $add_handle = $this->getHandle($add_value);
             if ($add_handle->getPolicyFiltered()) {
               return true;
             }
             return false;
             break;
           default:
             break;
         }
         break;
     }
 
     return false;
   }
 
   public function shouldHideForMail(array $xactions) {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_TOKEN:
         return true;
       case PhabricatorTransactions::TYPE_BUILDABLE:
         switch ($this->getNewValue()) {
           case HarbormasterBuildable::STATUS_FAILED:
             // For now, only ever send mail when builds fail. We might let
             // you customize this later, but in most cases this is probably
             // completely uninteresting.
             return false;
         }
         return true;
      case PhabricatorTransactions::TYPE_EDGE:
         $edge_type = $this->getMetadataValue('edge:type');
         switch ($edge_type) {
           case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
           case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
             return true;
             break;
           default:
             break;
         }
         break;
     }
 
     // If a transaction publishes an inline comment:
     //
     //   - Don't show it if there are other kinds of transactions. The
     //     rationale here is that application mail will make the presence
     //     of inline comments obvious enough by including them prominently
     //     in the body. We could change this in the future if the obviousness
     //     needs to be increased.
     //   - If there are only inline transactions, only show the first
     //     transaction. The rationale is that seeing multiple "added an inline
     //     comment" transactions is not useful.
 
     if ($this->isInlineCommentTransaction()) {
       foreach ($xactions as $xaction) {
         if (!$xaction->isInlineCommentTransaction()) {
           return true;
         }
       }
       return ($this !== head($xactions));
     }
 
     return $this->shouldHide();
   }
 
   public function shouldHideForFeed() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_TOKEN:
         return true;
       case PhabricatorTransactions::TYPE_BUILDABLE:
         switch ($this->getNewValue()) {
           case HarbormasterBuildable::STATUS_FAILED:
             // For now, don't notify on build passes either. These are pretty
             // high volume and annoying, with very little present value. We
             // might want to turn them back on in the specific case of
             // build successes on the current document?
             return false;
         }
         return true;
      case PhabricatorTransactions::TYPE_EDGE:
         $edge_type = $this->getMetadataValue('edge:type');
         switch ($edge_type) {
           case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
           case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
             return true;
             break;
           default:
             break;
         }
         break;
      case PhabricatorTransactions::TYPE_INLINESTATE:
        return true;
     }
 
     return $this->shouldHide();
   }
 
   public function getTitleForMail() {
     return id(clone $this)->setRenderingTarget('text')->getTitle();
   }
 
   public function getBodyForMail() {
     if ($this->isInlineCommentTransaction()) {
       // We don't return inline comment content as mail body content, because
       // applications need to contextualize it (by adding line numbers, for
       // example) in order for it to make sense.
       return null;
     }
 
     $comment = $this->getComment();
     if ($comment && strlen($comment->getContent())) {
       return $comment->getContent();
     }
 
     return null;
   }
 
   public function getNoEffectDescription() {
 
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         return pht('You can not post an empty comment.');
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
         return pht(
           'This %s already has that view policy.',
           $this->getApplicationObjectTypeName());
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         return pht(
           'This %s already has that edit policy.',
           $this->getApplicationObjectTypeName());
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
         return pht(
           'This %s already has that join policy.',
           $this->getApplicationObjectTypeName());
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         return pht(
           'All users are already subscribed to this %s.',
           $this->getApplicationObjectTypeName());
       case PhabricatorTransactions::TYPE_EDGE:
         return pht('Edges already exist; transaction has no effect.');
     }
 
     return pht('Transaction has no effect.');
   }
 
   public function getTitle() {
     $author_phid = $this->getAuthorPHID();
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         return pht(
           '%s added a comment.',
           $this->renderHandleLink($author_phid));
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
         return pht(
           '%s changed the visibility of this %s from "%s" to "%s".',
           $this->renderHandleLink($author_phid),
           $this->getApplicationObjectTypeName(),
           $this->renderPolicyName($old, 'old'),
           $this->renderPolicyName($new, 'new'));
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         return pht(
           '%s changed the edit policy of this %s from "%s" to "%s".',
           $this->renderHandleLink($author_phid),
           $this->getApplicationObjectTypeName(),
           $this->renderPolicyName($old, 'old'),
           $this->renderPolicyName($new, 'new'));
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
         return pht(
           '%s changed the join policy of this %s from "%s" to "%s".',
           $this->renderHandleLink($author_phid),
           $this->getApplicationObjectTypeName(),
           $this->renderPolicyName($old, 'old'),
           $this->renderPolicyName($new, 'new'));
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         $add = array_diff($new, $old);
         $rem = array_diff($old, $new);
 
         if ($add && $rem) {
           return pht(
             '%s edited subscriber(s), added %d: %s; removed %d: %s.',
             $this->renderHandleLink($author_phid),
             count($add),
             $this->renderSubscriberList($add, 'add'),
             count($rem),
             $this->renderSubscriberList($rem, 'rem'));
         } else if ($add) {
           return pht(
             '%s added %d subscriber(s): %s.',
             $this->renderHandleLink($author_phid),
             count($add),
             $this->renderSubscriberList($add, 'add'));
         } else if ($rem) {
           return pht(
             '%s removed %d subscriber(s): %s.',
             $this->renderHandleLink($author_phid),
             count($rem),
             $this->renderSubscriberList($rem, 'rem'));
         } else {
           // This is used when rendering previews, before the user actually
           // selects any CCs.
           return pht(
             '%s updated subscribers...',
             $this->renderHandleLink($author_phid));
         }
         break;
       case PhabricatorTransactions::TYPE_EDGE:
         $new = ipull($new, 'dst');
         $old = ipull($old, 'dst');
         $add = array_diff($new, $old);
         $rem = array_diff($old, $new);
         $type = $this->getMetadata('edge:type');
         $type = head($type);
 
         $type_obj = PhabricatorEdgeType::getByConstant($type);
 
         if ($add && $rem) {
           return $type_obj->getTransactionEditString(
             $this->renderHandleLink($author_phid),
             new PhutilNumber(count($add) + count($rem)),
             new PhutilNumber(count($add)),
             $this->renderHandleList($add),
             new PhutilNumber(count($rem)),
             $this->renderHandleList($rem));
         } else if ($add) {
           return $type_obj->getTransactionAddString(
             $this->renderHandleLink($author_phid),
             new PhutilNumber(count($add)),
             $this->renderHandleList($add));
         } else if ($rem) {
           return $type_obj->getTransactionRemoveString(
             $this->renderHandleLink($author_phid),
             new PhutilNumber(count($rem)),
             $this->renderHandleList($rem));
         } else {
           return $type_obj->getTransactionPreviewString(
             $this->renderHandleLink($author_phid));
         }
 
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getTransactionCustomField();
         if ($field) {
           return $field->getApplicationTransactionTitle($this);
         } else {
           return pht(
             '%s edited a custom field.',
             $this->renderHandleLink($author_phid));
         }
 
       case PhabricatorTransactions::TYPE_TOKEN:
         if ($old && $new) {
           return pht(
             '%s updated a token.',
             $this->renderHandleLink($author_phid));
         } else if ($old) {
           return pht(
             '%s rescinded a token.',
             $this->renderHandleLink($author_phid));
         } else {
           return pht(
             '%s awarded a token.',
             $this->renderHandleLink($author_phid));
         }
 
       case PhabricatorTransactions::TYPE_BUILDABLE:
         switch ($this->getNewValue()) {
           case HarbormasterBuildable::STATUS_BUILDING:
             return pht(
               '%s started building %s.',
               $this->renderHandleLink($author_phid),
               $this->renderHandleLink(
                 $this->getMetadataValue('harbormaster:buildablePHID')));
           case HarbormasterBuildable::STATUS_PASSED:
             return pht(
               '%s completed building %s.',
               $this->renderHandleLink($author_phid),
               $this->renderHandleLink(
                 $this->getMetadataValue('harbormaster:buildablePHID')));
           case HarbormasterBuildable::STATUS_FAILED:
             return pht(
               '%s failed to build %s!',
               $this->renderHandleLink($author_phid),
               $this->renderHandleLink(
                 $this->getMetadataValue('harbormaster:buildablePHID')));
           default:
             return null;
         }
 
       case PhabricatorTransactions::TYPE_INLINESTATE:
         $done = 0;
         $undone = 0;
         foreach ($new as $phid => $state) {
           if ($state == PhabricatorInlineCommentInterface::STATE_DONE) {
             $done++;
           } else {
             $undone++;
           }
         }
         if ($done && $undone) {
           return pht(
             '%s marked %s inline comment(s) as done and %s inline comment(s) '.
             'as not done.',
             $this->renderHandleLink($author_phid),
             new PhutilNumber($done),
             new PhutilNumber($undone));
         } else if ($done) {
           return pht(
             '%s marked %s inline comment(s) as done.',
             $this->renderHandleLink($author_phid),
             new PhutilNumber($done));
         } else {
           return pht(
             '%s marked %s inline comment(s) as not done.',
             $this->renderHandleLink($author_phid),
             new PhutilNumber($undone));
         }
         break;
 
       default:
         return pht(
           '%s edited this %s.',
           $this->renderHandleLink($author_phid),
           $this->getApplicationObjectTypeName());
     }
   }
 
   public function getTitleForFeed() {
     $author_phid = $this->getAuthorPHID();
     $object_phid = $this->getObjectPHID();
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         return pht(
           '%s added a comment to %s.',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($object_phid));
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
         return pht(
           '%s changed the visibility for %s.',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($object_phid));
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         return pht(
           '%s changed the edit policy for %s.',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($object_phid));
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
         return pht(
           '%s changed the join policy for %s.',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($object_phid));
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         return pht(
           '%s updated subscribers of %s.',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($object_phid));
       case PhabricatorTransactions::TYPE_EDGE:
         $new = ipull($new, 'dst');
         $old = ipull($old, 'dst');
         $add = array_diff($new, $old);
         $rem = array_diff($old, $new);
         $type = $this->getMetadata('edge:type');
         $type = head($type);
 
         $type_obj = PhabricatorEdgeType::getByConstant($type);
 
         if ($add && $rem) {
           return $type_obj->getFeedEditString(
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid),
             new PhutilNumber(count($add) + count($rem)),
             new PhutilNumber(count($add)),
             $this->renderHandleList($add),
             new PhutilNumber(count($rem)),
             $this->renderHandleList($rem));
         } else if ($add) {
           return $type_obj->getFeedAddString(
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid),
             new PhutilNumber(count($add)),
             $this->renderHandleList($add));
         } else if ($rem) {
           return $type_obj->getFeedRemoveString(
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid),
             new PhutilNumber(count($rem)),
             $this->renderHandleList($rem));
         } else {
           return pht(
             '%s edited edge metadata for %s.',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid));
         }
 
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getTransactionCustomField();
         if ($field) {
           return $field->getApplicationTransactionTitleForFeed($this);
         } else {
           return pht(
             '%s edited a custom field on %s.',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid));
         }
       case PhabricatorTransactions::TYPE_BUILDABLE:
         switch ($this->getNewValue()) {
           case HarbormasterBuildable::STATUS_BUILDING:
             return pht(
               '%s started building %s for %s.',
               $this->renderHandleLink($author_phid),
               $this->renderHandleLink(
                 $this->getMetadataValue('harbormaster:buildablePHID')),
               $this->renderHandleLink($object_phid));
           case HarbormasterBuildable::STATUS_PASSED:
             return pht(
               '%s completed building %s for %s.',
               $this->renderHandleLink($author_phid),
               $this->renderHandleLink(
                 $this->getMetadataValue('harbormaster:buildablePHID')),
               $this->renderHandleLink($object_phid));
           case HarbormasterBuildable::STATUS_FAILED:
             return pht(
               '%s failed to build %s for %s.',
               $this->renderHandleLink($author_phid),
               $this->renderHandleLink(
                 $this->getMetadataValue('harbormaster:buildablePHID')),
               $this->renderHandleLink($object_phid));
           default:
             return null;
         }
 
     }
 
     return $this->getTitle();
   }
 
   public function getMarkupFieldsForFeed(PhabricatorFeedStory $story) {
     $fields = array();
 
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         $text = $this->getComment()->getContent();
         if (strlen($text)) {
           $fields[] = 'comment/'.$this->getID();
         }
         break;
     }
 
     return $fields;
   }
 
   public function getMarkupTextForFeed(PhabricatorFeedStory $story, $field) {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         $text = $this->getComment()->getContent();
         return PhabricatorMarkupEngine::summarize($text);
     }
 
     return null;
   }
 
   public function getBodyForFeed(PhabricatorFeedStory $story) {
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     $body = null;
 
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         $text = $this->getComment()->getContent();
         if (strlen($text)) {
           $body = $story->getMarkupFieldOutput('comment/'.$this->getID());
         }
         break;
     }
 
     return $body;
   }
 
   public function getActionStrength() {
     if ($this->isInlineCommentTransaction()) {
       return 0.25;
     }
 
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         return 0.5;
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         $old = $this->getOldValue();
         $new = $this->getNewValue();
 
         $add = array_diff($old, $new);
         $rem = array_diff($new, $old);
 
         // If this action is the actor subscribing or unsubscribing themselves,
         // it is less interesting. In particular, if someone makes a comment and
         // also implicitly subscribes themselves, we should treat the
         // transaction group as "comment", not "subscribe". In this specific
         // case (one affected user, and that affected user it the actor),
         // decrease the action strength.
 
         if ((count($add) + count($rem)) != 1) {
           // Not exactly one CC change.
           break;
         }
 
         $affected_phid = head(array_merge($add, $rem));
         if ($affected_phid != $this->getAuthorPHID()) {
           // Affected user is someone else.
           break;
         }
 
         // Make this weaker than TYPE_COMMENT.
         return 0.25;
     }
     return 1.0;
   }
 
   public function isCommentTransaction() {
     if ($this->hasComment()) {
       return true;
     }
 
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         return true;
     }
 
     return false;
   }
 
   public function isInlineCommentTransaction() {
     return false;
   }
 
   public function getActionName() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         return pht('Commented On');
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
         return pht('Changed Policy');
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         return pht('Changed Subscribers');
       case PhabricatorTransactions::TYPE_BUILDABLE:
         switch ($this->getNewValue()) {
           case HarbormasterBuildable::STATUS_PASSED:
             return pht('Build Passed');
           case HarbormasterBuildable::STATUS_FAILED:
             return pht('Build Failed');
           default:
             return pht('Build Status');
         }
       default:
         return pht('Updated');
     }
   }
 
   public function getMailTags() {
     return array();
   }
 
   public function hasChangeDetails() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getTransactionCustomField();
         if ($field) {
           return $field->getApplicationTransactionHasChangeDetails($this);
         }
         break;
     }
     return false;
   }
 
   public function renderChangeDetails(PhabricatorUser $viewer) {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getTransactionCustomField();
         if ($field) {
           return $field->getApplicationTransactionChangeDetails($this, $viewer);
         }
         break;
     }
 
     return $this->renderTextCorpusChangeDetails(
       $viewer,
       $this->getOldValue(),
       $this->getNewValue());
   }
 
   public function renderTextCorpusChangeDetails(
     PhabricatorUser $viewer,
     $old,
     $new) {
 
     require_celerity_resource('differential-changeset-view-css');
 
     $view = id(new PhabricatorApplicationTransactionTextDiffDetailView())
       ->setUser($viewer)
       ->setOldText($old)
       ->setNewText($new);
 
     return $view->render();
   }
 
   public function attachTransactionGroup(array $group) {
-    assert_instances_of($group, 'PhabricatorApplicationTransaction');
+    assert_instances_of($group, __CLASS__);
     $this->transactionGroup = $group;
     return $this;
   }
 
   public function getTransactionGroup() {
     return $this->transactionGroup;
   }
 
   /**
    * Should this transaction be visually grouped with an existing transaction
    * group?
    *
    * @param list<PhabricatorApplicationTransaction> List of transactions.
    * @return bool True to display in a group with the other transactions.
    */
   public function shouldDisplayGroupWith(array $group) {
     $this_source = null;
     if ($this->getContentSource()) {
       $this_source = $this->getContentSource()->getSource();
     }
 
     foreach ($group as $xaction) {
       // Don't group transactions by different authors.
       if ($xaction->getAuthorPHID() != $this->getAuthorPHID()) {
         return false;
       }
 
       // Don't group transactions for different objects.
       if ($xaction->getObjectPHID() != $this->getObjectPHID()) {
         return false;
       }
 
       // Don't group anything into a group which already has a comment.
       if ($xaction->isCommentTransaction()) {
         return false;
       }
 
       // Don't group transactions from different content sources.
       $other_source = null;
       if ($xaction->getContentSource()) {
         $other_source = $xaction->getContentSource()->getSource();
       }
 
       if ($other_source != $this_source) {
         return false;
       }
 
       // Don't group transactions which happened more than 2 minutes apart.
       $apart = abs($xaction->getDateCreated() - $this->getDateCreated());
       if ($apart > (60 * 2)) {
         return false;
       }
     }
 
     return true;
   }
 
   public function renderExtraInformationLink() {
     $herald_xscript_id = $this->getMetadataValue('herald:transcriptID');
 
     if ($herald_xscript_id) {
       return phutil_tag(
         'a',
         array(
           'href' => '/herald/transcript/'.$herald_xscript_id.'/',
         ),
         pht('View Herald Transcript'));
     }
 
     return null;
   }
 
   public function renderAsTextForDoorkeeper(
     DoorkeeperFeedStoryPublisher $publisher,
     PhabricatorFeedStory $story,
     array $xactions) {
 
     $text = array();
     $body = array();
 
     foreach ($xactions as $xaction) {
       $xaction_body = $xaction->getBodyForMail();
       if ($xaction_body !== null) {
         $body[] = $xaction_body;
       }
 
       if ($xaction->shouldHideForMail($xactions)) {
         continue;
       }
 
       $old_target = $xaction->getRenderingTarget();
-      $new_target = PhabricatorApplicationTransaction::TARGET_TEXT;
+      $new_target = self::TARGET_TEXT;
       $xaction->setRenderingTarget($new_target);
 
       if ($publisher->getRenderWithImpliedContext()) {
         $text[] = $xaction->getTitle();
       } else {
         $text[] = $xaction->getTitleForFeed();
       }
 
       $xaction->setRenderingTarget($old_target);
     }
 
     $text = implode("\n", $text);
     $body = implode("\n\n", $body);
 
     return rtrim($text."\n\n".$body);
   }
 
 
 
 /* -(  PhabricatorPolicyInterface Implementation  )-------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
       PhabricatorPolicyCapability::CAN_EDIT,
     );
   }
 
   public function getPolicy($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         return $this->getViewPolicy();
       case PhabricatorPolicyCapability::CAN_EDIT:
         return $this->getEditPolicy();
     }
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     return ($viewer->getPHID() == $this->getAuthorPHID());
   }
 
   public function describeAutomaticCapability($capability) {
     return pht(
       'Transactions are visible to users that can see the object which was '.
       'acted upon. Some transactions - in particular, comments - are '.
       'editable by the transaction author.');
   }
 
 
 /* -(  PhabricatorDestructibleInterface  )----------------------------------- */
 
 
   public function destroyObjectPermanently(
     PhabricatorDestructionEngine $engine) {
 
     $this->openTransaction();
       $comment_template = null;
       try {
         $comment_template = $this->getApplicationTransactionCommentObject();
       } catch (Exception $ex) {
         // Continue; no comments for these transactions.
       }
 
       if ($comment_template) {
         $comments = $comment_template->loadAllWhere(
           'transactionPHID = %s',
           $this->getPHID());
         foreach ($comments as $comment) {
           $engine->destroyObject($comment);
         }
       }
 
       $this->delete();
     $this->saveTransaction();
   }
 
 
 }
diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php
index 77cc6a6371..311c7e8b9f 100644
--- a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php
+++ b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php
@@ -1,235 +1,235 @@
 <?php
 
 /**
  * @concrete-extensible
  */
 class PhabricatorApplicationTransactionCommentView extends AphrontView {
 
   private $submitButtonName;
   private $action;
 
   private $previewPanelID;
   private $previewTimelineID;
   private $previewToggleID;
   private $formID;
   private $statusID;
   private $commentID;
   private $draft;
   private $requestURI;
   private $showPreview = true;
   private $objectPHID;
   private $headerText;
 
   public function setObjectPHID($object_phid) {
     $this->objectPHID = $object_phid;
     return $this;
   }
 
   public function getObjectPHID() {
     return $this->objectPHID;
   }
 
   public function setShowPreview($show_preview) {
     $this->showPreview = $show_preview;
     return $this;
   }
 
   public function getShowPreview() {
     return $this->showPreview;
   }
 
   public function setRequestURI(PhutilURI $request_uri) {
     $this->requestURI = $request_uri;
     return $this;
   }
   public function getRequestURI() {
     return $this->requestURI;
   }
 
   public function setDraft(PhabricatorDraft $draft) {
     $this->draft = $draft;
     return $this;
   }
 
   public function getDraft() {
     return $this->draft;
   }
 
   public function setSubmitButtonName($submit_button_name) {
     $this->submitButtonName = $submit_button_name;
     return $this;
   }
 
   public function getSubmitButtonName() {
     return $this->submitButtonName;
   }
 
   public function setAction($action) {
     $this->action = $action;
     return $this;
   }
 
   public function getAction() {
     return $this->action;
   }
 
   public function setHeaderText($text) {
     $this->headerText = $text;
     return $this;
   }
 
   public function render() {
 
     $user = $this->getUser();
     if (!$user->isLoggedIn()) {
       $uri = id(new PhutilURI('/login/'))
         ->setQueryParam('next', (string) $this->getRequestURI());
       return id(new PHUIObjectBoxView())
         ->setFlush(true)
         ->setHeaderText(pht('Add Comment'))
         ->appendChild(
           javelin_tag(
             'a',
             array(
               'class' => 'login-to-comment button',
               'href' => $uri,
             ),
             pht('Login to Comment')));
     }
 
     $data = array();
 
     $comment = $this->renderCommentPanel();
 
     if ($this->getShowPreview()) {
       $preview = $this->renderPreviewPanel();
     } else {
       $preview = null;
     }
 
     Javelin::initBehavior(
       'phabricator-transaction-comment-form',
       array(
         'formID'        => $this->getFormID(),
         'timelineID'    => $this->getPreviewTimelineID(),
         'panelID'       => $this->getPreviewPanelID(),
         'statusID'      => $this->getStatusID(),
         'commentID'     => $this->getCommentID(),
 
         'loadingString' => pht('Loading Preview...'),
         'savingString'  => pht('Saving Draft...'),
         'draftString'   => pht('Saved Draft'),
 
         'showPreview'   => $this->getShowPreview(),
 
         'actionURI'     => $this->getAction(),
       ));
 
     $comment_box = id(new PHUIObjectBoxView())
       ->setFlush(true)
       ->setHeaderText($this->headerText)
       ->appendChild($comment);
 
     return array($comment_box, $preview);
   }
 
   private function renderCommentPanel() {
     $status = phutil_tag(
       'div',
       array(
         'id' => $this->getStatusID(),
       ),
       '');
 
     $draft_comment = '';
     $draft_key = null;
     if ($this->getDraft()) {
       $draft_comment = $this->getDraft()->getDraft();
       $draft_key = $this->getDraft()->getDraftKey();
     }
 
     if (!$this->getObjectPHID()) {
-      throw new Exception('Call setObjectPHID() before render()!');
+      throw new PhutilInvalidStateException('setObjectPHID', 'render');
     }
 
     return id(new AphrontFormView())
       ->setUser($this->getUser())
       ->addSigil('transaction-append')
       ->setWorkflow(true)
       ->setMetadata(
         array(
           'objectPHID' => $this->getObjectPHID(),
         ))
       ->setAction($this->getAction())
       ->setID($this->getFormID())
       ->addHiddenInput('__draft__', $draft_key)
       ->appendChild(
         id(new PhabricatorRemarkupControl())
           ->setID($this->getCommentID())
           ->setName('comment')
           ->setLabel(pht('Comment'))
           ->setUser($this->getUser())
           ->setValue($draft_comment))
       ->appendChild(
         id(new AphrontFormSubmitControl())
           ->setValue($this->getSubmitButtonName()))
       ->appendChild(
         id(new AphrontFormMarkupControl())
           ->setValue($status));
   }
 
   private function renderPreviewPanel() {
 
     $preview = id(new PHUITimelineView())
       ->setID($this->getPreviewTimelineID());
 
     return phutil_tag(
       'div',
       array(
         'id'    => $this->getPreviewPanelID(),
         'style' => 'display: none',
       ),
       $preview);
   }
 
   private function getPreviewPanelID() {
     if (!$this->previewPanelID) {
       $this->previewPanelID = celerity_generate_unique_node_id();
     }
     return $this->previewPanelID;
   }
 
   private function getPreviewTimelineID() {
     if (!$this->previewTimelineID) {
       $this->previewTimelineID = celerity_generate_unique_node_id();
     }
     return $this->previewTimelineID;
   }
 
   public function setFormID($id) {
     $this->formID = $id;
     return $this;
   }
 
   private function getFormID() {
     if (!$this->formID) {
       $this->formID = celerity_generate_unique_node_id();
     }
     return $this->formID;
   }
 
   private function getStatusID() {
     if (!$this->statusID) {
       $this->statusID = celerity_generate_unique_node_id();
     }
     return $this->statusID;
   }
 
   private function getCommentID() {
     if (!$this->commentID) {
       $this->commentID = celerity_generate_unique_node_id();
     }
     return $this->commentID;
   }
 
 }
diff --git a/src/infrastructure/customfield/field/PhabricatorCustomField.php b/src/infrastructure/customfield/field/PhabricatorCustomField.php
index ec5a02337e..28110e7334 100644
--- a/src/infrastructure/customfield/field/PhabricatorCustomField.php
+++ b/src/infrastructure/customfield/field/PhabricatorCustomField.php
@@ -1,1381 +1,1381 @@
 <?php
 
 /**
  * @task apps         Building Applications with Custom Fields
  * @task core         Core Properties and Field Identity
  * @task proxy        Field Proxies
  * @task context      Contextual Data
  * @task render       Rendering Utilities
  * @task storage      Field Storage
  * @task edit         Integration with Edit Views
  * @task view         Integration with Property Views
  * @task list         Integration with List views
  * @task appsearch    Integration with ApplicationSearch
  * @task appxaction   Integration with ApplicationTransactions
  * @task xactionmail  Integration with Transaction Mail
  * @task globalsearch Integration with Global Search
  * @task herald       Integration with Herald
  */
 abstract class PhabricatorCustomField {
 
   private $viewer;
   private $object;
   private $proxy;
 
   const ROLE_APPLICATIONTRANSACTIONS  = 'ApplicationTransactions';
   const ROLE_TRANSACTIONMAIL          = 'ApplicationTransactions.mail';
   const ROLE_APPLICATIONSEARCH        = 'ApplicationSearch';
   const ROLE_STORAGE                  = 'storage';
   const ROLE_DEFAULT                  = 'default';
   const ROLE_EDIT                     = 'edit';
   const ROLE_VIEW                     = 'view';
   const ROLE_LIST                     = 'list';
   const ROLE_GLOBALSEARCH             = 'GlobalSearch';
   const ROLE_CONDUIT                  = 'conduit';
   const ROLE_HERALD                   = 'herald';
 
 
 /* -(  Building Applications with Custom Fields  )--------------------------- */
 
 
   /**
    * @task apps
    */
   public static function getObjectFields(
     PhabricatorCustomFieldInterface $object,
     $role) {
 
     try {
       $attachment = $object->getCustomFields();
     } catch (PhabricatorDataNotAttachedException $ex) {
       $attachment = new PhabricatorCustomFieldAttachment();
       $object->attachCustomFields($attachment);
     }
 
     try {
       $field_list = $attachment->getCustomFieldList($role);
     } catch (PhabricatorCustomFieldNotAttachedException $ex) {
       $base_class = $object->getCustomFieldBaseClass();
 
       $spec = $object->getCustomFieldSpecificationForRole($role);
       if (!is_array($spec)) {
         $obj_class = get_class($object);
         throw new Exception(
           "Expected an array from getCustomFieldSpecificationForRole() for ".
           "object of class '{$obj_class}'.");
       }
 
-      $fields = PhabricatorCustomField::buildFieldList(
+      $fields = self::buildFieldList(
         $base_class,
         $spec,
         $object);
 
       foreach ($fields as $key => $field) {
         if (!$field->shouldEnableForRole($role)) {
           unset($fields[$key]);
         }
       }
 
       foreach ($fields as $field) {
         $field->setObject($object);
       }
 
       $field_list = new PhabricatorCustomFieldList($fields);
       $attachment->addCustomFieldList($role, $field_list);
     }
 
     return $field_list;
   }
 
 
   /**
    * @task apps
    */
   public static function getObjectField(
     PhabricatorCustomFieldInterface $object,
     $role,
     $field_key) {
 
     $fields = self::getObjectFields($object, $role)->getFields();
 
     return idx($fields, $field_key);
   }
 
 
   /**
    * @task apps
    */
   public static function buildFieldList($base_class, array $spec, $object) {
     $field_objects = id(new PhutilSymbolLoader())
       ->setAncestorClass($base_class)
       ->loadObjects();
 
     $fields = array();
     $from_map = array();
     foreach ($field_objects as $field_object) {
       $current_class = get_class($field_object);
       foreach ($field_object->createFields($object) as $field) {
         $key = $field->getFieldKey();
         if (isset($fields[$key])) {
           $original_class = $from_map[$key];
           throw new Exception(
             "Both '{$original_class}' and '{$current_class}' define a custom ".
             "field with field key '{$key}'. Field keys must be unique.");
         }
         $from_map[$key] = $current_class;
         $fields[$key] = $field;
       }
     }
 
     foreach ($fields as $key => $field) {
       if (!$field->isFieldEnabled()) {
         unset($fields[$key]);
       }
     }
 
     $fields = array_select_keys($fields, array_keys($spec)) + $fields;
 
     foreach ($spec as $key => $config) {
       if (empty($fields[$key])) {
         continue;
       }
       if (!empty($config['disabled'])) {
         if ($fields[$key]->canDisableField()) {
           unset($fields[$key]);
         }
       }
     }
 
     return $fields;
   }
 
 
 /* -(  Core Properties and Field Identity  )--------------------------------- */
 
 
   /**
    * Return a key which uniquely identifies this field, like
    * "mycompany:dinosaur:count". Normally you should provide some level of
    * namespacing to prevent collisions.
    *
    * @return string String which uniquely identifies this field.
    * @task core
    */
   public function getFieldKey() {
     if ($this->proxy) {
       return $this->proxy->getFieldKey();
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException(
       $this,
       $field_key_is_incomplete = true);
   }
 
 
   /**
    * Return a human-readable field name.
    *
    * @return string Human readable field name.
    * @task core
    */
   public function getFieldName() {
     if ($this->proxy) {
       return $this->proxy->getFieldName();
     }
     return $this->getFieldKey();
   }
 
 
   /**
    * Return a short, human-readable description of the field's behavior. This
    * provides more context to administrators when they are customizing fields.
    *
    * @return string|null Optional human-readable description.
    * @task core
    */
   public function getFieldDescription() {
     if ($this->proxy) {
       return $this->proxy->getFieldDescription();
     }
     return null;
   }
 
 
   /**
    * Most field implementations are unique, in that one class corresponds to
    * one field. However, some field implementations are general and a single
    * implementation may drive several fields.
    *
    * For general implementations, the general field implementation can return
    * multiple field instances here.
    *
    * @param object The object to create fields for.
    * @return list<PhabricatorCustomField> List of fields.
    * @task core
    */
   public function createFields($object) {
     return array($this);
   }
 
 
   /**
    * You can return `false` here if the field should not be enabled for any
    * role. For example, it might depend on something (like an application or
    * library) which isn't installed, or might have some global configuration
    * which allows it to be disabled.
    *
    * @return bool False to completely disable this field for all roles.
    * @task core
    */
   public function isFieldEnabled() {
     if ($this->proxy) {
       return $this->proxy->isFieldEnabled();
     }
     return true;
   }
 
 
   /**
    * Low level selector for field availability. Fields can appear in different
    * roles (like an edit view, a list view, etc.), but not every field needs
    * to appear everywhere. Fields that are disabled in a role won't appear in
    * that context within applications.
    *
    * Normally, you do not need to override this method. Instead, override the
    * methods specific to roles you want to enable. For example, implement
    * @{method:shouldUseStorage()} to activate the `'storage'` role.
    *
    * @return bool True to enable the field for the given role.
    * @task core
    */
   public function shouldEnableForRole($role) {
 
     // NOTE: All of these calls proxy individually, so we don't need to
     // proxy this call as a whole.
 
     switch ($role) {
       case self::ROLE_APPLICATIONTRANSACTIONS:
         return $this->shouldAppearInApplicationTransactions();
       case self::ROLE_APPLICATIONSEARCH:
         return $this->shouldAppearInApplicationSearch();
       case self::ROLE_STORAGE:
         return $this->shouldUseStorage();
       case self::ROLE_EDIT:
         return $this->shouldAppearInEditView();
       case self::ROLE_VIEW:
         return $this->shouldAppearInPropertyView();
       case self::ROLE_LIST:
         return $this->shouldAppearInListView();
       case self::ROLE_GLOBALSEARCH:
         return $this->shouldAppearInGlobalSearch();
       case self::ROLE_CONDUIT:
         return $this->shouldAppearInConduitDictionary();
       case self::ROLE_TRANSACTIONMAIL:
         return $this->shouldAppearInTransactionMail();
       case self::ROLE_HERALD:
         return $this->shouldAppearInHerald();
       case self::ROLE_DEFAULT:
         return true;
       default:
         throw new Exception("Unknown field role '{$role}'!");
     }
   }
 
 
   /**
    * Allow administrators to disable this field. Most fields should allow this,
    * but some are fundamental to the behavior of the application and can be
    * locked down to avoid chaos, disorder, and the decline of civilization.
    *
    * @return bool False to prevent this field from being disabled through
    *              configuration.
    * @task core
    */
   public function canDisableField() {
     return true;
   }
 
   public function shouldDisableByDefault() {
     return false;
   }
 
 
   /**
    * Return an index string which uniquely identifies this field.
    *
    * @return string Index string which uniquely identifies this field.
    * @task core
    */
   final public function getFieldIndex() {
     return PhabricatorHash::digestForIndex($this->getFieldKey());
   }
 
 
 /* -(  Field Proxies  )------------------------------------------------------ */
 
 
   /**
    * Proxies allow a field to use some other field's implementation for most
    * of their behavior while still subclassing an application field. When a
    * proxy is set for a field with @{method:setProxy}, all of its methods will
    * call through to the proxy by default.
    *
    * This is most commonly used to implement configuration-driven custom fields
    * using @{class:PhabricatorStandardCustomField}.
    *
    * This method must be overridden to return `true` before a field can accept
    * proxies.
    *
    * @return bool True if you can @{method:setProxy} this field.
    * @task proxy
    */
   public function canSetProxy() {
     if ($this instanceof PhabricatorStandardCustomFieldInterface) {
       return true;
     }
     return false;
   }
 
 
   /**
    * Set the proxy implementation for this field. See @{method:canSetProxy} for
    * discussion of field proxies.
    *
    * @param PhabricatorCustomField Field implementation.
    * @return this
    */
   final public function setProxy(PhabricatorCustomField $proxy) {
     if (!$this->canSetProxy()) {
       throw new PhabricatorCustomFieldNotProxyException($this);
     }
 
     $this->proxy = $proxy;
     return $this;
   }
 
 
   /**
    * Get the field's proxy implementation, if any. For discussion, see
    * @{method:canSetProxy}.
    *
    * @return PhabricatorCustomField|null  Proxy field, if one is set.
    */
   final public function getProxy() {
     return $this->proxy;
   }
 
 
 /* -(  Contextual Data  )---------------------------------------------------- */
 
 
   /**
    * Sets the object this field belongs to.
    *
    * @param PhabricatorCustomFieldInterface The object this field belongs to.
    * @return this
    * @task context
    */
   final public function setObject(PhabricatorCustomFieldInterface $object) {
     if ($this->proxy) {
       $this->proxy->setObject($object);
       return $this;
     }
 
     $this->object = $object;
     $this->didSetObject($object);
     return $this;
   }
 
 
   /**
    * Read object data into local field storage, if applicable.
    *
    * @param PhabricatorCustomFieldInterface The object this field belongs to.
    * @return this
    * @task context
    */
   public function readValueFromObject(PhabricatorCustomFieldInterface $object) {
     if ($this->proxy) {
       $this->proxy->readValueFromObject($object);
     }
     return $this;
   }
 
 
   /**
    * Get the object this field belongs to.
    *
    * @return PhabricatorCustomFieldInterface The object this field belongs to.
    * @task context
    */
   final public function getObject() {
     if ($this->proxy) {
       return $this->proxy->getObject();
     }
 
     return $this->object;
   }
 
 
   /**
    * This is a hook, primarily for subclasses to load object data.
    *
    * @return PhabricatorCustomFieldInterface The object this field belongs to.
    * @return void
    */
   protected function didSetObject(PhabricatorCustomFieldInterface $object) {
     return;
   }
 
 
   /**
    * @task context
    */
   final public function setViewer(PhabricatorUser $viewer) {
     if ($this->proxy) {
       $this->proxy->setViewer($viewer);
       return $this;
     }
 
     $this->viewer = $viewer;
     return $this;
   }
 
 
   /**
    * @task context
    */
   final public function getViewer() {
     if ($this->proxy) {
       return $this->proxy->getViewer();
     }
 
     return $this->viewer;
   }
 
 
   /**
    * @task context
    */
   final protected function requireViewer() {
     if ($this->proxy) {
       return $this->proxy->requireViewer();
     }
 
     if (!$this->viewer) {
       throw new PhabricatorCustomFieldDataNotAvailableException($this);
     }
     return $this->viewer;
   }
 
 
 /* -(  Rendering Utilities  )------------------------------------------------ */
 
 
   /**
    * @task render
    */
   protected function renderHandleList(array $handles) {
     if (!$handles) {
       return null;
     }
 
     $out = array();
     foreach ($handles as $handle) {
       $out[] = $handle->renderLink();
     }
 
     return phutil_implode_html(phutil_tag('br'), $out);
   }
 
 
 /* -(  Storage  )------------------------------------------------------------ */
 
 
   /**
    * Return true to use field storage.
    *
    * Fields which can be edited by the user will most commonly use storage,
    * while some other types of fields (for instance, those which just display
    * information in some stylized way) may not. Many builtin fields do not use
    * storage because their data is available on the object itself.
    *
    * If you implement this, you must also implement @{method:getValueForStorage}
    * and @{method:setValueFromStorage}.
    *
    * @return bool True to use storage.
    * @task storage
    */
   public function shouldUseStorage() {
     if ($this->proxy) {
       return $this->proxy->shouldUseStorage();
     }
     return false;
   }
 
 
   /**
    * Return a new, empty storage object. This should be a subclass of
    * @{class:PhabricatorCustomFieldStorage} which is bound to the application's
    * database.
    *
    * @return PhabricatorCustomFieldStorage New empty storage object.
    * @task storage
    */
   public function newStorageObject() {
     if ($this->proxy) {
       return $this->proxy->newStorageObject();
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
   /**
    * Return a serialized representation of the field value, appropriate for
    * storing in auxiliary field storage. You must implement this method if
    * you implement @{method:shouldUseStorage}.
    *
    * If the field value is a scalar, it can be returned unmodiifed. If not,
    * it should be serialized (for example, using JSON).
    *
    * @return string Serialized field value.
    * @task storage
    */
   public function getValueForStorage() {
     if ($this->proxy) {
       return $this->proxy->getValueForStorage();
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
   /**
    * Set the field's value given a serialized storage value. This is called
    * when the field is loaded; if no data is available, the value will be
    * null. You must implement this method if you implement
    * @{method:shouldUseStorage}.
    *
    * Usually, the value can be loaded directly. If it isn't a scalar, you'll
    * need to undo whatever serialization you applied in
    * @{method:getValueForStorage}.
    *
    * @param string|null Serialized field representation (from
    *                    @{method:getValueForStorage}) or null if no value has
    *                    ever been stored.
    * @return this
    * @task storage
    */
   public function setValueFromStorage($value) {
     if ($this->proxy) {
       return $this->proxy->setValueFromStorage($value);
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
 /* -(  ApplicationSearch  )-------------------------------------------------- */
 
 
   /**
    * Appearing in ApplicationSearch allows a field to be indexed and searched
    * for.
    *
    * @return bool True to appear in ApplicationSearch.
    * @task appsearch
    */
   public function shouldAppearInApplicationSearch() {
     if ($this->proxy) {
       return $this->proxy->shouldAppearInApplicationSearch();
     }
     return false;
   }
 
 
   /**
    * Return one or more indexes which this field can meaningfully query against
    * to implement ApplicationSearch.
    *
    * Normally, you should build these using @{method:newStringIndex} and
    * @{method:newNumericIndex}. For example, if a field holds a numeric value
    * it might return a single numeric index:
    *
    *   return array($this->newNumericIndex($this->getValue()));
    *
    * If a field holds a more complex value (like a list of users), it might
    * return several string indexes:
    *
    *   $indexes = array();
    *   foreach ($this->getValue() as $phid) {
    *     $indexes[] = $this->newStringIndex($phid);
    *   }
    *   return $indexes;
    *
    * @return list<PhabricatorCustomFieldIndexStorage> List of indexes.
    * @task appsearch
    */
   public function buildFieldIndexes() {
     if ($this->proxy) {
       return $this->proxy->buildFieldIndexes();
     }
     return array();
   }
 
 
   /**
    * Return an index against which this field can be meaningfully ordered
    * against to implement ApplicationSearch.
    *
    * This should be a single index, normally built using
    * @{method:newStringIndex} and @{method:newNumericIndex}.
    *
    * The value of the index is not used.
    *
    * Return null from this method if the field can not be ordered.
    *
    * @return PhabricatorCustomFieldIndexStorage A single index to order by.
    * @task appsearch
    */
   public function buildOrderIndex() {
     if ($this->proxy) {
       return $this->proxy->buildOrderIndex();
     }
     return null;
   }
 
 
   /**
    * Build a new empty storage object for storing string indexes. Normally,
    * this should be a concrete subclass of
    * @{class:PhabricatorCustomFieldStringIndexStorage}.
    *
    * @return PhabricatorCustomFieldStringIndexStorage Storage object.
    * @task appsearch
    */
   protected function newStringIndexStorage() {
     // NOTE: This intentionally isn't proxied, to avoid call cycles.
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
   /**
    * Build a new empty storage object for storing string indexes. Normally,
    * this should be a concrete subclass of
    * @{class:PhabricatorCustomFieldStringIndexStorage}.
    *
    * @return PhabricatorCustomFieldStringIndexStorage Storage object.
    * @task appsearch
    */
   protected function newNumericIndexStorage() {
     // NOTE: This intentionally isn't proxied, to avoid call cycles.
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
   /**
    * Build and populate storage for a string index.
    *
    * @param string String to index.
    * @return PhabricatorCustomFieldStringIndexStorage Populated storage.
    * @task appsearch
    */
   protected function newStringIndex($value) {
     if ($this->proxy) {
       return $this->proxy->newStringIndex();
     }
 
     $key = $this->getFieldIndex();
     return $this->newStringIndexStorage()
       ->setIndexKey($key)
       ->setIndexValue($value);
   }
 
 
   /**
    * Build and populate storage for a numeric index.
    *
    * @param string Numeric value to index.
    * @return PhabricatorCustomFieldNumericIndexStorage Populated storage.
    * @task appsearch
    */
   protected function newNumericIndex($value) {
     if ($this->proxy) {
       return $this->proxy->newNumericIndex();
     }
     $key = $this->getFieldIndex();
     return $this->newNumericIndexStorage()
       ->setIndexKey($key)
       ->setIndexValue($value);
   }
 
 
   /**
    * Read a query value from a request, for storage in a saved query. Normally,
    * this method should, e.g., read a string out of the request.
    *
    * @param PhabricatorApplicationSearchEngine Engine building the query.
    * @param AphrontRequest Request to read from.
    * @return wild
    * @task appsearch
    */
   public function readApplicationSearchValueFromRequest(
     PhabricatorApplicationSearchEngine $engine,
     AphrontRequest $request) {
     if ($this->proxy) {
       return $this->proxy->readApplicationSearchValueFromRequest(
         $engine,
         $request);
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
   /**
    * Constrain a query, given a field value. Generally, this method should
    * use `with...()` methods to apply filters or other constraints to the
    * query.
    *
    * @param PhabricatorApplicationSearchEngine Engine executing the query.
    * @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain.
    * @param wild Constraint provided by the user.
    * @return void
    * @task appsearch
    */
   public function applyApplicationSearchConstraintToQuery(
     PhabricatorApplicationSearchEngine $engine,
     PhabricatorCursorPagedPolicyAwareQuery $query,
     $value) {
     if ($this->proxy) {
       return $this->proxy->applyApplicationSearchConstraintToQuery(
         $engine,
         $query,
         $value);
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
   /**
    * Append search controls to the interface. If you need handles, use
    * @{method:getRequiredHandlePHIDsForApplicationSearch} to get them.
    *
    * @param PhabricatorApplicationSearchEngine Engine constructing the form.
    * @param AphrontFormView The form to update.
    * @param wild Value from the saved query.
    * @param list<PhabricatorObjectHandle> List of handles.
    * @return void
    * @task appsearch
    */
   public function appendToApplicationSearchForm(
     PhabricatorApplicationSearchEngine $engine,
     AphrontFormView $form,
     $value,
     array $handles) {
     if ($this->proxy) {
       return $this->proxy->appendToApplicationSearchForm(
         $engine,
         $form,
         $value,
         $handles);
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
   /**
    * Return a list of PHIDs which @{method:appendToApplicationSearchForm} needs
    * handles for. This is primarily useful if the field stores PHIDs and you
    * need to (for example) render a tokenizer control.
    *
    * @param wild Value from the saved query.
    * @return list<phid> List of PHIDs.
    * @task appsearch
    */
   public function getRequiredHandlePHIDsForApplicationSearch($value) {
     if ($this->proxy) {
       return $this->proxy->getRequiredHandlePHIDsForApplicationSearch($value);
     }
     return array();
   }
 
 
 /* -(  ApplicationTransactions  )-------------------------------------------- */
 
 
   /**
    * Appearing in ApplicationTrasactions allows a field to be edited using
    * standard workflows.
    *
    * @return bool True to appear in ApplicationTransactions.
    * @task appxaction
    */
   public function shouldAppearInApplicationTransactions() {
     if ($this->proxy) {
       return $this->proxy->shouldAppearInApplicationTransactions();
     }
     return false;
   }
 
 
   /**
    * @task appxaction
    */
   public function getApplicationTransactionType() {
     if ($this->proxy) {
       return $this->proxy->getApplicationTransactionType();
     }
     return PhabricatorTransactions::TYPE_CUSTOMFIELD;
   }
 
 
   /**
    * @task appxaction
    */
   public function getApplicationTransactionMetadata() {
     if ($this->proxy) {
       return $this->proxy->getApplicationTransactionMetadata();
     }
     return array();
   }
 
 
   /**
    * @task appxaction
    */
   public function getOldValueForApplicationTransactions() {
     if ($this->proxy) {
       return $this->proxy->getOldValueForApplicationTransactions();
     }
     return $this->getValueForStorage();
   }
 
 
   /**
    * @task appxaction
    */
   public function getNewValueForApplicationTransactions() {
     if ($this->proxy) {
       return $this->proxy->getNewValueForApplicationTransactions();
     }
     return $this->getValueForStorage();
   }
 
 
   /**
    * @task appxaction
    */
   public function setValueFromApplicationTransactions($value) {
     if ($this->proxy) {
       return $this->proxy->setValueFromApplicationTransactions($value);
     }
     return $this->setValueFromStorage($value);
   }
 
 
   /**
    * @task appxaction
    */
   public function getNewValueFromApplicationTransactions(
     PhabricatorApplicationTransaction $xaction) {
     if ($this->proxy) {
       return $this->proxy->getNewValueFromApplicationTransactions($xaction);
     }
     return $xaction->getNewValue();
   }
 
 
   /**
    * @task appxaction
    */
   public function getApplicationTransactionHasEffect(
     PhabricatorApplicationTransaction $xaction) {
     if ($this->proxy) {
       return $this->proxy->getApplicationTransactionHasEffect($xaction);
     }
     return ($xaction->getOldValue() !== $xaction->getNewValue());
   }
 
 
   /**
    * @task appxaction
    */
   public function applyApplicationTransactionInternalEffects(
     PhabricatorApplicationTransaction $xaction) {
     if ($this->proxy) {
       return $this->proxy->applyApplicationTransactionInternalEffects($xaction);
     }
     return;
   }
 
 
   /**
    * @task appxaction
    */
   public function getApplicationTransactionRemarkupBlocks(
     PhabricatorApplicationTransaction $xaction) {
     if ($this->proxy) {
       return $this->proxy->getApplicationTransactionRemarkupBlocks($xaction);
     }
     return array();
   }
 
 
   /**
    * @task appxaction
    */
   public function applyApplicationTransactionExternalEffects(
     PhabricatorApplicationTransaction $xaction) {
     if ($this->proxy) {
       return $this->proxy->applyApplicationTransactionExternalEffects($xaction);
     }
 
     if (!$this->shouldEnableForRole(self::ROLE_STORAGE)) {
       return;
     }
 
     $this->setValueFromApplicationTransactions($xaction->getNewValue());
     $value = $this->getValueForStorage();
 
     $table = $this->newStorageObject();
     $conn_w = $table->establishConnection('w');
 
     if ($value === null) {
       queryfx(
         $conn_w,
         'DELETE FROM %T WHERE objectPHID = %s AND fieldIndex = %s',
         $table->getTableName(),
         $this->getObject()->getPHID(),
         $this->getFieldIndex());
     } else {
       queryfx(
         $conn_w,
         'INSERT INTO %T (objectPHID, fieldIndex, fieldValue)
           VALUES (%s, %s, %s)
           ON DUPLICATE KEY UPDATE fieldValue = VALUES(fieldValue)',
         $table->getTableName(),
         $this->getObject()->getPHID(),
         $this->getFieldIndex(),
         $value);
     }
 
     return;
   }
 
 
   /**
    * Validate transactions for an object. This allows you to raise an error
    * when a transaction would set a field to an invalid value, or when a field
    * is required but no transactions provide value.
    *
    * @param PhabricatorLiskDAO Editor applying the transactions.
    * @param string Transaction type. This type is always
    *   `PhabricatorTransactions::TYPE_CUSTOMFIELD`, it is provided for
    *   convenience when constructing exceptions.
    * @param list<PhabricatorApplicationTransaction> Transactions being applied,
    *   which may be empty if this field is not being edited.
    * @return list<PhabricatorApplicationTransactionValidationError> Validation
    *   errors.
    *
    * @task appxaction
    */
   public function validateApplicationTransactions(
     PhabricatorApplicationTransactionEditor $editor,
     $type,
     array $xactions) {
     if ($this->proxy) {
       return $this->proxy->validateApplicationTransactions(
         $editor,
         $type,
         $xactions);
     }
     return array();
   }
 
   public function getApplicationTransactionTitle(
     PhabricatorApplicationTransaction $xaction) {
     if ($this->proxy) {
       return $this->proxy->getApplicationTransactionTitle(
         $xaction);
     }
 
     $author_phid = $xaction->getAuthorPHID();
     return pht(
       '%s updated this object.',
       $xaction->renderHandleLink($author_phid));
   }
 
   public function getApplicationTransactionTitleForFeed(
     PhabricatorApplicationTransaction $xaction) {
     if ($this->proxy) {
       return $this->proxy->getApplicationTransactionTitleForFeed(
         $xaction);
     }
 
     $author_phid = $xaction->getAuthorPHID();
     $object_phid = $xaction->getObjectPHID();
     return pht(
       '%s updated %s.',
       $xaction->renderHandleLink($author_phid),
       $xaction->renderHandleLink($object_phid));
   }
 
 
   public function getApplicationTransactionHasChangeDetails(
     PhabricatorApplicationTransaction $xaction) {
     if ($this->proxy) {
       return $this->proxy->getApplicationTransactionHasChangeDetails(
         $xaction);
     }
     return false;
   }
 
   public function getApplicationTransactionChangeDetails(
     PhabricatorApplicationTransaction $xaction,
     PhabricatorUser $viewer) {
     if ($this->proxy) {
       return $this->proxy->getApplicationTransactionChangeDetails(
         $xaction,
         $viewer);
     }
     return null;
   }
 
   public function getApplicationTransactionRequiredHandlePHIDs(
     PhabricatorApplicationTransaction $xaction) {
     if ($this->proxy) {
       return $this->proxy->getApplicationTransactionRequiredHandlePHIDs(
         $xaction);
     }
     return array();
   }
 
   public function shouldHideInApplicationTransactions(
     PhabricatorApplicationTransaction $xaction) {
     if ($this->proxy) {
       return $this->proxy->shouldHideInApplicationTransactions($xaction);
     }
     return false;
   }
 
   /**
    * TODO: this is only used by Diffusion right now and everything is completely
    * faked since Diffusion doesn't use ApplicationTransactions yet. This should
    * get fleshed out as we have more use cases.
    *
    * @task appxaction
    */
   public function buildApplicationTransactionMailBody(
     PhabricatorApplicationTransaction $xaction,
     PhabricatorMetaMTAMailBody $body) {
     if ($this->proxy) {
       return $this->proxy->buildApplicationTransactionMailBody($xaction, $body);
     }
     return;
   }
 
 
 /* -(  Transaction Mail  )--------------------------------------------------- */
 
 
   /**
    * @task xactionmail
    */
   public function shouldAppearInTransactionMail() {
     if ($this->proxy) {
       return $this->proxy->shouldAppearInTransactionMail();
     }
     return false;
   }
 
 
   /**
    * @task xactionmail
    */
   public function updateTransactionMailBody(
     PhabricatorMetaMTAMailBody $body,
     PhabricatorApplicationTransactionEditor $editor,
     array $xactions) {
     if ($this->proxy) {
       return $this->proxy->updateTransactionMailBody($body, $editor, $xactions);
     }
     return;
   }
 
 
 /* -(  Edit View  )---------------------------------------------------------- */
 
 
   /**
    * @task edit
    */
   public function shouldAppearInEditView() {
     if ($this->proxy) {
       return $this->proxy->shouldAppearInEditView();
     }
     return false;
   }
 
 
   /**
    * @task edit
    */
   public function readValueFromRequest(AphrontRequest $request) {
     if ($this->proxy) {
       return $this->proxy->readValueFromRequest($request);
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
   /**
    * @task edit
    */
   public function getRequiredHandlePHIDsForEdit() {
     if ($this->proxy) {
       return $this->proxy->getRequiredHandlePHIDsForEdit();
     }
     return array();
   }
 
 
   /**
    * @task edit
    */
   public function getInstructionsForEdit() {
     if ($this->proxy) {
       return $this->proxy->getInstructionsForEdit();
     }
     return null;
   }
 
 
   /**
    * @task edit
    */
   public function renderEditControl(array $handles) {
     if ($this->proxy) {
       return $this->proxy->renderEditControl($handles);
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
 /* -(  Property View  )------------------------------------------------------ */
 
 
   /**
    * @task view
    */
   public function shouldAppearInPropertyView() {
     if ($this->proxy) {
       return $this->proxy->shouldAppearInPropertyView();
     }
     return false;
   }
 
 
   /**
    * @task view
    */
   public function renderPropertyViewLabel() {
     if ($this->proxy) {
       return $this->proxy->renderPropertyViewLabel();
     }
     return $this->getFieldName();
   }
 
 
   /**
    * @task view
    */
   public function renderPropertyViewValue(array $handles) {
     if ($this->proxy) {
       return $this->proxy->renderPropertyViewValue($handles);
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
   /**
    * @task view
    */
   public function getStyleForPropertyView() {
     if ($this->proxy) {
       return $this->proxy->getStyleForPropertyView();
     }
     return 'property';
   }
 
 
   /**
    * @task view
    */
   public function getIconForPropertyView() {
     if ($this->proxy) {
       return $this->proxy->getIconForPropertyView();
     }
     return null;
   }
 
 
   /**
    * @task view
    */
   public function getRequiredHandlePHIDsForPropertyView() {
     if ($this->proxy) {
       return $this->proxy->getRequiredHandlePHIDsForPropertyView();
     }
     return array();
   }
 
 
 /* -(  List View  )---------------------------------------------------------- */
 
 
   /**
    * @task list
    */
   public function shouldAppearInListView() {
     if ($this->proxy) {
       return $this->proxy->shouldAppearInListView();
     }
     return false;
   }
 
 
   /**
    * @task list
    */
   public function renderOnListItem(PHUIObjectItemView $view) {
     if ($this->proxy) {
       return $this->proxy->renderOnListItem($view);
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
 /* -(  Global Search  )------------------------------------------------------ */
 
 
   /**
    * @task globalsearch
    */
   public function shouldAppearInGlobalSearch() {
     if ($this->proxy) {
       return $this->proxy->shouldAppearInGlobalSearch();
     }
     return false;
   }
 
 
   /**
    * @task globalsearch
    */
   public function updateAbstractDocument(
     PhabricatorSearchAbstractDocument $document) {
     if ($this->proxy) {
       return $this->proxy->updateAbstractDocument($document);
     }
     return $document;
   }
 
 
 /* -(  Conduit  )------------------------------------------------------------ */
 
 
   /**
    * @task conduit
    */
   public function shouldAppearInConduitDictionary() {
     if ($this->proxy) {
       return $this->proxy->shouldAppearInConduitDictionary();
     }
     return false;
   }
 
 
   /**
    * @task conduit
    */
   public function getConduitDictionaryValue() {
     if ($this->proxy) {
       return $this->proxy->getConduitDictionaryValue();
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
 /* -(  Herald  )------------------------------------------------------------- */
 
 
   /**
    * Return `true` to make this field available in Herald.
    *
    * @return bool True to expose the field in Herald.
    * @task herald
    */
   public function shouldAppearInHerald() {
     if ($this->proxy) {
       return $this->proxy->shouldAppearInHerald();
     }
     return false;
   }
 
 
   /**
    * Get the name of the field in Herald. By default, this uses the
    * normal field name.
    *
    * @return string Herald field name.
    * @task herald
    */
   public function getHeraldFieldName() {
     if ($this->proxy) {
       return $this->proxy->getHeraldFieldName();
     }
     return $this->getFieldName();
   }
 
 
   /**
    * Get the field value for evaluation by Herald.
    *
    * @return wild Field value.
    * @task herald
    */
   public function getHeraldFieldValue() {
     if ($this->proxy) {
       return $this->proxy->getHeraldFieldValue();
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
   /**
    * Get the available conditions for this field in Herald.
    *
    * @return list<const> List of Herald condition constants.
    * @task herald
    */
   public function getHeraldFieldConditions() {
     if ($this->proxy) {
       return $this->proxy->getHeraldFieldConditions();
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
   /**
    * Get the Herald value type for the given condition.
    *
    * @param   const       Herald condition constant.
    * @return  const|null  Herald value type, or null to use the default.
    * @task herald
    */
   public function getHeraldFieldValueType($condition) {
     if ($this->proxy) {
       return $this->proxy->getHeraldFieldValueType($condition);
     }
     return null;
   }
 
 
 }
diff --git a/src/infrastructure/daemon/bot/PhabricatorBot.php b/src/infrastructure/daemon/bot/PhabricatorBot.php
index 12ab9e663a..ef949cb9ff 100644
--- a/src/infrastructure/daemon/bot/PhabricatorBot.php
+++ b/src/infrastructure/daemon/bot/PhabricatorBot.php
@@ -1,154 +1,164 @@
 <?php
 
 /**
  * Simple IRC bot which runs as a Phabricator daemon. Although this bot is
  * somewhat useful, it is also intended to serve as a demo of how to write
  * "system agents" which communicate with Phabricator over Conduit, so you can
  * script system interactions and integrate with other systems.
  *
  * NOTE: This is super janky and experimental right now.
  */
 final class PhabricatorBot extends PhabricatorDaemon {
 
   private $handlers;
 
   private $conduit;
   private $config;
   private $pollFrequency;
 
   protected function run() {
     $argv = $this->getArgv();
     if (count($argv) !== 1) {
-      throw new Exception('usage: PhabricatorBot <json_config_file>');
+      throw new Exception(
+        pht(
+          'Usage: %s %s',
+          __CLASS__,
+          '<json_config_file>'));
     }
 
     $json_raw = Filesystem::readFile($argv[0]);
     try {
       $config = phutil_json_decode($json_raw);
     } catch (PhutilJSONParserException $ex) {
       throw new PhutilProxyException(
         pht("File '%s' is not valid JSON!", $argv[0]),
         $ex);
     }
 
     $nick                   = idx($config, 'nick', 'phabot');
     $handlers               = idx($config, 'handlers', array());
     $protocol_adapter_class = idx(
       $config,
       'protocol-adapter',
       'PhabricatorIRCProtocolAdapter');
     $this->pollFrequency = idx($config, 'poll-frequency', 1);
 
     $this->config = $config;
 
     foreach ($handlers as $handler) {
       $obj = newv($handler, array($this));
       $this->handlers[] = $obj;
     }
 
     $ca_bundle = idx($config, 'https.cabundle');
     if ($ca_bundle) {
       HTTPSFuture::setGlobalCABundleFromPath($ca_bundle);
     }
 
     $conduit_uri = idx($config, 'conduit.uri');
     if ($conduit_uri) {
-      $conduit_user = idx($config, 'conduit.user');
-      $conduit_cert = idx($config, 'conduit.cert');
+      $conduit_token = idx($config, 'conduit.token');
 
       // Normalize the path component of the URI so users can enter the
       // domain without the "/api/" part.
       $conduit_uri = new PhutilURI($conduit_uri);
 
       $conduit_host = (string)$conduit_uri->setPath('/');
       $conduit_uri = (string)$conduit_uri->setPath('/api/');
 
       $conduit = new ConduitClient($conduit_uri);
-      $response = $conduit->callMethodSynchronous(
-        'conduit.connect',
-        array(
-          'client'            => 'PhabricatorBot',
-          'clientVersion'     => '1.0',
-          'clientDescription' => php_uname('n').':'.$nick,
-          'host'              => $conduit_host,
-          'user'              => $conduit_user,
-          'certificate'       => $conduit_cert,
-        ));
+      if ($conduit_token) {
+        $conduit->setConduitToken($conduit_token);
+      } else {
+        $conduit_user = idx($config, 'conduit.user');
+        $conduit_cert = idx($config, 'conduit.cert');
+
+        $response = $conduit->callMethodSynchronous(
+          'conduit.connect',
+          array(
+            'client'            => __CLASS__,
+            'clientVersion'     => '1.0',
+            'clientDescription' => php_uname('n').':'.$nick,
+            'host'              => $conduit_host,
+            'user'              => $conduit_user,
+            'certificate'       => $conduit_cert,
+          ));
+      }
 
       $this->conduit = $conduit;
     }
 
     // Instantiate Protocol Adapter, for now follow same technique as
     // handler instantiation
     $this->protocolAdapter = newv($protocol_adapter_class, array());
     $this->protocolAdapter
       ->setConfig($this->config)
       ->connect();
 
     $this->runLoop();
 
     $this->protocolAdapter->disconnect();
   }
 
   public function getConfig($key, $default = null) {
     return idx($this->config, $key, $default);
   }
 
   private function runLoop() {
     do {
       $this->stillWorking();
 
       $messages = $this->protocolAdapter->getNextMessages($this->pollFrequency);
       if (count($messages) > 0) {
         foreach ($messages as $message) {
           $this->routeMessage($message);
         }
       }
 
       foreach ($this->handlers as $handler) {
         $handler->runBackgroundTasks();
       }
     } while (!$this->shouldExit());
 
   }
 
   public function writeMessage(PhabricatorBotMessage $message) {
     return $this->protocolAdapter->writeMessage($message);
   }
 
   private function routeMessage(PhabricatorBotMessage $message) {
     $ignore = $this->getConfig('ignore');
     if ($ignore) {
       $sender = $message->getSender();
       if ($sender && in_array($sender->getName(), $ignore)) {
         return;
       }
     }
 
     if ($message->getCommand() == 'LOG') {
       $this->log('[LOG] '.$message->getBody());
     }
 
     foreach ($this->handlers as $handler) {
       try {
         $handler->receiveMessage($message);
       } catch (Exception $ex) {
         phlog($ex);
       }
     }
   }
 
   public function getAdapter() {
     return $this->protocolAdapter;
   }
 
   public function getConduit() {
     if (empty($this->conduit)) {
       throw new Exception(
         "This bot is not configured with a Conduit uplink. Set 'conduit.uri', ".
         "'conduit.user' and 'conduit.cert' in the configuration to connect.");
     }
     return $this->conduit;
   }
 
 }
diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php
index cffa6faf35..f226d0ad76 100644
--- a/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php
+++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php
@@ -1,201 +1,200 @@
 <?php
 
 /**
  * Looks for Dxxxx, Txxxx and links to them.
  */
 final class PhabricatorBotObjectNameHandler extends PhabricatorBotHandler {
 
   /**
    * Map of PHIDs to the last mention of them (as an epoch timestamp); prevents
    * us from spamming chat when a single object is discussed.
    */
   private $recentlyMentioned = array();
 
   public function receiveMessage(PhabricatorBotMessage $original_message) {
     switch ($original_message->getCommand()) {
       case 'MESSAGE':
         $message = $original_message->getBody();
         $matches = null;
 
         $paste_ids = array();
         $commit_names = array();
         $vote_ids = array();
         $file_ids = array();
         $object_names = array();
         $output = array();
 
         $pattern =
           '@'.
-          '(?<!/)(?:^|\b)'.
+          '(?<![/:#-])(?:^|\b)'.
           '(R2D2)'.
           '(?:\b|$)'.
           '@';
 
         if (preg_match_all($pattern, $message, $matches, PREG_SET_ORDER)) {
           foreach ($matches as $match) {
             switch ($match[1]) {
               case 'R2D2':
                 $output[$match[1]] = pht('beep boop bop');
                 break;
             }
           }
         }
 
+        // Use a negative lookbehind to prevent matching "/D123", "#D123",
+        // ":D123", etc.
         $pattern =
           '@'.
-          '(?<!/)(?:^|\b)'. // Negative lookbehind prevent matching "/D123".
+          '(?<![/:#-])(?:^|\b)'.
           '([A-Z])(\d+)'.
           '(?:\b|$)'.
           '@';
 
         if (preg_match_all($pattern, $message, $matches, PREG_SET_ORDER)) {
           foreach ($matches as $match) {
             switch ($match[1]) {
               case 'P':
                 $paste_ids[] = $match[2];
                 break;
               case 'V':
                 $vote_ids[] = $match[2];
                 break;
               case 'F':
                 $file_ids[] = $match[2];
                 break;
               default:
                 $name = $match[1].$match[2];
                 switch ($name) {
                   case 'T1000':
                     $output[$name] = pht(
                       'T1000: A mimetic poly-alloy assassin controlled by '.
                       'Skynet');
                     break;
                   default:
                     $object_names[] = $name;
                     break;
                 }
                 break;
             }
           }
         }
 
         $pattern =
           '@'.
           '(?<!/)(?:^|\b)'.
           '(r[A-Z]+)([0-9a-z]{0,40})'.
           '(?:\b|$)'.
           '@';
         if (preg_match_all($pattern, $message, $matches, PREG_SET_ORDER)) {
           foreach ($matches as $match) {
             if ($match[2]) {
               $commit_names[] = $match[1].$match[2];
             } else {
               $object_names[] = $match[1];
             }
           }
         }
 
         if ($object_names) {
           $objects = $this->getConduit()->callMethodSynchronous(
             'phid.lookup',
             array(
               'names' => $object_names,
             ));
           foreach ($objects as $object) {
             $output[$object['phid']] = $object['fullName'].' - '.$object['uri'];
           }
         }
 
         if ($vote_ids) {
           foreach ($vote_ids as $vote_id) {
             $vote = $this->getConduit()->callMethodSynchronous(
               'slowvote.info',
               array(
                 'poll_id' => $vote_id,
               ));
             $output[$vote['phid']] = 'V'.$vote['id'].': '.$vote['question'].
               ' Come Vote '.$vote['uri'];
           }
         }
 
         if ($file_ids) {
           foreach ($file_ids as $file_id) {
             $file = $this->getConduit()->callMethodSynchronous(
               'file.info',
               array(
                 'id' => $file_id,
               ));
             $output[$file['phid']] = $file['objectName'].': '.
               $file['uri'].' - '.$file['name'];
           }
         }
 
         if ($paste_ids) {
           foreach ($paste_ids as $paste_id) {
             $paste = $this->getConduit()->callMethodSynchronous(
               'paste.query',
               array(
                 'ids' => array($paste_id),
               ));
             $paste = head($paste);
 
             $output[$paste['phid']] = 'P'.$paste['id'].': '.$paste['uri'].' - '.
               $paste['title'];
 
             if ($paste['language']) {
               $output[$paste['phid']] .= ' ('.$paste['language'].')';
             }
 
             $user = $this->getConduit()->callMethodSynchronous(
               'user.query',
               array(
                 'phids' => array($paste['authorPHID']),
               ));
             $user = head($user);
             if ($user) {
               $output[$paste['phid']] .= ' by '.$user['userName'];
             }
           }
         }
 
         if ($commit_names) {
           $commits = $this->getConduit()->callMethodSynchronous(
-            'diffusion.getcommits',
+            'diffusion.querycommits',
             array(
-              'commits' => $commit_names,
+              'names' => $commit_names,
             ));
-          foreach ($commits as $commit) {
-            if (isset($commit['error'])) {
-              continue;
-            }
-            $output[$commit['commitPHID']] = $commit['uri'];
+          foreach ($commits['data'] as $commit) {
+            $output[$commit['phid']] = $commit['uri'];
           }
         }
 
         foreach ($output as $phid => $description) {
 
           // Don't mention the same object more than once every 10 minutes
           // in public channels, so we avoid spamming the chat over and over
           // again for discsussions of a specific revision, for example.
 
           $target_name = $original_message->getTarget()->getName();
           if (empty($this->recentlyMentioned[$target_name])) {
             $this->recentlyMentioned[$target_name] = array();
           }
 
           $quiet_until = idx(
             $this->recentlyMentioned[$target_name],
             $phid,
             0) + (60 * 10);
 
           if (time() < $quiet_until) {
             // Remain quiet on this channel.
             continue;
           }
 
           $this->recentlyMentioned[$target_name][$phid] = time();
           $this->replyTo($original_message, $description);
         }
         break;
     }
   }
 
 }
diff --git a/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php b/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php
index 34cab99afa..c569f0c695 100644
--- a/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php
+++ b/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php
@@ -1,233 +1,233 @@
 <?php
 
 final class PhabricatorWorkerTriggerQuery
   extends PhabricatorPolicyAwareQuery {
 
   // NOTE: This is a PolicyAware query so it can work with other infrastructure
   // like handles; triggers themselves are low-level and do not have
   // meaninguful policies.
 
   const ORDER_ID = 'id';
   const ORDER_EXECUTION = 'execution';
   const ORDER_VERSION = 'version';
 
   private $ids;
   private $phids;
   private $versionMin;
   private $versionMax;
   private $nextEpochMin;
   private $nextEpochMax;
 
   private $needEvents;
   private $order = self::ORDER_ID;
 
   public function getQueryApplicationClass() {
     return null;
   }
 
   public function withIDs(array $ids) {
     $this->ids = $ids;
     return $this;
   }
 
   public function withPHIDs(array $phids) {
     $this->phids = $phids;
     return $this;
   }
 
   public function withVersionBetween($min, $max) {
     $this->versionMin = $min;
     $this->versionMax = $max;
     return $this;
   }
 
   public function withNextEventBetween($min, $max) {
     $this->nextEpochMin = $min;
     $this->nextEpochMax = $max;
     return $this;
   }
 
   public function needEvents($need_events) {
     $this->needEvents = $need_events;
     return $this;
   }
 
   /**
    * Set the result order.
    *
    * Note that using `ORDER_EXECUTION` will also filter results to include only
    * triggers which have been scheduled to execute. You should not use this
    * ordering when querying for specific triggers, e.g. by ID or PHID.
    *
    * @param const Result order.
    * @return this
    */
   public function setOrder($order) {
     $this->order = $order;
     return $this;
   }
 
   protected function nextPage(array $page) {
     // NOTE: We don't implement paging because we don't currently ever need
     // it and paging ORDER_EXCUTION is a hassle.
     throw new PhutilMethodNotImplementedException();
   }
 
   protected function loadPage() {
     $task_table = new PhabricatorWorkerTrigger();
 
     $conn_r = $task_table->establishConnection('r');
 
     $rows = queryfx_all(
       $conn_r,
       'SELECT t.* FROM %T t %Q %Q %Q %Q',
       $task_table->getTableName(),
       $this->buildJoinClause($conn_r),
       $this->buildWhereClause($conn_r),
       $this->buildOrderClause($conn_r),
       $this->buildLimitClause($conn_r));
 
     $triggers = $task_table->loadAllFromArray($rows);
 
     if ($triggers) {
       if ($this->needEvents) {
         $ids = mpull($triggers, 'getID');
 
         $events = id(new PhabricatorWorkerTriggerEvent())->loadAllWhere(
           'triggerID IN (%Ld)',
           $ids);
         $events = mpull($events, null, 'getTriggerID');
 
         foreach ($triggers as $key => $trigger) {
           $event = idx($events, $trigger->getID());
           $trigger->attachEvent($event);
         }
       }
 
       foreach ($triggers as $key => $trigger) {
         $clock_class = $trigger->getClockClass();
         if (!is_subclass_of($clock_class, 'PhabricatorTriggerClock')) {
           unset($triggers[$key]);
           continue;
         }
 
         try {
           $argv = array($trigger->getClockProperties());
           $clock = newv($clock_class, $argv);
         } catch (Exception $ex) {
           unset($triggers[$key]);
           continue;
         }
 
         $trigger->attachClock($clock);
       }
 
 
       foreach ($triggers as $key => $trigger) {
         $action_class = $trigger->getActionClass();
         if (!is_subclass_of($action_class, 'PhabricatorTriggerAction')) {
           unset($triggers[$key]);
           continue;
         }
 
         try {
           $argv = array($trigger->getActionProperties());
           $action = newv($action_class, $argv);
         } catch (Exception $ex) {
           unset($triggers[$key]);
           continue;
         }
 
         $trigger->attachAction($action);
       }
     }
 
     return $triggers;
   }
 
   protected function buildJoinClause(AphrontDatabaseConnection $conn_r) {
     $joins = array();
 
     if (($this->nextEpochMin !== null) ||
         ($this->nextEpochMax !== null) ||
-        ($this->order == PhabricatorWorkerTriggerQuery::ORDER_EXECUTION)) {
+        ($this->order == self::ORDER_EXECUTION)) {
       $joins[] = qsprintf(
         $conn_r,
         'JOIN %T e ON e.triggerID = t.id',
         id(new PhabricatorWorkerTriggerEvent())->getTableName());
     }
 
     return implode(' ', $joins);
   }
 
   protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
     $where = array();
 
     if ($this->ids !== null) {
       $where[] = qsprintf(
         $conn_r,
         't.id IN (%Ld)',
         $this->ids);
     }
 
     if ($this->phids !== null) {
       $where[] = qsprintf(
         $conn_r,
         't.phid IN (%Ls)',
         $this->phids);
     }
 
     if ($this->versionMin !== null) {
       $where[] = qsprintf(
         $conn_r,
         't.triggerVersion >= %d',
         $this->versionMin);
     }
 
     if ($this->versionMax !== null) {
       $where[] = qsprintf(
         $conn_r,
         't.triggerVersion <= %d',
         $this->versionMax);
     }
 
     if ($this->nextEpochMin !== null) {
       $where[] = qsprintf(
         $conn_r,
         'e.nextEventEpoch >= %d',
         $this->nextEpochMin);
     }
 
     if ($this->nextEpochMax !== null) {
       $where[] = qsprintf(
         $conn_r,
         'e.nextEventEpoch <= %d',
         $this->nextEpochMax);
     }
 
     return $this->formatWhereClause($where);
   }
 
   private function buildOrderClause(AphrontDatabaseConnection $conn_r) {
     switch ($this->order) {
       case self::ORDER_ID:
         return qsprintf(
           $conn_r,
           'ORDER BY id DESC');
       case self::ORDER_EXECUTION:
         return qsprintf(
           $conn_r,
           'ORDER BY e.nextEventEpoch ASC, e.id ASC');
       case self::ORDER_VERSION:
         return qsprintf(
           $conn_r,
           'ORDER BY t.triggerVersion ASC');
       default:
         throw new Exception(
           pht(
             'Unsupported order "%s".',
             $this->order));
     }
   }
 
 }
diff --git a/src/infrastructure/diff/PhabricatorInlineCommentController.php b/src/infrastructure/diff/PhabricatorInlineCommentController.php
index bc4358ba55..4608ebeffa 100644
--- a/src/infrastructure/diff/PhabricatorInlineCommentController.php
+++ b/src/infrastructure/diff/PhabricatorInlineCommentController.php
@@ -1,356 +1,358 @@
 <?php
 
 abstract class PhabricatorInlineCommentController
   extends PhabricatorController {
 
   abstract protected function createComment();
   abstract protected function loadComment($id);
   abstract protected function loadCommentForEdit($id);
   abstract protected function loadCommentForDone($id);
   abstract protected function loadCommentByPHID($phid);
   abstract protected function loadObjectOwnerPHID(
     PhabricatorInlineCommentInterface $inline);
   abstract protected function deleteComment(
     PhabricatorInlineCommentInterface $inline);
   abstract protected function saveComment(
     PhabricatorInlineCommentInterface $inline);
 
   private $changesetID;
   private $isNewFile;
   private $isOnRight;
   private $lineNumber;
   private $lineLength;
   private $commentText;
   private $operation;
   private $commentID;
   private $renderer;
   private $replyToCommentPHID;
 
   public function getCommentID() {
     return $this->commentID;
   }
 
   public function getOperation() {
     return $this->operation;
   }
 
   public function getCommentText() {
     return $this->commentText;
   }
 
   public function getLineLength() {
     return $this->lineLength;
   }
 
   public function getLineNumber() {
     return $this->lineNumber;
   }
 
   public function getIsOnRight() {
     return $this->isOnRight;
   }
 
   public function getChangesetID() {
     return $this->changesetID;
   }
 
   public function getIsNewFile() {
     return $this->isNewFile;
   }
 
   public function setRenderer($renderer) {
     $this->renderer = $renderer;
     return $this;
   }
 
   public function getRenderer() {
     return $this->renderer;
   }
 
   public function setReplyToCommentPHID($phid) {
     $this->replyToCommentPHID = $phid;
     return $this;
   }
 
   public function getReplyToCommentPHID() {
     return $this->replyToCommentPHID;
   }
 
   public function processRequest() {
     $request = $this->getRequest();
     $user = $request->getUser();
 
     $this->readRequestParameters();
 
     $op = $this->getOperation();
     switch ($op) {
       case 'done':
         if (!$request->validateCSRF()) {
           return new Aphront404Response();
         }
         $inline = $this->loadCommentForDone($this->getCommentID());
 
         $is_draft_state = false;
         switch ($inline->getFixedState()) {
           case PhabricatorInlineCommentInterface::STATE_DRAFT:
             $next_state = PhabricatorInlineCommentInterface::STATE_UNDONE;
             break;
           case PhabricatorInlineCommentInterface::STATE_UNDRAFT:
             $next_state = PhabricatorInlineCommentInterface::STATE_DONE;
             break;
           case PhabricatorInlineCommentInterface::STATE_DONE:
             $next_state = PhabricatorInlineCommentInterface::STATE_UNDRAFT;
             $is_draft_state = true;
             break;
           default:
           case PhabricatorInlineCommentInterface::STATE_UNDONE:
             $next_state = PhabricatorInlineCommentInterface::STATE_DRAFT;
             $is_draft_state = true;
             break;
         }
 
         $inline->setFixedState($next_state)->save();
 
         return id(new AphrontAjaxResponse())
           ->setContent(
             array(
               'draftState' => $is_draft_state,
             ));
       case 'delete':
       case 'undelete':
       case 'refdelete':
         if (!$request->validateCSRF()) {
           return new Aphront404Response();
         }
 
         // NOTE: For normal deletes, we just process the delete immediately
         // and show an "Undo" action. For deletes by reference from the
         // preview ("refdelete"), we prompt first (because the "Undo" may
         // not draw, or may not be easy to locate).
 
         if ($op == 'refdelete') {
           if (!$request->isFormPost()) {
             return $this->newDialog()
               ->setTitle(pht('Really delete comment?'))
               ->addHiddenInput('id', $this->getCommentID())
               ->addHiddenInput('op', $op)
               ->appendParagraph(pht('Delete this inline comment?'))
               ->addCancelButton('#')
               ->addSubmitButton(pht('Delete'));
           }
         }
 
         $is_delete = ($op == 'delete' || $op == 'refdelete');
 
         $inline = $this->loadCommentForEdit($this->getCommentID());
         $inline->setIsDeleted((int)$is_delete)->save();
 
         return $this->buildEmptyResponse();
       case 'edit':
         $inline = $this->loadCommentForEdit($this->getCommentID());
         $text = $this->getCommentText();
 
         if ($request->isFormPost()) {
           if (strlen($text)) {
             $inline->setContent($text);
             $this->saveComment($inline);
             return $this->buildRenderedCommentResponse(
               $inline,
               $this->getIsOnRight());
           } else {
             $this->deleteComment($inline);
             return $this->buildEmptyResponse();
           }
         }
 
         $edit_dialog = $this->buildEditDialog();
         $edit_dialog->setTitle(pht('Edit Inline Comment'));
 
         $edit_dialog->addHiddenInput('id', $this->getCommentID());
         $edit_dialog->addHiddenInput('op', 'edit');
 
         $edit_dialog->appendChild(
           $this->renderTextArea(
             nonempty($text, $inline->getContent())));
 
         $view = $this->buildScaffoldForView($edit_dialog);
 
         return id(new AphrontAjaxResponse())
           ->setContent($view->render());
       case 'create':
         $text = $this->getCommentText();
 
         if (!$request->isFormPost() || !strlen($text)) {
           return $this->buildEmptyResponse();
         }
 
         $inline = $this->createComment()
           ->setChangesetID($this->getChangesetID())
           ->setAuthorPHID($user->getPHID())
           ->setLineNumber($this->getLineNumber())
           ->setLineLength($this->getLineLength())
           ->setIsNewFile($this->getIsNewFile())
           ->setContent($text);
 
         if ($this->getReplyToCommentPHID()) {
           $inline->setReplyToCommentPHID($this->getReplyToCommentPHID());
         }
 
         $this->saveComment($inline);
 
         return $this->buildRenderedCommentResponse(
           $inline,
           $this->getIsOnRight());
       case 'reply':
       default:
         $edit_dialog = $this->buildEditDialog();
 
         if ($this->getOperation() == 'reply') {
           $edit_dialog->setTitle(pht('Reply to Inline Comment'));
         } else {
           $edit_dialog->setTitle(pht('New Inline Comment'));
         }
 
         // NOTE: We read the values from the client (the display values), not
         // the values from the database (the original values) when replying.
         // In particular, when replying to a ghost comment which was moved
         // across diffs and then moved backward to the most recent visible
         // line, we want to reply on the display line (which exists), not on
         // the comment's original line (which may not exist in this changeset).
         $is_new = $this->getIsNewFile();
         $number = $this->getLineNumber();
         $length = $this->getLineLength();
 
         $edit_dialog->addHiddenInput('op', 'create');
         $edit_dialog->addHiddenInput('is_new', $is_new);
         $edit_dialog->addHiddenInput('number', $number);
         $edit_dialog->addHiddenInput('length', $length);
 
         $text_area = $this->renderTextArea($this->getCommentText());
         $edit_dialog->appendChild($text_area);
 
         $view = $this->buildScaffoldForView($edit_dialog);
 
         return id(new AphrontAjaxResponse())
           ->setContent($view->render());
     }
   }
 
   private function readRequestParameters() {
     $request = $this->getRequest();
 
     // NOTE: This isn't necessarily a DifferentialChangeset ID, just an
     // application identifier for the changeset. In Diffusion, it's a Path ID.
     $this->changesetID = $request->getInt('changesetID');
 
     $this->isNewFile = (int)$request->getBool('is_new');
     $this->isOnRight = $request->getBool('on_right');
     $this->lineNumber = $request->getInt('number');
     $this->lineLength = $request->getInt('length');
     $this->commentText = $request->getStr('text');
     $this->commentID = $request->getInt('id');
     $this->operation = $request->getStr('op');
     $this->renderer = $request->getStr('renderer');
     $this->replyToCommentPHID = $request->getStr('replyToCommentPHID');
 
     if ($this->getReplyToCommentPHID()) {
       $reply_phid = $this->getReplyToCommentPHID();
       $reply_comment = $this->loadCommentByPHID($reply_phid);
       if (!$reply_comment) {
         throw new Exception(
           pht('Failed to load comment "%s".', $reply_phid));
       }
 
       // NOTE: It's fine to reply to a comment from a different changeset, so
       // the reply comment may not appear on the same changeset that the new
       // comment appears on. This is expected in the case of ghost comments.
       // We currently put the new comment on the visible changeset, not the
       // original comment's changeset.
+
+      $this->isNewFile = $reply_comment->getIsNewFile();
     }
   }
 
   private function buildEditDialog() {
     $request = $this->getRequest();
     $user = $request->getUser();
 
     $edit_dialog = id(new PHUIDiffInlineCommentEditView())
       ->setUser($user)
       ->setSubmitURI($request->getRequestURI())
       ->setIsOnRight($this->getIsOnRight())
       ->setIsNewFile($this->getIsNewFile())
       ->setNumber($this->getLineNumber())
       ->setLength($this->getLineLength())
       ->setRenderer($this->getRenderer())
       ->setReplyToCommentPHID($this->getReplyToCommentPHID())
       ->setChangesetID($this->getChangesetID());
 
     return $edit_dialog;
   }
 
   private function buildEmptyResponse() {
     return id(new AphrontAjaxResponse())
       ->setContent(
         array(
           'markup' => '',
         ));
   }
 
   private function buildRenderedCommentResponse(
     PhabricatorInlineCommentInterface $inline,
     $on_right) {
 
     $request = $this->getRequest();
     $user = $request->getUser();
 
     $engine = new PhabricatorMarkupEngine();
     $engine->setViewer($user);
     $engine->addObject(
       $inline,
       PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY);
     $engine->process();
 
     $phids = array($user->getPHID());
 
     $handles = $this->loadViewerHandles($phids);
     $object_owner_phid = $this->loadObjectOwnerPHID($inline);
 
     $view = id(new PHUIDiffInlineCommentDetailView())
       ->setUser($user)
       ->setInlineComment($inline)
       ->setIsOnRight($on_right)
       ->setMarkupEngine($engine)
       ->setHandles($handles)
       ->setEditable(true)
       ->setCanMarkDone(false)
       ->setObjectOwnerPHID($object_owner_phid);
 
     $view = $this->buildScaffoldForView($view);
 
     return id(new AphrontAjaxResponse())
       ->setContent(
         array(
           'inlineCommentID' => $inline->getID(),
           'markup'          => $view->render(),
         ));
   }
 
   private function renderTextArea($text) {
     return id(new PhabricatorRemarkupControl())
       ->setUser($this->getRequest()->getUser())
       ->setSigil('differential-inline-comment-edit-textarea')
       ->setName('text')
       ->setValue($text)
       ->setDisableFullScreen(true);
   }
 
   private function buildScaffoldForView(PHUIDiffInlineCommentView $view) {
     $renderer = DifferentialChangesetHTMLRenderer::getHTMLRendererByKey(
       $this->getRenderer());
 
     $view = $renderer->getRowScaffoldForInline($view);
 
     return id(new PHUIDiffInlineCommentTableScaffold())
       ->addRowScaffold($view);
   }
 
 }
diff --git a/src/infrastructure/diff/view/PHUIDiffInlineCommentEditView.php b/src/infrastructure/diff/view/PHUIDiffInlineCommentEditView.php
index 6634c397f9..3a9bc616d9 100644
--- a/src/infrastructure/diff/view/PHUIDiffInlineCommentEditView.php
+++ b/src/infrastructure/diff/view/PHUIDiffInlineCommentEditView.php
@@ -1,178 +1,178 @@
 <?php
 
 final class PHUIDiffInlineCommentEditView
   extends PHUIDiffInlineCommentView {
 
   private $inputs = array();
   private $uri;
   private $title;
   private $number;
   private $length;
   private $renderer;
   private $isNewFile;
   private $replyToCommentPHID;
   private $changesetID;
 
   public function setIsNewFile($is_new_file) {
     $this->isNewFile = $is_new_file;
     return $this;
   }
 
   public function getIsNewFile() {
     return $this->isNewFile;
   }
 
   public function setRenderer($renderer) {
     $this->renderer = $renderer;
     return $this;
   }
 
   public function getRenderer() {
     return $this->renderer;
   }
 
   public function addHiddenInput($key, $value) {
     $this->inputs[] = array($key, $value);
     return $this;
   }
 
   public function setSubmitURI($uri) {
     $this->uri = $uri;
     return $this;
   }
 
   public function setTitle($title) {
     $this->title = $title;
     return $this;
   }
 
   public function setReplyToCommentPHID($reply_to_phid) {
     $this->replyToCommentPHID = $reply_to_phid;
     return $this;
   }
 
   public function getReplyToCommentPHID() {
     return $this->replyToCommentPHID;
   }
 
   public function setChangesetID($changeset_id) {
     $this->changesetID = $changeset_id;
     return $this;
   }
 
   public function getChangesetID() {
     return $this->changesetID;
   }
 
   public function setNumber($number) {
     $this->number = $number;
     return $this;
   }
 
   public function setLength($length) {
     $this->length = $length;
     return $this;
   }
 
   public function render() {
     if (!$this->uri) {
-      throw new Exception('Call setSubmitURI() before render()!');
+      throw new PhutilInvalidStateException('setSubmitURI');
     }
     if (!$this->user) {
-      throw new Exception('Call setUser() before render()!');
+      throw new PhutilInvalidStateException('setUser');
     }
 
     $content = phabricator_form(
       $this->user,
       array(
         'action'    => $this->uri,
         'method'    => 'POST',
         'sigil'     => 'inline-edit-form',
       ),
       array(
         $this->renderInputs(),
         $this->renderBody(),
       ));
 
     return $content;
   }
 
   private function renderInputs() {
     $inputs = $this->inputs;
     $out = array();
 
     $inputs[] = array('on_right', (bool)$this->getIsOnRight());
     $inputs[] = array('replyToCommentPHID', $this->getReplyToCommentPHID());
     $inputs[] = array('renderer', $this->getRenderer());
     $inputs[] = array('changesetID', $this->getChangesetID());
 
     foreach ($inputs as $input) {
       list($name, $value) = $input;
       $out[] = phutil_tag(
         'input',
         array(
           'type'  => 'hidden',
           'name'  => $name,
           'value' => $value,
         ));
     }
     return $out;
   }
 
   private function renderBody() {
     $buttons = array();
 
     $buttons[] = phutil_tag('button', array(), pht('Save Draft'));
     $buttons[] = javelin_tag(
       'button',
       array(
         'sigil' => 'inline-edit-cancel',
         'class' => 'grey',
       ),
       pht('Cancel'));
 
     $title = phutil_tag(
       'div',
       array(
         'class' => 'differential-inline-comment-edit-title',
       ),
       $this->title);
 
     $body = phutil_tag(
       'div',
       array(
         'class' => 'differential-inline-comment-edit-body',
       ),
       $this->renderChildren());
 
     $edit = phutil_tag(
       'div',
       array(
         'class' => 'differential-inline-comment-edit-buttons grouped',
       ),
       array(
         $buttons,
       ));
 
     return javelin_tag(
       'div',
       array(
         'class' => 'differential-inline-comment-edit',
         'sigil' => 'differential-inline-comment',
         'meta' => array(
           'changesetID' => $this->getChangesetID(),
           'on_right' => $this->getIsOnRight(),
           'isNewFile' => (bool)$this->getIsNewFile(),
           'number' => $this->number,
           'length' => $this->length,
           'replyToCommentPHID' => $this->getReplyToCommentPHID(),
         ),
       ),
       array(
         $title,
         $body,
         $edit,
       ));
   }
 
 }
diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php
index 05ab7c677d..39f911e3c5 100644
--- a/src/infrastructure/env/PhabricatorEnv.php
+++ b/src/infrastructure/env/PhabricatorEnv.php
@@ -1,876 +1,876 @@
 <?php
 
 /**
  * Manages the execution environment configuration, exposing APIs to read
  * configuration settings and other similar values that are derived directly
  * from configuration settings.
  *
  *
  * = Reading Configuration =
  *
  * The primary role of this class is to provide an API for reading
  * Phabricator configuration, @{method:getEnvConfig}:
  *
  *   $value = PhabricatorEnv::getEnvConfig('some.key', $default);
  *
  * The class also handles some URI construction based on configuration, via
  * the methods @{method:getURI}, @{method:getProductionURI},
  * @{method:getCDNURI}, and @{method:getDoclink}.
  *
  * For configuration which allows you to choose a class to be responsible for
  * some functionality (e.g., which mail adapter to use to deliver email),
  * @{method:newObjectFromConfig} provides a simple interface that validates
  * the configured value.
  *
  *
  * = Unit Test Support =
  *
  * In unit tests, you can use @{method:beginScopedEnv} to create a temporary,
  * mutable environment. The method returns a scope guard object which restores
  * the environment when it is destroyed. For example:
  *
  *   public function testExample() {
  *     $env = PhabricatorEnv::beginScopedEnv();
  *     $env->overrideEnv('some.key', 'new-value-for-this-test');
  *
  *     // Some test which depends on the value of 'some.key'.
  *
  *   }
  *
  * Your changes will persist until the `$env` object leaves scope or is
  * destroyed.
  *
  * You should //not// use this in normal code.
  *
  *
  * @task read     Reading Configuration
  * @task uri      URI Validation
  * @task test     Unit Test Support
  * @task internal Internals
  */
 final class PhabricatorEnv {
 
   private static $sourceStack;
   private static $repairSource;
   private static $overrideSource;
   private static $requestBaseURI;
   private static $cache;
   private static $localeCode;
 
   /**
    * @phutil-external-symbol class PhabricatorStartup
    */
   public static function initializeWebEnvironment() {
     self::initializeCommonEnvironment();
   }
 
   public static function initializeScriptEnvironment() {
     self::initializeCommonEnvironment();
 
     // NOTE: This is dangerous in general, but we know we're in a script context
     // and are not vulnerable to CSRF.
     AphrontWriteGuard::allowDangerousUnguardedWrites(true);
 
     // There are several places where we log information (about errors, events,
     // service calls, etc.) for analysis via DarkConsole or similar. These are
     // useful for web requests, but grow unboundedly in long-running scripts and
     // daemons. Discard data as it arrives in these cases.
     PhutilServiceProfiler::getInstance()->enableDiscardMode();
     DarkConsoleErrorLogPluginAPI::enableDiscardMode();
     DarkConsoleEventPluginAPI::enableDiscardMode();
   }
 
 
   private static function initializeCommonEnvironment() {
     PhutilErrorHandler::initialize();
 
     self::buildConfigurationSourceStack();
 
     // Force a valid timezone. If both PHP and Phabricator configuration are
     // invalid, use UTC.
-    $tz = PhabricatorEnv::getEnvConfig('phabricator.timezone');
+    $tz = self::getEnvConfig('phabricator.timezone');
     if ($tz) {
       @date_default_timezone_set($tz);
     }
     $ok = @date_default_timezone_set(date_default_timezone_get());
     if (!$ok) {
       date_default_timezone_set('UTC');
     }
 
     // Prepend '/support/bin' and append any paths to $PATH if we need to.
     $env_path = getenv('PATH');
     $phabricator_path = dirname(phutil_get_library_root('phabricator'));
     $support_path = $phabricator_path.'/support/bin';
     $env_path = $support_path.PATH_SEPARATOR.$env_path;
-    $append_dirs = PhabricatorEnv::getEnvConfig('environment.append-paths');
+    $append_dirs = self::getEnvConfig('environment.append-paths');
     if (!empty($append_dirs)) {
       $append_path = implode(PATH_SEPARATOR, $append_dirs);
       $env_path = $env_path.PATH_SEPARATOR.$append_path;
     }
     putenv('PATH='.$env_path);
 
     // Write this back into $_ENV, too, so ExecFuture picks it up when creating
     // subprocess environments.
     $_ENV['PATH'] = $env_path;
 
 
     // If an instance identifier is defined, write it into the environment so
     // it's available to subprocesses.
-    $instance = PhabricatorEnv::getEnvConfig('cluster.instance');
+    $instance = self::getEnvConfig('cluster.instance');
     if (strlen($instance)) {
       putenv('PHABRICATOR_INSTANCE='.$instance);
       $_ENV['PHABRICATOR_INSTANCE'] = $instance;
     }
 
     PhabricatorEventEngine::initialize();
 
     // TODO: Add a "locale.default" config option once we have some reasonable
     // defaults which aren't silly nonsense.
     self::setLocaleCode('en_US');
   }
 
   public static function setLocaleCode($locale_code) {
     if ($locale_code == self::$localeCode) {
       return;
     }
 
     try {
       $locale = PhutilLocale::loadLocale($locale_code);
       $translations = PhutilTranslation::getTranslationMapForLocale(
         $locale_code);
 
-      $override = PhabricatorEnv::getEnvConfig('translation.override');
+      $override = self::getEnvConfig('translation.override');
       if (!is_array($override)) {
         $override = array();
       }
 
       PhutilTranslator::getInstance()
         ->setLocale($locale)
         ->setTranslations($override + $translations);
 
       self::$localeCode = $locale_code;
     } catch (Exception $ex) {
       // Just ignore this; the user likely has an out-of-date locale code.
     }
   }
 
   private static function buildConfigurationSourceStack() {
     self::dropConfigCache();
 
     $stack = new PhabricatorConfigStackSource();
     self::$sourceStack = $stack;
 
     $default_source = id(new PhabricatorConfigDefaultSource())
       ->setName(pht('Global Default'));
     $stack->pushSource($default_source);
 
     $env = self::getSelectedEnvironmentName();
     if ($env) {
       $stack->pushSource(
         id(new PhabricatorConfigFileSource($env))
           ->setName(pht("File '%s'", $env)));
     }
 
     $stack->pushSource(
       id(new PhabricatorConfigLocalSource())
         ->setName(pht('Local Config')));
 
     // If the install overrides the database adapter, we might need to load
     // the database adapter class before we can push on the database config.
     // This config is locked and can't be edited from the web UI anyway.
-    foreach (PhabricatorEnv::getEnvConfig('load-libraries') as $library) {
+    foreach (self::getEnvConfig('load-libraries') as $library) {
       phutil_load_library($library);
     }
 
     // If custom libraries specify config options, they won't get default
     // values as the Default source has already been loaded, so we get it to
     // pull in all options from non-phabricator libraries now they are loaded.
     $default_source->loadExternalOptions();
 
     // If this install has site config sources, load them now.
     $site_sources = id(new PhutilSymbolLoader())
       ->setAncestorClass('PhabricatorConfigSiteSource')
       ->loadObjects();
     $site_sources = msort($site_sources, 'getPriority');
     foreach ($site_sources as $site_source) {
       $stack->pushSource($site_source);
     }
 
     try {
       $stack->pushSource(
         id(new PhabricatorConfigDatabaseSource('default'))
           ->setName(pht('Database')));
     } catch (AphrontQueryException $exception) {
       // If the database is not available, just skip this configuration
       // source. This happens during `bin/storage upgrade`, `bin/conf` before
       // schema setup, etc.
     }
   }
 
   public static function repairConfig($key, $value) {
     if (!self::$repairSource) {
       self::$repairSource = id(new PhabricatorConfigDictionarySource(array()))
         ->setName(pht('Repaired Config'));
       self::$sourceStack->pushSource(self::$repairSource);
     }
     self::$repairSource->setKeys(array($key => $value));
     self::dropConfigCache();
   }
 
   public static function overrideConfig($key, $value) {
     if (!self::$overrideSource) {
       self::$overrideSource = id(new PhabricatorConfigDictionarySource(array()))
         ->setName(pht('Overridden Config'));
       self::$sourceStack->pushSource(self::$overrideSource);
     }
     self::$overrideSource->setKeys(array($key => $value));
     self::dropConfigCache();
   }
 
   public static function getUnrepairedEnvConfig($key, $default = null) {
     foreach (self::$sourceStack->getStack() as $source) {
       if ($source === self::$repairSource) {
         continue;
       }
       $result = $source->getKeys(array($key));
       if ($result) {
         return $result[$key];
       }
     }
     return $default;
   }
 
   public static function getSelectedEnvironmentName() {
     $env_var = 'PHABRICATOR_ENV';
 
     $env = idx($_SERVER, $env_var);
 
     if (!$env) {
       $env = getenv($env_var);
     }
 
     if (!$env) {
       $env = idx($_ENV, $env_var);
     }
 
     if (!$env) {
       $root = dirname(phutil_get_library_root('phabricator'));
       $path = $root.'/conf/local/ENVIRONMENT';
       if (Filesystem::pathExists($path)) {
         $env = trim(Filesystem::readFile($path));
       }
     }
 
     return $env;
   }
 
   public static function calculateEnvironmentHash() {
     $keys = self::getKeysForConsistencyCheck();
 
     $values = array();
     foreach ($keys as $key) {
       $values[$key] = self::getEnvConfigIfExists($key);
     }
 
     return PhabricatorHash::digest(json_encode($values));
   }
 
   /**
    * Returns a summary of non-default configuration settings to allow the
    * "daemons and web have different config" setup check to list divergent
    * keys.
    */
   public static function calculateEnvironmentInfo() {
     $keys = self::getKeysForConsistencyCheck();
 
     $info = array();
 
     $defaults = id(new PhabricatorConfigDefaultSource())->getAllKeys();
     foreach ($keys as $key) {
       $current = self::getEnvConfigIfExists($key);
       $default = idx($defaults, $key, null);
       if ($current !== $default) {
         $info[$key] = PhabricatorHash::digestForIndex(json_encode($current));
       }
     }
 
     $keys_hash = array_keys($defaults);
     sort($keys_hash);
     $keys_hash = implode("\0", $keys_hash);
     $keys_hash = PhabricatorHash::digestForIndex($keys_hash);
 
     return array(
       'version' => 1,
       'keys' => $keys_hash,
       'values' => $info,
     );
   }
 
 
   /**
    * Compare two environment info summaries to generate a human-readable
    * list of discrepancies.
    */
   public static function compareEnvironmentInfo(array $u, array $v) {
     $issues = array();
 
     $uversion = idx($u, 'version');
     $vversion = idx($v, 'version');
     if ($uversion != $vversion) {
       $issues[] = pht(
         'The two configurations were generated by different versions '.
         'of Phabricator.');
 
       // These may not be comparable, so stop here.
       return $issues;
     }
 
     if ($u['keys'] !== $v['keys']) {
       $issues[] = pht(
         'The two configurations have different keys. This usually means '.
         'that they are running different versions of Phabricator.');
     }
 
     $uval = idx($u, 'values', array());
     $vval = idx($v, 'values', array());
 
     $all_keys = array_keys($uval + $vval);
 
     foreach ($all_keys as $key) {
       $uv = idx($uval, $key);
       $vv = idx($vval, $key);
       if ($uv !== $vv) {
         if ($uv && $vv) {
           $issues[] = pht(
             'The configuration key "%s" is set in both configurations, but '.
             'set to different values.',
             $key);
         } else {
           $issues[] = pht(
             'The configuration key "%s" is set in only one configuration.',
             $key);
         }
       }
     }
 
     return $issues;
   }
 
   private static function getKeysForConsistencyCheck() {
     $keys = array_keys(self::getAllConfigKeys());
     sort($keys);
 
     $skip_keys = self::getEnvConfig('phd.variant-config');
     return array_diff($keys, $skip_keys);
   }
 
 
 /* -(  Reading Configuration  )---------------------------------------------- */
 
 
   /**
    * Get the current configuration setting for a given key.
    *
    * If the key is not found, then throw an Exception.
    *
    * @task read
    */
   public static function getEnvConfig($key) {
     if (isset(self::$cache[$key])) {
       return self::$cache[$key];
     }
 
     if (array_key_exists($key, self::$cache)) {
       return self::$cache[$key];
     }
 
     $result = self::$sourceStack->getKeys(array($key));
     if (array_key_exists($key, $result)) {
       self::$cache[$key] = $result[$key];
       return $result[$key];
     } else {
       throw new Exception("No config value specified for key '{$key}'.");
     }
   }
 
 
   /**
    * Get the current configuration setting for a given key. If the key
    * does not exist, return a default value instead of throwing. This is
    * primarily useful for migrations involving keys which are slated for
    * removal.
    *
    * @task read
    */
   public static function getEnvConfigIfExists($key, $default = null) {
     try {
       return self::getEnvConfig($key);
     } catch (Exception $ex) {
       return $default;
     }
   }
 
 
   /**
    * Get the fully-qualified URI for a path.
    *
    * @task read
    */
   public static function getURI($path) {
     return rtrim(self::getAnyBaseURI(), '/').$path;
   }
 
 
   /**
    * Get the fully-qualified production URI for a path.
    *
    * @task read
    */
   public static function getProductionURI($path) {
     // If we're passed a URI which already has a domain, simply return it
     // unmodified. In particular, files may have URIs which point to a CDN
     // domain.
     $uri = new PhutilURI($path);
     if ($uri->getDomain()) {
       return $path;
     }
 
     $production_domain = self::getEnvConfig('phabricator.production-uri');
     if (!$production_domain) {
       $production_domain = self::getAnyBaseURI();
     }
     return rtrim($production_domain, '/').$path;
   }
 
   public static function getAllowedURIs($path) {
     $uri = new PhutilURI($path);
     if ($uri->getDomain()) {
       return $path;
     }
 
     $allowed_uris = self::getEnvConfig('phabricator.allowed-uris');
     $return = array();
     foreach ($allowed_uris as $allowed_uri) {
       $return[] = rtrim($allowed_uri, '/').$path;
     }
 
     return $return;
   }
 
 
   /**
    * Get the fully-qualified production URI for a static resource path.
    *
    * @task read
    */
   public static function getCDNURI($path) {
     $alt = self::getEnvConfig('security.alternate-file-domain');
     if (!$alt) {
       $alt = self::getAnyBaseURI();
     }
     $uri = new PhutilURI($alt);
     $uri->setPath($path);
     return (string)$uri;
   }
 
 
   /**
    * Get the fully-qualified production URI for a documentation resource.
    *
    * @task read
    */
   public static function getDoclink($resource, $type = 'article') {
     $uri = new PhutilURI('https://secure.phabricator.com/diviner/find/');
     $uri->setQueryParam('name', $resource);
     $uri->setQueryParam('type', $type);
     $uri->setQueryParam('jump', true);
     return (string)$uri;
   }
 
 
   /**
    * Build a concrete object from a configuration key.
    *
    * @task read
    */
   public static function newObjectFromConfig($key, $args = array()) {
     $class = self::getEnvConfig($key);
     return newv($class, $args);
   }
 
   public static function getAnyBaseURI() {
     $base_uri = self::getEnvConfig('phabricator.base-uri');
 
     if (!$base_uri) {
       $base_uri = self::getRequestBaseURI();
     }
 
     if (!$base_uri) {
       throw new Exception(
         "Define 'phabricator.base-uri' in your configuration to continue.");
     }
 
     return $base_uri;
   }
 
   public static function getRequestBaseURI() {
     return self::$requestBaseURI;
   }
 
   public static function setRequestBaseURI($uri) {
     self::$requestBaseURI = $uri;
   }
 
 /* -(  Unit Test Support  )-------------------------------------------------- */
 
 
   /**
    * @task test
    */
   public static function beginScopedEnv() {
     return new PhabricatorScopedEnv(self::pushTestEnvironment());
   }
 
 
   /**
    * @task test
    */
   private static function pushTestEnvironment() {
     self::dropConfigCache();
     $source = new PhabricatorConfigDictionarySource(array());
     self::$sourceStack->pushSource($source);
     return spl_object_hash($source);
   }
 
 
   /**
    * @task test
    */
   public static function popTestEnvironment($key) {
     self::dropConfigCache();
     $source = self::$sourceStack->popSource();
     $stack_key = spl_object_hash($source);
     if ($stack_key !== $key) {
       self::$sourceStack->pushSource($source);
       throw new Exception(
         'Scoped environments were destroyed in a diffent order than they '.
         'were initialized.');
     }
   }
 
 
 /* -(  URI Validation  )----------------------------------------------------- */
 
 
   /**
    * Detect if a URI satisfies either @{method:isValidLocalURIForLink} or
    * @{method:isValidRemoteURIForLink}, i.e. is a page on this server or the
    * URI of some other resource which has a valid protocol. This rejects
    * garbage URIs and URIs with protocols which do not appear in the
    * `uri.allowed-protocols` configuration, notably 'javascript:' URIs.
    *
    * NOTE: This method is generally intended to reject URIs which it may be
    * unsafe to put in an "href" link attribute.
    *
    * @param string URI to test.
    * @return bool True if the URI identifies a web resource.
    * @task uri
    */
   public static function isValidURIForLink($uri) {
     return self::isValidLocalURIForLink($uri) ||
            self::isValidRemoteURIForLink($uri);
   }
 
 
   /**
    * Detect if a URI identifies some page on this server.
    *
    * NOTE: This method is generally intended to reject URIs which it may be
    * unsafe to issue a "Location:" redirect to.
    *
    * @param string URI to test.
    * @return bool True if the URI identifies a local page.
    * @task uri
    */
   public static function isValidLocalURIForLink($uri) {
     $uri = (string)$uri;
 
     if (!strlen($uri)) {
       return false;
     }
 
     if (preg_match('/\s/', $uri)) {
       // PHP hasn't been vulnerable to header injection attacks for a bunch of
       // years, but we can safely reject these anyway since they're never valid.
       return false;
     }
 
     // Chrome (at a minimum) interprets backslashes in Location headers and the
     // URL bar as forward slashes. This is probably intended to reduce user
     // error caused by confusion over which key is "forward slash" vs "back
     // slash".
     //
     // However, it means a URI like "/\evil.com" is interpreted like
     // "//evil.com", which is a protocol relative remote URI.
     //
     // Since we currently never generate URIs with backslashes in them, reject
     // these unconditionally rather than trying to figure out how browsers will
     // interpret them.
     if (preg_match('/\\\\/', $uri)) {
       return false;
     }
 
     // Valid URIs must begin with '/', followed by the end of the string or some
     // other non-'/' character. This rejects protocol-relative URIs like
     // "//evil.com/evil_stuff/".
     return (bool)preg_match('@^/([^/]|$)@', $uri);
   }
 
 
   /**
    * Detect if a URI identifies some valid linkable remote resource.
    *
    * @param string URI to test.
    * @return bool True if a URI idenfies a remote resource with an allowed
    *              protocol.
    * @task uri
    */
   public static function isValidRemoteURIForLink($uri) {
     try {
       self::requireValidRemoteURIForLink($uri);
       return true;
     } catch (Exception $ex) {
       return false;
     }
   }
 
 
   /**
    * Detect if a URI identifies a valid linkable remote resource, throwing a
    * detailed message if it does not.
    *
    * A valid linkable remote resource can be safely linked or redirected to.
    * This is primarily a protocol whitelist check.
    *
    * @param string URI to test.
    * @return void
    * @task uri
    */
   public static function requireValidRemoteURIForLink($uri) {
     $uri = new PhutilURI($uri);
 
     $proto = $uri->getProtocol();
     if (!strlen($proto)) {
       throw new Exception(
         pht(
           'URI "%s" is not a valid linkable resource. A valid linkable '.
           'resource URI must specify a protocol.',
           $uri));
     }
 
     $protocols = self::getEnvConfig('uri.allowed-protocols');
     if (!isset($protocols[$proto])) {
       throw new Exception(
         pht(
           'URI "%s" is not a valid linkable resource. A valid linkable '.
           'resource URI must use one of these protocols: %s.',
           $uri,
           implode(', ', array_keys($protocols))));
     }
 
     $domain = $uri->getDomain();
     if (!strlen($domain)) {
       throw new Exception(
         pht(
           'URI "%s" is not a valid linkable resource. A valid linkable '.
           'resource URI must specify a domain.',
           $uri));
     }
   }
 
 
   /**
    * Detect if a URI identifies a valid fetchable remote resource.
    *
    * @param string URI to test.
    * @param list<string> Allowed protocols.
    * @return bool True if the URI is a valid fetchable remote resource.
    * @task uri
    */
   public static function isValidRemoteURIForFetch($uri, array $protocols) {
     try {
       self::requireValidRemoteURIForFetch($uri, $protocols);
       return true;
     } catch (Exception $ex) {
       return false;
     }
   }
 
 
   /**
    * Detect if a URI identifies a valid fetchable remote resource, throwing
    * a detailed message if it does not.
    *
    * A valid fetchable remote resource can be safely fetched using a request
    * originating on this server. This is a primarily an address check against
    * the outbound address blacklist.
    *
    * @param string URI to test.
    * @param list<string> Allowed protocols.
    * @return pair<string, string> Pre-resolved URI and domain.
    * @task uri
    */
   public static function requireValidRemoteURIForFetch(
     $uri,
     array $protocols) {
 
     $uri = new PhutilURI($uri);
 
     $proto = $uri->getProtocol();
     if (!strlen($proto)) {
       throw new Exception(
         pht(
           'URI "%s" is not a valid fetchable resource. A valid fetchable '.
           'resource URI must specify a protocol.',
           $uri));
     }
 
     $protocols = array_fuse($protocols);
     if (!isset($protocols[$proto])) {
       throw new Exception(
         pht(
           'URI "%s" is not a valid fetchable resource. A valid fetchable '.
           'resource URI must use one of these protocols: %s.',
           $uri,
           implode(', ', array_keys($protocols))));
     }
 
     $domain = $uri->getDomain();
     if (!strlen($domain)) {
       throw new Exception(
         pht(
           'URI "%s" is not a valid fetchable resource. A valid fetchable '.
           'resource URI must specify a domain.',
           $uri));
     }
 
     $addresses = gethostbynamel($domain);
     if (!$addresses) {
       throw new Exception(
         pht(
           'URI "%s" is not a valid fetchable resource. The domain "%s" could '.
           'not be resolved.',
           $uri,
           $domain));
     }
 
     foreach ($addresses as $address) {
       if (self::isBlacklistedOutboundAddress($address)) {
         throw new Exception(
           pht(
             'URI "%s" is not a valid fetchable resource. The domain "%s" '.
             'resolves to the address "%s", which is blacklisted for '.
             'outbound requests.',
             $uri,
             $domain,
             $address));
       }
     }
 
     $resolved_uri = clone $uri;
     $resolved_uri->setDomain(head($addresses));
 
     return array($resolved_uri, $domain);
   }
 
 
   /**
    * Determine if an IP address is in the outbound address blacklist.
    *
    * @param string IP address.
    * @return bool True if the address is blacklisted.
    */
   public static function isBlacklistedOutboundAddress($address) {
     $blacklist = self::getEnvConfig('security.outbound-blacklist');
 
     return PhutilCIDRList::newList($blacklist)->containsAddress($address);
   }
 
   public static function isClusterRemoteAddress() {
     $address = idx($_SERVER, 'REMOTE_ADDR');
     if (!$address) {
       throw new Exception(
         pht(
           'Unable to test remote address against cluster whitelist: '.
           'REMOTE_ADDR is not defined.'));
     }
 
     return self::isClusterAddress($address);
   }
 
   public static function isClusterAddress($address) {
-    $cluster_addresses = PhabricatorEnv::getEnvConfig('cluster.addresses');
+    $cluster_addresses = self::getEnvConfig('cluster.addresses');
     if (!$cluster_addresses) {
       throw new Exception(
         pht(
           'Phabricator is not configured to serve cluster requests. '.
           'Set `cluster.addresses` in the configuration to whitelist '.
           'cluster hosts before sending requests that use a cluster '.
           'authentication mechanism.'));
     }
 
     return PhutilCIDRList::newList($cluster_addresses)
       ->containsAddress($address);
   }
 
 /* -(  Internals  )---------------------------------------------------------- */
 
 
   /**
    * @task internal
    */
   public static function envConfigExists($key) {
     return array_key_exists($key, self::$sourceStack->getKeys(array($key)));
   }
 
 
   /**
    * @task internal
    */
   public static function getAllConfigKeys() {
     return self::$sourceStack->getAllKeys();
   }
 
   public static function getConfigSourceStack() {
     return self::$sourceStack;
   }
 
   /**
    * @task internal
    */
   public static function overrideTestEnvConfig($stack_key, $key, $value) {
     $tmp = array();
 
     // If we don't have the right key, we'll throw when popping the last
     // source off the stack.
     do {
       $source = self::$sourceStack->popSource();
       array_unshift($tmp, $source);
       if (spl_object_hash($source) == $stack_key) {
         $source->setKeys(array($key => $value));
         break;
       }
     } while (true);
 
     foreach ($tmp as $source) {
       self::$sourceStack->pushSource($source);
     }
 
     self::dropConfigCache();
   }
 
   private static function dropConfigCache() {
     self::$cache = array();
   }
 
 }
diff --git a/src/infrastructure/events/PhabricatorExampleEventListener.php b/src/infrastructure/events/PhabricatorExampleEventListener.php
index 0a7bd699df..fb85678da1 100644
--- a/src/infrastructure/events/PhabricatorExampleEventListener.php
+++ b/src/infrastructure/events/PhabricatorExampleEventListener.php
@@ -1,28 +1,31 @@
 <?php
 
 /**
  * Example event listener. For details about installing Phabricator event hooks,
  * refer to @{article:Events User Guide: Installing Event Listeners}.
  */
 final class PhabricatorExampleEventListener extends PhabricatorEventListener {
 
   public function register() {
     // When your listener is installed, its register() method will be called.
     // You should listen() to any events you are interested in here.
     $this->listen(PhabricatorEventType::TYPE_TEST_DIDRUNTEST);
   }
 
   public function handleEvent(PhutilEvent $event) {
     // When an event you have called listen() for in your register() method
     // occurs, this method will be invoked. You should respond to the event.
 
     // In this case, we just echo a message out so the event test script will
     // do something visible.
 
     $console = PhutilConsole::getConsole();
     $console->writeOut(
-      "PhabricatorExampleEventListener got test event at %d\n",
-      $event->getValue('time'));
+      "%s\n",
+      pht(
+        '% got test event at %d',
+        __CLASS__,
+        $event->getValue('time')));
   }
 
 }
diff --git a/src/infrastructure/markup/PhabricatorMarkupEngine.php b/src/infrastructure/markup/PhabricatorMarkupEngine.php
index 491940d23f..c29dddd821 100644
--- a/src/infrastructure/markup/PhabricatorMarkupEngine.php
+++ b/src/infrastructure/markup/PhabricatorMarkupEngine.php
@@ -1,631 +1,634 @@
 <?php
 
 /**
  * Manages markup engine selection, configuration, application, caching and
  * pipelining.
  *
  * @{class:PhabricatorMarkupEngine} can be used to render objects which
  * implement @{interface:PhabricatorMarkupInterface} in a batched, cache-aware
  * way. For example, if you have a list of comments written in remarkup (and
  * the objects implement the correct interface) you can render them by first
  * building an engine and adding the fields with @{method:addObject}.
  *
  *   $field  = 'field:body'; // Field you want to render. Each object exposes
  *                           // one or more fields of markup.
  *
  *   $engine = new PhabricatorMarkupEngine();
  *   foreach ($comments as $comment) {
  *     $engine->addObject($comment, $field);
  *   }
  *
  * Now, call @{method:process} to perform the actual cache/rendering
  * step. This is a heavyweight call which does batched data access and
  * transforms the markup into output.
  *
  *   $engine->process();
  *
  * Finally, do something with the results:
  *
  *   $results = array();
  *   foreach ($comments as $comment) {
  *     $results[] = $engine->getOutput($comment, $field);
  *   }
  *
  * If you have a single object to render, you can use the convenience method
  * @{method:renderOneObject}.
  *
  * @task markup Markup Pipeline
  * @task engine Engine Construction
  */
 final class PhabricatorMarkupEngine {
 
   private $objects = array();
   private $viewer;
   private $contextObject;
-  private $version = 14;
+  private $version = 15;
 
 
 /* -(  Markup Pipeline  )---------------------------------------------------- */
 
 
   /**
    * Convenience method for pushing a single object through the markup
    * pipeline.
    *
    * @param PhabricatorMarkupInterface  The object to render.
    * @param string                      The field to render.
    * @param PhabricatorUser             User viewing the markup.
    * @param object                      A context object for policy checks
    * @return string                     Marked up output.
    * @task markup
    */
   public static function renderOneObject(
     PhabricatorMarkupInterface $object,
     $field,
     PhabricatorUser $viewer,
     $context_object = null) {
     return id(new PhabricatorMarkupEngine())
       ->setViewer($viewer)
       ->setContextObject($context_object)
       ->addObject($object, $field)
       ->process()
       ->getOutput($object, $field);
   }
 
 
   /**
    * Queue an object for markup generation when @{method:process} is
    * called. You can retrieve the output later with @{method:getOutput}.
    *
    * @param PhabricatorMarkupInterface  The object to render.
    * @param string                      The field to render.
    * @return this
    * @task markup
    */
   public function addObject(PhabricatorMarkupInterface $object, $field) {
     $key = $this->getMarkupFieldKey($object, $field);
     $this->objects[$key] = array(
       'object' => $object,
       'field'  => $field,
     );
 
     return $this;
   }
 
 
   /**
    * Process objects queued with @{method:addObject}. You can then retrieve
    * the output with @{method:getOutput}.
    *
    * @return this
    * @task markup
    */
   public function process() {
     $keys = array();
     foreach ($this->objects as $key => $info) {
       if (!isset($info['markup'])) {
         $keys[] = $key;
       }
     }
 
     if (!$keys) {
       return;
     }
 
     $objects = array_select_keys($this->objects, $keys);
 
     // Build all the markup engines. We need an engine for each field whether
     // we have a cache or not, since we still need to postprocess the cache.
     $engines = array();
     foreach ($objects as $key => $info) {
       $engines[$key] = $info['object']->newMarkupEngine($info['field']);
       $engines[$key]->setConfig('viewer', $this->viewer);
       $engines[$key]->setConfig('contextObject', $this->contextObject);
     }
 
     // Load or build the preprocessor caches.
     $blocks = $this->loadPreprocessorCaches($engines, $objects);
     $blocks = mpull($blocks, 'getCacheData');
 
     $this->engineCaches = $blocks;
 
     // Finalize the output.
     foreach ($objects as $key => $info) {
       $engine = $engines[$key];
       $field = $info['field'];
       $object = $info['object'];
 
       $output = $engine->postprocessText($blocks[$key]);
       $output = $object->didMarkupText($field, $output, $engine);
       $this->objects[$key]['output'] = $output;
     }
 
     return $this;
   }
 
 
   /**
    * Get the output of markup processing for a field queued with
    * @{method:addObject}. Before you can call this method, you must call
    * @{method:process}.
    *
    * @param PhabricatorMarkupInterface  The object to retrieve.
    * @param string                      The field to retrieve.
    * @return string                     Processed output.
    * @task markup
    */
   public function getOutput(PhabricatorMarkupInterface $object, $field) {
     $key = $this->getMarkupFieldKey($object, $field);
     $this->requireKeyProcessed($key);
 
     return $this->objects[$key]['output'];
   }
 
 
   /**
    * Retrieve engine metadata for a given field.
    *
    * @param PhabricatorMarkupInterface  The object to retrieve.
    * @param string                      The field to retrieve.
    * @param string                      The engine metadata field to retrieve.
    * @param wild                        Optional default value.
    * @task markup
    */
   public function getEngineMetadata(
     PhabricatorMarkupInterface $object,
     $field,
     $metadata_key,
     $default = null) {
 
     $key = $this->getMarkupFieldKey($object, $field);
     $this->requireKeyProcessed($key);
 
     return idx($this->engineCaches[$key]['metadata'], $metadata_key, $default);
   }
 
 
   /**
    * @task markup
    */
   private function requireKeyProcessed($key) {
     if (empty($this->objects[$key])) {
       throw new Exception(
         "Call addObject() before using results (key = '{$key}').");
     }
 
     if (!isset($this->objects[$key]['output'])) {
       throw new Exception(
         'Call process() before using results.');
     }
   }
 
 
   /**
    * @task markup
    */
   private function getMarkupFieldKey(
     PhabricatorMarkupInterface $object,
     $field) {
 
     static $custom;
     if ($custom === null) {
       $custom = array_merge(
         self::loadCustomInlineRules(),
         self::loadCustomBlockRules());
 
       $custom = mpull($custom, 'getRuleVersion', null);
       ksort($custom);
       $custom = PhabricatorHash::digestForIndex(serialize($custom));
     }
 
     return $object->getMarkupFieldKey($field).'@'.$this->version.'@'.$custom;
   }
 
 
   /**
    * @task markup
    */
   private function loadPreprocessorCaches(array $engines, array $objects) {
     $blocks = array();
 
     $use_cache = array();
     foreach ($objects as $key => $info) {
       if ($info['object']->shouldUseMarkupCache($info['field'])) {
         $use_cache[$key] = true;
       }
     }
 
     if ($use_cache) {
       try {
         $blocks = id(new PhabricatorMarkupCache())->loadAllWhere(
           'cacheKey IN (%Ls)',
           array_keys($use_cache));
         $blocks = mpull($blocks, null, 'getCacheKey');
       } catch (Exception $ex) {
         phlog($ex);
       }
     }
 
     foreach ($objects as $key => $info) {
       // False check in case MySQL doesn't support unicode characters
       // in the string (T1191), resulting in unserialize returning false.
       if (isset($blocks[$key]) && $blocks[$key]->getCacheData() !== false) {
         // If we already have a preprocessing cache, we don't need to rebuild
         // it.
         continue;
       }
 
       $text = $info['object']->getMarkupText($info['field']);
       $data = $engines[$key]->preprocessText($text);
 
       // NOTE: This is just debugging information to help sort out cache issues.
       // If one machine is misconfigured and poisoning caches you can use this
       // field to hunt it down.
 
       $metadata = array(
         'host' => php_uname('n'),
       );
 
       $blocks[$key] = id(new PhabricatorMarkupCache())
         ->setCacheKey($key)
         ->setCacheData($data)
         ->setMetadata($metadata);
 
       if (isset($use_cache[$key])) {
         // This is just filling a cache and always safe, even on a read pathway.
         $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
           $blocks[$key]->replace();
         unset($unguarded);
       }
     }
 
     return $blocks;
   }
 
 
   /**
    * Set the viewing user. Used to implement object permissions.
    *
    * @param PhabricatorUser The viewing user.
    * @return this
    * @task markup
    */
   public function setViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
   /**
    * Set the context object. Used to implement object permissions.
    *
    * @param The object in which context this remarkup is used.
    * @return this
    * @task markup
    */
   public function setContextObject($object) {
     $this->contextObject = $object;
     return $this;
   }
 
 
 /* -(  Engine Construction  )------------------------------------------------ */
 
 
 
   /**
    * @task engine
    */
   public static function newManiphestMarkupEngine() {
     return self::newMarkupEngine(array(
     ));
   }
 
 
   /**
    * @task engine
    */
   public static function newPhrictionMarkupEngine() {
     return self::newMarkupEngine(array(
       'header.generate-toc' => true,
     ));
   }
 
 
   /**
    * @task engine
    */
   public static function newPhameMarkupEngine() {
     return self::newMarkupEngine(array(
       'macros' => false,
+      'uri.full' => true,
     ));
   }
 
 
   /**
    * @task engine
    */
   public static function newFeedMarkupEngine() {
     return self::newMarkupEngine(
       array(
         'macros'      => false,
         'youtube'     => false,
-
       ));
   }
 
   /**
    * @task engine
    */
   public static function newCalendarMarkupEngine() {
     return self::newMarkupEngine(array(
     ));
   }
 
 
   /**
    * @task engine
    */
   public static function newDifferentialMarkupEngine(array $options = array()) {
     return self::newMarkupEngine(array(
       'differential.diff' => idx($options, 'differential.diff'),
     ));
   }
 
 
   /**
    * @task engine
    */
   public static function newDiffusionMarkupEngine(array $options = array()) {
     return self::newMarkupEngine(array(
       'header.generate-toc' => true,
     ));
   }
 
   /**
    * @task engine
    */
   public static function getEngine($ruleset = 'default') {
     static $engines = array();
     if (isset($engines[$ruleset])) {
       return $engines[$ruleset];
     }
 
     $engine = null;
     switch ($ruleset) {
       case 'default':
         $engine = self::newMarkupEngine(array());
         break;
       case 'nolinebreaks':
         $engine = self::newMarkupEngine(array());
         $engine->setConfig('preserve-linebreaks', false);
         break;
       case 'diffusion-readme':
         $engine = self::newMarkupEngine(array());
         $engine->setConfig('preserve-linebreaks', false);
         $engine->setConfig('header.generate-toc', true);
         break;
       case 'diviner':
         $engine = self::newMarkupEngine(array());
         $engine->setConfig('preserve-linebreaks', false);
   //    $engine->setConfig('diviner.renderer', new DivinerDefaultRenderer());
         $engine->setConfig('header.generate-toc', true);
         break;
       case 'extract':
         // Engine used for reference/edge extraction. Turn off anything which
         // is slow and doesn't change reference extraction.
         $engine = self::newMarkupEngine(array());
         $engine->setConfig('pygments.enabled', false);
         break;
       default:
         throw new Exception("Unknown engine ruleset: {$ruleset}!");
     }
 
     $engines[$ruleset] = $engine;
     return $engine;
   }
 
   /**
    * @task engine
    */
   private static function getMarkupEngineDefaultConfiguration() {
     return array(
       'pygments'      => PhabricatorEnv::getEnvConfig('pygments.enabled'),
       'youtube'       => PhabricatorEnv::getEnvConfig(
         'remarkup.enable-embedded-youtube'),
       'differential.diff' => null,
       'header.generate-toc' => false,
       'macros'        => true,
       'uri.allowed-protocols' => PhabricatorEnv::getEnvConfig(
         'uri.allowed-protocols'),
+      'uri.full' => false,
       'syntax-highlighter.engine' => PhabricatorEnv::getEnvConfig(
         'syntax-highlighter.engine'),
       'preserve-linebreaks' => true,
     );
   }
 
 
   /**
    * @task engine
    */
   public static function newMarkupEngine(array $options) {
 
     $options += self::getMarkupEngineDefaultConfiguration();
 
     $engine = new PhutilRemarkupEngine();
 
     $engine->setConfig('preserve-linebreaks', $options['preserve-linebreaks']);
     $engine->setConfig('pygments.enabled', $options['pygments']);
     $engine->setConfig(
       'uri.allowed-protocols',
       $options['uri.allowed-protocols']);
     $engine->setConfig('differential.diff', $options['differential.diff']);
     $engine->setConfig('header.generate-toc', $options['header.generate-toc']);
     $engine->setConfig(
       'syntax-highlighter.engine',
       $options['syntax-highlighter.engine']);
 
+    $engine->setConfig('uri.full', $options['uri.full']);
+
     $rules = array();
     $rules[] = new PhutilRemarkupEscapeRemarkupRule();
     $rules[] = new PhutilRemarkupMonospaceRule();
 
 
     $rules[] = new PhutilRemarkupDocumentLinkRule();
     $rules[] = new PhabricatorNavigationRemarkupRule();
 
     if ($options['youtube']) {
       $rules[] = new PhabricatorYoutubeRemarkupRule();
     }
 
     $applications = PhabricatorApplication::getAllInstalledApplications();
     foreach ($applications as $application) {
       foreach ($application->getRemarkupRules() as $rule) {
         $rules[] = $rule;
       }
     }
 
     $rules[] = new PhutilRemarkupHyperlinkRule();
 
     if ($options['macros']) {
       $rules[] = new PhabricatorImageMacroRemarkupRule();
       $rules[] = new PhabricatorMemeRemarkupRule();
     }
 
     $rules[] = new PhutilRemarkupBoldRule();
     $rules[] = new PhutilRemarkupItalicRule();
     $rules[] = new PhutilRemarkupDelRule();
     $rules[] = new PhutilRemarkupUnderlineRule();
 
     foreach (self::loadCustomInlineRules() as $rule) {
       $rules[] = $rule;
     }
 
     $blocks = array();
     $blocks[] = new PhutilRemarkupQuotesBlockRule();
     $blocks[] = new PhutilRemarkupReplyBlockRule();
     $blocks[] = new PhutilRemarkupLiteralBlockRule();
     $blocks[] = new PhutilRemarkupHeaderBlockRule();
     $blocks[] = new PhutilRemarkupHorizontalRuleBlockRule();
     $blocks[] = new PhutilRemarkupListBlockRule();
     $blocks[] = new PhutilRemarkupCodeBlockRule();
     $blocks[] = new PhutilRemarkupNoteBlockRule();
     $blocks[] = new PhutilRemarkupTableBlockRule();
     $blocks[] = new PhutilRemarkupSimpleTableBlockRule();
     $blocks[] = new PhutilRemarkupInterpreterBlockRule();
     $blocks[] = new PhutilRemarkupDefaultBlockRule();
 
     foreach (self::loadCustomBlockRules() as $rule) {
       $blocks[] = $rule;
     }
 
     foreach ($blocks as $block) {
       $block->setMarkupRules($rules);
     }
 
     $engine->setBlockRules($blocks);
 
     return $engine;
   }
 
   public static function extractPHIDsFromMentions(
     PhabricatorUser $viewer,
     array $content_blocks) {
 
     $mentions = array();
 
     $engine = self::newDifferentialMarkupEngine();
     $engine->setConfig('viewer', $viewer);
 
     foreach ($content_blocks as $content_block) {
       $engine->markupText($content_block);
       $phids = $engine->getTextMetadata(
         PhabricatorMentionRemarkupRule::KEY_MENTIONED,
         array());
       $mentions += $phids;
     }
 
     return $mentions;
   }
 
   public static function extractFilePHIDsFromEmbeddedFiles(
     PhabricatorUser $viewer,
     array $content_blocks) {
     $files = array();
 
     $engine = self::newDifferentialMarkupEngine();
     $engine->setConfig('viewer', $viewer);
 
     foreach ($content_blocks as $content_block) {
       $engine->markupText($content_block);
       $phids = $engine->getTextMetadata(
         PhabricatorEmbedFileRemarkupRule::KEY_EMBED_FILE_PHIDS,
         array());
       foreach ($phids as $phid) {
         $files[$phid] = $phid;
       }
     }
 
     return array_values($files);
   }
 
   /**
    * Produce a corpus summary, in a way that shortens the underlying text
    * without truncating it somewhere awkward.
    *
    * TODO: We could do a better job of this.
    *
    * @param string  Remarkup corpus to summarize.
    * @return string Summarized corpus.
    */
   public static function summarize($corpus) {
 
     // Major goals here are:
     //  - Don't split in the middle of a character (utf-8).
     //  - Don't split in the middle of, e.g., **bold** text, since
     //    we end up with hanging '**' in the summary.
     //  - Try not to pick an image macro, header, embedded file, etc.
     //  - Hopefully don't return too much text. We don't explicitly limit
     //    this right now.
 
-    $blocks = preg_split("/\n *\n\s*/", trim($corpus));
+    $blocks = preg_split("/\n *\n\s*/", $corpus);
 
     $best = null;
     foreach ($blocks as $block) {
       // This is a test for normal spaces in the block, i.e. a heuristic to
       // distinguish standard paragraphs from things like image macros. It may
       // not work well for non-latin text. We prefer to summarize with a
       // paragraph of normal words over an image macro, if possible.
       $has_space = preg_match('/\w\s\w/', $block);
 
       // This is a test to find embedded images and headers. We prefer to
       // summarize with a normal paragraph over a header or an embedded object,
       // if possible.
       $has_embed = preg_match('/^[{=]/', $block);
 
       if ($has_space && !$has_embed) {
         // This seems like a good summary, so return it.
         return $block;
       }
 
       if (!$best) {
         // This is the first block we found; if everything is garbage just
         // use the first block.
         $best = $block;
       }
     }
 
     return $best;
   }
 
   private static function loadCustomInlineRules() {
     return id(new PhutilSymbolLoader())
       ->setAncestorClass('PhabricatorRemarkupCustomInlineRule')
       ->loadObjects();
   }
 
   private static function loadCustomBlockRules() {
     return id(new PhutilSymbolLoader())
       ->setAncestorClass('PhabricatorRemarkupCustomBlockRule')
       ->loadObjects();
   }
 
 }
diff --git a/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php b/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php
index 0b4471d811..8f776716ea 100644
--- a/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php
+++ b/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php
@@ -1,377 +1,382 @@
 <?php
 
 abstract class PhabricatorObjectRemarkupRule extends PhutilRemarkupRule {
 
   const KEY_RULE_OBJECT = 'rule.object';
   const KEY_MENTIONED_OBJECTS = 'rule.object.mentioned';
 
   abstract protected function getObjectNamePrefix();
   abstract protected function loadObjects(array $ids);
 
   public function getPriority() {
     return 450.0;
   }
 
   protected function getObjectNamePrefixBeginsWithWordCharacter() {
     $prefix = $this->getObjectNamePrefix();
     return preg_match('/^\w/', $prefix);
   }
 
   protected function getObjectIDPattern() {
     return '[1-9]\d*';
   }
 
   protected function shouldMarkupObject(array $params) {
     return true;
   }
 
   protected function loadHandles(array $objects) {
     $phids = mpull($objects, 'getPHID');
 
-    $handles = id(new PhabricatorHandleQuery($phids))
-      ->withPHIDs($phids)
-      ->setViewer($this->getEngine()->getConfig('viewer'))
-      ->execute();
+    $viewer = $this->getEngine()->getConfig('viewer');
+    $handles = $viewer->loadHandles($phids);
+    $handles = iterator_to_array($handles);
 
     $result = array();
     foreach ($objects as $id => $object) {
       $result[$id] = $handles[$object->getPHID()];
     }
     return $result;
   }
 
   protected function getObjectHref(
     $object,
     PhabricatorObjectHandle $handle,
     $id) {
 
-    return $handle->getURI();
+    $uri = $handle->getURI();
+
+    if ($this->getEngine()->getConfig('uri.full')) {
+      $uri = PhabricatorEnv::getURI($uri);
+    }
+
+    return $uri;
   }
 
   protected function renderObjectRefForAnyMedia (
     $object,
     PhabricatorObjectHandle $handle,
     $anchor,
     $id) {
 
     $href = $this->getObjectHref($object, $handle, $id);
     $text = $this->getObjectNamePrefix().$id;
 
     if ($anchor) {
       $href = $href.'#'.$anchor;
       $text = $text.'#'.$anchor;
     }
 
     if ($this->getEngine()->isTextMode()) {
       return PhabricatorEnv::getProductionURI($href);
     } else if ($this->getEngine()->isHTMLMailMode()) {
       $href = PhabricatorEnv::getProductionURI($href);
       return $this->renderObjectTagForMail($text, $href, $handle);
     }
 
     return $this->renderObjectRef($object, $handle, $anchor, $id);
 
   }
 
   protected function renderObjectRef(
     $object,
     PhabricatorObjectHandle $handle,
     $anchor,
     $id) {
 
     $href = $this->getObjectHref($object, $handle, $id);
     $text = $this->getObjectNamePrefix().$id;
     $status_closed = PhabricatorObjectHandleStatus::STATUS_CLOSED;
 
     if ($anchor) {
       $href = $href.'#'.$anchor;
       $text = $text.'#'.$anchor;
     }
 
     $attr = array(
       'phid'    => $handle->getPHID(),
       'closed'  => ($handle->getStatus() == $status_closed),
     );
 
     return $this->renderHovertag($text, $href, $attr);
   }
 
   protected function renderObjectEmbedForAnyMedia(
     $object,
     PhabricatorObjectHandle $handle,
     $options) {
 
     $name = $handle->getFullName();
     $href = $handle->getURI();
 
     if ($this->getEngine()->isTextMode()) {
       return $name.' <'.PhabricatorEnv::getProductionURI($href).'>';
     } else if ($this->getEngine()->isHTMLMailMode()) {
       $href = PhabricatorEnv::getProductionURI($href);
       return $this->renderObjectTagForMail($name, $href, $handle);
     }
 
     return $this->renderObjectEmbed($object, $handle, $options);
   }
 
   protected function renderObjectEmbed(
     $object,
     PhabricatorObjectHandle $handle,
     $options) {
 
     $name = $handle->getFullName();
     $href = $handle->getURI();
     $status_closed = PhabricatorObjectHandleStatus::STATUS_CLOSED;
     $attr = array(
       'phid' => $handle->getPHID(),
       'closed'  => ($handle->getStatus() == $status_closed),
     );
 
     return $this->renderHovertag($name, $href, $attr);
   }
 
   protected function renderObjectTagForMail(
     $text,
     $href,
     PhabricatorObjectHandle $handle) {
 
     $status_closed = PhabricatorObjectHandleStatus::STATUS_CLOSED;
     $strikethrough = $handle->getStatus() == $status_closed ?
       'text-decoration: line-through;' :
       'text-decoration: none;';
 
     return phutil_tag(
       'a',
       array(
         'href' => $href,
         'style' => 'background-color: #e7e7e7;
           border-color: #e7e7e7;
           border-radius: 3px;
           padding: 0 4px;
           font-weight: bold;
           color: black;'
           .$strikethrough,
       ),
       $text);
   }
 
   protected function renderHovertag($name, $href, array $attr = array()) {
     return id(new PHUITagView())
       ->setName($name)
       ->setHref($href)
       ->setType(PHUITagView::TYPE_OBJECT)
       ->setPHID(idx($attr, 'phid'))
       ->setClosed(idx($attr, 'closed'))
       ->render();
   }
 
   public function apply($text) {
     $text = preg_replace_callback(
       $this->getObjectEmbedPattern(),
       array($this, 'markupObjectEmbed'),
       $text);
 
     $text = preg_replace_callback(
       $this->getObjectReferencePattern(),
       array($this, 'markupObjectReference'),
       $text);
 
     return $text;
   }
 
   private function getObjectEmbedPattern() {
     $prefix = $this->getObjectNamePrefix();
     $prefix = preg_quote($prefix);
     $id = $this->getObjectIDPattern();
 
     return '(\B{'.$prefix.'('.$id.')([,\s](?:[^}\\\\]|\\\\.)*)?}\B)u';
   }
 
   private function getObjectReferencePattern() {
     $prefix = $this->getObjectNamePrefix();
     $prefix = preg_quote($prefix);
 
     $id = $this->getObjectIDPattern();
 
     // If the prefix starts with a word character (like "D"), we want to
     // require a word boundary so that we don't match "XD1" as "D1". If the
     // prefix does not start with a word character, we want to require no word
     // boundary for the same reasons. Test if the prefix starts with a word
     // character.
     if ($this->getObjectNamePrefixBeginsWithWordCharacter()) {
       $boundary = '\\b';
     } else {
       $boundary = '\\B';
     }
 
     // The "(?<![#-])" prevents us from linking "#abcdef" or similar, and
     // "ABC-T1" (see T5714).
 
     // The "\b" allows us to link "(abcdef)" or similar without linking things
     // in the middle of words.
 
     return '((?<![#-])'.$boundary.$prefix.'('.$id.')(?:#([-\w\d]+))?(?!\w))u';
   }
 
 
   /**
    * Extract matched object references from a block of text.
    *
    * This is intended to make it easy to write unit tests for object remarkup
    * rules. Production code is not normally expected to call this method.
    *
    * @param   string  Text to match rules against.
    * @return  wild    Matches, suitable for writing unit tests against.
    */
   public function extractReferences($text) {
     $embed_matches = null;
     preg_match_all(
       $this->getObjectEmbedPattern(),
       $text,
       $embed_matches,
       PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
 
     $ref_matches = null;
     preg_match_all(
       $this->getObjectReferencePattern(),
       $text,
       $ref_matches,
       PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
 
     $results = array();
     $sets = array(
       'embed' => $embed_matches,
       'ref' => $ref_matches,
     );
     foreach ($sets as $type => $matches) {
       $formatted = array();
       foreach ($matches as $match) {
         $format = array(
           'offset' => $match[1][1],
           'id' => $match[1][0],
         );
         if (isset($match[2][0])) {
           $format['tail'] = $match[2][0];
         }
         $formatted[] = $format;
       }
       $results[$type] = $formatted;
     }
 
     return $results;
   }
 
   public function markupObjectEmbed(array $matches) {
     if (!$this->isFlatText($matches[0])) {
       return $matches[0];
     }
 
     return $this->markupObject(array(
       'type' => 'embed',
       'id' => $matches[1],
       'options' => idx($matches, 2),
       'original' => $matches[0],
     ));
   }
 
   public function markupObjectReference(array $matches) {
     if (!$this->isFlatText($matches[0])) {
       return $matches[0];
     }
 
     return $this->markupObject(array(
       'type' => 'ref',
       'id' => $matches[1],
       'anchor' => idx($matches, 2),
       'original' => $matches[0],
     ));
   }
 
   private function markupObject(array $params) {
     if (!$this->shouldMarkupObject($params)) {
       return $params['original'];
     }
 
     $regex = trim(
       PhabricatorEnv::getEnvConfig('remarkup.ignored-object-names'));
     if ($regex && preg_match($regex, $params['original'])) {
       return $params['original'];
     }
 
     $engine = $this->getEngine();
     $token = $engine->storeText('x');
 
     $metadata_key = self::KEY_RULE_OBJECT.'.'.$this->getObjectNamePrefix();
     $metadata = $engine->getTextMetadata($metadata_key, array());
 
     $metadata[] = array(
       'token'   => $token,
     ) + $params;
 
     $engine->setTextMetadata($metadata_key, $metadata);
 
     return $token;
   }
 
   public function didMarkupText() {
     $engine = $this->getEngine();
     $metadata_key = self::KEY_RULE_OBJECT.'.'.$this->getObjectNamePrefix();
     $metadata = $engine->getTextMetadata($metadata_key, array());
 
     if (!$metadata) {
       return;
     }
 
 
     $ids = ipull($metadata, 'id');
     $objects = $this->loadObjects($ids);
 
     // For objects that are invalid or which the user can't see, just render
     // the original text.
 
     // TODO: We should probably distinguish between these cases and render a
     // "you can't see this" state for nonvisible objects.
 
     foreach ($metadata as $key => $spec) {
       if (empty($objects[$spec['id']])) {
         $engine->overwriteStoredText(
           $spec['token'],
           $spec['original']);
         unset($metadata[$key]);
       }
     }
 
     $phids = $engine->getTextMetadata(self::KEY_MENTIONED_OBJECTS, array());
     foreach ($objects as $object) {
       $phids[$object->getPHID()] = $object->getPHID();
     }
     $engine->setTextMetadata(self::KEY_MENTIONED_OBJECTS, $phids);
 
     $handles = $this->loadHandles($objects);
     foreach ($metadata as $key => $spec) {
       $handle = $handles[$spec['id']];
       $object = $objects[$spec['id']];
       switch ($spec['type']) {
         case 'ref':
 
           $view = $this->renderObjectRefForAnyMedia(
             $object,
             $handle,
             $spec['anchor'],
             $spec['id']);
           break;
         case 'embed':
           $spec['options'] = $this->assertFlatText($spec['options']);
           $view = $this->renderObjectEmbedForAnyMedia(
             $object,
             $handle,
             $spec['options']);
           break;
       }
       $engine->overwriteStoredText($spec['token'], $view);
     }
 
     $engine->setTextMetadata($metadata_key, array());
   }
 
 }
diff --git a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php
index 1f353d7dfb..e1b2028a19 100644
--- a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php
+++ b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php
@@ -1,649 +1,683 @@
 <?php
 
 /**
  * A @{class:PhabricatorQuery} which filters results according to visibility
  * policies for the querying user. Broadly, this class allows you to implement
  * a query that returns only objects the user is allowed to see.
  *
  *   $results = id(new ExampleQuery())
  *     ->setViewer($user)
  *     ->withConstraint($example)
  *     ->execute();
  *
  * Normally, you should extend @{class:PhabricatorCursorPagedPolicyAwareQuery},
  * not this class. @{class:PhabricatorCursorPagedPolicyAwareQuery} provides a
  * more practical interface for building usable queries against most object
  * types.
  *
  * NOTE: Although this class extends @{class:PhabricatorOffsetPagedQuery},
  * offset paging with policy filtering is not efficient. All results must be
  * loaded into the application and filtered here: skipping `N` rows via offset
  * is an `O(N)` operation with a large constant. Prefer cursor-based paging
  * with @{class:PhabricatorCursorPagedPolicyAwareQuery}, which can filter far
  * more efficiently in MySQL.
  *
  * @task config     Query Configuration
  * @task exec       Executing Queries
  * @task policyimpl Policy Query Implementation
  */
 abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery {
 
   private $viewer;
   private $parentQuery;
   private $rawResultLimit;
   private $capabilities;
   private $workspace = array();
+  private $inFlightPHIDs = array();
   private $policyFilteredPHIDs = array();
   private $canUseApplication;
 
   /**
    * Should we continue or throw an exception when a query result is filtered
    * by policy rules?
    *
    * Values are `true` (raise exceptions), `false` (do not raise exceptions)
    * and `null` (inherit from parent query, with no exceptions by default).
    */
   private $raisePolicyExceptions;
 
 
 /* -(  Query Configuration  )------------------------------------------------ */
 
 
   /**
    * Set the viewer who is executing the query. Results will be filtered
    * according to the viewer's capabilities. You must set a viewer to execute
    * a policy query.
    *
    * @param PhabricatorUser The viewing user.
    * @return this
    * @task config
    */
   final public function setViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
 
   /**
    * Get the query's viewer.
    *
    * @return PhabricatorUser The viewing user.
    * @task config
    */
   final public function getViewer() {
     return $this->viewer;
   }
 
 
   /**
    * Set the parent query of this query. This is useful for nested queries so
    * that configuration like whether or not to raise policy exceptions is
    * seamlessly passed along to child queries.
    *
    * @return this
    * @task config
    */
   final public function setParentQuery(PhabricatorPolicyAwareQuery $query) {
     $this->parentQuery = $query;
     return $this;
   }
 
 
   /**
    * Get the parent query. See @{method:setParentQuery} for discussion.
    *
    * @return PhabricatorPolicyAwareQuery The parent query.
    * @task config
    */
   final public function getParentQuery() {
     return $this->parentQuery;
   }
 
 
   /**
    * Hook to configure whether this query should raise policy exceptions.
    *
    * @return this
    * @task config
    */
   final public function setRaisePolicyExceptions($bool) {
     $this->raisePolicyExceptions = $bool;
     return $this;
   }
 
 
   /**
    * @return bool
    * @task config
    */
   final public function shouldRaisePolicyExceptions() {
     return (bool)$this->raisePolicyExceptions;
   }
 
 
   /**
    * @task config
    */
   final public function requireCapabilities(array $capabilities) {
     $this->capabilities = $capabilities;
     return $this;
   }
 
 
 /* -(  Query Execution  )---------------------------------------------------- */
 
 
   /**
    * Execute the query, expecting a single result. This method simplifies
    * loading objects for detail pages or edit views.
    *
    *   // Load one result by ID.
    *   $obj = id(new ExampleQuery())
    *     ->setViewer($user)
    *     ->withIDs(array($id))
    *     ->executeOne();
    *   if (!$obj) {
    *     return new Aphront404Response();
    *   }
    *
    * If zero results match the query, this method returns `null`.
    * If one result matches the query, this method returns that result.
    *
    * If two or more results match the query, this method throws an exception.
    * You should use this method only when the query constraints guarantee at
    * most one match (e.g., selecting a specific ID or PHID).
    *
    * If one result matches the query but it is caught by the policy filter (for
    * example, the user is trying to view or edit an object which exists but
    * which they do not have permission to see) a policy exception is thrown.
    *
    * @return mixed Single result, or null.
    * @task exec
    */
   final public function executeOne() {
 
     $this->setRaisePolicyExceptions(true);
     try {
       $results = $this->execute();
     } catch (Exception $ex) {
       $this->setRaisePolicyExceptions(false);
       throw $ex;
     }
 
     if (count($results) > 1) {
       throw new Exception('Expected a single result!');
     }
 
     if (!$results) {
       return null;
     }
 
     return head($results);
   }
 
 
   /**
    * Execute the query, loading all visible results.
    *
    * @return list<PhabricatorPolicyInterface> Result objects.
    * @task exec
    */
   final public function execute() {
     if (!$this->viewer) {
       throw new Exception('Call setViewer() before execute()!');
     }
 
     $parent_query = $this->getParentQuery();
     if ($parent_query && ($this->raisePolicyExceptions === null)) {
       $this->setRaisePolicyExceptions(
         $parent_query->shouldRaisePolicyExceptions());
     }
 
     $results = array();
 
     $filter = $this->getPolicyFilter();
 
     $offset = (int)$this->getOffset();
     $limit  = (int)$this->getLimit();
     $count  = 0;
 
     if ($limit) {
       $need = $offset + $limit;
     } else {
       $need = 0;
     }
 
     $this->willExecute();
 
     do {
       if ($need) {
         $this->rawResultLimit = min($need - $count, 1024);
       } else {
         $this->rawResultLimit = 0;
       }
 
       if ($this->canViewerUseQueryApplication()) {
         try {
           $page = $this->loadPage();
         } catch (PhabricatorEmptyQueryException $ex) {
           $page = array();
         }
       } else {
         $page = array();
       }
 
       if ($page) {
         $maybe_visible = $this->willFilterPage($page);
       } else {
         $maybe_visible = array();
       }
 
       if ($this->shouldDisablePolicyFiltering()) {
         $visible = $maybe_visible;
       } else {
         $visible = $filter->apply($maybe_visible);
 
         $policy_filtered = array();
         foreach ($maybe_visible as $key => $object) {
           if (empty($visible[$key])) {
             $phid = $object->getPHID();
             if ($phid) {
               $policy_filtered[$phid] = $phid;
             }
           }
         }
         $this->addPolicyFilteredPHIDs($policy_filtered);
       }
 
       if ($visible) {
         $this->putObjectsInWorkspace($this->getWorkspaceMapForPage($visible));
         $visible = $this->didFilterPage($visible);
       }
 
       $removed = array();
       foreach ($maybe_visible as $key => $object) {
         if (empty($visible[$key])) {
           $removed[$key] = $object;
         }
       }
 
       $this->didFilterResults($removed);
 
       foreach ($visible as $key => $result) {
         ++$count;
 
         // If we have an offset, we just ignore that many results and start
         // storing them only once we've hit the offset. This reduces memory
         // requirements for large offsets, compared to storing them all and
         // slicing them away later.
         if ($count > $offset) {
           $results[$key] = $result;
         }
 
         if ($need && ($count >= $need)) {
           // If we have all the rows we need, break out of the paging query.
           break 2;
         }
       }
 
       if (!$this->rawResultLimit) {
         // If we don't have a load count, we loaded all the results. We do
         // not need to load another page.
         break;
       }
 
       if (count($page) < $this->rawResultLimit) {
         // If we have a load count but the unfiltered results contained fewer
         // objects, we know this was the last page of objects; we do not need
         // to load another page because we can deduce it would be empty.
         break;
       }
 
       $this->nextPage($page);
     } while (true);
 
     $results = $this->didLoadResults($results);
 
     return $results;
   }
 
   private function getPolicyFilter() {
     $filter = new PhabricatorPolicyFilter();
     $filter->setViewer($this->viewer);
     $capabilities = $this->getRequiredCapabilities();
     $filter->requireCapabilities($capabilities);
     $filter->raisePolicyExceptions($this->shouldRaisePolicyExceptions());
 
     return $filter;
   }
 
   protected function getRequiredCapabilities() {
     if ($this->capabilities) {
       return $this->capabilities;
     }
 
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
     );
   }
 
   protected function applyPolicyFilter(array $objects, array $capabilities) {
     if ($this->shouldDisablePolicyFiltering()) {
       return $objects;
     }
     $filter = $this->getPolicyFilter();
     $filter->requireCapabilities($capabilities);
     return $filter->apply($objects);
   }
 
   protected function didRejectResult(PhabricatorPolicyInterface $object) {
     $this->getPolicyFilter()->rejectObject(
       $object,
       $object->getPolicy(PhabricatorPolicyCapability::CAN_VIEW),
       PhabricatorPolicyCapability::CAN_VIEW);
   }
 
   public function addPolicyFilteredPHIDs(array $phids) {
     $this->policyFilteredPHIDs += $phids;
     if ($this->getParentQuery()) {
       $this->getParentQuery()->addPolicyFilteredPHIDs($phids);
     }
     return $this;
   }
 
   /**
    * Return a map of all object PHIDs which were loaded in the query but
    * filtered out by policy constraints. This allows a caller to distinguish
    * between objects which do not exist (or, at least, were filtered at the
    * content level) and objects which exist but aren't visible.
    *
    * @return map<phid, phid> Map of object PHIDs which were filtered
    *   by policies.
    * @task exec
    */
   public function getPolicyFilteredPHIDs() {
     return $this->policyFilteredPHIDs;
   }
 
 
 /* -(  Query Workspace  )---------------------------------------------------- */
 
 
   /**
    * Put a map of objects into the query workspace. Many queries perform
    * subqueries, which can eventually end up loading the same objects more than
    * once (often to perform policy checks).
    *
    * For example, loading a user may load the user's profile image, which might
    * load the user object again in order to verify that the viewer has
    * permission to see the file.
    *
    * The "query workspace" allows queries to load objects from elsewhere in a
    * query block instead of refetching them.
    *
    * When using the query workspace, it's important to obey two rules:
    *
    * **Never put objects into the workspace which the viewer may not be able
    * to see**. You need to apply all policy filtering //before// putting
    * objects in the workspace. Otherwise, subqueries may read the objects and
    * use them to permit access to content the user shouldn't be able to view.
    *
    * **Fully enrich objects pulled from the workspace.** After pulling objects
    * from the workspace, you still need to load and attach any additional
    * content the query requests. Otherwise, a query might return objects without
    * requested content.
    *
    * Generally, you do not need to update the workspace yourself: it is
    * automatically populated as a side effect of objects surviving policy
    * filtering.
    *
    * @param map<phid, PhabricatorPolicyInterface> Objects to add to the query
    *   workspace.
    * @return this
    * @task workspace
    */
   public function putObjectsInWorkspace(array $objects) {
     assert_instances_of($objects, 'PhabricatorPolicyInterface');
 
     $viewer_phid = $this->getViewer()->getPHID();
 
     // The workspace is scoped per viewer to prevent accidental contamination.
     if (empty($this->workspace[$viewer_phid])) {
       $this->workspace[$viewer_phid] = array();
     }
 
     $this->workspace[$viewer_phid] += $objects;
 
     return $this;
   }
 
 
   /**
    * Retrieve objects from the query workspace. For more discussion about the
    * workspace mechanism, see @{method:putObjectsInWorkspace}. This method
    * searches both the current query's workspace and the workspaces of parent
    * queries.
    *
    * @param list<phid> List of PHIDs to retrieve.
    * @return this
    * @task workspace
    */
   public function getObjectsFromWorkspace(array $phids) {
     $viewer_phid = $this->getViewer()->getPHID();
 
     $results = array();
     foreach ($phids as $key => $phid) {
       if (isset($this->workspace[$viewer_phid][$phid])) {
         $results[$phid] = $this->workspace[$viewer_phid][$phid];
         unset($phids[$key]);
       }
     }
 
     if ($phids && $this->getParentQuery()) {
       $results += $this->getParentQuery()->getObjectsFromWorkspace($phids);
     }
 
     return $results;
   }
 
 
   /**
    * Convert a result page to a `<phid, PhabricatorPolicyInterface>` map.
    *
    * @param list<PhabricatorPolicyInterface> Objects.
    * @return map<phid, PhabricatorPolicyInterface> Map of objects which can
    *   be put into the workspace.
    * @task workspace
    */
   protected function getWorkspaceMapForPage(array $results) {
     $map = array();
     foreach ($results as $result) {
       $phid = $result->getPHID();
       if ($phid !== null) {
         $map[$phid] = $result;
       }
     }
 
     return $map;
   }
 
 
+  /**
+   * Mark PHIDs as in flight.
+   *
+   * PHIDs which are "in flight" are actively being queried for. Using this
+   * list can prevent infinite query loops by aborting queries which cycle.
+   *
+   * @param list<phid> List of PHIDs which are now in flight.
+   * @return this
+   */
+  public function putPHIDsInFlight(array $phids) {
+    foreach ($phids as $phid) {
+      $this->inFlightPHIDs[$phid] = $phid;
+    }
+    return $this;
+  }
+
+
+  /**
+   * Get PHIDs which are currently in flight.
+   *
+   * PHIDs which are "in flight" are actively being queried for.
+   *
+   * @return map<phid, phid> PHIDs currently in flight.
+   */
+  public function getPHIDsInFlight() {
+    $results = $this->inFlightPHIDs;
+    if ($this->getParentQuery()) {
+      $results += $this->getParentQuery()->getPHIDsInFlight();
+    }
+    return $results;
+  }
+
+
 /* -(  Policy Query Implementation  )---------------------------------------- */
 
 
   /**
    * Get the number of results @{method:loadPage} should load. If the value is
    * 0, @{method:loadPage} should load all available results.
    *
    * @return int The number of results to load, or 0 for all results.
    * @task policyimpl
    */
   final protected function getRawResultLimit() {
     return $this->rawResultLimit;
   }
 
 
   /**
    * Hook invoked before query execution. Generally, implementations should
    * reset any internal cursors.
    *
    * @return void
    * @task policyimpl
    */
   protected function willExecute() {
     return;
   }
 
 
   /**
    * Load a raw page of results. Generally, implementations should load objects
    * from the database. They should attempt to return the number of results
    * hinted by @{method:getRawResultLimit}.
    *
    * @return list<PhabricatorPolicyInterface> List of filterable policy objects.
    * @task policyimpl
    */
   abstract protected function loadPage();
 
 
   /**
    * Update internal state so that the next call to @{method:loadPage} will
    * return new results. Generally, you should adjust a cursor position based
    * on the provided result page.
    *
    * @param list<PhabricatorPolicyInterface> The current page of results.
    * @return void
    * @task policyimpl
    */
   abstract protected function nextPage(array $page);
 
 
   /**
    * Hook for applying a page filter prior to the privacy filter. This allows
    * you to drop some items from the result set without creating problems with
    * pagination or cursor updates. You can also load and attach data which is
    * required to perform policy filtering.
    *
    * Generally, you should load non-policy data and perform non-policy filtering
    * later, in @{method:didFilterPage}. Strictly fewer objects will make it that
    * far (so the program will load less data) and subqueries from that context
    * can use the query workspace to further reduce query load.
    *
    * This method will only be called if data is available. Implementations
    * do not need to handle the case of no results specially.
    *
    * @param   list<wild>  Results from `loadPage()`.
    * @return  list<PhabricatorPolicyInterface> Objects for policy filtering.
    * @task policyimpl
    */
   protected function willFilterPage(array $page) {
     return $page;
   }
 
   /**
    * Hook for performing additional non-policy loading or filtering after an
    * object has satisfied all policy checks. Generally, this means loading and
    * attaching related data.
    *
    * Subqueries executed during this phase can use the query workspace, which
    * may improve performance or make circular policies resolvable. Data which
    * is not necessary for policy filtering should generally be loaded here.
    *
    * This callback can still filter objects (for example, if attachable data
    * is discovered to not exist), but should not do so for policy reasons.
    *
    * This method will only be called if data is available. Implementations do
    * not need to handle the case of no results specially.
    *
    * @param list<wild> Results from @{method:willFilterPage()}.
    * @return list<PhabricatorPolicyInterface> Objects after additional
    *   non-policy processing.
    */
   protected function didFilterPage(array $page) {
     return $page;
   }
 
 
   /**
    * Hook for removing filtered results from alternate result sets. This
    * hook will be called with any objects which were returned by the query but
    * filtered for policy reasons. The query should remove them from any cached
    * or partial result sets.
    *
    * @param list<wild>  List of objects that should not be returned by alternate
    *                    result mechanisms.
    * @return void
    * @task policyimpl
    */
   protected function didFilterResults(array $results) {
     return;
   }
 
 
   /**
    * Hook for applying final adjustments before results are returned. This is
    * used by @{class:PhabricatorCursorPagedPolicyAwareQuery} to reverse results
    * that are queried during reverse paging.
    *
    * @param   list<PhabricatorPolicyInterface> Query results.
    * @return  list<PhabricatorPolicyInterface> Final results.
    * @task policyimpl
    */
   protected function didLoadResults(array $results) {
     return $results;
   }
 
 
   /**
    * Allows a subclass to disable policy filtering. This method is dangerous.
    * It should be used only if the query loads data which has already been
    * filtered (for example, because it wraps some other query which uses
    * normal policy filtering).
    *
    * @return bool True to disable all policy filtering.
    * @task policyimpl
    */
   protected function shouldDisablePolicyFiltering() {
     return false;
   }
 
 
   /**
    * If this query belongs to an application, return the application class name
    * here. This will prevent the query from returning results if the viewer can
    * not access the application.
    *
    * If this query does not belong to an application, return `null`.
    *
    * @return string|null Application class name.
    */
   abstract public function getQueryApplicationClass();
 
 
   /**
    * Determine if the viewer has permission to use this query's application.
    * For queries which aren't part of an application, this method always returns
    * true.
    *
    * @return bool True if the viewer has application-level permission to
    *   execute the query.
    */
   public function canViewerUseQueryApplication() {
     if ($this->canUseApplication === null) {
       $class = $this->getQueryApplicationClass();
       if (!$class) {
         $this->canUseApplication = true;
       } else {
         $result = id(new PhabricatorApplicationQuery())
           ->setViewer($this->getViewer())
           ->withClasses(array($class))
           ->execute();
 
         $this->canUseApplication = (bool)$result;
       }
     }
 
     return $this->canUseApplication;
   }
 
 }
diff --git a/src/infrastructure/sms/storage/PhabricatorSMS.php b/src/infrastructure/sms/storage/PhabricatorSMS.php
index c17d8f16da..7b6c85f159 100644
--- a/src/infrastructure/sms/storage/PhabricatorSMS.php
+++ b/src/infrastructure/sms/storage/PhabricatorSMS.php
@@ -1,75 +1,75 @@
 <?php
 
 final class PhabricatorSMS
   extends PhabricatorSMSDAO {
 
   const MAXIMUM_SEND_TRIES        = 5;
 
   /**
    * Status constants should be 16 characters or less. See status entries
    * for details on what they indicate about the underlying SMS.
    */
 
   // in the beginning, all SMS are unsent
   const STATUS_UNSENT             = 'unsent';
   // that nebulous time when we've sent it from Phabricator but haven't
   // heard anything from the external API
   const STATUS_SENT_UNCONFIRMED   = 'sent-unconfirmed';
   // "success"
   const STATUS_SENT               = 'sent';
   // "fail" but we'll try again
   const STATUS_FAILED             = 'failed';
   // we're giving up on our external API partner
   const STATUS_FAILED_PERMANENTLY = 'permafailed';
 
   const SHORTNAME_PLACEHOLDER     = 'phabricator';
 
   protected $providerShortName;
   protected $providerSMSID;
   // numbers can be up to 20 digits long
   protected $toNumber;
   protected $fromNumber;
   protected $body;
   protected $sendStatus;
 
   public static function initializeNewSMS($body) {
     // NOTE: these values will be updated to correct values when the
     // SMS is sent for the first time. In particular, the ProviderShortName
     // and ProviderSMSID are totally garbage data before a send it attempted.
     return id(new PhabricatorSMS())
       ->setBody($body)
-      ->setSendStatus(PhabricatorSMS::STATUS_UNSENT)
-      ->setProviderShortName(PhabricatorSMS::SHORTNAME_PLACEHOLDER)
+      ->setSendStatus(self::STATUS_UNSENT)
+      ->setProviderShortName(self::SHORTNAME_PLACEHOLDER)
       ->setProviderSMSID(Filesystem::readRandomCharacters(40));
   }
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_COLUMN_SCHEMA => array(
         'providerShortName' => 'text16',
         'providerSMSID' => 'text40',
         'toNumber' => 'text20',
         'fromNumber' => 'text20?',
         'body' => 'text',
         'sendStatus' => 'text16?',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_provider' => array(
           'columns' => array('providerSMSID', 'providerShortName'),
           'unique' => true,
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function getTableName() {
     // Slightly non-standard, but otherwise this class needs "MetaMTA" in its
     // name. :/
     return 'sms';
   }
 
   public function hasBeenSentAtLeastOnce() {
     return ($this->getProviderShortName() !=
-      PhabricatorSMS::SHORTNAME_PLACEHOLDER);
+      self::SHORTNAME_PLACEHOLDER);
   }
 }
diff --git a/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php b/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php
index 0eefe68069..390d591a07 100644
--- a/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php
+++ b/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php
@@ -1,181 +1,181 @@
 <?php
 
 abstract class PhabricatorSQLPatchList {
 
   public abstract function getNamespace();
   public abstract function getPatches();
 
   /**
    * Examine a directory for `.php` and `.sql` files and build patch
    * specifications for them.
    */
   protected function buildPatchesFromDirectory($directory) {
     $patch_list = Filesystem::listDirectory(
       $directory,
       $include_hidden = false);
 
     sort($patch_list);
     $patches = array();
 
     foreach ($patch_list as $patch) {
       $matches = null;
       if (!preg_match('/\.(sql|php)$/', $patch, $matches)) {
         throw new Exception(
           pht(
             'Unknown patch "%s" in "%s", expected ".php" or ".sql" suffix.',
             $patch,
             $directory));
       }
 
       $patches[$patch] = array(
         'type' => $matches[1],
         'name' => rtrim($directory, '/').'/'.$patch,
       );
     }
 
     return $patches;
   }
 
   final public static function buildAllPatches() {
     $patch_lists = id(new PhutilSymbolLoader())
-      ->setAncestorClass('PhabricatorSQLPatchList')
+      ->setAncestorClass(__CLASS__)
       ->setConcreteOnly(true)
       ->selectAndLoadSymbols();
 
     $specs = array();
     $seen_namespaces = array();
 
     foreach ($patch_lists as $patch_class) {
       $patch_class = $patch_class['name'];
       $patch_list = newv($patch_class, array());
 
       $namespace = $patch_list->getNamespace();
       if (isset($seen_namespaces[$namespace])) {
         $prior = $seen_namespaces[$namespace];
         throw new Exception(
           "PatchList '{$patch_class}' has the same namespace, '{$namespace}', ".
           "as another patch list class, '{$prior}'. Each patch list MUST have ".
           "a unique namespace.");
       }
 
       $last_key = null;
       foreach ($patch_list->getPatches() as $key => $patch) {
         if (!is_array($patch)) {
           throw new Exception(
             "PatchList '{$patch_class}' has a patch '{$key}' which is not ".
             "an array.");
         }
 
         $valid = array(
           'type'    => true,
           'name'    => true,
           'after'   => true,
           'legacy'  => true,
           'dead'    => true,
         );
 
         foreach ($patch as $pkey => $pval) {
           if (empty($valid[$pkey])) {
             throw new Exception(
               "PatchList '{$patch_class}' has a patch, '{$key}', with an ".
               "unknown property, '{$pkey}'. Patches must have only valid ".
               "keys: ".implode(', ', array_keys($valid)).'.');
           }
         }
 
         if (is_numeric($key)) {
           throw new Exception(
             "PatchList '{$patch_class}' has a patch with a numeric key, ".
             "'{$key}'. Patches must use string keys.");
         }
 
         if (strpos($key, ':') !== false) {
           throw new Exception(
             "PatchList '{$patch_class}' has a patch with a colon in the ".
             "key name, '{$key}'. Patch keys may not contain colons.");
         }
 
         $full_key = "{$namespace}:{$key}";
 
         if (isset($specs[$full_key])) {
           throw new Exception(
             "PatchList '{$patch_class}' has a patch '{$key}' which ".
             "duplicates an existing patch key.");
         }
 
         $patch['key']     = $key;
         $patch['fullKey'] = $full_key;
         $patch['dead']    = (bool)idx($patch, 'dead', false);
 
         if (isset($patch['legacy'])) {
           if ($namespace != 'phabricator') {
             throw new Exception(
               "Only patches in the 'phabricator' namespace may contain ".
               "'legacy' keys.");
           }
         } else {
           $patch['legacy'] = false;
         }
 
         if (!array_key_exists('after', $patch)) {
           if ($last_key === null) {
             throw new Exception(
               "Patch '{$full_key}' is missing key 'after', and is the first ".
               "patch in the patch list '{$patch_class}', so its application ".
               "order can not be determined implicitly. The first patch in a ".
               "patch list must list the patch or patches it depends on ".
               "explicitly.");
           } else {
             $patch['after'] = array($last_key);
           }
         }
         $last_key = $full_key;
 
         foreach ($patch['after'] as $after_key => $after) {
           if (strpos($after, ':') === false) {
             $patch['after'][$after_key] = $namespace.':'.$after;
           }
         }
 
         $type = idx($patch, 'type');
         if (!$type) {
           throw new Exception(
             "Patch '{$namespace}:{$key}' is missing key 'type'. Every patch ".
             "must have a type.");
         }
 
         switch ($type) {
           case 'db':
           case 'sql':
           case 'php':
             break;
           default:
             throw new Exception(
               "Patch '{$namespace}:{$key}' has unknown patch type '{$type}'.");
         }
 
         $specs[$full_key] = $patch;
       }
     }
 
     foreach ($specs as $key => $patch) {
       foreach ($patch['after'] as $after) {
         if (empty($specs[$after])) {
           throw new Exception(
             "Patch '{$key}' references nonexistent dependency, '{$after}'. ".
             "Patches may only depend on patches which actually exist.");
         }
       }
     }
 
     $patches = array();
     foreach ($specs as $full_key => $spec) {
       $patches[$full_key] = new PhabricatorStoragePatch($spec);
     }
 
     // TODO: Detect cycles?
 
     return $patches;
   }
 
 }
diff --git a/src/infrastructure/time/PhabricatorTime.php b/src/infrastructure/time/PhabricatorTime.php
index d2866d99c2..03495d8412 100644
--- a/src/infrastructure/time/PhabricatorTime.php
+++ b/src/infrastructure/time/PhabricatorTime.php
@@ -1,72 +1,75 @@
 <?php
 
 final class PhabricatorTime {
 
   private static $stack = array();
   private static $originalZone;
 
   public static function pushTime($epoch, $timezone) {
     if (empty(self::$stack)) {
       self::$originalZone = date_default_timezone_get();
     }
 
     $ok = date_default_timezone_set($timezone);
     if (!$ok) {
       throw new Exception("Invalid timezone '{$timezone}'!");
     }
 
     self::$stack[] = array(
       'epoch'       => $epoch,
       'timezone'    => $timezone,
     );
 
     return new PhabricatorTimeGuard(last_key(self::$stack));
   }
 
   public static function popTime($key) {
     if ($key !== last_key(self::$stack)) {
-      throw new Exception('PhabricatorTime::popTime with bad key.');
+      throw new Exception(
+        pht(
+          '%s with bad key.',
+          __METHOD__));
     }
     array_pop(self::$stack);
 
     if (empty(self::$stack)) {
       date_default_timezone_set(self::$originalZone);
     } else {
       $frame = end(self::$stack);
       date_default_timezone_set($frame['timezone']);
     }
   }
 
   public static function getNow() {
     if (self::$stack) {
       $frame = end(self::$stack);
       return $frame['epoch'];
     }
     return time();
   }
 
   public static function parseLocalTime($time, PhabricatorUser $user) {
     $old_zone = date_default_timezone_get();
 
     date_default_timezone_set($user->getTimezoneIdentifier());
-      $timestamp = (int)strtotime($time, PhabricatorTime::getNow());
+      $timestamp = (int)strtotime($time, self::getNow());
       if ($timestamp <= 0) {
         $timestamp = null;
       }
     date_default_timezone_set($old_zone);
 
     return $timestamp;
   }
 
   public static function getTodayMidnightDateTime($viewer) {
     $timezone = new DateTimeZone($viewer->getTimezoneIdentifier());
     $today = new DateTime('@'.time());
     $today->setTimeZone($timezone);
     $year = $today->format('Y');
     $month = $today->format('m');
     $day = $today->format('d');
     $today = new DateTime("{$year}-{$month}-{$day}", $timezone);
     return $today;
   }
 
 }
diff --git a/src/infrastructure/util/PhabricatorHash.php b/src/infrastructure/util/PhabricatorHash.php
index dd158d1168..df1fbfa08b 100644
--- a/src/infrastructure/util/PhabricatorHash.php
+++ b/src/infrastructure/util/PhabricatorHash.php
@@ -1,124 +1,124 @@
 <?php
 
 final class PhabricatorHash extends Phobject {
 
   const INDEX_DIGEST_LENGTH = 12;
 
   /**
    * Digest a string for general use, including use which relates to security.
    *
    * @param   string  Input string.
    * @return  string  32-byte hexidecimal SHA1+HMAC hash.
    */
   public static function digest($string, $key = null) {
     if ($key === null) {
       $key = PhabricatorEnv::getEnvConfig('security.hmac-key');
     }
 
     if (!$key) {
       throw new Exception(
         "Set a 'security.hmac-key' in your Phabricator configuration!");
     }
 
     return hash_hmac('sha1', $string, $key);
   }
 
 
   /**
    * Digest a string into a password hash. This is similar to @{method:digest},
    * but requires a salt and iterates the hash to increase cost.
    */
   public static function digestPassword(PhutilOpaqueEnvelope $envelope, $salt) {
     $result = $envelope->openEnvelope();
     if (!$result) {
       throw new Exception('Trying to digest empty password!');
     }
 
     for ($ii = 0; $ii < 1000; $ii++) {
-      $result = PhabricatorHash::digest($result, $salt);
+      $result = self::digest($result, $salt);
     }
 
     return $result;
   }
 
 
   /**
    * Digest a string for use in, e.g., a MySQL index. This produces a short
    * (12-byte), case-sensitive alphanumeric string with 72 bits of entropy,
    * which is generally safe in most contexts (notably, URLs).
    *
    * This method emphasizes compactness, and should not be used for security
    * related hashing (for general purpose hashing, see @{method:digest}).
    *
    * @param   string  Input string.
    * @return  string  12-byte, case-sensitive alphanumeric hash of the string
    *                  which
    */
   public static function digestForIndex($string) {
     $hash = sha1($string, $raw_output = true);
 
     static $map;
     if ($map === null) {
       $map = '0123456789'.
              'abcdefghij'.
              'klmnopqrst'.
              'uvwxyzABCD'.
              'EFGHIJKLMN'.
              'OPQRSTUVWX'.
              'YZ._';
     }
 
     $result = '';
     for ($ii = 0; $ii < self::INDEX_DIGEST_LENGTH; $ii++) {
       $result .= $map[(ord($hash[$ii]) & 0x3F)];
     }
 
     return $result;
   }
 
 
   /**
    * Shorten a string to a maximum byte length in a collision-resistant way
    * while retaining some degree of human-readability.
    *
    * This function converts an input string into a prefix plus a hash. For
    * example, a very long string beginning with "crabapplepie..." might be
    * digested to something like "crabapp-N1wM1Nz3U84k".
    *
    * This allows the maximum length of identifiers to be fixed while
    * maintaining a high degree of collision resistance and a moderate degree
    * of human readability.
    *
    * @param string The string to shorten.
    * @param int Maximum length of the result.
    * @return string String shortened in a collision-resistant way.
    */
   public static function digestToLength($string, $length) {
     // We need at least two more characters than the hash length to fit in a
     // a 1-character prefix and a separator.
     $min_length = self::INDEX_DIGEST_LENGTH + 2;
     if ($length < $min_length) {
       throw new Exception(
         pht(
           'Length parameter in digestToLength() must be at least %s, '.
           'but %s was provided.',
           new PhutilNumber($min_length),
           new PhutilNumber($length)));
     }
 
     // We could conceivably return the string unmodified if it's shorter than
     // the specified length. Instead, always hash it. This makes the output of
     // the method more recognizable and consistent (no surprising new behavior
     // once you hit a string longer than `$length`) and prevents an attacker
     // who can control the inputs from intentionally using the hashed form
     // of a string to cause a collision.
 
-    $hash = PhabricatorHash::digestForIndex($string);
+    $hash = self::digestForIndex($string);
 
     $prefix = substr($string, 0, ($length - ($min_length - 1)));
 
     return $prefix.'-'.$hash;
   }
 
 
 }
diff --git a/src/infrastructure/util/password/PhabricatorPasswordHasher.php b/src/infrastructure/util/password/PhabricatorPasswordHasher.php
index 4409163509..3b136b4ec3 100644
--- a/src/infrastructure/util/password/PhabricatorPasswordHasher.php
+++ b/src/infrastructure/util/password/PhabricatorPasswordHasher.php
@@ -1,431 +1,431 @@
 <?php
 
 /**
  * Provides a mechanism for hashing passwords, like "iterated md5", "bcrypt",
  * "scrypt", etc.
  *
  * Hashers define suitability and strength, and the system automatically
  * chooses the strongest available hasher and can prompt users to upgrade as
  * soon as a stronger hasher is available.
  *
  * @task hasher   Implementing a Hasher
  * @task hashing  Using Hashers
  */
 abstract class PhabricatorPasswordHasher extends Phobject {
 
   const MAXIMUM_STORAGE_SIZE = 128;
 
 
 /* -(  Implementing a Hasher  )---------------------------------------------- */
 
 
   /**
    * Return a human-readable description of this hasher, like "Iterated MD5".
    *
    * @return string Human readable hash name.
    * @task hasher
    */
   abstract public function getHumanReadableName();
 
 
   /**
    * Return a short, unique, key identifying this hasher, like "md5" or
    * "bcrypt". This identifier should not be translated.
    *
    * @return string Short, unique hash name.
    * @task hasher
    */
   abstract public function getHashName();
 
 
   /**
    * Return the maximum byte length of hashes produced by this hasher. This is
    * used to prevent storage overflows.
    *
    * @return int  Maximum number of bytes in hashes this class produces.
    * @task hasher
    */
   abstract public function getHashLength();
 
 
   /**
    * Return `true` to indicate that any required extensions or dependencies
    * are available, and this hasher is able to perform hashing.
    *
    * @return bool True if this hasher can execute.
    * @task hasher
    */
   abstract public function canHashPasswords();
 
 
   /**
    * Return a human-readable string describing why this hasher is unable
    * to operate. For example, "To use bcrypt, upgrade to PHP 5.5.0 or newer.".
    *
    * @return string Human-readable description of how to enable this hasher.
    * @task hasher
    */
   abstract public function getInstallInstructions();
 
 
   /**
    * Return an indicator of this hasher's strength. When choosing to hash
    * new passwords, the strongest available hasher which is usuable for new
    * passwords will be used, and the presence of a stronger hasher will
    * prompt users to update their hashes.
    *
    * Generally, this method should return a larger number than hashers it is
    * preferable to, but a smaller number than hashers which are better than it
    * is. This number does not need to correspond directly with the actual hash
    * strength.
    *
    * @return float  Strength of this hasher.
    * @task hasher
    */
   abstract public function getStrength();
 
 
   /**
    * Return a short human-readable indicator of this hasher's strength, like
    * "Weak", "Okay", or "Good".
    *
    * This is only used to help administrators make decisions about
    * configuration.
    *
    * @return string Short human-readable description of hash strength.
    * @task hasher
    */
   abstract public function getHumanReadableStrength();
 
 
   /**
    * Produce a password hash.
    *
    * @param   PhutilOpaqueEnvelope  Text to be hashed.
    * @return  PhutilOpaqueEnvelope  Hashed text.
    * @task hasher
    */
   abstract protected function getPasswordHash(PhutilOpaqueEnvelope $envelope);
 
 
   /**
    * Verify that a password matches a hash.
    *
    * The default implementation checks for equality; if a hasher embeds salt in
    * hashes it should override this method and perform a salt-aware comparison.
    *
    * @param   PhutilOpaqueEnvelope  Password to compare.
    * @param   PhutilOpaqueEnvelope  Bare password hash.
    * @return  bool                  True if the passwords match.
    * @task hasher
    */
   protected function verifyPassword(
     PhutilOpaqueEnvelope $password,
     PhutilOpaqueEnvelope $hash) {
 
     $actual_hash = $this->getPasswordHash($password)->openEnvelope();
     $expect_hash = $hash->openEnvelope();
 
     return ($actual_hash === $expect_hash);
   }
 
 
   /**
    * Check if an existing hash created by this algorithm is upgradeable.
    *
    * The default implementation returns `false`. However, hash algorithms which
    * have (for example) an internal cost function may be able to upgrade an
    * existing hash to a stronger one with a higher cost.
    *
    * @param PhutilOpaqueEnvelope  Bare hash.
    * @return bool                 True if the hash can be upgraded without
    *                              changing the algorithm (for example, to a
    *                              higher cost).
    * @task hasher
    */
   protected function canUpgradeInternalHash(PhutilOpaqueEnvelope $hash) {
     return false;
   }
 
 
 /* -(  Using Hashers  )------------------------------------------------------ */
 
 
   /**
    * Get the hash of a password for storage.
    *
    * @param   PhutilOpaqueEnvelope  Password text.
    * @return  PhutilOpaqueEnvelope  Hashed text.
    * @task hashing
    */
   final public function getPasswordHashForStorage(
     PhutilOpaqueEnvelope $envelope) {
 
     $name = $this->getHashName();
     $hash = $this->getPasswordHash($envelope);
 
     $actual_len = strlen($hash->openEnvelope());
     $expect_len = $this->getHashLength();
     if ($actual_len > $expect_len) {
       throw new Exception(
         pht(
           "Password hash '%s' produced a hash of length %d, but a ".
           "maximum length of %d was expected.",
           $name,
           new PhutilNumber($actual_len),
           new PhutilNumber($expect_len)));
     }
 
     return new PhutilOpaqueEnvelope($name.':'.$hash->openEnvelope());
   }
 
 
   /**
    * Parse a storage hash into its components, like the hash type and hash
    * data.
    *
    * @return map  Dictionary of information about the hash.
    * @task hashing
    */
   private static function parseHashFromStorage(PhutilOpaqueEnvelope $hash) {
     $raw_hash = $hash->openEnvelope();
     if (strpos($raw_hash, ':') === false) {
       throw new Exception(
         pht(
           'Malformed password hash, expected "name:hash".'));
     }
 
     list($name, $hash) = explode(':', $raw_hash);
 
     return array(
       'name' => $name,
       'hash' => new PhutilOpaqueEnvelope($hash),
     );
   }
 
 
   /**
    * Get all available password hashers. This may include hashers which can not
    * actually be used (for example, a required extension is missing).
    *
    * @return list<PhabicatorPasswordHasher> Hasher objects.
    * @task hashing
    */
   public static function getAllHashers() {
     $objects = id(new PhutilSymbolLoader())
-      ->setAncestorClass('PhabricatorPasswordHasher')
+      ->setAncestorClass(__CLASS__)
       ->loadObjects();
 
     $map = array();
     foreach ($objects as $object) {
       $name = $object->getHashName();
 
       $potential_length = strlen($name) + $object->getHashLength() + 1;
       $maximum_length = self::MAXIMUM_STORAGE_SIZE;
 
       if ($potential_length > $maximum_length) {
         throw new Exception(
           pht(
             'Hasher "%s" may produce hashes which are too long to fit in '.
             'storage. %d characters are available, but its hashes may be '.
             'up to %d characters in length.',
             $name,
             $maximum_length,
             $potential_length));
       }
 
       if (isset($map[$name])) {
         throw new Exception(
           pht(
             'Two hashers use the same hash name ("%s"), "%s" and "%s". Each '.
             'hasher must have a unique name.',
             $name,
             get_class($object),
             get_class($map[$name])));
       }
       $map[$name] = $object;
     }
 
     return $map;
   }
 
 
   /**
    * Get all usable password hashers. This may include hashers which are
    * not desirable or advisable.
    *
    * @return list<PhabicatorPasswordHasher> Hasher objects.
    * @task hashing
    */
   public static function getAllUsableHashers() {
     $hashers = self::getAllHashers();
     foreach ($hashers as $key => $hasher) {
       if (!$hasher->canHashPasswords()) {
         unset($hashers[$key]);
       }
     }
     return $hashers;
   }
 
 
   /**
    * Get the best (strongest) available hasher.
    *
    * @return PhabicatorPasswordHasher Best hasher.
    * @task hashing
    */
   public static function getBestHasher() {
     $hashers = self::getAllUsableHashers();
     $hashers = msort($hashers, 'getStrength');
 
     $hasher = last($hashers);
     if (!$hasher) {
       throw new PhabricatorPasswordHasherUnavailableException(
         pht(
           'There are no password hashers available which are usable for '.
           'new passwords.'));
     }
 
     return $hasher;
   }
 
 
   /**
    * Get the hashser for a given stored hash.
    *
    * @return PhabicatorPasswordHasher Corresponding hasher.
    * @task hashing
    */
   public static function getHasherForHash(PhutilOpaqueEnvelope $hash) {
     $info = self::parseHashFromStorage($hash);
     $name = $info['name'];
 
     $usable = self::getAllUsableHashers();
     if (isset($usable[$name])) {
       return $usable[$name];
     }
 
     $all = self::getAllHashers();
     if (isset($all[$name])) {
       throw new PhabricatorPasswordHasherUnavailableException(
         pht(
           'Attempting to compare a password saved with the "%s" hash. The '.
           'hasher exists, but is not currently usable. %s',
           $name,
           $all[$name]->getInstallInstructions()));
     }
 
     throw new PhabricatorPasswordHasherUnavailableException(
       pht(
         'Attempting to compare a password saved with the "%s" hash. No such '.
         'hasher is known to Phabricator.',
         $name));
   }
 
 
   /**
    * Test if a password is using an weaker hash than the strongest available
    * hash. This can be used to prompt users to upgrade, or automatically upgrade
    * on login.
    *
    * @return bool True to indicate that rehashing this password will improve
    *              the hash strength.
    * @task hashing
    */
   public static function canUpgradeHash(PhutilOpaqueEnvelope $hash) {
     if (!strlen($hash->openEnvelope())) {
       throw new Exception(
         pht('Expected a password hash, received nothing!'));
     }
 
     $current_hasher = self::getHasherForHash($hash);
     $best_hasher = self::getBestHasher();
 
     if ($current_hasher->getHashName() != $best_hasher->getHashName()) {
       // If the algorithm isn't the best one, we can upgrade.
       return true;
     }
 
     $info = self::parseHashFromStorage($hash);
     if ($current_hasher->canUpgradeInternalHash($info['hash'])) {
       // If the algorithm provides an internal upgrade, we can also upgrade.
       return true;
     }
 
     // Already on the best algorithm with the best settings.
     return false;
   }
 
 
   /**
    * Generate a new hash for a password, using the best available hasher.
    *
    * @param   PhutilOpaqueEnvelope  Password to hash.
    * @return  PhutilOpaqueEnvelope  Hashed password, using best available
    *                                hasher.
    * @task hashing
    */
   public static function generateNewPasswordHash(
     PhutilOpaqueEnvelope $password) {
     $hasher = self::getBestHasher();
     return $hasher->getPasswordHashForStorage($password);
   }
 
 
   /**
    * Compare a password to a stored hash.
    *
    * @param   PhutilOpaqueEnvelope  Password to compare.
    * @param   PhutilOpaqueEnvelope  Stored password hash.
    * @return  bool                  True if the passwords match.
    * @task hashing
    */
   public static function comparePassword(
     PhutilOpaqueEnvelope $password,
     PhutilOpaqueEnvelope $hash) {
 
     $hasher = self::getHasherForHash($hash);
     $parts = self::parseHashFromStorage($hash);
 
     return $hasher->verifyPassword($password, $parts['hash']);
   }
 
 
   /**
    * Get the human-readable algorithm name for a given hash.
    *
    * @param   PhutilOpaqueEnvelope  Storage hash.
    * @return  string                Human-readable algorithm name.
    */
   public static function getCurrentAlgorithmName(PhutilOpaqueEnvelope $hash) {
     $raw_hash = $hash->openEnvelope();
     if (!strlen($raw_hash)) {
       return pht('None');
     }
 
     try {
-      $current_hasher = PhabricatorPasswordHasher::getHasherForHash($hash);
+      $current_hasher = self::getHasherForHash($hash);
       return $current_hasher->getHumanReadableName();
     } catch (Exception $ex) {
       $info = self::parseHashFromStorage($hash);
       $name = $info['name'];
       return pht('Unknown ("%s")', $name);
     }
   }
 
 
   /**
    * Get the human-readable algorithm name for the best available hash.
    *
    * @return  string                Human-readable name for best hash.
    */
   public static function getBestAlgorithmName() {
     try {
-      $best_hasher = PhabricatorPasswordHasher::getBestHasher();
+      $best_hasher = self::getBestHasher();
       return $best_hasher->getHumanReadableName();
     } catch (Exception $ex) {
       return pht('Unknown');
     }
   }
 
 }
diff --git a/src/view/AphrontDialogView.php b/src/view/AphrontDialogView.php
index ac946969d5..408a92d94d 100644
--- a/src/view/AphrontDialogView.php
+++ b/src/view/AphrontDialogView.php
@@ -1,355 +1,358 @@
 <?php
 
 final class AphrontDialogView extends AphrontView {
 
   private $title;
   private $shortTitle;
   private $submitButton;
   private $cancelURI;
   private $cancelText = 'Cancel';
   private $submitURI;
   private $hidden = array();
   private $class;
   private $renderAsForm = true;
   private $formID;
   private $headerColor = PHUIActionHeaderView::HEADER_WHITE;
   private $footers = array();
   private $isStandalone;
   private $method = 'POST';
   private $disableWorkflowOnSubmit;
   private $disableWorkflowOnCancel;
   private $width      = 'default';
   private $errors = array();
   private $flush;
   private $validationException;
 
 
   const WIDTH_DEFAULT = 'default';
   const WIDTH_FORM    = 'form';
   const WIDTH_FULL    = 'full';
 
   public function setMethod($method) {
     $this->method = $method;
     return $this;
   }
 
   public function setIsStandalone($is_standalone) {
     $this->isStandalone = $is_standalone;
     return $this;
   }
 
   public function setErrors(array $errors) {
     $this->errors = $errors;
     return $this;
   }
 
   public function getIsStandalone() {
     return $this->isStandalone;
   }
 
   public function setSubmitURI($uri) {
     $this->submitURI = $uri;
     return $this;
   }
 
   public function setTitle($title) {
     $this->title = $title;
     return $this;
   }
 
   public function getTitle() {
     return $this->title;
   }
 
   public function setShortTitle($short_title) {
     $this->shortTitle = $short_title;
     return $this;
   }
 
   public function getShortTitle() {
     return $this->shortTitle;
   }
 
   public function addSubmitButton($text = null) {
     if (!$text) {
       $text = pht('Okay');
     }
 
     $this->submitButton = $text;
     return $this;
   }
 
   public function addCancelButton($uri, $text = null) {
     if (!$text) {
       $text = pht('Cancel');
     }
 
     $this->cancelURI = $uri;
     $this->cancelText = $text;
     return $this;
   }
 
   public function addFooter($footer) {
     $this->footers[] = $footer;
     return $this;
   }
 
   public function addHiddenInput($key, $value) {
     if (is_array($value)) {
       foreach ($value as $hidden_key => $hidden_value) {
         $this->hidden[] = array($key.'['.$hidden_key.']', $hidden_value);
       }
     } else {
       $this->hidden[] = array($key, $value);
     }
     return $this;
   }
 
   public function setClass($class) {
     $this->class = $class;
     return $this;
   }
 
   public function setFlush($flush) {
     $this->flush = $flush;
     return $this;
   }
 
   public function setRenderDialogAsDiv() {
     // TODO: This API is awkward.
     $this->renderAsForm = false;
     return $this;
   }
 
   public function setFormID($id) {
     $this->formID = $id;
     return $this;
   }
 
   public function setWidth($width) {
     $this->width = $width;
     return $this;
   }
 
   public function setHeaderColor($color) {
     $this->headerColor = $color;
     return $this;
   }
 
   public function appendParagraph($paragraph) {
     return $this->appendChild(
       phutil_tag(
         'p',
         array(
           'class' => 'aphront-dialog-view-paragraph',
         ),
         $paragraph));
   }
 
   public function appendForm(AphrontFormView $form) {
     return $this->appendChild($form->buildLayoutView());
   }
 
   public function setDisableWorkflowOnSubmit($disable_workflow_on_submit) {
     $this->disableWorkflowOnSubmit = $disable_workflow_on_submit;
     return $this;
   }
 
   public function getDisableWorkflowOnSubmit() {
     return $this->disableWorkflowOnSubmit;
   }
 
   public function setDisableWorkflowOnCancel($disable_workflow_on_cancel) {
     $this->disableWorkflowOnCancel = $disable_workflow_on_cancel;
     return $this;
   }
 
   public function getDisableWorkflowOnCancel() {
     return $this->disableWorkflowOnCancel;
   }
 
   public function setValidationException(
     PhabricatorApplicationTransactionValidationException $ex = null) {
     $this->validationException = $ex;
     return $this;
   }
 
   final public function render() {
     require_celerity_resource('aphront-dialog-view-css');
 
     $buttons = array();
     if ($this->submitButton) {
       $meta = array();
       if ($this->disableWorkflowOnSubmit) {
         $meta['disableWorkflow'] = true;
       }
 
       $buttons[] = javelin_tag(
         'button',
         array(
           'name' => '__submit__',
           'sigil' => '__default__',
           'type' => 'submit',
           'meta' => $meta,
         ),
         $this->submitButton);
     }
 
     if ($this->cancelURI) {
       $meta = array();
       if ($this->disableWorkflowOnCancel) {
         $meta['disableWorkflow'] = true;
       }
 
       $buttons[] = javelin_tag(
         'a',
         array(
           'href'  => $this->cancelURI,
           'class' => 'button grey',
           'name'  => '__cancel__',
           'sigil' => 'jx-workflow-button',
           'meta' => $meta,
         ),
         $this->cancelText);
     }
 
     if (!$this->user) {
       throw new Exception(
-        pht('You must call setUser() when rendering an AphrontDialogView.'));
+        pht(
+          'You must call %s when rendering an %s.',
+          'setUser()',
+          __CLASS__));
     }
 
     $more = $this->class;
     if ($this->flush) {
       $more .= ' aphront-dialog-flush';
     }
 
     switch ($this->width) {
       case self::WIDTH_FORM:
       case self::WIDTH_FULL:
         $more .= ' aphront-dialog-view-width-'.$this->width;
         break;
       case self::WIDTH_DEFAULT:
         break;
       default:
         throw new Exception("Unknown dialog width '{$this->width}'!");
     }
 
     if ($this->isStandalone) {
       $more .= ' aphront-dialog-view-standalone';
     }
 
     $attributes = array(
       'class'   => 'aphront-dialog-view '.$more,
       'sigil'   => 'jx-dialog',
     );
 
     $form_attributes = array(
       'action'  => $this->submitURI,
       'method'  => $this->method,
       'id'      => $this->formID,
     );
 
     $hidden_inputs = array();
     $hidden_inputs[] = phutil_tag(
       'input',
       array(
         'type' => 'hidden',
         'name' => '__dialog__',
         'value' => '1',
       ));
 
     foreach ($this->hidden as $desc) {
       list($key, $value) = $desc;
       $hidden_inputs[] = javelin_tag(
         'input',
         array(
           'type' => 'hidden',
           'name' => $key,
           'value' => $value,
           'sigil' => 'aphront-dialog-application-input',
         ));
     }
 
     if (!$this->renderAsForm) {
       $buttons = array(phabricator_form(
         $this->user,
         $form_attributes,
         array_merge($hidden_inputs, $buttons)),
       );
     }
 
     $children = $this->renderChildren();
 
     $errors = $this->errors;
 
     $ex = $this->validationException;
     $exception_errors = null;
     if ($ex) {
       foreach ($ex->getErrors() as $error) {
         $errors[] = $error->getMessage();
       }
     }
 
     if ($errors) {
       $children = array(
         id(new PHUIInfoView())->setErrors($errors),
         $children,
       );
     }
 
     $header = new PHUIActionHeaderView();
     $header->setHeaderTitle($this->title);
     $header->setHeaderColor($this->headerColor);
 
     $footer = null;
     if ($this->footers) {
       $footer = phutil_tag(
         'div',
         array(
           'class' => 'aphront-dialog-foot',
         ),
         $this->footers);
     }
 
     $tail = null;
     if ($buttons || $footer) {
       $tail = phutil_tag(
         'div',
         array(
           'class' => 'aphront-dialog-tail grouped',
         ),
         array(
           $buttons,
           $footer,
         ));
     }
 
     $content = array(
       phutil_tag(
         'div',
         array(
           'class' => 'aphront-dialog-head',
         ),
         $header),
       phutil_tag('div',
         array(
           'class' => 'aphront-dialog-body grouped',
         ),
         $children),
       $tail,
     );
 
     if ($this->renderAsForm) {
       return phabricator_form(
         $this->user,
         $form_attributes + $attributes,
         array($hidden_inputs, $content));
     } else {
       return javelin_tag(
         'div',
         $attributes,
         $content);
     }
   }
 
 }
diff --git a/src/view/control/AphrontCursorPagerView.php b/src/view/control/AphrontCursorPagerView.php
index a683eb3d6d..81ab33df81 100644
--- a/src/view/control/AphrontCursorPagerView.php
+++ b/src/view/control/AphrontCursorPagerView.php
@@ -1,182 +1,178 @@
 <?php
 
 final class AphrontCursorPagerView extends AphrontView {
 
   private $afterID;
   private $beforeID;
 
   private $pageSize = 100;
 
   private $nextPageID;
   private $prevPageID;
   private $moreResults;
 
   private $uri;
 
   final public function setPageSize($page_size) {
     $this->pageSize = max(1, $page_size);
     return $this;
   }
 
   final public function getPageSize() {
     return $this->pageSize;
   }
 
   final public function setURI(PhutilURI $uri) {
     $this->uri = $uri;
     return $this;
   }
 
   final public function readFromRequest(AphrontRequest $request) {
     $this->uri = $request->getRequestURI();
     $this->afterID = $request->getStr('after');
     $this->beforeID = $request->getStr('before');
     return $this;
   }
 
   final public function setAfterID($after_id) {
     $this->afterID = $after_id;
     return $this;
   }
 
   final public function getAfterID() {
     return $this->afterID;
   }
 
   final public function setBeforeID($before_id) {
     $this->beforeID = $before_id;
     return $this;
   }
 
   final public function getBeforeID() {
     return $this->beforeID;
   }
 
   final public function setNextPageID($next_page_id) {
     $this->nextPageID = $next_page_id;
     return $this;
   }
 
   final public function getNextPageID() {
     return $this->nextPageID;
   }
 
   final public function setPrevPageID($prev_page_id) {
     $this->prevPageID = $prev_page_id;
     return $this;
   }
 
   final public function getPrevPageID() {
     return $this->prevPageID;
   }
 
   final public function sliceResults(array $results) {
     if (count($results) > $this->getPageSize()) {
       $offset = ($this->beforeID ? count($results) - $this->getPageSize() : 0);
       $results = array_slice($results, $offset, $this->getPageSize(), true);
       $this->moreResults = true;
     }
     return $results;
   }
 
   final public function getHasMoreResults() {
     return $this->moreResults;
   }
 
   public function willShowPagingControls() {
     return $this->prevPageID ||
            $this->nextPageID ||
            $this->afterID ||
            ($this->beforeID && $this->moreResults);
   }
 
   public function getFirstPageURI() {
     if (!$this->uri) {
-      throw new Exception(
-        pht('You must call setURI() before you can call getFirstPageURI().'));
+      throw new PhutilInvalidStateException('setURI');
     }
 
     if (!$this->afterID && !($this->beforeID && $this->moreResults)) {
       return null;
     }
 
     return $this->uri
       ->alter('before', null)
       ->alter('after', null);
   }
 
   public function getPrevPageURI() {
     if (!$this->uri) {
-      throw new Exception(
-        pht('You must call setURI() before you can call getPrevPageURI().'));
+      throw new PhutilInvalidStateException('getPrevPageURI');
     }
 
     if (!$this->prevPageID) {
       return null;
     }
 
     return $this->uri
       ->alter('after', null)
       ->alter('before', $this->prevPageID);
   }
 
   public function getNextPageURI() {
     if (!$this->uri) {
-      throw new Exception(
-        pht('You must call setURI() before you can call getNextPageURI().'));
+      throw new PhutilInvalidStateException('setURI');
     }
 
     if (!$this->nextPageID) {
       return null;
     }
 
     return $this->uri
       ->alter('after', $this->nextPageID)
       ->alter('before', null);
   }
 
   public function render() {
     if (!$this->uri) {
-      throw new Exception(
-        pht('You must call setURI() before you can call render().'));
+      throw new PhutilInvalidStateException('setURI');
     }
 
     $links = array();
 
     $first_uri = $this->getFirstPageURI();
     if ($first_uri) {
       $links[] = phutil_tag(
         'a',
         array(
           'href' => $first_uri,
         ),
         "\xC2\xAB ".pht('First'));
     }
 
     $prev_uri = $this->getPrevPageURI();
     if ($prev_uri) {
       $links[] = phutil_tag(
         'a',
         array(
           'href' => $prev_uri,
         ),
         "\xE2\x80\xB9 ".pht('Prev'));
     }
 
     $next_uri = $this->getNextPageURI();
     if ($next_uri) {
       $links[] = phutil_tag(
         'a',
         array(
           'href' => $next_uri,
         ),
         pht('Next')." \xE2\x80\xBA");
     }
 
     return phutil_tag(
       'div',
       array('class' => 'aphront-pager-view'),
       $links);
   }
 
 }
diff --git a/src/view/control/AphrontPagerView.php b/src/view/control/AphrontPagerView.php
index ba6a98736c..8b08b15c5a 100644
--- a/src/view/control/AphrontPagerView.php
+++ b/src/view/control/AphrontPagerView.php
@@ -1,229 +1,228 @@
 <?php
 
 final class AphrontPagerView extends AphrontView {
 
   private $offset;
   private $pageSize = 100;
 
   private $count;
   private $hasMorePages;
 
   private $uri;
   private $pagingParameter;
   private $surroundingPages = 2;
   private $enableKeyboardShortcuts;
 
   final public function setPageSize($page_size) {
     $this->pageSize = max(1, $page_size);
     return $this;
   }
 
   final public function setOffset($offset) {
     $this->offset = max(0, $offset);
     return $this;
   }
 
   final public function getOffset() {
     return $this->offset;
   }
 
   final public function getPageSize() {
     return $this->pageSize;
   }
 
   final public function setCount($count) {
     $this->count = $count;
     return $this;
   }
 
   final public function setHasMorePages($has_more) {
     $this->hasMorePages = $has_more;
     return $this;
   }
 
   final public function setURI(PhutilURI $uri, $paging_parameter) {
     $this->uri = $uri;
     $this->pagingParameter = $paging_parameter;
     return $this;
   }
 
   final public function readFromRequest(AphrontRequest $request) {
     $this->uri = $request->getRequestURI();
     $this->pagingParameter = 'offset';
     $this->offset = $request->getInt($this->pagingParameter);
     return $this;
   }
 
   final public function willShowPagingControls() {
     return $this->hasMorePages;
   }
 
   final public function setSurroundingPages($pages) {
     $this->surroundingPages = max(0, $pages);
     return $this;
   }
 
   private function computeCount() {
     if ($this->count !== null) {
       return $this->count;
     }
     return $this->getOffset()
       + $this->getPageSize()
       + ($this->hasMorePages ? 1 : 0);
   }
 
   private function isExactCountKnown() {
     return $this->count !== null;
   }
 
   /**
    * A common paging strategy is to select one extra record and use that to
    * indicate that there's an additional page (this doesn't give you a
    * complete page count but is often faster than counting the total number
    * of items). This method will take a result array, slice it down to the
    * page size if necessary, and call setHasMorePages() if there are more than
    * one page of results.
    *
    *    $results = queryfx_all(
    *      $conn,
    *      'SELECT ... LIMIT %d, %d',
    *      $pager->getOffset(),
    *      $pager->getPageSize() + 1);
    *    $results = $pager->sliceResults($results);
    *
    * @param   list  Result array.
    * @return  list  One page of results.
    */
   public function sliceResults(array $results) {
     if (count($results) > $this->getPageSize()) {
       $results = array_slice($results, 0, $this->getPageSize(), true);
       $this->setHasMorePages(true);
     }
     return $results;
   }
 
   public function setEnableKeyboardShortcuts($enable) {
     $this->enableKeyboardShortcuts = $enable;
     return $this;
   }
 
   public function render() {
     if (!$this->uri) {
-      throw new Exception(
-        pht('You must call setURI() before you can call render().'));
+      throw new PhutilInvalidStateException('setURI');
     }
 
     require_celerity_resource('aphront-pager-view-css');
 
     $page = (int)floor($this->getOffset() / $this->getPageSize());
     $last = ((int)ceil($this->computeCount() / $this->getPageSize())) - 1;
     $near = $this->surroundingPages;
 
     $min = $page - $near;
     $max = $page + $near;
 
     // Limit the window size to no larger than the number of available pages.
     if ($max - $min > $last) {
       $max = $min + $last;
       if ($max == $min) {
         return phutil_tag('div', array('class' => 'aphront-pager-view'), '');
       }
     }
 
     // Slide the window so it is entirely over displayable pages.
     if ($min < 0) {
       $max += 0 - $min;
       $min += 0 - $min;
     }
 
     if ($max > $last) {
       $min -= $max - $last;
       $max -= $max - $last;
     }
 
 
     // Build up a list of <index, label, css-class> tuples which describe the
     // links we'll display, then render them all at once.
 
     $links = array();
 
     $prev_index = null;
     $next_index = null;
 
     if ($min > 0) {
       $links[] = array(0, pht('First'), null);
     }
 
     if ($page > 0) {
       $links[] = array($page - 1, pht('Prev'), null);
       $prev_index = $page - 1;
     }
 
     for ($ii = $min; $ii <= $max; $ii++) {
       $links[] = array($ii, $ii + 1, ($ii == $page) ? 'current' : null);
     }
 
     if ($page < $last && $last > 0) {
       $links[] = array($page + 1, pht('Next'), null);
       $next_index = $page + 1;
     }
 
     if ($max < ($last - 1)) {
       $links[] = array($last, pht('Last'), null);
     }
 
     $base_uri = $this->uri;
     $parameter = $this->pagingParameter;
 
     if ($this->enableKeyboardShortcuts) {
       $pager_links = array();
       $pager_index = array(
         'prev' => $prev_index,
         'next' => $next_index,
       );
       foreach ($pager_index as $key => $index) {
         if ($index !== null) {
           $display_index = $this->getDisplayIndex($index);
           $pager_links[$key] = (string)$base_uri->alter(
             $parameter,
             $display_index);
         }
       }
       Javelin::initBehavior('phabricator-keyboard-pager', $pager_links);
     }
 
     // Convert tuples into rendered nodes.
     $rendered_links = array();
     foreach ($links as $link) {
       list($index, $label, $class) = $link;
       $display_index = $this->getDisplayIndex($index);
       $link = $base_uri->alter($parameter, $display_index);
       $rendered_links[] = phutil_tag(
         'a',
         array(
           'href' => $link,
           'class' => $class,
         ),
         $label);
     }
 
     return phutil_tag(
       'div',
       array('class' => 'aphront-pager-view'),
       $rendered_links);
   }
 
   private function getDisplayIndex($page_index) {
     $page_size = $this->getPageSize();
     // Use a 1-based sequence for display so that the number in the URI is
     // the same as the page number you're on.
     if ($page_index == 0) {
       // No need for the first page to say page=1.
       $display_index = null;
     } else {
       $display_index = $page_index * $page_size;
     }
     return $display_index;
   }
 
 }
diff --git a/src/view/control/AphrontTableView.php b/src/view/control/AphrontTableView.php
index 9edd851c19..a6814a030e 100644
--- a/src/view/control/AphrontTableView.php
+++ b/src/view/control/AphrontTableView.php
@@ -1,331 +1,336 @@
 <?php
 
 final class AphrontTableView extends AphrontView {
 
   protected $data;
   protected $headers;
   protected $shortHeaders;
   protected $rowClasses = array();
   protected $columnClasses = array();
   protected $cellClasses = array();
   protected $zebraStripes = true;
   protected $noDataString;
   protected $className;
   protected $columnVisibility = array();
   private $deviceVisibility = array();
 
   protected $sortURI;
   protected $sortParam;
   protected $sortSelected;
   protected $sortReverse;
   protected $sortValues;
   private $deviceReadyTable;
 
   public function __construct(array $data) {
     $this->data = $data;
   }
 
   public function setHeaders(array $headers) {
     $this->headers = $headers;
     return $this;
   }
 
   public function setColumnClasses(array $column_classes) {
     $this->columnClasses = $column_classes;
     return $this;
   }
 
   public function setRowClasses(array $row_classes) {
     $this->rowClasses = $row_classes;
     return $this;
   }
 
   public function setCellClasses(array $cell_classes) {
     $this->cellClasses = $cell_classes;
     return $this;
   }
 
   public function setNoDataString($no_data_string) {
     $this->noDataString = $no_data_string;
     return $this;
   }
 
   public function setClassName($class_name) {
     $this->className = $class_name;
     return $this;
   }
 
   public function setZebraStripes($zebra_stripes) {
     $this->zebraStripes = $zebra_stripes;
     return $this;
   }
 
   public function setColumnVisibility(array $visibility) {
     $this->columnVisibility = $visibility;
     return $this;
   }
 
   public function setDeviceVisibility(array $device_visibility) {
     $this->deviceVisibility = $device_visibility;
     return $this;
   }
 
   public function setDeviceReadyTable($ready) {
     $this->deviceReadyTable = $ready;
     return $this;
   }
 
   public function setShortHeaders(array $short_headers) {
     $this->shortHeaders = $short_headers;
     return $this;
   }
 
   /**
    * Parse a sorting parameter:
    *
    *   list($sort, $reverse) = AphrontTableView::parseSortParam($sort_param);
    *
    * @param string  Sort request parameter.
    * @return pair   Sort value, sort direction.
    */
   public static function parseSort($sort) {
     return array(ltrim($sort, '-'), preg_match('/^-/', $sort));
   }
 
   public function makeSortable(
     PhutilURI $base_uri,
     $param,
     $selected,
     $reverse,
     array $sort_values) {
 
     $this->sortURI        = $base_uri;
     $this->sortParam      = $param;
     $this->sortSelected   = $selected;
     $this->sortReverse    = $reverse;
     $this->sortValues     = array_values($sort_values);
 
     return $this;
   }
 
   public function render() {
     require_celerity_resource('aphront-table-view-css');
 
     $table = array();
 
     $col_classes = array();
     foreach ($this->columnClasses as $key => $class) {
       if (strlen($class)) {
         $col_classes[] = $class;
       } else {
         $col_classes[] = null;
       }
     }
 
     $visibility = array_values($this->columnVisibility);
     $device_visibility = array_values($this->deviceVisibility);
+
     $headers = $this->headers;
     $short_headers = $this->shortHeaders;
     $sort_values = $this->sortValues;
     if ($headers) {
       while (count($headers) > count($visibility)) {
         $visibility[] = true;
       }
       while (count($headers) > count($device_visibility)) {
         $device_visibility[] = true;
       }
       while (count($headers) > count($short_headers)) {
         $short_headers[] = null;
       }
       while (count($headers) > count($sort_values)) {
         $sort_values[] = null;
       }
 
       $tr = array();
       foreach ($headers as $col_num => $header) {
         if (!$visibility[$col_num]) {
           continue;
         }
 
         $classes = array();
 
         if (!empty($col_classes[$col_num])) {
           $classes[] = $col_classes[$col_num];
         }
 
         if (empty($device_visibility[$col_num])) {
           $classes[] = 'aphront-table-view-nodevice';
         }
 
         if ($sort_values[$col_num] !== null) {
           $classes[] = 'aphront-table-view-sortable';
 
           $sort_value = $sort_values[$col_num];
           $sort_glyph_class = 'aphront-table-down-sort';
           if ($sort_value == $this->sortSelected) {
             if ($this->sortReverse) {
               $sort_glyph_class = 'aphront-table-up-sort';
             } else if (!$this->sortReverse) {
               $sort_value = '-'.$sort_value;
             }
             $classes[] = 'aphront-table-view-sortable-selected';
           }
 
           $sort_glyph = phutil_tag(
             'span',
             array(
               'class' => $sort_glyph_class,
             ),
             '');
 
           $header = phutil_tag(
             'a',
             array(
               'href'  => $this->sortURI->alter($this->sortParam, $sort_value),
               'class' => 'aphront-table-view-sort-link',
             ),
             array(
               $header,
               ' ',
               $sort_glyph,
             ));
         }
 
         if ($classes) {
           $class = implode(' ', $classes);
         } else {
           $class = null;
         }
 
         if ($short_headers[$col_num] !== null) {
           $header_nodevice = phutil_tag(
             'span',
             array(
               'class' => 'aphront-table-view-nodevice',
             ),
             $header);
           $header_device = phutil_tag(
             'span',
             array(
               'class' => 'aphront-table-view-device',
             ),
             $short_headers[$col_num]);
 
           $header = hsprintf('%s %s', $header_nodevice, $header_device);
         }
 
         $tr[] = phutil_tag('th', array('class' => $class), $header);
       }
       $table[] = phutil_tag('tr', array(), $tr);
     }
 
     foreach ($col_classes as $key => $value) {
 
       if (($sort_values[$key] !== null) &&
           ($sort_values[$key] == $this->sortSelected)) {
         $value = trim($value.' sorted-column');
       }
 
       if ($value !== null) {
         $col_classes[$key] = $value;
       }
     }
 
     $data = $this->data;
     if ($data) {
       $row_num = 0;
       foreach ($data as $row) {
+        $row_size = count($row);
         while (count($row) > count($col_classes)) {
           $col_classes[] = null;
         }
         while (count($row) > count($visibility)) {
           $visibility[] = true;
         }
+        while (count($row) > count($device_visibility)) {
+          $device_visibility[] = true;
+        }
         $tr = array();
         // NOTE: Use of a separate column counter is to allow this to work
         // correctly if the row data has string or non-sequential keys.
         $col_num = 0;
         foreach ($row as $value) {
           if (!$visibility[$col_num]) {
             ++$col_num;
             continue;
           }
           $class = $col_classes[$col_num];
           if (empty($device_visibility[$col_num])) {
             $class = trim($class.' aphront-table-view-nodevice');
           }
           if (!empty($this->cellClasses[$row_num][$col_num])) {
             $class = trim($class.' '.$this->cellClasses[$row_num][$col_num]);
           }
           $tr[] = phutil_tag('td', array('class' => $class), $value);
           ++$col_num;
         }
 
         $class = idx($this->rowClasses, $row_num);
         if ($this->zebraStripes && ($row_num % 2)) {
           if ($class !== null) {
             $class = 'alt alt-'.$class;
           } else {
             $class = 'alt';
           }
         }
 
         $table[] = phutil_tag('tr', array('class' => $class), $tr);
         ++$row_num;
       }
     } else {
       $colspan = max(count(array_filter($visibility)), 1);
       $table[] = phutil_tag(
         'tr',
         array('class' => 'no-data'),
         phutil_tag(
           'td',
           array('colspan' => $colspan),
           coalesce($this->noDataString, pht('No data available.'))));
     }
 
     $classes = array();
     $classes[] = 'aphront-table-view';
     if ($this->className !== null) {
       $classes[] = $this->className;
     }
     if ($this->deviceReadyTable) {
       $classes[] = 'aphront-table-view-device-ready';
     }
 
     $html = phutil_tag(
       'table',
       array(
         'class' => implode(' ', $classes),
       ),
       $table);
     return phutil_tag_div('aphront-table-wrap', $html);
   }
 
   public static function renderSingleDisplayLine($line) {
 
     // TODO: Is there a cleaner way to do this? We use a relative div with
     // overflow hidden to provide the bounds, and an absolute span with
     // white-space: pre to prevent wrapping. We need to append a character
     // (&nbsp; -- nonbreaking space) afterward to give the bounds div height
     // (alternatively, we could hard-code the line height). This is gross but
     // it's not clear that there's a better appraoch.
 
     return phutil_tag(
       'div',
       array(
         'class' => 'single-display-line-bounds',
       ),
       array(
         phutil_tag(
           'span',
           array(
             'class' => 'single-display-line-content',
           ),
           $line),
         "\xC2\xA0",
       ));
   }
 
 
 }
diff --git a/src/view/form/AphrontFormView.php b/src/view/form/AphrontFormView.php
index 6b30656131..f9f281ff20 100644
--- a/src/view/form/AphrontFormView.php
+++ b/src/view/form/AphrontFormView.php
@@ -1,168 +1,171 @@
 <?php
 
 final class AphrontFormView extends AphrontView {
 
   private $action;
   private $method = 'POST';
   private $header;
   private $data = array();
   private $encType;
   private $workflow;
   private $id;
   private $shaded = false;
   private $sigils = array();
   private $metadata;
   private $controls = array();
   private $fullWidth = false;
 
   public function setMetadata($metadata) {
     $this->metadata = $metadata;
     return $this;
   }
 
   public function getMetadata() {
     return $this->metadata;
   }
 
   public function setID($id) {
     $this->id = $id;
     return $this;
   }
 
   public function setAction($action) {
     $this->action = $action;
     return $this;
   }
 
   public function setMethod($method) {
     $this->method = $method;
     return $this;
   }
 
   public function setEncType($enc_type) {
     $this->encType = $enc_type;
     return $this;
   }
 
   public function setShaded($shaded) {
     $this->shaded = $shaded;
     return $this;
   }
 
   public function addHiddenInput($key, $value) {
     $this->data[$key] = $value;
     return $this;
   }
 
   public function setWorkflow($workflow) {
     $this->workflow = $workflow;
     return $this;
   }
 
   public function addSigil($sigil) {
     $this->sigils[] = $sigil;
     return $this;
   }
 
   public function setFullWidth($full_width) {
     $this->fullWidth = $full_width;
     return $this;
   }
 
   public function getFullWidth() {
     return $this->fullWidth;
   }
 
   public function appendInstructions($text) {
     return $this->appendChild(
       phutil_tag(
         'div',
         array(
           'class' => 'aphront-form-instructions',
         ),
         $text));
   }
 
   public function appendRemarkupInstructions($remarkup) {
     return $this->appendInstructions(
       PhabricatorMarkupEngine::renderOneObject(
         id(new PhabricatorMarkupOneOff())->setContent($remarkup),
         'default',
         $this->getUser()));
   }
 
   public function buildLayoutView() {
     foreach ($this->controls as $control) {
       $control->setUser($this->getUser());
       $control->willRender();
     }
 
     return id(new PHUIFormLayoutView())
       ->setFullWidth($this->getFullWidth())
       ->appendChild($this->renderDataInputs())
       ->appendChild($this->renderChildren());
   }
 
 
   /**
    * Append a control to the form.
    *
    * This method behaves like @{method:appendChild}, but it only takes
    * controls. It will propagate some information from the form to the
    * control to simplify rendering.
    *
    * @param AphrontFormControl Control to append.
    * @return this
    */
   public function appendControl(AphrontFormControl $control) {
     $this->controls[] = $control;
     return $this->appendChild($control);
   }
 
 
   public function render() {
     require_celerity_resource('phui-form-view-css');
 
     $layout = $this->buildLayoutView();
 
     if (!$this->user) {
-      throw new Exception(pht('You must pass the user to AphrontFormView.'));
+      throw new Exception(
+        pht(
+          'You must pass the user to %s.',
+          __CLASS__));
     }
 
     $sigils = $this->sigils;
     if ($this->workflow) {
       $sigils[] = 'workflow';
     }
 
     return phabricator_form(
       $this->user,
       array(
         'class'   => $this->shaded ? 'phui-form-shaded' : null,
         'action'  => $this->action,
         'method'  => $this->method,
         'enctype' => $this->encType,
         'sigil'   => $sigils ? implode(' ', $sigils) : null,
         'meta'    => $this->metadata,
         'id'      => $this->id,
       ),
       $layout->render());
   }
 
   private function renderDataInputs() {
     $inputs = array();
     foreach ($this->data as $key => $value) {
       if ($value === null) {
         continue;
       }
       $inputs[] = phutil_tag(
         'input',
         array(
           'type'  => 'hidden',
           'name'  => $key,
           'value' => $value,
         ));
     }
     return $inputs;
   }
 
 }
diff --git a/src/view/form/PHUIFormLayoutView.php b/src/view/form/PHUIFormLayoutView.php
index fa05a72a59..1c5ccbb7f2 100644
--- a/src/view/form/PHUIFormLayoutView.php
+++ b/src/view/form/PHUIFormLayoutView.php
@@ -1,55 +1,61 @@
 <?php
 
 /**
  * This provides the layout of an AphrontFormView without actually providing
  * the <form /> tag. Useful on its own for creating forms in other forms (like
  * dialogs) or forms which aren't submittable.
  */
 final class PHUIFormLayoutView extends AphrontView {
 
+  private $classes = array();
   private $fullWidth;
 
   public function setFullWidth($width) {
     $this->fullWidth = $width;
     return $this;
   }
 
+  public function addClass($class) {
+    $this->classes[] = $class;
+    return $this;
+  }
+
   public function appendInstructions($text) {
     return $this->appendChild(
       phutil_tag(
         'div',
         array(
           'class' => 'aphront-form-instructions',
         ),
         $text));
   }
 
   public function appendRemarkupInstructions($remarkup) {
     if ($this->getUser() === null) {
-      throw new Exception(
-        'Call `setUser` before appending Remarkup to PHUIFormLayoutView.');
+      throw new PhutilInvalidStateException('setUser');
     }
 
     return $this->appendInstructions(
       PhabricatorMarkupEngine::renderOneObject(
         id(new PhabricatorMarkupOneOff())->setContent($remarkup),
         'default',
         $this->getUser()));
   }
 
   public function render() {
-    $classes = array('phui-form-view');
+    $classes = $this->classes;
+    $classes[] = 'phui-form-view';
 
     if ($this->fullWidth) {
       $classes[] = 'phui-form-full-width';
     }
 
     return phutil_tag(
       'div',
       array(
         'class' => implode(' ', $classes),
       ),
       $this->renderChildren());
 
   }
 }
diff --git a/src/view/form/PHUIInfoView.php b/src/view/form/PHUIInfoView.php
index a079e3f6ca..497fcf3f78 100644
--- a/src/view/form/PHUIInfoView.php
+++ b/src/view/form/PHUIInfoView.php
@@ -1,123 +1,122 @@
 <?php
 
 final class PHUIInfoView extends AphrontView {
 
   const SEVERITY_ERROR = 'error';
   const SEVERITY_WARNING = 'warning';
   const SEVERITY_NOTICE = 'notice';
   const SEVERITY_NODATA = 'nodata';
   const SEVERITY_SUCCESS = 'success';
 
   private $title;
   private $errors;
   private $severity;
   private $id;
   private $buttons = array();
 
   public function setTitle($title) {
     $this->title = $title;
     return $this;
   }
 
   public function setSeverity($severity) {
     $this->severity = $severity;
     return $this;
   }
 
   public function setErrors(array $errors) {
     $this->errors = $errors;
     return $this;
   }
 
   public function setID($id) {
     $this->id = $id;
     return $this;
   }
 
   public function addButton(PHUIButtonView $button) {
 
     $this->buttons[] = $button;
     return $this;
   }
 
   final public function render() {
-
     require_celerity_resource('phui-info-view-css');
 
     $errors = $this->errors;
     if ($errors) {
       $list = array();
       foreach ($errors as $error) {
         $list[] = phutil_tag(
           'li',
           array(),
           $error);
       }
       $list = phutil_tag(
         'ul',
         array(
           'class' => 'phui-info-view-list',
         ),
         $list);
     } else {
       $list = null;
     }
 
     $title = $this->title;
     if (strlen($title)) {
       $title = phutil_tag(
         'h1',
         array(
           'class' => 'phui-info-view-head',
         ),
         $title);
     } else {
       $title = null;
     }
 
     $this->severity = nonempty($this->severity, self::SEVERITY_ERROR);
 
     $classes = array();
     $classes[] = 'phui-info-view';
     $classes[] = 'phui-info-severity-'.$this->severity;
     $classes[] = 'grouped';
     $classes = implode(' ', $classes);
 
     $children = $this->renderChildren();
     if ($list) {
       $children[] = $list;
     }
 
     $body = null;
     if (!empty($children)) {
       $body = phutil_tag(
         'div',
         array(
           'class' => 'phui-info-view-body',
         ),
         $children);
     }
 
     $buttons = null;
     if (!empty($this->buttons)) {
       $buttons = phutil_tag(
         'div',
         array(
           'class' => 'phui-info-view-actions',
         ),
         $this->buttons);
     }
 
     return phutil_tag(
       'div',
       array(
         'id' => $this->id,
         'class' => $classes,
       ),
       array(
         $buttons,
         $title,
         $body,
       ));
   }
 }
diff --git a/src/view/form/PHUIPagedFormView.php b/src/view/form/PHUIPagedFormView.php
index 82e58c7f58..ff72f67ca0 100644
--- a/src/view/form/PHUIPagedFormView.php
+++ b/src/view/form/PHUIPagedFormView.php
@@ -1,278 +1,278 @@
 <?php
 
 /**
  * @task page   Managing Pages
  */
 final class PHUIPagedFormView extends AphrontView {
 
   private $name = 'pages';
   private $pages = array();
   private $selectedPage;
   private $choosePage;
   private $nextPage;
   private $prevPage;
   private $complete;
   private $cancelURI;
 
   protected function canAppendChild() {
     return false;
   }
 
 
 /* -(  Managing Pages  )----------------------------------------------------- */
 
 
   /**
    * @task page
    */
   public function addPage($key, PHUIFormPageView $page) {
     if (isset($this->pages[$key])) {
       throw new Exception("Duplicate page with key '{$key}'!");
     }
     $this->pages[$key] = $page;
     $page->setPagedFormView($this, $key);
 
     $this->selectedPage = null;
     $this->complete = null;
 
     return $this;
   }
 
 
   /**
    * @task page
    */
   public function getPage($key) {
     if (!$this->pageExists($key)) {
       throw new Exception("No page '{$key}' exists!");
     }
     return $this->pages[$key];
   }
 
 
   /**
    * @task page
    */
   public function pageExists($key) {
     return isset($this->pages[$key]);
   }
 
 
   /**
    * @task page
    */
   protected function getPageIndex($key) {
     $page = $this->getPage($key);
 
     $index = 0;
     foreach ($this->pages as $target_page) {
       if ($page === $target_page) {
         break;
       }
       $index++;
     }
 
     return $index;
   }
 
 
   /**
    * @task page
    */
   protected function getPageByIndex($index) {
     foreach ($this->pages as $page) {
       if (!$index) {
         return $page;
       }
       $index--;
     }
 
     throw new Exception("Requesting out-of-bounds page '{$index}'.");
   }
 
   protected function getLastIndex() {
     return count($this->pages) - 1;
   }
 
   protected function isFirstPage(PHUIFormPageView $page) {
     return ($this->getPageIndex($page->getKey()) === 0);
 
   }
 
   protected function isLastPage(PHUIFormPageView $page) {
     return ($this->getPageIndex($page->getKey()) === (count($this->pages) - 1));
   }
 
   public function getSelectedPage() {
     return $this->selectedPage;
   }
 
   public function readFromObject($object) {
     return $this->processForm($is_request = false, $object);
   }
 
   public function writeToResponse($response) {
     foreach ($this->pages as $page) {
       $page->validateResponseType($response);
       $response = $page->writeToResponse($page);
     }
 
     return $response;
   }
 
   public function readFromRequest(AphrontRequest $request) {
     $this->choosePage = $request->getStr($this->getRequestKey('page'));
     $this->nextPage = $request->getStr('__submit__');
     $this->prevPage = $request->getStr('__back__');
 
     return $this->processForm($is_request = true, $request);
   }
 
   public function setName($name) {
     $this->name = $name;
     return $this;
   }
 
 
   public function getValue($page, $key, $default = null) {
     return $this->getPage($page)->getValue($key, $default);
   }
 
   public function setValue($page, $key, $value) {
     $this->getPage($page)->setValue($key, $value);
     return $this;
   }
 
   private function processForm($is_request, $source) {
     if ($this->pageExists($this->choosePage)) {
       $selected = $this->getPage($this->choosePage);
     } else {
       $selected = $this->getPageByIndex(0);
     }
 
     $is_attempt_complete = false;
     if ($this->prevPage) {
-      $prev_index = $this->getPageIndex($selected->getKey()) - 1;;
+      $prev_index = $this->getPageIndex($selected->getKey()) - 1;
       $index = max(0, $prev_index);
       $selected = $this->getPageByIndex($index);
     } else if ($this->nextPage) {
       $next_index = $this->getPageIndex($selected->getKey()) + 1;
       if ($next_index > $this->getLastIndex()) {
         $is_attempt_complete = true;
       }
       $index = min($this->getLastIndex(), $next_index);
       $selected = $this->getPageByIndex($index);
     }
 
     $validation_error = false;
     $found_selected = false;
     foreach ($this->pages as $key => $page) {
       if ($is_request) {
         if ($key === $this->choosePage) {
           $page->readFromRequest($source);
         } else {
           $page->readSerializedValues($source);
         }
       } else {
         $page->readFromObject($source);
       }
 
       if (!$found_selected) {
         $page->adjustFormPage();
       }
 
       if ($page === $selected) {
         $found_selected = true;
       }
 
       if (!$found_selected || $is_attempt_complete) {
         if (!$page->isValid()) {
           $selected = $page;
           $validation_error = true;
           break;
         }
       }
     }
 
     if ($is_attempt_complete && !$validation_error) {
       $this->complete = true;
     } else {
       $this->selectedPage = $selected;
     }
 
     return $this;
   }
 
   public function isComplete() {
     return $this->complete;
   }
 
   public function getRequestKey($key) {
     return $this->name.':'.$key;
   }
 
   public function setCancelURI($cancel_uri) {
     $this->cancelURI = $cancel_uri;
     return $this;
   }
 
   public function getCancelURI() {
     return $this->cancelURI;
   }
 
   public function render() {
     $form = id(new AphrontFormView())
       ->setUser($this->getUser());
 
     $selected_page = $this->getSelectedPage();
     if (!$selected_page) {
       throw new Exception('No selected page!');
     }
 
     $form->addHiddenInput(
       $this->getRequestKey('page'),
       $selected_page->getKey());
 
     $errors = array();
 
     foreach ($this->pages as $page) {
       if ($page == $selected_page) {
         $errors = $page->getPageErrors();
         continue;
       }
       foreach ($page->getSerializedValues() as $key => $value) {
         $form->addHiddenInput($key, $value);
       }
     }
 
     $submit = id(new PHUIFormMultiSubmitControl());
 
     if (!$this->isFirstPage($selected_page)) {
       $submit->addBackButton();
     } else if ($this->getCancelURI()) {
       $submit->addCancelButton($this->getCancelURI());
     }
 
     if ($this->isLastPage($selected_page)) {
       $submit->addSubmitButton(pht('Save'));
     } else {
       $submit->addSubmitButton(pht("Continue \xC2\xBB"));
     }
 
     $form->appendChild($selected_page);
     $form->appendChild($submit);
 
     $box = id(new PHUIObjectBoxView())
       ->setFormErrors($errors)
       ->setForm($form);
 
     if ($selected_page->getPageName()) {
       $header = id(new PHUIHeaderView())
         ->setHeader($selected_page->getPageName());
       $box->setHeader($header);
     }
 
     return $box;
   }
 
 }
diff --git a/src/view/form/control/AphrontFormCheckboxControl.php b/src/view/form/control/AphrontFormCheckboxControl.php
index 673c1c6d82..0d89e0a6ad 100644
--- a/src/view/form/control/AphrontFormCheckboxControl.php
+++ b/src/view/form/control/AphrontFormCheckboxControl.php
@@ -1,52 +1,61 @@
 <?php
 
 final class AphrontFormCheckboxControl extends AphrontFormControl {
 
   private $boxes = array();
 
-  public function addCheckbox($name, $value, $label, $checked = false) {
+  public function addCheckbox(
+    $name,
+    $value,
+    $label,
+    $checked = false,
+    $id = null) {
     $this->boxes[] = array(
       'name'    => $name,
       'value'   => $value,
       'label'   => $label,
       'checked' => $checked,
+      'id' => $id,
     );
     return $this;
   }
 
   protected function getCustomControlClass() {
     return 'aphront-form-control-checkbox';
   }
 
   protected function renderInput() {
     $rows = array();
     foreach ($this->boxes as $box) {
-      $id = celerity_generate_unique_node_id();
+      $id = idx($box, 'id');
+      if ($id === null) {
+        $id = celerity_generate_unique_node_id();
+      }
       $checkbox = phutil_tag(
         'input',
         array(
           'id' => $id,
           'type' => 'checkbox',
           'name' => $box['name'],
           'value' => $box['value'],
           'checked' => $box['checked'] ? 'checked' : null,
           'disabled' => $this->getDisabled() ? 'disabled' : null,
         ));
       $label = phutil_tag(
         'label',
         array(
           'for' => $id,
         ),
         $box['label']);
       $rows[] = phutil_tag('tr', array(), array(
         phutil_tag('td', array(), $checkbox),
         phutil_tag('th', array(), $label),
       ));
     }
     return phutil_tag(
       'table',
       array('class' => 'aphront-form-control-checkbox-layout'),
       $rows);
   }
 
 }
diff --git a/src/view/form/control/AphrontFormDateControl.php b/src/view/form/control/AphrontFormDateControl.php
index ed79392fbb..84de3b3215 100644
--- a/src/view/form/control/AphrontFormDateControl.php
+++ b/src/view/form/control/AphrontFormDateControl.php
@@ -1,352 +1,362 @@
 <?php
 
 final class AphrontFormDateControl extends AphrontFormControl {
 
   private $initialTime;
   private $zone;
 
   private $valueDay;
   private $valueMonth;
   private $valueYear;
   private $valueTime;
   private $allowNull;
   private $continueOnInvalidDate = false;
+  private $isTimeDisabled;
   private $isDisabled;
 
   public function setAllowNull($allow_null) {
     $this->allowNull = $allow_null;
     return $this;
   }
 
+  public function setIsTimeDisabled($is_disabled) {
+    $this->isTimeDisabled = $is_disabled;
+    return $this;
+  }
+
   const TIME_START_OF_DAY         = 'start-of-day';
   const TIME_END_OF_DAY           = 'end-of-day';
   const TIME_START_OF_BUSINESS    = 'start-of-business';
   const TIME_END_OF_BUSINESS      = 'end-of-business';
 
   public function setInitialTime($time) {
     $this->initialTime = $time;
     return $this;
   }
 
   public function readValueFromRequest(AphrontRequest $request) {
     $day = $request->getInt($this->getDayInputName());
     $month = $request->getInt($this->getMonthInputName());
     $year = $request->getInt($this->getYearInputName());
     $time = $request->getStr($this->getTimeInputName());
     $enabled = $request->getBool($this->getCheckboxInputName());
 
     if ($this->allowNull && !$enabled) {
       $this->setError(null);
       $this->setValue(null);
       return;
     }
 
     $err = $this->getError();
 
     if ($day || $month || $year || $time) {
       $this->valueDay = $day;
       $this->valueMonth = $month;
       $this->valueYear = $year;
       $this->valueTime = $time;
 
       // Assume invalid.
       $err = 'Invalid';
 
       $zone = $this->getTimezone();
 
       try {
         $date = new DateTime("{$year}-{$month}-{$day} {$time}", $zone);
         $value = $date->format('U');
       } catch (Exception $ex) {
         $value = null;
       }
 
       if ($value) {
         $this->setValue($value);
         $err = null;
       } else {
         $this->setValue(null);
       }
     } else {
       $value = $this->getInitialValue();
       if ($value) {
         $this->setValue($value);
       } else {
         $this->setValue(null);
       }
     }
 
     $this->setError($err);
 
     return $this->getValue();
   }
 
   protected function getCustomControlClass() {
     return 'aphront-form-control-date';
   }
 
   public function setValue($epoch) {
     if ($epoch instanceof AphrontFormDateControlValue) {
       $this->continueOnInvalidDate = true;
       $this->valueYear  = $epoch->getValueYear();
       $this->valueMonth = $epoch->getValueMonth();
       $this->valueDay   = $epoch->getValueDay();
       $this->valueTime  = $epoch->getValueTime();
       $this->allowNull = $epoch->getOptional();
       $this->isDisabled = $epoch->isDisabled();
 
       return parent::setValue($epoch->getEpoch());
     }
 
     $result = parent::setValue($epoch);
 
     if ($epoch === null) {
       return $result;
     }
 
     $readable = $this->formatTime($epoch, 'Y!m!d!g:i A');
     $readable = explode('!', $readable, 4);
 
     $this->valueYear  = $readable[0];
     $this->valueMonth = $readable[1];
     $this->valueDay   = $readable[2];
     $this->valueTime  = $readable[3];
 
     return $result;
   }
 
   private function getMinYear() {
     $cur_year = $this->formatTime(
       time(),
       'Y');
     $val_year = $this->getYearInputValue();
 
     return min($cur_year, $val_year) - 3;
   }
 
   private function getMaxYear() {
     $cur_year = $this->formatTime(
       time(),
       'Y');
     $val_year = $this->getYearInputValue();
 
     return max($cur_year, $val_year) + 3;
   }
 
   private function getDayInputValue() {
     return $this->valueDay;
   }
 
   private function getMonthInputValue() {
     return $this->valueMonth;
   }
 
   private function getYearInputValue() {
     return $this->valueYear;
   }
 
   private function getTimeInputValue() {
     return $this->valueTime;
   }
 
   private function formatTime($epoch, $fmt) {
     return phabricator_format_local_time(
       $epoch,
       $this->user,
       $fmt);
   }
 
   private function getDayInputName() {
     return $this->getName().'_d';
   }
 
   private function getMonthInputName() {
     return $this->getName().'_m';
   }
 
   private function getYearInputName() {
     return $this->getName().'_y';
   }
 
   private function getTimeInputName() {
     return $this->getName().'_t';
   }
 
   private function getCheckboxInputName() {
     return $this->getName().'_e';
   }
 
   protected function renderInput() {
 
     $disabled = null;
     if ($this->getValue() === null && !$this->continueOnInvalidDate) {
       $this->setValue($this->getInitialValue());
       if ($this->allowNull) {
         $disabled = 'disabled';
       }
     }
 
     if ($this->isDisabled) {
       $disabled = 'disabled';
     }
 
     $min_year = $this->getMinYear();
     $max_year = $this->getMaxYear();
 
     $days = range(1, 31);
     $days = array_fuse($days);
 
     $months = array(
       1 => pht('Jan'),
       2 => pht('Feb'),
       3 => pht('Mar'),
       4 => pht('Apr'),
       5 => pht('May'),
       6 => pht('Jun'),
       7 => pht('Jul'),
       8 => pht('Aug'),
       9 => pht('Sep'),
       10 => pht('Oct'),
       11 => pht('Nov'),
       12 => pht('Dec'),
     );
 
     $checkbox = null;
     if ($this->allowNull) {
       $checkbox = javelin_tag(
         'input',
         array(
           'type' => 'checkbox',
           'name' => $this->getCheckboxInputName(),
           'sigil' => 'calendar-enable',
           'class' => 'aphront-form-date-enabled-input',
           'value' => 1,
           'checked' => ($disabled === null ? 'checked' : null),
         ));
     }
 
     $years = range($this->getMinYear(), $this->getMaxYear());
     $years = array_fuse($years);
 
     $days_sel = AphrontFormSelectControl::renderSelectTag(
       $this->getDayInputValue(),
       $days,
       array(
         'name' => $this->getDayInputName(),
         'sigil' => 'day-input',
       ));
 
     $months_sel = AphrontFormSelectControl::renderSelectTag(
       $this->getMonthInputValue(),
       $months,
       array(
         'name' => $this->getMonthInputName(),
         'sigil' => 'month-input',
       ));
 
     $years_sel = AphrontFormSelectControl::renderSelectTag(
       $this->getYearInputValue(),
       $years,
       array(
         'name'  => $this->getYearInputName(),
         'sigil' => 'year-input',
       ));
 
     $cicon = id(new PHUIIconView())
       ->setIconFont('fa-calendar');
 
     $cal_icon = javelin_tag(
       'a',
       array(
         'href'  => '#',
         'class' => 'calendar-button',
         'sigil' => 'calendar-button',
       ),
       $cicon);
 
     $time_sel = javelin_tag(
       'input',
       array(
         'name'  => $this->getTimeInputName(),
         'sigil' => 'time-input',
         'value' => $this->getTimeInputValue(),
         'type'  => 'text',
         'class' => 'aphront-form-date-time-input',
       ),
       '');
 
     Javelin::initBehavior('fancy-datepicker', array());
 
     $classes = array();
     $classes[] = 'aphront-form-date-container';
     if ($disabled) {
       $classes[] = 'datepicker-disabled';
     }
+    if ($this->isTimeDisabled) {
+      $classes[] = 'no-time';
+    }
 
     return javelin_tag(
       'div',
       array(
         'class' => implode(' ', $classes),
         'sigil' => 'phabricator-date-control',
         'meta'  => array(
           'disabled' => (bool)$disabled,
         ),
+        'id' => $this->getID(),
       ),
       array(
         $checkbox,
         $days_sel,
         $months_sel,
         $years_sel,
         $cal_icon,
         $time_sel,
       ));
   }
 
   private function getTimezone() {
     if ($this->zone) {
       return $this->zone;
     }
 
     $user = $this->getUser();
     if (!$this->getUser()) {
       throw new Exception('Call setUser() before getTimezone()!');
     }
 
     $user_zone = $user->getTimezoneIdentifier();
     $this->zone = new DateTimeZone($user_zone);
     return $this->zone;
   }
 
   private function getInitialValue() {
     $zone = $this->getTimezone();
 
     // TODO: We could eventually allow these to be customized per install or
     // per user or both, but let's wait and see.
     switch ($this->initialTime) {
       case self::TIME_START_OF_DAY:
       default:
         $time = '12:00 AM';
         break;
       case self::TIME_START_OF_BUSINESS:
         $time = '9:00 AM';
         break;
       case self::TIME_END_OF_BUSINESS:
         $time = '5:00 PM';
         break;
       case self::TIME_END_OF_DAY:
         $time = '11:59 PM';
         break;
     }
 
     $today = $this->formatTime(time(), 'Y-m-d');
     try {
       $date = new DateTime("{$today} {$time}", $zone);
       $value = $date->format('U');
     } catch (Exception $ex) {
       $value = null;
     }
 
     return $value;
   }
 
 }
diff --git a/src/view/form/control/PhabricatorRemarkupControl.php b/src/view/form/control/PhabricatorRemarkupControl.php
index 7d5451a1b2..f384dd3ba3 100644
--- a/src/view/form/control/PhabricatorRemarkupControl.php
+++ b/src/view/form/control/PhabricatorRemarkupControl.php
@@ -1,227 +1,226 @@
 <?php
 
 final class PhabricatorRemarkupControl extends AphrontFormTextAreaControl {
   private $disableMacro = false;
 
   private $disableFullScreen = false;
 
   public function setDisableMacros($disable) {
     $this->disableMacro = $disable;
     return $this;
   }
 
   public function setDisableFullScreen($disable) {
     $this->disableFullScreen = $disable;
     return $this;
   }
 
   protected function renderInput() {
     $id = $this->getID();
     if (!$id) {
       $id = celerity_generate_unique_node_id();
       $this->setID($id);
     }
 
     $viewer = $this->getUser();
     if (!$viewer) {
-      throw new Exception(
-        pht('Call setUser() before rendering a PhabricatorRemarkupControl!'));
+      throw new PhutilInvalidStateException('setUser');
     }
 
     // We need to have this if previews render images, since Ajax can not
     // currently ship JS or CSS.
     require_celerity_resource('lightbox-attachment-css');
 
     Javelin::initBehavior(
       'aphront-drag-and-drop-textarea',
       array(
         'target' => $id,
         'activatedClass' => 'aphront-textarea-drag-and-drop',
         'uri' => '/file/dropupload/',
         'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(),
       ));
 
     Javelin::initBehavior(
       'phabricator-remarkup-assist',
       array(
         'pht' => array(
           'bold text' => pht('bold text'),
           'italic text' => pht('italic text'),
           'monospaced text' => pht('monospaced text'),
           'List Item' => pht('List Item'),
           'data' => pht('data'),
           'name' => pht('name'),
           'URL' => pht('URL'),
         ),
       ));
     Javelin::initBehavior('phabricator-tooltips', array());
 
     $actions = array(
       'fa-bold' => array(
         'tip' => pht('Bold'),
       ),
       'fa-italic' => array(
         'tip' => pht('Italics'),
       ),
       'fa-text-width' => array(
         'tip' => pht('Monospaced'),
       ),
       'fa-link' => array(
         'tip' => pht('Link'),
       ),
       array(
         'spacer' => true,
       ),
       'fa-list-ul' => array(
         'tip' => pht('Bulleted List'),
       ),
       'fa-list-ol' => array(
         'tip' => pht('Numbered List'),
       ),
       'fa-code' => array(
         'tip' => pht('Code Block'),
       ),
       'fa-table' => array(
         'tip' => pht('Table'),
       ),
       'fa-cloud-upload' => array(
         'tip' => pht('Upload File'),
       ),
     );
 
     $can_use_macros =
       (!$this->disableMacro) &&
       (function_exists('imagettftext'));
 
     if ($can_use_macros) {
       $can_use_macros = PhabricatorApplication::isClassInstalledForViewer(
         'PhabricatorMacroApplication',
         $viewer);
     }
 
     if ($can_use_macros) {
       $actions[] = array(
         'spacer' => true,
         );
       $actions['fa-meh-o'] = array(
         'tip' => pht('Meme'),
       );
     }
 
     $actions['fa-life-bouy'] = array(
         'tip' => pht('Help'),
         'align' => 'right',
         'href'  => PhabricatorEnv::getDoclink('Remarkup Reference'),
       );
 
     if (!$this->disableFullScreen) {
       $actions[] = array(
         'spacer' => true,
         'align' => 'right',
       );
 
       $actions['fa-arrows-alt'] = array(
         'tip' => pht('Fullscreen Mode'),
         'align' => 'right',
       );
     }
 
     $buttons = array();
     foreach ($actions as $action => $spec) {
 
       $classes = array();
 
       if (idx($spec, 'align') == 'right') {
         $classes[] = 'remarkup-assist-right';
       }
 
       if (idx($spec, 'spacer')) {
         $classes[] = 'remarkup-assist-separator';
         $buttons[] = phutil_tag(
           'span',
           array(
             'class' => implode(' ', $classes),
           ),
           '');
         continue;
       } else {
         $classes[] = 'remarkup-assist-button';
       }
 
       $href = idx($spec, 'href', '#');
       if ($href == '#') {
         $meta = array('action' => $action);
         $mustcapture = true;
         $target = null;
       } else {
         $meta = array();
         $mustcapture = null;
         $target = '_blank';
       }
 
       $content = null;
 
       $tip = idx($spec, 'tip');
       if ($tip) {
         $meta['tip'] = $tip;
         $content = javelin_tag(
           'span',
           array(
             'aural' => true,
           ),
           $tip);
       }
 
       $buttons[] = javelin_tag(
         'a',
         array(
           'class'       => implode(' ', $classes),
           'href'        => $href,
           'sigil'       => 'remarkup-assist has-tooltip',
           'meta'        => $meta,
           'mustcapture' => $mustcapture,
           'target'      => $target,
           'tabindex'    => -1,
         ),
         phutil_tag(
           'div',
           array(
             'class' =>
               'remarkup-assist phui-icon-view phui-font-fa bluegrey '.$action,
           ),
           $content));
     }
 
     $buttons = phutil_tag(
       'div',
       array(
         'class' => 'remarkup-assist-bar',
       ),
       $buttons);
 
     $monospaced_textareas = null;
     $monospaced_textareas_class = null;
 
     $monospaced_textareas = $viewer
       ->loadPreferences()
       ->getPreference(
         PhabricatorUserPreferences::PREFERENCE_MONOSPACED_TEXTAREAS);
     if ($monospaced_textareas == 'enabled') {
       $monospaced_textareas_class = 'PhabricatorMonospaced';
     }
 
     $this->setCustomClass(
       'remarkup-assist-textarea '.$monospaced_textareas_class);
 
     return javelin_tag(
       'div',
       array(
         'sigil' => 'remarkup-assist-control',
       ),
       array(
         $buttons,
         parent::renderInput(),
       ));
   }
 
 }
diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php
index b0f8b5d0be..7c5dcaf1cb 100644
--- a/src/view/page/PhabricatorStandardPageView.php
+++ b/src/view/page/PhabricatorStandardPageView.php
@@ -1,731 +1,722 @@
 <?php
 
 /**
  * This is a standard Phabricator page with menus, Javelin, DarkConsole, and
  * basic styles.
- *
  */
 final class PhabricatorStandardPageView extends PhabricatorBarePageView {
 
   private $baseURI;
   private $applicationName;
   private $glyph;
   private $menuContent;
   private $showChrome = true;
   private $disableConsole;
   private $pageObjects = array();
   private $applicationMenu;
   private $showFooter = true;
   private $showDurableColumn = true;
 
   public function setShowFooter($show_footer) {
     $this->showFooter = $show_footer;
     return $this;
   }
 
   public function getShowFooter() {
     return $this->showFooter;
   }
 
   public function setApplicationMenu(PHUIListView $application_menu) {
     $this->applicationMenu = $application_menu;
     return $this;
   }
 
   public function getApplicationMenu() {
     return $this->applicationMenu;
   }
 
   public function setApplicationName($application_name) {
     $this->applicationName = $application_name;
     return $this;
   }
 
   public function setDisableConsole($disable) {
     $this->disableConsole = $disable;
     return $this;
   }
 
   public function getApplicationName() {
     return $this->applicationName;
   }
 
   public function setBaseURI($base_uri) {
     $this->baseURI = $base_uri;
     return $this;
   }
 
   public function getBaseURI() {
     return $this->baseURI;
   }
 
   public function setShowChrome($show_chrome) {
     $this->showChrome = $show_chrome;
     return $this;
   }
 
   public function getShowChrome() {
     return $this->showChrome;
   }
 
   public function appendPageObjects(array $objs) {
     foreach ($objs as $obj) {
       $this->pageObjects[] = $obj;
     }
   }
 
   public function setShowDurableColumn($show) {
     $this->showDurableColumn = $show;
     return $this;
   }
 
   public function getShowDurableColumn() {
     $request = $this->getRequest();
     if (!$request) {
       return false;
     }
 
     $viewer = $request->getUser();
     if (!$viewer->isLoggedIn()) {
       return false;
     }
 
     $conpherence_installed = PhabricatorApplication::isClassInstalledForViewer(
       'PhabricatorConpherenceApplication',
       $viewer);
     if (!$conpherence_installed) {
       return false;
     }
 
     if ($this->isQuicksandBlacklistURI()) {
       return false;
     }
 
     return true;
   }
 
   private function isQuicksandBlacklistURI() {
     $request = $this->getRequest();
     if (!$request) {
       return false;
     }
 
     $patterns = $this->getQuicksandURIPatternBlacklist();
     $path = $request->getRequestURI()->getPath();
     foreach ($patterns as $pattern) {
       if (preg_match('(^'.$pattern.'$)', $path)) {
         return true;
       }
     }
     return false;
   }
 
   public function getDurableColumnVisible() {
     $column_key = PhabricatorUserPreferences::PREFERENCE_CONPHERENCE_COLUMN;
     return (bool)$this->getUserPreference($column_key, 0);
   }
 
 
   public function getTitle() {
     $glyph_key = PhabricatorUserPreferences::PREFERENCE_TITLES;
     if ($this->getUserPreference($glyph_key) == 'text') {
       $use_glyph = false;
     } else {
       $use_glyph = true;
     }
 
     $title = parent::getTitle();
 
     $prefix = null;
     if ($use_glyph) {
       $prefix = $this->getGlyph();
     } else {
       $application_name = $this->getApplicationName();
       if (strlen($application_name)) {
         $prefix = '['.$application_name.']';
       }
     }
 
     if (strlen($prefix)) {
       $title = $prefix.' '.$title;
     }
 
     return $title;
   }
 
 
   protected function willRenderPage() {
     parent::willRenderPage();
 
     if (!$this->getRequest()) {
       throw new Exception(
         pht(
-          'You must set the Request to render a PhabricatorStandardPageView.'));
+          'You must set the Request to render a %s.',
+          __CLASS__));
     }
 
     $console = $this->getConsole();
 
     require_celerity_resource('phabricator-core-css');
     require_celerity_resource('phabricator-zindex-css');
     require_celerity_resource('phui-button-css');
     require_celerity_resource('phui-spacing-css');
     require_celerity_resource('phui-form-css');
     require_celerity_resource('sprite-gradient-css');
     require_celerity_resource('phabricator-standard-page-view');
     require_celerity_resource('conpherence-durable-column-view');
 
     Javelin::initBehavior('workflow', array());
 
     $request = $this->getRequest();
     $user = null;
     if ($request) {
       $user = $request->getUser();
     }
 
     if ($user) {
       $default_img_uri =
         celerity_get_resource_uri(
           'rsrc/image/icon/fatcow/document_black.png');
       $download_form = phabricator_form(
         $user,
         array(
           'action' => '#',
           'method' => 'POST',
           'class'  => 'lightbox-download-form',
           'sigil'  => 'download',
         ),
         phutil_tag(
           'button',
           array(),
           pht('Download')));
 
       Javelin::initBehavior(
         'lightbox-attachments',
         array(
           'defaultImageUri' => $default_img_uri,
           'downloadForm'    => $download_form,
         ));
     }
 
     Javelin::initBehavior('aphront-form-disable-on-submit');
     Javelin::initBehavior('toggle-class', array());
     Javelin::initBehavior('history-install');
     Javelin::initBehavior('phabricator-gesture');
 
     $current_token = null;
     if ($user) {
       $current_token = $user->getCSRFToken();
     }
 
     Javelin::initBehavior(
       'refresh-csrf',
       array(
         'tokenName' => AphrontRequest::getCSRFTokenName(),
         'header'    => AphrontRequest::getCSRFHeaderName(),
         'current'   => $current_token,
       ));
 
     Javelin::initBehavior('device');
 
     Javelin::initBehavior(
       'high-security-warning',
       $this->getHighSecurityWarningConfig());
 
     if ($console) {
       require_celerity_resource('aphront-dark-console-css');
 
       $headers = array();
       if (DarkConsoleXHProfPluginAPI::isProfilerStarted()) {
         $headers[DarkConsoleXHProfPluginAPI::getProfilerHeader()] = 'page';
       }
       if (DarkConsoleServicesPlugin::isQueryAnalyzerRequested()) {
         $headers[DarkConsoleServicesPlugin::getQueryAnalyzerHeader()] = true;
       }
 
       Javelin::initBehavior(
         'dark-console',
         $this->getConsoleConfig());
 
       // Change this to initBehavior when there is some behavior to initialize
       require_celerity_resource('javelin-behavior-error-log');
     }
 
     if ($user) {
       $viewer = $user;
     } else {
       $viewer = new PhabricatorUser();
     }
 
     $menu = id(new PhabricatorMainMenuView())
       ->setUser($viewer);
 
     if ($this->getController()) {
       $menu->setController($this->getController());
     }
 
     if ($this->getApplicationMenu()) {
       $menu->setApplicationMenu($this->getApplicationMenu());
     }
 
     $this->menuContent = $menu->render();
   }
 
 
   protected function getHead() {
     $monospaced = null;
 
     $request = $this->getRequest();
     if ($request) {
       $user = $request->getUser();
       if ($user) {
         $monospaced = $user->loadPreferences()->getPreference(
           PhabricatorUserPreferences::PREFERENCE_MONOSPACED);
       }
     }
 
     $response = CelerityAPI::getStaticResourceResponse();
 
     $font_css = null;
     if (!empty($monospaced)) {
       // We can't print this normally because escaping quotation marks will
       // break the CSS. Instead, filter it strictly and then mark it as safe.
       $monospaced = new PhutilSafeHTML(
         PhabricatorUserPreferences::filterMonospacedCSSRule(
           $monospaced));
 
       $font_css = hsprintf(
         '<style type="text/css">'.
         '.PhabricatorMonospaced, '.
         '.phabricator-remarkup .remarkup-code-block '.
           '.remarkup-code { font: %s !important; } '.
         '</style>',
         $monospaced);
     }
 
     return hsprintf(
       '%s%s%s',
       parent::getHead(),
       $font_css,
       $response->renderSingleResource('javelin-magical-init', 'phabricator'));
   }
 
   public function setGlyph($glyph) {
     $this->glyph = $glyph;
     return $this;
   }
 
   public function getGlyph() {
     return $this->glyph;
   }
 
   protected function willSendResponse($response) {
     $request = $this->getRequest();
     $response = parent::willSendResponse($response);
 
     $console = $request->getApplicationConfiguration()->getConsole();
 
     if ($console) {
       $response = PhutilSafeHTML::applyFunction(
         'str_replace',
         hsprintf('<darkconsole />'),
         $console->render($request),
         $response);
     }
 
     return $response;
   }
 
   protected function getBody() {
     $user = null;
     $request = $this->getRequest();
     if ($request) {
       $user = $request->getUser();
     }
 
     $header_chrome = null;
     if ($this->getShowChrome()) {
       $header_chrome = $this->menuContent;
     }
 
     $classes = array();
     $classes[] = 'main-page-frame';
     $developer_warning = null;
     if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode') &&
         DarkConsoleErrorLogPluginAPI::getErrors()) {
       $developer_warning = phutil_tag_div(
         'aphront-developer-error-callout',
         pht(
           'This page raised PHP errors. Find them in DarkConsole '.
           'or the error log.'));
     }
 
     // Render the "you have unresolved setup issues..." warning.
     $setup_warning = null;
     if ($user && $user->getIsAdmin()) {
       $open = PhabricatorSetupCheck::getOpenSetupIssueKeys();
       if ($open) {
         $classes[] = 'page-has-warning';
         $setup_warning = phutil_tag_div(
           'setup-warning-callout',
           phutil_tag(
             'a',
             array(
               'href' => '/config/issue/',
               'title' => implode(', ', $open),
             ),
             pht('You have %d unresolved setup issue(s)...', count($open))));
       }
     }
 
-    if (!$this->isQuicksandBlacklistURI()) {
-      Javelin::initBehavior(
-        'scrollbar',
-        array(
-          'nodeID' => 'phabricator-standard-page',
-          'isMainContent' => true,
-        ));
-    }
-
     $main_page = phutil_tag(
       'div',
       array(
         'id' => 'phabricator-standard-page',
         'class' => 'phabricator-standard-page',
       ),
       array(
         $developer_warning,
         $header_chrome,
         $setup_warning,
         phutil_tag(
           'div',
           array(
             'id' => 'phabricator-standard-page-body',
             'class' => 'phabricator-standard-page-body',
           ),
           $this->renderPageBodyContent()),
       ));
 
     $durable_column = null;
     if ($this->getShowDurableColumn()) {
       $is_visible = $this->getDurableColumnVisible();
       $durable_column = id(new ConpherenceDurableColumnView())
         ->setSelectedConpherence(null)
         ->setUser($user)
         ->setQuicksandConfig($this->buildQuicksandConfig())
         ->setVisible($is_visible)
         ->setInitialLoad(true);
     }
 
     Javelin::initBehavior('quicksand-blacklist', array(
       'patterns' => $this->getQuicksandURIPatternBlacklist(),
     ));
 
     return phutil_tag(
       'div',
       array(
         'class' => implode(' ', $classes),
       ),
       array(
         $main_page,
         $durable_column,
       ));
   }
 
   private function renderPageBodyContent() {
     $console = $this->getConsole();
 
     return array(
       ($console ? hsprintf('<darkconsole />') : null),
       parent::getBody(),
       $this->renderFooter(),
     );
   }
 
   protected function getTail() {
     $request = $this->getRequest();
     $user = $request->getUser();
 
     $tail = array(
       parent::getTail(),
     );
 
     $response = CelerityAPI::getStaticResourceResponse();
 
     if (PhabricatorEnv::getEnvConfig('notification.enabled')) {
       if ($user && $user->isLoggedIn()) {
 
         $client_uri = PhabricatorEnv::getEnvConfig('notification.client-uri');
         $client_uri = new PhutilURI($client_uri);
         if ($client_uri->getDomain() == 'localhost') {
           $this_host = $this->getRequest()->getHost();
           $this_host = new PhutilURI('http://'.$this_host.'/');
           $client_uri->setDomain($this_host->getDomain());
         }
 
         if ($request->isHTTPS()) {
           $client_uri->setProtocol('wss');
         } else {
           $client_uri->setProtocol('ws');
         }
 
         Javelin::initBehavior(
           'aphlict-listen',
           array(
             'websocketURI'  => (string)$client_uri,
           ) + $this->buildAphlictListenConfigData());
       }
     }
 
     $tail[] = $response->renderHTMLFooter();
 
     return $tail;
   }
 
   protected function getBodyClasses() {
     $classes = array();
 
     if (!$this->getShowChrome()) {
       $classes[] = 'phabricator-chromeless-page';
     }
 
     $agent = AphrontRequest::getHTTPHeader('User-Agent');
 
     // Try to guess the device resolution based on UA strings to avoid a flash
     // of incorrectly-styled content.
     $device_guess = 'device-desktop';
     if (preg_match('@iPhone|iPod|(Android.*Chrome/[.0-9]* Mobile)@', $agent)) {
       $device_guess = 'device-phone device';
     } else if (preg_match('@iPad|(Android.*Chrome/)@', $agent)) {
       $device_guess = 'device-tablet device';
     }
 
     $classes[] = $device_guess;
 
     if (preg_match('@Windows@', $agent)) {
       $classes[] = 'platform-windows';
     } else if (preg_match('@Macintosh@', $agent)) {
       $classes[] = 'platform-mac';
     } else if (preg_match('@X11@', $agent)) {
       $classes[] = 'platform-linux';
     }
 
     if ($this->getRequest()->getStr('__print__')) {
       $classes[] = 'printable';
     }
 
     if ($this->getRequest()->getStr('__aural__')) {
       $classes[] = 'audible';
     }
 
     return implode(' ', $classes);
   }
 
   private function getConsole() {
     if ($this->disableConsole) {
       return null;
     }
     return $this->getRequest()->getApplicationConfiguration()->getConsole();
   }
 
   private function getConsoleConfig() {
     $user = $this->getRequest()->getUser();
 
     $headers = array();
     if (DarkConsoleXHProfPluginAPI::isProfilerStarted()) {
       $headers[DarkConsoleXHProfPluginAPI::getProfilerHeader()] = 'page';
     }
     if (DarkConsoleServicesPlugin::isQueryAnalyzerRequested()) {
       $headers[DarkConsoleServicesPlugin::getQueryAnalyzerHeader()] = true;
     }
 
     return array(
       // NOTE: We use a generic label here to prevent input reflection
       // and mitigate compression attacks like BREACH. See discussion in
       // T3684.
       'uri' => pht('Main Request'),
       'selected' => $user ? $user->getConsoleTab() : null,
       'visible'  => $user ? (int)$user->getConsoleVisible() : true,
       'headers' => $headers,
     );
   }
 
   private function getHighSecurityWarningConfig() {
     $user = $this->getRequest()->getUser();
 
     $show = false;
     if ($user->hasSession()) {
       $hisec = ($user->getSession()->getHighSecurityUntil() - time());
       if ($hisec > 0) {
         $show = true;
       }
     }
 
     return array(
       'show' => $show,
       'uri' => '/auth/session/downgrade/',
       'message' => pht(
         'Your session is in high security mode. When you '.
         'finish using it, click here to leave.'),
         );
   }
 
   private function renderFooter() {
     if (!$this->getShowChrome()) {
       return null;
     }
 
     if (!$this->getShowFooter()) {
       return null;
     }
 
     $items = PhabricatorEnv::getEnvConfig('ui.footer-items');
     if (!$items) {
       return null;
     }
 
     $foot = array();
     foreach ($items as $item) {
       $name = idx($item, 'name', pht('Unnamed Footer Item'));
 
       $href = idx($item, 'href');
       if (!PhabricatorEnv::isValidURIForLink($href)) {
         $href = null;
       }
 
       if ($href !== null) {
         $tag = 'a';
       } else {
         $tag = 'span';
       }
 
       $foot[] = phutil_tag(
         $tag,
         array(
           'href' => $href,
         ),
         $name);
     }
     $foot = phutil_implode_html(" \xC2\xB7 ", $foot);
 
     return phutil_tag(
       'div',
       array(
         'class' => 'phabricator-standard-page-footer grouped',
       ),
       $foot);
   }
 
   public function renderForQuicksand(array $extra_config) {
     parent::willRenderPage();
     $response = $this->renderPageBodyContent();
     $response = $this->willSendResponse($response);
 
     return array(
       'content' => hsprintf('%s', $response),
     ) + $this->buildQuicksandConfig()
       + $extra_config;
   }
 
   private function buildQuicksandConfig() {
     $viewer = $this->getRequest()->getUser();
     $controller = $this->getController();
 
     $dropdown_query = id(new AphlictDropdownDataQuery())
       ->setViewer($viewer);
     $dropdown_query->execute();
 
     $rendered_dropdowns = array();
     $applications = array(
       'PhabricatorHelpApplication',
     );
     foreach ($applications as $application_class) {
       if (!PhabricatorApplication::isClassInstalledForViewer(
         $application_class,
         $viewer)) {
         continue;
       }
       $application = PhabricatorApplication::getByClass($application_class);
       $rendered_dropdowns[$application_class] =
         $application->buildMainMenuExtraNodes(
           $viewer,
           $controller);
     }
 
     $hisec_warning_config = $this->getHighSecurityWarningConfig();
 
     $console_config = null;
     $console = $this->getConsole();
     if ($console) {
       $console_config = $this->getConsoleConfig();
     }
 
     $upload_enabled = false;
     if ($controller) {
       $upload_enabled = $controller->isGlobalDragAndDropUploadEnabled();
     }
 
     $application_class = null;
     $application_search_icon = null;
     $controller = $this->getController();
     if ($controller) {
       $application = $controller->getCurrentApplication();
       if ($application) {
         $application_class = get_class($application);
         if ($application->getApplicationSearchDocumentTypes()) {
           $application_search_icon = $application->getFontIcon();
         }
       }
     }
 
     return array(
       'title' => $this->getTitle(),
       'aphlictDropdownData' => array(
         $dropdown_query->getNotificationData(),
         $dropdown_query->getConpherenceData(),
       ),
       'globalDragAndDrop' => $upload_enabled,
       'aphlictDropdowns' => $rendered_dropdowns,
       'hisecWarningConfig' => $hisec_warning_config,
       'consoleConfig' => $console_config,
       'applicationClass' => $application_class,
       'applicationSearchIcon' => $application_search_icon,
     ) + $this->buildAphlictListenConfigData();
   }
 
   private function buildAphlictListenConfigData() {
     $user = $this->getRequest()->getUser();
     $subscriptions = $this->pageObjects;
     $subscriptions[] = $user->getPHID();
 
     return array(
       'pageObjects'   => array_fill_keys($this->pageObjects, true),
       'subscriptions' => $subscriptions,
     );
   }
 
   private function getQuicksandURIPatternBlacklist() {
     $applications = PhabricatorApplication::getAllApplications();
 
     $blacklist = array();
     foreach ($applications as $application) {
       $blacklist[] = $application->getQuicksandURIPatternBlacklist();
     }
 
     return array_mergev($blacklist);
   }
 
   private function getUserPreference($key, $default = null) {
     $request = $this->getRequest();
     if (!$request) {
       return $default;
     }
 
     $user = $request->getUser();
     if (!$user) {
       return $default;
     }
 
     return $user->loadPreferences()->getPreference($key, $default);
   }
 
 }
diff --git a/src/view/phui/PHUIDocumentView.php b/src/view/phui/PHUIDocumentView.php
index 5140d0aa7b..2d4689b8bf 100644
--- a/src/view/phui/PHUIDocumentView.php
+++ b/src/view/phui/PHUIDocumentView.php
@@ -1,227 +1,227 @@
 <?php
 
 final class PHUIDocumentView extends AphrontTagView {
 
   /* For mobile displays, where do you want the sidebar */
   const NAV_BOTTOM = 'nav_bottom';
   const NAV_TOP = 'nav_top';
   const FONT_SOURCE_SANS = 'source-sans';
 
   private $offset;
   private $header;
   private $sidenav;
   private $topnav;
   private $crumbs;
   private $bookname;
   private $bookdescription;
   private $mobileview;
   private $fontKit;
   private $actionListID;
   private $fluid;
 
   public function setOffset($offset) {
     $this->offset = $offset;
     return $this;
   }
 
   public function setHeader(PHUIHeaderView $header) {
     $header->setHeaderColor(PHUIActionHeaderView::HEADER_WHITE);
     $this->header = $header;
     return $this;
   }
 
   public function setSideNav(PHUIListView $list, $display = self::NAV_BOTTOM) {
     $list->setType(PHUIListView::SIDENAV_LIST);
     $this->sidenav = $list;
     $this->mobileview = $display;
     return $this;
   }
 
   public function setTopNav(PHUIListView $list) {
     $list->setType(PHUIListView::NAVBAR_LIST);
     $this->topnav = $list;
     return $this;
   }
 
   public function setCrumbs(PHUIListView $list) {
     $this->crumbs  = $list;
     return $this;
   }
 
   public function setBook($name, $description) {
     $this->bookname = $name;
     $this->bookdescription = $description;
     return $this;
   }
 
   public function setFontKit($kit) {
     $this->fontKit = $kit;
     return $this;
   }
 
   public function setActionListID($id) {
     $this->actionListID = $id;
     return $this;
   }
 
   public function setFluid($fluid) {
     $this->fluid = $fluid;
     return $this;
   }
 
   protected function getTagAttributes() {
     $classes = array();
 
     if ($this->offset) {
       $classes[] = 'phui-document-offset';
-    };
+    }
 
     if ($this->fluid) {
       $classes[] = 'phui-document-fluid';
     }
 
     return array(
       'class' => $classes,
     );
   }
 
   protected function getTagContent() {
     require_celerity_resource('phui-document-view-css');
     if ($this->fontKit) {
       require_celerity_resource('phui-fontkit-css');
     }
 
     switch ($this->fontKit) {
       case self::FONT_SOURCE_SANS:
         require_celerity_resource('font-source-sans-pro');
         break;
     }
 
     $classes = array();
     $classes[] = 'phui-document-view';
     if ($this->offset) {
       $classes[] = 'phui-offset-view';
     }
     if ($this->sidenav) {
       $classes[] = 'phui-sidenav-view';
     }
 
     $sidenav = null;
     if ($this->sidenav) {
       $sidenav = phutil_tag(
         'div',
         array(
           'class' => 'phui-document-sidenav',
         ),
         $this->sidenav);
     }
 
     $book = null;
     if ($this->bookname) {
       $book = phutil_tag(
         'div',
         array(
           'class' => 'phui-document-bookname grouped',
         ),
         array(
           phutil_tag(
             'span',
             array('class' => 'bookname'),
             $this->bookname),
           phutil_tag(
             'span',
             array('class' => 'bookdescription'),
           $this->bookdescription),
         ));
     }
 
     $topnav = null;
     if ($this->topnav) {
       $topnav = phutil_tag(
         'div',
         array(
           'class' => 'phui-document-topnav',
         ),
         $this->topnav);
     }
 
     $crumbs = null;
     if ($this->crumbs) {
       $crumbs = phutil_tag(
         'div',
         array(
           'class' => 'phui-document-crumbs',
         ),
         $this->bookName);
     }
 
     if ($this->fontKit) {
       $main_content = phutil_tag(
         'div',
         array(
           'class' => 'phui-font-'.$this->fontKit,
         ),
         $this->renderChildren());
     } else {
       $main_content = $this->renderChildren();
     }
 
     if ($this->actionListID) {
       $icon_id = celerity_generate_unique_node_id();
       $icon = id(new PHUIIconView())
         ->setIconFont('fa-bars');
       $meta = array(
         'map' => array(
           $this->actionListID => 'phabricator-action-list-toggle',
           $icon_id => 'phuix-dropdown-open',
         ),
       );
       $mobile_menu = id(new PHUIButtonView())
         ->setTag('a')
         ->setText(pht('Actions'))
         ->setHref('#')
         ->setIcon($icon)
         ->addClass('phui-mobile-menu')
         ->setID($icon_id)
         ->addSigil('jx-toggle-class')
         ->setMetadata($meta);
       $this->header->addActionLink($mobile_menu);
     }
 
     $content_inner = phutil_tag(
         'div',
         array(
           'class' => 'phui-document-inner',
         ),
         array(
           $book,
           $this->header,
           $topnav,
           $main_content,
           $crumbs,
         ));
 
     if ($this->mobileview == self::NAV_BOTTOM) {
       $order = array($content_inner, $sidenav);
     } else {
       $order = array($sidenav, $content_inner);
     }
 
     $content = phutil_tag(
         'div',
         array(
           'class' => 'phui-document-content',
         ),
         $order);
 
     $view = phutil_tag(
       'div',
       array(
         'class' => implode(' ', $classes),
       ),
       $content);
 
     return $view;
   }
 
 }
diff --git a/src/view/phui/PHUIHeaderView.php b/src/view/phui/PHUIHeaderView.php
index ad3561d930..14159e500c 100644
--- a/src/view/phui/PHUIHeaderView.php
+++ b/src/view/phui/PHUIHeaderView.php
@@ -1,289 +1,297 @@
 <?php
 
-final class PHUIHeaderView extends AphrontView {
+final class PHUIHeaderView extends AphrontTagView {
 
   const PROPERTY_STATUS = 1;
 
   private $objectName;
   private $header;
   private $tags = array();
   private $image;
   private $imageURL = null;
   private $subheader;
   private $headerColor;
   private $noBackground;
   private $bleedHeader;
   private $properties = array();
   private $actionLinks = array();
   private $buttonBar = null;
   private $policyObject;
   private $epoch;
 
   public function setHeader($header) {
     $this->header = $header;
     return $this;
   }
 
   public function setObjectName($object_name) {
     $this->objectName = $object_name;
     return $this;
   }
 
   public function setNoBackground($nada) {
     $this->noBackground = $nada;
     return $this;
   }
 
   public function addTag(PHUITagView $tag) {
     $this->tags[] = $tag;
     return $this;
   }
 
   public function setImage($uri) {
     $this->image = $uri;
     return $this;
   }
 
   public function setImageURL($url) {
     $this->imageURL = $url;
     return $this;
   }
 
   public function setSubheader($subheader) {
     $this->subheader = $subheader;
     return $this;
   }
 
   public function setBleedHeader($bleed) {
     $this->bleedHeader = $bleed;
     return $this;
   }
 
   public function setHeaderColor($color) {
     $this->headerColor = $color;
     return $this;
   }
 
   public function setPolicyObject(PhabricatorPolicyInterface $object) {
     $this->policyObject = $object;
     return $this;
   }
 
   public function addProperty($property, $value) {
     $this->properties[$property] = $value;
     return $this;
   }
 
   public function addActionLink(PHUIButtonView $button) {
     $this->actionLinks[] = $button;
     return $this;
   }
 
   public function setButtonBar(PHUIButtonBarView $bb) {
     $this->buttonBar = $bb;
     return $this;
   }
 
   public function setStatus($icon, $color, $name) {
     $header_class = 'phui-header-status';
 
     if ($color) {
       $icon = $icon.' '.$color;
       $header_class = $header_class.'-'.$color;
     }
 
     $img = id(new PHUIIconView())
       ->setIconFont($icon);
 
     $tag = phutil_tag(
       'span',
       array(
         'class' => "{$header_class} plr",
       ),
       array(
         $img,
         $name,
       ));
 
     return $this->addProperty(self::PROPERTY_STATUS, $tag);
   }
 
   public function setEpoch($epoch) {
     $age = time() - $epoch;
     $age = floor($age / (60 * 60 * 24));
     if ($age < 1) {
       $when = pht('Today');
     } else if ($age == 1) {
       $when = pht('Yesterday');
     } else {
       $when = pht('%d Days Ago', $age);
     }
 
     $this->setStatus('fa-clock-o bluegrey', null, pht('Updated %s', $when));
     return $this;
   }
 
-  public function render() {
+  protected function getTagName() {
+    return 'div';
+  }
+
+  protected function getTagAttributes() {
     require_celerity_resource('phui-header-view-css');
 
     $classes = array();
     $classes[] = 'phui-header-shell';
 
     if ($this->noBackground) {
       $classes[] = 'phui-header-no-backgound';
     }
 
     if ($this->bleedHeader) {
       $classes[] = 'phui-bleed-header';
     }
 
     if ($this->headerColor) {
       $classes[] = 'sprite-gradient';
       $classes[] = 'gradient-'.$this->headerColor.'-header';
     }
 
     if ($this->properties || $this->policyObject || $this->subheader) {
       $classes[] = 'phui-header-tall';
     }
 
+    if ($this->image) {
+      $classes[] = 'phui-header-has-image';
+    }
+
+    return array(
+      'class' => $classes,
+    );
+  }
+
+  protected function getTagContent() {
     $image = null;
     if ($this->image) {
       $image = phutil_tag(
         ($this->imageURL ? 'a' : 'span'),
         array(
           'href' => $this->imageURL,
           'class' => 'phui-header-image',
           'style' => 'background-image: url('.$this->image.')',
         ),
         ' ');
-      $classes[] = 'phui-header-has-image';
     }
 
     $header = array();
 
     if ($this->actionLinks) {
       $actions = array();
       foreach ($this->actionLinks as $button) {
         $button->setColor(PHUIButtonView::SIMPLE);
         $button->addClass(PHUI::MARGIN_SMALL_LEFT);
         $button->addClass('phui-header-action-link');
         $actions[] = $button;
       }
       $header[] = phutil_tag(
         'div',
         array(
           'class' => 'phui-header-action-links',
         ),
         $actions);
     }
 
     if ($this->buttonBar) {
       $header[] = phutil_tag(
         'div',
         array(
           'class' => 'phui-header-action-links',
         ),
         $this->buttonBar);
     }
     $header[] = $this->header;
 
     if ($this->objectName) {
       array_unshift(
         $header,
         phutil_tag(
           'a',
           array(
             'href' => '/'.$this->objectName,
           ),
           $this->objectName),
         ' ');
     }
 
     if ($this->tags) {
       $header[] = ' ';
       $header[] = phutil_tag(
         'span',
         array(
           'class' => 'phui-header-tags',
         ),
         array_interleave(' ', $this->tags));
     }
 
     if ($this->subheader) {
       $header[] = phutil_tag(
         'div',
         array(
           'class' => 'phui-header-subheader',
         ),
         $this->subheader);
     }
 
     if ($this->properties || $this->policyObject) {
       $property_list = array();
       foreach ($this->properties as $type => $property) {
         switch ($type) {
           case self::PROPERTY_STATUS:
             $property_list[] = $property;
           break;
           default:
             throw new Exception('Incorrect Property Passed');
           break;
         }
       }
 
       if ($this->policyObject) {
         $property_list[] = $this->renderPolicyProperty($this->policyObject);
       }
 
       $header[] = phutil_tag(
         'div',
         array(
           'class' => 'phui-header-subheader',
         ),
         $property_list);
     }
 
-    return phutil_tag(
-      'div',
-      array(
-        'class' => implode(' ', $classes),
-      ),
-      array(
-        $image,
-        phutil_tag(
-          'h1',
-          array(
-            'class' => 'phui-header-view grouped',
-          ),
-          $header),
-      ));
+    return array(
+      $image,
+      phutil_tag(
+        'h1',
+        array(
+          'class' => 'phui-header-view grouped',
+        ),
+        $header),
+      );
   }
 
   private function renderPolicyProperty(PhabricatorPolicyInterface $object) {
     $policies = PhabricatorPolicyQuery::loadPolicies(
       $this->getUser(),
       $object);
 
     $view_capability = PhabricatorPolicyCapability::CAN_VIEW;
     $policy = idx($policies, $view_capability);
     if (!$policy) {
       return null;
     }
 
     $phid = $object->getPHID();
 
     $icon = id(new PHUIIconView())
       ->setIconFont($policy->getIcon().' bluegrey');
 
     $link = javelin_tag(
       'a',
       array(
         'class' => 'policy-link',
         'href' => '/policy/explain/'.$phid.'/'.$view_capability.'/',
         'sigil' => 'workflow',
       ),
       $policy->getShortName());
 
     return array($icon, $link);
   }
 }
diff --git a/src/view/phui/PHUIRemarkupPreviewPanel.php b/src/view/phui/PHUIRemarkupPreviewPanel.php
index 8fa02f983c..9ca2f620f9 100644
--- a/src/view/phui/PHUIRemarkupPreviewPanel.php
+++ b/src/view/phui/PHUIRemarkupPreviewPanel.php
@@ -1,135 +1,138 @@
 <?php
 
 /**
  * Render a simple preview panel for a bound Remarkup text control.
  */
 final class PHUIRemarkupPreviewPanel extends AphrontTagView {
 
   private $header;
   private $loadingText;
   private $controlID;
   private $previewURI;
   private $skin = 'default';
 
   protected function canAppendChild() {
     return false;
   }
 
   public function setPreviewURI($preview_uri) {
     $this->previewURI = $preview_uri;
     return $this;
   }
 
   public function setControlID($control_id) {
     $this->controlID = $control_id;
     return $this;
   }
 
   public function setHeader($header) {
     $this->header = $header;
     return $this;
   }
 
   public function setLoadingText($loading_text) {
     $this->loadingText = $loading_text;
     return $this;
   }
 
   public function setSkin($skin) {
     static $skins = array(
       'default' => true,
       'document' => true,
     );
 
     if (empty($skins[$skin])) {
-      $valid = implode(', ', array_keys($skins));
-      throw new Exception("Invalid skin '{$skin}'. Valid skins are: {$valid}.");
+      throw new Exception(
+        pht(
+          "Invalid skin '%s'. Valid skins are: %s.",
+          $skin,
+          implode(', ', array_keys($skins))));
     }
 
     $this->skin = $skin;
     return $this;
   }
 
   protected function getTagName() {
     return 'div';
   }
 
   protected function getTagAttributes() {
     $classes = array();
     $classes[] = 'phui-remarkup-preview';
 
     if ($this->skin) {
       $classes[] = 'phui-remarkup-preview-skin-'.$this->skin;
     }
 
     return array(
       'class' => $classes,
     );
   }
 
   protected function getTagContent() {
     if ($this->previewURI === null) {
-      throw new Exception('Call setPreviewURI() before rendering!');
+      throw new PhutilInvalidStateException('setPreviewURI');
     }
     if ($this->controlID === null) {
-      throw new Exception('Call setControlID() before rendering!');
+      throw new PhutilInvalidStateException('setControlID');
     }
 
     $preview_id = celerity_generate_unique_node_id();
 
     require_celerity_resource('phui-remarkup-preview-css');
     Javelin::initBehavior(
       'remarkup-preview',
       array(
         'previewID' => $preview_id,
         'controlID' => $this->controlID,
         'uri' => $this->previewURI,
       ));
 
     $loading = phutil_tag(
       'div',
       array(
         'class' => 'phui-preview-loading-text',
       ),
       nonempty($this->loadingText, pht('Loading preview...')));
 
     $header = null;
     if ($this->header) {
       $header = phutil_tag(
         'div',
         array(
           'class' => 'phui-preview-header',
         ),
         $this->header);
     }
 
     $preview = phutil_tag(
       'div',
       array(
         'id' => $preview_id,
         'class' => 'phabricator-remarkup',
       ),
       $loading);
 
     $content = array($header, $preview);
 
     switch ($this->skin) {
       case 'document':
         $content = id(new PHUIDocumentView())
           ->appendChild($content)
           ->setFontKit(PHUIDocumentView::FONT_SOURCE_SANS);
         break;
       default:
         $content = id(new PHUIBoxView())
           ->appendChild($content)
           ->setBorder(true)
           ->addMargin(PHUI::MARGIN_LARGE)
           ->addPadding(PHUI::PADDING_LARGE)
           ->addClass('phui-panel-preview');
         break;
     }
 
     return $content;
   }
 
 }
diff --git a/src/view/phui/calendar/PHUICalendarDayView.php b/src/view/phui/calendar/PHUICalendarDayView.php
index 6d42fdcec3..21c893125d 100644
--- a/src/view/phui/calendar/PHUICalendarDayView.php
+++ b/src/view/phui/calendar/PHUICalendarDayView.php
@@ -1,411 +1,540 @@
 <?php
 
 final class PHUICalendarDayView extends AphrontView {
+  private $rangeStart;
+  private $rangeEnd;
 
   private $day;
   private $month;
   private $year;
   private $browseURI;
   private $events = array();
+  private $todayEvents = array();
+
+  private $allDayEvents = array();
 
   public function addEvent(AphrontCalendarEventView $event) {
     $this->events[] = $event;
     return $this;
   }
 
   public function setBrowseURI($browse_uri) {
     $this->browseURI = $browse_uri;
     return $this;
   }
   private function getBrowseURI() {
     return $this->browseURI;
   }
 
-  public function __construct($year, $month, $day = null) {
+  public function __construct(
+    $range_start,
+    $range_end,
+    $year,
+    $month,
+    $day = null) {
+
+    $this->rangeStart = $range_start;
+    $this->rangeEnd = $range_end;
+
     $this->day = $day;
     $this->month = $month;
     $this->year = $year;
   }
 
   public function render() {
     require_celerity_resource('phui-calendar-day-css');
 
     $hours = $this->getHoursOfDay();
     $hourly_events = array();
-    $rows = array();
 
-    // sort events into buckets by their start time
-    // pretend no events overlap
+    $first_event_hour = null;
+
+    $all_day_events = $this->getAllDayEvents();
+    $today_all_day_events = array();
+
+    $day_start = $this->getDateTime();
+    $day_end = id(clone $day_start)->modify('+1 day');
+
+    $day_start_epoch = $day_start->format('U');
+    $day_end_epoch = $day_end->format('U') - 1;
+
+    foreach ($all_day_events as $all_day_event) {
+      $all_day_start = $all_day_event->getEpochStart();
+      $all_day_end = $all_day_event->getEpochEnd();
+
+      if ($all_day_start < $day_end_epoch && $all_day_end > $day_start_epoch) {
+        $today_all_day_events[] = $all_day_event;
+      }
+    }
+
     foreach ($hours as $hour) {
-      $events = array();
+      $current_hour_events = array();
       $hour_start = $hour->format('U');
       $hour_end = id(clone $hour)->modify('+1 hour')->format('U');
+
       foreach ($this->events as $event) {
-        if ($event->getEpochStart() >= $hour_start
-          && $event->getEpochStart() < $hour_end) {
-          $events[] = $event;
+        if ($event->getIsAllDay()) {
+          continue;
+        }
+        if (($hour == $day_start &&
+          $event->getEpochStart() <= $hour_start &&
+          $event->getEpochEnd() > $day_start_epoch) ||
+          ($event->getEpochStart() >= $hour_start
+          && $event->getEpochStart() < $hour_end)) {
+          $current_hour_events[] = $event;
+          $this->todayEvents[] = $event;
         }
       }
-      $count_events = count($events);
-      $n = 0;
-      foreach ($events as $event) {
-        $event_start = $event->getEpochStart();
-        $event_end = $event->getEpochEnd();
+      foreach ($current_hour_events as $event) {
+        $day_start_epoch = $this->getDateTime()->format('U');
+        $event_start = max($event->getEpochStart(), $day_start_epoch);
+        $event_end = min($event->getEpochEnd(), $day_end_epoch);
+
+        $top = (($event_start - $hour_start) / ($hour_end - $hour_start))
+          * 100;
+        $top = max(0, $top);
 
-        $top = ((($event_start - $hour_start) / ($hour_end - $hour_start))
-          * 100).'%';
-        $height = ((($event_end - $event_start) / ($hour_end - $hour_start))
-          * 100).'%';
+        $height = (($event_end - $event_start) / ($hour_end - $hour_start))
+          * 100;
+        $height = min(2400, $height);
+
+        if ($first_event_hour === null) {
+          $first_event_hour = $hour;
+        }
 
         $hourly_events[$event->getEventID()] = array(
           'hour' => $hour,
           'event' => $event,
           'offset' => '0',
           'width' => '100%',
-          'top' => $top,
-          'height' => $height,
+          'top' => $top.'%',
+          'height' => $height.'%',
         );
-
-        $n++;
       }
     }
 
-    $clusters = $this->findClusters();
+    $clusters = $this->findTodayClusters();
     foreach ($clusters as $cluster) {
       $hourly_events = $this->updateEventsFromCluster(
         $cluster,
         $hourly_events);
     }
 
-    // actually construct table
+    $rows = array();
+
     foreach ($hours as $hour) {
+      $early_hours = array(8);
+      if ($first_event_hour) {
+        $early_hours[] = $first_event_hour->format('G');
+      }
+      if ($hour->format('G') < min($early_hours)) {
+        continue;
+      }
+
       $drawn_hourly_events = array();
       $cell_time = phutil_tag(
         'td',
         array('class' => 'phui-calendar-day-hour'),
         $hour->format('g A'));
 
       foreach ($hourly_events as $hourly_event) {
         if ($hourly_event['hour'] == $hour) {
+
           $drawn_hourly_events[] = $this->drawEvent(
             $hourly_event['event'],
             $hourly_event['offset'],
             $hourly_event['width'],
             $hourly_event['top'],
             $hourly_event['height']);
         }
       }
       $cell_event = phutil_tag(
         'td',
         array('class' => 'phui-calendar-day-events'),
         $drawn_hourly_events);
 
       $row = phutil_tag(
         'tr',
         array(),
         array($cell_time, $cell_event));
 
       $rows[] = $row;
     }
 
     $table = phutil_tag(
       'table',
       array('class' => 'phui-calendar-day-view'),
-      array(
-        '',
-        $rows,
-      ));
+      $rows);
+
+    $all_day_event_box = new PHUIBoxView();
+    foreach ($today_all_day_events as $all_day_event) {
+      $all_day_event_box->appendChild(
+        $this->drawAllDayEvent($all_day_event));
+    }
 
     $header = $this->renderDayViewHeader();
     $sidebar = $this->renderSidebar();
+    $warnings = $this->getQueryRangeWarning();
 
     $table_box = id(new PHUIObjectBoxView())
       ->setHeader($header)
+      ->appendChild($all_day_event_box)
       ->appendChild($table)
+      ->setFormErrors($warnings)
       ->setFlush(true);
 
     $layout = id(new AphrontMultiColumnView())
       ->addColumn($sidebar, 'third')
       ->addColumn($table_box, 'thirds')
       ->setFluidLayout(true)
       ->setGutter(AphrontMultiColumnView::GUTTER_MEDIUM);
 
     return phutil_tag(
       'div',
         array(
           'class' => 'ml',
         ),
         $layout);
   }
 
+  private function getAllDayEvents() {
+    $all_day_events = array();
+
+    foreach ($this->events as $event) {
+      if ($event->getIsAllDay()) {
+        $all_day_events[] = $event;
+      }
+    }
+
+    $all_day_events = array_values(msort($all_day_events, 'getEpochStart'));
+    return $all_day_events;
+  }
+
+  private function getQueryRangeWarning() {
+    $errors = array();
+
+    $range_start_epoch = $this->rangeStart->getEpoch();
+    $range_end_epoch = $this->rangeEnd->getEpoch();
+
+    $day_start = $this->getDateTime();
+    $day_end = id(clone $day_start)->modify('+1 day');
+
+    $day_start = $day_start->format('U');
+    $day_end = $day_end->format('U') - 1;
+
+    if (($range_start_epoch != null &&
+        $range_start_epoch < $day_end &&
+        $range_start_epoch > $day_start) ||
+      ($range_end_epoch != null &&
+        $range_end_epoch < $day_end &&
+        $range_end_epoch > $day_start)) {
+      $errors[] = pht('Part of the day is out of range');
+    }
+
+    if (($this->rangeEnd->getEpoch() != null &&
+        $this->rangeEnd->getEpoch() < $day_start) ||
+      ($this->rangeStart->getEpoch() != null &&
+        $this->rangeStart->getEpoch() > $day_end)) {
+      $errors[] = pht('Day is out of query range');
+    }
+    return $errors;
+  }
+
   private function renderSidebar() {
     $this->events = msort($this->events, 'getEpochStart');
     $week_of_boxes = $this->getWeekOfBoxes();
     $filled_boxes = array();
 
-    foreach ($week_of_boxes as $weekly_box) {
-      $start = $weekly_box['start'];
-      $end = id(clone $start)->modify('+1 day');
+    foreach ($week_of_boxes as $day_box) {
+      $box_start = $day_box['start'];
+      $box_end = id(clone $box_start)->modify('+1 day');
+
+      $box_start = $box_start->format('U');
+      $box_end = $box_end->format('U');
 
       $box_events = array();
 
       foreach ($this->events as $event) {
-        if ($event->getEpochStart() >= $start->format('U') &&
-        $event->getEpochStart() < $end->format('U')) {
+        $event_start = $event->getEpochStart();
+        $event_end = $event->getEpochEnd();
+
+        if ($event_start < $box_end && $event_end > $box_start) {
           $box_events[] = $event;
         }
       }
+
       $filled_boxes[] = $this->renderSidebarBox(
         $box_events,
-        $weekly_box['title']);
+        $day_box['title']);
     }
 
     return $filled_boxes;
   }
 
   private function renderSidebarBox($events, $title) {
-    $widget = new PHUICalendarWidgetView();
+    $widget = id(new PHUICalendarWidgetView())
+      ->addClass('calendar-day-view-sidebar');
 
     $list = id(new PHUICalendarListView())
       ->setUser($this->user);
 
     if (count($events) == 0) {
       $list->showBlankState(true);
     } else {
-      foreach ($events as $event) {
+      $sorted_events = msort($events, 'getEpochStart');
+      foreach ($sorted_events as $event) {
         $list->addEvent($event);
       }
     }
 
     $widget
       ->setCalendarList($list)
       ->setHeader($title);
     return $widget;
   }
 
   private function getWeekOfBoxes() {
     $sidebar_day_boxes = array();
 
     $display_start_day = $this->getDateTime();
     $display_end_day = id(clone $display_start_day)->modify('+6 day');
 
     $box_start_time = clone $display_start_day;
 
     $today_time = PhabricatorTime::getTodayMidnightDateTime($this->user);
     $tomorrow_time = clone $today_time;
     $tomorrow_time->modify('+1 day');
 
     while ($box_start_time <= $display_end_day) {
       if ($box_start_time == $today_time) {
         $title = pht('Today');
       } else if ($box_start_time == $tomorrow_time) {
         $title = pht('Tomorrow');
       } else {
         $title = $box_start_time->format('l');
       }
 
       $sidebar_day_boxes[] = array(
         'title' => $title,
         'start' => clone $box_start_time,
         );
 
       $box_start_time->modify('+1 day');
     }
     return $sidebar_day_boxes;
   }
 
   private function renderDayViewHeader() {
     $button_bar = null;
-
-    // check for a browseURI, which means we need "fancy" prev / next UI
     $uri = $this->getBrowseURI();
     if ($uri) {
       list($prev_year, $prev_month, $prev_day) = $this->getPrevDay();
       $prev_uri = $uri.$prev_year.'/'.$prev_month.'/'.$prev_day.'/';
 
       list($next_year, $next_month, $next_day) = $this->getNextDay();
       $next_uri = $uri.$next_year.'/'.$next_month.'/'.$next_day.'/';
 
       $button_bar = new PHUIButtonBarView();
 
       $left_icon = id(new PHUIIconView())
           ->setIconFont('fa-chevron-left bluegrey');
       $left = id(new PHUIButtonView())
         ->setTag('a')
         ->setColor(PHUIButtonView::GREY)
         ->setHref($prev_uri)
         ->setTitle(pht('Previous Day'))
         ->setIcon($left_icon);
 
       $right_icon = id(new PHUIIconView())
           ->setIconFont('fa-chevron-right bluegrey');
       $right = id(new PHUIButtonView())
         ->setTag('a')
         ->setColor(PHUIButtonView::GREY)
         ->setHref($next_uri)
         ->setTitle(pht('Next Day'))
         ->setIcon($right_icon);
 
       $button_bar->addButton($left);
       $button_bar->addButton($right);
 
     }
 
-    $day_of_week = $this->getDayOfWeek();
-    $header_text = $this->getDateTime()->format('F j, Y');
-    $header_text = $day_of_week.', '.$header_text;
+    $display_day = $this->getDateTime();
+    $header_text = $display_day->format('l, F j, Y');
 
     $header = id(new PHUIHeaderView())
       ->setHeader($header_text);
 
     if ($button_bar) {
       $header->setButtonBar($button_bar);
     }
 
     return $header;
   }
 
   private function updateEventsFromCluster($cluster, $hourly_events) {
     $cluster_size = count($cluster);
-
     $n = 0;
     foreach ($cluster as $cluster_member) {
       $event_id = $cluster_member->getEventID();
       $offset = (($n / $cluster_size) * 100).'%';
       $width = ((1 / $cluster_size) * 100).'%';
 
       if (isset($hourly_events[$event_id])) {
         $hourly_events[$event_id]['offset'] = $offset;
         $hourly_events[$event_id]['width'] = $width;
       }
       $n++;
     }
 
     return $hourly_events;
   }
 
+  private function drawAllDayEvent(AphrontCalendarEventView $event) {
+    $name = phutil_tag(
+      'a',
+      array(
+        'class' => 'day-view-all-day',
+        'href' => $event->getURI(),
+      ),
+      $event->getName());
+
+    $all_day_label = phutil_tag(
+      'span',
+      array(
+        'class' => 'phui-calendar-all-day-label',
+      ),
+      pht('All Day'));
+
+    $div = phutil_tag(
+      'div',
+      array(
+        'class' => 'phui-calendar-day-event',
+      ),
+      array(
+        $all_day_label,
+        $name,
+      ));
+
+    return $div;
+  }
+
   private function drawEvent(
     AphrontCalendarEventView $event,
     $offset,
     $width,
     $top,
     $height) {
     $name = phutil_tag(
       'a',
       array(
         'class' => 'phui-calendar-day-event-link',
         'href' => $event->getURI(),
       ),
       $event->getName());
 
     $div = phutil_tag(
       'div',
       array(
         'class' => 'phui-calendar-day-event',
         'style' => 'left: '.$offset
           .'; width: '.$width
           .'; top: '.$top
           .'; height: '.$height
           .';',
       ),
       $name);
 
     return $div;
   }
 
-  private function getDayOfWeek() {
-    $date = $this->getDateTime();
-    $day_of_week = $date->format('l');
-    return $day_of_week;
-  }
-
   // returns DateTime of each hour in the day
   private function getHoursOfDay() {
     $included_datetimes = array();
 
     $day_datetime = $this->getDateTime();
     $day_epoch = $day_datetime->format('U');
 
     $day_datetime->modify('+1 day');
     $next_day_epoch = $day_datetime->format('U');
 
     $included_time = $day_epoch;
     $included_datetime = $this->getDateTime();
 
     while ($included_time < $next_day_epoch) {
       $included_datetimes[] = clone $included_datetime;
 
       $included_datetime->modify('+1 hour');
       $included_time = $included_datetime->format('U');
     }
 
     return $included_datetimes;
   }
 
   private function getPrevDay() {
     $prev = $this->getDateTime();
     $prev->modify('-1 day');
     return array(
       $prev->format('Y'),
       $prev->format('m'),
       $prev->format('d'),
     );
   }
 
   private function getNextDay() {
     $next = $this->getDateTime();
     $next->modify('+1 day');
     return array(
       $next->format('Y'),
       $next->format('m'),
       $next->format('d'),
     );
   }
 
   private function getDateTime() {
     $user = $this->user;
     $timezone = new DateTimeZone($user->getTimezoneIdentifier());
 
     $day = $this->day;
     $month = $this->month;
     $year = $this->year;
 
     $date = new DateTime("{$year}-{$month}-{$day} ", $timezone);
 
     return $date;
   }
 
-  private function findClusters() {
-    $events = msort($this->events, 'getEpochStart');
+  private function findTodayClusters() {
+    $events = msort($this->todayEvents, 'getEpochStart');
     $clusters = array();
 
     foreach ($events as $event) {
       $destination_cluster_key = null;
-      $event_start = $event->getEpochStart();
-      $event_end = $event->getEpochEnd();
+      $event_start = $event->getEpochStart() - (30 * 60);
+      $event_end = $event->getEpochEnd() + (30 * 60);
 
       foreach ($clusters as $key => $cluster) {
         foreach ($cluster as $clustered_event) {
           $compare_event_start = $clustered_event->getEpochStart();
           $compare_event_end = $clustered_event->getEpochEnd();
 
           if ($event_start < $compare_event_end
             && $event_end > $compare_event_start) {
             $destination_cluster_key = $key;
             break;
           }
         }
       }
 
       if ($destination_cluster_key !== null) {
         $clusters[$destination_cluster_key][] = $event;
       } else {
         $next_cluster = array();
         $next_cluster[] = $event;
         $clusters[] = $next_cluster;
       }
     }
 
     return $clusters;
   }
 }
diff --git a/src/view/phui/calendar/PHUICalendarListView.php b/src/view/phui/calendar/PHUICalendarListView.php
index fd6a7a0e73..fc40d87745 100644
--- a/src/view/phui/calendar/PHUICalendarListView.php
+++ b/src/view/phui/calendar/PHUICalendarListView.php
@@ -1,130 +1,138 @@
 <?php
 
 final class PHUICalendarListView extends AphrontTagView {
 
   private $events = array();
   private $blankState;
 
   public function addEvent(AphrontCalendarEventView $event) {
     $this->events[] = $event;
     return $this;
   }
 
   public function showBlankState($state) {
     $this->blankState = $state;
     return $this;
   }
 
   protected function getTagName() {
     return 'div';
   }
 
   protected function getTagAttributes() {
     require_celerity_resource('phui-calendar-css');
     require_celerity_resource('phui-calendar-list-css');
-    return array('class' => 'phui-calendar-day-list');
+    return array('class' => 'phui-calendar-event-list');
   }
 
   protected function getTagContent() {
     if (!$this->blankState && empty($this->events)) {
       return '';
     }
 
-    $events = msort($this->events, 'getEpochStart');
-
     $singletons = array();
     $allday = false;
-    foreach ($events as $event) {
+    foreach ($this->events as $event) {
       $color = $event->getColor();
+      $start_epoch = $event->getEpochStart();
 
-      if ($event->getAllDay()) {
+      if ($event->getIsAllDay()) {
         $timelabel = pht('All Day');
+        $dot = null;
       } else {
         $timelabel = phabricator_time(
           $event->getEpochStart(),
           $this->getUser());
+
+        $dot = phutil_tag(
+          'span',
+          array(
+            'class' => 'phui-calendar-list-dot',
+          ),
+          '');
       }
 
-      $dot = phutil_tag(
-        'span',
-        array(
-          'class' => 'phui-calendar-list-dot',
-        ),
-        '');
       $title = phutil_tag(
         'span',
         array(
           'class' => 'phui-calendar-list-title',
         ),
         $this->renderEventLink($event, $allday));
       $time = phutil_tag(
         'span',
         array(
           'class' => 'phui-calendar-list-time',
         ),
         $timelabel);
 
+      $class = 'phui-calendar-list-item phui-calendar-'.$color;
+      if ($event->getIsAllDay()) {
+        $class = $class.' all-day';
+      }
+
       $singletons[] = phutil_tag(
         'li',
         array(
-          'class' => 'phui-calendar-list-item phui-calendar-'.$color,
+          'class' => $class,
           ),
         array(
           $dot,
           $title,
           $time,
         ));
     }
 
     if (empty($singletons)) {
       $singletons[] = phutil_tag(
         'li',
         array(
           'class' => 'phui-calendar-list-item-empty',
         ),
         pht('Clear sailing ahead.'));
     }
 
     $list = phutil_tag(
       'ul',
       array(
         'class' => 'phui-calendar-list',
       ),
       $singletons);
 
     return $list;
   }
 
   private function renderEventLink($event) {
 
     Javelin::initBehavior('phabricator-tooltips');
 
     if ($event->getMultiDay()) {
       $tip = pht('%s, Until: %s', $event->getName(),
         phabricator_date($event->getEpochEnd(), $this->getUser()));
     } else {
       $tip = pht('%s, Until: %s', $event->getName(),
         phabricator_time($event->getEpochEnd(), $this->getUser()));
     }
 
     $description = $event->getDescription();
     if (strlen($description) == 0) {
       $description = pht('(%s)', $event->getName());
     }
 
+    $class = 'phui-calendar-item-link';
+
     $anchor = javelin_tag(
       'a',
       array(
         'sigil' => 'has-tooltip',
-        'class' => 'phui-calendar-item-link',
+        'class' => $class,
         'href' => '/E'.$event->getEventID(),
         'meta'  => array(
           'tip'  => $tip,
           'size' => 200,
         ),
       ),
       $event->getName());
 
     return $anchor;
   }
 }
diff --git a/src/view/phui/calendar/PHUICalendarMonthView.php b/src/view/phui/calendar/PHUICalendarMonthView.php
index 089d98144c..7eb35f76a5 100644
--- a/src/view/phui/calendar/PHUICalendarMonthView.php
+++ b/src/view/phui/calendar/PHUICalendarMonthView.php
@@ -1,317 +1,464 @@
 <?php
 
 final class PHUICalendarMonthView extends AphrontView {
+  private $rangeStart;
+  private $rangeEnd;
 
   private $day;
   private $month;
   private $year;
-  private $holidays = array();
   private $events   = array();
   private $browseURI;
   private $image;
   private $error;
 
   public function setBrowseURI($browse_uri) {
     $this->browseURI = $browse_uri;
     return $this;
   }
   private function getBrowseURI() {
     return $this->browseURI;
   }
 
   public function addEvent(AphrontCalendarEventView $event) {
     $this->events[] = $event;
     return $this;
   }
 
   public function setImage($uri) {
     $this->image = $uri;
     return $this;
   }
 
   public function setInfoView(PHUIInfoView $error) {
     $this->error = $error;
     return $this;
   }
 
-  public function setHolidays(array $holidays) {
-    assert_instances_of($holidays, 'PhabricatorCalendarHoliday');
-    $this->holidays = mpull($holidays, null, 'getDay');
-    return $this;
-  }
+  public function __construct(
+    $range_start,
+    $range_end,
+    $month,
+    $year,
+    $day = null) {
+
+    $this->rangeStart = $range_start;
+    $this->rangeEnd = $range_end;
 
-  public function __construct($month, $year, $day = null) {
     $this->day = $day;
     $this->month = $month;
     $this->year = $year;
   }
 
   public function render() {
     if (empty($this->user)) {
-      throw new Exception('Call setUser() before render()!');
+      throw new PhutilInvalidStateException('setUser');
     }
 
     $events = msort($this->events, 'getEpochStart');
-
     $days = $this->getDatesInMonth();
 
+    $cell_lists = array();
+    $empty_cell = array(
+        'list' => null,
+        'date' => null,
+        'uri' => null,
+        'count' => 0,
+        'class' => null,
+      );
+
     require_celerity_resource('phui-calendar-month-css');
 
     $first = reset($days);
-    $empty = $first->format('w');
-
-    $markup = array();
+    $start_of_week = 0;
 
-    $empty_box = phutil_tag(
-      'div',
-      array('class' => 'phui-calendar-day phui-calendar-empty'),
-      '');
+    $empty = $first->format('w');
 
     for ($ii = 0; $ii < $empty; $ii++) {
-      $markup[] = $empty_box;
+      $cell_lists[] = $empty_cell;
     }
 
-    $show_events = array();
-
     foreach ($days as $day) {
       $day_number = $day->format('j');
 
-      $holiday = idx($this->holidays, $day->format('Y-m-d'));
-      $class = 'phui-calendar-day';
+      $class = 'phui-calendar-month-day';
       $weekday = $day->format('w');
 
-      if ($day_number == $this->day) {
-        $class .= ' phui-calendar-today';
-      }
-
-      if ($holiday || $weekday == 0 || $weekday == 6) {
-        $class .= ' phui-calendar-not-work-day';
-      }
-
       $day->setTime(0, 0, 0);
-      $epoch_start = $day->format('U');
-
-      $day->modify('+1 day');
-      $epoch_end = $day->format('U');
-
-      if ($weekday == 0) {
-        $show_events = array();
-      } else {
-        $show_events = array_fill_keys(
-          array_keys($show_events),
-          phutil_tag_div(
-            'phui-calendar-event phui-calendar-event-empty',
-            "\xC2\xA0")); // &nbsp;
-      }
+      $day_start_epoch = $day->format('U');
+      $day_end_epoch = id(clone $day)->modify('+1 day')->format('U');
 
       $list_events = array();
+      $all_day_events = array();
+
       foreach ($events as $event) {
-        if ($event->getEpochStart() >= $epoch_end) {
-          // This list is sorted, so we can stop looking.
+        if ($event->getEpochStart() >= $day_end_epoch) {
           break;
         }
-        if ($event->getEpochStart() < $epoch_end &&
-            $event->getEpochEnd() > $epoch_start) {
-          $list_events[] = $event;
+        if ($event->getEpochStart() < $day_end_epoch &&
+            $event->getEpochEnd() > $day_start_epoch) {
+          if ($event->getIsAllDay()) {
+            $all_day_events[] = $event;
+          } else {
+            $list_events[] = $event;
+          }
         }
       }
 
       $list = new PHUICalendarListView();
       $list->setUser($this->user);
-      foreach ($list_events as $item) {
+      foreach ($all_day_events as $item) {
         $list->addEvent($item);
       }
-
-      $holiday_markup = null;
-      if ($holiday) {
-        $name = $holiday->getName();
-        $holiday_markup = phutil_tag(
-          'div',
-          array(
-            'class' => 'phui-calendar-holiday',
-            'title' => $name,
-          ),
-          $name);
+      foreach ($list_events as $item) {
+        $list->addEvent($item);
       }
 
-      $markup[] = phutil_tag_div(
-        $class,
-        array(
-          phutil_tag_div('phui-calendar-date-number', $day_number),
-          $holiday_markup,
-          $list,
-        ));
+      $uri = $this->getBrowseURI();
+      $uri = $uri.$day->format('Y').'/'.
+        $day->format('m').'/'.
+        $day->format('d').'/';
+
+      $cell_lists[] = array(
+        'list' => $list,
+        'date' => $day,
+        'uri' => $uri,
+        'count' => count($all_day_events) + count($list_events),
+        'class' => $class,
+        );
     }
 
-    $table = array();
-    $rows = array_chunk($markup, 7);
-    foreach ($rows as $row) {
+    $rows = array();
+    $cell_lists_by_week = array_chunk($cell_lists, 7);
+
+    foreach ($cell_lists_by_week as $week_of_cell_lists) {
       $cells = array();
-      while (count($row) < 7) {
-        $row[] = $empty_box;
+      while (count($week_of_cell_lists) < 7) {
+        $week_of_cell_lists[] = $empty_cell;
       }
-      $j = 0;
-      foreach ($row as $cell) {
-        if ($j == 0) {
-          $cells[] = phutil_tag(
-            'td',
-            array(
-              'class' => 'phui-calendar-month-weekstart',
-            ),
-            $cell);
-        } else {
-          $cells[] = phutil_tag('td', array(), $cell);
-        }
-        $j++;
+      foreach ($week_of_cell_lists as $cell_list) {
+        $cells[] = $this->getEventListCell($cell_list);
       }
-      $table[] = phutil_tag('tr', array(), $cells);
+      $rows[] = phutil_tag('tr', array(), $cells);
+
+      $cells = array();
+      foreach ($week_of_cell_lists as $cell_list) {
+        $cells[] = $this->getDayNumberCell($cell_list);
+      }
+      $rows[] = phutil_tag('tr', array(), $cells);
     }
 
-    $header = phutil_tag(
-      'tr',
-      array('class' => 'phui-calendar-day-of-week-header'),
-      array(
-        phutil_tag('th', array(), pht('Sun')),
-        phutil_tag('th', array(), pht('Mon')),
-        phutil_tag('th', array(), pht('Tue')),
-        phutil_tag('th', array(), pht('Wed')),
-        phutil_tag('th', array(), pht('Thu')),
-        phutil_tag('th', array(), pht('Fri')),
-        phutil_tag('th', array(), pht('Sat')),
-      ));
+    $header = $this->getDayNamesHeader();
 
     $table = phutil_tag(
       'table',
       array('class' => 'phui-calendar-view'),
       array(
         $header,
-        phutil_implode_html("\n", $table),
+        $rows,
       ));
 
+    $warnings = $this->getQueryRangeWarning();
+
     $box = id(new PHUIObjectBoxView())
       ->setHeader($this->renderCalendarHeader($first))
-      ->appendChild($table);
+      ->appendChild($table)
+      ->setFormErrors($warnings);
     if ($this->error) {
       $box->setInfoView($this->error);
 
     }
 
     return $box;
   }
 
+  private function getEventListCell($event_list) {
+    $list = $event_list['list'];
+    $class = $event_list['class'];
+    $uri = $event_list['uri'];
+    $count = $event_list['count'];
+
+    $event_count_badge = $this->getEventCountBadge($count);
+    $cell_day_secret_link = $this->getHiddenDayLink($uri);
+
+    $cell_data_div = phutil_tag(
+      'div',
+      array(
+        'class' => 'phui-calendar-month-cell-div',
+      ),
+      array(
+        $cell_day_secret_link,
+        $event_count_badge,
+        $list,
+      ));
+
+    return phutil_tag(
+      'td',
+      array(
+        'class' => 'phui-calendar-month-event-list '.$class,
+      ),
+      $cell_data_div);
+  }
+
+  private function getDayNumberCell($event_list) {
+    $class = $event_list['class'];
+    $date = $event_list['date'];
+    $cell_day_secret_link = null;
+
+    if ($date) {
+      $uri = $event_list['uri'];
+      $cell_day_secret_link = $this->getHiddenDayLink($uri);
+
+      $cell_day = phutil_tag(
+        'a',
+        array(
+          'class' => 'phui-calendar-date-number',
+          'href' => $uri,
+        ),
+        $date->format('j'));
+    } else {
+      $cell_day = null;
+    }
+
+    if ($date && $date->format('j') == $this->day) {
+      $today_class = 'phui-calendar-today-slot phui-calendar-today';
+    } else {
+      $today_class = 'phui-calendar-today-slot';
+    }
+
+    $today_slot = phutil_tag (
+      'div',
+      array(
+        'class' => $today_class,
+      ),
+      null);
+
+    $cell_div = phutil_tag(
+      'div',
+      array(
+        'class' => 'phui-calendar-month-cell-div',
+      ),
+      array(
+        $cell_day_secret_link,
+        $cell_day,
+        $today_slot,
+      ));
+
+    return phutil_tag(
+      'td',
+      array(
+        'class' => 'phui-calendar-date-number-container '.$class,
+      ),
+      $cell_div);
+  }
+
+  private function getEventCountBadge($count) {
+    $event_count = null;
+    if ($count > 0) {
+      $event_count = phutil_tag(
+        'div',
+        array(
+          'class' => 'phui-calendar-month-count-badge',
+        ),
+        $count);
+    }
+
+    return phutil_tag(
+      'div',
+      array(
+        'class' => 'phui-calendar-month-event-count',
+      ),
+      $event_count);
+  }
+
+  private function getHiddenDayLink($uri) {
+    return phutil_tag(
+      'a',
+      array(
+        'class' => 'phui-calendar-month-secret-link',
+        'href' => $uri,
+      ),
+      null);
+  }
+
+  private function getDayNamesHeader() {
+    return phutil_tag(
+      'tr',
+      array('class' => 'phui-calendar-day-of-week-header'),
+      array(
+        phutil_tag('th', array(), pht('Sun')),
+        phutil_tag('th', array(), pht('Mon')),
+        phutil_tag('th', array(), pht('Tue')),
+        phutil_tag('th', array(), pht('Wed')),
+        phutil_tag('th', array(), pht('Thu')),
+        phutil_tag('th', array(), pht('Fri')),
+        phutil_tag('th', array(), pht('Sat')),
+      ));
+  }
+
   private function renderCalendarHeader(DateTime $date) {
     $button_bar = null;
 
     // check for a browseURI, which means we need "fancy" prev / next UI
     $uri = $this->getBrowseURI();
     if ($uri) {
       list($prev_year, $prev_month) = $this->getPrevYearAndMonth();
       $prev_uri = $uri.$prev_year.'/'.$prev_month.'/';
 
       list($next_year, $next_month) = $this->getNextYearAndMonth();
       $next_uri = $uri.$next_year.'/'.$next_month.'/';
 
       $button_bar = new PHUIButtonBarView();
 
       $left_icon = id(new PHUIIconView())
           ->setIconFont('fa-chevron-left bluegrey');
       $left = id(new PHUIButtonView())
         ->setTag('a')
         ->setColor(PHUIButtonView::GREY)
         ->setHref($prev_uri)
         ->setTitle(pht('Previous Month'))
         ->setIcon($left_icon);
 
       $right_icon = id(new PHUIIconView())
           ->setIconFont('fa-chevron-right bluegrey');
       $right = id(new PHUIButtonView())
         ->setTag('a')
         ->setColor(PHUIButtonView::GREY)
         ->setHref($next_uri)
         ->setTitle(pht('Next Month'))
         ->setIcon($right_icon);
 
       $button_bar->addButton($left);
       $button_bar->addButton($right);
 
     }
 
     $header = id(new PHUIHeaderView())
       ->setHeader($date->format('F Y'));
 
     if ($button_bar) {
       $header->setButtonBar($button_bar);
     }
 
     if ($this->image) {
       $header->setImage($this->image);
     }
 
     return $header;
   }
 
+  private function getQueryRangeWarning() {
+    $errors = array();
+
+    $range_start_epoch = $this->rangeStart->getEpoch();
+    $range_end_epoch = $this->rangeEnd->getEpoch();
+
+    $month_start = $this->getDateTime();
+    $month_end = id(clone $month_start)->modify('+1 month');
+
+    $month_start = $month_start->format('U');
+    $month_end = $month_end->format('U') - 1;
+
+    if (($range_start_epoch != null &&
+        $range_start_epoch < $month_end &&
+        $range_start_epoch > $month_start) ||
+      ($range_end_epoch != null &&
+        $range_end_epoch < $month_end &&
+        $range_end_epoch > $month_start)) {
+      $errors[] = pht('Part of the month is out of range');
+    }
+
+    if (($this->rangeEnd->getEpoch() != null &&
+        $this->rangeEnd->getEpoch() < $month_start) ||
+      ($this->rangeStart->getEpoch() != null &&
+        $this->rangeStart->getEpoch() > $month_end)) {
+      $errors[] = pht('Month is out of query range');
+    }
+
+    return $errors;
+  }
+
   private function getNextYearAndMonth() {
     $next = $this->getDateTime();
     $next->modify('+1 month');
     return array(
       $next->format('Y'),
       $next->format('m'),
     );
   }
 
   private function getPrevYearAndMonth() {
     $prev = $this->getDateTime();
     $prev->modify('-1 month');
     return array(
       $prev->format('Y'),
       $prev->format('m'),
     );
   }
 
   /**
    * Return a DateTime object representing the first moment in each day in the
    * month, according to the user's locale.
    *
    * @return list List of DateTimes, one for each day.
    */
   private function getDatesInMonth() {
     $user = $this->user;
 
     $timezone = new DateTimeZone($user->getTimezoneIdentifier());
 
     $month = $this->month;
     $year = $this->year;
 
-    // Get the year and month numbers of the following month, so we can
-    // determine when this month ends.
     list($next_year, $next_month) = $this->getNextYearAndMonth();
-
     $end_date = new DateTime("{$next_year}-{$next_month}-01", $timezone);
-    $end_epoch = $end_date->format('U');
+
+    $start_of_week = 0;
+    $end_of_week = 6 - $start_of_week;
+    $days_in_month = id(clone $end_date)->modify('-1 day')->format('d');
+
+    $first_month_day_date = new DateTime("{$year}-{$month}-01", $timezone);
+    $last_month_day_date = id(clone $end_date)->modify('-1 day');
+
+    $first_weekday_of_month = $first_month_day_date->format('w');
+    $last_weekday_of_month = $last_month_day_date->format('w');
+
+    $num_days_display = $days_in_month;
+    if ($start_of_week < $first_weekday_of_month) {
+      $num_days_display += $first_weekday_of_month;
+    }
+    if ($end_of_week > $last_weekday_of_month) {
+      $num_days_display += (6 - $last_weekday_of_month);
+      $end_date->modify('+'.(6 - $last_weekday_of_month).' days');
+    }
 
     $days = array();
-    for ($day = 1; $day <= 31; $day++) {
-      $day_date = new DateTime("{$year}-{$month}-{$day}", $timezone);
+    $day_date = id(clone $first_month_day_date)
+      ->modify('-'.$first_weekday_of_month.' days');
+
+    for ($day = 1; $day <= $num_days_display; $day++) {
       $day_epoch = $day_date->format('U');
+      $end_epoch = $end_date->format('U');
       if ($day_epoch >= $end_epoch) {
         break;
       } else {
-        $days[] = $day_date;
+        $days[] = clone $day_date;
       }
+      $day_date->modify('+1 day');
     }
 
     return $days;
   }
 
   private function getDateTime() {
     $user = $this->user;
     $timezone = new DateTimeZone($user->getTimezoneIdentifier());
 
     $month = $this->month;
     $year = $this->year;
 
     $date = new DateTime("{$year}-{$month}-01 ", $timezone);
 
     return $date;
   }
 }
diff --git a/src/view/widget/hovercard/PhabricatorHovercardView.php b/src/view/widget/hovercard/PhabricatorHovercardView.php
index 40cb6fa30e..58131a6a6b 100644
--- a/src/view/widget/hovercard/PhabricatorHovercardView.php
+++ b/src/view/widget/hovercard/PhabricatorHovercardView.php
@@ -1,171 +1,171 @@
 <?php
 
 /**
  * The default one-for-all hovercard. We may derive from this one to create
  * more specialized ones
  */
 final class PhabricatorHovercardView extends AphrontView {
 
   /**
    * @var PhabricatorObjectHandle
    */
   private $handle;
 
   private $title = array();
   private $detail;
   private $tags = array();
   private $fields = array();
   private $actions = array();
 
   private $color = 'lightblue';
   public function setObjectHandle(PhabricatorObjectHandle $handle) {
     $this->handle = $handle;
     return $this;
   }
 
   public function setTitle($title) {
     $this->title = $title;
     return $this;
   }
 
   public function setDetail($detail) {
     $this->detail = $detail;
     return $this;
   }
 
   public function addField($label, $value) {
     $this->fields[] = array(
       'label' => $label,
       'value' => $value,
     );
     return $this;
   }
 
   public function addAction($label, $uri, $workflow = false) {
     $this->actions[] = array(
       'label'    => $label,
       'uri'      => $uri,
       'workflow' => $workflow,
     );
     return $this;
   }
 
   public function addTag(PHUITagView $tag) {
     $this->tags[] = $tag;
     return $this;
   }
 
   public function setColor($color) {
     $this->color = $color;
     return $this;
   }
 
   public function render() {
     if (!$this->handle) {
-      throw new Exception('Call setObjectHandle() before calling render()!');
+      throw new PhutilInvalidStateException('setObjectHandle');
     }
 
     $handle = $this->handle;
 
     require_celerity_resource('phabricator-hovercard-view-css');
 
     $title = pht('%s: %s',
       $handle->getTypeName(),
       $this->title ? $this->title : $handle->getName());
 
     $header = new PHUIActionHeaderView();
     $header->setHeaderColor($this->color);
     $header->setHeaderTitle($title);
     if ($this->tags) {
       foreach ($this->tags as $tag) {
         $header->setTag($tag);
       }
     }
 
     $body = array();
 
     if ($this->detail) {
       $body_title = $this->detail;
     } else {
       // Fallback for object handles
       $body_title = $handle->getFullName();
     }
 
     $body[] = phutil_tag_div('phabricator-hovercard-body-header', $body_title);
 
     foreach ($this->fields as $field) {
       $item = array(
         phutil_tag('strong', array(), $field['label']),
         ' ',
         phutil_tag('span', array(), $field['value']),
       );
       $body[] = phutil_tag_div('phabricator-hovercard-body-item', $item);
     }
 
     if ($handle->getImageURI()) {
       // Probably a user, we don't need to assume something else
       // "Prepend" the image by appending $body
       $body = phutil_tag(
         'div',
         array(
           'class' => 'phabricator-hovercard-body-image',
         ),
         phutil_tag(
           'div',
           array(
             'class' => 'profile-header-picture-frame',
             'style' => 'background-image: url('.$handle->getImageURI().');',
           ),
           ''))
       ->appendHTML(
         phutil_tag(
           'div',
           array(
             'class' => 'phabricator-hovercard-body-details',
           ),
           $body));
     }
 
     $buttons = array();
 
     foreach ($this->actions as $action) {
       $options = array(
         'class' => 'button grey',
         'href'  => $action['uri'],
       );
 
       if ($action['workflow']) {
         $options['sigil'] = 'workflow';
         $buttons[] = javelin_tag(
           'a',
           $options,
           $action['label']);
       } else {
         $buttons[] = phutil_tag(
           'a',
           $options,
           $action['label']);
       }
     }
 
     $tail = null;
     if ($buttons) {
       $tail = phutil_tag_div('phabricator-hovercard-tail', $buttons);
     }
 
     // Assemble container
     // TODO: Add color support
     $hovercard = phutil_tag_div(
       'phabricator-hovercard-container',
       array(
         phutil_tag_div('phabricator-hovercard-head', $header),
         phutil_tag_div('phabricator-hovercard-body grouped', $body),
         $tail,
       ));
 
     // Wrap for thick border
     // and later the tip at the bottom
     return phutil_tag_div('phabricator-hovercard-wrapper', $hovercard);
   }
 
 }
diff --git a/support/PhabricatorStartup.php b/support/PhabricatorStartup.php
index 3945049ffb..30e8f3c937 100644
--- a/support/PhabricatorStartup.php
+++ b/support/PhabricatorStartup.php
@@ -1,885 +1,885 @@
 <?php
 
 /**
  * Handle request startup, before loading the environment or libraries. This
  * class bootstraps the request state up to the point where we can enter
  * Phabricator code.
  *
  * NOTE: This class MUST NOT have any dependencies. It runs before libraries
  * load.
  *
  * Rate Limiting
  * =============
  *
  * Phabricator limits the rate at which clients can request pages, and issues
  * HTTP 429 "Too Many Requests" responses if clients request too many pages too
  * quickly. Although this is not a complete defense against high-volume attacks,
  * it can  protect an install against aggressive crawlers, security scanners,
  * and some types of malicious activity.
  *
  * To perform rate limiting, each page increments a score counter for the
  * requesting user's IP. The page can give the IP more points for an expensive
  * request, or fewer for an authetnicated request.
  *
  * Score counters are kept in buckets, and writes move to a new bucket every
  * minute. After a few minutes (defined by @{method:getRateLimitBucketCount}),
  * the oldest bucket is discarded. This provides a simple mechanism for keeping
  * track of scores without needing to store, access, or read very much data.
  *
  * Users are allowed to accumulate up to 1000 points per minute, averaged across
  * all of the tracked buckets.
  *
  * @task info         Accessing Request Information
  * @task hook         Startup Hooks
  * @task apocalypse   In Case Of Apocalypse
  * @task validation   Validation
  * @task ratelimit    Rate Limiting
  */
 final class PhabricatorStartup {
 
   private static $startTime;
   private static $debugTimeLimit;
   private static $globals = array();
   private static $capturingOutput;
   private static $rawInput;
   private static $oldMemoryLimit;
 
   // TODO: For now, disable rate limiting entirely by default. We need to
   // iterate on it a bit for Conduit, some of the specific score levels, and
   // to deal with NAT'd offices.
   private static $maximumRate = 0;
 
 
 /* -(  Accessing Request Information  )-------------------------------------- */
 
 
   /**
    * @task info
    */
   public static function getStartTime() {
     return self::$startTime;
   }
 
 
   /**
    * @task info
    */
   public static function getMicrosecondsSinceStart() {
     return (int)(1000000 * (microtime(true) - self::getStartTime()));
   }
 
 
   /**
    * @task info
    */
   public static function setGlobal($key, $value) {
     self::validateGlobal($key);
 
     self::$globals[$key] = $value;
   }
 
 
   /**
    * @task info
    */
   public static function getGlobal($key, $default = null) {
     self::validateGlobal($key);
 
     if (!array_key_exists($key, self::$globals)) {
       return $default;
     }
 
     return self::$globals[$key];
   }
 
   /**
    * @task info
    */
   public static function getRawInput() {
     return self::$rawInput;
   }
 
 
 /* -(  Startup Hooks  )------------------------------------------------------ */
 
 
   /**
    * @task hook
    */
   public static function didStartup() {
     self::$startTime = microtime(true);
     self::$globals = array();
 
     static $registered;
     if (!$registered) {
       // NOTE: This protects us against multiple calls to didStartup() in the
       // same request, but also against repeated requests to the same
       // interpreter state, which we may implement in the future.
       register_shutdown_function(array(__CLASS__, 'didShutdown'));
       $registered = true;
     }
 
     self::setupPHP();
     self::verifyPHP();
 
     // If we've made it this far, the environment isn't completely broken so
     // we can switch over to relying on our own exception recovery mechanisms.
     ini_set('display_errors', 0);
 
     if (isset($_SERVER['REMOTE_ADDR'])) {
       self::rateLimitRequest($_SERVER['REMOTE_ADDR']);
     }
 
     self::normalizeInput();
 
     self::verifyRewriteRules();
 
     self::detectPostMaxSizeTriggered();
 
     self::beginOutputCapture();
 
     self::$rawInput = (string)file_get_contents('php://input');
   }
 
 
   /**
    * @task hook
    */
   public static function didShutdown() {
     $event = error_get_last();
 
     if (!$event) {
       return;
     }
 
     switch ($event['type']) {
       case E_ERROR:
       case E_PARSE:
       case E_COMPILE_ERROR:
         break;
       default:
         return;
     }
 
     $msg = ">>> UNRECOVERABLE FATAL ERROR <<<\n\n";
     if ($event) {
       // Even though we should be emitting this as text-plain, escape things
       // just to be sure since we can't really be sure what the program state
       // is when we get here.
       $msg .= htmlspecialchars(
         $event['message']."\n\n".$event['file'].':'.$event['line'],
         ENT_QUOTES,
         'UTF-8');
     }
 
     // flip dem tables
     $msg .= "\n\n\n";
     $msg .= "\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb\x20\xef\xb8\xb5\x20\xc2\xaf".
             "\x5c\x5f\x28\xe3\x83\x84\x29\x5f\x2f\xc2\xaf\x20\xef\xb8\xb5\x20".
             "\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb";
 
     self::didFatal($msg);
   }
 
   public static function loadCoreLibraries() {
     $phabricator_root = dirname(dirname(__FILE__));
     $libraries_root = dirname($phabricator_root);
 
     $root = null;
     if (!empty($_SERVER['PHUTIL_LIBRARY_ROOT'])) {
       $root = $_SERVER['PHUTIL_LIBRARY_ROOT'];
     }
 
     ini_set(
       'include_path',
       $libraries_root.PATH_SEPARATOR.ini_get('include_path'));
 
     @include_once $root.'libphutil/src/__phutil_library_init__.php';
     if (!@constant('__LIBPHUTIL__')) {
       self::didFatal(
         "Unable to load libphutil. Put libphutil/ next to phabricator/, or ".
         "update your PHP 'include_path' to include the parent directory of ".
         "libphutil/.");
     }
 
     phutil_load_library('arcanist/src');
 
     // Load Phabricator itself using the absolute path, so we never end up doing
     // anything surprising (loading index.php and libraries from different
     // directories).
     phutil_load_library($phabricator_root.'/src');
   }
 
 /* -(  Output Capture  )----------------------------------------------------- */
 
 
   public static function beginOutputCapture() {
     if (self::$capturingOutput) {
       self::didFatal('Already capturing output!');
     }
     self::$capturingOutput = true;
     ob_start();
   }
 
 
   public static function endOutputCapture() {
     if (!self::$capturingOutput) {
       return null;
     }
     self::$capturingOutput = false;
     return ob_get_clean();
   }
 
 
 /* -(  Debug Time Limit  )--------------------------------------------------- */
 
 
   /**
    * Set a time limit (in seconds) for the current script. After time expires,
    * the script fatals.
    *
    * This works like `max_execution_time`, but prints out a useful stack trace
    * when the time limit expires. This is primarily intended to make it easier
    * to debug pages which hang by allowing extraction of a stack trace: set a
    * short debug limit, then use the trace to figure out what's happening.
    *
    * The limit is implemented with a tick function, so enabling it implies
    * some accounting overhead.
    *
    * @param int Time limit in seconds.
    * @return void
    */
   public static function setDebugTimeLimit($limit) {
     self::$debugTimeLimit = $limit;
 
     static $initialized;
     if (!$initialized) {
       declare(ticks=1);
-      register_tick_function(array('PhabricatorStartup', 'onDebugTick'));
+      register_tick_function(array(__CLASS__, 'onDebugTick'));
     }
   }
 
 
   /**
    * Callback tick function used by @{method:setDebugTimeLimit}.
    *
    * Fatals with a useful stack trace after the time limit expires.
    *
    * @return void
    */
   public static function onDebugTick() {
     $limit = self::$debugTimeLimit;
     if (!$limit) {
       return;
     }
 
     $elapsed = (microtime(true) - self::getStartTime());
     if ($elapsed > $limit) {
       $frames = array();
       foreach (debug_backtrace() as $frame) {
         $file = isset($frame['file']) ? $frame['file'] : '-';
         $file = basename($file);
 
         $line = isset($frame['line']) ? $frame['line'] : '-';
         $class = isset($frame['class']) ? $frame['class'].'->' : null;
         $func = isset($frame['function']) ? $frame['function'].'()' : '?';
 
         $frames[] = "{$file}:{$line} {$class}{$func}";
       }
 
       self::didFatal(
         "Request aborted by debug time limit after {$limit} seconds.\n\n".
         "STACK TRACE\n".
         implode("\n", $frames));
     }
   }
 
 
 /* -(  In Case of Apocalypse  )---------------------------------------------- */
 
 
   /**
    * Fatal the request completely in response to an exception, sending a plain
    * text message to the client. Calls @{method:didFatal} internally.
    *
    * @param   string    Brief description of the exception context, like
    *                    `"Rendering Exception"`.
    * @param   Exception The exception itself.
    * @param   bool      True if it's okay to show the exception's stack trace
    *                    to the user. The trace will always be logged.
    * @return  exit      This method **does not return**.
    *
    * @task apocalypse
    */
   public static function didEncounterFatalException(
     $note,
     Exception $ex,
     $show_trace) {
 
     $message = '['.$note.'/'.get_class($ex).'] '.$ex->getMessage();
 
     $full_message = $message;
     $full_message .= "\n\n";
     $full_message .= $ex->getTraceAsString();
 
     if ($show_trace) {
       $message = $full_message;
     }
 
     self::didFatal($message, $full_message);
   }
 
 
   /**
    * Fatal the request completely, sending a plain text message to the client.
    *
    * @param   string  Plain text message to send to the client.
    * @param   string  Plain text message to send to the error log. If not
    *                  provided, the client message is used. You can pass a more
    *                  detailed message here (e.g., with stack traces) to avoid
    *                  showing it to users.
    * @return  exit    This method **does not return**.
    *
    * @task apocalypse
    */
   public static function didFatal($message, $log_message = null) {
     if ($log_message === null) {
       $log_message = $message;
     }
 
     self::endOutputCapture();
     $access_log = self::getGlobal('log.access');
 
     if ($access_log) {
       // We may end up here before the access log is initialized, e.g. from
       // verifyPHP().
       $access_log->setData(
         array(
           'c' => 500,
         ));
       $access_log->write();
     }
 
     header(
       'Content-Type: text/plain; charset=utf-8',
       $replace = true,
       $http_error = 500);
 
     error_log($log_message);
     echo $message;
 
     exit(1);
   }
 
 
 /* -(  Validation  )--------------------------------------------------------- */
 
 
   /**
    * @task validation
    */
   private static function setupPHP() {
     error_reporting(E_ALL | E_STRICT);
     self::$oldMemoryLimit = ini_get('memory_limit');
     ini_set('memory_limit', -1);
 
     // If we have libxml, disable the incredibly dangerous entity loader.
     if (function_exists('libxml_disable_entity_loader')) {
       libxml_disable_entity_loader(true);
     }
   }
 
 
   /**
    * @task validation
    */
   public static function getOldMemoryLimit() {
     return self::$oldMemoryLimit;
   }
 
   /**
    * @task validation
    */
   private static function normalizeInput() {
     // Replace superglobals with unfiltered versions, disrespect php.ini (we
     // filter ourselves)
     $filter = array(INPUT_GET, INPUT_POST,
       INPUT_SERVER, INPUT_ENV, INPUT_COOKIE,
     );
     foreach ($filter as $type) {
       $filtered = filter_input_array($type, FILTER_UNSAFE_RAW);
       if (!is_array($filtered)) {
         continue;
       }
       switch ($type) {
         case INPUT_SERVER:
           $_SERVER = array_merge($_SERVER, $filtered);
           break;
         case INPUT_GET:
           $_GET = array_merge($_GET, $filtered);
           break;
         case INPUT_COOKIE:
           $_COOKIE = array_merge($_COOKIE, $filtered);
           break;
         case INPUT_POST:
           $_POST = array_merge($_POST, $filtered);
           break;
         case INPUT_ENV;
           $env = array_merge($_ENV, $filtered);
           $_ENV = self::filterEnvSuperglobal($env);
           break;
       }
     }
 
     // rebuild $_REQUEST, respecting order declared in ini files
     $order = ini_get('request_order');
     if (!$order) {
       $order = ini_get('variables_order');
     }
     if (!$order) {
       // $_REQUEST will be empty, leave it alone
       return;
     }
     $_REQUEST = array();
     for ($i = 0; $i < strlen($order); $i++) {
       switch ($order[$i]) {
         case 'G':
           $_REQUEST = array_merge($_REQUEST, $_GET);
           break;
         case 'P':
           $_REQUEST = array_merge($_REQUEST, $_POST);
           break;
         case 'C':
           $_REQUEST = array_merge($_REQUEST, $_COOKIE);
           break;
         default:
           // $_ENV and $_SERVER never go into $_REQUEST
           break;
       }
     }
   }
 
 
   /**
    * Adjust `$_ENV` before execution.
    *
    * Adjustments here primarily impact the environment as seen by subprocesses.
    * The environment is forwarded explicitly by @{class:ExecFuture}.
    *
    * @param map<string, wild> Input `$_ENV`.
    * @return map<string, string> Suitable `$_ENV`.
    * @task validation
    */
   private static function filterEnvSuperglobal(array $env) {
 
     // In some configurations, we may get "argc" and "argv" set in $_ENV.
     // These are not real environmental variables, and "argv" may have an array
     // value which can not be forwarded to subprocesses. Remove these from the
     // environment if they are present.
     unset($env['argc']);
     unset($env['argv']);
 
     return $env;
   }
 
 
   /**
    * @task validation
    */
   private static function verifyPHP() {
     $required_version = '5.2.3';
     if (version_compare(PHP_VERSION, $required_version) < 0) {
       self::didFatal(
         "You are running PHP version '".PHP_VERSION."', which is older than ".
         "the minimum version, '{$required_version}'. Update to at least ".
         "'{$required_version}'.");
     }
 
     if (get_magic_quotes_gpc()) {
       self::didFatal(
         "Your server is configured with PHP 'magic_quotes_gpc' enabled. This ".
         "feature is 'highly discouraged' by PHP's developers and you must ".
         "disable it to run Phabricator. Consult the PHP manual for ".
         "instructions.");
     }
 
     if (extension_loaded('apc')) {
       $apc_version = phpversion('apc');
       $known_bad = array(
         '3.1.14' => true,
         '3.1.15' => true,
         '3.1.15-dev' => true,
       );
       if (isset($known_bad[$apc_version])) {
         self::didFatal(
           "You have APC {$apc_version} installed. This version of APC is ".
           "known to be bad, and does not work with Phabricator (it will ".
           "cause Phabricator to fatal unrecoverably with nonsense errors). ".
           "Downgrade to version 3.1.13.");
       }
     }
   }
 
 
   /**
    * @task validation
    */
   private static function verifyRewriteRules() {
     if (isset($_REQUEST['__path__']) && strlen($_REQUEST['__path__'])) {
       return;
     }
 
     if (php_sapi_name() == 'cli-server') {
       // Compatibility with PHP 5.4+ built-in web server.
       $url = parse_url($_SERVER['REQUEST_URI']);
       $_REQUEST['__path__'] = $url['path'];
       return;
     }
 
     if (!isset($_REQUEST['__path__'])) {
       self::didFatal(
         "Request parameter '__path__' is not set. Your rewrite rules ".
         "are not configured correctly.");
     }
 
     if (!strlen($_REQUEST['__path__'])) {
       self::didFatal(
         "Request parameter '__path__' is set, but empty. Your rewrite rules ".
         "are not configured correctly. The '__path__' should always ".
         "begin with a '/'.");
     }
   }
 
 
   /**
    * @task validation
    */
   private static function validateGlobal($key) {
     static $globals = array(
       'log.access' => true,
       'csrf.salt'  => true,
     );
 
     if (empty($globals[$key])) {
       throw new Exception("Access to unknown startup global '{$key}'!");
     }
   }
 
 
   /**
    * Detect if this request has had its POST data stripped by exceeding the
    * 'post_max_size' PHP configuration limit.
    *
    * PHP has a setting called 'post_max_size'. If a POST request arrives with
    * a body larger than the limit, PHP doesn't generate $_POST but processes
    * the request anyway, and provides no formal way to detect that this
    * happened.
    *
    * We can still read the entire body out of `php://input`. However according
    * to the documentation the stream isn't available for "multipart/form-data"
    * (on nginx + php-fpm it appears that it is available, though, at least) so
    * any attempt to generate $_POST would be fragile.
    *
    * @task validation
    */
   private static function detectPostMaxSizeTriggered() {
     // If this wasn't a POST, we're fine.
     if ($_SERVER['REQUEST_METHOD'] != 'POST') {
       return;
     }
 
     // If there's POST data, clearly we're in good shape.
     if ($_POST) {
       return;
     }
 
     // For HTML5 drag-and-drop file uploads, Safari submits the data as
     // "application/x-www-form-urlencoded". For most files this generates
     // something in POST because most files decode to some nonempty (albeit
     // meaningless) value. However, some files (particularly small images)
     // don't decode to anything. If we know this is a drag-and-drop upload,
     // we can skip this check.
     if (isset($_REQUEST['__upload__'])) {
       return;
     }
 
     // PHP generates $_POST only for two content types. This routing happens
     // in `main/php_content_types.c` in PHP. Normally, all forms use one of
     // these content types, but some requests may not -- for example, Firefox
     // submits files sent over HTML5 XMLHTTPRequest APIs with the Content-Type
     // of the file itself. If we don't have a recognized content type, we
     // don't need $_POST.
     //
     // NOTE: We use strncmp() because the actual content type may be something
     // like "multipart/form-data; boundary=...".
     //
     // NOTE: Chrome sometimes omits this header, see some discussion in T1762
     // and http://code.google.com/p/chromium/issues/detail?id=6800
     $content_type = isset($_SERVER['CONTENT_TYPE'])
       ? $_SERVER['CONTENT_TYPE']
       : '';
 
     $parsed_types = array(
       'application/x-www-form-urlencoded',
       'multipart/form-data',
     );
 
     $is_parsed_type = false;
     foreach ($parsed_types as $parsed_type) {
       if (strncmp($content_type, $parsed_type, strlen($parsed_type)) === 0) {
         $is_parsed_type = true;
         break;
       }
     }
 
     if (!$is_parsed_type) {
       return;
     }
 
     // Check for 'Content-Length'. If there's no data, we don't expect $_POST
     // to exist.
     $length = (int)$_SERVER['CONTENT_LENGTH'];
     if (!$length) {
       return;
     }
 
     // Time to fatal: we know this was a POST with data that should have been
     // populated into $_POST, but it wasn't.
 
     $config = ini_get('post_max_size');
-    PhabricatorStartup::didFatal(
+    self::didFatal(
       "As received by the server, this request had a nonzero content length ".
       "but no POST data.\n\n".
       "Normally, this indicates that it exceeds the 'post_max_size' setting ".
       "in the PHP configuration on the server. Increase the 'post_max_size' ".
       "setting or reduce the size of the request.\n\n".
       "Request size according to 'Content-Length' was '{$length}', ".
       "'post_max_size' is set to '{$config}'.");
   }
 
 
 /* -(  Rate Limiting  )------------------------------------------------------ */
 
 
   /**
    * Adjust the permissible rate limit score.
    *
    * By default, the limit is `1000`. You can use this method to set it to
    * a larger or smaller value. If you set it to `2000`, users may make twice
    * as many requests before rate limiting.
    *
    * @param int Maximum score before rate limiting.
    * @return void
    * @task ratelimit
    */
   public static function setMaximumRate($rate) {
     self::$maximumRate = $rate;
   }
 
 
   /**
    * Check if the user (identified by `$user_identity`) has issued too many
    * requests recently. If they have, end the request with a 429 error code.
    *
    * The key just needs to identify the user. Phabricator uses both user PHIDs
    * and user IPs as keys, tracking logged-in and logged-out users separately
    * and enforcing different limits.
    *
    * @param   string  Some key which identifies the user making the request.
    * @return  void    If the user has exceeded the rate limit, this method
    *                  does not return.
    * @task ratelimit
    */
   public static function rateLimitRequest($user_identity) {
     if (!self::canRateLimit()) {
       return;
     }
 
     $score = self::getRateLimitScore($user_identity);
     if ($score > (self::$maximumRate * self::getRateLimitBucketCount())) {
       // Give the user some bonus points for getting rate limited. This keeps
       // bad actors who keep slamming the 429 page locked out completely,
       // instead of letting them get a burst of requests through every minute
       // after a bucket expires.
       self::addRateLimitScore($user_identity, 50);
       self::didRateLimit($user_identity);
     }
   }
 
 
   /**
    * Add points to the rate limit score for some user.
    *
    * If users have earned more than 1000 points per minute across all the
    * buckets they'll be locked out of the application, so awarding 1 point per
    * request roughly corresponds to allowing 1000 requests per second, while
    * awarding 50 points roughly corresponds to allowing 20 requests per second.
    *
    * @param string  Some key which identifies the user making the request.
    * @param float   The cost for this request; more points pushes them toward
    *                the limit faster.
    * @return void
    * @task ratelimit
    */
   public static function addRateLimitScore($user_identity, $score) {
     if (!self::canRateLimit()) {
       return;
     }
 
     $current = self::getRateLimitBucket();
 
     // There's a bit of a race here, if a second process reads the bucket before
     // this one writes it, but it's fine if we occasionally fail to record a
     // user's score. If they're making requests fast enough to hit rate
     // limiting, we'll get them soon.
 
     $bucket_key = self::getRateLimitBucketKey($current);
     $bucket = apc_fetch($bucket_key);
     if (!is_array($bucket)) {
       $bucket = array();
     }
 
     if (empty($bucket[$user_identity])) {
       $bucket[$user_identity] = 0;
     }
 
     $bucket[$user_identity] += $score;
     apc_store($bucket_key, $bucket);
   }
 
 
   /**
    * Determine if rate limiting is available.
    *
    * Rate limiting depends on APC, and isn't available unless the APC user
    * cache is available.
    *
    * @return bool True if rate limiting is available.
    * @task ratelimit
    */
   private static function canRateLimit() {
     if (!self::$maximumRate) {
       return false;
     }
 
     if (!function_exists('apc_fetch')) {
       return false;
     }
 
     return true;
   }
 
 
   /**
    * Get the current bucket for storing rate limit scores.
    *
    * @return int The current bucket.
    * @task ratelimit
    */
   private static function getRateLimitBucket() {
     return (int)(time() / 60);
   }
 
 
   /**
    * Get the total number of rate limit buckets to retain.
    *
    * @return int Total number of rate limit buckets to retain.
    * @task ratelimit
    */
   private static function getRateLimitBucketCount() {
     return 5;
   }
 
 
   /**
    * Get the APC key for a given bucket.
    *
    * @param int Bucket to get the key for.
    * @return string APC key for the bucket.
    * @task ratelimit
    */
   private static function getRateLimitBucketKey($bucket) {
     return 'rate:bucket:'.$bucket;
   }
 
 
   /**
    * Get the APC key for the smallest stored bucket.
    *
    * @return string APC key for the smallest stored bucket.
    * @task ratelimit
    */
   private static function getRateLimitMinKey() {
     return 'rate:min';
   }
 
 
   /**
    * Get the current rate limit score for a given user.
    *
    * @param string Unique key identifying the user.
    * @return float The user's current score.
    * @task ratelimit
    */
   private static function getRateLimitScore($user_identity) {
     $min_key = self::getRateLimitMinKey();
 
     // Identify the oldest bucket stored in APC.
     $cur = self::getRateLimitBucket();
     $min = apc_fetch($min_key);
 
     // If we don't have any buckets stored yet, store the current bucket as
     // the oldest bucket.
     if (!$min) {
       apc_store($min_key, $cur);
       $min = $cur;
     }
 
     // Destroy any buckets that are older than the minimum bucket we're keeping
     // track of. Under load this normally shouldn't do anything, but will clean
     // up an old bucket once per minute.
     $count = self::getRateLimitBucketCount();
     for ($cursor = $min; $cursor < ($cur - $count); $cursor++) {
       apc_delete(self::getRateLimitBucketKey($cursor));
       apc_store($min_key, $cursor + 1);
     }
 
     // Now, sum up the user's scores in all of the active buckets.
     $score = 0;
     for (; $cursor <= $cur; $cursor++) {
       $bucket = apc_fetch(self::getRateLimitBucketKey($cursor));
       if (isset($bucket[$user_identity])) {
         $score += $bucket[$user_identity];
       }
     }
 
     return $score;
   }
 
 
   /**
    * Emit an HTTP 429 "Too Many Requests" response (indicating that the user
    * has exceeded application rate limits) and exit.
    *
    * @return exit This method **does not return**.
    * @task ratelimit
    */
   private static function didRateLimit() {
     $message =
       "TOO MANY REQUESTS\n".
       "You are issuing too many requests too quickly.\n".
       "To adjust limits, see \"Configuring a Preamble Script\" in the ".
       "documentation.";
 
     header(
       'Content-Type: text/plain; charset=utf-8',
       $replace = true,
       $http_error = 429);
 
     echo $message;
 
     exit(1);
   }
 
 }
diff --git a/webroot/rsrc/css/application/auth/auth.css b/webroot/rsrc/css/application/auth/auth.css
index 219c8795a3..07eaccd46c 100644
--- a/webroot/rsrc/css/application/auth/auth.css
+++ b/webroot/rsrc/css/application/auth/auth.css
@@ -1,42 +1,52 @@
 /**
  * @provides auth-css
  */
 
 .phabricator-login-buttons {
   max-width: 508px;
   margin: 16px auto;
 }
 
 .phabricator-login-buttons .phabricator-login-button .button {
   width: 246px;
 }
 
 .device-desktop .phabricator-login-buttons .aphront-multi-column-column-last {
   text-align: right;
 }
 
 .device .phabricator-login-buttons {
   text-align: center;
 }
 
 .phabricator-link-button {
   text-align: center;
 }
 
 .auth-account-view {
   background-color: #fff;
   border: 1px solid {$lightblueborder};
-  background-repeat: no-repeat;
-  background-position: 4px 4px;
-  padding: 4px 4px 4px 62px;
-  min-height: 50px;
   border-radius: 2px;
+  min-height: 50px;
+  position: relative;
+  padding: 4px 4px 4px 64px;
+}
+
+.auth-account-view-profile-image {
+  width: 50px;
+  height: 50px;
+  top: 4px;
+  left: 4px;
+
+  background-repeat: no-repeat;
+  background-size: 100%;
+  position: absolute;
 }
 
 .auth-account-view-name {
   font-weight: bold;
 }
 
 .auth-account-view-provider-name {
   color: {$lightgreytext};
 }
diff --git a/webroot/rsrc/css/application/base/standard-page-view.css b/webroot/rsrc/css/application/base/standard-page-view.css
index d31594852c..93a9364412 100644
--- a/webroot/rsrc/css/application/base/standard-page-view.css
+++ b/webroot/rsrc/css/application/base/standard-page-view.css
@@ -1,213 +1,192 @@
 /**
  * @provides phabricator-standard-page-view
  */
 
 .phabricator-anchor-view,
 .phabricator-anchor-navigation-marker {
   position: absolute;
   margin-top: -15px;
 }
 
 .phabricator-chromeless-page .phabricator-standard-page {
   background: transparent;
   border-width: 0px;
 }
 
 .phabricator-standard-page-footer {
   text-align: right;
   margin: 4px 16px;
   padding: 12px 0;
   border-top: 1px solid {$lightgreyborder};
   color: {$greytext};
 }
 
 .device-desktop .has-local-nav + .phabricator-standard-page-footer {
   margin-left: 221px;
 }
 
 .device-desktop div.phabricator-icon-nav + .phabricator-standard-page-footer {
   margin-left: 58px;
 }
 
 .device .phabricator-side-menu-home + .phabricator-standard-page-footer {
   display: none;
 }
 
 .keyboard-shortcut-help td,
 .keyboard-shortcut-help th {
   padding: 8px;
   vertical-align: middle;
 }
 
 .keyboard-shortcut-help th {
   white-space: nowrap;
   color: {$greytext};
 }
 
 .keyboard-shortcut-help kbd {
   background: #222222;
   padding: 6px;
   color: #ffffff;
   font-weight: bold;
   border: 1px solid #555555;
 }
 
 .keyboard-focus-focus-reticle {
   background: #ffffd3;
   position: absolute;
   border: 1px solid #999900;
 }
 
 a.handle-status-closed {
   text-decoration: line-through;
   color: #676767;
 }
 
 a.handle-status-closed:hover {
   text-decoration: line-through;
   color: #19558D;
 }
 
 a.handle-disabled,
 a.handle-status-away,
 a.handle-status-sporadic {
   padding-left: 11px;
   background-repeat: no-repeat;
   background-position: -4px center;
 }
 
 a.handle-status-away {
   background-image: url(/rsrc/image/icon/fatcow/bullet_red.png);
 }
 
 a.handle-status-sporadic {
   background-image: url(/rsrc/image/icon/fatcow/bullet_orange.png);
 }
 
 a.handle-disabled {
   background-image: url(/rsrc/image/icon/fatcow/bullet_black.png);
 }
 
 .aphront-developer-error-callout {
   position: relative;
   padding: 2em;
   background: #aa0000;
   color: white;
   text-align: center;
   font-size: 11px;
   font-family: "Verdana";
 }
 
 .setup-warning-callout {
   padding: 8px 16px;
   background: {$lightred};
   border-bottom: 1px solid {$sh-redborder};
   position: relative;
 }
 
 .setup-warning-callout a {
   color: {$red};
 }
 
 .phui-handle .phui-icon-view {
   display: inline-block;
   margin: 2px 2px -2px 0;
 }
 
-.main-page-frame {
-  position: absolute;
-  top: 0;
-  bottom: 0;
-  right: 0;
-  left: 0;
-  overflow: hidden;
-}
-
-.phabricator-standard-page {
-  /* If we don't activate JX.Scrollbar because the default scrollbars are
-     satisfactory, make sure the page still has sensible behavior. These
-     settings will be overwritten by .jx-scrollbar-frame if JX.Scrollbar
-     activates. */
-  position: relative;
-  height: 100%;
-  overflow-y: scroll;
-
-  -webkit-overflow-scrolling: touch;
-}
-
 .jx-scrollbar-frame {
   position: relative;
   overflow: hidden;
 }
 
 .jx-scrollbar-viewport {
   position: absolute;
   overflow-x: hidden;
   overflow-y: scroll;
   top: 0;
   bottom: 0;
   left: 0;
   right: 0;
 }
 
 /* Fixes so pages actually print when magic scrollbar is present */
 !print .main-page-frame {
   position: static;
   overflow: visible;
 }
 
 !print .jx-scrollbar-viewport {
   position: static;
   width: auto !important;
   height: auto !important;
 }
 
 .jx-scrollbar-test {
   position: absolute;
   left: -300px;
 }
 
 .jx-scrollbar-bar {
   position: absolute;
   top: 0;
   right: 0;
   bottom: 7px;
   width: 11px;
 }
 
 .jx-scrollbar-bar .jx-scrollbar-handle {
   position: absolute;
   right: 2px;
   -webkit-border-radius: 7px;
   -moz-border-radius: 7px;
   border-radius: 7px;
   min-height: 10px;
   width: 7px;
   opacity: 0;
   -webkit-transition: opacity 0.2s linear;
   -moz-transition: opacity 0.2s linear;
   -o-transition: opacity 0.2s linear;
   -ms-transition: opacity 0.2s linear;
   transition: opacity 0.2s linear;
   background: #6c6e71;
   -webkit-background-clip: padding-box;
   -moz-background-clip: padding;
 }
 
 .jx-scrollbar-bar:hover .jx-scrollbar-handle {
   opacity: 0.7;
   -webkit-transition: opacity 0 linear;
   -moz-transition: opacity 0 linear;
   -o-transition: opacity 0 linear;
   -ms-transition: opacity 0 linear;
   transition: opacity 0 linear;
 }
 
 .jx-scrollbar-bar .jx-scrollbar-visible {
   opacity: 0.7;
 }
 
 .jx-scrollbar-link {
   position: absolute;
   left: -50px;
 }
diff --git a/webroot/rsrc/css/application/conduit/conduit-api.css b/webroot/rsrc/css/application/conduit/conduit-api.css
new file mode 100644
index 0000000000..fbe577e908
--- /dev/null
+++ b/webroot/rsrc/css/application/conduit/conduit-api.css
@@ -0,0 +1,16 @@
+/**
+ * @provides conduit-api-css
+ */
+.conduit-api-example-code {
+  margin: 16px;
+  white-space: pre;
+  color: {$darkgreytext};
+}
+
+.conduit-api-example-code strong {
+  color: {$red};
+}
+
+.conduit-api-example-code strong.real {
+  color: {$blue};
+}
diff --git a/webroot/rsrc/css/application/conpherence/durable-column.css b/webroot/rsrc/css/application/conpherence/durable-column.css
index 324d76c27c..1dbb829565 100644
--- a/webroot/rsrc/css/application/conpherence/durable-column.css
+++ b/webroot/rsrc/css/application/conpherence/durable-column.css
@@ -1,275 +1,300 @@
 /**
  * @provides conpherence-durable-column-view
  */
 
-.with-durable-column .phabricator-standard-page {
+.with-durable-column .phabricator-standard-page-body {
   margin-right: 300px;
 }
 
+.with-durable-margin .phabricator-standard-page-body {
+  margin-right: 312px;
+}
+
+.with-durable-column .phabricator-main-menu {
+  padding-right: 304px;
+}
+
+.with-durable-margin .phabricator-main-menu {
+  padding-right: 316px;
+}
+
 .with-durable-column
 .phabricator-global-upload-instructions {
   font-size: 28px;
   width: 50%;
 }
 
 .global-upload-mask {
   pointer-events: none;
 }
 
 .with-durable-column .global-upload-mask {
   right: 300px;
 }
 
+.with-durable-margin .global-upload-mask {
+  right: 312px;
+}
+
 .conpherence-durable-column {
-  position: absolute;
+  position: fixed;
   top: 0;
   bottom: 0;
   right: 0;
   width: 300px;
   background: #fff;
 }
 
+.with-durable-margin .conpherence-durable-column {
+  right: 12px;
+  box-shadow: 1px 0px 2px rgba(0, 0, 0, 0.10);
+}
+
 .conpherence-durable-column .loading-mask {
   position: absolute;
   top: 90px;
   bottom: 0;
   right: 1px;
   width: 298px;
   background: #fff;
   display: none;
   opacity: .6;
   z-index: 2;
 }
 
 .device-desktop .conpherence-durable-column.loading .loading-mask {
   display: block;
 }
 
 .conpherence-durable-column-header {
   height: 44px;
 }
 
 .conpherence-durable-column-header .conpherence-settings-dropdown {
   z-index: 1;
 }
 
 .conpherence-durable-column-header .phabricator-application-menu {
   padding-right: 4px;
 }
 
 .conpherence-durable-column-header-text {
   float: left;
   padding: 13px 0 12px 12px;
   font-size: 15px;
   color: rgba(255,255,255,.8);
   width: 230px;
   text-overflow: ellipsis;
   overflow: hidden;
   white-space: nowrap;
   border-left: 1px solid rgba(0,0,0,.2);
 }
 
 .conpherence-durable-column-header-text .phui-icon-view {
   color: rgba(255, 255, 255, 0.8);
 }
 
 .conpherence-durable-column-icon-bar {
   height: 38px;
   padding: 4px;
   background-color: {$lightgreybackground};
 }
 
 .conpherence-durable-column-icon-bar .conpherence-durable-column-thread-icon {
   float: left;
   display: block;
   height: 34px;
   width: 34px;
   border: 2px solid transparent;
   border-radius: 3px;
   margin: 0 4px 0 0;
 }
 
 .conpherence-durable-column-icon-bar .conpherence-durable-column-search-button {
   margin: 4px 0px 0px 0px;
 }
 .conpherence-durable-column-icon-bar .phui-button-bar {
 }
 .conpherence-durable-column-icon-bar .phui-button-bar a.button.has-icon {
   height: 21px;
 }
 .conpherence-durable-column-icon-bar .phui-button-bar .button .phui-icon-view {
   top: 8px;
 }
 
 .conpherence-durable-column-icon-bar
 .conpherence-durable-column-thread-icon.selected {
   border-color: {$sky};
 }
 
 .conpherence-durable-column-icon-bar
 .conpherence-durable-column-thread-icon span {
   position: relative;
   display: block;
   width: 30px;
   height: 30px;
   top: 2px;
   left: 2px;
   background-size: 30px 30px;
 }
 
 .conpherence-durable-column-body {
   position: absolute;
   top: 44px;
   bottom: 0;
   right: 0;
   left: 0;
   border-left: 1px solid {$lightblueborder};
 }
 
+.with-durable-margin .conpherence-durable-column-body {
+  border-right: 1px solid {$lightblueborder};
+}
+
 .conpherence-durable-column-main {
   position: absolute;
   top: 46px;
   bottom: 134px;
   left: 0;
   right: 0;
   overflow-x: hidden;
   border-top: 1px solid {$thinblueborder};
 }
 
 .conpherence-durable-column-transactions {
   padding: 8px 12px 0;
 }
 
 .conpherence-durable-column-transactions
   .conpherence-transaction-view.conpherence-edited {
     color: {$lightgreytext};
     font-size: 12px;
     margin: 0;
     padding: 0;
 }
 
 .conpherence-durable-column-transactions .conpherence-edited
   .conpherence-transaction-header {
     display: none;
 }
 
 .conpherence-durable-column-transactions .conpherence-transaction-view {
   background: none;
   margin: 0;
   padding: 4px 0;
   min-height: 0;
 }
 
 .conpherence-durable-column-transactions
   .conpherence-transaction-view
   .conpherence-message {
   word-wrap: break-word;
 }
 
 .conpherence-durable-column-transactions .conpherence-transaction-detail {
   border: 0;
   margin: 0;
 }
 
 .conpherence-durable-column-transactions .conpherence-transaction-detail
 .conpherence-transaction-header {
   background: none;
   padding: 0 0 2px 0;
 }
 
 .conpherence-durable-column-transactions
 .conpherence-transaction-view.date-marker {
   margin: 20px 0px 8px;
 }
 
 .conpherence-durable-column-transactions
 .conpherence-transaction-view.date-marker .date {
   left: 0;
   font-size: 12px;
   padding: 0 6px 0 0;
 }
 
 .conpherence-durable-column-transactions .conpherence-transaction-detail
 .conpherence-transaction-header .conpherence-transaction-info,
 .conpherence-transaction-header .epoch-link {
   color: {$lightbluetext};
   float: none;
   font-size: 12px;
 }
 
 .conpherence-durable-column-transactions .conpherence-transaction-detail
 .conpherence-transaction-header .phui-link-person {
   margin: 0 8px 0 0;
 }
 
 .conpherence-durable-column-transactions .conpherence-transaction-detail
 .conpherence-transaction-content .phui-link-person {
   color: {$darkbluetext};
 }
 
 .conpherence-durable-column-transactions
 .conpherence-transaction-detail
 .conpherence-transaction-content
 .phui-pinboard-item-view {
   width: 273px;
 }
 
 .conpherence-durable-column-transactions
 .conpherence-transaction-detail
 .conpherence-transaction-content
 .phui-pinboard-item-view
 .phui-pinboard-item-image-link
 img {
   width: 265px;
   height: 199px;
 }
 
 .conpherence-durable-column-transactions .conpherence-transaction-detail
 .conpherence-transaction-content {
   background: #fff;
   padding: 0 0 8px 0;
 }
 
 .conpherence-durable-column-textarea {
   position: absolute;
   left: 0;
   right: 0;
   bottom: 34px;
   height: 100px;
   margin: 0;
   border-width: 1px 0;
   border-style: solid;
   border-top-color: {$thinblueborder};
   border-bottom-color: {$thinblueborder};
   padding: 8px 12px;
   width: 100%;
   resize: none;
 }
 
 .conpherence-durable-column-textarea:focus {
   outline: 0;
   border-top-color: {$sky};
   border-bottom-color: {$sky};
   box-shadow: none;
 }
 
 .conpherence-durable-column-footer {
   position: absolute;
   height: 26px;
   padding: 4px 8px 4px 12px;
   left: 0;
   right: 0;
   bottom: 0;
   background-color: {$lightgreybackground};
 }
 
 .conpherence-durable-column-footer button {
   float: right;
 }
 
 .conpherence-durable-column-status {
   vertical-align: middle;
   line-height: 24px;
   font-size: 12px;
   color: {$lightbluetext};
 }
diff --git a/webroot/rsrc/css/application/conpherence/message-pane.css b/webroot/rsrc/css/application/conpherence/message-pane.css
index b450703d96..41815d086f 100644
--- a/webroot/rsrc/css/application/conpherence/message-pane.css
+++ b/webroot/rsrc/css/application/conpherence/message-pane.css
@@ -1,337 +1,345 @@
 /**
  * @provides conpherence-message-pane-css
  */
 
 .conpherence-message-pane,
 .loading .messages-loading-mask,
 .loading .messages-loading-icon,
 .conpherence-layout .conpherence-no-threads {
   position: fixed;
   left: 241px;
   right: 241px;
   top: 76px;
   bottom: 0px;
   min-width: 300px;
   width: auto;
 }
 
 .device .conpherence-message-pane,
 .device .loading .messages-loading-mask,
 .device .loading .messages-loading-icon,
 .device .conpherence-layout .conpherence-no-threads {
   left: 0;
   right: 0;
   width: 100%;
 }
 
 .conpherence-layout .conpherence-no-threads {
   text-align: center;
 }
 
 .conpherence-layout .conpherence-no-threads .text {
   margin: 16px 0px 16px 0px;
 }
 
 .conpherence-layout .phui-crumbs-view {
   padding: 0 0 0 8px;
   background: #f7f7f7;
 }
 
 .conpherence-show-more-messages {
   display: block;
   background: #e0e3ec;
   margin: 10px;
   text-align: center;
   padding: 10px;
   color: {$bluetext};
 }
 
 .conpherence-show-more-messages-loading {
   font-style: italic;
 }
 
 .conpherence-message-pane .conpherence-messages {
   position: fixed;
   left: 241px;
   right: 241px;
   top: 76px;
   bottom: 172px;
   overflow-x: hidden;
   overflow-y: auto;
   -webkit-overflow-scrolling: touch;
 }
 
 .page-has-warning .conpherence-message-pane .conpherence-messages {
   top: 110px;
 }
 
 .conpherence-messages.jx-scrollbar-frame {
   overflow-y: hidden;
 }
 
 .conpherence-messages .jx-scrollbar-content > .conpherence-edited:first-child,
 .conpherence-messages > .conpherence-edited:first-child {
   padding-top: 20px;
 }
 
 .conpherence-messages > .jx-scrollbar-content .conpherence-edited:last-child,
 .conpherence-messages > .conpherence-edited:last-child {
   padding-bottom: 20px;
 }
 
 .conpherence-message-pane .conpherence-edited + .date-marker  {
   margin-top: 24px;
 }
 
 .device .conpherence-message-pane .conpherence-messages {
   left: 0;
   right: 0;
   bottom: 52px;
   width: 100%;
   box-shadow: none;
 }
 
 .conpherence-message-pane .messages-loading-mask {
   opacity: .6;
   background: #fff;
   display: none;
 }
 
 .loading .messages-loading-mask {
   display: block;
 }
 
 .conpherence-message-pane .phui-form-view {
   border-width: 0;
   background-color: {$lightgreybackground};
   height: 156px;
   padding: 8px;
   position: fixed;
   bottom: 0;
   border-top: 1px solid {$thinblueborder};
   left: 241px;
   right: 241px;
 }
 
+.conpherence-message-pane .phui-form-view.login-to-participate {
+  height: 28px;
+}
+
+.conpherence-message-pane .login-to-participate a.button {
+  float: right;
+}
+
 .conpherence-message-pane .aphront-form-control-submit button,
 .conpherence-message-pane .aphront-form-control-submit a.button {
   margin-top: 6px;
 }
 
 /**
  * When entering "Fullscreen Mode" in the remarkup control, we need to drop
  * all of the "position: fixed" on parent elements or Chrome doesn't put the
  * textarea on top.
  */
 .remarkup-fullscreen-mode .conpherence-message-pane,
 .remarkup-fullscreen-mode .conpherence-message-pane .conpherence-messages,
 .remarkup-fullscreen-mode .conpherence-message-pane .phui-form-view,
 .remarkup-fullscreen-mode .conpherence-layout {
   position: static;
 }
 
 .conpherence-message-pane .remarkup-assist-bar {
   border-color: {$lightblueborder};
 }
 
 .device .conpherence-message-pane .remarkup-assist-bar {
   display: none;
 }
 
 .device .conpherence-message-pane .phui-form-view {
   left: 0;
   right: 0;
   height: 34px;
   width: auto;
 }
 
 .conpherence-layout .conpherence-message-pane .phui-form-view
   div.aphront-form-input {
   margin: 0;
   width: 100%;
 }
 
 .conpherence-message-pane .conpherence-transaction-view {
   padding: 2px 0px;
   margin: 4px 12px;
-  background-size: 35px;
+  background-size: 100%;
   min-height: auto;
 }
 
 .device-phone .conpherence-message-pane .conpherence-transaction-view {
   margin: 0 8px;
 }
 
 .conpherence-message-pane .conpherence-transaction-image {
   float: left;
   border-radius: 3px;
   height: 35px;
   width: 35px;
   background-size: 35px;
   position: absolute;
   top: 5px;
 }
 
 .device-phone .conpherence-message-pane .conpherence-transaction-image {
   display: none;
 }
 
 .conpherence-message-pane .conpherence-comment.anchor-target,
 .conpherence-message-pane .conpherence-edited.anchor-target {
   background: {$lightyellow};
 }
 
 .conpherence-message-pane .conpherence-comment.anchor-target {
   margin: 4px 8px 4px 8px;
   padding: 2px 4px 2px 4px;
 }
 
 .conpherence-message-pane .conpherence-edited.anchor-target {
   margin: 0px 8px 0px 8px;
   padding: 0px 4px 0px 4px;
 }
 
 .conpherence-message-pane .conpherence-transaction-detail {
   border-width: 0;
   margin-left: 45px;
 }
 
 .device-phone .conpherence-message-pane .conpherence-transaction-detail {
   margin: 0;
 }
 
 .conpherence-message-pane .conpherence-transaction-view.date-marker {
   padding: 0;
   margin: 20px 12px 4px;
   min-height: auto;
 }
 
 .device-phone .conpherence-message-pane
 .conpherence-transaction-view.date-marker {
   margin: 12px 0 4px;
 }
 
 .device-tablet .conpherence-message-pane
   .conpherence-transaction-view.date-marker {
     padding-left: 37px;
 }
 
 .conpherence-message-pane .conpherence-transaction-view.date-marker
   .date {
     left: 40px;
     font-size: 13px;
     padding: 0px 4px;
 }
 
 .device .conpherence-message-pane .conpherence-transaction-view.date-marker
   .date {
     color: {$lightbluetext};
     left: 4px;
 }
 
 .device-phone .conpherence-message-pane .conpherence-edited {
   min-height: none;
   color: {$lightgreytext};
   margin: 0 8px;
 }
 
 .conpherence-message-pane .conpherence-edited .conpherence-transaction-content {
   color: {$lightgreytext};
   font-size: 13px;
   margin: 0;
   padding: 0;
   float: left;
 }
 
 .conpherence-message-pane .conpherence-edited {
   padding: 0;
   margin-top: 0;
   margin-bottom: 0;
   min-height: inherit;
 }
 
 .conpherence-message-pane .conpherence-edited + .conpherence-comment {
   margin-top: 16px;
 }
 
 .conpherence-transaction-view.conpherence-edited +
   .conpherence-transaction-view.date-marker {
     margin-top: 24px;
 }
 
 .conpherence-message-pane .conpherence-edited .conpherence-transaction-header {
   float: right;
 }
 
 .conpherence-message-pane .conpherence-edited
   .conpherence-transaction-content a {
   color: {$darkbluetext};
 }
 
 .device-phone .conpherence-message-pane .conpherence-transaction-info {
   display: none;
 }
 
 .conpherence-message-pane .conpherence-transaction-info,
 .conpherence-message-pane .anchor-link,
 .conpherence-message-pane .phabricator-content-source-view {
   color: {$lightbluetext};
   line-height: 16px;
   font-size: 12px;
 }
 
 .conpherence-message-pane .conpherence-transaction-info {
   float: right;
 }
 
 .conpherence-message-pane .conpherence-transaction-header,
 .conpherence-message-pane .conpherence-transaction-info,
 .conpherence-message-pane .anchor-link,
 .conpherence-message-pane .conpherence-transaction-content {
   background: none;
   padding: 0;
 }
 
 .conpherence-message-pane .conpherence-transaction-content {
   padding: 2px 0 8px 0;
 }
 
 .conpherence-message-pane .aphront-form-control {
   padding: 0;
 }
 
 .conpherence-message-pane .remarkup-assist-textarea {
   height: 100px;
   padding: 6px;
   border-color: {$lightblueborder};
   border-top-color: {$thinblueborder};
   box-sizing: border-box;
   -moz-box-sizing: border-box;
   -webkit-box-sizing: border-box;
   resize: none;
 }
 
 .device .conpherence-message-pane .remarkup-assist-textarea {
   margin: 0;
   padding: 8px 68px 8px 8px;
   width: 100%;
   height: 34px;
   resize: none;
 }
 
 .conpherence-message-pane .remarkup-assist-textarea:focus {
   outline: none;
 }
 
 .device .conpherence-message-pane .aphront-form-control-submit {
   padding: 0;
   position: absolute;
   top: 7px;
   right: 13px;
 }
 
 .device .conpherence-message-pane .aphront-form-control-textarea {
   float: left;
   height: 24px;
   width: 100%;
 }
diff --git a/webroot/rsrc/css/application/conpherence/notification.css b/webroot/rsrc/css/application/conpherence/notification.css
index 75403f991d..e5bd40274a 100644
--- a/webroot/rsrc/css/application/conpherence/notification.css
+++ b/webroot/rsrc/css/application/conpherence/notification.css
@@ -1,71 +1,71 @@
 /**
  * @provides conpherence-notification-css
  */
 
 /* kill styles on phabricator-notification */
 .conpherence-notification {
   padding: 0;
 }
 
 .phabricator-notification .conpherence-menu-item-view {
   display: block;
   height: 46px;
   overflow: hidden;
   position: relative;
   text-decoration: none;
   border-bottom: none;
   border-right: 0;
   border-left: 0;
 }
 
 .phabricator-notification .conpherence-menu-item-view
 .conpherence-menu-item-image {
   top: 8px;
   left: 8px;
   display: block;
   position: absolute;
   width: 30px;
   height: 30px;
-  background-size: 30px;
+  background-size: 100%;
 }
 
 .phabricator-notification .conpherence-menu-item-view
 .conpherence-menu-item-title {
   display: block;
   margin-top: 8px;
   margin-left: 46px;
   text-align: left;
   font-weight: bold;
   font-size: 13px;
   color: {$darkgreytext};
   width: 314px;
   text-overflow: ellipsis;
   white-space: nowrap;
   overflow: hidden;
 }
 
 .phabricator-notification .conpherence-menu-item-view
 .conpherence-menu-item-subtitle {
   display: block;
   color: {$lightgreytext};
   font-size: 11px;
   margin-top: 2px;
   margin-left: 46px;
   width: 290px;
   text-overflow: ellipsis;
   white-space: nowrap;
   overflow: hidden;
 }
 
 .phabricator-notification .conpherence-menu-item-view
 .conpherence-menu-item-unread-count {
   position: absolute;
   right: 8px;
   top: 15px;
   background: {$blue};
   border-radius: 2px;
   color: #fff;
   font-weight: bold;
   padding: 0 5px 1px;
   font-size: 11px;
 }
diff --git a/webroot/rsrc/css/application/conpherence/transaction.css b/webroot/rsrc/css/application/conpherence/transaction.css
index d67d31692e..6ebd53b2ee 100644
--- a/webroot/rsrc/css/application/conpherence/transaction.css
+++ b/webroot/rsrc/css/application/conpherence/transaction.css
@@ -1,27 +1,46 @@
 /**
  * @provides conpherence-transaction-css
  */
 
 .conpherence-transaction-view {
   position: relative;
 }
 
 .conpherence-transaction-content {
   overflow: auto;
 }
 
 .conpherence-transaction-header .phui-link-person {
   font-weight: bold;
   color: {$darkbluetext};
 }
 
 .conpherence-transaction-view.date-marker {
   border-top: 1px solid {$thinblueborder};
 }
 .conpherence-transaction-view.date-marker .date {
   position: relative;
   top: -11px;
   background-color: #fff;
   color: {$darkbluetext};
   font-weight: bold;
 }
+
+.conpherence-fulltext-results {
+  margin: 0 8px 8px;
+  background: {$lightgreybackground};
+  border: 1px solid {$lightgreyborder};
+}
+
+.conpherence-fulltext-result {
+  margin: 0 0 1px;
+  padding: 8px;
+}
+
+.conpherence-fulltext-match {
+  background: {$lightyellow};
+}
+
+.conpherence-fulltext-results .epoch-link {
+  float: right;
+}
diff --git a/webroot/rsrc/css/core/core.css b/webroot/rsrc/css/core/core.css
index 7333d87994..4bb08ea3df 100644
--- a/webroot/rsrc/css/core/core.css
+++ b/webroot/rsrc/css/core/core.css
@@ -1,171 +1,175 @@
 /**
  * @provides phabricator-core-css
  */
 
 body, div, dl, dt, dd, ul, ol, li,
 h1, h2, h3, h4, h5, h6,
 pre, form, fieldset,
 p, blockquote, th, td, button {
   margin: 0;
   padding: 0;
   border: 0;
 }
 
 table {
   border-collapse: collapse;
   border-spacing: 0;
 }
 
 fieldset, img {
   border: 0;
 }
 
 address, caption, cite, code, dfn, th, var {
   font-style: normal;
   font-weight: normal;
 }
 
 ol, ul {
   list-style: none;
 }
 
 caption, th {
   text-align: left;
 }
 
 td, th {
   vertical-align: top;
 }
 
 h1, h2, h3, h4, h5, h6 {
   font-size: 100%;
   font-weight: bold;
 }
 
 body {
   font: {$basefont};
   direction: ltr;
   text-align: left;
   unicode-bidi: embed;
   background: #f7f7f7;
 
   /* By default, the iPhone zooms all text on the page by some percentage when
      you rotate from portrait mode to landscape mode. Disable this, since it
      breaks lots of things and prevents you from using landscape to see more
      columns in source code views. */
   -webkit-text-size-adjust: none;
+
+  /* Prevent content from resizing abruptly when shifting between scrollable
+     and unscrollable pages. */
+  overflow-y: scroll;
 }
 
 textarea {
   font: inherit;
 }
 
 table {
   font-size: inherit;
   font: 100%;
 }
 
 h1 {
   font-size: 16px;
 }
 
 h2 {
   font-size: 14px;
 }
 
 a {
   -moz-outline-style: none;
   text-decoration: none;
   color: #18559D;
   cursor: pointer;
 }
 
 a:hover {
   text-decoration: underline;
 }
 
 img {
   display: block;
 }
 
 .busy {
   position: fixed;
   bottom: 8px;
   right: 8px;
   width: 32px;
   height: 32px;
 }
 
 .with-durable-column .busy {
   right: 308px;
 }
 
 .busy .phui-icon-view {
   font-size: 32px;
 }
 
 .grouped:after {
   content: "";
   display: table;
   clear: both;
 }
 
 hr {
   height: 1px;
   background: #bbbbbb;
   border: none;
 }
 
 .aural-only {
   position: absolute !important;
   clip: rect(1px, 1px, 1px, 1px);
 
   /* NOTE: Without this, Safari sometimes lays these elements out at normal
      size. An example is the label on the comment action menu on timelines. */
 
   width: 0;
   height: 0;
   overflow: hidden;
 }
 
 .visual-only {
   /* These elements are hidden by the 'aria-hidden' attribute. */
 }
 
 .audible .aural-only {
   clip: auto;
   width: auto;
   height: auto;
   overflow: auto;
   background: #006699;
   color: #ffffff;
 }
 
 .audible .aural-only a {
   color: #ffffff;
   font-weight: bold;
 }
 
 .audible .visual-only {
   position: absolute !important;
   background: #990066;
   opacity: 0.25;
 }
 
 .routing-bar {
   position: fixed;
   top: 0;
   width: 100%;
   height: 2px;
   background: {$darkbluetext};
   z-index: 80;
   box-shadow: 0 2px 1px rgba(0, 128, 255, 0.25);
 }
 
 .routing-progress {
   position: fixed;
   top: 0;
   left: 0;
   height: 2px;
   background: {$sky};
 }
diff --git a/webroot/rsrc/css/core/z-index.css b/webroot/rsrc/css/core/z-index.css
index 4c2c5d457a..c69638eede 100644
--- a/webroot/rsrc/css/core/z-index.css
+++ b/webroot/rsrc/css/core/z-index.css
@@ -1,160 +1,163 @@
 /**
  * @provides phabricator-zindex-css
  */
 
 .keyboard-focus-focus-reticle {
   z-index: 1;
 }
 
 .device .phabricator-action-list-view.phabricator-action-list-toggle,
 .device-desktop .phui-document-content
   .phabricator-action-list-view.phabricator-action-list-toggle {
   z-index: 1;
 }
 
 .device-desktop .phui-timeline-minor-event .phui-timeline-image {
   z-index: 2;
 }
 
 .differential-reticle {
   z-index: 2;
 }
 
 .differential-changeset {
   z-index: 2;
 }
 
 .pholio-new-inline-comment {
   z-index: 2;
 }
 
 .slowvote-bar {
   z-index: 2;
 }
 
 .slowvote-above-the-bar {
   z-index: 3;
 }
 
 .phui-timeline-icon-fill {
   z-index: 3;
 }
 
 .phui-crumbs-view {
   z-index: 3;
 }
 
 .phabricator-nav-local {
   z-index: 4;
 }
 
 .conpherence-layout .conpherence-no-threads {
   z-index: 4;
 }
 
 .conpherence-menu-pane {
   z-index: 4;
 }
 
 .phabricator-nav-drag {
   z-index: 4;
 }
 
 .setup-warning-callout {
   z-index: 5;
 }
 
 .loading .messages-loading-mask,
 .loading .widgets-loading-mask {
   z-index: 5;
 }
 
 .dark-console {
   z-index: 5;
 }
 
 .drag-dragging {
   z-index: 5;
 }
 
 .phui-calendar-date-number  {
   z-index: 5;
 }
 
-.conpherence-durable-column-header,
 .phabricator-main-menu {
   z-index: 6;
 }
 
 .aphront-developer-error-callout {
   z-index: 6;
 }
 
-.jx-scrollbar-bar {
+.conpherence-durable-column {
   z-index: 7;
 }
 
+.jx-scrollbar-bar {
+  z-index: 8;
+}
+
 .differential-haunt-mode-1 .differential-add-comment-panel,
 .differential-haunt-mode-2 .differential-add-comment-panel {
   z-index: 8;
 }
 
 .device-desktop .phabricator-notification-menu {
   z-index: 9;
 }
 
 .jx-mask {
   z-index: 10;
 }
 
 .jx-notification-container {
   z-index: 11;
 }
 
 .phabricator-global-upload-instructions {
   z-index: 11;
 }
 
 .lightbox-attachment {
   z-index: 12;
 }
 
 div.jx-typeahead-results {
   z-index: 13;
 }
 
 .jx-client-dialog {
   z-index: 14;
 }
 
 .fancy-datepicker {
   z-index: 15;
 }
 
 .jx-hovercard-container {
   z-index: 17;
 }
 
 .pholio-device-lightbox {
   z-index: 20;
 }
 
 .phuix-dropdown-menu {
   z-index: 32;
 }
 
 .busy {
   z-index: 40;
 }
 
 .remarkup-control-fullscreen-mode {
   z-index: 50;
 }
 
 .jx-tooltip-container {
   z-index: 51;
 }
 
 .audible .aural-only {
   z-index: 100;
 }
diff --git a/webroot/rsrc/css/phui/calendar/phui-calendar-day.css b/webroot/rsrc/css/phui/calendar/phui-calendar-day.css
index bb07f8b8f8..11f4045fe1 100644
--- a/webroot/rsrc/css/phui/calendar/phui-calendar-day.css
+++ b/webroot/rsrc/css/phui/calendar/phui-calendar-day.css
@@ -1,50 +1,72 @@
 /**
  * @provides phui-calendar-day-css
  */
 
 .phui-calendar-day-view {
   overflow: scroll;
   width: 100%;
 }
 
 .phui-calendar-day-hour {
   width: 60px;
   color: {$lightgreytext};
   text-align: right;
   padding: 4px 4px;
   border-right: 1px solid {$lightgreyborder};
 }
 
 .phui-calendar-day-view tr {
   height: 60px;
 }
 
 .phui-calendar-day-view td {
   position: relative;
 }
 
 .phui-calendar-day-view tr + tr td.phui-calendar-day-events {
   border-top: 1px solid {$lightgreyborder};
 }
 
 .phui-calendar-day-view td div.phui-calendar-day-event {
   width: 100%;
   position: absolute;
   top: 0;
   bottom: 0;
   min-height: 30px;
 }
 
 .phui-calendar-day-event-link {
   padding: 8px;
   border: 1px solid {$blueborder};
   background-color: {$bluebackground};
   margin: 0 4px;
   position: absolute;
   left: 0;
   right: 0;
   top: 0;
   bottom: 0;
   text-decoration: none;
   color: {$greytext};
 }
+
+.day-view-all-day {
+  border: 1px solid {$blueborder};
+  height: 12px;
+  margin: 0;
+  display: block;
+  padding: 8px;
+  background-color: {$bluebackground};
+  text-decoration: none;
+  color: {$greytext};
+}
+
+.phui-calendar-day-event + .phui-calendar-day-event .day-view-all-day {
+  border-top-style: none;
+  border-top-width: 0;
+}
+
+.phui-calendar-all-day-label {
+  color: {$greytext};
+  float: right;
+  margin: 8px 8px 0 0;
+}
diff --git a/webroot/rsrc/css/phui/calendar/phui-calendar-month.css b/webroot/rsrc/css/phui/calendar/phui-calendar-month.css
index c3abbb131c..3f53e89b25 100644
--- a/webroot/rsrc/css/phui/calendar/phui-calendar-month.css
+++ b/webroot/rsrc/css/phui/calendar/phui-calendar-month.css
@@ -1,100 +1,167 @@
 /**
  * @provides phui-calendar-month-css
  */
 
 .phui-calendar-view {
   width: 100%;
   border-collapse: collapse;
   background: #fff;
 }
 
 tr.phui-calendar-day-of-week-header th {
   text-align: center;
   font-size: 11px;
   padding: 3px;
   color: {$lightbluetext};
   background: {$lightgreybackground};
 }
 
 table.phui-calendar-view td {
-  border: 1px solid #dfdfdf;
+  border: solid #dfdfdf;
+  border-width: 1px 1px 0 1px;
   width: 14.2857%; /* This is one seventh, approximately. */
 }
 
-table.phui-calendar-view td div.phui-calendar-day {
-  min-height: 125px;
+.phui-calendar-month-cell-div {
   position: relative;
 }
 
-.phui-calendar-holiday {
-  color: {$greytext};
-  padding: .5em;
-  max-height: 1em;
-  overflow: hidden;
+.phui-calendar-month-event-list .phui-calendar-month-cell-div {
+  min-height: 125px;
 }
 
-table.phui-calendar-view td.phui-calendar-month-weekstart {
-  border-left: none;
+.device .phui-calendar-month-event-list .phui-calendar-month-cell-div {
+  min-height: 60px;
 }
 
-.phui-calendar-date-number {
-  font-weight: normal;
-  color: {$lightgreytext};
-  padding: 4px;
-  border-color: {$thinblueborder};
-  border-style: solid;
-  border-width: 0 0 1px 1px;
+a.phui-calendar-month-secret-link {
   position: absolute;
-  background: #ffffff;
-  width: 16px;
-  height: 16px;
-  text-align: center;
-  top: 0;
+  left: 0;
   right: 0;
+  top: 0;
+  bottom: 0;
+  outline: 0;
+}
+
+table.phui-calendar-view tr td:first-child {
+  border-left-width: 0px;
+}
+
+.device table.phui-calendar-view .phui-calendar-event-list {
+  display: none;
+}
+
+.phui-calendar-month-event-count {
+  display: none;
+}
+
+.device .phui-calendar-month-event-count {
+  display: block;
+  text-align: center;
+  padding-top: 12px;
+}
+
+.phui-calendar-month-event-count .phui-calendar-month-count-badge {
+  border: 1px solid {$lightgreyborder};
+  color: {$lightgreytext};
+  width: 20px;
+  height: 20px;
+  border-radius: 50%;
+  text-align: center;
+  vertical-align: middle;
+  padding: 5px 3px 0px 3px;
+  margin: 0 auto;
+}
+
+table.phui-calendar-view a.phui-calendar-date-number {
+  color: {$lightgreytext};
+  padding: 0 4px;
+  display: inline-block;
+  min-width: 16px;
+  text-align: center;
+}
+
+table.phui-calendar-view td.phui-calendar-date-number-container {
+  font-weight: normal;
+  color: {$lightgreytext};
+  border-width: 0 1px 0 1px;
+  text-align: right;
 }
 
 .phui-calendar-not-work-day {
   background-color: {$lightgreybackground};
 }
 
-.phui-calendar-today {
-  background-color: {$lightgreen};
+.phui-calendar-today-slot {
+  display: block;
+  width: 100%;
+  height: 4px;
+  padding: 0;
+  margin: 0;
 }
 
-.phui-calendar-empty {
-  background-color: {$greybackground};
+.phui-calendar-today-slot.phui-calendar-today {
+  background-color: {$lightblueborder};
 }
 
 .phui-calendar-event-empty {
   border-color: transparent;
   background: transparent;
 }
 
 .phui-calendar-view .phui-calendar-list {
-  padding: 8px;
+  padding: 1px;
+}
+
+.phui-calendar-list-item.all-day span {
+  padding: 0;
+  margin: 0;
+}
+
+.phui-calendar-view .phui-calendar-list li.phui-calendar-list-item.all-day {
+  margin: 0;
+  padding: 4px 8px;
+  background-color: {$lightpink};
+  display: block;
+  float: none;
+}
+
+li.phui-calendar-list-item.all-day:first-child {
+  margin-top: 0;
+}
+
+.phui-calendar-view .phui-calendar-list li {
+  margin: 0 8px;
+  display: inline-block;
+  float: left;
+  clear: both;
 }
 
 .phui-calendar-view .phui-calendar-list li:first-child {
-  margin-right: 16px;
+  margin-top: 8px;
 }
 
 .phui-calendar-view .phui-calendar-list-dot {
   width: 3px;
   height: 3px;
   margin-right: 4px;
   border-radius: 10px;
   position: absolute;
   top: 5px;
   left: 0;
 }
 
 .phui-calendar-view .phui-calendar-list-title {
   width: auto;
   margin-left: 10px;
   white-space: normal;
   word-break: break-word;
 }
 
+li.phui-calendar-list-item.all-day .phui-calendar-list-title a{
+  color: {$pink};
+}
+
 .phui-calendar-view .phui-calendar-list-time {
   display: none;
 }
diff --git a/webroot/rsrc/css/phui/phui-form-view.css b/webroot/rsrc/css/phui/phui-form-view.css
index c9026c3980..2f7462cf11 100644
--- a/webroot/rsrc/css/phui/phui-form-view.css
+++ b/webroot/rsrc/css/phui/phui-form-view.css
@@ -1,506 +1,510 @@
 /**
  * @provides phui-form-view-css
  */
 
 .phui-form-view {
   padding: 16px;
 }
 
 .phui-form-view.phui-form-full-width {
   padding: 0;
 }
 
 /* only used in transaction comments */
 .phui-form-shaded .phui-form-view {
   border-bottom: 1px solid #D4DAE0;
   background: #F4F5F8;
 }
 
 .phui-form-view label.aphront-form-label {
   padding-top: 5px;
   width: 19%;
   float: left;
   text-align: right;
   font-weight: bold;
   font-size: 13px;
   color: {$bluetext};
   -webkit-font-smoothing: antialiased;
 }
 
 .device-phone .phui-form-view label.aphront-form-label,
 .phui-form-full-width.phui-form-view label.aphront-form-label {
   display: block;
   float: none;
   text-align: left;
   width: 100%;
   margin-bottom: 3px;
 }
 
 .aphront-form-input {
   margin-left: 20%;
   margin-right: 20%;
   width: 60%;
 }
 
 .device-phone .aphront-form-input,
 .device .aphront-form-input select,
 .device .aphront-form-input pre,
 .phui-form-full-width .aphront-form-input {
   margin-left: 0%;
   margin-right: 0%;
   width: 100%;
 }
 
 .aphront-form-input *::-webkit-input-placeholder {
   color:{$greytext} !important;
 }
 
 .aphront-form-input *::-moz-placeholder {
   color:{$greytext} !important;
   opacity: 1; /* Firefox nudges the opacity to 0.4 */
 }
 
 .aphront-form-input *:-ms-input-placeholder  {
   color:{$greytext} !important;
 }
 
 
 .aphront-form-error {
   width: 18%;
   float: right;
   color: {$red};
   font-weight: bold;
   padding-top: 5px;
 }
 
 .aphront-form-label .aphront-form-error {
   display: none;
 }
 
 .aphront-dialog-body .phui-form-view {
   padding: 0;
 }
 
 .device-phone .aphront-form-error,
 .phui-form-full-width .aphront-form-error {
   display: none;
 }
 
 .device-phone .aphront-form-label .aphront-form-error,
 .phui-form-full-width .aphront-form-label .aphront-form-error {
   display: block;
   float: right;
   padding: 0;
   width: auto;
 }
 
 .device-phone .aphront-form-drag-and-drop-upload {
   display: none;
 }
 
 .aphront-form-required {
   font-weight: normal;
   color: {$lightgreytext};
   font-size: 11px;
   -webkit-font-smoothing: antialiased;
 }
 
 .aphront-form-input input {
   width: 100%;
 }
 
 .aphront-form-cvc-input input {
   width: 64px;
 }
 
 .aphront-form-input textarea {
   display: block;
   width: 100%;
   box-sizing: border-box;
   height: 12em;
 }
 
 .aphront-form-control {
   padding: 4px;
 }
 
 .phui-form-full-width .aphront-form-control {
   padding: 4px 0;
 }
 
 .aphront-form-control-submit button,
 .aphront-form-control-submit a.button {
   float: right;
   margin: 4px 0 0 8px;
 }
 
 .phui-form-control-multi-submit input,
 .phui-form-control-multi-submit button,
 .phui-form-control-multi-submit a {
   float: right;
   margin: 4px 0 0 8px;
   width: auto;
 }
 
 .aphront-form-control-textarea textarea.aphront-textarea-very-short {
   height: 44px;
 }
 
 .aphront-form-control-textarea textarea.aphront-textarea-very-tall {
   height: 24em;
 }
 
 .aphront-form-control-select .aphront-form-input {
   padding-top: 2px;
 }
 
 .phui-form-view .aphront-form-caption {
   font-size: 12px;
   color: {$bluetext};
   padding: 8px 0;
   margin-right: 20%;
   margin-left: 20%;
   -webkit-font-smoothing: antialiased;
   line-height: 15px;
 }
 
 .device-phone .phui-form-view .aphront-form-caption,
 .phui-form-full-width .phui-form-view .aphront-form-caption {
   margin: 0;
 }
 
 .aphront-form-instructions {
   width: 60%;
   margin-left: 20%;
   padding: 10px 4px;
 }
 
 .device .aphront-form-instructions,
 .phui-form-full-width .aphront-form-instructions {
   width: 100%;
   margin: 0;
 }
 
 .aphront-form-important {
   margin: .5em 0;
   background: #ffffdd;
   padding: .5em 1em;
 }
 .aphront-form-important code {
   display: block;
   padding: .25em;
   margin: .5em 2em;
 }
 
 .aphront-form-control-static .aphront-form-input,
 .aphront-form-control-markup .aphront-form-input {
   padding-top: 6px;
   font-size: 13px;
 }
 
 .aphront-form-control-togglebuttons .aphront-form-input {
   padding: 2px 0 0 0;
 }
 
 table.aphront-form-control-radio-layout,
 table.aphront-form-control-checkbox-layout {
   margin-top: 3px;
   font-size: 13px;
 }
 
 table.aphront-form-control-radio-layout th {
   padding-top: 3px;
   padding-left: 8px;
   padding-bottom: 4px;
   font-weight: bold;
   color: {$darkgreytext};
 }
 
 
 table.aphront-form-control-checkbox-layout th {
   padding-top: 2px;
   padding-left: 8px;
   padding-bottom: 4px;
   color: {$darkgreytext};
 }
 
 .aphront-form-control-radio-layout td input,
 .aphront-form-control-checkbox-layout td input {
   margin-top: 4px;
   width: auto;
 }
 
 .aphront-form-control-radio-layout label.disabled,
 .aphront-form-control-checkbox-layout label.disabled {
   color: {$greytext};
 }
 
 .aphront-form-radio-caption {
   margin-top: 4px;
   font-size: 12px;
   font-weight: normal;
   color: #555;
 }
 
 .aphront-form-control-image span {
   margin: 0 4px 0 2px;
 }
 
 .aphront-form-control-image .default-image {
   display: inline;
   width: 12px;
 }
 
 .aphront-form-input hr {
   border: none;
   background: #bbbbbb;
   height: 1px;
   position: relative;
 }
 
 .phui-form-inset {
   margin: 4px 0 8px;
   padding: 8px;
   background: #f7f9fd;
   border: 1px solid {$lightblueborder};
   border-bottom: 1px solid {$blueborder};
   border-radius: 3px;
 }
 
 .phui-form-inset h1 {
   color: {$bluetext};
   padding-bottom: 8px;
   margin-bottom: 8px;
   font-size: 14px;
   border-bottom: 1px solid {$thinblueborder};
 }
 
 .aphront-form-drag-and-drop-file-list {
   width: 400px;
 }
 
 .drag-and-drop-instructions {
   color: {$darkgreytext};
   font-size: 11px;
   padding: 6px 8px;
 }
 
 .drag-and-drop-file-target {
   border: 1px dashed #bfbfbf;
   padding-top: 12px;
   padding-bottom: 12px;
 }
 
 .aphront-textarea-drag-and-drop {
   background: {$lightgreen};
   border-color: {$green};
 }
 
 .aphront-form-crop .crop-box {
   cursor: move;
   overflow: hidden;
 }
 
 .aphront-form-crop .crop-box .crop-image {
   position: relative;
   top: 0px;
   left: 0px;
 }
 
 .calendar-button {
   display: inline;
   padding: 8px 4px;
   margin: 2px 8px 2px 2px;
   position: relative;
 }
 
 .aphront-form-date-container {
   position: relative;
   display: inline;
 }
 
 .aphront-form-date-container select {
   margin: 2px;
   display: inline;
 }
 .aphront-form-date-container input.aphront-form-date-enabled-input {
   width: auto;
   display: inline;
   margin-right: 8px;
   font-size: 16px;
 }
 
 .aphront-form-date-container input.aphront-form-date-time-input {
   width: 7em;
   display: inline;
 }
 
 .fancy-datepicker {
   position: absolute;
   width: 240px;
 }
 
 .fancy-datepicker-core {
   padding: 1px;
   font-size: 12px;
   text-align: center;
 }
 
 .fancy-datepicker-core .month-table,
 .fancy-datepicker-core .day-table {
   margin: 0 auto;
   border-collapse: separate;
   border-spacing: 1px;
   width: 100%;
 }
 
 .fancy-datepicker-core .month-table {
   margin-bottom: 6px;
   font-size: 13px;
   background-color: {$hoverblue};
   border-radius: 2px;
 }
 
 .fancy-datepicker-core .month-table td.lrbutton {
   width: 18%;
   color: {$lightbluetext};
 }
 
 .fancy-datepicker-core .month-table td {
   padding: 4px;
   font-weight: bold;
   color: {$bluetext};
 }
 
 .fancy-datepicker-core .month-table td.lrbutton:hover {
   border-radius: 2px;
   background: {$hoverselectedblue};
   color: {$darkbluetext};
 }
 
 .fancy-datepicker-core .day-table td {
   overflow: hidden;
   vertical-align: center;
   text-align: center;
   border: 1px solid {$thinblueborder};
   padding: 4px 0;
 }
 
 .fancy-datepicker .fancy-datepicker-core .day-table td.day:hover {
   background-color: {$hoverblue};
   border-color: {$lightblueborder};
 }
 
 .fancy-datepicker-core .day-table td.day-placeholder {
   border-color: transparent;
   background: transparent;
 }
 
 .fancy-datepicker-core .day-table td.weekend {
   color: {$lightgreytext};
   border-color: {$lightgreyborder};
   background: {$lightgreybackground};
 }
 
 .fancy-datepicker-core .day-table td.day-name {
   background: transparent;
   border: 1px transparent;
   vertical-align: bottom;
   color: {$lightgreytext};
 }
 
 .fancy-datepicker-core .day-table td.today {
   background: {$greybackground};
   border-color: {$greyborder};
   color: {$darkgreytext};
   font-weight: bold;
 }
 
 .fancy-datepicker-core .day-table td.datepicker-selected {
   background: {$lightgreen};
   border-color: {$green};
   color: {$green};
 }
 
 .fancy-datepicker-core td {
   cursor: pointer;
 }
 
 .fancy-datepicker-core td.novalue {
   cursor: inherit;
 }
 
 .picker-open .calendar-button .phui-icon-view {
   color: {$sky};
 }
 
 .fancy-datepicker-core {
   background-color: white;
   border: 1px solid {$lightblueborder};
   border-bottom: 1px solid {$blueborder};
   box-shadow: {$dropshadow};
   border-radius: 3px;
 }
 
 /* When the activation checkbox for the control is toggled off, visually
 disable the individual controls. We don't actually use the "disabled" property
 because we still want the values to submit. This is just a visual hint that
 the controls won't be used. The controls themselves are still live, work
 properly, and submit values. */
 .datepicker-disabled select,
 .datepicker-disabled .calendar-button,
 .datepicker-disabled input[type="text"] {
   opacity: 0.5;
 }
 
+.aphront-form-date-container.no-time .aphront-form-date-time-input{
+  display: none;
+}
+
 
 .login-to-comment {
   margin: 12px;
 }
 
 .phui-form-divider hr {
   height: 1px;
   border: 0;
   background: {$thinblueborder};
   width: 85%;
   margin: 15px auto;
 }
 
 .recaptcha_only_if_privacy {
   display: none;
 }
 
 .phabricator-standard-custom-field-header {
   font-size: 16px;
   color: {$bluetext};
   border-bottom: 1px solid {$lightbluetext};
   padding: 16px 0 4px;
   margin-bottom: 4px;
 }
 
 .device-desktop .text-with-submit-control-outer-bounds {
   position: relative;
 }
 
 .device-desktop .text-with-submit-control-text-bounds {
   position: absolute;
   left: 0;
   right: 184px;
 }
 
 .device-desktop .text-with-submit-control-submit-bounds {
   text-align: right;
 }
 
 .device-desktop .text-with-submit-control-submit {
   width: 180px;
 }
 
 .aphront-form-choose-table td {
   vertical-align: middle;
   padding: 4px 0;
 }
 
 .aphront-form-choose-table .aphront-form-choose-button-cell {
   padding: 4px 8px;
 }
diff --git a/webroot/rsrc/css/phui/phui-header-view.css b/webroot/rsrc/css/phui/phui-header-view.css
index 6352681156..a270536711 100644
--- a/webroot/rsrc/css/phui/phui-header-view.css
+++ b/webroot/rsrc/css/phui/phui-header-view.css
@@ -1,144 +1,145 @@
 /**
  * @provides phui-header-view-css
  */
 
 .phui-header-shell {
   border-width: 1px 0;
   border-style: solid;
   border-color: {$hovergrey};
   overflow: hidden;
 }
 
 .phui-header-shell.sprite-gradient.gradient-white-header {
   background: transparent;
 }
 
 body .phui-header-shell.phui-header-no-backgound {
   background-color: transparent;
   border: none;
 }
 
 body .phui-header-shell.phui-bleed-header {
   background-color: #fff;
   border-bottom: 1px solid {$thinblueborder};
   width: auto;
   margin: 16px;
 }
 
 body .phui-header-shell.phui-bleed-header
   .phui-header-view {
   padding: 8px 24px 8px 0;
   color: {$bluetext};
 }
 
 .phui-header-shell + .phabricator-form-view {
   border-top-width: 0;
 }
 
 .phui-property-list-view + .diviner-document-section {
   margin-top: -1px;
 }
 
 .phui-header-view {
   padding: 16px;
   font-size: 15px;
   color: #000;
   position: relative;
 }
 
 .phui-header-view a,
 .phui-header-view a.simple {
   color: {$darkbluetext};
 }
 
 .phui-header-view .phui-header-action-links {
   float: right;
 }
 
 .phui-object-box .phui-header-view .phui-header-action-links {
   margin-right: 12px;
   margin-top: 4px;
 }
 
 .device-phone .phui-object-box .phui-header-view .phui-header-action-links {
   margin-right: 4px;
   margin-top: -1px;
 }
 
 .device-phone .phui-header-action-link .phui-button-text {
   visibility: hidden;
   width: 0;
   margin-left: 8px;
 }
 
 .phui-header-divider {
   margin: 0 4px;
   font-weight: normal;
   color: {$lightbluetext};
 }
 
 body.device-phone .phui-header-view {
   padding: 12px 8px;
 }
 
 .phui-header-tags {
   margin-left: 12px;
   font-size: 13px;
 }
 
 .phui-header-tags .phui-tag-view {
   margin-left: 4px;
 }
 
 .phui-header-image {
   display: inline-block;
   background-repeat: no-repeat;
+  background-size: 100%;
   border: 2px solid white;
   width: 50px;
   height: 50px;
   margin: 12px;
   float: left;
   border-radius: 2px;
 }
 
 .phui-header-subheader {
   font-weight: normal;
   font-size: 14px;
   margin-top: 6px;
 }
 
 .phui-header-subheader .phui-icon-view {
   display: inline-block;
   margin: -2px 4px -2px 0;
   font-size: 15px;
 }
 
 .phui-header-subheader,
 .phui-header-subheader .policy-link {
   color: {$darkbluetext};
 }
 
 .phui-header-subheader .phui-header-status-dark {
   color: {$indigo};
   text-shadow: 0 1px #fff;
 }
 
 .phui-header-subheader .phui-header-status-dark .phui-icon-view {
   color: {$indigo};
 }
 
 .phui-header-subheader .phui-header-status-red {
   color: {$red};
 }
 
 .phui-header-subheader .phui-header-status-green {
   color: {$green};
 }
 
 .phui-header-action-links .phui-mobile-menu {
   display: none;
 }
 
 .device .phui-header-action-links .phui-mobile-menu {
   display: inline-block;
 }
diff --git a/webroot/rsrc/css/phui/phui-object-item-list-view.css b/webroot/rsrc/css/phui/phui-object-item-list-view.css
index a8ba8d5c01..a6251754ef 100644
--- a/webroot/rsrc/css/phui/phui-object-item-list-view.css
+++ b/webroot/rsrc/css/phui/phui-object-item-list-view.css
@@ -1,702 +1,702 @@
 /**
  * @provides phui-object-item-list-view-css
  */
 
 ul.phui-object-item-list-view {
   padding: 8px;
   list-style: none;
 }
 
 .device-desktop .phui-object-item-list-view {
   padding: 16px;
 }
 
 .phui-object-item-list-view + .phui-object-item-list-view {
   padding-top: 0;
 }
 
 .phui-object-item-list-view.phui-object-list-flush {
   padding: 0;
 }
 
 .phui-object-box .phui-object-list-flush .phui-object-item,
 .homepage-panel .phui-object-list-flush .phui-object-item {
   margin: 0;
 }
 
 .phui-object-item-list-view .phui-info-view {
   margin: 0;
 }
 
 .phui-object-box .phui-object-item-list-view .phui-info-view {
   margin: 4px 0;
   color: {$greytext};
   border: none;
 }
 
 .phui-object-item {
   border-style: solid;
   border-color: {$lightgreyborder};
   margin: 5px 0;
   overflow: hidden;
   background: #fff;
   margin-bottom: 4px;
 }
 
 .phui-object-item .phui-icon-view {
   display: inline-block;
 }
 
 .phui-object-item-frame {
   border: 1px solid {$lightgreyborder};
   position: relative;
   min-height: 33px;
   overflow: hidden;
 }
 
 .device-desktop .phui-object-item {
   margin: 0 0 4px 0;
 }
 
 .phui-object-box .phui-object-list-flush .phui-object-item {
   margin: 0;
 }
 
 .phui-object-item-name {
   font-weight: bold;
   padding: 8px 8px 0;
   white-space: nowrap;
   word-wrap: break-word;
   overflow: hidden;
   text-overflow: ellipsis;
 }
 
 .device-phone .phui-object-item-name {
   overflow: normal;
   white-space: normal;
 }
 
 .phui-object-item-link {
   display: inline;
 }
 
 .phui-object-item-objname {
   color: {$darkgreytext};
   cursor: text;
 }
 
 .phui-object-item-content {
   margin: 4px 8px 2px 0;
   overflow: hidden;
 }
 
 .phui-object-item-grippable {
   cursor: move;
 }
 
 .device .phui-object-item-grippable {
   cursor: normal;
 }
 
 .phui-object-item-grip {
   position: absolute;
   top: 0;
   bottom: 0;
   left: 0;
   width: 17px;
   background: url('/rsrc/image/texture/grip.png') center center no-repeat;
 }
 
 .device .phui-object-item-grip {
   display: none;
 }
 
 .phui-object-item-grippable .phui-object-item-frame {
   padding-left: 11px;
 }
 
 .device .phui-object-item-grippable .phui-object-item-frame {
   padding-left: 0;
 }
 
 .phui-object-item-list-header {
   padding: 0 0 8px 0;
   color: {$darkgreytext};
 }
 
 .phui-object-item-table {
   display: table;
   table-layout: fixed;
   width: 100%;
 }
 
 .phui-object-item-table-row {
   display: table-row;
 }
 
 .phui-object-item-col1 {
   display: table-cell;
   vertical-align: top;
 }
 
 .phui-object-item-col2 {
   width: 160px;
   display: table-cell;
   vertical-align: top;
 }
 
 .device-phone .phui-object-item-col1,
 .device-phone .phui-object-item-col2 {
   display: block;
   width: auto;
 }
 
 /* - Item Actions --------------------------------------------------------------
 
   Action buttons, like "Edit" and "Delete".
 
 */
 
 .phui-object-item-actions {
   position: absolute;
   right: 0;
   top: 0;
   bottom: 0;
   vertical-align: middle;
   text-align: right;
   border-left: 1px solid {$lightgreyborder};
 }
 
 .phui-object-item-actions .phui-list-item-view {
   float: right;
   height: 100%;
   width: 24px;
   display: inline-block;
   position: relative;
 }
 
 .phui-object-item-actions .phui-list-item-view +
 .phui-list-item-view {
   border-right: 1px solid #d6d6e9;
 }
 
 .phui-object-item-actions .phui-list-item-href {
   display: inline-block;
   position: relative;
   width: 24px;
   height: 100%;
 }
 
 .device-desktop .phui-object-item-actions .phui-list-item-href:hover {
   background: {$hoverblue};
 }
 
 .phui-object-item-actions .phui-list-item-icon {
   width: 14px;
   height: 14px;
   position: absolute;
   display: block;
   top: 50%;
   margin-top: -7px;
   left: 3px;
 }
 
 .phui-object-item-actions .phui-list-item-name {
   display: none;
 }
 
 .phui-object-item-with-1-actions .phui-object-item-content-box {
   margin-right: 24px;
   overflow: hidden;
 }
 
 .phui-object-item-with-2-actions .phui-object-item-content-box {
   margin-right: 48px;
   overflow: hidden;
 }
 
 .phui-object-item-with-3-actions .phui-object-item-content-box {
   margin-right: 72px;
   overflow: hidden;
 }
 
 
 /* - Stackable List ------------------------------------------------------------
 
   Tighter, stacking list.
 
 */
 
 .phui-object-item-list-view.phui-object-list-stackable
   .phui-object-item {
   margin: -1px 0 0 0;
   background: #fff;
 }
 
 .phui-object-box .phui-object-list-stackable {
   padding: 0;
 }
 
 .phui-object-box .phui-object-list-stackable .phui-object-item {
   border: none;
 }
 
 .phui-object-box .phui-object-list-stackable .phui-object-item-frame {
   border-right: none;
 }
 
 .phui-object-box .phui-object-item:last-child
   .phui-object-item-frame {
   border-bottom: none;
 }
 
 
 /* - Subhead -------------------------------------------------------------------
 
   Descriptive Text or Links under the main header, before attributes.
 
 */
 
 .phui-object-item-subhead {
   color: {$greytext};
   padding: 0 8px 6px;
 }
 
 
 /* - Attribute List ------------------------------------------------------------
 
   Object attributes, commonly used to render created date, etc.
 
 */
 
 .phui-object-item-attributes {
   padding: 0 8px 6px;
   line-height: 18px;
 }
 
 .phui-object-item-attribute {
   display: inline;
   color: {$greytext};
 }
 
 .phui-object-item-attribute-spacer {
   padding: 0 4px;
 }
 
 
 /* - Icons ---------------------------------------------------------------------
 
   Icons, which show object state. On mobile, they are rendered without labels
   to save space.
 
 */
 
 .phui-object-icon-pane {
   margin: 8px 0 4px;
 }
 
 .device-phone .phui-object-icon-pane {
   margin: 0 0 4px;
 }
 
 .phui-object-item-with-handle-icons .phui-object-item-icons {
   padding-bottom: 30px;
 }
 
 .phui-object-item-icons {
   padding: 0 10px 0 0;
 }
 
 .device-phone .phui-object-item-icons {
   padding: 0 0 0 8px;
 }
 
 ul.phui-object-item-icons {
   margin: 0;
 }
 
 .phui-object-item-icon {
   vertical-align: middle;
   font-size: 12px;
   color: {$lightgreytext};
   text-align: right;
   white-space: nowrap;
   overflow: hidden;
   min-height: 18px;
   line-height: 18px;
 }
 
 .device-phone .phui-object-item-icon {
   text-align: left;
   font-size: 13px;
 }
 
 /*
  * Items with icon 'none' still have on mobile, thus creating a weird vertical
  * margin for elements which follow
  */
 .device-phone .phui-object-item-icon .none {
   display: none;
 }
 
 .phui-object-item-icon-image {
   width: 14px;
   height: 14px;
   font-size: 13px;
   margin-right: 4px;
 }
 
 /* - Disabled ------------------------------------------------------------------
 
   Disabled/inactive objects.
 
 */
 
 .phui-object-item-disabled {
   border-left-color: #d7d7d7;
 }
 
 .phui-object-item-disabled .phui-object-item-link,
 .phui-object-item-disabled .phui-object-item-link a {
   color: {$lightgreytext};
 }
 
 .phui-object-item-disabled .phui-object-item-frame {
   border-color: #d7d7d7;
 }
 
 .phui-object-item-disabled .phui-object-item-objname {
   color: {$greytext};
   text-decoration: line-through;
 }
 
 
 /* - Effects -------------------------------------------------------------------
 
   Effects like highlighted items.
 
 */
 
 .phui-object-item.phui-object-item-highlighted {
   background: {$lightyellow};
 }
 
 .phui-object-item-highlighted .phui-object-item-frame {
   border-color: {$yellow};
 }
 
 .phui-object-item-selected {
   background: {$lightblue};
 }
 
 .phui-object-item-selected .phui-object-item-frame {
   border-color: {$blue};
 }
 
 
 /* - Foot Icons ----------------------------------------------------------------
 
   Object counts shown in the footer.
 
 */
 
 .phui-object-item-foot-icons {
   margin-left: 10px;
   bottom: 0;
   position: absolute;
 }
 
 .phui-object-item-with-foot-icons .phui-object-item-content,
 .device-phone .phui-object-item-with-foot-icons .phui-object-item-col2  {
   padding-bottom: 24px;
 }
 
 .device-phone .phui-object-item-with-foot-icons .phui-object-item-content {
   padding-bottom: 0;
 }
 
 .phui-object-item-foot-icon {
   display: inline-block;
   background: {$lightgreyborder};
   color: #ffffff;
   font-weight: bold;
   margin-right: 3px;
   padding: 3px 6px 0;
   height: 17px;
   vertical-align: middle;
   position: relative;
   font-size: 12px;
   -webkit-font-smoothing: antialiased;
 }
 
 .phui-object-item-foot-icon .phui-icon-view {
   margin-right: 4px;
 }
 
 
 /* - Handle Icons --------------------------------------------------------------
 
   Shows owners, reviewers, etc., using profile picture icons.
 
 */
 
 .phui-object-item-handle-icons {
   height: 28px;
   margin-right: 10px;
   bottom: 0;
   right: 0;
   text-align: right;
   position: absolute;
 }
 
 .phui-object-item-handle-icon {
   margin: 1px;
   width: 28px;
   height: 28px;
   display: inline-block;
   background-size: 28px 28px;
   background-repeat: no-repeat;
 }
 
 
 /* - Bylines -------------------------------------------------------------------
 
   Shows owners, authors, reviewers, etc., in text.
 
 */
 
 .phui-object-item-bylines {
   padding: 0 10px;
   margin: 4px 0 8px;
   font-size: 12px;
   color: {$lightgreytext};
   text-align: right;
 }
 
 .phui-object-item-byline {
   white-space: nowrap;
   text-overflow: ellipsis;
   overflow: hidden;
 }
 
 .device-phone .phui-object-item-bylines {
   float: none;
   text-align: left;
   padding: 0 8px;
   font-size: 13px;
 }
 
 
 /* - Draggable List ------------------------------------------------------------
 
   These classes are applied by and/or provided for use with JX.DraggableList.
 
 */
 
 .drag-ghost {
   position: relative;
   border: 1px dashed #fff;
   background: rgba(255,255,255,.5);
   margin-bottom: 4px;
 }
 
 .phui-object-list-stackable .drag-ghost {
   background: {$hoverblue};
   margin: 0;
   border: none;
   border-top: 1px solid {$lightgreyborder}
 }
 
 .drag-dragging {
   position: relative;
   opacity: 0.85;
 }
 
 .phui-object-box .phui-object-list-stackable .drag-dragging
  .phui-object-item-frame {
   border-bottom: 1px solid {$lightgreyborder};
 }
 
 .drag-sending {
   opacity: 0.5;
 }
 
 /* - Plain ---------------------------------------------------------------------
 
   Remove all border styles, just a list of objects
 
 */
 
 .phui-object-list-plain .phui-object-item {
   background: transparent;
 }
 
 .phui-object-list-plain .phui-object-item,
 .phui-object-list-plain .phui-object-item-frame {
   border: none;
 }
 
 .phui-object-list-plain .phui-object-item-attributes,
 .phui-object-list-plain .phui-object-item-name {
   padding-left: 0;
   padding-top: 0;
 }
 
 .phui-object-item-image {
   width: 40px;
   height: 40px;
-  background-size: 40px;
+  background-size: 100%;
   margin: 6px;
   position: absolute;
   background-color: {$lightbluebackground};
 }
 
 .phui-object-item-with-image .phui-object-item-frame {
   min-height: 52px;
 }
 
 .phui-object-item-with-image .phui-object-item-content-box {
   margin-left: 46px;
 }
 
 /* - State ---------------------------------------------------------------------
 
   Provides a list of object status or states, success or fail, etc
 
 */
 
 .phui-object-item-ficon {
   width: 48px;
   height: 26px;
   margin-top: 12px;
   position: absolute;
   text-align: center;
   font-size: 24px;
 }
 
 .phui-object-item-with-ficon .phui-object-item-content-box {
   margin-left: 38px;
 }
 
 .phui-object-box .phui-object-list-states {
   padding: 0;
 }
 
 .phui-object-list-states .phui-info-view {
   margin: 0;
   border: none;
 }
 
 /* - Dashboards ------------------------------------------------------------ */
 
 .dashboard-panel .phui-object-item-list-view {
   padding: 0;
   border-left: 1px solid {$lightblueborder};
   border-right: 1px solid {$lightblueborder};
   border-bottom: 1px solid {$lightblueborder};
   margin-bottom: -1px;
 }
 
 .dashboard-panel .phui-object-item-list-view .phui-object-item,
 .phui-object-box .phui-object-item-list-view.phui-object-list-flush {
   margin: 0;
   background-image: none;
   background-color: #fff;
 }
 
 .dashboard-panel .phui-object-item-frame,
 .phui-object-box .phui-object-list-flush .phui-object-item-frame {
   border: none;
   border-bottom: 1px solid {$thinblueborder};
 }
 
 .dashboard-panel .phui-object-item-list-header,
 .dashboard-panel .maniphest-task-group-header {
   font-size: 13px;
   color: {$bluetext};
   background: {$lightgreybackground};
   border-bottom: 1px solid {$thinblueborder};
   padding: 8px 12px;
   -webkit-font-smoothing: antialiased;
 }
 
 .dashboard-panel .phui-object-item-empty .phui-info-view {
   border: none;
   border-bottom: 1px solid {$thinblueborder};
   margin: 0;
 }
 
 .device-desktop .aphront-multi-column-fluid .aphront-multi-column-2-up
  .aphront-multi-column-column-outer.third .phui-object-item-col2 {
    display: none;
  }
 
  .dashboard-panel .phui-object-box .phui-header-shell {
    display: none;
  }
 
  .dashboard-panel .phui-object-box {
    margin: 0;
  }
 
 
 /* - Launcher List ---------------------------------------------------------- */
 
 .launcher-header {
   margin: 8px 16px -4px;
   clear: both;
   color: {$darkbluetext};
 }
 
 .launcher-header:nth-of-type(1) {
   margin-top: 24px;
 }
 
 .phui-object-item-launcher-list {
   overflow: hidden;
 }
 
 .device-desktop .phui-object-item-launcher-list .phui-object-item {
   width: 49%;
   float: left;
   margin-right: 1%;
   box-sizing: border-box;
 }
 
 .phui-object-item-image-icon {
   background: none;
 }
 
 .phui-object-item-image-icon {
   width: 30px;
   height: 30px;
   margin: 4px;
   position: absolute;
 }
 
 .phui-object-item-image-icon .phui-icon-view {
   position: absolute;
   width: 24px;
   height: 24px;
   left: 6px;
   top: 10px;
   font-size: 24px;
   text-align: center;
   vertical-align: bottom;
 }
 
 .phui-object-item-with-image-icon .phui-object-item-frame {
   min-height: 48px;
 }
 
 .phui-object-item-with-image-icon .phui-object-item-content-box {
   margin-left: 36px;
 }
 
 .device-desktop .phui-object-item-launcher-list .phui-object-item-content {
   margin-right: 0;
 }
 
 .device-desktop .phui-object-item-launcher-list .phui-object-icon-pane {
   width: auto;
 }
diff --git a/webroot/rsrc/css/phui/phui-timeline-view.css b/webroot/rsrc/css/phui/phui-timeline-view.css
index b82c9385a7..d35a5458ee 100644
--- a/webroot/rsrc/css/phui/phui-timeline-view.css
+++ b/webroot/rsrc/css/phui/phui-timeline-view.css
@@ -1,376 +1,377 @@
 /**
  * @provides phui-timeline-view-css
  */
 
 .phui-timeline-view {
   padding: 0 16px;
   background-image: url('/rsrc/image/BFCFDA.png');
   background-repeat: repeat-y;
   background-position: 94px;
 }
 
 .device-tablet .phui-timeline-view {
   background-position: 31px;
 }
 
 .device-phone .phui-timeline-view {
   padding: 0;
   background-position: 24px;
 }
 
 .phui-timeline-major-event .phui-timeline-group {
   border-left: 1px solid {$lightblueborder};
   border-right: 1px solid {$lightblueborder};
 }
 
 .device-desktop .phui-timeline-event-view {
   margin-left: 62px;
   position: relative;
 }
 
 .device-desktop .phui-timeline-event-view.phui-timeline-minor-event {
   margin-left: 65px;
 }
 
 .device-desktop .phui-timeline-spacer {
   min-height: 16px;
 }
 
 .device-desktop .phui-timeline-event-view.the-worlds-end {
   background: {$lightblueborder};
   width: 9px;
   height: 9px;
   border-radius: 2px;
   margin-left: 74px;
 }
 
 .device-desktop .phui-timeline-wedge {
   border-bottom: 1px solid {$lightblueborder};
   position: absolute;
   width: 12px;
 }
 
 .device-phone .phui-timeline-minor-event,
 .device-tablet .phui-timeline-minor-event {
   padding-left: 3px;
 }
 
 .phui-timeline-major-event .phui-timeline-content {
   border-top: 1px solid {$lightblueborder};
   border-bottom: 1px solid {$lightblueborder};
 }
 
 .phui-timeline-title {
   line-height: 18px;
   min-height: 19px;
   position: relative;
   color: {$bluetext};
 }
 
 .phui-timeline-minor-event .phui-timeline-title {
   padding: 4px 8px 4px 33px;
 }
 
 .phui-timeline-title a {
   font-weight: bold;
   color: {$darkbluetext};
 }
 
 .device-desktop .phui-timeline-wedge {
   left: -12px;
 }
 
 .device-desktop .phui-timeline-major-event .phui-timeline-wedge {
   top: 24px;
 }
 
 .device-desktop .phui-timeline-minor-event .phui-timeline-wedge {
   top: 12px;
   left: -18px;
   width: 20px;
 }
 
 .phui-timeline-image {
   background-repeat: no-repeat;
+  background-size: 100%;
   position: absolute;
   border-radius: 3px;
 }
 
 .device-desktop .phui-timeline-major-event .phui-timeline-image {
   width: 50px;
   height: 50px;
   top: 0px;
   left: -62px;
 }
 
 .device-desktop .phui-timeline-minor-event .phui-timeline-image {
   width: 26px;
   height: 26px;
   background-size: 26px auto;
   left: -41px;
 }
 
 .phui-timeline-major-event .phui-timeline-title {
   background: {$lightgreybackground};
   min-height: 18px;
 }
 
 .phui-timeline-title {
   padding: 5px 8px;
   overflow-x: auto;
   overflow-y: hidden;
 }
 
 .phui-timeline-title-with-icon {
   padding-left: 38px;
 }
 
 .phui-timeline-title-with-menu {
   padding-right: 36px;
 }
 
 .phui-timeline-view .phui-icon-view.phui-timeline-token {
   vertical-align: middle;
   margin-right: 4px;
 }
 
 .phui-timeline-token.strikethrough {
   position: relative;
 }
 
 .phui-timeline-token.strikethrough:before {
   position: absolute;
   content: "";
   left: 0;
   top: 50%;
   right: 0;
   border-top: 3px solid;
   border-color: {$darkbluetext};
 
   -webkit-transform:rotate(-40deg);
   -moz-transform:rotate(-40deg);
   -ms-transform:rotate(-40deg);
   -o-transform:rotate(-40deg);
   transform:rotate(-40deg);
 }
 
 .phui-timeline-major-event .phui-timeline-content
   .phui-timeline-core-content {
   padding: 16px 12px;
   line-height: 18px;
   background: #fff;
 }
 
 .phui-timeline-core-content {
   overflow-x: auto;
 }
 
 .phui-timeline-core-content .comment-deleted {
   font-style: italic;
 }
 
 .device .phui-timeline-event-view {
   min-height: 23px;
   position: relative;
 }
 
 .device-phone .phui-timeline-event-view {
   margin: 0 8px;
 }
 
 .device .phui-timeline-image {
   display: none;
 }
 
 .device .phui-timeline-spacer {
   min-height: 8px;
   border-width: 0;
 }
 
 .phui-timeline-spacer.phui-timeline-spacer-bold {
   border-bottom: 4px solid {$lightblueborder};
   margin: 0;
 }
 
 .phui-timeline-spacer-bold + .phui-timeline-spacer {
   background-color: #ebecee;
 }
 
 .phui-timeline-icon-fill {
   position: absolute;
   width: 30px;
   height: 30px;
   background-color: {$lightblueborder};
   top: 0;
   left: 0;
   text-align: center;
 }
 
 .phui-icon-view.phui-timeline-icon:before {
   font-size: 14px;
 }
 
 .phui-timeline-minor-event .phui-timeline-icon-fill {
   height: 26px;
   width: 26px;
   border-radius: 3px;
 }
 
 .phui-timeline-icon-fill .phui-timeline-icon {
   margin-top: 7px;
 }
 
 .phui-timeline-minor-event .phui-timeline-icon-fill .phui-timeline-icon {
   margin-top: 6px;
 }
 
 .phui-timeline-extra,
 .phui-timeline-extra .phabricator-content-source-view {
   font-size: 11px;
   font-weight: normal;
   color: {$lightbluetext};
 }
 
 .phui-timeline-title .phui-timeline-extra a {
   font-weight: normal;
   color: {$bluetext};
 }
 
 .device-desktop .phui-timeline-extra {
   float: right;
 }
 
 .device .phui-timeline-extra {
   display: inline-block;
   line-height: 16px;
   margin-left: 8px;
   white-space: nowrap;
 }
 
 .device-phone .phui-timeline-extra {
   display: block;
   margin: 0;
 }
 
 .phui-timeline-icon-fill-red {
   background-color: {$red};
 }
 
 .phui-timeline-icon-fill-orange {
   background-color: {$orange};
 }
 
 .phui-timeline-icon-fill-yellow {
   background-color: {$yellow};
 }
 
 .phui-timeline-icon-fill-green {
   background-color: {$green};
 }
 
 .phui-timeline-icon-fill-sky {
   background-color: {$sky};
 }
 
 .phui-timeline-icon-fill-blue {
   background-color: {$blue};
 }
 
 .phui-timeline-icon-fill-indigo {
   background-color: {$indigo};
 }
 
 .phui-timeline-icon-fill-violet {
   background-color: {$violet};
 }
 
 .phui-timeline-icon-fill-grey {
   background-color: #888;
 }
 
 .phui-timeline-icon-fill-black {
   background-color: #333;
 }
 
 .phui-timeline-shell.anchor-target {
   background: {$lightyellow};
   padding: 4px;
   margin: -4px;
 }
 
 .phui-timeline-preview-header {
   background: #e0e3ec;
   color: {$darkgreytext};
   padding: 4px 1.25%;
   border: solid {$blueborder} 1px 0;
 }
 
 .phui-timeline-change-details {
   padding: 10px 0;
   border-style: solid;
   border-color: #efefef;
   border-width: 1px 0;
 }
 
 .phui-timeline-older-transactions-are-hidden {
   background: {$lightyellow};
   border: 1px solid {$yellow};
   text-align: center;
   padding: 12px;
   color: {$darkgreytext};
   cursor: pointer;
 }
 
 .device-phone .phui-timeline-older-transactions-are-hidden {
   margin: 0 8px;
 }
 
 
 .phui-timeline-title .phui-timeline-extra-information a {
   font-weight: normal;
   color: {$bluetext};
 }
 
 .phui-timeline-comment-actions .phui-icon-view {
   width: 16px;
   height: 16px;
   font-size: 16px;
   text-align: center;
   overflow: hidden;
 }
 
 .phui-timeline-menu {
   position: absolute;
   right: 3px;
   top: 4px;
   width: 28px;
   height: 22px;
   text-align: center;
   line-height: 22px;
   font-size: 15px;
   border-left: 1px solid {$lightblueborder};
 }
 
 .phui-timeline-menu:focus {
   outline: none;
 }
 
 .phui-timeline-menu .phui-icon-view {
   color: {$lightgreytext};
 }
 
 a.phui-timeline-menu .phui-icon-view {
   color: {$bluetext};
 }
 
 .device-desktop a.phui-timeline-menu:hover .phui-icon-view {
   color: {$darkgreytext};
 }
 
 .phui-timeline-menu.phuix-dropdown-open {
   background: {$hovergrey};
 }
 
 .phui-timeline-view + .phui-object-box {
   margin-top: 0;
 }
diff --git a/webroot/rsrc/externals/javelin/lib/Scrollbar.js b/webroot/rsrc/externals/javelin/lib/Scrollbar.js
index 684fb4ce08..7596939581 100644
--- a/webroot/rsrc/externals/javelin/lib/Scrollbar.js
+++ b/webroot/rsrc/externals/javelin/lib/Scrollbar.js
@@ -1,430 +1,458 @@
 /**
  * @provides javelin-scrollbar
  * @requires javelin-install
  *           javelin-dom
  *           javelin-stratcom
  *           javelin-vector
  * @javelin
  */
 
 /**
  * Provides an aesthetic scrollbar.
  *
  * This shoves an element's scrollbar under a hidden overflow and draws a
  * pretty looking fake one in its place. This makes complex UIs with multiple
  * independently scrollable panels less hideous by (a) making the scrollbar
  * itself prettier and (b) reclaiming the space occupied by the scrollbar.
  *
  * Note that on OSX the heavy scrollbars are normally drawn only if you have
  * a mouse connected. OSX uses more aesthetic touchpad scrollbars normally,
  * which these scrollbars emulate.
  *
  * This class was initially adapted from "Trackpad Scroll Emulator", by
  * Jonathan Nicol. See <https://github.com/jnicol/trackpad-scroll-emulator>.
  */
 JX.install('Scrollbar', {
 
   construct: function(frame) {
     this._frame = frame;
 
     JX.DOM.listen(frame, 'load', null, JX.bind(this, this._onload));
     this._onload();
 
     // Before doing anything, check if the scrollbar control has a measurable
     // width. If it doesn't, we're already in an environment with an aesthetic
     // scrollbar (like Safari on OSX with no mouse connected, or an iPhone)
     // and we don't need to do anything.
     if (JX.Scrollbar._getScrollbarControlWidth() === 0) {
       return;
     }
 
     // Wrap the frame content in a bunch of nodes. The frame itself stays on
     // the outside so that any positioning information the node had isn't
     // disrupted.
 
     // We put a "viewport" node inside of it, which is what actually scrolls.
     // This is the node that gets a scrollbar, but we make the viewport very
     // slightly too wide for the frame. That hides the scrollbar underneath
     // the edge of the frame.
 
     // We put a "content" node inside of the viewport. This allows us to
     // measure the content height so we can resize and offset the scrollbar
     // handle properly.
 
     // We move all the actual frame content into the "content" node. So it
     // ends up wrapped by the "content" node, then by the "viewport" node,
     // and finally by the original "frame" node.
 
     JX.DOM.alterClass(frame, 'jx-scrollbar-frame', true);
 
     var content = JX.$N('div', {className: 'jx-scrollbar-content'});
     while (frame.firstChild) {
       JX.DOM.appendContent(content, frame.firstChild);
     }
 
     var viewport = JX.$N('div', {className: 'jx-scrollbar-viewport'}, content);
     JX.DOM.appendContent(frame, viewport);
 
     this._viewport = viewport;
     this._content = content;
 
     // The handle is the visible node which you can click and drag.
     this._handle = JX.$N('div', {className: 'jx-scrollbar-handle'});
 
     // The bar is the area the handle slides up and down in.
     this._bar = JX.$N('div', {className: 'jx-scrollbar-bar'}, this._handle);
 
     JX.DOM.prependContent(frame, this._bar);
 
     JX.DOM.listen(this._handle, 'mousedown', null, JX.bind(this, this._ondrag));
     JX.DOM.listen(this._bar, 'mousedown', null, JX.bind(this, this._onjump));
 
     JX.enableDispatch(document.body, 'mouseenter');
     JX.DOM.listen(viewport, 'mouseenter', null, JX.bind(this, this._onenter));
 
     JX.DOM.listen(frame, 'scroll', null, JX.bind(this, this._onscroll));
 
     // Enabling dispatch for this event on `window` allows us to scroll even
     // if the mouse cursor is dragged outside the window in at least some
     // browsers (for example, Safari on OSX).
     JX.enableDispatch(window, 'mousemove');
     JX.Stratcom.listen('mousemove', null, JX.bind(this, this._onmove));
 
     JX.Stratcom.listen('mouseup', null, JX.bind(this, this._ondrop));
     JX.Stratcom.listen('resize', null, JX.bind(this, this._onresize));
 
     this._resizeViewport();
     this._resizeBar();
   },
 
   statics: {
     _controlWidth: null,
 
+
     /**
      * Compute the width of the browser's scrollbar control, in pixels.
      */
     _getScrollbarControlWidth: function() {
       var self = JX.Scrollbar;
 
       if (self._controlWidth === null) {
         var tmp = JX.$N('div', {className: 'jx-scrollbar-test'}, '-');
         document.body.appendChild(tmp);
         var d1 = JX.Vector.getDim(tmp);
         tmp.style.overflowY = 'scroll';
         var d2 = JX.Vector.getDim(tmp);
         JX.DOM.remove(tmp);
 
         self._controlWidth = (d2.x - d1.x);
       }
 
       return self._controlWidth;
+    },
+
+
+    /**
+     * Get the margin width required to avoid double scrollbars.
+     *
+     * For most browsers which render a real scrollbar control, this is 0.
+     * Adjacent elements may touch the edge of the content directly without
+     * overlapping.
+     *
+     * On OSX with a trackpad, scrollbars are only drawn when content is
+     * scrolled. Content panes with internal scrollbars may overlap adjacent
+     * scrollbars if they are not laid out with a margin.
+     *
+     * @return int Control margin width in pixels.
+     */
+    getScrollbarControlMargin: function() {
+      var self = JX.Scrollbar;
+
+      // If this browser and OS don't render a real scrollbar control, we
+      // need to leave a margin. Generally, this is OSX with no mouse attached.
+      if (self._getScrollbarControlWidth() === 0) {
+        return 12;
+      }
+
+      return 0;
     }
 
+
   },
 
   members: {
     _frame: null,
     _viewport: null,
     _content: null,
 
     _bar: null,
     _handle: null,
 
     _timeout: null,
     _dragOrigin: null,
     _scrollOrigin: null,
     _lastHeight: null,
 
 
     /**
      * Mark this content as the scroll frame.
      *
      * This changes the behavior of the @{class:JX.DOM} scroll functions so the
      * continue to work properly if the main page content is reframed to scroll
      * independently.
      */
     setAsScrollFrame: function() {
       if (this._viewport) {
         // If we activated the scrollbar, the viewport and content nodes become
         // the new scroll and content frames.
         JX.DOM.setContentFrame(this._viewport, this._content);
 
         // If nothing is focused, or the document body is focused, change focus
         // to the viewport. This makes the arrow keys, spacebar, and page
         // up/page down keys work immediately after the page loads, without
         // requiring a click.
 
         // Focusing the <div /> itself doesn't work on any browser, so we
         // add a fake, focusable element and focus that instead.
         var focus = document.activeElement;
         if (!focus || focus == window.document.body) {
           var link = JX.$N('a', {href: '#', className: 'jx-scrollbar-link'});
           JX.DOM.listen(link, 'blur', null, function() {
             // When the user clicks anything else, remove this.
             try {
               JX.DOM.remove(link);
             } catch (ignored) {
               // We can get a second blur event, likey related to T447.
               // Fix doesn't seem trivial so just ignore it.
             }
           });
           JX.DOM.listen(link, 'click', null, function(e) {
             // Don't respond to clicks. Since the link isn't visible, this
             // most likely means the user hit enter or something like that.
             e.kill();
           });
           JX.DOM.prependContent(this._viewport, link);
           JX.DOM.focus(link);
         }
       } else {
         // Otherwise, the unaltered content frame is both the scroll frame and
         // content frame.
         JX.DOM.setContentFrame(this._frame, this._frame);
       }
     },
 
 
     /**
      * After the user scrolls the page, show the scrollbar to give them
      * feedback about their position.
      */
     _onscroll: function() {
       this._showBar();
     },
 
 
     /**
      * When the user mouses over the viewport, show the scrollbar.
      */
     _onenter: function() {
       this._showBar();
     },
 
 
     /**
      * When the user resizes the window, recalculate everything.
      */
     _onresize: function() {
       this._resizeViewport();
       this._resizeBar();
     },
 
 
     /**
      * When the user clicks the bar area (but not the handle), jump up or
      * down a page.
      */
     _onjump: function(e) {
       if (e.getTarget() === this._handle) {
         return;
       }
 
       var distance = JX.Vector.getDim(this._viewport).y * (7/8);
       var epos = JX.$V(e);
       var hpos = JX.$V(this._handle);
 
       if (epos.y > hpos.y) {
         this._viewport.scrollTop += distance;
       } else {
         this._viewport.scrollTop -= distance;
       }
     },
 
 
     /**
      * When the user clicks the scroll handle, begin dragging it.
      */
     _ondrag: function(e) {
       e.kill();
 
       // Store the position where the drag started.
       this._dragOrigin = JX.$V(e);
 
       // Store the original position of the handle.
       this._scrollOrigin = this._viewport.scrollTop;
     },
 
 
     /**
      * As the user drags the scroll handle up or down, scroll the viewport.
      */
     _onmove: function(e) {
       if (this._dragOrigin === null) {
         return;
       }
 
       var p = JX.$V(e);
       var offset = (p.y - this._dragOrigin.y);
       var ratio = offset / JX.Vector.getDim(this._bar).y;
       var adjust = ratio * JX.Vector.getDim(this._content).y;
 
       if (this._shouldSnapback()) {
         if (Math.abs(p.x - this._dragOrigin.x) > 140) {
           adjust = 0;
         }
       }
 
       this._viewport.scrollTop = this._scrollOrigin + adjust;
     },
 
 
     /**
      * Should the scrollbar snap back to the original position if the user
      * drags the mouse away to the left or right, perpendicular to the
      * scrollbar?
      *
      * Scrollbars have this behavior on Windows, but not on OSX or Linux.
      */
     _shouldSnapback: function() {
       // Since this is an OS-specific behavior, detect the OS. We can't
       // reasonably use feature detection here.
       return (navigator.platform.indexOf('Win') > -1);
     },
 
 
     /**
      * When the user releases the mouse after a drag, stop moving the
      * viewport.
      */
     _ondrop: function() {
       this._dragOrigin = null;
 
       // Reset the timer to hide the bar.
       this._showBar();
     },
 
 
 
     /**
      * Something inside the frame fired a load event.
      *
      * The typical case is that an image loaded. This may have changed the
      * height of the scroll area, and we may want to make adjustments.
      */
     _onload: function() {
       var viewport = this.getViewportNode();
       var height = viewport.scrollHeight;
       var visible = JX.Vector.getDim(viewport).y;
       if (this._lastHeight !== null && this._lastHeight != height) {
 
         // If the viewport was scrollable and was scrolled down to near the
         // bottom, scroll it down to account for the new height. The effect
         // of this rule is to keep panels like the chat column scrolled to
         // the bottom as images load into the thread.
         if (viewport.scrollTop > 0) {
           if ((viewport.scrollTop + visible + 64) >= this._lastHeight) {
             viewport.scrollTop += (height - this._lastHeight);
           }
         }
 
       }
 
       this._lastHeight = height;
     },
 
 
     /**
      * Shove the scrollbar on the viewport under the edge of the frame so the
      * user can't see it.
      */
     _resizeViewport: function() {
       var fdim = JX.Vector.getDim(this._frame);
       fdim.x += JX.Scrollbar._getScrollbarControlWidth();
       fdim.setDim(this._viewport);
     },
 
 
     /**
      * Figure out the correct size and offset of the scrollbar handle.
      */
     _resizeBar: function() {
       // We're hiding and showing the bar itself, not just the handle, because
       // pages that contain other panels may have scrollbars underneath the
       // bar. If we don't hide the bar, it ends up eating clicks targeting
       // these panels.
 
       // Because the bar may be hidden, we can't measure it. Measure the
       // viewport instead.
 
       var cdim = JX.Vector.getDim(this._content);
       var spos = JX.Vector.getAggregateScrollForNode(this._viewport);
       var vdim = JX.Vector.getDim(this._viewport);
 
       var ratio = (vdim.y / cdim.y);
 
       // We're scaling things down very slightly to leave a 2px margin at
       // either end of the scroll gutter, so the bar doesn't quite bump up
       // against the chrome.
       ratio = ratio * (vdim.y / (vdim.y + 4));
 
       var offset = Math.round(ratio * spos.y) + 2;
       var size = Math.floor(ratio * vdim.y);
 
       if (size < cdim.y) {
         this._handle.style.top = offset + 'px';
         this._handle.style.height = size + 'px';
 
         JX.DOM.show(this._bar);
       } else {
         JX.DOM.hide(this._bar);
       }
     },
 
 
     /**
      * Show the scrollbar for the next second.
      */
     _showBar: function() {
       this._resizeBar();
 
       JX.DOM.alterClass(this._handle, 'jx-scrollbar-visible', true);
 
       this._clearTimeout();
       this._timeout = setTimeout(JX.bind(this, this._hideBar), 1000);
     },
 
 
     /**
      * Hide the scrollbar.
      */
     _hideBar: function() {
       if (this._dragOrigin !== null) {
         // If we're currently dragging the handle, we never want to hide
         // it.
         return;
       }
 
       JX.DOM.alterClass(this._handle, 'jx-scrollbar-visible', false);
       this._clearTimeout();
     },
 
 
     /**
      * Clear the scrollbar hide timeout, if one is set.
      */
     _clearTimeout: function() {
       if (this._timeout) {
         clearTimeout(this._timeout);
         this._timeout = null;
       }
     },
 
     getContentNode: function() {
       return this._content || this._frame;
     },
 
     getViewportNode: function() {
       return this._viewport || this._frame;
     },
 
     scrollTo: function(scroll) {
       if (this._viewport !== null) {
         this._viewport.scrollTop = scroll;
       } else {
         this._frame.scrollTop = scroll;
       }
       return this;
     }
   }
 
 });
diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/default.p100.png b/webroot/rsrc/image/icon/fatcow/thumbnails/default.p100.png
deleted file mode 100644
index f713c2398b..0000000000
Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/default.p100.png and /dev/null differ
diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/default160x120.png b/webroot/rsrc/image/icon/fatcow/thumbnails/default160x120.png
deleted file mode 100644
index 16d6fd4f90..0000000000
Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/default160x120.png and /dev/null differ
diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/default280x210.png b/webroot/rsrc/image/icon/fatcow/thumbnails/default280x210.png
deleted file mode 100644
index 7288c81954..0000000000
Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/default280x210.png and /dev/null differ
diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/default60x45.png b/webroot/rsrc/image/icon/fatcow/thumbnails/default60x45.png
deleted file mode 100644
index 145ea1eb63..0000000000
Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/default60x45.png and /dev/null differ
diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/image.p100.png b/webroot/rsrc/image/icon/fatcow/thumbnails/image.p100.png
deleted file mode 100644
index f5fa35ab08..0000000000
Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/image.p100.png and /dev/null differ
diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/image160x120.png b/webroot/rsrc/image/icon/fatcow/thumbnails/image160x120.png
deleted file mode 100644
index 90cc11c02d..0000000000
Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/image160x120.png and /dev/null differ
diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/image280x210.png b/webroot/rsrc/image/icon/fatcow/thumbnails/image280x210.png
deleted file mode 100644
index efdf733f8e..0000000000
Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/image280x210.png and /dev/null differ
diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/image60x45.png b/webroot/rsrc/image/icon/fatcow/thumbnails/image60x45.png
deleted file mode 100644
index 9077e69586..0000000000
Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/image60x45.png and /dev/null differ
diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/pdf.p100.png b/webroot/rsrc/image/icon/fatcow/thumbnails/pdf.p100.png
deleted file mode 100644
index ad3a39b490..0000000000
Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/pdf.p100.png and /dev/null differ
diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/pdf160x120.png b/webroot/rsrc/image/icon/fatcow/thumbnails/pdf160x120.png
deleted file mode 100644
index 20f08f955b..0000000000
Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/pdf160x120.png and /dev/null differ
diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/pdf280x210.png b/webroot/rsrc/image/icon/fatcow/thumbnails/pdf280x210.png
deleted file mode 100644
index 8036981aca..0000000000
Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/pdf280x210.png and /dev/null differ
diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/pdf60x45.png b/webroot/rsrc/image/icon/fatcow/thumbnails/pdf60x45.png
deleted file mode 100644
index 8a16eaf488..0000000000
Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/pdf60x45.png and /dev/null differ
diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/zip.p100.png b/webroot/rsrc/image/icon/fatcow/thumbnails/zip.p100.png
deleted file mode 100644
index 86fa739b3b..0000000000
Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/zip.p100.png and /dev/null differ
diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/zip160x120.png b/webroot/rsrc/image/icon/fatcow/thumbnails/zip160x120.png
deleted file mode 100644
index fbe19e59f6..0000000000
Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/zip160x120.png and /dev/null differ
diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/zip280x210.png b/webroot/rsrc/image/icon/fatcow/thumbnails/zip280x210.png
deleted file mode 100644
index 8db127b282..0000000000
Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/zip280x210.png and /dev/null differ
diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/zip60x45.png b/webroot/rsrc/image/icon/fatcow/thumbnails/zip60x45.png
deleted file mode 100644
index c66416c318..0000000000
Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/zip60x45.png and /dev/null differ
diff --git a/webroot/rsrc/js/application/aphlict/Aphlict.js b/webroot/rsrc/js/application/aphlict/Aphlict.js
index d4886a12a0..78d0958c81 100644
--- a/webroot/rsrc/js/application/aphlict/Aphlict.js
+++ b/webroot/rsrc/js/application/aphlict/Aphlict.js
@@ -1,168 +1,172 @@
 /**
  * @provides javelin-aphlict
  * @requires javelin-install
  *           javelin-util
  *           javelin-websocket
  *           javelin-leader
  *           javelin-json
  */
 
 /**
  * Client for the notification server. Example usage:
  *
  *   var aphlict = new JX.Aphlict('ws://localhost:22280', subscriptions)
  *     .setHandler(function(message) {
  *       // ...
  *     })
  *     .start();
  *
  */
 JX.install('Aphlict', {
 
   construct: function(uri, subscriptions) {
     if (__DEV__) {
       if (JX.Aphlict._instance) {
         JX.$E('Aphlict object is a singleton.');
       }
     }
 
     this._uri = uri;
     this._subscriptions = subscriptions;
     this._setStatus('setup');
 
     JX.Aphlict._instance = this;
   },
 
   events: ['didChangeStatus'],
 
   members: {
     _uri: null,
     _socket: null,
     _subscriptions: null,
     _status: null,
 
     start: function() {
       JX.Leader.listen('onBecomeLeader', JX.bind(this, this._lead));
       JX.Leader.listen('onReceiveBroadcast', JX.bind(this, this._receive));
       JX.Leader.start();
 
       JX.Leader.call(JX.bind(this, this._begin));
     },
 
+    getSubscriptions: function() {
+      return this._subscriptions;
+    },
+
     setSubscriptions: function(subscriptions) {
       this._subscriptions = subscriptions;
       JX.Leader.broadcast(
         null,
         {type: 'aphlict.subscribe', data: this._subscriptions});
     },
 
     clearSubscriptions: function(subscriptions) {
       this._subscriptions = null;
       JX.Leader.broadcast(
         null,
         {type: 'aphlict.unsubscribe', data: subscriptions});
     },
 
     getStatus: function() {
       return this._status;
     },
 
     _begin: function() {
       JX.Leader.broadcast(
         null,
         {type: 'aphlict.getstatus'});
       JX.Leader.broadcast(
         null,
         {type: 'aphlict.subscribe', data: this._subscriptions});
     },
 
     _lead: function() {
       this._socket = new JX.WebSocket(this._uri);
       this._socket.setOpenHandler(JX.bind(this, this._open));
       this._socket.setMessageHandler(JX.bind(this, this._message));
       this._socket.setCloseHandler(JX.bind(this, this._close));
 
       this._socket.open();
     },
 
     _open: function() {
       this._broadcastStatus('open');
       JX.Leader.broadcast(null, {type: 'aphlict.getsubscribers'});
     },
 
     _close: function() {
       this._broadcastStatus('closed');
     },
 
     _broadcastStatus: function(status) {
       JX.Leader.broadcast(null, {type: 'aphlict.status', data: status});
     },
 
     _message: function(raw) {
       var message = JX.JSON.parse(raw);
       JX.Leader.broadcast(null, {type: 'aphlict.server', data: message});
     },
 
     _receive: function(message, is_leader) {
       switch (message.type) {
         case 'aphlict.status':
           this._setStatus(message.data);
           break;
 
         case 'aphlict.getstatus':
           if (is_leader) {
             this._broadcastStatus(this.getStatus());
           }
           break;
 
         case 'aphlict.getsubscribers':
           JX.Leader.broadcast(
             null,
             {type: 'aphlict.subscribe', data: this._subscriptions});
           break;
 
         case 'aphlict.subscribe':
           if (is_leader) {
             this._write({
               command: 'subscribe',
               data: message.data
             });
           }
           break;
 
         default:
           var handler = this.getHandler();
           handler && handler(message);
           break;
       }
     },
 
     _setStatus: function(status) {
       this._status = status;
       this.invoke('didChangeStatus');
     },
 
     _write: function(message) {
       this._socket.send(JX.JSON.stringify(message));
     }
 
   },
 
   properties: {
     handler: null
   },
 
   statics: {
     _instance: null,
 
     getInstance: function() {
       var self = JX.Aphlict;
       if (!self._instance) {
         return null;
       }
       return self._instance;
     }
 
   }
 
 });
diff --git a/webroot/rsrc/js/application/aphlict/behavior-aphlict-dropdown.js b/webroot/rsrc/js/application/aphlict/behavior-aphlict-dropdown.js
index 042855ee4e..d1ebc33d9f 100644
--- a/webroot/rsrc/js/application/aphlict/behavior-aphlict-dropdown.js
+++ b/webroot/rsrc/js/application/aphlict/behavior-aphlict-dropdown.js
@@ -1,225 +1,236 @@
 /**
  * @provides javelin-behavior-aphlict-dropdown
  * @requires javelin-behavior
  *           javelin-request
  *           javelin-stratcom
  *           javelin-vector
  *           javelin-dom
  *           javelin-uri
  *           javelin-behavior-device
  *           phabricator-title
  */
 
 JX.behavior('aphlict-dropdown', function(config, statics) {
   // Track the current globally visible menu.
   statics.visible = statics.visible || null;
 
   var dropdown = JX.$(config.dropdownID);
   var bubble = JX.$(config.bubbleID);
   var icon = JX.DOM.scry(bubble, 'span', 'menu-icon')[0];
 
   var count;
   if (config.countID) {
     count = JX.$(config.countID);
   }
 
   var request = null;
   var dirty = config.local ? false : true;
 
   JX.Title.setCount(config.countType, config.countNumber);
 
   function _updateCount(number) {
     JX.Title.setCount(config.countType, number);
 
     JX.DOM.setContent(count, number);
     if (number === 0) {
       JX.DOM.alterClass(bubble, config.unreadClass, false);
     } else {
       JX.DOM.alterClass(bubble, config.unreadClass, true);
     }
   }
 
   function refresh() {
     if (dirty) {
       JX.DOM.setContent(dropdown, config.loadingText);
       JX.DOM.alterClass(
         dropdown,
         'phabricator-notification-menu-loading',
         true);
     }
 
     if (request) {
       // Already fetching.
       return;
     }
 
     request = new JX.Request(config.uri, function(response) {
       var number = response.number;
       _updateCount(number);
       dirty = false;
       JX.DOM.alterClass(
         dropdown,
         'phabricator-notification-menu-loading',
         false);
       JX.DOM.setContent(dropdown, JX.$H(response.content));
       request = null;
     });
     request.send();
   }
 
   JX.Stratcom.listen(
     'quicksand-redraw',
     null,
     function (e) {
       var data = e.getData();
       if (config.local && config.applicationClass) {
         var local_dropdowns = data.newResponse.aphlictDropdowns;
         if (local_dropdowns[config.applicationClass]) {
           JX.DOM.replace(
             dropdown,
             JX.$H(local_dropdowns[config.applicationClass]));
           dropdown = JX.$(config.dropdownID);
           if (dropdown.childNodes.length === 0) {
             JX.DOM.hide(bubble);
           } else {
             JX.DOM.show(bubble);
           }
         } else {
           JX.DOM.hide(bubble);
         }
         return;
       }
 
       if (!data.fromServer) {
         return;
       }
-      var updated = false;
       var new_data = data.newResponse.aphlictDropdownData;
-      for (var ii = 0; ii < new_data.length; ii++) {
-        if (new_data[ii].countType != config.countType) {
-          continue;
-        }
-        if (!new_data[ii].isInstalled) {
-          continue;
-        }
-        updated = true;
-        _updateCount(parseInt(new_data[ii].count));
+      update_counts(new_data);
+    });
+
+  JX.Stratcom.listen(
+    'conpherence-redraw-aphlict',
+    null,
+    function (e) {
+      update_counts(e.getData());
+    });
+
+  function update_counts(new_data) {
+    var updated = false;
+    for (var ii = 0; ii < new_data.length; ii++) {
+      if (new_data[ii].countType != config.countType) {
+        continue;
       }
-      if (updated) {
-        dirty = true;
+      if (!new_data[ii].isInstalled) {
+        continue;
       }
-    });
+      updated = true;
+      _updateCount(parseInt(new_data[ii].count));
+    }
+    if (updated) {
+      dirty = true;
+    }
+  }
 
   function set_visible(menu, icon) {
     if (menu) {
       statics.visible = {menu: menu, icon: icon};
       if (icon) {
         JX.DOM.alterClass(icon, 'sky', true);
       }
     } else {
       if (statics.visible) {
         JX.DOM.hide(statics.visible.menu);
         if (statics.visible.icon) {
           JX.DOM.alterClass(statics.visible.icon, 'sky', false);
         }
       }
       statics.visible = null;
     }
   }
 
   JX.Stratcom.listen(
     'click',
     null,
     function(e) {
       if (!e.getNode('phabricator-notification-menu')) {
         // Click outside the dropdown; hide it.
         set_visible(null);
         return;
       }
 
       if (e.getNode('tag:a')) {
         // User clicked a link. Hide the menu, then follow the link.
         set_visible(null);
         return;
       }
 
       if (!e.getNode('notification')) {
         // User clicked somewhere in the dead area of the menu, like the header
         // or footer.
         return;
       }
 
       // If the user clicked a notification (but missed a link) and it has a
       // primary URI, go there.
       var href = e.getNodeData('notification').href;
       if (href) {
         JX.$U(href).go();
         e.kill();
         set_visible(null);
       }
     });
 
   JX.DOM.listen(
     bubble,
     'click',
     null,
     function(e) {
       if (!e.isNormalClick()) {
         return;
       }
 
       if (config.desktop && JX.Device.getDevice() != 'desktop') {
         return;
       }
 
       e.kill();
 
       // If a menu is currently open, close it.
       if (statics.visible) {
         var previously_visible = statics.visible;
         set_visible(null);
 
         // If the menu we just closed was the menu attached to the clicked
         // icon, we're all done -- clicking the icon for an open menu just
         // closes it. Otherwise, we closed some other menu and still need to
         // open the one the user just clicked.
         if (previously_visible.menu === dropdown) {
           return;
         }
       }
 
       if (dirty) {
         refresh();
       }
 
       var p = JX.$V(bubble);
       JX.DOM.show(dropdown);
 
       p.y = null;
       if (config.containerDivID) {
         var pc = JX.$V(JX.$(config.containerDivID));
         p.x -= (JX.Vector.getDim(dropdown).x - JX.Vector.getDim(bubble).x +
             pc.x);
       } else if (config.right) {
         p.x -= (JX.Vector.getDim(dropdown).x - JX.Vector.getDim(bubble).x);
       } else {
         p.x -= 6;
       }
       p.setPos(dropdown);
 
       set_visible(dropdown, icon);
     }
   );
 
   JX.Stratcom.listen('notification-panel-update', null, function() {
     if (config.local) {
       return;
     }
     dirty = true;
     refresh();
   });
 
   JX.Stratcom.listen('notification-panel-close', null, function() {
     set_visible(null);
   });
 });
diff --git a/webroot/rsrc/js/application/calendar/event-all-day.js b/webroot/rsrc/js/application/calendar/event-all-day.js
new file mode 100644
index 0000000000..a8bd7da7a8
--- /dev/null
+++ b/webroot/rsrc/js/application/calendar/event-all-day.js
@@ -0,0 +1,16 @@
+/**
+ * @provides javelin-behavior-event-all-day
+ */
+
+
+JX.behavior('event-all-day', function(config) {
+  var checkbox = JX.$(config.allDayID);
+  JX.DOM.listen(checkbox, 'change', null, function() {
+    var start = JX.$(config.startDateID);
+    var end = JX.$(config.endDateID);
+
+    JX.DOM.alterClass(start, 'no-time', checkbox.checked);
+    JX.DOM.alterClass(end, 'no-time', checkbox.checked);
+  });
+
+});
diff --git a/webroot/rsrc/js/application/conpherence/ConpherenceThreadManager.js b/webroot/rsrc/js/application/conpherence/ConpherenceThreadManager.js
index b1694e19ca..c1c47c5c78 100644
--- a/webroot/rsrc/js/application/conpherence/ConpherenceThreadManager.js
+++ b/webroot/rsrc/js/application/conpherence/ConpherenceThreadManager.js
@@ -1,351 +1,508 @@
 /**
  * @provides conpherence-thread-manager
  * @requires javelin-dom
  *           javelin-util
  *           javelin-stratcom
  *           javelin-install
+ *           javelin-aphlict
  *           javelin-workflow
  *           javelin-router
  *           javelin-behavior-device
  *           javelin-vector
  */
 JX.install('ConpherenceThreadManager', {
 
   construct : function() {
     if (__DEV__) {
       if (JX.ConpherenceThreadManager._instance) {
         JX.$E('ConpherenceThreadManager object is a singleton.');
       }
     }
     JX.ConpherenceThreadManager._instance = this;
     return this;
   },
 
   members: {
     _loadThreadURI: null,
     _loadedThreadID: null,
     _loadedThreadPHID: null,
     _latestTransactionID: null,
+    _transactionIDMap: null,
+    _transactionCache: null,
     _canEditLoadedThread: null,
     _updating:  null,
     _minimalDisplay: false,
+    _messagesRootCallback: JX.bag,
     _willLoadThreadCallback: JX.bag,
     _didLoadThreadCallback: JX.bag,
     _didUpdateThreadCallback:  JX.bag,
     _willSendMessageCallback: JX.bag,
     _didSendMessageCallback: JX.bag,
     _willUpdateWorkflowCallback: JX.bag,
     _didUpdateWorkflowCallback: JX.bag,
 
     setLoadThreadURI: function(uri) {
       this._loadThreadURI = uri;
       return this;
     },
 
     getLoadThreadURI: function() {
       return this._loadThreadURI;
     },
 
     isThreadLoaded: function() {
       return Boolean(this._loadedThreadID);
     },
 
     isThreadIDLoaded: function(thread_id) {
       return this._loadedThreadID == thread_id;
     },
 
     getLoadedThreadID: function() {
       return this._loadedThreadID;
     },
 
     setLoadedThreadID: function(id) {
       this._loadedThreadID = id;
       return this;
     },
 
     getLoadedThreadPHID: function() {
       return this._loadedThreadPHID;
     },
 
     setLoadedThreadPHID: function(phid) {
       this._loadedThreadPHID = phid;
       return this;
     },
 
     getLatestTransactionID: function() {
       return this._latestTransactionID;
     },
 
     setLatestTransactionID: function(id) {
       this._latestTransactionID = id;
       return this;
     },
 
+    _updateTransactionIDMap: function(transactions) {
+      var loaded_id = this.getLoadedThreadID();
+      if (!this._transactionIDMap[loaded_id]) {
+        this._transactionIDMap[this._loadedThreadID] = {};
+      }
+      var loaded_transaction_ids = this._transactionIDMap[loaded_id];
+      var transaction;
+      for (var ii = 0; ii < transactions.length; ii++) {
+        transaction = transactions[ii];
+        loaded_transaction_ids[JX.Stratcom.getData(transaction).id] = 1;
+      }
+      this._transactionIDMap[this._loadedThreadID] = loaded_transaction_ids;
+      return this;
+    },
+
+    _updateTransactionCache: function(transactions) {
+      var transaction;
+      for (var ii = 0; ii < transactions.length; ii++) {
+        transaction = transactions[ii];
+        this._transactionCache[JX.Stratcom.getData(transaction).id] =
+          transaction;
+      }
+      return this;
+    },
+
+    _getLoadedTransactions: function() {
+      var loaded_id = this.getLoadedThreadID();
+      var loaded_tx_ids = JX.keys(this._transactionIDMap[loaded_id]);
+      loaded_tx_ids.sort(function (a, b) {
+        var x = parseFloat(a);
+        var y = parseFloat(b);
+        if (x > y) {
+          return 1;
+        }
+        if (x < y) {
+          return -1;
+        }
+        return 0;
+      });
+      var transactions = [];
+      for (var ii = 0; ii < loaded_tx_ids.length; ii++) {
+        transactions.push(this._transactionCache[loaded_tx_ids[ii]]);
+      }
+      return transactions;
+    },
+
+    _deleteTransactionCaches: function(id) {
+      delete this._transactionCache[id];
+      delete this._transactionIDMap[this._loadedThreadID][id];
+
+      return this;
+    },
+
     setCanEditLoadedThread: function(bool) {
       this._canEditLoadedThread = bool;
       return this;
     },
 
     getCanEditLoadedThread: function() {
       if (this._canEditLoadedThread === null) {
         return false;
       }
       return this._canEditLoadedThread;
     },
 
     setMinimalDisplay: function(bool) {
       this._minimalDisplay = bool;
       return this;
     },
 
+    setMessagesRootCallback: function(callback) {
+      this._messagesRootCallback = callback;
+      return this;
+    },
+
     setWillLoadThreadCallback: function(callback) {
       this._willLoadThreadCallback = callback;
       return this;
     },
 
     setDidLoadThreadCallback: function(callback) {
       this._didLoadThreadCallback = callback;
       return this;
     },
 
     setDidUpdateThreadCallback: function(callback) {
       this._didUpdateThreadCallback = callback;
       return this;
     },
 
     setWillSendMessageCallback: function(callback) {
       this._willSendMessageCallback = callback;
       return this;
     },
 
     setDidSendMessageCallback: function(callback) {
       this._didSendMessageCallback = callback;
       return this;
     },
 
     setWillUpdateWorkflowCallback: function(callback) {
       this._willUpdateWorkflowCallback = callback;
       return this;
     },
 
     setDidUpdateWorkflowCallback: function(callback) {
       this._didUpdateWorkflowCallback = callback;
       return this;
     },
 
     _getParams: function(base_params) {
       if (this._minimalDisplay) {
         base_params.minimal_display = true;
       }
       if (this._latestTransactionID) {
         base_params.latest_transaction_id = this._latestTransactionID;
       }
       return base_params;
     },
 
     start: function() {
+
+      this._transactionIDMap = {};
+      this._transactionCache = {};
+
       JX.Stratcom.listen(
         'aphlict-server-message',
         null,
         JX.bind(this, function(e) {
           var message = e.getData();
 
           if (message.type != 'message') {
             // Not a message event.
             return;
           }
 
           if (message.threadPHID != this._loadedThreadPHID) {
             // Message event for some thread other than the visible one.
             return;
           }
 
           if (message.messageID <= this._latestTransactionID) {
             // Message event for something we already know about.
             return;
           }
-          // If we're currently updating, wait for the update to complete.
+
           // If this notification tells us about a message which is newer than
-          // the newest one we know to exist, keep track of it so we can
-          // update once the in-flight update finishes.
+          // the newest one we know to exist, update our latest knownID so we
+          // can properly update later.
           if (this._updating &&
               this._updating.threadPHID == this._loadedThreadPHID) {
             if (message.messageID > this._updating.knownID) {
               this._updating.knownID = message.messageID;
-              return;
+              // We're currently updating, so wait for the update to complete.
+              // this.syncWorkflow has us covered in this case.
+              if (this._updating.active) {
+                return;
+              }
             }
           }
 
           this._updateThread();
         }));
+
+      JX.Stratcom.listen(
+        'click',
+        'show-older-messages',
+        JX.bind(this, function(e) {
+          e.kill();
+          var data = e.getNodeData('show-older-messages');
+
+          var node = e.getNode('show-older-messages');
+          JX.DOM.setContent(node, 'Loading...');
+          JX.DOM.alterClass(
+            node,
+            'conpherence-show-more-messages-loading',
+            true);
+
+          new JX.Workflow(this._getMoreMessagesURI(), data)
+            .setHandler(JX.bind(this, function(r) {
+              this._deleteTransactionCaches(JX.Stratcom.getData(node).id);
+              JX.DOM.remove(node);
+              this._updateTransactions(r);
+            })).start();
+        }));
+      JX.Stratcom.listen(
+        'click',
+        'show-newer-messages',
+        JX.bind(this, function(e) {
+          e.kill();
+          var data = e.getNodeData('show-newer-messages');
+          var node = e.getNode('show-newer-messages');
+          JX.DOM.setContent(node, 'Loading...');
+          JX.DOM.alterClass(
+            node,
+            'conpherence-show-more-messages-loading',
+            true);
+
+          new JX.Workflow(this._getMoreMessagesURI(), data)
+          .setHandler(JX.bind(this, function(r) {
+            this._deleteTransactionCaches(JX.Stratcom.getData(node).id);
+            JX.DOM.remove(node);
+            this._updateTransactions(r);
+          })).start();
+        }));
     },
 
     _shouldUpdateDOM: function(r) {
       if (this._updating &&
           this._updating.threadPHID == this._loadedThreadPHID) {
 
         if (r.non_update) {
           return false;
         }
 
         // we have a different, more current update in progress so
         // return early
         if (r.latest_transaction_id < this._updating.knownID) {
           return false;
         }
       }
       return true;
     },
 
-    _markUpdated: function(r) {
+    _updateDOM: function(r) {
+      this._updateTransactions(r);
+
       this._updating.knownID = r.latest_transaction_id;
       this._latestTransactionID = r.latest_transaction_id;
-      JX.Stratcom.invoke('notification-panel-update', null, {});
+      JX.Stratcom.invoke(
+        'conpherence-redraw-aphlict',
+        null,
+        r.aphlictDropdownData);
+    },
+
+    _updateTransactions: function(r) {
+      var new_transactions = JX.$H(r.transactions).getFragment().childNodes;
+      this._updateTransactionIDMap(new_transactions);
+      this._updateTransactionCache(new_transactions);
+
+      var transactions = this._getLoadedTransactions();
+
+      JX.DOM.setContent(this._messagesRootCallback(), transactions);
     },
 
     _updateThread: function() {
       var params = this._getParams({
         action: 'load',
       });
 
-      var uri = '/conpherence/update/' + this._loadedThreadID + '/';
-
-      var workflow = new JX.Workflow(uri)
+      var workflow = new JX.Workflow(this._getUpdateURI())
         .setData(params)
         .setHandler(JX.bind(this, function(r) {
           if (this._shouldUpdateDOM(r)) {
-            this._markUpdated(r);
-
+            this._updateDOM(r);
             this._didUpdateThreadCallback(r);
           }
         }));
 
       this.syncWorkflow(workflow, 'finally');
     },
 
     syncWorkflow: function(workflow, stage) {
       this._updating = {
         threadPHID: this._loadedThreadPHID,
-        knownID: this._latestTransactionID
+        knownID: this._latestTransactionID,
+        active: true
       };
       workflow.listen(stage, JX.bind(this, function() {
         // TODO - do we need to handle if we switch threads somehow?
         var need_sync = this._updating &&
           (this._updating.knownID > this._latestTransactionID);
         if (need_sync) {
           return this._updateThread();
         }
+        this._updating.active = false;
       }));
       workflow.start();
     },
 
     runUpdateWorkflowFromLink: function(link, params) {
       params = this._getParams(params);
       this._willUpdateWorkflowCallback();
       var workflow = new JX.Workflow.newFromLink(link)
         .setData(params)
         .setHandler(JX.bind(this, function(r) {
           if (this._shouldUpdateDOM(r)) {
-            this._markUpdated(r);
-
+            this._updateDOM(r);
             this._didUpdateWorkflowCallback(r);
           }
         }));
       this.syncWorkflow(workflow, params.stage);
     },
 
     loadThreadByID: function(thread_id, force_reload) {
       if (this.isThreadLoaded() &&
           this.isThreadIDLoaded(thread_id) &&
           !force_reload) {
         return;
       }
 
       this._willLoadThreadCallback();
 
       var params = {};
       // We pick a thread from the server if not specified
       if (thread_id) {
         params.id = thread_id;
       }
       params = this._getParams(params);
 
       var handler = JX.bind(this, function(r) {
+        var client = JX.Aphlict.getInstance();
+        if (client) {
+          var old_subs = client.getSubscriptions();
+          var new_subs = [];
+          for (var ii = 0; ii < old_subs.length; ii++) {
+            if (old_subs[ii] == this._loadedThreadPHID) {
+              continue;
+            } else {
+              new_subs.push(old_subs[ii]);
+            }
+          }
+          new_subs.push(r.threadPHID);
+          client.clearSubscriptions(client.getSubscriptions());
+          client.setSubscriptions(new_subs);
+        }
         this._loadedThreadID = r.threadID;
         this._loadedThreadPHID = r.threadPHID;
         this._latestTransactionID = r.latestTransactionID;
         this._canEditLoadedThread = r.canEdit;
-        JX.Stratcom.invoke('notification-panel-update', null, {});
+
+        JX.Stratcom.invoke(
+          'conpherence-redraw-aphlict',
+          null,
+          r.aphlictDropdownData);
 
         this._didLoadThreadCallback(r);
+        var messages_root = this._messagesRootCallback();
+        var messages = JX.DOM.scry(
+          messages_root,
+          'div',
+          'conpherence-transaction-view');
+        this._updateTransactionIDMap(messages);
+        this._updateTransactionCache(messages);
 
         if (force_reload) {
           JX.Stratcom.invoke('hashchange');
         }
       });
 
       // should this be sync'd too?
       new JX.Workflow(this.getLoadThreadURI())
         .setData(params)
         .setHandler(handler)
         .start();
     },
 
     sendMessage: function(form, params) {
       // don't bother sending up text if there is nothing to submit
       var textarea = JX.DOM.find(form, 'textarea');
       if (!textarea.value.length) {
         return;
       }
       params = this._getParams(params);
 
       var keep_enabled = true;
 
       var workflow = JX.Workflow.newFromForm(form, params, keep_enabled)
         .setHandler(JX.bind(this, function(r) {
           if (this._shouldUpdateDOM(r)) {
-            this._markUpdated(r);
-
+            this._updateDOM(r);
             this._didSendMessageCallback(r);
           } else if (r.non_update) {
             this._didSendMessageCallback(r, true);
           }
         }));
       this.syncWorkflow(workflow, 'finally');
 
       this._willSendMessageCallback();
     },
 
     handleDraftKeydown: function(e) {
       var form = e.getNode('tag:form');
       var data = e.getNodeData('tag:form');
 
       if (!data.preview) {
-        var uri = '/conpherence/update/' + this._loadedThreadID + '/';
         data.preview = new JX.PhabricatorShapedRequest(
-          uri,
+          this._getUpdateURI(),
           JX.bag,
           JX.bind(this, function () {
             var data = JX.DOM.convertFormToDictionary(form);
             data.action = 'draft';
             data = this._getParams(data);
             return data;
           }));
       }
       data.preview.trigger();
+    },
+
+    _getUpdateURI: function() {
+      return '/conpherence/update/' + this._loadedThreadID + '/';
+    },
+
+    _getMoreMessagesURI: function() {
+      return '/conpherence/' + this._loadedThreadID + '/';
     }
   },
 
   statics: {
     _instance: null,
 
     getInstance: function() {
       var self = JX.ConpherenceThreadManager;
       if (!self._instance) {
         return null;
       }
       return self._instance;
     }
   }
 
 });
diff --git a/webroot/rsrc/js/application/conpherence/behavior-durable-column.js b/webroot/rsrc/js/application/conpherence/behavior-durable-column.js
index adcaa382a3..4a9eac1eaf 100644
--- a/webroot/rsrc/js/application/conpherence/behavior-durable-column.js
+++ b/webroot/rsrc/js/application/conpherence/behavior-durable-column.js
@@ -1,354 +1,365 @@
 /**
  * @provides javelin-behavior-durable-column
  * @requires javelin-behavior
  *           javelin-dom
  *           javelin-stratcom
  *           javelin-behavior-device
  *           javelin-scrollbar
  *           javelin-quicksand
  *           phabricator-keyboard-shortcut
  *           conpherence-thread-manager
  */
 
 JX.behavior('durable-column', function(config, statics) {
   // TODO: Currently, updating the column sends the entire column back. This
   // includes the `durable-column` behavior itself, which tries to re-initialize
   // the column. Detect this and bail.
   //
   // If ThreadManager gets separated into a UI part and a thread part (which
   // seems likely), responses may no longer ship back the entire column. This
   // might let us remove this check.
   if (statics.initialized) {
     return;
   } else {
     statics.initialized = true;
   }
 
   var userVisible = config.visible;
   var show = null;
   var loadThreadID = null;
   var scrollbar = null;
 
-  var columnWidth = 300;
+  var margin = JX.Scrollbar.getScrollbarControlMargin();
+
+  var columnWidth = (300 + margin);
   // This is the smallest window size where we'll enable the column.
-  var minimumViewportWidth = 768;
+  var minimumViewportWidth = (768 - margin);
 
   var quick = JX.$('phabricator-standard-page-body');
 
   function _getColumnNode() {
     return JX.$('conpherence-durable-column');
   }
 
   function _getColumnScrollNode() {
     var column = _getColumnNode();
     return JX.DOM.find(column, 'div', 'conpherence-durable-column-main');
   }
 
   function _isViewportWideEnoughForColumn() {
     var viewport = JX.Vector.getViewport();
     if (viewport.x < minimumViewportWidth) {
       return false;
     } else {
       return true;
     }
   }
 
   function _updateColumnVisibility() {
     var new_value = (userVisible && _isViewportWideEnoughForColumn());
     if (new_value !== show) {
       show = new_value;
       _drawColumn(show);
     }
   }
 
   function _toggleColumn() {
     userVisible = !userVisible;
     _updateColumnVisibility();
 
     new JX.Request(config.settingsURI)
       .setData({value: (show ? 1 : 0)})
       .send();
   }
 
   function _drawColumn(visible) {
-    JX.DOM.alterClass(document.body, 'with-durable-column', visible);
+    JX.DOM.alterClass(
+      document.body,
+      'with-durable-column',
+      visible);
+    JX.DOM.alterClass(
+      document.body,
+      'with-durable-margin',
+      visible && !!margin);
+
     var column = _getColumnNode();
     if (visible) {
       JX.DOM.show(column);
       threadManager.loadThreadByID(loadThreadID);
     } else {
       JX.DOM.hide(column);
     }
     JX.Quicksand.setFrame(visible ? quick : null);
 
     // When we activate the column, adjust the tablet breakpoint so that we
     // convert the left side of the screen to tablet mode on narrow displays.
     var breakpoint;
     if (visible) {
       breakpoint = minimumViewportWidth + columnWidth;
     } else {
       breakpoint = minimumViewportWidth;
     }
     JX.Device.setTabletBreakpoint(breakpoint);
 
     JX.Stratcom.invoke('resize');
   }
 
   new JX.KeyboardShortcut('\\', 'Toggle Conpherence Column')
     .setHandler(_toggleColumn)
     .register();
 
   scrollbar = new JX.Scrollbar(_getColumnScrollNode());
 
   JX.Quicksand.setFrame(userVisible ? quick : null);
   JX.Quicksand.start(config.quicksandConfig);
 
   /* Conpherence Thread Manager configuration - lots of display
    * callbacks.
    */
 
   var threadManager = new JX.ConpherenceThreadManager();
   threadManager.setMinimalDisplay(true);
+  threadManager.setMessagesRootCallback(function() {
+    return _getColumnMessagesNode();
+  });
   threadManager.setLoadThreadURI('/conpherence/columnview/');
   threadManager.setWillLoadThreadCallback(function() {
     _markLoading(true);
   });
   threadManager.setDidLoadThreadCallback(function(r) {
     var column = _getColumnNode();
     var new_column = JX.$H(r.content);
     JX.DOM.replace(column, new_column);
     if (show) {
       JX.DOM.show(_getColumnNode());
     } else {
       JX.DOM.hide(_getColumnNode());
     }
     var messages = _getColumnMessagesNode();
     scrollbar = new JX.Scrollbar(_getColumnScrollNode());
     scrollbar.scrollTo(messages.scrollHeight);
     _markLoading(false);
     loadThreadID = threadManager.getLoadedThreadID();
   });
   threadManager.setDidUpdateThreadCallback(function(r) {
     var messages = _getColumnMessagesNode();
     JX.DOM.appendContent(messages, JX.$H(r.transactions));
     scrollbar.scrollTo(messages.scrollHeight);
   });
 
   threadManager.setWillSendMessageCallback(function() {
     // Wipe the textarea immediately so the user can start typing more text.
     var textarea = _getColumnTextareaNode();
     textarea.value = '';
     _focusColumnTextareaNode();
   });
 
   threadManager.setDidSendMessageCallback(function(r, non_update) {
     if (non_update) {
       return;
     }
     var messages = _getColumnMessagesNode();
-    JX.DOM.appendContent(messages, JX.$H(r.transactions));
     scrollbar.scrollTo(messages.scrollHeight);
   });
 
   threadManager.setWillUpdateWorkflowCallback(function() {
     JX.Stratcom.invoke('notification-panel-close');
   });
   threadManager.setDidUpdateWorkflowCallback(function(r) {
     var messages = _getColumnMessagesNode();
-    JX.DOM.appendContent(messages, JX.$H(r.transactions));
     scrollbar.scrollTo(messages.scrollHeight);
     JX.DOM.setContent(_getColumnTitleNode(), r.conpherence_title);
   });
   threadManager.start();
 
   JX.Stratcom.listen(
     'click',
     'conpherence-durable-column-header-action',
     function (e) {
       e.kill();
       var data = e.getNodeData('conpherence-durable-column-header-action');
       var action = data.action;
       var link = e.getNode('tag:a');
       var params = null;
 
       switch (action) {
         case 'metadata':
           threadManager.runUpdateWorkflowFromLink(
             link,
             {
               action: action,
               force_ajax: true,
               stage: 'submit'
             });
           break;
         case 'add_person':
           threadManager.runUpdateWorkflowFromLink(
             link,
             {
               action: action,
               stage: 'submit'
             });
           break;
         case 'go_conpherence':
           JX.$U(link.href).go();
           break;
         case 'hide_column':
           JX.Stratcom.invoke('notification-panel-close');
           _toggleColumn();
           break;
       }
     });
 
   JX.Stratcom.listen(
     'click',
     'conpherence-durable-column-thread-icon',
     function (e) {
       e.kill();
       var icons = JX.DOM.scry(
         JX.$('conpherence-durable-column'),
         'a',
         'conpherence-durable-column-thread-icon');
       var data = e.getNodeData('conpherence-durable-column-thread-icon');
       var cdata = null;
       for (var i = 0; i < icons.length; i++) {
         cdata = JX.Stratcom.getData(icons[i]);
         JX.DOM.alterClass(
           icons[i],
           'selected',
           cdata.threadID == data.threadID);
       }
       JX.DOM.setContent(_getColumnTitleNode(), JX.$H(data.threadTitle));
       threadManager.loadThreadByID(data.threadID);
     });
 
   JX.Stratcom.listen('resize', null, _updateColumnVisibility);
 
   function _getColumnBodyNode() {
     var column = JX.$('conpherence-durable-column');
     return JX.DOM.find(
       column,
       'div',
       'conpherence-durable-column-body');
   }
 
   function _getColumnMessagesNode() {
     var column = JX.$('conpherence-durable-column');
     return JX.DOM.find(
       column,
       'div',
       'conpherence-durable-column-transactions');
   }
 
   function _getColumnTitleNode() {
     var column = JX.$('conpherence-durable-column');
     return JX.DOM.find(
       column,
       'div',
       'conpherence-durable-column-header-text');
   }
 
   function _getColumnFormNode() {
     var column = JX.$('conpherence-durable-column');
     return JX.DOM.find(
       column,
       'form',
       'conpherence-message-form');
   }
 
   function _getColumnTextareaNode() {
     var column = JX.$('conpherence-durable-column');
     return JX.DOM.find(
         column,
         'textarea',
         'conpherence-durable-column-textarea');
   }
 
   function _focusColumnTextareaNode() {
     var textarea = _getColumnTextareaNode();
     setTimeout(function() { JX.DOM.focus(textarea); }, 1);
   }
 
   function _markLoading(loading) {
     var column = _getColumnNode();
     JX.DOM.alterClass(column, 'loading', loading);
   }
 
   function _sendMessage(e) {
     e.kill();
     var form = _getColumnFormNode();
     threadManager.sendMessage(form, { minimal_display: true });
   }
 
   JX.Stratcom.listen(
     'click',
     'conpherence-send-message',
     _sendMessage);
 
   JX.Stratcom.listen(
     ['submit', 'didSyntheticSubmit'],
     'conpherence-message-form',
     _sendMessage);
 
   // Send on enter if the shift key is not held.
   JX.Stratcom.listen(
     'keydown',
     'conpherence-message-form',
     function(e) {
       if (e.getSpecialKey() != 'return') {
         return;
       }
 
       var raw = e.getRawEvent();
       if (raw.shiftKey) {
         // If the shift key is pressed, let the browser write a newline into
         // the textarea.
         return;
       }
 
       // From here on, interpret this as a "send" action, not a literal
       // newline.
       e.kill();
 
       _sendMessage(e);
     });
 
   JX.Stratcom.listen(
     ['keydown'],
     'conpherence-durable-column-textarea',
     function (e) {
       threadManager.handleDraftKeydown(e);
     });
 
   // HTML5 placeholders are rendered as long as the input is empty, even if the
   // input is currently focused. This is undesirable for the chat input,
   // especially immediately after sending a message. Hide the placeholder while
   // the input is focused.
   JX.Stratcom.listen(
     ['focus', 'blur'],
     'conpherence-durable-column-textarea',
     function (e) {
       var node = e.getTarget();
       if (e.getType() == 'focus') {
         if (node.placeholder) {
           node.placeholderStorage = node.placeholder;
           node.placeholder = '';
         }
       } else {
         if (node.placeholderStorage) {
           node.placeholder = node.placeholderStorage;
           node.placeholderStorage = '';
         }
       }
     });
 
   JX.Stratcom.listen(
     'quicksand-redraw',
     null,
     function (e) {
       var new_data = e.getData().newResponse;
       JX.Title.setTitle(new_data.title);
     });
 
   _updateColumnVisibility();
 
 });
diff --git a/webroot/rsrc/js/application/conpherence/behavior-menu.js b/webroot/rsrc/js/application/conpherence/behavior-menu.js
index 26e184813e..2233b3ff28 100644
--- a/webroot/rsrc/js/application/conpherence/behavior-menu.js
+++ b/webroot/rsrc/js/application/conpherence/behavior-menu.js
@@ -1,575 +1,526 @@
 /**
  * @provides javelin-behavior-conpherence-menu
  * @requires 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
  */
 
 JX.behavior('conpherence-menu', function(config) {
   /**
    * State for displayed thread.
    */
   var _thread = {
     selected: null,
     visible: null,
     node: null
   };
 
   var scrollbar = null;
 
   // TODO - move more logic into the ThreadManager
   var threadManager = new JX.ConpherenceThreadManager();
+  threadManager.setMessagesRootCallback(function() {
+    return scrollbar.getContentNode();
+  });
   threadManager.setWillLoadThreadCallback(function() {
     markThreadLoading(true);
   });
   threadManager.setDidLoadThreadCallback(function(r) {
     var header = JX.$H(r.header);
-    var messages = JX.$H(r.messages);
+    var messages = JX.$H(r.transactions);
     var form = JX.$H(r.form);
     var root = JX.DOM.find(document, 'div', 'conpherence-layout');
     var header_root = JX.DOM.find(root, 'div', 'conpherence-header-pane');
     var form_root = JX.DOM.find(root, 'div', 'conpherence-form');
     JX.DOM.setContent(header_root, header);
     JX.DOM.setContent(scrollbar.getContentNode(), messages);
     JX.DOM.setContent(form_root, form);
 
     markThreadLoading(false);
 
     didRedrawThread(true);
   });
 
   threadManager.setDidUpdateThreadCallback(function(r) {
-    JX.DOM.appendContent(scrollbar.getContentNode(), JX.$H(r.transactions));
     _scrollMessageWindow();
   });
 
   threadManager.setWillSendMessageCallback(function () {
     var root = JX.DOM.find(document, 'div', 'conpherence-layout');
     var form_root = JX.DOM.find(root, 'div', 'conpherence-form');
     markThreadLoading(true);
     JX.DOM.alterClass(form_root, 'loading', true);
   });
 
   threadManager.setDidSendMessageCallback(function (r, non_update) {
     var root = JX.DOM.find(document, 'div', 'conpherence-layout');
     var form_root = JX.DOM.find(root, 'div', 'conpherence-form');
     var textarea = JX.DOM.find(form_root, 'textarea');
     if (!non_update) {
       var fileWidget = null;
       try {
         fileWidget = JX.DOM.find(root, 'div', 'widgets-files');
       } catch (ex) {
         // Ignore; maybe no files widget
       }
-      JX.DOM.appendContent(scrollbar.getContentNode(), JX.$H(r.transactions));
-      _scrollMessageWindow();
-
       if (fileWidget) {
         JX.DOM.setContent(
           fileWidget,
           JX.$H(r.file_widget));
       }
+
+      _scrollMessageWindow();
       textarea.value = '';
     }
     markThreadLoading(false);
 
     setTimeout(function() { JX.DOM.focus(textarea); }, 100);
   });
   threadManager.start();
 
   /**
    * Current role of this behavior. The two possible roles are to show a 'list'
    * of threads or a specific 'thread'. On devices, this behavior stays in the
    * 'list' role indefinitely, treating clicks normally and the next page
    * loads the behavior with role = 'thread'. On desktop, this behavior
    * auto-loads a thread as part of the 'list' role. As the thread loads the
    * role is changed to 'thread'.
    */
   var _currentRole = null;
 
   /**
    * When _oldDevice is null the code is executing for the first time.
    */
   var _oldDevice = null;
 
   /**
    * Initializes this behavior based on all the configuraton jonx and the
    * result of JX.Device.getDevice();
    */
   function init() {
     _currentRole = config.role;
 
     if (_currentRole == 'thread') {
       markThreadsLoading(true);
     } else {
       markThreadLoading(true);
     }
     markWidgetLoading(true);
     onDeviceChange();
     var root = JX.DOM.find(document, 'div', 'conpherence-layout');
     var messages_root = JX.DOM.find(root, 'div', 'conpherence-message-pane');
     var messages = JX.DOM.find(messages_root, 'div', 'conpherence-messages');
     scrollbar = new JX.Scrollbar(messages);
     scrollbar.setAsScrollFrame();
   }
   init();
 
   /**
    * Selecting threads
    */
   function selectThreadByID(id, update_page_data) {
     var thread = JX.$(id);
     selectThread(thread, update_page_data);
   }
 
   function selectThread(node, update_page_data) {
     if (_thread.node) {
       JX.DOM.alterClass(_thread.node, 'conpherence-selected', false);
     }
 
     JX.DOM.alterClass(node, 'conpherence-selected', true);
     JX.DOM.alterClass(node, 'hide-unread-count', true);
 
     _thread.node = node;
 
     var data = JX.Stratcom.getData(node);
     _thread.selected = data.threadID;
 
     if (update_page_data) {
       updatePageData(data);
     }
 
     redrawThread();
   }
 
   function updatePageData(data) {
     var uri = '/Z' + _thread.selected;
     JX.History.replace(uri);
     if (data.title) {
       JX.Title.setTitle(data.title);
     } else if (_thread.node) {
       var threadData = JX.Stratcom.getData(_thread.node);
       JX.Title.setTitle(threadData.title);
     }
   }
 
   JX.Stratcom.listen(
     'conpherence-update-page-data',
     null,
     function (e) {
       updatePageData(e.getData());
     }
   );
 
   function redrawThread() {
     if (!_thread.node) {
       return;
     }
 
     if (_thread.visible == _thread.selected) {
       return;
     }
 
     var data = JX.Stratcom.getData(_thread.node);
 
     if (_thread.visible !== null || !config.hasThread) {
       threadManager.setLoadThreadURI('/conpherence/' + data.threadID + '/');
       threadManager.loadThreadByID(data.threadID);
     } else if (config.hasThread) {
       // we loaded the thread via the server so let the thread manager know
       threadManager.setLoadedThreadID(config.selectedThreadID);
       threadManager.setLoadedThreadPHID(config.selectedThreadPHID);
       threadManager.setLatestTransactionID(config.latestTransactionID);
       threadManager.setCanEditLoadedThread(config.canEditSelectedThread);
       _scrollMessageWindow();
       _focusTextarea();
     } else {
       didRedrawThread();
     }
 
     if (_thread.visible !== null || !config.hasWidgets) {
       reloadWidget(data);
     } else {
      JX.Stratcom.invoke(
       'conpherence-update-widgets',
       null,
       {
         widget : getDefaultWidget(),
         buildSelectors : false,
         toggleWidget : true,
         threadID : _thread.selected
       });
     }
 
     _thread.visible = _thread.selected;
   }
 
   function markThreadsLoading(loading) {
     var root = JX.DOM.find(document, 'div', 'conpherence-layout');
     var menu = JX.DOM.find(root, 'div', 'conpherence-menu-pane');
     JX.DOM.alterClass(menu, 'loading', loading);
   }
 
   function markThreadLoading(loading) {
     var root = JX.DOM.find(document, 'div', 'conpherence-layout');
     var header_root = JX.DOM.find(root, 'div', 'conpherence-header-pane');
     var messages_root = JX.DOM.find(root, 'div', 'conpherence-message-pane');
     var form_root = JX.DOM.find(root, 'div', 'conpherence-form');
 
     JX.DOM.alterClass(header_root, 'loading', loading);
     JX.DOM.alterClass(messages_root, 'loading', loading);
     JX.DOM.alterClass(form_root, 'loading', loading);
 
     try {
       var textarea = JX.DOM.find(form, 'textarea');
       textarea.disabled = loading;
       var button = JX.DOM.find(form, 'button');
       button.disabled = loading;
     } catch (ex) {
       // haven't loaded it yet!
     }
   }
 
   function markWidgetLoading(loading) {
     var root = JX.DOM.find(document, 'div', 'conpherence-layout');
     var widgets_root = JX.DOM.find(root, 'div', 'conpherence-widget-pane');
 
     JX.DOM.alterClass(widgets_root, 'loading', loading);
   }
 
   function reloadWidget(data) {
     markWidgetLoading(true);
     if (!data.widget) {
       data.widget = getDefaultWidget();
     }
     var widget_uri = config.baseURI + 'widget/' + data.threadID + '/';
     new JX.Workflow(widget_uri, {})
       .setHandler(JX.bind(null, onWidgetResponse, data.threadID, data.widget))
       .start();
   }
   JX.Stratcom.listen(
     'conpherence-reload-widget',
     null,
     function (e) {
       var data = e.getData();
       if (data.threadID != _thread.selected) {
         return;
       }
       reloadWidget(data);
     }
   );
 
   function onWidgetResponse(thread_id, widget, response) {
     // we got impatient and this is no longer the right answer :/
     if (_thread.selected != thread_id) {
       return;
     }
     var root = JX.DOM.find(document, 'div', 'conpherence-layout');
     var widgets_root = JX.DOM.find(root, 'div', 'conpherence-widgets-holder');
     JX.DOM.setContent(widgets_root, JX.$H(response.widgets));
 
     JX.Stratcom.invoke(
       'conpherence-update-widgets',
       null,
       {
         widget : widget,
         buildSelectors : true,
         toggleWidget : true,
         threadID : _thread.selected
       });
 
     markWidgetLoading(false);
   }
 
   function getDefaultWidget() {
     var device = JX.Device.getDevice();
     var widget = 'conpherence-message-pane';
     if (device == 'desktop') {
       widget = 'widgets-people';
     }
     return widget;
   }
 
   /**
    * This function is a wee bit tricky. Internally, we want to scroll the
    * message window and let other stuff - notably widgets - redraw / build if
    * necessary. Externally, we want a hook to scroll the message window
    * - notably when the widget selector is used to invoke the message pane.
    * The following three functions get 'er done.
    */
   function didRedrawThread(build_device_widget_selector) {
     _scrollMessageWindow();
     _focusTextarea();
     JX.Stratcom.invoke(
       'conpherence-did-redraw-thread',
       null,
       {
         widget : getDefaultWidget(),
         threadID : _thread.selected,
         buildDeviceWidgetSelector : build_device_widget_selector
       });
   }
 
   var _firstScroll = true;
   function _scrollMessageWindow() {
     if (_firstScroll) {
       _firstScroll = false;
 
       // We want to let the standard #anchor tech take over after we make sure
       // we don't have to present the user with a "load older message?" dialog
       if (window.location.hash) {
         var hash = window.location.hash.replace(/^#/, '');
         try {
           JX.$('anchor-' + hash);
         } catch (ex) {
           var uri = '/conpherence/' +
             _thread.selected + '/' + hash + '/';
           threadManager.setLoadThreadURI(uri);
           threadManager.loadThreadByID(_thread.selected, true);
           _firstScroll = true;
           return;
         }
         return;
       }
     }
     scrollbar.scrollTo(scrollbar.getViewportNode().scrollHeight);
   }
   function _focusTextarea() {
     var root = JX.DOM.find(document, 'div', 'conpherence-layout');
     var form_root = JX.DOM.find(root, 'div', 'conpherence-form');
     try {
       var textarea = JX.DOM.find(form_root, 'textarea');
       // We may have a draft so do this JS trick so we end up focused at the
       // end of the draft.
       var textarea_value = textarea.value;
       textarea.value = '';
       JX.DOM.focus(textarea);
       textarea.value = textarea_value;
     } catch (ex) {
       // no textarea? no problem
     }
   }
   JX.Stratcom.listen(
     'conpherence-redraw-thread',
     null,
     function () {
       _scrollMessageWindow();
     }
   );
 
   JX.Stratcom.listen(
     'click',
     'conpherence-menu-click',
     function(e) {
       if (!e.isNormalClick()) {
         return;
       }
 
       // On devices, just follow the link normally.
       if (JX.Device.getDevice() != 'desktop') {
         return;
       }
 
       e.kill();
       selectThread(e.getNode('conpherence-menu-click'), true);
     });
 
   JX.Stratcom.listen('click', 'conpherence-edit-metadata', function (e) {
     e.kill();
     var root = e.getNode('conpherence-layout');
     var form = JX.DOM.find(root, 'form', 'conpherence-pontificate');
     var data = e.getNodeData('conpherence-edit-metadata');
     var header = JX.DOM.find(root, 'div', 'conpherence-header-pane');
     var messages = scrollbar.getContentNode();
 
     new JX.Workflow.newFromForm(form, data)
       .setHandler(JX.bind(this, function(r) {
         JX.DOM.appendContent(messages, JX.$H(r.transactions));
         _scrollMessageWindow();
 
         JX.DOM.setContent(
           header,
           JX.$H(r.header)
         );
 
         try {
           // update the menu entry
           JX.DOM.replace(
             JX.$(r.conpherence_phid + '-nav-item'),
             JX.$H(r.nav_item)
           );
           selectThreadByID(r.conpherence_phid + '-nav-item');
         } catch (ex) {
           // Ignore; this view may not have a menu.
         }
       }))
       .start();
   });
 
-  var _oldLoadingTransactionID = null;
-  JX.Stratcom.listen('click', 'show-older-messages', function(e) {
-    e.kill();
-    var data = e.getNodeData('show-older-messages');
-    if (data.oldest_transaction_id == _oldLoadingTransactionID) {
-      return;
-    }
-    _oldLoadingTransactionID = data.oldest_transaction_id;
-
-    var node = e.getNode('show-older-messages');
-    JX.DOM.setContent(node, 'Loading...');
-    JX.DOM.alterClass(node, 'conpherence-show-more-messages-loading', true);
-
-    var conf_id = _thread.selected;
-    var messages_root = scrollbar.getContentNode();
-    new JX.Workflow(config.baseURI + conf_id + '/', data)
-    .setHandler(function(r) {
-      JX.DOM.remove(node);
-      var messages = JX.$H(r.messages);
-      JX.DOM.prependContent(
-        messages_root,
-        JX.$H(messages));
-    }).start();
-  });
-
-  var _newLoadingTransactionID = null;
-  JX.Stratcom.listen('click', 'show-newer-messages', function(e) {
-    e.kill();
-    var data = e.getNodeData('show-newer-messages');
-    if (data.newest_transaction_id == _newLoadingTransactionID) {
-      return;
-    }
-    _newLoadingTransactionID = data.newest_transaction_id;
-
-    var node = e.getNode('show-newer-messages');
-    JX.DOM.setContent(node, 'Loading...');
-    JX.DOM.alterClass(node, 'conpherence-show-more-messages-loading', true);
-
-    var conf_id = _thread.selected;
-    var messages_root = scrollbar.getContentNode();
-    new JX.Workflow(config.baseURI + conf_id + '/', data)
-    .setHandler(function(r) {
-      JX.DOM.remove(node);
-      var messages = JX.$H(r.messages);
-      JX.DOM.appendContent(
-        messages_root,
-        JX.$H(messages));
-    }).start();
-  });
-
   /**
    * On devices, we just show a thread list, so we don't want to automatically
    * select or load any threads. On desktop, we automatically select the first
    * thread, changing the _currentRole from list to thread.
    */
   function onDeviceChange() {
     var new_device = JX.Device.getDevice();
     if (new_device === _oldDevice) {
       return;
     }
 
     if (_oldDevice === null) {
       _oldDevice = new_device;
       if (_currentRole == 'list') {
         if (new_device != 'desktop') {
           return;
         }
       } else {
         loadThreads();
         return;
       }
     }
     var update_toggled_widget =
       new_device == 'desktop' || _oldDevice == 'desktop';
     _oldDevice = new_device;
 
     if (_thread.visible !== null && update_toggled_widget) {
       JX.Stratcom.invoke(
         'conpherence-did-redraw-thread',
         null,
         {
           widget : getDefaultWidget(),
           threadID : _thread.selected
         });
     }
 
     if (_currentRole == 'list' && new_device == 'desktop') {
       // this selects a thread and loads it
       didLoadThreads();
       _currentRole = 'thread';
       var root = JX.DOM.find(document, 'div', 'conpherence-layout');
       JX.DOM.alterClass(root, 'conpherence-role-list', false);
       JX.DOM.alterClass(root, 'conpherence-role-thread', true);
     }
   }
   JX.Stratcom.listen('phabricator-device-change', null, onDeviceChange);
 
   function loadThreads() {
     markThreadsLoading(true);
     var uri = config.baseURI + 'thread/' + config.selectedThreadID + '/';
     new JX.Workflow(uri)
       .setHandler(onLoadThreadsResponse)
       .start();
   }
 
   function onLoadThreadsResponse(r) {
     var layout = JX.$(config.layoutID);
     var menu = JX.DOM.find(layout, 'div', 'conpherence-menu-pane');
     JX.DOM.setContent(menu, JX.$H(r));
 
     config.selectedID && selectThreadByID(config.selectedID);
 
     markThreadsLoading(false);
   }
 
   function didLoadThreads() {
     // If there's no thread selected yet, select the current thread or the
     // first thread.
     if (!_thread.selected) {
       if (config.selectedID) {
         selectThreadByID(config.selectedID, true);
       } else {
         var layout = JX.$(config.layoutID);
         var threads = JX.DOM.scry(layout, 'a', 'conpherence-menu-click');
         if (threads.length) {
           selectThread(threads[0]);
         } else {
           var nothreads = JX.DOM.find(layout, 'div', 'conpherence-no-threads');
           nothreads.style.display = 'block';
           markThreadLoading(false);
           markWidgetLoading(false);
         }
       }
     }
   }
 
   JX.Stratcom.listen(
     ['click'],
     'conpherence-menu-see-more',
     function (e) {
       e.kill();
       var sigil = e.getNodeData('conpherence-menu-see-more').moreSigil;
       var root = JX.$('conpherence-menu-pane');
       var more = JX.DOM.scry(root, 'li', sigil);
       for (var i = 0; i < more.length; i++) {
         JX.DOM.alterClass(more[i], 'hidden', false);
       }
       JX.DOM.hide(e.getNode('conpherence-menu-see-more'));
     });
 
   JX.Stratcom.listen(
     ['keydown'],
     'conpherence-pontificate',
     function (e) {
       threadManager.handleDraftKeydown(e);
     });
 
 });
diff --git a/webroot/rsrc/js/application/differential/DifferentialInlineCommentEditor.js b/webroot/rsrc/js/application/differential/DifferentialInlineCommentEditor.js
index 049e3f1db1..9842da11f5 100644
--- a/webroot/rsrc/js/application/differential/DifferentialInlineCommentEditor.js
+++ b/webroot/rsrc/js/application/differential/DifferentialInlineCommentEditor.js
@@ -1,356 +1,356 @@
 /**
  * @provides differential-inline-comment-editor
  * @requires javelin-dom
  *           javelin-util
  *           javelin-stratcom
  *           javelin-install
  *           javelin-request
  *           javelin-workflow
  */
 
 JX.install('DifferentialInlineCommentEditor', {
 
   construct : function(uri) {
     this._uri = uri;
   },
 
   events : ['done'],
 
   members : {
     _uri : null,
     _undoText : null,
     _completed: false,
     _skipOverInlineCommentRows : function(node) {
       // TODO: Move this semantic information out of class names.
       while (node && node.className.indexOf('inline') !== -1) {
         node = node.nextSibling;
       }
       return node;
     },
     _buildRequestData : function() {
       return {
         op : this.getOperation(),
         on_right : this.getOnRight(),
         id : this.getID(),
         number : this.getLineNumber(),
-        is_new : this.getIsNew(),
+        is_new : (this.getIsNew() ? 1 : 0),
         length : this.getLength(),
         changesetID : this.getChangesetID(),
         text : this.getText() || '',
         renderer: this.getRenderer(),
         replyToCommentPHID: this.getReplyToCommentPHID() || '',
       };
     },
     _draw : function(content, exact_row) {
       var row = this.getRow();
       var table = this.getTable();
       var target = exact_row ? row : this._skipOverInlineCommentRows(row);
 
       function copyRows(dst, src, before) {
         var rows = JX.DOM.scry(src, 'tr');
         for (var ii = 0; ii < rows.length; ii++) {
 
           // Find the table this <tr /> belongs to. If it's a sub-table, like a
           // table in an inline comment, don't copy it.
           if (JX.DOM.findAbove(rows[ii], 'table') !== src) {
             continue;
           }
 
           if (before) {
             dst.insertBefore(rows[ii], before);
           } else {
             dst.appendChild(rows[ii]);
           }
         }
         return rows;
       }
 
       return copyRows(table, content, target);
     },
     _removeUndoLink : function() {
       var rows = JX.DifferentialInlineCommentEditor._undoRows;
       if (rows) {
         for (var ii = 0; ii < rows.length; ii++) {
           JX.DOM.remove(rows[ii]);
         }
       }
       JX.DifferentialInlineCommentEditor._undoRows = [];
     },
     _undo : function() {
       this._removeUndoLink();
 
       if (this._undoText) {
         this.setText(this._undoText);
       } else {
         this.setOperation('undelete');
       }
 
       this.start();
     },
     _registerUndoListener : function() {
       if (!JX.DifferentialInlineCommentEditor._activeEditor) {
         JX.Stratcom.listen(
           'click',
           'differential-inline-comment-undo',
           function(e) {
             JX.DifferentialInlineCommentEditor._activeEditor._undo();
             e.kill();
           });
       }
       JX.DifferentialInlineCommentEditor._activeEditor = this;
     },
     _setRowState : function(state) {
       var is_hidden   = (state == 'hidden');
       var is_loading  = (state == 'loading');
       var row = this.getRow();
       JX.DOM.alterClass(row, 'differential-inline-hidden', is_hidden);
       JX.DOM.alterClass(row, 'differential-inline-loading', is_loading);
     },
     _didContinueWorkflow : function(response) {
       var drawn = this._draw(JX.$H(response).getNode());
 
       var op = this.getOperation();
       if (op == 'edit') {
         this._setRowState('hidden');
       }
 
       JX.DOM.find(
         drawn[0],
         'textarea',
         'differential-inline-comment-edit-textarea').focus();
 
       var oncancel = JX.bind(this, function(e) {
         e.kill();
 
         this._didCancelWorkflow();
 
         if (op == 'edit') {
           this._setRowState('visible');
         }
 
         JX.DOM.remove(drawn[0]);
       });
       JX.DOM.listen(drawn[0], 'click', 'inline-edit-cancel', oncancel);
 
       var onsubmit = JX.bind(this, function(e) {
         e.kill();
 
         JX.Workflow.newFromForm(e.getTarget())
           .setHandler(JX.bind(this, function(response) {
             JX.DOM.remove(drawn[0]);
             if (op == 'edit') {
               this._setRowState('visible');
             }
             this._didCompleteWorkflow(response);
           }))
           .start();
 
         JX.DOM.alterClass(drawn[0], 'differential-inline-loading', true);
       });
       JX.DOM.listen(
         drawn[0],
         ['submit', 'didSyntheticSubmit'],
         'inline-edit-form',
         onsubmit);
     },
 
 
     _didCompleteWorkflow : function(response) {
       var op = this.getOperation();
 
       // We don't get any markup back if the user deletes a comment, or saves
       // an empty comment (which effects a delete).
       if (response.markup) {
         this._draw(JX.$H(response.markup).getNode());
       }
 
       if (op == 'delete' || op == 'refdelete') {
         this._undoText = null;
         this._drawUndo();
       } else {
         this._removeUndoLink();
       }
 
       // These operations remove the old row (edit adds a new row first).
       var remove_old = (op == 'edit' || op == 'delete' || op == 'refdelete');
       if (remove_old) {
         this._setRowState('hidden');
       }
 
       if (op == 'undelete') {
         this._setRowState('visible');
       }
 
       this._completed = true;
 
       JX.Stratcom.invoke('differential-inline-comment-update');
       this.invoke('done');
     },
 
 
     _didCancelWorkflow : function() {
       this.invoke('done');
 
       switch (this.getOperation()) {
         case 'delete':
         case 'refdelete':
           if (!this._completed) {
             this._setRowState('visible');
           }
           return;
         case 'undelete':
           return;
       }
 
       var textarea;
       try {
         textarea = JX.DOM.find(
           document.body, // TODO: use getDialogRootNode() when available
           'textarea',
           'differential-inline-comment-edit-textarea');
       } catch (ex) {
         // The close handler is called whenever the dialog closes, even if the
         // user closed it by completing the workflow with "Save". The
         // JX.Workflow API should probably be refined to allow programmatic
         // distinction of close caused by 'cancel' vs 'submit'. Testing for
         // presence of the textarea serves as a proxy for detecting a 'cancel'.
         return;
       }
 
       var text = textarea.value;
 
       // If the user hasn't edited the text (i.e., no change from original for
       // 'edit' or no text at all), don't offer them an undo.
       if (text == this.getOriginalText() || text === '') {
         return;
       }
 
       // Save the text so we can 'undo' back to it.
       this._undoText = text;
 
       this._drawUndo();
     },
 
     _drawUndo: function() {
       var templates = this.getTemplates();
       var template = this.getOnRight() ? templates.r : templates.l;
       template = JX.$H(template).getNode();
 
       // NOTE: Operation order matters here; we can't remove anything until
       // after we draw the new rows because _draw uses the old rows to figure
       // out where to place the comment.
 
       // We use 'exact_row' to put the "undo" text directly above the affected
       // comment.
       var exact_row = true;
       var rows = this._draw(template, exact_row);
 
       this._removeUndoLink();
 
       JX.DifferentialInlineCommentEditor._undoRows = rows;
     },
 
     start : function() {
       this._registerUndoListener();
 
       var data = this._buildRequestData();
       var op = this.getOperation();
 
       if (op == 'delete' || op == 'refdelete' || op == 'undelete') {
         this._setRowState('loading');
 
         var oncomplete = JX.bind(this, this._didCompleteWorkflow);
         var oncancel = JX.bind(this, this._didCancelWorkflow);
 
         new JX.Workflow(this._uri, data)
           .setHandler(oncomplete)
           .setCloseHandler(oncancel)
           .start();
       } else {
         var handler = JX.bind(this, this._didContinueWorkflow);
 
         if (op == 'edit') {
           this._setRowState('loading');
         }
 
         new JX.Request(this._uri, handler)
           .setData(data)
           .send();
       }
 
       return this;
     },
 
     deleteByID: function(id) {
       var data = {
         op: 'refdelete',
         id: id
       };
 
       new JX.Workflow(this._uri, data)
         .setHandler(JX.bind(this, function() {
           this._didUpdate();
         }))
         .start();
     },
 
     toggleCheckbox: function(id, checkbox) {
       var data = {
         op: 'done',
         id: id
       };
 
       new JX.Workflow(this._uri, data)
         .setHandler(JX.bind(this, function(r) {
           checkbox.checked = !checkbox.checked;
 
           var comment = JX.DOM.findAbove(
             checkbox,
             'div',
             'differential-inline-comment');
           JX.DOM.alterClass(comment, 'inline-is-done', !!checkbox.checked);
           JX.DOM.alterClass(comment, 'inline-state-is-draft', r.draftState);
 
           this._didUpdate();
         }))
         .start();
     },
 
     _didUpdate: function() {
       JX.Stratcom.invoke('differential-inline-comment-update');
     }
 
   },
 
   statics : {
     /**
      * Global refernece to the 'undo' rows currently rendered in the document.
      */
     _undoRows : null,
 
     /**
      * Global listener for the 'undo' click associated with the currently
      * displayed 'undo' link. When an editor is start()ed, it becomes the active
      * editor.
      */
     _activeEditor : null
   },
 
   properties : {
     operation : null,
     row : null,
     table : null,
     onRight : null,
     ID : null,
     lineNumber : null,
     changesetID : null,
     length : null,
     isNew : null,
     text : null,
     templates : null,
     originalText : null,
     renderer: null,
     replyToCommentPHID: null
   }
 
 });
diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js
index 20a78d0963..04e99fde28 100644
--- a/webroot/rsrc/js/application/projects/behavior-project-boards.js
+++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js
@@ -1,351 +1,358 @@
 /**
  * @provides javelin-behavior-project-boards
  * @requires javelin-behavior
  *           javelin-dom
  *           javelin-util
  *           javelin-vector
  *           javelin-stratcom
  *           javelin-workflow
  *           phabricator-draggable-list
  */
 
 JX.behavior('project-boards', function(config, statics) {
 
   function finditems(col) {
     return JX.DOM.scry(col, 'li', 'project-card');
   }
 
   function onupdate(col) {
     var data = JX.Stratcom.getData(col);
     var cards = finditems(col);
 
     // Update the count of tasks in the column header.
     if (!data.countTagNode) {
       data.countTagNode = JX.$(data.countTagID);
       JX.DOM.show(data.countTagNode);
     }
 
     var sum = 0;
     for (var ii = 0; ii < cards.length; ii++) {
       // TODO: Allow this to be computed in some more clever way.
       sum += 1;
     }
 
     // TODO: This is a little bit hacky, but we don't have a PHUIX version of
     // this element yet.
 
     var over_limit = (data.pointLimit && (sum > data.pointLimit));
 
     var display_value = sum;
     if (data.pointLimit) {
       display_value = sum + ' / ' + data.pointLimit;
     }
     JX.DOM.setContent(JX.$(data.countTagContentID), display_value);
 
 
     var panel_map = {
       'project-panel-empty': !cards.length,
       'project-panel-over-limit': over_limit
     };
     var panel = JX.DOM.findAbove(col, 'div', 'workpanel');
     for (var p in panel_map) {
       JX.DOM.alterClass(panel, p, !!panel_map[p]);
     }
 
     var color_map = {
       'phui-tag-shade-disabled': (sum === 0),
       'phui-tag-shade-blue': (sum > 0 && !over_limit),
       'phui-tag-shade-red': (over_limit)
     };
     for (var c in color_map) {
       JX.DOM.alterClass(data.countTagNode, c, !!color_map[c]);
     }
   }
 
   function onresponse(response, item, list) {
     list.unlock();
     JX.DOM.alterClass(item, 'drag-sending', false);
     JX.DOM.replace(item, JX.$H(response.task));
   }
 
   function getcolumns() {
     return JX.DOM.scry(JX.$(statics.boardID), 'ul', 'project-column');
   }
 
   function colsort(u, v) {
     var ud = JX.Stratcom.getData(u).sort || [];
     var vd = JX.Stratcom.getData(v).sort || [];
 
     for (var ii = 0; ii < ud.length; ii++) {
 
       if (parseInt(ud[ii]) < parseInt(vd[ii])) {
         return 1;
       }
       if (parseInt(ud[ii]) > parseInt(vd[ii])) {
         return -1;
       }
     }
 
     return 0;
   }
 
   function getcontainer() {
     return JX.DOM.find(
       JX.$(statics.boardID),
       'div',
       'aphront-multi-column-view');
   }
 
   function onbegindrag(item) {
     // If the longest column on the board is taller than the window, the board
     // will scroll vertically. Dragging an item to the longest column may
     // make it longer, by the total height of the board, plus the height of
     // the drop target.
 
     // If this happens, the scrollbar will jump around and the scroll position
     // can be adjusted in a disorienting way. To reproduce this, drag a task
     // to the bottom of the longest column on a scrolling board and wave the
     // task in and out of the column. The scroll bar will jump around and
     // it will be hard to lock onto a target.
 
     // To fix this, set the minimum board height to the current board height
     // plus the size of the drop target (which is the size of the item plus
     // a bit of margin). This makes sure the scroll bar never needs to
     // recalculate.
 
     var item_size = JX.Vector.getDim(item);
     var container = getcontainer();
     var container_size = JX.Vector.getDim(container);
 
     container.style.minHeight = (item_size.y + container_size.y + 12) + 'px';
   }
 
   function onenddrag() {
     getcontainer().style.minHeight = '';
   }
 
   function ondrop(list, item, after) {
     list.lock();
     JX.DOM.alterClass(item, 'drag-sending', true);
 
     var item_phid = JX.Stratcom.getData(item).objectPHID;
     var data = {
       objectPHID: item_phid,
       columnPHID: JX.Stratcom.getData(list.getRootNode()).columnPHID
     };
 
     var after_phid = null;
     var items = finditems(list.getRootNode());
     if (after) {
       after_phid = JX.Stratcom.getData(after).objectPHID;
       data.afterPHID = after_phid;
     }
     var ii;
     var ii_item;
     var ii_item_phid;
     var ii_prev_item_phid = null;
     var before_phid = null;
     for (ii = 0; ii < items.length; ii++) {
       ii_item = items[ii];
       ii_item_phid = JX.Stratcom.getData(ii_item).objectPHID;
       if (ii_item_phid == item_phid) {
         // skip the item we just dropped
         continue;
       }
       // note this handles when there is no after phid - we are at the top of
       // the list - quite nicely
       if (ii_prev_item_phid == after_phid) {
         before_phid = ii_item_phid;
         break;
       }
       ii_prev_item_phid = ii_item_phid;
     }
     if (before_phid) {
       data.beforePHID = before_phid;
     }
 
     data.order = statics.order;
 
     var workflow = new JX.Workflow(statics.moveURI, data)
       .setHandler(function(response) {
         onresponse(response, item, list);
       });
 
     workflow.start();
   }
 
   function onedit(column, r) {
     var new_card = JX.$H(r.tasks).getNode();
     var new_data = JX.Stratcom.getData(new_card);
     var items = finditems(column);
     var edited = false;
     var remove_index = null;
 
     for (var ii = 0; ii < items.length; ii++) {
       var item = items[ii];
 
       var data = JX.Stratcom.getData(item);
       var phid = data.objectPHID;
 
       if (phid == new_data.objectPHID) {
         if (r.data.removeFromBoard) {
           remove_index = ii;
         }
         items[ii] = new_card;
         data = new_data;
         edited = true;
       }
 
       data.sort = r.data.sortMap[data.objectPHID] || data.sort;
     }
 
     // this is an add then...!
     if (!edited) {
       items[items.length + 1] = new_card;
       new_data.sort = r.data.sortMap[new_data.objectPHID] || new_data.sort;
     }
 
     if (remove_index !== null) {
       items.splice(remove_index, 1);
     }
 
     items.sort(colsort);
 
     JX.DOM.setContent(column, items);
 
     onupdate(column);
   };
 
   function update_statics(update_config) {
     statics.boardID = update_config.boardID;
     statics.projectPHID = update_config.projectPHID;
     statics.order = update_config.order;
     statics.moveURI = update_config.moveURI;
     statics.createURI = update_config.createURI;
   }
 
   function init_board() {
     var lists = [];
     var ii;
     var cols = getcolumns();
 
     for (ii = 0; ii < cols.length; ii++) {
       var list = new JX.DraggableList('project-card', cols[ii])
         .setFindItemsHandler(JX.bind(null, finditems, cols[ii]));
 
       list.listen('didSend', JX.bind(list, onupdate, cols[ii]));
       list.listen('didReceive', JX.bind(list, onupdate, cols[ii]));
 
       list.listen('didDrop', JX.bind(null, ondrop, list));
 
       list.listen('didBeginDrag', JX.bind(null, onbegindrag));
       list.listen('didEndDrag', JX.bind(null, onenddrag));
 
       lists.push(list);
 
       onupdate(cols[ii]);
     }
 
     for (ii = 0; ii < lists.length; ii++) {
       lists[ii].setGroup(lists);
     }
+  }
+
+  function setup() {
 
     JX.Stratcom.listen(
       'click',
       ['edit-project-card'],
       function(e) {
         e.kill();
         var column = e.getNode('project-column');
         var request_data = {
           responseType: 'card',
           columnPHID: JX.Stratcom.getData(column).columnPHID,
           order: statics.order
         };
         new JX.Workflow(e.getNode('tag:a').href, request_data)
           .setHandler(JX.bind(null, onedit, column))
           .start();
       });
 
     JX.Stratcom.listen(
       'click',
       ['column-add-task'],
       function (e) {
 
         // We want the 'boards-dropdown-menu' behavior to see this event and
         // close the dropdown, but don't want to follow the link.
         e.prevent();
 
         var column_phid = e.getNodeData('column-add-task').columnPHID;
         var request_data = {
           responseType: 'card',
           columnPHID: column_phid,
           projects: statics.projectPHID,
           order: statics.order
         };
         var cols = getcolumns();
         var ii;
         var column;
         for (ii = 0; ii < cols.length; ii++) {
           if (JX.Stratcom.getData(cols[ii]).columnPHID == column_phid) {
             column = cols[ii];
             break;
           }
         }
         new JX.Workflow(statics.createURI, request_data)
           .setHandler(JX.bind(null, onedit, column))
           .start();
       });
 
     JX.Stratcom.listen('click', 'boards-dropdown-menu', function(e) {
       var data = e.getNodeData('boards-dropdown-menu');
       if (data.menu) {
         return;
       }
 
       e.kill();
 
       var list = JX.$H(data.items).getFragment().firstChild;
 
       var button = e.getNode('boards-dropdown-menu');
       data.menu = new JX.PHUIXDropdownMenu(button);
       data.menu.setContent(list);
       data.menu.open();
 
       JX.DOM.listen(list, 'click', 'tag:a', function(e) {
         if (!e.isNormalClick()) {
           return;
         }
         data.menu.close();
       });
     });
 
     JX.Stratcom.listen(
       'quicksand-redraw',
       null,
       function (e) {
         var data = e.getData();
         if (!data.newResponse.boardConfig) {
           return;
         }
         var new_config;
         if (data.fromServer) {
           new_config = data.newResponse.boardConfig;
           statics.boardConfigCache[data.newResponseID] = new_config;
         } else {
           new_config = statics.boardConfigCache[data.newResponseID];
           statics.boardID = new_config.boardID;
         }
         update_statics(new_config);
+        if (data.fromServer) {
+          init_board();
+        }
       });
     return true;
   }
 
   if (!statics.setup) {
     update_statics(config);
     var current_page_id = JX.Quicksand.getCurrentPageID();
     statics.boardConfigCache = {};
     statics.boardConfigCache[current_page_id] = config;
-    statics.setup = init_board();
+    init_board();
+    statics.setup = setup();
   }
 
 });
diff --git a/webroot/rsrc/js/core/Hovercard.js b/webroot/rsrc/js/core/Hovercard.js
index e83b0fcba0..15149eceeb 100644
--- a/webroot/rsrc/js/core/Hovercard.js
+++ b/webroot/rsrc/js/core/Hovercard.js
@@ -1,158 +1,163 @@
 /**
  * @requires javelin-install
  *           javelin-dom
  *           javelin-vector
  *           javelin-request
  *           javelin-uri
  * @provides phabricator-hovercard
  * @javelin
  */
 
 JX.install('Hovercard', {
 
   statics : {
     _node : null,
     _activeRoot : null,
     _visiblePHID : null,
 
     fetchUrl : '/search/hovercard/retrieve/',
 
     /**
      * Hovercard storage. {"PHID-XXXX-YYYY":"<...>", ...}
      */
     _cards : {},
 
     getAnchor : function() {
       return this._activeRoot;
     },
 
     getCard : function() {
       var self = JX.Hovercard;
       return self._node;
     },
 
     show : function(root, phid) {
       var self = JX.Hovercard;
       // Already displaying
       if (self.getCard() && phid == self._visiblePHID) {
         return;
       }
       self.hide();
 
       self._visiblePHID = phid;
       self._activeRoot = root;
 
       if (!(phid in self._cards)) {
         self._load([phid]);
       } else {
         self._drawCard(phid);
       }
     },
 
     _drawCard : function(phid) {
       var self = JX.Hovercard;
       // card is loading...
       if (self._cards[phid] === true) {
         return;
       }
       // Not the current requested card
       if (phid != self._visiblePHID) {
         return;
       }
       // Not loaded
       if (!(phid in self._cards)) {
         return;
       }
 
       var root = self._activeRoot;
       var node = JX.$N('div',
         { className: 'jx-hovercard-container' },
         JX.$H(self._cards[phid]));
 
       self._node = node;
 
       // Append the card to the document, but offscreen, so we can measure it.
       node.style.left = '-10000px';
       document.body.appendChild(node);
 
       // Retrieve size from child (wrapper), since node gives wrong dimensions?
       var child = node.firstChild;
       var p = JX.$V(root);
       var d = JX.Vector.getDim(root);
       var n = JX.Vector.getDim(child);
+      var v = JX.Vector.getViewport();
 
       // Move the tip so it's nicely aligned.
       // I'm just doing north/south alignment for now
       // TODO: Fix southern graceful align
       var margin = 20;
       // We can't shift left by ~$margin or more here due to Pholio, Phriction
       var x = parseInt(p.x, 10) - margin / 2;
       var y = parseInt(p.y - n.y, 10) - margin;
 
+      // If running off the edge of the viewport, make it margin / 2 away
+      // from the far right edge of the viewport instead
+      if ((x + n.x) > (v.x)) {
+        x = x - parseInt(x + n.x - v.x + margin / 2, 10);
       // If more in the center, we can safely center
-      if (x > (n.x / 2) + margin) {
+      } else if (x > (n.x / 2) + margin) {
         x = parseInt(p.x - (n.x / 2) + d.x, 10);
       }
 
       // Temporarily disabled, since it gives weird results (you can only see
       // a hovercard once, as soon as it's hidden, it cannot be shown again)
       // if (y < n.y) {
       //   // Place it southern, since northern is not enough space
       //   y = p.y + d.y + margin;
       // }
 
       node.style.left = x + 'px';
       node.style.top  = y + 'px';
     },
 
     hide : function() {
       var self = JX.Hovercard;
       self._visiblePHID = null;
       self._activeRoot = null;
       if (self._node) {
         JX.DOM.remove(self._node);
         self._node = null;
       }
     },
 
     /**
      * Pass it an array of phids to load them into storage
      *
      * @param list phids
      */
     _load : function(phids) {
       var self = JX.Hovercard;
       var uri = JX.$U(self.fetchUrl);
 
       var send = false;
       for (var ii = 0; ii < phids.length; ii++) {
         var phid = phids[ii];
         if (phid in self._cards) {
           continue;
         }
         self._cards[phid] = true; // means "loading"
         uri.setQueryParam('phids['+ii+']', phids[ii]);
         send = true;
       }
 
       if (!send) {
         // already loaded / loading everything!
         return;
       }
 
       new JX.Request(uri, function(r) {
         for (var phid in r.cards) {
           self._cards[phid] = r.cards[phid];
 
           // Don't draw if the user is faster than the browser
           // Only draw if the user is still requesting the original card
           if (self.getCard() && phid != self._visiblePHID) {
             continue;
           }
 
           self._drawCard(phid);
         }
       }).send();
     }
   }
 });