diff --git a/resources/celerity/map.php b/resources/celerity/map.php
index d9c1bcda8..eef8a2b47 100644
--- a/resources/celerity/map.php
+++ b/resources/celerity/map.php
@@ -1,2197 +1,2198 @@
 <?php
 
 /**
  * This file is automatically generated. Use 'bin/celerity map' to rebuild it.
  *
  * @generated
  */
 return array(
   'names' => array(
     'core.pkg.css' => 'a9770fbb',
     'core.pkg.js' => 'a77025a1',
     'darkconsole.pkg.js' => '8ab24e01',
     'differential.pkg.css' => 'd8866ed8',
-    'differential.pkg.js' => '7b5a4aa4',
+    'differential.pkg.js' => '9e55f9f5',
     'diffusion.pkg.css' => '591664fa',
     'diffusion.pkg.js' => 'bfc0737b',
     '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' => 'd2e76b88',
     'rsrc/css/aphront/lightbox-attachment.css' => '7acac05d',
     'rsrc/css/aphront/list-filter-view.css' => '2ae43867',
     'rsrc/css/aphront/multi-column.css' => '41a848c0',
     'rsrc/css/aphront/notification.css' => '9c279160',
     'rsrc/css/aphront/pager-view.css' => '2e3539af',
     'rsrc/css/aphront/panel-view.css' => '7e0c3a56',
     'rsrc/css/aphront/phabricator-nav-view.css' => '7aeaf435',
     'rsrc/css/aphront/table-view.css' => 'b22b7216',
     'rsrc/css/aphront/tokenizer.css' => '82ce2142',
     'rsrc/css/aphront/tooltip.css' => '4099b97e',
     'rsrc/css/aphront/transaction.css' => '5d0cae25',
     'rsrc/css/aphront/two-column.css' => '16ab3ad2',
     'rsrc/css/aphront/typeahead.css' => '0e403212',
     'rsrc/css/application/almanac/almanac.css' => 'dbb9b3af',
     'rsrc/css/application/auth/auth.css' => '1e655982',
     'rsrc/css/application/base/main-menu-view.css' => 'f9f5cd1b',
     'rsrc/css/application/base/notification-menu.css' => '6aa0a74b',
     'rsrc/css/application/base/phabricator-application-launch-view.css' => '16ca323f',
     'rsrc/css/application/base/standard-page-view.css' => 'df338a4b',
     'rsrc/css/application/chatlog/chatlog.css' => '852140ff',
     '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' => '3b836442',
     'rsrc/css/application/conpherence/menu.css' => '73774137',
     'rsrc/css/application/conpherence/message-pane.css' => '17a9517f',
     'rsrc/css/application/conpherence/notification.css' => '04a6e10a',
     'rsrc/css/application/conpherence/update.css' => '1099a660',
     'rsrc/css/application/conpherence/widget-pane.css' => '3d575438',
     'rsrc/css/application/contentsource/content-source-view.css' => '4b8b05d4',
     'rsrc/css/application/countdown/timer.css' => '86b7b0a0',
     'rsrc/css/application/dashboard/dashboard.css' => '17937d22',
     'rsrc/css/application/diff/inline-comment-summary.css' => 'eb5f8e8c',
     'rsrc/css/application/differential/add-comment.css' => 'c478bcaa',
     'rsrc/css/application/differential/changeset-view.css' => 'b600950c',
     'rsrc/css/application/differential/core.css' => '7ac3cabc',
     'rsrc/css/application/differential/results-table.css' => '181aa9d9',
     'rsrc/css/application/differential/revision-comment.css' => '48186045',
     '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-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/home/home.css' => 'e34bf140',
     'rsrc/css/application/maniphest/batch-editor.css' => '8f380ebc',
     'rsrc/css/application/maniphest/report.css' => 'd8ed71ad',
     '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' => 'ebab8a70',
     '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' => '559cc554',
     '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' => '86bfbe8c',
     'rsrc/css/core/remarkup.css' => '2dbff225',
     'rsrc/css/core/syntax.css' => '56c1ba38',
     'rsrc/css/core/z-index.css' => '9ec70c03',
     'rsrc/css/diviner/diviner-shared.css' => '38813222',
     'rsrc/css/font/font-awesome.css' => 'ae9a7b4d',
     'rsrc/css/font/font-source-sans-pro.css' => '0d859f60',
     'rsrc/css/font/phui-font-icon-base.css' => '3dad2ae3',
     'rsrc/css/layout/phabricator-filetree-view.css' => 'fccf9f82',
     'rsrc/css/layout/phabricator-hovercard-view.css' => '893f4783',
     'rsrc/css/layout/phabricator-side-menu-view.css' => '7e8c6341',
     'rsrc/css/layout/phabricator-source-code-view.css' => '2ceee894',
     'rsrc/css/phui/calendar/phui-calendar-day.css' => 'de035c8a',
     'rsrc/css/phui/calendar/phui-calendar-list.css' => 'c1d0ca59',
     'rsrc/css/phui/calendar/phui-calendar-month.css' => 'a92e47d2',
     'rsrc/css/phui/calendar/phui-calendar.css' => '8675968e',
     'rsrc/css/phui/phui-action-header-view.css' => '89c497e7',
     'rsrc/css/phui/phui-action-list.css' => '9ee9910a',
     'rsrc/css/phui/phui-action-panel.css' => '3ee9afd5',
     'rsrc/css/phui/phui-box.css' => '7b3a2eed',
     'rsrc/css/phui/phui-button.css' => '21cb97f9',
     'rsrc/css/phui/phui-crumbs-view.css' => '594d719e',
     'rsrc/css/phui/phui-document.css' => '0f83a7df',
     'rsrc/css/phui/phui-feed-story.css' => 'c9f3a0b5',
     'rsrc/css/phui/phui-fontkit.css' => 'd30f4fa3',
     'rsrc/css/phui/phui-form-view.css' => '76f0b086',
     'rsrc/css/phui/phui-form.css' => 'f535f938',
     'rsrc/css/phui/phui-header-view.css' => '083669db',
     'rsrc/css/phui/phui-icon.css' => 'd35aa857',
     'rsrc/css/phui/phui-image-mask.css' => '5a8b09c8',
     'rsrc/css/phui/phui-info-panel.css' => '27ea50a1',
     'rsrc/css/phui/phui-info-view.css' => '6c6cc4dd',
     'rsrc/css/phui/phui-list.css' => '53deb25c',
     'rsrc/css/phui/phui-object-box.css' => 'd68ce5dc',
     'rsrc/css/phui/phui-object-item-list-view.css' => '9db65899',
     'rsrc/css/phui/phui-pinboard-view.css' => '3dd4a269',
     'rsrc/css/phui/phui-property-list-view.css' => '51480060',
     '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' => 'ea469f3a',
     'rsrc/css/phui/phui-text.css' => 'cf019f54',
     'rsrc/css/phui/phui-timeline-view.css' => 'b0fbc4d7',
     'rsrc/css/phui/phui-workboard-view.css' => '8896938c',
     'rsrc/css/phui/phui-workpanel-view.css' => 'e495a5cc',
     'rsrc/css/sprite-gradient.css' => '4bdb98a7',
     'rsrc/css/sprite-login.css' => 'a355d921',
     'rsrc/css/sprite-main-header.css' => '28d01b0b',
     'rsrc/css/sprite-menu.css' => '9ef76324',
     '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-Bold.woff2' => '165f5f74',
     'rsrc/externals/font/sourcesans/SourceSansPro-BoldIt.woff' => 'd09a7d54',
     'rsrc/externals/font/sourcesans/SourceSansPro-BoldIt.woff2' => 'd2e33102',
     '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.woff' => '3614608c',
     'rsrc/externals/font/sourcesans/SourceSansProBold.woff' => 'cbf46566',
     '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' => '2bd3c675',
     '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' => '2e0148bc',
     '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' => 'f960d43d',
     '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' => '5b2f5a08',
     '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' => '84d6aea0',
     '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' => '7644823e',
     'rsrc/externals/javelin/lib/control/typeahead/Typeahead.js' => '70baed2f',
     'rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js' => '6f7a9da8',
     '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/loading.gif' => '75d384cc',
     'rsrc/image/loading/boating_24.gif' => '5c90f086',
     'rsrc/image/loading/compass_24.gif' => 'b36b4f46',
     'rsrc/image/loading/loading_24.gif' => '26bc9adc',
     'rsrc/image/loading/loading_48.gif' => '6a4994c7',
     'rsrc/image/loading/loading_d48.gif' => 'cdcbe900',
     'rsrc/image/loading/loading_w24.gif' => '7662fa2b',
     '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/search-white.png' => '64cc0d45',
     'rsrc/image/search.png' => '82625a7e',
     'rsrc/image/sprite-gradient.png' => 'ec15a417',
     'rsrc/image/sprite-login-X2.png' => '5ae6de3a',
     'rsrc/image/sprite-login.png' => '07f2c67c',
     'rsrc/image/sprite-main-header.png' => '39419fa6',
     'rsrc/image/sprite-menu-X2.png' => '1c90d7bc',
     'rsrc/image/sprite-menu.png' => '619781ee',
     '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' => '2be71d56',
     'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => 'cc2d9c80',
     'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => '851f167c',
     'rsrc/js/application/aphlict/behavior-aphlict-status.js' => 'ea681761',
     'rsrc/js/application/auth/behavior-persona-login.js' => '9414ff18',
     'rsrc/js/application/config/behavior-reorder-fields.js' => '14a827de',
     'rsrc/js/application/conpherence/behavior-durable-column.js' => 'a88a26cc',
     'rsrc/js/application/conpherence/behavior-menu.js' => '869e3445',
     'rsrc/js/application/conpherence/behavior-pontificate.js' => '86df5915',
     'rsrc/js/application/conpherence/behavior-widget-pane.js' => '40b1ff90',
     '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' => 'c024db3d',
-    'rsrc/js/application/differential/DifferentialInlineCommentEditor.js' => 'f2441746',
+    'rsrc/js/application/differential/ChangesetViewManager.js' => 'fce415a0',
+    'rsrc/js/application/differential/DifferentialInlineCommentEditor.js' => '6a049cf7',
     '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' => '6932def3',
     'rsrc/js/application/differential/behavior-diff-radios.js' => 'e1ff79b1',
-    'rsrc/js/application/differential/behavior-dropdown-menus.js' => 'e33d4bc5',
+    'rsrc/js/application/differential/behavior-dropdown-menus.js' => '2035b9cb',
     'rsrc/js/application/differential/behavior-edit-inline-comments.js' => '65936067',
     'rsrc/js/application/differential/behavior-keyboard-nav.js' => '2c426492',
     'rsrc/js/application/differential/behavior-populate.js' => 'bdb3e4d0',
     'rsrc/js/application/differential/behavior-show-field-details.js' => 'bba9eedf',
-    'rsrc/js/application/differential/behavior-show-more.js' => '954d2de0',
+    'rsrc/js/application/differential/behavior-show-more.js' => 'c662904a',
     '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' => 'f7f1289f',
     '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' => '6e2de6f2',
     'rsrc/js/application/herald/PathTypeahead.js' => 'f7fc67ec',
     'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3',
     'rsrc/js/application/maniphest/behavior-batch-editor.js' => 'f24f3253',
     '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' => '9c2623f4',
     'rsrc/js/application/pholio/behavior-pholio-mock-view.js' => 'e58bf807',
     'rsrc/js/application/phortune/behavior-balanced-payment-form.js' => 'b2c03e60',
     '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' => 'f3fef818',
     'rsrc/js/application/policy/behavior-policy-rule-editor.js' => 'fe9a552f',
     'rsrc/js/application/ponder/behavior-votebox.js' => '4e9b766b',
     'rsrc/js/application/projects/behavior-boards-dropdown.js' => '0ec56e1d',
     'rsrc/js/application/projects/behavior-project-boards.js' => '87cb6b51',
     '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/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' => '6453c869',
     'rsrc/js/core/DragAndDropFileUpload.js' => '8c49f386',
     'rsrc/js/core/DraggableList.js' => 'a16ec1c6',
     'rsrc/js/core/FileUpload.js' => 'a4ae61bf',
     'rsrc/js/core/Hovercard.js' => '7e8468ae',
     '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' => '72da38cc',
     'rsrc/js/core/ShapedRequest.js' => '7cbe244b',
     'rsrc/js/core/TextAreaUtils.js' => '5c93c52c',
     'rsrc/js/core/Title.js' => '5c1c758c',
     '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' => '08883e8b',
     'rsrc/js/core/behavior-device.js' => '03d6ed07',
     'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '92eb531d',
     'rsrc/js/core/behavior-error-log.js' => '6882e80a',
     'rsrc/js/core/behavior-fancy-datepicker.js' => 'c51ae228',
     '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' => '07f199d8',
     'rsrc/js/core/behavior-high-security-warning.js' => '8fc1c918',
     '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-konami.js' => '5bc2cb21',
     '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' => '724b1247',
     'rsrc/js/core/behavior-select-on-click.js' => '4e3e79a6',
     'rsrc/js/core/behavior-toggle-class.js' => 'e566f52c',
     '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' => '6e8cefa4',
     'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'bd4c8dca',
   ),
   'symbols' => array(
     'almanac-css' => 'dbb9b3af',
     'aphront-bars' => '231ac33c',
     'aphront-dark-console-css' => '6378ef3d',
     'aphront-dialog-view-css' => 'd2e76b88',
     'aphront-list-filter-view-css' => '2ae43867',
     'aphront-multi-column-view-css' => '41a848c0',
     'aphront-pager-view-css' => '2e3539af',
     'aphront-panel-view-css' => '7e0c3a56',
     'aphront-table-view-css' => 'b22b7216',
     'aphront-tokenizer-control-css' => '82ce2142',
     'aphront-tooltip-css' => '4099b97e',
     'aphront-two-column-view-css' => '16ab3ad2',
     'aphront-typeahead-control-css' => '0e403212',
     'auth-css' => '1e655982',
-    'changeset-view-manager' => 'c024db3d',
+    'changeset-view-manager' => 'fce415a0',
     'config-options-css' => '7fedf08b',
     'config-welcome-css' => '6abd79be',
     'conpherence-durable-column-view' => '3b836442',
     'conpherence-menu-css' => '73774137',
     'conpherence-message-pane-css' => '17a9517f',
     'conpherence-notification-css' => '04a6e10a',
     'conpherence-update-css' => '1099a660',
     'conpherence-widget-pane-css' => '3d575438',
     'differential-changeset-view-css' => 'b600950c',
     'differential-core-view-css' => '7ac3cabc',
-    'differential-inline-comment-editor' => 'f2441746',
+    'differential-inline-comment-editor' => '6a049cf7',
     'differential-results-table-css' => '181aa9d9',
     'differential-revision-add-comment-css' => 'c478bcaa',
     'differential-revision-comment-css' => '48186045',
     'differential-revision-history-css' => '0e8eb855',
     'differential-revision-list-css' => 'f3c47d33',
     'differential-table-of-contents-css' => '63f3ef4a',
     'diffusion-icons-css' => '9c5828da',
     'diffusion-source-css' => '66fdf661',
     'diviner-shared-css' => '38813222',
     'font-fontawesome' => 'ae9a7b4d',
     'font-source-sans-pro' => '0d859f60',
     'global-drag-and-drop-css' => '697324ad',
     'harbormaster-css' => '49d64eb4',
     'herald-css' => '826075fa',
     'herald-rule-editor' => '6e2de6f2',
     'herald-test-css' => '778b008e',
     'homepage-panel-css' => 'e34bf140',
     'inline-comment-summary-css' => 'eb5f8e8c',
     'javelin-aphlict' => '2be71d56',
     'javelin-behavior' => '61cbc29a',
     'javelin-behavior-aphlict-dropdown' => 'cc2d9c80',
     'javelin-behavior-aphlict-listen' => '851f167c',
     'javelin-behavior-aphlict-status' => 'ea681761',
     'javelin-behavior-aphront-basic-tokenizer' => 'b3a4b884',
     'javelin-behavior-aphront-crop' => 'fa0f4fc2',
     'javelin-behavior-aphront-drag-and-drop-textarea' => '92eb531d',
     '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-balanced-payment-form' => 'b2c03e60',
     'javelin-behavior-boards-dropdown' => '0ec56e1d',
     'javelin-behavior-choose-control' => '6153c708',
     'javelin-behavior-config-reorder-fields' => '14a827de',
     'javelin-behavior-conpherence-menu' => '869e3445',
     'javelin-behavior-conpherence-pontificate' => '86df5915',
     'javelin-behavior-conpherence-widget-pane' => '40b1ff90',
     'javelin-behavior-countdown-timer' => 'e4cc26b3',
     'javelin-behavior-dark-console' => '08883e8b',
     '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' => '03d6ed07',
     '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' => 'e33d4bc5',
+    'javelin-behavior-differential-dropdown-menus' => '2035b9cb',
     'javelin-behavior-differential-edit-inline-comments' => '65936067',
     'javelin-behavior-differential-feedback-preview' => '6932def3',
     'javelin-behavior-differential-keyboard-navigation' => '2c426492',
     'javelin-behavior-differential-populate' => 'bdb3e4d0',
     'javelin-behavior-differential-show-field-details' => 'bba9eedf',
-    'javelin-behavior-differential-show-more' => '954d2de0',
+    'javelin-behavior-differential-show-more' => 'c662904a',
     'javelin-behavior-differential-toggle-files' => 'ca3f91eb',
     'javelin-behavior-differential-user-select' => 'a8d8459d',
     'javelin-behavior-diffusion-commit-branches' => 'bdaf4d04',
     'javelin-behavior-diffusion-commit-graph' => 'f7f1289f',
     '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' => 'a88a26cc',
     'javelin-behavior-error-log' => '6882e80a',
     'javelin-behavior-fancy-datepicker' => 'c51ae228',
     'javelin-behavior-global-drag-and-drop' => '07f199d8',
     'javelin-behavior-herald-rule-editor' => '7ebaeed3',
     'javelin-behavior-high-security-warning' => '8fc1c918',
     'javelin-behavior-history-install' => '7ee2b591',
     'javelin-behavior-icon-composer' => '8ef9ab58',
     'javelin-behavior-konami' => '5bc2cb21',
     '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' => 'f24f3253',
     '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' => '724b1247',
     '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' => '9c2623f4',
     'javelin-behavior-pholio-mock-view' => 'e58bf807',
     'javelin-behavior-phui-object-box-tabs' => '2bfa2836',
     'javelin-behavior-phui-timeline-dropdown-menu' => '4d94d9c3',
     'javelin-behavior-policy-control' => 'f3fef818',
     'javelin-behavior-policy-rule-editor' => 'fe9a552f',
     'javelin-behavior-ponder-votebox' => '4e9b766b',
     'javelin-behavior-project-boards' => '87cb6b51',
     'javelin-behavior-project-create' => '065227cc',
     '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' => 'e566f52c',
     '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' => '2e0148bc',
     'javelin-install' => '05270951',
     'javelin-json' => '69adf288',
     'javelin-leader' => '331b1611',
     'javelin-magical-init' => '2bd3c675',
     'javelin-mask' => '8a41885b',
     'javelin-quicksand' => 'f960d43d',
     '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' => '5b2f5a08',
     'javelin-stratcom' => '6c53634d',
     'javelin-tokenizer' => '7644823e',
     'javelin-typeahead' => '70baed2f',
     'javelin-typeahead-composite-source' => '503e17fd',
     'javelin-typeahead-normalizer' => '6f7a9da8',
     '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' => '84d6aea0',
     'lightbox-attachment-css' => '7acac05d',
     'maniphest-batch-editor' => '8f380ebc',
     'maniphest-report-css' => 'd8ed71ad',
     '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' => '9ee9910a',
     'phabricator-application-launch-view-css' => '16ca323f',
     'phabricator-busy' => '6453c869',
     'phabricator-chatlog-css' => '852140ff',
     'phabricator-content-source-view-css' => '4b8b05d4',
     'phabricator-core-css' => '86bfbe8c',
     'phabricator-countdown-css' => '86b7b0a0',
     'phabricator-dashboard-css' => '17937d22',
     'phabricator-drag-and-drop-file-upload' => '8c49f386',
     'phabricator-draggable-list' => 'a16ec1c6',
     'phabricator-fatal-config-template-css' => '8e6c6fcd',
     'phabricator-feed-css' => 'b513b5f4',
     'phabricator-file-upload' => 'a4ae61bf',
     'phabricator-filetree-view-css' => 'fccf9f82',
     'phabricator-flag-css' => '5337623f',
     'phabricator-hovercard' => '7e8468ae',
     'phabricator-hovercard-view-css' => '893f4783',
     'phabricator-keyboard-shortcut' => '1ae869f2',
     'phabricator-keyboard-shortcut-manager' => 'c1700f6f',
     'phabricator-main-menu-view' => 'f9f5cd1b',
     'phabricator-nav-view-css' => '7aeaf435',
     'phabricator-notification' => '0c6946e7',
     'phabricator-notification-css' => '9c279160',
     'phabricator-notification-menu-css' => '6aa0a74b',
     'phabricator-object-selector-css' => '029a133d',
     'phabricator-phtize' => 'd254d646',
     'phabricator-prefab' => '72da38cc',
     'phabricator-profile-css' => '1a20dcbf',
     'phabricator-remarkup-css' => '2dbff225',
     'phabricator-search-results-css' => '559cc554',
     'phabricator-shaped-request' => '7cbe244b',
     'phabricator-side-menu-view-css' => '7e8c6341',
     'phabricator-slowvote-css' => '266df6a1',
     'phabricator-source-code-view-css' => '2ceee894',
     'phabricator-standard-page-view' => 'df338a4b',
     'phabricator-textareautils' => '5c93c52c',
     'phabricator-title' => '5c1c758c',
     'phabricator-tooltip' => '1d298e3a',
     'phabricator-transaction-view-css' => '5d0cae25',
     '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' => '9ec70c03',
     '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' => '89c497e7',
     'phui-action-panel-css' => '3ee9afd5',
     'phui-box-css' => '7b3a2eed',
     'phui-button-css' => '21cb97f9',
     'phui-calendar-css' => '8675968e',
     'phui-calendar-day-css' => 'de035c8a',
     'phui-calendar-list-css' => 'c1d0ca59',
     'phui-calendar-month-css' => 'a92e47d2',
     'phui-crumbs-view-css' => '594d719e',
     'phui-document-view-css' => '0f83a7df',
     'phui-feed-story-css' => 'c9f3a0b5',
     'phui-font-icon-base-css' => '3dad2ae3',
     'phui-fontkit-css' => 'd30f4fa3',
     'phui-form-css' => 'f535f938',
     'phui-form-view-css' => '76f0b086',
     'phui-header-view-css' => '083669db',
     'phui-icon-view-css' => 'd35aa857',
     'phui-image-mask-css' => '5a8b09c8',
     'phui-info-panel-css' => '27ea50a1',
     'phui-info-view-css' => '6c6cc4dd',
     'phui-list-view-css' => '53deb25c',
     'phui-object-box-css' => 'd68ce5dc',
     'phui-object-item-list-view-css' => '9db65899',
     'phui-pinboard-view-css' => '3dd4a269',
     'phui-property-list-view-css' => '51480060',
     'phui-remarkup-preview-css' => '19ad512b',
     'phui-spacing-css' => '042804d6',
     'phui-status-list-view-css' => '888cedb8',
     'phui-tag-view-css' => 'ea469f3a',
     'phui-text-css' => 'cf019f54',
     'phui-timeline-view-css' => 'b0fbc4d7',
     'phui-workboard-view-css' => '8896938c',
     'phui-workpanel-view-css' => 'e495a5cc',
     'phuix-action-list-view' => 'b5c256b8',
     'phuix-action-view' => '6e8cefa4',
     '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' => 'ebab8a70',
     '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' => 'a355d921',
     'sprite-main-header-css' => '28d01b0b',
     'sprite-menu-css' => '9ef76324',
     'sprite-projects-css' => 'b0d9e24f',
     'sprite-tokens-css' => '1706b943',
     'syntax-highlighting-css' => '56c1ba38',
     'tokens-css' => '3d0f239e',
     'unhandled-exception-css' => '37d4f9a2',
   ),
   'requires' => array(
     '029a133d' => array(
       'aphront-dialog-view-css',
     ),
     '03d6ed07' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-vector',
       'javelin-install',
     ),
     '05270951' => array(
       'javelin-util',
       'javelin-magical-init',
     ),
     '065227cc' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-workflow',
     ),
     '07f199d8' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-uri',
       'javelin-mask',
       'phabricator-drag-and-drop-file-upload',
     ),
     '08883e8b' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-util',
       'javelin-dom',
       'javelin-request',
       'phabricator-keyboard-shortcut',
     ),
     '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',
     ),
     '0ec56e1d' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'phuix-dropdown-menu',
     ),
     '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',
     ),
     '14d7a8b8' => array(
       'javelin-behavior',
       'javelin-behavior-device',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-magical-init',
       'javelin-vector',
       'javelin-request',
       'javelin-util',
     ),
     '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',
+    ),
     '2290aeef' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-json',
       'javelin-workflow',
       'javelin-util',
     ),
     '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',
     ),
     '2be71d56' => array(
       'javelin-install',
       'javelin-util',
       'javelin-websocket',
       'javelin-leader',
       'javelin-json',
     ),
     '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',
     ),
     '2e0148bc' => array(
       'javelin-stratcom',
       'javelin-install',
       'javelin-uri',
       'javelin-util',
     ),
     '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',
     ),
     '40b1ff90' => 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',
     ),
     42126667 => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-request',
     ),
     '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',
     ),
     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',
     ),
     '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',
     ),
     '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',
     ),
     '59b251eb' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-vector',
       'javelin-dom',
     ),
     '5b2f5a08' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-vector',
     ),
     '5bc2cb21' => array(
       'javelin-behavior',
       'javelin-stratcom',
     ),
     '5c1c758c' => array(
       'javelin-install',
     ),
     '5c54cbf3' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
     ),
     '5c93c52c' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-vector',
     ),
     '5fefb143' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-workflow',
       'javelin-stratcom',
     ),
     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',
     ),
     '6453c869' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-fx',
     ),
     65936067 => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-util',
       'javelin-vector',
       'differential-inline-comment-editor',
     ),
     '6882e80a' => array(
       'javelin-dom',
     ),
     '6932def3' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-request',
       'javelin-util',
       'phabricator-shaped-request',
     ),
     '69adf288' => array(
       'javelin-install',
     ),
+    '6a049cf7' => array(
+      'javelin-dom',
+      'javelin-util',
+      'javelin-stratcom',
+      'javelin-install',
+      'javelin-request',
+      'javelin-workflow',
+    ),
     '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',
     ),
     '6e2de6f2' => array(
       'multirow-row-manager',
       'javelin-install',
       'javelin-util',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-json',
       'phabricator-prefab',
     ),
     '6e8cefa4' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-util',
     ),
     '6eff08aa' => array(
       'javelin-install',
       'javelin-util',
       'javelin-stratcom',
     ),
     '6f7962d5' => array(
       'javelin-magical-init',
       'javelin-install',
       'javelin-util',
       'javelin-vector',
       'javelin-stratcom',
     ),
     '6f7a9da8' => array(
       'javelin-install',
     ),
     '70baed2f' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-vector',
       'javelin-util',
     ),
     '724b1247' => array(
       'javelin-behavior',
       'javelin-typeahead-ondemand-source',
       'javelin-typeahead',
       'javelin-dom',
       'javelin-uri',
       'javelin-util',
       'javelin-stratcom',
       'phabricator-prefab',
     ),
     '72da38cc' => 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',
     ),
     '7319e029' => array(
       'javelin-behavior',
       'javelin-dom',
     ),
     '73d09eef' => array(
       'javelin-behavior',
       'javelin-vector',
       'javelin-dom',
     ),
     '7644823e' => array(
       'javelin-dom',
       'javelin-util',
       'javelin-stratcom',
       'javelin-install',
     ),
     '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',
     ),
     '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',
     ),
     82439934 => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'javelin-stratcom',
       'javelin-workflow',
       'phabricator-draggable-list',
     ),
     '82ce2142' => array(
       'aphront-typeahead-control-css',
     ),
     '834a1173' => array(
       'javelin-behavior',
       'javelin-scrollbar',
     ),
     '84845b5b' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-workflow',
       'phabricator-draggable-list',
     ),
     '84d6aea0' => array(
       'javelin-stratcom',
       'javelin-request',
       'javelin-dom',
       'javelin-vector',
       'javelin-install',
       'javelin-util',
       'javelin-mask',
       'javelin-uri',
       'javelin-routable',
     ),
     '851f167c' => array(
       'javelin-behavior',
       'javelin-aphlict',
       'javelin-stratcom',
       'javelin-request',
       'javelin-uri',
       'javelin-dom',
       'javelin-json',
       'javelin-router',
       'javelin-util',
       'javelin-leader',
       'phabricator-notification',
     ),
     '85ea0626' => array(
       'javelin-install',
     ),
     '869e3445' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-behavior-device',
       'javelin-history',
       'javelin-vector',
       'phabricator-shaped-request',
     ),
     '86df5915' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'javelin-workflow',
       'javelin-stratcom',
     ),
     '87cb6b51' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'javelin-vector',
       'javelin-stratcom',
       'javelin-workflow',
       'phabricator-draggable-list',
     ),
     '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',
     ),
     '8a41885b' => array(
       'javelin-install',
       'javelin-dom',
     ),
     '8b3fd187' => array(
       'javelin-install',
       'javelin-util',
       'javelin-request',
       'javelin-typeahead-source',
     ),
     '8c49f386' => array(
       'javelin-install',
       'javelin-util',
       'javelin-request',
       'javelin-dom',
       'javelin-uri',
       'phabricator-file-upload',
     ),
     '8ce821c5' => array(
       'phabricator-notification',
       'javelin-stratcom',
       'javelin-behavior',
     ),
     '8ef9ab58' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
     ),
     '8fc1c918' => array(
       'javelin-behavior',
       'javelin-uri',
       'phabricator-notification',
     ),
     '92eb531d' => array(
       'javelin-behavior',
       'javelin-dom',
       'phabricator-drag-and-drop-file-upload',
       'phabricator-textareautils',
     ),
     '9414ff18' => array(
       'javelin-behavior',
       'javelin-resource',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-util',
     ),
     '94b750d2' => array(
       'javelin-install',
       'javelin-stratcom',
       'javelin-util',
       'javelin-behavior',
       'javelin-json',
       'javelin-dom',
       'javelin-resource',
       'javelin-routable',
     ),
-    '954d2de0' => array(
-      'javelin-behavior',
-      'javelin-dom',
-      'javelin-workflow',
-      'javelin-util',
-      'javelin-stratcom',
-    ),
     '988040b4' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-reactor-dom',
     ),
     '9c2623f4' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-workflow',
       'phabricator-phtize',
       'phabricator-drag-and-drop-file-upload',
       'phabricator-draggable-list',
     ),
     '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',
     ),
     'a4ae61bf' => array(
       'javelin-install',
       'javelin-dom',
       'phabricator-notification',
     ),
     'a80d0378' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
     ),
     'a88a26cc' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-scrollbar',
       'javelin-quicksand',
       'phabricator-keyboard-shortcut',
       'javelin-behavior-conpherence-widget-pane',
     ),
     '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',
     ),
     'b1f0ccee' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-reactor-dom',
     ),
     'b2b4fbaf' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-uri',
       'javelin-request',
     ),
     'b2c03e60' => array(
       'javelin-behavior',
       'javelin-dom',
       'phortune-credit-card-form',
     ),
     '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',
     ),
     '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',
     ),
     'bdb3e4d0' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'phabricator-tooltip',
       'changeset-view-manager',
     ),
     'be807912' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'phabricator-shaped-request',
     ),
-    'c024db3d' => array(
-      'javelin-dom',
-      'javelin-util',
-      'javelin-stratcom',
-      'javelin-install',
-      'javelin-workflow',
-      'javelin-router',
-      'javelin-behavior-device',
-      'javelin-vector',
-    ),
     'c1700f6f' => array(
       'javelin-install',
       'javelin-util',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-vector',
     ),
     'c51ae228' => array(
       'javelin-behavior',
       'javelin-util',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-vector',
     ),
+    'c662904a' => array(
+      'javelin-behavior',
+      'javelin-dom',
+      'javelin-workflow',
+      'javelin-util',
+      'javelin-stratcom',
+      'changeset-view-manager',
+    ),
     'c90a04fc' => array(
       'javelin-dom',
       'javelin-dynval',
       'javelin-reactor',
       'javelin-reactornode',
       'javelin-install',
       'javelin-util',
     ),
     'ca3f91eb' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'phabricator-phtize',
     ),
     'cc2d9c80' => array(
       'javelin-behavior',
       'javelin-request',
       'javelin-stratcom',
       'javelin-vector',
       'javelin-dom',
       'javelin-uri',
       'javelin-behavior-device',
       'phabricator-title',
     ),
     'd19198c8' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-util',
       'javelin-dynval',
       'javelin-reactor-dom',
     ),
     'd254d646' => array(
       'javelin-util',
     ),
     'd4a14807' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-view',
     ),
     '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',
     ),
     'e10f8e18' => array(
       'javelin-behavior',
       'javelin-dom',
       'phabricator-prefab',
     ),
     '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',
     ),
-    'e33d4bc5' => 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',
-    ),
     'e379b58e' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-vector',
       'javelin-dom',
       'javelin-uri',
     ),
     'e4cc26b3' => array(
       'javelin-behavior',
       'javelin-dom',
     ),
     'e566f52c' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
     ),
     'e5822781' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-json',
       'javelin-workflow',
       'javelin-magical-init',
     ),
     'e58bf807' => 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',
     ),
     'e9581f08' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-dom',
       'phabricator-draggable-list',
     ),
     'ea681761' => array(
       'javelin-behavior',
       'javelin-aphlict',
       'phabricator-phtize',
       'javelin-dom',
     ),
     'efe49472' => array(
       'javelin-install',
       'javelin-util',
     ),
-    'f2441746' => array(
-      'javelin-dom',
-      'javelin-util',
-      'javelin-stratcom',
-      'javelin-install',
-      'javelin-request',
-      'javelin-workflow',
-    ),
     'f24f3253' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'phabricator-prefab',
       'multirow-row-manager',
       'javelin-json',
     ),
     'f36e01af' => array(
       'javelin-behavior',
       'javelin-behavior-device',
       'javelin-stratcom',
       'javelin-vector',
       'phabricator-hovercard',
     ),
     'f3fef818' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'phuix-dropdown-menu',
       'phuix-action-list-view',
       'phuix-action-view',
       'javelin-workflow',
     ),
     'f6555212' => array(
       'javelin-install',
       'javelin-reactornode',
       'javelin-util',
       'javelin-reactor',
     ),
     'f7379f45' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'phabricator-shaped-request',
     ),
     'f7f1289f' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
     ),
     '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',
     ),
     'f960d43d' => array(
       'javelin-install',
     ),
     'fa0f4fc2' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-vector',
       'javelin-magical-init',
     ),
     'fc91ab6c' => array(
       'javelin-behavior',
       'javelin-dom',
       'phortune-credit-card-form',
     ),
+    'fce415a0' => array(
+      'javelin-dom',
+      'javelin-util',
+      'javelin-stratcom',
+      'javelin-install',
+      'javelin-workflow',
+      'javelin-router',
+      'javelin-behavior-device',
+      'javelin-vector',
+    ),
     'fe287620' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-view-visitor',
       'javelin-util',
     ),
     'fe9a552f' => array(
       'javelin-behavior',
       'multirow-row-manager',
       'javelin-dom',
       'javelin-util',
       'phabricator-prefab',
       'javelin-json',
     ),
   ),
   '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',
       'phabricator-transaction-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',
     ),
     '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-konami',
       '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',
     ),
     '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',
     ),
     '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-show-more',
       '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',
     ),
     '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/src/applications/differential/controller/DifferentialChangesetViewController.php b/src/applications/differential/controller/DifferentialChangesetViewController.php
index 33597e002..59691aa40 100644
--- a/src/applications/differential/controller/DifferentialChangesetViewController.php
+++ b/src/applications/differential/controller/DifferentialChangesetViewController.php
@@ -1,388 +1,382 @@
 <?php
 
 final class DifferentialChangesetViewController extends DifferentialController {
 
   public function shouldAllowPublic() {
     return true;
   }
 
   public function processRequest() {
     $request = $this->getRequest();
 
     $author_phid = $request->getUser()->getPHID();
 
     $rendering_reference = $request->getStr('ref');
     $parts = explode('/', $rendering_reference);
     if (count($parts) == 2) {
       list($id, $vs) = $parts;
     } else {
       $id = $parts[0];
       $vs = 0;
     }
 
     $id = (int)$id;
     $vs = (int)$vs;
 
     $load_ids = array($id);
     if ($vs && ($vs != -1)) {
       $load_ids[] = $vs;
     }
 
     $changesets = id(new DifferentialChangesetQuery())
       ->setViewer($request->getUser())
       ->withIDs($load_ids)
       ->needHunks(true)
       ->execute();
     $changesets = mpull($changesets, null, 'getID');
 
     $changeset = idx($changesets, $id);
     if (!$changeset) {
       return new Aphront404Response();
     }
 
     $vs_changeset = null;
     if ($vs && ($vs != -1)) {
       $vs_changeset = idx($changesets, $vs);
       if (!$vs_changeset) {
         return new Aphront404Response();
       }
     }
 
     $view = $request->getStr('view');
     if ($view) {
       $phid = idx($changeset->getMetadata(), "$view:binary-phid");
       if ($phid) {
         return id(new AphrontRedirectResponse())->setURI("/file/info/$phid/");
       }
       switch ($view) {
         case 'new':
           return $this->buildRawFileResponse($changeset, $is_new = true);
         case 'old':
           if ($vs_changeset) {
             return $this->buildRawFileResponse($vs_changeset, $is_new = true);
           }
           return $this->buildRawFileResponse($changeset, $is_new = false);
         default:
           return new Aphront400Response();
       }
     }
 
     if (!$vs) {
       $right = $changeset;
       $left  = null;
 
       $right_source = $right->getID();
       $right_new = true;
       $left_source = $right->getID();
       $left_new = false;
 
       $render_cache_key = $right->getID();
     } else if ($vs == -1) {
       $right = null;
       $left = $changeset;
 
       $right_source = $left->getID();
       $right_new = false;
       $left_source = $left->getID();
       $left_new = true;
 
       $render_cache_key = null;
     } else {
       $right = $changeset;
       $left = $vs_changeset;
 
       $right_source = $right->getID();
       $right_new = true;
       $left_source = $left->getID();
       $left_new = true;
 
       $render_cache_key = null;
     }
 
     if ($left) {
       $left_data = $left->makeNewFile();
       if ($right) {
         $right_data = $right->makeNewFile();
       } else {
         $right_data = $left->makeOldFile();
       }
 
       $engine = new PhabricatorDifferenceEngine();
       $synthetic = $engine->generateChangesetFromFileContent(
         $left_data,
         $right_data);
 
       $choice = clone nonempty($left, $right);
       $choice->attachHunks($synthetic->getHunks());
 
       $changeset = $choice;
     }
 
     $coverage = null;
     if ($right && $right->getDiffID()) {
       $unit = id(new DifferentialDiffProperty())->loadOneWhere(
         'diffID = %d AND name = %s',
         $right->getDiffID(),
         'arc:unit');
 
       if ($unit) {
         $coverage = array();
         foreach ($unit->getData() as $result) {
           $result_coverage = idx($result, 'coverage');
           if (!$result_coverage) {
             continue;
           }
           $file_coverage = idx($result_coverage, $right->getFileName());
           if (!$file_coverage) {
             continue;
           }
           $coverage[] = $file_coverage;
         }
 
         $coverage = ArcanistUnitTestResult::mergeCoverage($coverage);
       }
     }
 
     $spec = $request->getStr('range');
     list($range_s, $range_e, $mask) =
       DifferentialChangesetParser::parseRangeSpecification($spec);
 
     $parser = new DifferentialChangesetParser();
     $parser->setCoverage($coverage);
     $parser->setChangeset($changeset);
     $parser->setRenderingReference($rendering_reference);
     $parser->setRenderCacheKey($render_cache_key);
     $parser->setRightSideCommentMapping($right_source, $right_new);
     $parser->setLeftSideCommentMapping($left_source, $left_new);
-    $parser->setWhitespaceMode($request->getStr('whitespace'));
-    $parser->setCharacterEncoding($request->getStr('encoding'));
-    $parser->setHighlightAs($request->getStr('highlight'));
 
-    if ($request->getStr('renderer') == '1up') {
-      $parser->setRenderer(new DifferentialChangesetOneUpRenderer());
-    }
+    $parser->readParametersFromRequest($request);
 
     if ($left && $right) {
       $parser->setOriginals($left, $right);
     }
 
     // Load both left-side and right-side inline comments.
     $inlines = $this->loadInlineComments(
       array($left_source, $right_source),
       $author_phid);
 
     if ($left_new) {
       $inlines = array_merge(
         $inlines,
         $this->buildLintInlineComments($left));
     }
 
     if ($right_new) {
       $inlines = array_merge(
         $inlines,
         $this->buildLintInlineComments($right));
     }
 
     $phids = array();
     foreach ($inlines as $inline) {
       $parser->parseInlineComment($inline);
       if ($inline->getAuthorPHID()) {
         $phids[$inline->getAuthorPHID()] = true;
       }
     }
     $phids = array_keys($phids);
 
     $handles = $this->loadViewerHandles($phids);
     $parser->setHandles($handles);
 
     $engine = new PhabricatorMarkupEngine();
     $engine->setViewer($request->getUser());
 
     foreach ($inlines as $inline) {
       $engine->addObject(
         $inline,
         PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY);
     }
 
     $engine->process();
     $parser->setMarkupEngine($engine);
     $parser->setUser($request->getUser());
 
     if ($request->isAjax()) {
       $parser->setShowEditAndReplyLinks(true);
     } else {
       $parser->setShowEditAndReplyLinks(false);
     }
 
     $output = $parser->render($range_s, $range_e, $mask);
 
     $mcov = $parser->renderModifiedCoverage();
 
     if ($request->isAjax()) {
       $coverage = array(
         'differential-mcoverage-'.md5($changeset->getFilename()) => $mcov,
       );
 
       return id(new PhabricatorChangesetResponse())
         ->setRenderedChangeset($output)
         ->setCoverage($coverage);
     }
 
     // TODO: [HTML] Clean up DifferentialChangesetParser output, but it's
     // undergoing like six kinds of refactoring anyway.
     $output = phutil_safe_html($output);
 
     $detail = id(new DifferentialChangesetDetailView())
       ->setUser($this->getViewer())
       ->setChangeset($changeset)
+      ->setRenderingRef($rendering_reference)
+      ->setRenderURI('/differential/changeset/')
+      ->setRenderer($parser->getRenderer()->getRendererKey())
       ->appendChild($output)
       ->setVsChangesetID($left_source);
 
     Javelin::initBehavior('differential-populate', array(
       'changesetViewIDs' => array($detail->getID()),
     ));
 
-    Javelin::initBehavior('differential-show-more', array(
-      'uri' => '/differential/changeset/',
-      'whitespace' => $request->getStr('whitespace'),
-    ));
-
+    Javelin::initBehavior('differential-show-more');
     Javelin::initBehavior('differential-comment-jump', array());
 
     $panel = new DifferentialPrimaryPaneView();
     $panel->appendChild(
       phutil_tag(
       'div',
       array(
         'class' => 'differential-review-stage',
         'id'    => 'differential-review-stage',
       ),
       $detail->render()));
 
     $crumbs = $this->buildApplicationCrumbs();
 
     $revision_id = $changeset->getDiff()->getRevisionID();
     if ($revision_id) {
       $crumbs->addTextCrumb('D'.$revision_id, '/D'.$revision_id);
     }
 
     $diff_id = $changeset->getDiff()->getID();
     if ($diff_id) {
       $crumbs->addTextCrumb(
         pht('Diff %d', $diff_id),
         $this->getApplicationURI('diff/'.$diff_id));
     }
 
     $crumbs->addTextCrumb($changeset->getDisplayFilename());
 
     $box = id(new PHUIObjectBoxView())
       ->setHeaderText(pht('Standalone View'))
       ->appendChild($panel);
 
     return $this->buildApplicationPage(
       array(
         $crumbs,
         $box,
       ),
       array(
         'title' => pht('Changeset View'),
         'device' => false,
       ));
   }
 
   private function loadInlineComments(array $changeset_ids, $author_phid) {
     $changeset_ids = array_unique(array_filter($changeset_ids));
     if (!$changeset_ids) {
       return;
     }
 
     return id(new DifferentialInlineCommentQuery())
       ->withViewerAndChangesetIDs($author_phid, $changeset_ids)
       ->execute();
   }
 
   private function buildRawFileResponse(
     DifferentialChangeset $changeset,
     $is_new) {
 
     $viewer = $this->getRequest()->getUser();
 
     if ($is_new) {
       $key = 'raw:new:phid';
     } else {
       $key = 'raw:old:phid';
     }
 
     $metadata = $changeset->getMetadata();
 
     $file = null;
     $phid = idx($metadata, $key);
     if ($phid) {
       $file = id(new PhabricatorFileQuery())
         ->setViewer($viewer)
         ->withPHIDs(array($phid))
         ->execute();
       if ($file) {
         $file = head($file);
       }
     }
 
     if (!$file) {
       // This is just building a cache of the changeset content in the file
       // tool, and is safe to run on a read pathway.
       $unguard = AphrontWriteGuard::beginScopedUnguardedWrites();
 
       if ($is_new) {
         $data = $changeset->makeNewFile();
       } else {
         $data = $changeset->makeOldFile();
       }
 
       $file = PhabricatorFile::newFromFileData(
         $data,
         array(
           'name'      => $changeset->getFilename(),
           'mime-type' => 'text/plain',
         ));
 
       $metadata[$key] = $file->getPHID();
       $changeset->setMetadata($metadata);
       $changeset->save();
 
       unset($unguard);
     }
 
     return $file->getRedirectResponse();
   }
 
   private function buildLintInlineComments($changeset) {
     $lint = id(new DifferentialDiffProperty())->loadOneWhere(
       'diffID = %d AND name = %s',
       $changeset->getDiffID(),
       'arc:lint');
     if (!$lint) {
       return array();
     }
     $lint = $lint->getData();
 
     $inlines = array();
     foreach ($lint as $msg) {
       if ($msg['path'] != $changeset->getFilename()) {
         continue;
       }
       $inline = new DifferentialInlineComment();
       $inline->setChangesetID($changeset->getID());
       $inline->setIsNewFile(1);
       $inline->setSyntheticAuthor('Lint: '.$msg['name']);
       $inline->setLineNumber($msg['line']);
       $inline->setLineLength(0);
 
       $inline->setContent('%%%'.$msg['description'].'%%%');
 
       $inlines[] = $inline;
     }
 
     return $inlines;
   }
 
 }
diff --git a/src/applications/differential/parser/DifferentialChangesetParser.php b/src/applications/differential/parser/DifferentialChangesetParser.php
index 7d979bedd..cd689153f 100644
--- a/src/applications/differential/parser/DifferentialChangesetParser.php
+++ b/src/applications/differential/parser/DifferentialChangesetParser.php
@@ -1,1308 +1,1346 @@
 <?php
 
 final class DifferentialChangesetParser {
 
   protected $visible      = array();
   protected $new          = array();
   protected $old          = array();
   protected $intra        = array();
   protected $newRender    = null;
   protected $oldRender    = null;
 
   protected $filename     = null;
   protected $hunkStartLines = array();
 
   protected $comments     = array();
   protected $specialAttributes = array();
 
   protected $changeset;
   protected $whitespaceMode = null;
 
   protected $renderCacheKey = null;
 
   private $handles = array();
   private $user;
 
   private $leftSideChangesetID;
   private $leftSideAttachesToNewFile;
 
   private $rightSideChangesetID;
   private $rightSideAttachesToNewFile;
 
   private $originalLeft;
   private $originalRight;
 
   private $renderingReference;
   private $isSubparser;
 
   private $isTopLevel;
   private $coverage;
   private $markupEngine;
   private $highlightErrors;
   private $disableCache;
   private $renderer;
   private $characterEncoding;
   private $highlightAs;
   private $showEditAndReplyLinks = true;
 
   public function setShowEditAndReplyLinks($bool) {
     $this->showEditAndReplyLinks = $bool;
     return $this;
   }
   public function getShowEditAndReplyLinks() {
     return $this->showEditAndReplyLinks;
   }
 
   public function setHighlightAs($highlight_as) {
     $this->highlightAs = $highlight_as;
     return $this;
   }
 
   public function getHighlightAs() {
     return $this->highlightAs;
   }
 
   public function setCharacterEncoding($character_encoding) {
     $this->characterEncoding = $character_encoding;
     return $this;
   }
 
   public function getCharacterEncoding() {
     return $this->characterEncoding;
   }
 
   public function setRenderer(DifferentialChangesetRenderer $renderer) {
     $this->renderer = $renderer;
     return $this;
   }
 
   public function getRenderer() {
     if (!$this->renderer) {
       return new DifferentialChangesetTwoUpRenderer();
     }
     return $this->renderer;
   }
 
   public function setDisableCache($disable_cache) {
     $this->disableCache = $disable_cache;
     return $this;
   }
 
   public function getDisableCache() {
     return $this->disableCache;
   }
 
+  public static function getDefaultRendererForViewer(PhabricatorUser $viewer) {
+    $prefs = $viewer->loadPreferences();
+    $pref_unified = PhabricatorUserPreferences::PREFERENCE_DIFF_UNIFIED;
+    if ($prefs->getPreference($pref_unified) == 'unified') {
+      return '1up';
+    }
+    return null;
+  }
+
+  public function readParametersFromRequest(AphrontRequest $request) {
+    $this->setWhitespaceMode($request->getStr('whitespace'));
+    $this->setCharacterEncoding($request->getStr('encoding'));
+    $this->setHighlightAs($request->getStr('highlight'));
+
+    $renderer = null;
+
+    // If the viewer prefers unified diffs, always set the renderer to unified.
+    // Otherwise, we leave it unspecified and the client will choose a
+    // renderer based on the screen size.
+
+    if ($request->getStr('renderer')) {
+      $renderer = $request->getStr('renderer');
+    } else {
+      $renderer = self::getDefaultRendererForViewer($request->getViewer());
+    }
+
+    switch ($renderer) {
+      case '1up':
+        $this->setRenderer(new DifferentialChangesetOneUpRenderer());
+        break;
+      default:
+        $this->setRenderer(new DifferentialChangesetTwoUpRenderer());
+        break;
+    }
+
+    return $this;
+  }
+
   const CACHE_VERSION = 11;
   const CACHE_MAX_SIZE = 8e6;
 
   const ATTR_GENERATED  = 'attr:generated';
   const ATTR_DELETED    = 'attr:deleted';
   const ATTR_UNCHANGED  = 'attr:unchanged';
   const ATTR_WHITELINES = 'attr:white';
   const ATTR_MOVEAWAY   = 'attr:moveaway';
 
   const LINES_CONTEXT = 8;
 
   const WHITESPACE_SHOW_ALL         = 'show-all';
   const WHITESPACE_IGNORE_TRAILING  = 'ignore-trailing';
   const WHITESPACE_IGNORE_MOST      = 'ignore-most';
   const WHITESPACE_IGNORE_ALL       = 'ignore-all';
 
   public function setOldLines(array $lines) {
     $this->old = $lines;
     return $this;
   }
 
   public function setNewLines(array $lines) {
     $this->new = $lines;
     return $this;
   }
 
   public function setSpecialAttributes(array $attributes) {
     $this->specialAttributes = $attributes;
     return $this;
   }
 
   public function setIntraLineDiffs(array $diffs) {
     $this->intra = $diffs;
     return $this;
   }
 
   public function setVisibileLinesMask(array $mask) {
     $this->visible = $mask;
     return $this;
   }
 
   /**
    * Configure which Changeset comments added to the right side of the visible
    * diff will be attached to. The ID must be the ID of a real Differential
    * Changeset.
    *
    * The complexity here is that we may show an arbitrary side of an arbitrary
    * changeset as either the left or right part of a diff. This method allows
    * the left and right halves of the displayed diff to be correctly mapped to
    * storage changesets.
    *
    * @param id    The Differential Changeset ID that comments added to the right
    *              side of the visible diff should be attached to.
    * @param bool  If true, attach new comments to the right side of the storage
    *              changeset. Note that this may be false, if the left side of
    *              some storage changeset is being shown as the right side of
    *              a display diff.
    * @return this
    */
   public function setRightSideCommentMapping($id, $is_new) {
     $this->rightSideChangesetID = $id;
     $this->rightSideAttachesToNewFile = $is_new;
     return $this;
   }
 
   /**
    * See setRightSideCommentMapping(), but this sets information for the left
    * side of the display diff.
    */
   public function setLeftSideCommentMapping($id, $is_new) {
     $this->leftSideChangesetID = $id;
     $this->leftSideAttachesToNewFile = $is_new;
     return $this;
   }
 
   public function setOriginals(
     DifferentialChangeset $left,
     DifferentialChangeset $right) {
 
     $this->originalLeft = $left;
     $this->originalRight = $right;
   }
 
   public function diffOriginals() {
     $engine = new PhabricatorDifferenceEngine();
     $changeset = $engine->generateChangesetFromFileContent(
       implode('', mpull($this->originalLeft->getHunks(), 'getChanges')),
       implode('', mpull($this->originalRight->getHunks(), 'getChanges')));
 
     $parser = new DifferentialHunkParser();
 
     return $parser->parseHunksForHighlightMasks(
       $changeset->getHunks(),
       $this->originalLeft->getHunks(),
       $this->originalRight->getHunks());
   }
 
   /**
    * Set a key for identifying this changeset in the render cache. If set, the
    * parser will attempt to use the changeset render cache, which can improve
    * performance for frequently-viewed changesets.
    *
    * By default, there is no render cache key and parsers do not use the cache.
    * This is appropriate for rarely-viewed changesets.
    *
    * NOTE: Currently, this key must be a valid Differential Changeset ID.
    *
    * @param   string  Key for identifying this changeset in the render cache.
    * @return  this
    */
   public function setRenderCacheKey($key) {
     $this->renderCacheKey = $key;
     return $this;
   }
 
   private function getRenderCacheKey() {
     return $this->renderCacheKey;
   }
 
   public function setChangeset(DifferentialChangeset $changeset) {
     $this->changeset = $changeset;
 
     $this->setFilename($changeset->getFilename());
 
     return $this;
   }
 
   public function setWhitespaceMode($whitespace_mode) {
     $this->whitespaceMode = $whitespace_mode;
     return $this;
   }
 
   public function setRenderingReference($ref) {
     $this->renderingReference = $ref;
     return $this;
   }
 
   private function getRenderingReference() {
     return $this->renderingReference;
   }
 
   public function getChangeset() {
     return $this->changeset;
   }
 
   public function setFilename($filename) {
     $this->filename = $filename;
     return $this;
   }
 
   public function setHandles(array $handles) {
     assert_instances_of($handles, 'PhabricatorObjectHandle');
     $this->handles = $handles;
     return $this;
   }
 
   public function setMarkupEngine(PhabricatorMarkupEngine $engine) {
     $this->markupEngine = $engine;
     return $this;
   }
 
   public function setUser(PhabricatorUser $user) {
     $this->user = $user;
     return $this;
   }
 
   public function getUser() {
     return $this->user;
   }
 
   public function setCoverage($coverage) {
     $this->coverage = $coverage;
     return $this;
   }
   private function getCoverage() {
     return $this->coverage;
   }
 
   public function parseInlineComment(
     PhabricatorInlineCommentInterface $comment) {
 
     // Parse only comments which are actually visible.
     if ($this->isCommentVisibleOnRenderedDiff($comment)) {
       $this->comments[] = $comment;
     }
     return $this;
   }
 
   private function loadCache() {
     $render_cache_key = $this->getRenderCacheKey();
     if (!$render_cache_key) {
       return false;
     }
 
     $data = null;
 
     $changeset = new DifferentialChangeset();
     $conn_r = $changeset->establishConnection('r');
     $data = queryfx_one(
       $conn_r,
       'SELECT * FROM %T WHERE id = %d',
       $changeset->getTableName().'_parse_cache',
       $render_cache_key);
 
     if (!$data) {
       return false;
     }
 
     if ($data['cache'][0] == '{') {
       // This is likely an old-style JSON cache which we will not be able to
       // deserialize.
       return false;
     }
 
     $data = unserialize($data['cache']);
     if (!is_array($data) || !$data) {
       return false;
     }
 
     foreach (self::getCacheableProperties() as $cache_key) {
       if (!array_key_exists($cache_key, $data)) {
         // If we're missing a cache key, assume we're looking at an old cache
         // and ignore it.
         return false;
       }
     }
 
     if ($data['cacheVersion'] !== self::CACHE_VERSION) {
       return false;
     }
 
     // Someone displays contents of a partially cached shielded file.
     if (!isset($data['newRender']) && (!$this->isTopLevel || $this->comments)) {
       return false;
     }
 
     unset($data['cacheVersion'], $data['cacheHost']);
     $cache_prop = array_select_keys($data, self::getCacheableProperties());
     foreach ($cache_prop as $cache_key => $v) {
       $this->$cache_key = $v;
     }
 
     return true;
   }
 
   protected static function getCacheableProperties() {
     return array(
       'visible',
       'new',
       'old',
       'intra',
       'newRender',
       'oldRender',
       'specialAttributes',
       'hunkStartLines',
       'cacheVersion',
       'cacheHost',
     );
   }
 
   public function saveCache() {
     if ($this->highlightErrors) {
       return false;
     }
 
     $render_cache_key = $this->getRenderCacheKey();
     if (!$render_cache_key) {
       return false;
     }
 
     $cache = array();
     foreach (self::getCacheableProperties() as $cache_key) {
       switch ($cache_key) {
         case 'cacheVersion':
           $cache[$cache_key] = self::CACHE_VERSION;
           break;
         case 'cacheHost':
           $cache[$cache_key] = php_uname('n');
           break;
         default:
           $cache[$cache_key] = $this->$cache_key;
           break;
       }
     }
     $cache = serialize($cache);
 
     // We don't want to waste too much space by a single changeset.
     if (strlen($cache) > self::CACHE_MAX_SIZE) {
       return;
     }
 
     $changeset = new DifferentialChangeset();
     $conn_w = $changeset->establishConnection('w');
 
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
       try {
         queryfx(
           $conn_w,
           'INSERT INTO %T (id, cache, dateCreated) VALUES (%d, %B, %d)
             ON DUPLICATE KEY UPDATE cache = VALUES(cache)',
           DifferentialChangeset::TABLE_CACHE,
           $render_cache_key,
           $cache,
           time());
       } catch (AphrontQueryException $ex) {
         // Ignore these exceptions. A common cause is that the cache is
         // larger than 'max_allowed_packet', in which case we're better off
         // not writing it.
 
         // TODO: It would be nice to tailor this more narrowly.
       }
     unset($unguarded);
   }
 
   private function markGenerated($new_corpus_block = '') {
     $generated_guess = (strpos($new_corpus_block, '@'.'generated') !== false);
 
     if (!$generated_guess) {
       $generated_path_regexps = PhabricatorEnv::getEnvConfig(
         'differential.generated-paths');
       foreach ($generated_path_regexps as $regexp) {
         if (preg_match($regexp, $this->changeset->getFilename())) {
           $generated_guess = true;
           break;
         }
       }
     }
 
     $event = new PhabricatorEvent(
       PhabricatorEventType::TYPE_DIFFERENTIAL_WILLMARKGENERATED,
       array(
         'corpus' => $new_corpus_block,
         'is_generated' => $generated_guess,
       )
     );
     PhutilEventEngine::dispatchEvent($event);
 
     $generated = $event->getValue('is_generated');
     $this->specialAttributes[self::ATTR_GENERATED] = $generated;
   }
 
   public function isGenerated() {
     return idx($this->specialAttributes, self::ATTR_GENERATED, false);
   }
 
   public function isDeleted() {
     return idx($this->specialAttributes, self::ATTR_DELETED, false);
   }
 
   public function isUnchanged() {
     return idx($this->specialAttributes, self::ATTR_UNCHANGED, false);
   }
 
   public function isWhitespaceOnly() {
     return idx($this->specialAttributes, self::ATTR_WHITELINES, false);
   }
 
   public function isMoveAway() {
     return idx($this->specialAttributes, self::ATTR_MOVEAWAY, false);
   }
 
   private function applyIntraline(&$render, $intra, $corpus) {
 
     foreach ($render as $key => $text) {
       if (isset($intra[$key])) {
         $render[$key] = ArcanistDiffUtils::applyIntralineDiff(
           $text,
           $intra[$key]);
       }
     }
   }
 
   private function getHighlightFuture($corpus) {
     $language = $this->highlightAs;
 
     if (!$language) {
       $language = $this->highlightEngine->getLanguageFromFilename(
         $this->filename);
     }
 
     return $this->highlightEngine->getHighlightFuture(
       $language,
       $corpus);
   }
 
   protected function processHighlightedSource($data, $result) {
 
     $result_lines = phutil_split_lines($result);
     foreach ($data as $key => $info) {
       if (!$info) {
         unset($result_lines[$key]);
       }
     }
     return $result_lines;
   }
 
   private function tryCacheStuff() {
     $whitespace_mode = $this->whitespaceMode;
     switch ($whitespace_mode) {
       case self::WHITESPACE_SHOW_ALL:
       case self::WHITESPACE_IGNORE_TRAILING:
       case self::WHITESPACE_IGNORE_ALL:
         break;
       default:
         $whitespace_mode = self::WHITESPACE_IGNORE_MOST;
         break;
     }
 
     $skip_cache = ($whitespace_mode != self::WHITESPACE_IGNORE_MOST);
     if ($this->disableCache) {
       $skip_cache = true;
     }
 
     if ($this->characterEncoding) {
       $skip_cache = true;
     }
 
     if ($this->highlightAs) {
       $skip_cache = true;
     }
 
     $this->whitespaceMode = $whitespace_mode;
 
     $changeset = $this->changeset;
 
     if ($changeset->getFileType() != DifferentialChangeType::FILE_TEXT &&
         $changeset->getFileType() != DifferentialChangeType::FILE_SYMLINK) {
 
       $this->markGenerated();
 
     } else {
       if ($skip_cache || !$this->loadCache()) {
         $this->process();
         if (!$skip_cache) {
           $this->saveCache();
         }
       }
     }
   }
 
   private function process() {
     $whitespace_mode = $this->whitespaceMode;
     $changeset = $this->changeset;
 
     $ignore_all = (($whitespace_mode == self::WHITESPACE_IGNORE_MOST) ||
                   ($whitespace_mode == self::WHITESPACE_IGNORE_ALL));
 
     $force_ignore = ($whitespace_mode == self::WHITESPACE_IGNORE_ALL);
 
     if (!$force_ignore) {
       if ($ignore_all && $changeset->getWhitespaceMatters()) {
         $ignore_all = false;
       }
     }
 
     // The "ignore all whitespace" algorithm depends on rediffing the
     // files, and we currently need complete representations of both
     // files to do anything reasonable. If we only have parts of the files,
     // don't use the "ignore all" algorithm.
     if ($ignore_all) {
       $hunks = $changeset->getHunks();
       if (count($hunks) !== 1) {
         $ignore_all = false;
       } else {
         $first_hunk = reset($hunks);
         if ($first_hunk->getOldOffset() != 1 ||
             $first_hunk->getNewOffset() != 1) {
             $ignore_all = false;
         }
       }
     }
 
     if ($ignore_all) {
       $old_file = $changeset->makeOldFile();
       $new_file = $changeset->makeNewFile();
       if ($old_file == $new_file) {
         // If the old and new files are exactly identical, the synthetic
         // diff below will give us nonsense and whitespace modes are
         // irrelevant anyway. This occurs when you, e.g., copy a file onto
         // itself in Subversion (see T271).
         $ignore_all = false;
       }
     }
 
     $hunk_parser = new DifferentialHunkParser();
     $hunk_parser->setWhitespaceMode($whitespace_mode);
     $hunk_parser->parseHunksForLineData($changeset->getHunks());
 
     // Depending on the whitespace mode, we may need to compute a different
     // set of changes than the set of changes in the hunk data (specificaly,
     // we might want to consider changed lines which have only whitespace
     // changes as unchanged).
     if ($ignore_all) {
       $engine = new PhabricatorDifferenceEngine();
       $engine->setIgnoreWhitespace(true);
       $no_whitespace_changeset = $engine->generateChangesetFromFileContent(
         $old_file,
         $new_file);
 
       $type_parser = new DifferentialHunkParser();
       $type_parser->parseHunksForLineData($no_whitespace_changeset->getHunks());
 
       $hunk_parser->setOldLineTypeMap($type_parser->getOldLineTypeMap());
       $hunk_parser->setNewLineTypeMap($type_parser->getNewLineTypeMap());
     }
 
     $hunk_parser->reparseHunksForSpecialAttributes();
 
     $unchanged = false;
     if (!$hunk_parser->getHasAnyChanges()) {
       $filetype = $this->changeset->getFileType();
       if ($filetype == DifferentialChangeType::FILE_TEXT ||
           $filetype == DifferentialChangeType::FILE_SYMLINK) {
         $unchanged = true;
       }
     }
 
     $moveaway = false;
     $changetype = $this->changeset->getChangeType();
     if ($changetype == DifferentialChangeType::TYPE_MOVE_AWAY) {
       $moveaway = true;
     }
 
     $this->setSpecialAttributes(array(
       self::ATTR_UNCHANGED  => $unchanged,
       self::ATTR_DELETED    => $hunk_parser->getIsDeleted(),
       self::ATTR_WHITELINES => !$hunk_parser->getHasTextChanges(),
       self::ATTR_MOVEAWAY   => $moveaway,
     ));
 
     $hunk_parser->generateIntraLineDiffs();
     $hunk_parser->generateVisibileLinesMask();
 
     $this->setOldLines($hunk_parser->getOldLines());
     $this->setNewLines($hunk_parser->getNewLines());
     $this->setIntraLineDiffs($hunk_parser->getIntraLineDiffs());
     $this->setVisibileLinesMask($hunk_parser->getVisibleLinesMask());
     $this->hunkStartLines = $hunk_parser->getHunkStartLines(
       $changeset->getHunks());
 
     $new_corpus = $hunk_parser->getNewCorpus();
     $new_corpus_block = implode('', $new_corpus);
     $this->markGenerated($new_corpus_block);
 
     if ($this->isTopLevel &&
         !$this->comments &&
           ($this->isGenerated() ||
            $this->isUnchanged() ||
            $this->isDeleted())) {
       return;
     }
 
     $old_corpus = $hunk_parser->getOldCorpus();
     $old_corpus_block = implode('', $old_corpus);
     $old_future = $this->getHighlightFuture($old_corpus_block);
     $new_future = $this->getHighlightFuture($new_corpus_block);
     $futures = array(
       'old' => $old_future,
       'new' => $new_future,
     );
     $corpus_blocks = array(
       'old' => $old_corpus_block,
       'new' => $new_corpus_block,
     );
 
     $this->highlightErrors = false;
     foreach (new FutureIterator($futures) as $key => $future) {
       try {
         try {
           $highlighted = $future->resolve();
         } catch (PhutilSyntaxHighlighterException $ex) {
           $this->highlightErrors = true;
           $highlighted = id(new PhutilDefaultSyntaxHighlighter())
             ->getHighlightFuture($corpus_blocks[$key])
             ->resolve();
         }
         switch ($key) {
         case 'old':
           $this->oldRender = $this->processHighlightedSource(
             $this->old,
             $highlighted);
           break;
         case 'new':
           $this->newRender = $this->processHighlightedSource(
             $this->new,
             $highlighted);
           break;
         }
       } catch (Exception $ex) {
         phlog($ex);
         throw $ex;
       }
     }
 
     $this->applyIntraline(
       $this->oldRender,
       ipull($this->intra, 0),
       $old_corpus);
     $this->applyIntraline(
       $this->newRender,
       ipull($this->intra, 1),
       $new_corpus);
   }
 
   private function shouldRenderPropertyChangeHeader($changeset) {
     if (!$this->isTopLevel) {
       // We render properties only at top level; otherwise we get multiple
       // copies of them when a user clicks "Show More".
       return false;
     }
 
     return true;
   }
 
   public function render(
     $range_start  = null,
     $range_len    = null,
     $mask_force   = array()) {
 
     // "Top level" renders are initial requests for the whole file, versus
     // requests for a specific range generated by clicking "show more". We
     // generate property changes and "shield" UI elements only for toplevel
     // requests.
     $this->isTopLevel = (($range_start === null) && ($range_len === null));
     $this->highlightEngine = PhabricatorSyntaxHighlighter::newEngine();
 
     $encoding = null;
     if ($this->characterEncoding) {
       // We are forcing this changeset to be interpreted with a specific
       // character encoding, so force all the hunks into that encoding and
       // propagate it to the renderer.
       $encoding = $this->characterEncoding;
       foreach ($this->changeset->getHunks() as $hunk) {
         $hunk->forceEncoding($this->characterEncoding);
       }
     } else {
       // We're just using the default, so tell the renderer what that is
       // (by reading the encoding from the first hunk).
       foreach ($this->changeset->getHunks() as $hunk) {
         $encoding = $hunk->getDataEncoding();
         break;
       }
     }
 
     $this->tryCacheStuff();
     $render_pch = $this->shouldRenderPropertyChangeHeader($this->changeset);
 
     $rows = max(
       count($this->old),
       count($this->new));
 
     $renderer = $this->getRenderer()
       ->setUser($this->getUser())
       ->setChangeset($this->changeset)
       ->setRenderPropertyChangeHeader($render_pch)
       ->setIsTopLevel($this->isTopLevel)
       ->setOldRender($this->oldRender)
       ->setNewRender($this->newRender)
       ->setHunkStartLines($this->hunkStartLines)
       ->setOldChangesetID($this->leftSideChangesetID)
       ->setNewChangesetID($this->rightSideChangesetID)
       ->setOldAttachesToNewFile($this->leftSideAttachesToNewFile)
       ->setNewAttachesToNewFile($this->rightSideAttachesToNewFile)
       ->setCodeCoverage($this->getCoverage())
       ->setRenderingReference($this->getRenderingReference())
       ->setMarkupEngine($this->markupEngine)
       ->setHandles($this->handles)
       ->setOldLines($this->old)
       ->setNewLines($this->new)
       ->setOriginalCharacterEncoding($encoding)
       ->setShowEditAndReplyLinks($this->getShowEditAndReplyLinks());
 
     $shield = null;
     if ($this->isTopLevel && !$this->comments) {
       if ($this->isGenerated()) {
         $shield = $renderer->renderShield(
           pht(
             'This file contains generated code, which does not normally '.
             'need to be reviewed.'));
       } else if ($this->isMoveAway()) {
         // We put an empty shield on these files. Normally, they do not have
         // any diff content anyway. However, if they come through `arc`, they
         // may have content. We don't want to show it (it's not useful) and
         // we bailed out of fully processing it earlier anyway.
 
         // We could show a message like "this file was moved", but we show
         // that as a change header anyway, so it would be redundant. Instead,
         // just render an empty shield to skip rendering the diff body.
         $shield = '';
       } else if ($this->isUnchanged()) {
         $type = 'text';
         if (!$rows) {
           // NOTE: Normally, diffs which don't change files do not include
           // file content (for example, if you "chmod +x" a file and then
           // run "git show", the file content is not available). Similarly,
           // if you move a file from A to B without changing it, diffs normally
           // do not show the file content. In some cases `arc` is able to
           // synthetically generate content for these diffs, but for raw diffs
           // we'll never have it so we need to be prepared to not render a link.
           $type = 'none';
         }
         $shield = $renderer->renderShield(
           pht('The contents of this file were not changed.'),
           $type);
       } else if ($this->isWhitespaceOnly()) {
         $shield = $renderer->renderShield(
           pht('This file was changed only by adding or removing whitespace.'),
           'whitespace');
       } else if ($this->isDeleted()) {
         $shield = $renderer->renderShield(
           pht('This file was completely deleted.'));
       } else if ($this->changeset->getAffectedLineCount() > 2500) {
         $lines = number_format($this->changeset->getAffectedLineCount());
         $shield = $renderer->renderShield(
           pht(
             'This file has a very large number of changes (%s lines).',
             $lines));
       }
     }
 
     if ($shield !== null) {
       return $renderer->renderChangesetTable($shield);
     }
 
     $old_comments = array();
     $new_comments = array();
     $old_mask = array();
     $new_mask = array();
     $feedback_mask = array();
 
     if ($this->comments) {
       foreach ($this->comments as $comment) {
         $start = max($comment->getLineNumber() - self::LINES_CONTEXT, 0);
         $end = $comment->getLineNumber() +
           $comment->getLineLength() +
           self::LINES_CONTEXT;
         $new_side = $this->isCommentOnRightSideWhenDisplayed($comment);
         for ($ii = $start; $ii <= $end; $ii++) {
           if ($new_side) {
             $new_mask[$ii] = true;
           } else {
             $old_mask[$ii] = true;
           }
         }
       }
 
       foreach ($this->old as $ii => $old) {
         if (isset($old['line']) && isset($old_mask[$old['line']])) {
           $feedback_mask[$ii] = true;
         }
       }
 
       foreach ($this->new as $ii => $new) {
         if (isset($new['line']) && isset($new_mask[$new['line']])) {
           $feedback_mask[$ii] = true;
         }
       }
       $this->comments = msort($this->comments, 'getID');
       foreach ($this->comments as $comment) {
         $final = $comment->getLineNumber() +
           $comment->getLineLength();
         $final = max(1, $final);
         if ($this->isCommentOnRightSideWhenDisplayed($comment)) {
           $new_comments[$final][] = $comment;
         } else {
           $old_comments[$final][] = $comment;
         }
       }
     }
     $renderer
       ->setOldComments($old_comments)
       ->setNewComments($new_comments);
 
     switch ($this->changeset->getFileType()) {
       case DifferentialChangeType::FILE_IMAGE:
         $old = null;
         $new = null;
         // TODO: Improve the architectural issue as discussed in D955
         // https://secure.phabricator.com/D955
         $reference = $this->getRenderingReference();
         $parts = explode('/', $reference);
         if (count($parts) == 2) {
           list($id, $vs) = $parts;
         } else {
           $id = $parts[0];
           $vs = 0;
         }
         $id = (int)$id;
         $vs = (int)$vs;
 
         if (!$vs) {
           $metadata = $this->changeset->getMetadata();
           $data = idx($metadata, 'attachment-data');
 
           $old_phid = idx($metadata, 'old:binary-phid');
           $new_phid = idx($metadata, 'new:binary-phid');
         } else {
           $vs_changeset = id(new DifferentialChangeset())->load($vs);
           $old_phid = null;
           $new_phid = null;
 
           // TODO: This is spooky, see D6851
           if ($vs_changeset) {
             $vs_metadata = $vs_changeset->getMetadata();
             $old_phid = idx($vs_metadata, 'new:binary-phid');
           }
 
           $changeset = id(new DifferentialChangeset())->load($id);
           if ($changeset) {
             $metadata = $changeset->getMetadata();
             $new_phid = idx($metadata, 'new:binary-phid');
           }
         }
 
         if ($old_phid || $new_phid) {
           // grab the files, (micro) optimization for 1 query not 2
           $file_phids = array();
           if ($old_phid) {
             $file_phids[] = $old_phid;
           }
           if ($new_phid) {
             $file_phids[] = $new_phid;
           }
 
           $files = id(new PhabricatorFileQuery())
             ->setViewer($this->getUser())
             ->withPHIDs($file_phids)
             ->execute();
           foreach ($files as $file) {
             if (empty($file)) {
               continue;
             }
             if ($file->getPHID() == $old_phid) {
               $old = $file;
             } else if ($file->getPHID() == $new_phid) {
               $new = $file;
             }
           }
         }
 
         $renderer->attachOldFile($old);
         $renderer->attachNewFile($new);
 
         return $renderer->renderFileChange($old, $new, $id, $vs);
       case DifferentialChangeType::FILE_DIRECTORY:
       case DifferentialChangeType::FILE_BINARY:
         $output = $renderer->renderChangesetTable(null);
         return $output;
     }
 
     if ($this->originalLeft && $this->originalRight) {
       list($highlight_old, $highlight_new) = $this->diffOriginals();
       $highlight_old = array_flip($highlight_old);
       $highlight_new = array_flip($highlight_new);
       $renderer
         ->setHighlightOld($highlight_old)
         ->setHighlightNew($highlight_new);
     }
     $renderer
       ->setOriginalOld($this->originalLeft)
       ->setOriginalNew($this->originalRight);
 
     if ($range_start === null) {
       $range_start = 0;
     }
     if ($range_len === null) {
       $range_len = $rows;
     }
     $range_len = min($range_len, $rows - $range_start);
 
     list($gaps, $mask, $depths) = $this->calculateGapsMaskAndDepths(
       $mask_force,
       $feedback_mask,
       $range_start,
       $range_len);
 
     $renderer
       ->setGaps($gaps)
       ->setMask($mask)
       ->setDepths($depths);
 
     $html = $renderer->renderTextChange(
       $range_start,
       $range_len,
       $rows);
 
     return $renderer->renderChangesetTable($html);
   }
 
   /**
    * This function calculates a lot of stuff we need to know to display
    * the diff:
    *
    * Gaps - compute gaps in the visible display diff, where we will render
    * "Show more context" spacers. If a gap is smaller than the context size,
    * we just display it. Otherwise, we record it into $gaps and will render a
    * "show more context" element instead of diff text below. A given $gap
    * is a tuple of $gap_line_number_start and $gap_length.
    *
    * Mask - compute the actual lines that need to be shown (because they
    * are near changes lines, near inline comments, or the request has
    * explicitly asked for them, i.e. resulting from the user clicking
    * "show more"). The $mask returned is a sparesely populated dictionary
    * of $visible_line_number => true.
    *
    * Depths - compute how indented any given line is. The $depths returned
    * is a sparesely populated dictionary of $visible_line_number => $depth.
    *
    * This function also has the side effect of modifying member variable
    * new such that tabs are normalized to spaces for each line of the diff.
    *
    * @return array($gaps, $mask, $depths)
    */
   private function calculateGapsMaskAndDepths($mask_force,
                                               $feedback_mask,
                                               $range_start,
                                               $range_len) {
 
     // Calculate gaps and mask first
     $gaps = array();
     $gap_start = 0;
     $in_gap = false;
     $base_mask = $this->visible + $mask_force + $feedback_mask;
     $base_mask[$range_start + $range_len] = true;
     for ($ii = $range_start; $ii <= $range_start + $range_len; $ii++) {
       if (isset($base_mask[$ii])) {
         if ($in_gap) {
           $gap_length = $ii - $gap_start;
           if ($gap_length <= self::LINES_CONTEXT) {
             for ($jj = $gap_start; $jj <= $gap_start + $gap_length; $jj++) {
               $base_mask[$jj] = true;
             }
           } else {
             $gaps[] = array($gap_start, $gap_length);
           }
           $in_gap = false;
         }
       } else {
         if (!$in_gap) {
           $gap_start = $ii;
           $in_gap = true;
         }
       }
     }
     $gaps = array_reverse($gaps);
     $mask = $base_mask;
 
     // Time to calculate depth.
     // We need to go backwards to properly indent whitespace in this code:
     //
     //   0: class C {
     //   1:
     //   1:   function f() {
     //   2:
     //   2:     return;
     //   1:
     //   1:   }
     //   0:
     //   0: }
     //
     $depths = array();
     $last_depth = 0;
     $range_end = $range_start + $range_len;
     if (!isset($this->new[$range_end])) {
       $range_end--;
     }
     for ($ii = $range_end; $ii >= $range_start; $ii--) {
       // We need to expand tabs to process mixed indenting and to round
       // correctly later.
       $line = str_replace("\t", '  ', $this->new[$ii]['text']);
       $trimmed = ltrim($line);
       if ($trimmed != '') {
         // We round down to flatten "/**" and " *".
         $last_depth = floor((strlen($line) - strlen($trimmed)) / 2);
       }
       $depths[$ii] = $last_depth;
     }
 
     return array($gaps, $mask, $depths);
   }
 
   /**
    * Determine if an inline comment will appear on the rendered diff,
    * taking into consideration which halves of which changesets will actually
    * be shown.
    *
    * @param PhabricatorInlineCommentInterface Comment to test for visibility.
    * @return bool True if the comment is visible on the rendered diff.
    */
   private function isCommentVisibleOnRenderedDiff(
     PhabricatorInlineCommentInterface $comment) {
 
     $changeset_id = $comment->getChangesetID();
     $is_new = $comment->getIsNewFile();
 
     if ($changeset_id == $this->rightSideChangesetID &&
         $is_new == $this->rightSideAttachesToNewFile) {
         return true;
     }
 
     if ($changeset_id == $this->leftSideChangesetID &&
         $is_new == $this->leftSideAttachesToNewFile) {
         return true;
     }
 
     return false;
   }
 
 
   /**
    * Determine if a comment will appear on the right side of the display diff.
    * Note that the comment must appear somewhere on the rendered changeset, as
    * per isCommentVisibleOnRenderedDiff().
    *
    * @param PhabricatorInlineCommentInterface Comment to test for display
    *              location.
    * @return bool True for right, false for left.
    */
   private function isCommentOnRightSideWhenDisplayed(
     PhabricatorInlineCommentInterface $comment) {
 
     if (!$this->isCommentVisibleOnRenderedDiff($comment)) {
       throw new Exception('Comment is not visible on changeset!');
     }
 
     $changeset_id = $comment->getChangesetID();
     $is_new = $comment->getIsNewFile();
 
     if ($changeset_id == $this->rightSideChangesetID &&
         $is_new == $this->rightSideAttachesToNewFile) {
       return true;
     }
 
     return false;
   }
 
   /**
    * Parse the 'range' specification that this class and the client-side JS
    * emit to indicate that a user clicked "Show more..." on a diff. Generally,
    * use is something like this:
    *
    *   $spec = $request->getStr('range');
    *   $parsed = DifferentialChangesetParser::parseRangeSpecification($spec);
    *   list($start, $end, $mask) = $parsed;
    *   $parser->render($start, $end, $mask);
    *
    * @param string Range specification, indicating the range of the diff that
    *               should be rendered.
    * @return tuple List of <start, end, mask> suitable for passing to
    *               @{method:render}.
    */
   public static function parseRangeSpecification($spec) {
     $range_s = null;
     $range_e = null;
     $mask = array();
 
     if ($spec) {
       $match = null;
       if (preg_match('@^(\d+)-(\d+)(?:/(\d+)-(\d+))?$@', $spec, $match)) {
         $range_s = (int)$match[1];
         $range_e = (int)$match[2];
         if (count($match) > 3) {
           $start = (int)$match[3];
           $len = (int)$match[4];
           for ($ii = $start; $ii < $start + $len; $ii++) {
             $mask[$ii] = true;
           }
         }
       }
     }
 
     return array($range_s, $range_e, $mask);
   }
 
   /**
    * Render "modified coverage" information; test coverage on modified lines.
    * This synthesizes diff information with unit test information into a useful
    * indicator of how well tested a change is.
    */
   public function renderModifiedCoverage() {
     $na = phutil_tag('em', array(), '-');
 
     $coverage = $this->getCoverage();
     if (!$coverage) {
       return $na;
     }
 
     $covered = 0;
     $not_covered = 0;
 
     foreach ($this->new as $k => $new) {
       if (!$new['line']) {
         continue;
       }
 
       if (!$new['type']) {
         continue;
       }
 
       if (empty($coverage[$new['line'] - 1])) {
         continue;
       }
 
       switch ($coverage[$new['line'] - 1]) {
         case 'C':
           $covered++;
           break;
         case 'U':
           $not_covered++;
           break;
       }
     }
 
     if (!$covered && !$not_covered) {
       return $na;
     }
 
     return sprintf('%d%%', 100 * ($covered / ($covered + $not_covered)));
   }
 
   public function detectCopiedCode(
     array $changesets,
     $min_width = 30,
     $min_lines = 3) {
 
     assert_instances_of($changesets, 'DifferentialChangeset');
 
     $map = array();
     $files = array();
     $types = array();
     foreach ($changesets as $changeset) {
       $file = $changeset->getFilename();
       foreach ($changeset->getHunks() as $hunk) {
         $line = $hunk->getOldOffset();
         foreach (explode("\n", $hunk->getChanges()) as $code) {
           $type = (isset($code[0]) ? $code[0] : '');
           if ($type == '-' || $type == ' ') {
             $code = trim(substr($code, 1));
             $files[$file][$line] = $code;
             $types[$file][$line] = $type;
             if (strlen($code) >= $min_width) {
               $map[$code][] = array($file, $line);
             }
             $line++;
           }
         }
       }
     }
 
     foreach ($changesets as $changeset) {
       $copies = array();
       foreach ($changeset->getHunks() as $hunk) {
         $added = array_map('trim', $hunk->getAddedLines());
         for (reset($added); list($line, $code) = each($added); ) {
           if (isset($map[$code])) { // We found a long matching line.
 
             if (count($map[$code]) > 16) {
               // If there are a large number of identical lines in this diff,
               // don't try to figure out where this block came from: the
               // analysis is O(N^2), since we need to compare every line
               // against every other line. Even if we arrive at a result, it
               // is unlikely to be meaningful. See T5041.
               continue 2;
             }
 
             $best_length = 0;
             foreach ($map[$code] as $val) { // Explore all candidates.
               list($file, $orig_line) = $val;
               $length = 1;
               // Search also backwards for short lines.
               foreach (array(-1, 1) as $direction) {
                 $offset = $direction;
                 while (!isset($copies[$line + $offset]) &&
                     isset($added[$line + $offset]) &&
                     idx($files[$file], $orig_line + $offset) ===
                       $added[$line + $offset]) {
                   $length++;
                   $offset += $direction;
                 }
               }
               if ($length > $best_length ||
                   ($length == $best_length && // Prefer moves.
                    idx($types[$file], $orig_line) == '-')) {
                 $best_length = $length;
                 // ($offset - 1) contains number of forward matching lines.
                 $best_offset = $offset - 1;
                 $best_file = $file;
                 $best_line = $orig_line;
               }
             }
             $file = ($best_file == $changeset->getFilename() ? '' : $best_file);
             for ($i = $best_length; $i--; ) {
               $type = idx($types[$best_file], $best_line + $best_offset - $i);
               $copies[$line + $best_offset - $i] = ($best_length < $min_lines
                 ? array() // Ignore short blocks.
                 : array($file, $best_line + $best_offset - $i, $type));
             }
             for ($i = 0; $i < $best_offset; $i++) {
               next($added);
             }
           }
         }
       }
       $copies = array_filter($copies);
       if ($copies) {
         $metadata = $changeset->getMetadata();
         $metadata['copy:lines'] = $copies;
         $changeset->setMetadata($metadata);
       }
     }
     return $changesets;
   }
 
 }
diff --git a/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php b/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php
index b4ef3b640..b03497560 100644
--- a/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php
+++ b/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php
@@ -1,530 +1,529 @@
 <?php
 
 abstract class DifferentialChangesetHTMLRenderer
   extends DifferentialChangesetRenderer {
 
   abstract protected function getRendererTableClass();
 
   protected function renderChangeTypeHeader($force) {
     $changeset = $this->getChangeset();
 
     $change = $changeset->getChangeType();
     $file = $changeset->getFileType();
 
     $messages = array();
     switch ($change) {
 
       case DifferentialChangeType::TYPE_ADD:
         switch ($file) {
           case DifferentialChangeType::FILE_TEXT:
             $messages[] = pht('This file was added.');
             break;
           case DifferentialChangeType::FILE_IMAGE:
             $messages[] = pht('This image was added.');
             break;
           case DifferentialChangeType::FILE_DIRECTORY:
             $messages[] = pht('This directory was added.');
             break;
           case DifferentialChangeType::FILE_BINARY:
             $messages[] = pht('This binary file was added.');
             break;
           case DifferentialChangeType::FILE_SYMLINK:
             $messages[] = pht('This symlink was added.');
             break;
           case DifferentialChangeType::FILE_SUBMODULE:
             $messages[] = pht('This submodule was added.');
             break;
         }
         break;
 
       case DifferentialChangeType::TYPE_DELETE:
         switch ($file) {
           case DifferentialChangeType::FILE_TEXT:
             $messages[] = pht('This file was deleted.');
             break;
           case DifferentialChangeType::FILE_IMAGE:
             $messages[] = pht('This image was deleted.');
             break;
           case DifferentialChangeType::FILE_DIRECTORY:
             $messages[] = pht('This directory was deleted.');
             break;
           case DifferentialChangeType::FILE_BINARY:
             $messages[] = pht('This binary file was deleted.');
             break;
           case DifferentialChangeType::FILE_SYMLINK:
             $messages[] = pht('This symlink was deleted.');
             break;
           case DifferentialChangeType::FILE_SUBMODULE:
             $messages[] = pht('This submodule was deleted.');
             break;
         }
         break;
 
       case DifferentialChangeType::TYPE_MOVE_HERE:
         $from = phutil_tag('strong', array(), $changeset->getOldFile());
         switch ($file) {
           case DifferentialChangeType::FILE_TEXT:
             $messages[] = pht('This file was moved from %s.', $from);
             break;
           case DifferentialChangeType::FILE_IMAGE:
             $messages[] = pht('This image was moved from %s.', $from);
             break;
           case DifferentialChangeType::FILE_DIRECTORY:
             $messages[] = pht('This directory was moved from %s.', $from);
             break;
           case DifferentialChangeType::FILE_BINARY:
             $messages[] = pht('This binary file was moved from %s.', $from);
             break;
           case DifferentialChangeType::FILE_SYMLINK:
             $messages[] = pht('This symlink was moved from %s.', $from);
             break;
           case DifferentialChangeType::FILE_SUBMODULE:
             $messages[] = pht('This submodule was moved from %s.', $from);
             break;
         }
         break;
 
       case DifferentialChangeType::TYPE_COPY_HERE:
         $from = phutil_tag('strong', array(), $changeset->getOldFile());
         switch ($file) {
           case DifferentialChangeType::FILE_TEXT:
             $messages[] = pht('This file was copied from %s.', $from);
             break;
           case DifferentialChangeType::FILE_IMAGE:
             $messages[] = pht('This image was copied from %s.', $from);
             break;
           case DifferentialChangeType::FILE_DIRECTORY:
             $messages[] = pht('This directory was copied from %s.', $from);
             break;
           case DifferentialChangeType::FILE_BINARY:
             $messages[] = pht('This binary file was copied from %s.', $from);
             break;
           case DifferentialChangeType::FILE_SYMLINK:
             $messages[] = pht('This symlink was copied from %s.', $from);
             break;
           case DifferentialChangeType::FILE_SUBMODULE:
             $messages[] = pht('This submodule was copied from %s.', $from);
             break;
         }
         break;
 
       case DifferentialChangeType::TYPE_MOVE_AWAY:
         $paths = phutil_tag(
           'strong',
           array(),
           implode(', ', $changeset->getAwayPaths()));
         switch ($file) {
           case DifferentialChangeType::FILE_TEXT:
             $messages[] = pht('This file was moved to %s.', $paths);
             break;
           case DifferentialChangeType::FILE_IMAGE:
             $messages[] = pht('This image was moved to %s.', $paths);
             break;
           case DifferentialChangeType::FILE_DIRECTORY:
             $messages[] = pht('This directory was moved to %s.', $paths);
             break;
           case DifferentialChangeType::FILE_BINARY:
             $messages[] = pht('This binary file was moved to %s.', $paths);
             break;
           case DifferentialChangeType::FILE_SYMLINK:
             $messages[] = pht('This symlink was moved to %s.', $paths);
             break;
           case DifferentialChangeType::FILE_SUBMODULE:
             $messages[] = pht('This submodule was moved to %s.', $paths);
             break;
         }
         break;
 
       case DifferentialChangeType::TYPE_COPY_AWAY:
         $paths = phutil_tag(
           'strong',
           array(),
           implode(', ', $changeset->getAwayPaths()));
         switch ($file) {
           case DifferentialChangeType::FILE_TEXT:
             $messages[] = pht('This file was copied to %s.', $paths);
             break;
           case DifferentialChangeType::FILE_IMAGE:
             $messages[] = pht('This image was copied to %s.', $paths);
             break;
           case DifferentialChangeType::FILE_DIRECTORY:
             $messages[] = pht('This directory was copied to %s.', $paths);
             break;
           case DifferentialChangeType::FILE_BINARY:
             $messages[] = pht('This binary file was copied to %s.', $paths);
             break;
           case DifferentialChangeType::FILE_SYMLINK:
             $messages[] = pht('This symlink was copied to %s.', $paths);
             break;
           case DifferentialChangeType::FILE_SUBMODULE:
             $messages[] = pht('This submodule was copied to %s.', $paths);
             break;
         }
         break;
 
       case DifferentialChangeType::TYPE_MULTICOPY:
         $paths = phutil_tag(
           'strong',
           array(),
           implode(', ', $changeset->getAwayPaths()));
         switch ($file) {
           case DifferentialChangeType::FILE_TEXT:
             $messages[] = pht(
               'This file was deleted after being copied to %s.',
               $paths);
             break;
           case DifferentialChangeType::FILE_IMAGE:
             $messages[] = pht(
               'This image was deleted after being copied to %s.',
               $paths);
             break;
           case DifferentialChangeType::FILE_DIRECTORY:
             $messages[] = pht(
               'This directory was deleted after being copied to %s.',
               $paths);
             break;
           case DifferentialChangeType::FILE_BINARY:
             $messages[] = pht(
               'This binary file was deleted after being copied to %s.',
               $paths);
             break;
           case DifferentialChangeType::FILE_SYMLINK:
             $messages[] = pht(
               'This symlink was deleted after being copied to %s.',
               $paths);
             break;
           case DifferentialChangeType::FILE_SUBMODULE:
             $messages[] = pht(
               'This submodule was deleted after being copied to %s.',
               $paths);
             break;
         }
         break;
 
       default:
         switch ($file) {
           case DifferentialChangeType::FILE_TEXT:
             // This is the default case, so we only render this header if
             // forced to since it's not very useful.
             if ($force) {
               $messages[] = pht('This file was not modified.');
             }
             break;
           case DifferentialChangeType::FILE_IMAGE:
             $messages[] = pht('This is an image.');
             break;
           case DifferentialChangeType::FILE_DIRECTORY:
             $messages[] = pht('This is a directory.');
             break;
           case DifferentialChangeType::FILE_BINARY:
             $messages[] = pht('This is a binary file.');
             break;
           case DifferentialChangeType::FILE_SYMLINK:
             $messages[] = pht('This is a symlink.');
             break;
           case DifferentialChangeType::FILE_SUBMODULE:
             $messages[] = pht('This is a submodule.');
             break;
         }
         break;
     }
 
     // If this is a text file with at least one hunk, we may have converted
     // the text encoding. In this case, show a note.
     $show_encoding = ($file == DifferentialChangeType::FILE_TEXT) &&
                      ($changeset->getHunks());
 
     if ($show_encoding) {
       $encoding = $this->getOriginalCharacterEncoding();
       if ($encoding != 'utf8') {
         if ($encoding) {
           $messages[] = pht(
             'This file was converted from %s for display.',
             phutil_tag('strong', array(), $encoding));
         } else {
           $messages[] = pht(
             'This file uses an unknown character encoding.');
         }
       }
     }
 
     if (!$messages) {
       return null;
     }
 
     foreach ($messages as $key => $message) {
       $messages[$key] = phutil_tag('li', array(), $message);
     }
 
     return phutil_tag(
       'ul',
       array(
         'class' => 'differential-meta-notice',
       ),
       $messages);
   }
 
   protected function renderPropertyChangeHeader() {
     $changeset = $this->getChangeset();
     list($old, $new) = $this->getChangesetProperties($changeset);
 
     // If we don't have any property changes, don't render this table.
     if ($old === $new) {
       return null;
     }
 
     $keys = array_keys($old + $new);
     sort($keys);
 
     $key_map = array(
       'unix:filemode' => pht('File Mode'),
       'file:dimensions' => pht('Image Dimensions'),
       'file:mimetype' => pht('MIME Type'),
       'file:size' => pht('File Size'),
     );
 
     $rows = array();
     foreach ($keys as $key) {
       $oval = idx($old, $key);
       $nval = idx($new, $key);
       if ($oval !== $nval) {
         if ($oval === null) {
           $oval = phutil_tag('em', array(), 'null');
         } else {
           $oval = phutil_escape_html_newlines($oval);
         }
 
         if ($nval === null) {
           $nval = phutil_tag('em', array(), 'null');
         } else {
           $nval = phutil_escape_html_newlines($nval);
         }
 
         $readable_key = idx($key_map, $key, $key);
 
         $row = array(
           $readable_key,
           $oval,
           $nval,
         );
         $rows[] = $row;
 
       }
     }
 
     $classes = array('', 'oval', 'nval');
     $headers = array(
       pht('Property'),
       pht('Old Value'),
       pht('New Value'),
     );
     $table = id(new AphrontTableView($rows))
       ->setHeaders($headers)
       ->setColumnClasses($classes);
     return phutil_tag(
       'div',
       array(
         'class' => 'differential-property-table',
       ),
       $table);
   }
 
   public function renderShield($message, $force = 'default') {
     $end = count($this->getOldLines());
     $reference = $this->getRenderingReference();
 
     if ($force !== 'text' &&
         $force !== 'whitespace' &&
         $force !== 'none' &&
         $force !== 'default') {
       throw new Exception("Invalid 'force' parameter '{$force}'!");
     }
 
     $range = "0-{$end}";
     if ($force == 'text') {
       // If we're forcing text, force the whole file to be rendered.
       $range = "{$range}/0-{$end}";
     }
 
     $meta = array(
       'ref'   => $reference,
       'range' => $range,
     );
 
     if ($force == 'whitespace') {
       $meta['whitespace'] = DifferentialChangesetParser::WHITESPACE_SHOW_ALL;
     }
 
     $content = array();
     $content[] = $message;
     if ($force !== 'none') {
       $content[] = ' ';
       $content[] = javelin_tag(
         'a',
         array(
           'mustcapture' => true,
           'sigil'       => 'show-more',
           'class'       => 'complete',
           'href'        => '#',
           'meta'        => $meta,
         ),
         pht('Show File Contents'));
     }
 
     return $this->wrapChangeInTable(
       javelin_tag(
         'tr',
         array(
           'sigil' => 'context-target',
         ),
         phutil_tag(
           'td',
           array(
             'class' => 'differential-shield',
             'colspan' => 6,
           ),
           $content)));
   }
 
   abstract protected function renderColgroup();
 
 
   protected function wrapChangeInTable($content) {
     if (!$content) {
       return null;
     }
 
     $classes = array();
     $classes[] = 'differential-diff';
     $classes[] = 'remarkup-code';
     $classes[] = 'PhabricatorMonospaced';
     $classes[] = $this->getRendererTableClass();
 
     return javelin_tag(
       'table',
       array(
         'class' => implode(' ', $classes),
         'sigil' => 'differential-diff',
       ),
       array(
         $this->renderColgroup(),
         $content,
       ));
   }
 
   protected function renderInlineComment(
     PhabricatorInlineCommentInterface $comment,
     $on_right = false) {
 
     return $this->buildInlineComment($comment, $on_right)->render();
   }
 
   protected function buildInlineComment(
     PhabricatorInlineCommentInterface $comment,
     $on_right = false) {
 
     $user = $this->getUser();
     $edit = $user &&
             ($comment->getAuthorPHID() == $user->getPHID()) &&
             ($comment->isDraft())
             && $this->getShowEditAndReplyLinks();
     $allow_reply = (bool)$user && $this->getShowEditAndReplyLinks();
 
     return id(new DifferentialInlineCommentView())
       ->setInlineComment($comment)
       ->setOnRight($on_right)
       ->setHandles($this->getHandles())
       ->setMarkupEngine($this->getMarkupEngine())
       ->setEditable($edit)
       ->setAllowReply($allow_reply);
   }
 
 
   /**
    * Build links which users can click to show more context in a changeset.
    *
    * @param int Beginning of the line range to build links for.
    * @param int Length of the line range to build links for.
    * @param int Total number of lines in the changeset.
    * @return markup Rendered links.
    */
   protected function renderShowContextLinks($top, $len, $changeset_length) {
     $block_size = 20;
     $end = ($top + $len) - $block_size;
 
     // If this is a large block, such that the "top" and "bottom" ranges are
     // non-overlapping, we'll provide options to show the top, bottom or entire
     // block. For smaller blocks, we only provide an option to show the entire
     // block, since it would be silly to show the bottom 20 lines of a 25-line
     // block.
     $is_large_block = ($len > ($block_size * 2));
 
     $links = array();
 
     if ($is_large_block) {
       $is_first_block = ($top == 0);
       if ($is_first_block) {
         $text = pht('Show First %d Line(s)', $block_size);
       } else {
         $text = pht("\xE2\x96\xB2 Show %d Line(s)", $block_size);
       }
 
       $links[] = $this->renderShowContextLink(
         false,
         "{$top}-{$len}/{$top}-20",
         $text);
     }
 
     $links[] = $this->renderShowContextLink(
       true,
       "{$top}-{$len}/{$top}-{$len}",
       pht('Show All %d Line(s)', $len));
 
     if ($is_large_block) {
       $is_last_block = (($top + $len) >= $changeset_length);
       if ($is_last_block) {
         $text = pht('Show Last %d Line(s)', $block_size);
       } else {
         $text = pht("\xE2\x96\xBC Show %d Line(s)", $block_size);
       }
 
       $links[] = $this->renderShowContextLink(
         false,
         "{$top}-{$len}/{$end}-20",
         $text);
     }
 
     return phutil_implode_html(" \xE2\x80\xA2 ", $links);
   }
 
 
   /**
    * Build a link that shows more context in a changeset.
    *
    * See @{method:renderShowContextLinks}.
    *
    * @param bool Does this link show all context when clicked?
    * @param string Range specification for lines to show.
    * @param string Text of the link.
    * @return markup Rendered link.
    */
   private function renderShowContextLink($is_all, $range, $text) {
     $reference = $this->getRenderingReference();
 
     return javelin_tag(
       'a',
       array(
         'href' => '#',
         'mustcapture' => true,
         'sigil' => 'show-more',
         'meta' => array(
           'type' => ($is_all ? 'all' : null),
-          'ref' => $reference,
           'range' => $range,
         ),
       ),
       $text);
   }
 
 
 }
diff --git a/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php b/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php
index e3d64f872..cdefbc303 100644
--- a/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php
+++ b/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php
@@ -1,115 +1,119 @@
 <?php
 
 final class DifferentialChangesetOneUpRenderer
   extends DifferentialChangesetHTMLRenderer {
 
   public function isOneUpRenderer() {
     return true;
   }
 
   protected function getRendererTableClass() {
     return 'diff-1up';
   }
 
+  public function getRendererKey() {
+    return '1up';
+  }
+
   protected function renderColgroup() {
     return phutil_tag('colgroup', array(), array(
       phutil_tag('col', array('class' => 'num')),
       phutil_tag('col', array('class' => 'num')),
       phutil_tag('col', array('class' => 'unified')),
     ));
   }
 
   public function renderTextChange(
     $range_start,
     $range_len,
     $rows) {
 
     $primitives = $this->buildPrimitives($range_start, $range_len);
 
     $out = array();
     foreach ($primitives as $p) {
       $type = $p['type'];
       switch ($type) {
         case 'old':
         case 'new':
           $out[] = hsprintf('<tr>');
           if ($type == 'old') {
             if ($p['htype']) {
               $class = 'left old';
             } else {
               $class = 'left';
             }
             $out[] = phutil_tag('th', array(), $p['line']);
             $out[] = phutil_tag('th', array());
             $out[] = phutil_tag('td', array('class' => $class), $p['render']);
           } else if ($type == 'new') {
             if ($p['htype']) {
               $class = 'right new';
               $out[] = phutil_tag('th', array());
             } else {
               $class = 'right';
               $out[] = phutil_tag('th', array(), $p['oline']);
             }
             $out[] = phutil_tag('th', array(), $p['line']);
             $out[] = phutil_tag('td', array('class' => $class), $p['render']);
           }
           $out[] = hsprintf('</tr>');
           break;
         case 'inline':
           $out[] = hsprintf('<tr><th /><th />');
           $out[] = hsprintf('<td>');
 
           $inline = $this->buildInlineComment(
             $p['comment'],
             $p['right']);
           $inline->setBuildScaffolding(false);
           $out[] = $inline->render();
 
           $out[] = hsprintf('</td></tr>');
           break;
         case 'no-context':
           $out[] = hsprintf(
             '<tr><td class="show-more" colspan="3">%s</td></tr>',
             pht('Context not available.'));
           break;
         case 'context':
           $top = $p['top'];
           $len = $p['len'];
 
           $links = $this->renderShowContextLinks($top, $len, $rows);
 
           $out[] = javelin_tag(
             'tr',
             array(
               'sigil' => 'context-target',
             ),
             phutil_tag(
               'td',
               array(
                 'class' => 'show-more',
                 'colspan' => 3,
               ),
               $links));
           break;
         default:
           $out[] = hsprintf('<tr><th /><th /><td>%s</td></tr>', $type);
           break;
       }
     }
 
     if ($out) {
       return $this->wrapChangeInTable(phutil_implode_html('', $out));
     }
     return null;
   }
 
   public function renderFileChange(
     $old_file = null,
     $new_file = null,
     $id = 0,
     $vs = 0) {
 
     throw new PhutilMethodNotImplementedException();
   }
 
 }
diff --git a/src/applications/differential/render/DifferentialChangesetOneUpTestRenderer.php b/src/applications/differential/render/DifferentialChangesetOneUpTestRenderer.php
index b6ea30e51..a9afb43af 100644
--- a/src/applications/differential/render/DifferentialChangesetOneUpTestRenderer.php
+++ b/src/applications/differential/render/DifferentialChangesetOneUpTestRenderer.php
@@ -1,10 +1,14 @@
 <?php
 
 final class DifferentialChangesetOneUpTestRenderer
   extends DifferentialChangesetTestRenderer {
 
   public function isOneUpRenderer() {
     return true;
   }
 
+  public function getRendererKey() {
+    return '1up-test';
+  }
+
 }
diff --git a/src/applications/differential/render/DifferentialChangesetRenderer.php b/src/applications/differential/render/DifferentialChangesetRenderer.php
index be440b47e..09eb27ab4 100644
--- a/src/applications/differential/render/DifferentialChangesetRenderer.php
+++ b/src/applications/differential/render/DifferentialChangesetRenderer.php
@@ -1,584 +1,586 @@
 <?php
 
 abstract class DifferentialChangesetRenderer {
 
   private $user;
   private $changeset;
   private $renderingReference;
   private $renderPropertyChangeHeader;
   private $isTopLevel;
   private $hunkStartLines;
   private $oldLines;
   private $newLines;
   private $oldComments;
   private $newComments;
   private $oldChangesetID;
   private $newChangesetID;
   private $oldAttachesToNewFile;
   private $newAttachesToNewFile;
   private $highlightOld = array();
   private $highlightNew = array();
   private $codeCoverage;
   private $handles;
   private $markupEngine;
   private $oldRender;
   private $newRender;
   private $originalOld;
   private $originalNew;
   private $gaps;
   private $mask;
   private $depths;
   private $originalCharacterEncoding;
   private $showEditAndReplyLinks;
 
   private $oldFile = false;
   private $newFile = false;
 
+  abstract public function getRendererKey();
+
   public function setShowEditAndReplyLinks($bool) {
     $this->showEditAndReplyLinks = $bool;
     return $this;
   }
   public function getShowEditAndReplyLinks() {
     return $this->showEditAndReplyLinks;
   }
 
   public function setOriginalCharacterEncoding($original_character_encoding) {
     $this->originalCharacterEncoding = $original_character_encoding;
     return $this;
   }
 
   public function getOriginalCharacterEncoding() {
     return $this->originalCharacterEncoding;
   }
 
   public function setDepths($depths) {
     $this->depths = $depths;
     return $this;
   }
   protected function getDepths() {
     return $this->depths;
   }
 
   public function setMask($mask) {
     $this->mask = $mask;
     return $this;
   }
   protected function getMask() {
     return $this->mask;
   }
 
   public function setGaps($gaps) {
     $this->gaps = $gaps;
     return $this;
   }
   protected function getGaps() {
     return $this->gaps;
   }
 
   public function attachOldFile(PhabricatorFile $old = null) {
     $this->oldFile = $old;
     return $this;
   }
 
   public function getOldFile() {
     if ($this->oldFile === false) {
       throw new PhabricatorDataNotAttachedException($this);
     }
     return $this->oldFile;
   }
 
   public function hasOldFile() {
     return (bool)$this->oldFile;
   }
 
   public function attachNewFile(PhabricatorFile $new = null) {
     $this->newFile = $new;
     return $this;
   }
 
   public function getNewFile() {
     if ($this->newFile === false) {
       throw new PhabricatorDataNotAttachedException($this);
     }
     return $this->newFile;
   }
 
   public function hasNewFile() {
     return (bool)$this->newFile;
   }
 
   public function setOriginalNew($original_new) {
     $this->originalNew = $original_new;
     return $this;
   }
   protected function getOriginalNew() {
     return $this->originalNew;
   }
 
   public function setOriginalOld($original_old) {
     $this->originalOld = $original_old;
     return $this;
   }
   protected function getOriginalOld() {
     return $this->originalOld;
   }
 
   public function setNewRender($new_render) {
     $this->newRender = $new_render;
     return $this;
   }
   protected function getNewRender() {
     return $this->newRender;
   }
 
   public function setOldRender($old_render) {
     $this->oldRender = $old_render;
     return $this;
   }
   protected function getOldRender() {
     return $this->oldRender;
   }
 
   public function setMarkupEngine(PhabricatorMarkupEngine $markup_engine) {
     $this->markupEngine = $markup_engine;
     return $this;
   }
   public function getMarkupEngine() {
     return $this->markupEngine;
   }
 
   public function setHandles(array $handles) {
     assert_instances_of($handles, 'PhabricatorObjectHandle');
     $this->handles = $handles;
     return $this;
   }
   protected function getHandles() {
     return $this->handles;
   }
 
   public function setCodeCoverage($code_coverage) {
     $this->codeCoverage = $code_coverage;
     return $this;
   }
   protected function getCodeCoverage() {
     return $this->codeCoverage;
   }
 
   public function setHighlightNew($highlight_new) {
     $this->highlightNew = $highlight_new;
     return $this;
   }
   protected function getHighlightNew() {
     return $this->highlightNew;
   }
 
   public function setHighlightOld($highlight_old) {
     $this->highlightOld = $highlight_old;
     return $this;
   }
   protected function getHighlightOld() {
     return $this->highlightOld;
   }
 
   public function setNewAttachesToNewFile($attaches) {
     $this->newAttachesToNewFile = $attaches;
     return $this;
   }
   protected function getNewAttachesToNewFile() {
     return $this->newAttachesToNewFile;
   }
 
   public function setOldAttachesToNewFile($attaches) {
     $this->oldAttachesToNewFile = $attaches;
     return $this;
   }
   protected function getOldAttachesToNewFile() {
     return $this->oldAttachesToNewFile;
   }
 
   public function setNewChangesetID($new_changeset_id) {
     $this->newChangesetID = $new_changeset_id;
     return $this;
   }
   protected function getNewChangesetID() {
     return $this->newChangesetID;
   }
 
   public function setOldChangesetID($old_changeset_id) {
     $this->oldChangesetID = $old_changeset_id;
     return $this;
   }
   protected function getOldChangesetID() {
     return $this->oldChangesetID;
   }
 
   public function setNewComments(array $new_comments) {
     foreach ($new_comments as $line_number => $comments) {
       assert_instances_of($comments, 'PhabricatorInlineCommentInterface');
     }
     $this->newComments = $new_comments;
     return $this;
   }
   protected function getNewComments() {
     return $this->newComments;
   }
 
   public function setOldComments(array $old_comments) {
     foreach ($old_comments as $line_number => $comments) {
       assert_instances_of($comments, 'PhabricatorInlineCommentInterface');
     }
     $this->oldComments = $old_comments;
     return $this;
   }
   protected function getOldComments() {
     return $this->oldComments;
   }
 
   public function setNewLines(array $new_lines) {
     $this->newLines = $new_lines;
     return $this;
   }
   protected function getNewLines() {
     return $this->newLines;
   }
 
   public function setOldLines(array $old_lines) {
     $this->oldLines = $old_lines;
     return $this;
   }
   protected function getOldLines() {
     return $this->oldLines;
   }
 
   public function setHunkStartLines(array $hunk_start_lines) {
     $this->hunkStartLines = $hunk_start_lines;
     return $this;
   }
 
   protected function getHunkStartLines() {
     return $this->hunkStartLines;
   }
 
   public function setUser(PhabricatorUser $user) {
     $this->user = $user;
     return $this;
   }
   protected function getUser() {
     return $this->user;
   }
 
   public function setChangeset(DifferentialChangeset $changeset) {
     $this->changeset = $changeset;
     return $this;
   }
   protected function getChangeset() {
     return $this->changeset;
   }
 
   public function setRenderingReference($rendering_reference) {
     $this->renderingReference = $rendering_reference;
     return $this;
   }
   protected function getRenderingReference() {
     return $this->renderingReference;
   }
 
   public function setRenderPropertyChangeHeader($should_render) {
     $this->renderPropertyChangeHeader = $should_render;
     return $this;
   }
   private function shouldRenderPropertyChangeHeader() {
     return $this->renderPropertyChangeHeader;
   }
 
   public function setIsTopLevel($is) {
     $this->isTopLevel = $is;
     return $this;
   }
   private function getIsTopLevel() {
     return $this->isTopLevel;
   }
 
   final public function renderChangesetTable($content) {
     $props = null;
     if ($this->shouldRenderPropertyChangeHeader()) {
       $props = $this->renderPropertyChangeHeader();
     }
 
     $notice = null;
     if ($this->getIsTopLevel()) {
       $force = (!$content && !$props);
       $notice = $this->renderChangeTypeHeader($force);
     }
 
     $result = $notice.$props.$content;
 
     // TODO: Let the user customize their tab width / display style.
     // TODO: We should possibly post-process "\r" as well.
     // TODO: Both these steps should happen earlier.
     $result = str_replace("\t", '  ', $result);
 
     return phutil_safe_html($result);
   }
 
   abstract public function isOneUpRenderer();
   abstract public function renderTextChange(
     $range_start,
     $range_len,
     $rows);
   abstract public function renderFileChange(
     $old = null,
     $new = null,
     $id = 0,
     $vs = 0);
 
   abstract protected function renderChangeTypeHeader($force);
 
   protected function didRenderChangesetTableContents($contents) {
     return $contents;
   }
 
   /**
    * Render a "shield" over the diff, with a message like "This file is
    * generated and does not need to be reviewed." or "This file was completely
    * deleted." This UI element hides unimportant text so the reviewer doesn't
    * need to scroll past it.
    *
    * The shield includes a link to view the underlying content. This link
    * may force certain rendering modes when the link is clicked:
    *
    *    - `"default"`: Render the diff normally, as though it was not
    *      shielded. This is the default and appropriate if the underlying
    *      diff is a normal change, but was hidden for reasons of not being
    *      important (e.g., generated code).
    *    - `"text"`: Force the text to be shown. This is probably only relevant
    *      when a file is not changed.
    *    - `"whitespace"`: Force the text to be shown, and the diff to be
    *      rendered with all whitespace shown. This is probably only relevant
    *      when a file is changed only by altering whitespace.
    *    - `"none"`: Don't show the link (e.g., text not available).
    *
    * @param   string        Message explaining why the diff is hidden.
    * @param   string|null   Force mode, see above.
    * @return  string        Shield markup.
    */
   abstract public function renderShield($message, $force = 'default');
 
   abstract protected function renderPropertyChangeHeader();
 
   protected function buildPrimitives($range_start, $range_len) {
     $primitives = array();
 
     $hunk_starts = $this->getHunkStartLines();
 
     $mask = $this->getMask();
     $gaps = $this->getGaps();
 
     $old = $this->getOldLines();
     $new = $this->getNewLines();
     $old_render = $this->getOldRender();
     $new_render = $this->getNewRender();
     $old_comments = $this->getOldComments();
     $new_comments = $this->getNewComments();
 
     $size = count($old);
     for ($ii = $range_start; $ii < $range_start + $range_len; $ii++) {
       if (empty($mask[$ii])) {
         list($top, $len) = array_pop($gaps);
         $primitives[] = array(
           'type' => 'context',
           'top' => $top,
           'len' => $len,
         );
 
         $ii += ($len - 1);
         continue;
       }
 
       $ospec = array(
         'type' => 'old',
         'htype' => null,
         'cursor' => $ii,
         'line' => null,
         'oline' => null,
         'render' => null,
       );
 
       $nspec = array(
         'type' => 'new',
         'htype' => null,
         'cursor' => $ii,
         'line' => null,
         'oline' => null,
         'render' => null,
         'copy' => null,
         'coverage' => null,
       );
 
       if (isset($old[$ii])) {
         $ospec['line'] = (int)$old[$ii]['line'];
         $nspec['oline'] = (int)$old[$ii]['line'];
         $ospec['htype'] = $old[$ii]['type'];
         if (isset($old_render[$ii])) {
           $ospec['render'] = $old_render[$ii];
         }
       }
 
       if (isset($new[$ii])) {
         $nspec['line'] = (int)$new[$ii]['line'];
         $ospec['oline'] = (int)$new[$ii]['line'];
         $nspec['htype'] = $new[$ii]['type'];
         if (isset($new_render[$ii])) {
           $nspec['render'] = $new_render[$ii];
         }
       }
 
       if (isset($hunk_starts[$ospec['line']])) {
         $primitives[] = array(
           'type' => 'no-context',
         );
       }
 
       $primitives[] = $ospec;
       $primitives[] = $nspec;
 
       if ($ospec['line'] !== null && isset($old_comments[$ospec['line']])) {
         foreach ($old_comments[$ospec['line']] as $comment) {
           $primitives[] = array(
             'type' => 'inline',
             'comment' => $comment,
             'right' => false,
           );
         }
       }
 
       if ($nspec['line'] !== null && isset($new_comments[$nspec['line']])) {
         foreach ($new_comments[$nspec['line']] as $comment) {
           $primitives[] = array(
             'type' => 'inline',
             'comment' => $comment,
             'right' => true,
           );
         }
       }
 
       if ($hunk_starts && ($ii == $size - 1)) {
         $primitives[] = array(
           'type' => 'no-context',
         );
       }
     }
 
     if ($this->isOneUpRenderer()) {
       $primitives = $this->processPrimitivesForOneUp($primitives);
     }
 
     return $primitives;
   }
 
   private function processPrimitivesForOneUp(array $primitives) {
     // Primitives come out of buildPrimitives() in two-up format, because it
     // is the most general, flexible format. To put them into one-up format,
     // we need to filter and reorder them. In particular:
     //
     //   - We discard unchanged lines in the old file; in one-up format, we
     //     render them only once.
     //   - We group contiguous blocks of old-modified and new-modified lines, so
     //     they render in "block of old, block of new" order instead of
     //     alternating old and new lines.
 
     $out = array();
 
     $old_buf = array();
     $new_buf = array();
     foreach ($primitives as $primitive) {
       $type = $primitive['type'];
 
       if ($type == 'old') {
         if (!$primitive['htype']) {
           // This is a line which appears in both the old file and the new
           // file, or the spacer corresponding to a line added in the new file.
           // Ignore it when rendering a one-up diff.
           continue;
         }
         if ($new_buf) {
           $out[] = $new_buf;
           $new_buf = array();
         }
         $old_buf[] = $primitive;
       } else if ($type == 'new') {
         if ($primitive['line'] === null) {
           // This is an empty spacer corresponding to a line removed from the
           // old file. Ignore it when rendering a one-up diff.
           continue;
         }
         if ($old_buf) {
           $out[] = $old_buf;
           $old_buf = array();
         }
         $new_buf[] = $primitive;
       } else if ($type == 'context' || $type == 'no-context') {
         $out[] = $old_buf;
         $out[] = $new_buf;
         $old_buf = array();
         $new_buf = array();
         $out[] = array($primitive);
       } else if ($type == 'inline') {
         $out[] = $old_buf;
         $out[] = $new_buf;
         $old_buf = array();
         $new_buf = array();
 
         $out[] = array($primitive);
       } else {
         throw new Exception("Unknown primitive type '{$primitive}'!");
       }
     }
 
     $out[] = $old_buf;
     $out[] = $new_buf;
     $out = array_mergev($out);
 
     return $out;
   }
 
   protected function getChangesetProperties($changeset) {
     $old = $changeset->getOldProperties();
     $new = $changeset->getNewProperties();
 
     // When adding files, don't show the uninteresting 644 filemode change.
     if ($changeset->getChangeType() == DifferentialChangeType::TYPE_ADD &&
         $new == array('unix:filemode' => '100644')) {
       unset($new['unix:filemode']);
     }
 
     // Likewise when removing files.
     if ($changeset->getChangeType() == DifferentialChangeType::TYPE_DELETE &&
         $old == array('unix:filemode' => '100644')) {
       unset($old['unix:filemode']);
     }
 
     if ($this->hasOldFile()) {
       $file = $this->getOldFile();
       if ($file->getImageWidth()) {
         $dimensions = $file->getImageWidth().'x'.$file->getImageHeight();
         $old['file:dimensions'] = $dimensions;
       }
       $old['file:mimetype'] = $file->getMimeType();
       $old['file:size'] = phutil_format_bytes($file->getByteSize());
     }
 
     if ($this->hasNewFile()) {
       $file = $this->getNewFile();
       if ($file->getImageWidth()) {
         $dimensions = $file->getImageWidth().'x'.$file->getImageHeight();
         $new['file:dimensions'] = $dimensions;
       }
       $new['file:mimetype'] = $file->getMimeType();
       $new['file:size'] = phutil_format_bytes($file->getByteSize());
     }
 
     return array($old, $new);
   }
 
 }
diff --git a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php
index 112933004..682bcd51e 100644
--- a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php
+++ b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php
@@ -1,470 +1,474 @@
 <?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();
     $left_id = $this->getOldChangesetID();
     $right_id = $this->getNewChangesetID();
 
     // "N" stands for 'new' and means the comment should attach to the new file
     // when stored, i.e. DifferentialInlineComment->setIsNewFile().
     // "O" stands for 'old' and means the comment should attach to the old file.
 
     $left_char = $this->getOldAttachesToNewFile()
       ? 'N'
       : 'O';
     $right_char = $this->getNewAttachesToNewFile()
       ? 'N'
       : 'O';
 
     $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);
 
         // TODO: Move this to renderShowContextLinks() once that is stable.
 
         $top = $gap[0];
         $len = $gap[1];
 
         $end   = $top + $len - 20;
 
         $contents = array();
 
         if ($len > 40) {
           $is_first_block = false;
           if ($ii == 0) {
             $is_first_block = true;
           }
 
           $contents[] = javelin_tag(
             'a',
             array(
               'href' => '#',
               'mustcapture' => true,
               'sigil'       => 'show-more',
               'meta'        => array(
                 'ref'    => $reference,
                 'range' => "{$top}-{$len}/{$top}-20",
               ),
             ),
             $is_first_block
               ? pht('Show First 20 Lines')
               : pht("\xE2\x96\xB2 Show 20 Lines"));
         }
 
         $contents[] = javelin_tag(
           'a',
           array(
             'href' => '#',
             'mustcapture' => true,
             'sigil'       => 'show-more',
             'meta'        => array(
               'type'   => 'all',
               'ref'    => $reference,
               'range'  => "{$top}-{$len}/{$top}-{$len}",
             ),
           ),
           pht('Show All %d Lines', $len));
 
         $is_last_block = false;
         if ($ii + $len >= $rows) {
           $is_last_block = true;
         }
 
         if ($len > 40) {
           $contents[] = javelin_tag(
             'a',
             array(
               'href' => '#',
               'mustcapture' => true,
               'sigil'       => 'show-more',
               'meta'        => array(
                 'ref'    => $reference,
                 'range' => "{$top}-{$len}/{$end}-20",
               ),
             ),
             $is_last_block
               ? pht('Show Last 20 Lines')
               : pht("\xE2\x96\xBC Show 20 Lines"));
         }
 
         $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',
               ),
               phutil_implode_html(
                 " \xE2\x80\xA2 ", // Bullet
                 $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_id) {
         $o_id = 'C'.$left_id.$left_char.'L'.$o_num;
       } else {
         $o_id = null;
       }
 
       if ($n_num && $right_id) {
         $n_id = 'C'.$right_id.$right_char.'L'.$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";
 
       // NOTE: The Javascript is sensitive to whitespace changes in this
       // block!
 
       $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) {
           $comment_html = $this->renderInlineComment($comment,
                                                      $on_right = false);
           $new = '';
           if ($n_num && isset($new_comments[$n_num])) {
             foreach ($new_comments[$n_num] as $key => $new_comment) {
               if ($comment->isCompatible($new_comment)) {
                 $new = $this->renderInlineComment($new_comment,
                                                   $on_right = true);
                 unset($new_comments[$n_num][$key]);
               }
             }
           }
           $html[] = phutil_tag('tr', array('class' => 'inline'), array(
             phutil_tag('th', array()),
             phutil_tag('td', array(), $comment_html),
             phutil_tag('th', array()),
             phutil_tag('td', array('colspan' => 3), $new),
           ));
         }
       }
       if ($n_num && isset($new_comments[$n_num])) {
         foreach ($new_comments[$n_num] as $comment) {
           $comment_html = $this->renderInlineComment($comment,
                                                      $on_right = true);
           $html[] = phutil_tag('tr', array('class' => 'inline'), array(
             phutil_tag('th', array()),
             phutil_tag('td', array()),
             phutil_tag('th', array()),
             phutil_tag(
               'td',
               array('colspan' => 3),
               $comment_html),
           ));
         }
       }
     }
 
     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 = phutil_tag(
         'div',
         array(
           'class' => 'differential-image-stage',
         ),
         phutil_tag(
           'img',
           array(
             'src' => $old_file->getBestURI(),
           )));
     }
 
     $new = null;
     if ($new_file) {
       $new = phutil_tag(
         'div',
         array(
           'class' => 'differential-image-stage',
         ),
         phutil_tag(
           'img',
           array(
             'src' => $new_file->getBestURI(),
           )));
     }
 
     $html_old = array();
     $html_new = array();
     foreach ($this->getOldComments() as $on_line => $comment_group) {
       foreach ($comment_group as $comment) {
         $comment_html = $this->renderInlineComment($comment, $on_right = false);
         $html_old[] = phutil_tag('tr', array('class' => 'inline'), array(
           phutil_tag('th', array()),
           phutil_tag('td', array(), $comment_html),
           phutil_tag('th', array()),
           phutil_tag('td', array('colspan' => 3)),
         ));
       }
     }
     foreach ($this->getNewComments() as $lin_line => $comment_group) {
       foreach ($comment_group as $comment) {
         $comment_html = $this->renderInlineComment($comment, $on_right = true);
         $html_new[] = phutil_tag('tr', array('class' => 'inline'), array(
           phutil_tag('th', array()),
           phutil_tag('td', array()),
           phutil_tag('th', array()),
           phutil_tag(
             'td',
             array('colspan' => 3),
             $comment_html),
         ));
       }
     }
 
     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);
   }
 
 }
diff --git a/src/applications/differential/render/DifferentialChangesetTwoUpTestRenderer.php b/src/applications/differential/render/DifferentialChangesetTwoUpTestRenderer.php
index e144577db..226758b9f 100644
--- a/src/applications/differential/render/DifferentialChangesetTwoUpTestRenderer.php
+++ b/src/applications/differential/render/DifferentialChangesetTwoUpTestRenderer.php
@@ -1,10 +1,14 @@
 <?php
 
 final class DifferentialChangesetTwoUpTestRenderer
   extends DifferentialChangesetTestRenderer {
 
   public function isOneUpRenderer() {
     return false;
   }
 
+  public function getRendererKey() {
+    return '2up-test';
+  }
+
 }
diff --git a/src/applications/differential/view/DifferentialChangesetDetailView.php b/src/applications/differential/view/DifferentialChangesetDetailView.php
index 6857fe633..7723a7be7 100644
--- a/src/applications/differential/view/DifferentialChangesetDetailView.php
+++ b/src/applications/differential/view/DifferentialChangesetDetailView.php
@@ -1,247 +1,244 @@
 <?php
 
 final class DifferentialChangesetDetailView extends AphrontView {
 
   private $changeset;
   private $buttons = array();
   private $editable;
   private $symbolIndex;
   private $id;
   private $vsChangesetID;
   private $renderURI;
   private $whitespace;
   private $renderingRef;
   private $autoload;
+  private $renderer;
 
   public function setAutoload($autoload) {
     $this->autoload = $autoload;
     return $this;
   }
 
   public function getAutoload() {
     return $this->autoload;
   }
 
   public function setRenderingRef($rendering_ref) {
     $this->renderingRef = $rendering_ref;
     return $this;
   }
 
   public function getRenderingRef() {
     return $this->renderingRef;
   }
 
   public function setWhitespace($whitespace) {
     $this->whitespace = $whitespace;
     return $this;
   }
 
   public function getWhitespace() {
     return $this->whitespace;
   }
 
   public function setRenderURI($render_uri) {
     $this->renderURI = $render_uri;
     return $this;
   }
 
   public function getRenderURI() {
     return $this->renderURI;
   }
 
   public function setChangeset($changeset) {
     $this->changeset = $changeset;
     return $this;
   }
 
   public function addButton($button) {
     $this->buttons[] = $button;
     return $this;
   }
 
   public function setEditable($editable) {
     $this->editable = $editable;
     return $this;
   }
 
   public function setSymbolIndex($symbol_index) {
     $this->symbolIndex = $symbol_index;
     return $this;
   }
 
+  public function setRenderer($renderer) {
+    $this->renderer = $renderer;
+    return $this;
+  }
+
+  public function getRenderer() {
+    return $this->renderer;
+  }
+
   public function getID() {
     if (!$this->id) {
       $this->id = celerity_generate_unique_node_id();
     }
     return $this->id;
   }
 
   public function setID($id) {
     $this->id = $id;
     return $this;
   }
 
   public function setVsChangesetID($vs_changeset_id) {
     $this->vsChangesetID = $vs_changeset_id;
     return $this;
   }
 
   public function getVsChangesetID() {
     return $this->vsChangesetID;
   }
 
   public function getFileIcon($filename) {
     $path_info = pathinfo($filename);
     $extension = idx($path_info, 'extension');
     switch ($extension) {
       case 'psd':
       case 'ai':
         $icon = 'fa-eye';
         break;
       case 'conf':
         $icon = 'fa-wrench';
         break;
       case 'wav':
       case 'mp3':
       case 'aiff':
         $icon = 'fa-file-sound-o';
         break;
       case 'm4v':
       case 'mov':
         $icon = 'fa-file-movie-o';
         break;
       case 'sql':
       case 'db':
         $icon = 'fa-database';
         break;
       case 'xls':
       case 'csv':
         $icon = 'fa-file-excel-o';
         break;
       case 'ics':
         $icon = 'fa-calendar';
         break;
       case 'zip':
       case 'tar':
       case 'bz':
       case 'tgz':
       case 'gz':
         $icon = 'fa-file-archive-o';
         break;
       case 'png':
       case 'jpg':
       case 'bmp':
       case 'gif':
         $icon = 'fa-file-picture-o';
         break;
       case 'txt':
         $icon = 'fa-file-text-o';
         break;
       case 'doc':
       case 'docx':
         $icon = 'fa-file-word-o';
         break;
       case 'pdf':
         $icon = 'fa-file-pdf-o';
         break;
       default:
         $icon = 'fa-file-code-o';
         break;
     }
     return $icon;
   }
 
   public function render() {
     $this->requireResource('differential-changeset-view-css');
     $this->requireResource('syntax-highlighting-css');
 
     Javelin::initBehavior('phabricator-oncopy', array());
 
     $changeset = $this->changeset;
     $class = 'differential-changeset';
     if (!$this->editable) {
       $class .= ' differential-changeset-immutable';
     }
 
     $buttons = null;
     if ($this->buttons) {
       $buttons = phutil_tag(
         'div',
         array(
           'class' => 'differential-changeset-buttons',
         ),
         $this->buttons);
     }
 
     $id = $this->getID();
 
     if ($this->symbolIndex) {
       Javelin::initBehavior(
         'repository-crossreference',
         array(
           'container' => $id,
         ) + $this->symbolIndex);
     }
 
     $display_filename = $changeset->getDisplayFilename();
     $display_icon = $this->getFileIcon($display_filename);
     $icon = id(new PHUIIconView())
       ->setIconFont($display_icon);
 
-    $renderer = null;
-
-    // If the viewer prefers unified diffs, always set the renderer to unified.
-    // Otherwise, we leave it unspecified and the client will choose a
-    // renderer based on the screen size.
-
-    $viewer = $this->getUser();
-    $prefs = $viewer->loadPreferences();
-    $pref_unified = PhabricatorUserPreferences::PREFERENCE_DIFF_UNIFIED;
-    if ($prefs->getPreference($pref_unified) == 'unified') {
-      $renderer = '1up';
-    }
-
     return javelin_tag(
       'div',
       array(
         'sigil' => 'differential-changeset',
         'meta'  => array(
           'left'  => nonempty(
             $this->getVsChangesetID(),
             $this->changeset->getID()),
           'right' => $this->changeset->getID(),
           'renderURI' => $this->getRenderURI(),
           'whitespace' => $this->getWhitespace(),
           'highlight' => null,
-          'renderer' => $renderer,
+          'renderer' => $this->getRenderer(),
           'ref' => $this->getRenderingRef(),
           'autoload' => $this->getAutoload(),
         ),
         'class' => $class,
         'id'    => $id,
       ),
       array(
         id(new PhabricatorAnchorView())
           ->setAnchorName($changeset->getAnchorName())
           ->setNavigationMarker(true)
           ->render(),
         $buttons,
         phutil_tag('h1',
           array(
             'class' => 'differential-file-icon-header',
           ),
           array(
             $icon,
             $display_filename,
           )),
         javelin_tag(
           'div',
           array(
             'class' => 'changeset-view-content',
             'sigil' => 'changeset-view-content',
           ),
           $this->renderChildren()),
       ));
   }
 
 }
diff --git a/src/applications/differential/view/DifferentialChangesetListView.php b/src/applications/differential/view/DifferentialChangesetListView.php
index 4ec0e19a3..e63c3d613 100644
--- a/src/applications/differential/view/DifferentialChangesetListView.php
+++ b/src/applications/differential/view/DifferentialChangesetListView.php
@@ -1,362 +1,362 @@
 <?php
 
 final class DifferentialChangesetListView extends AphrontView {
 
   private $changesets = array();
   private $visibleChangesets = array();
   private $references = array();
   private $inlineURI;
   private $renderURI = '/differential/changeset/';
   private $whitespace;
 
   private $standaloneURI;
   private $leftRawFileURI;
   private $rightRawFileURI;
 
   private $symbolIndexes = array();
   private $repository;
   private $branch;
   private $diff;
   private $vsMap = array();
 
   private $title;
 
   public function setTitle($title) {
     $this->title = $title;
     return $this;
   }
   private function getTitle() {
     return $this->title;
   }
 
   public function setBranch($branch) {
     $this->branch = $branch;
     return $this;
   }
   private function getBranch() {
     return $this->branch;
   }
 
   public function setChangesets($changesets) {
     $this->changesets = $changesets;
     return $this;
   }
 
   public function setVisibleChangesets($visible_changesets) {
     $this->visibleChangesets = $visible_changesets;
     return $this;
   }
 
   public function setInlineCommentControllerURI($uri) {
     $this->inlineURI = $uri;
     return $this;
   }
 
   public function setRepository(PhabricatorRepository $repository) {
     $this->repository = $repository;
     return $this;
   }
 
   public function setDiff(DifferentialDiff $diff) {
     $this->diff = $diff;
     return $this;
   }
 
   public function setRenderingReferences(array $references) {
     $this->references = $references;
     return $this;
   }
 
   public function setSymbolIndexes(array $indexes) {
     $this->symbolIndexes = $indexes;
     return $this;
   }
 
   public function setRenderURI($render_uri) {
     $this->renderURI = $render_uri;
     return $this;
   }
 
   public function setWhitespace($whitespace) {
     $this->whitespace = $whitespace;
     return $this;
   }
 
   public function setVsMap(array $vs_map) {
     $this->vsMap = $vs_map;
     return $this;
   }
 
   public function getVsMap() {
     return $this->vsMap;
   }
 
   public function setStandaloneURI($uri) {
     $this->standaloneURI = $uri;
     return $this;
   }
 
   public function setRawFileURIs($l, $r) {
     $this->leftRawFileURI = $l;
     $this->rightRawFileURI = $r;
     return $this;
   }
 
   public function render() {
     $this->requireResource('differential-changeset-view-css');
 
     $changesets = $this->changesets;
 
     Javelin::initBehavior('differential-toggle-files', array(
       'pht' => array(
         'undo' => pht('Undo'),
         'collapsed' => pht('This file content has been collapsed.'),
       ),
     ));
     Javelin::initBehavior(
       'differential-dropdown-menus',
       array(
         'pht' => array(
           'Open in Editor' => pht('Open in Editor'),
           'Show Entire File' => pht('Show Entire File'),
           'Entire File Shown' => pht('Entire File Shown'),
           "Can't Toggle Unloaded File" => pht("Can't Toggle Unloaded File"),
           'Expand File' => pht('Expand File'),
           'Collapse File' => pht('Collapse File'),
           'Browse in Diffusion' => pht('Browse in Diffusion'),
           'View Standalone' => pht('View Standalone'),
           'Show Raw File (Left)' => pht('Show Raw File (Left)'),
           'Show Raw File (Right)' => pht('Show Raw File (Right)'),
           'Configure Editor' => pht('Configure Editor'),
           'Load Changes' => pht('Load Changes'),
           'View Side-by-Side' => pht('View Side-by-Side'),
           'View Unified' => pht('View Unified'),
           'Change Text Encoding...' => pht('Change Text Encoding...'),
           'Highlight As...' => pht('Highlight As...'),
         ),
       ));
 
+    $renderer = DifferentialChangesetParser::getDefaultRendererForViewer(
+      $this->getUser());
+
     $output = array();
     $ids = array();
     foreach ($changesets as $key => $changeset) {
 
       $file = $changeset->getFilename();
       $class = 'differential-changeset';
       if (!$this->inlineURI) {
         $class .= ' differential-changeset-noneditable';
       }
 
       $ref = $this->references[$key];
 
       $detail = id(new DifferentialChangesetDetailView())
         ->setUser($this->getUser());
 
       $uniq_id = 'diff-'.$changeset->getAnchorName();
       $detail->setID($uniq_id);
 
       $view_options = $this->renderViewOptionsDropdown(
         $detail,
         $ref,
         $changeset);
 
       $detail->setChangeset($changeset);
       $detail->addButton($view_options);
       $detail->setSymbolIndex(idx($this->symbolIndexes, $key));
       $detail->setVsChangesetID(idx($this->vsMap, $changeset->getID()));
       $detail->setEditable(true);
       $detail->setRenderingRef($ref);
       $detail->setAutoload(isset($this->visibleChangesets[$key]));
 
       $detail->setRenderURI($this->renderURI);
       $detail->setWhitespace($this->whitespace);
+      $detail->setRenderer($renderer);
 
       if (isset($this->visibleChangesets[$key])) {
         $load = 'Loading...';
       } else {
         $load = javelin_tag(
           'a',
           array(
             'class' => 'button grey',
             'href' => '#'.$uniq_id,
             'sigil' => 'differential-load',
             'meta' => array(
               'id' => $detail->getID(),
               'kill' => true,
             ),
             'mustcapture' => true,
           ),
           pht('Load File'));
       }
       $detail->appendChild(
         phutil_tag(
           'div',
           array(
             'id' => $uniq_id,
           ),
           phutil_tag('div', array('class' => 'differential-loading'), $load)));
       $output[] = $detail->render();
 
       $ids[] = $detail->getID();
     }
 
     $this->requireResource('aphront-tooltip-css');
 
     $this->initBehavior('differential-populate', array(
       'changesetViewIDs' => $ids,
     ));
 
-    $this->initBehavior('differential-show-more', array(
-      'uri' => $this->renderURI,
-      'whitespace' => $this->whitespace,
-    ));
-
+    $this->initBehavior('differential-show-more');
     $this->initBehavior('differential-comment-jump', array());
 
     if ($this->inlineURI) {
       $undo_templates = $this->renderUndoTemplates();
 
       Javelin::initBehavior('differential-edit-inline-comments', array(
         'uri'             => $this->inlineURI,
         'undo_templates'  => $undo_templates,
         'stage'           => 'differential-review-stage',
       ));
     }
 
     $header = id(new PHUIHeaderView())
       ->setHeader($this->getTitle());
 
     $content = phutil_tag(
       'div',
       array(
         'class' => 'differential-review-stage',
         'id'    => 'differential-review-stage',
       ),
       $output);
 
     $object_box = id(new PHUIObjectBoxView())
       ->setHeader($header)
       ->appendChild($content);
 
     return $object_box;
   }
 
   /**
    * Render the "Undo" markup for the inline comment undo feature.
    */
   private function renderUndoTemplates() {
     $link = javelin_tag(
       'a',
       array(
         'href'  => '#',
         'sigil' => 'differential-inline-comment-undo',
       ),
       pht('Undo'));
 
     $div = phutil_tag(
       'div',
       array(
         'class' => 'differential-inline-undo',
       ),
       array('Changes discarded. ', $link));
 
     return array(
       'l' => phutil_tag('table', array(),
         phutil_tag('tr', array(), array(
           phutil_tag('th', array()),
           phutil_tag('td', array(), $div),
           phutil_tag('th', array()),
           phutil_tag('td', array('colspan' => 3)),
         ))),
 
       'r' => phutil_tag('table', array(),
         phutil_tag('tr', array(), array(
           phutil_tag('th', array()),
           phutil_tag('td', array()),
           phutil_tag('th', array()),
           phutil_tag('td', array('colspan' => 3), $div),
         ))),
     );
   }
 
   private function renderViewOptionsDropdown(
     DifferentialChangesetDetailView $detail,
     $ref,
     DifferentialChangeset $changeset) {
 
     $meta = array();
 
     $qparams = array(
       'ref'         => $ref,
       'whitespace'  => $this->whitespace,
     );
 
     if ($this->standaloneURI) {
       $uri = new PhutilURI($this->standaloneURI);
       $uri->setQueryParams($uri->getQueryParams() + $qparams);
       $meta['standaloneURI'] = (string)$uri;
     }
 
     $repository = $this->repository;
     if ($repository) {
       try {
         $meta['diffusionURI'] =
           (string)$repository->getDiffusionBrowseURIForPath(
             $this->user,
             $changeset->getAbsoluteRepositoryPath($repository, $this->diff),
             idx($changeset->getMetadata(), 'line:first'),
             $this->getBranch());
       } catch (DiffusionSetupException $e) {
         // Ignore
       }
     }
 
     $change = $changeset->getChangeType();
 
     if ($this->leftRawFileURI) {
       if ($change != DifferentialChangeType::TYPE_ADD) {
         $uri = new PhutilURI($this->leftRawFileURI);
         $uri->setQueryParams($uri->getQueryParams() + $qparams);
         $meta['leftURI'] = (string)$uri;
       }
     }
 
     if ($this->rightRawFileURI) {
       if ($change != DifferentialChangeType::TYPE_DELETE &&
           $change != DifferentialChangeType::TYPE_MULTICOPY) {
         $uri = new PhutilURI($this->rightRawFileURI);
         $uri->setQueryParams($uri->getQueryParams() + $qparams);
         $meta['rightURI'] = (string)$uri;
       }
     }
 
     $user = $this->user;
     if ($user && $repository) {
       $path = ltrim(
         $changeset->getAbsoluteRepositoryPath($repository, $this->diff),
         '/');
       $line = idx($changeset->getMetadata(), 'line:first', 1);
       $callsign = $repository->getCallsign();
       $editor_link = $user->loadEditorLink($path, $line, $callsign);
       if ($editor_link) {
         $meta['editor'] = $editor_link;
       } else {
         $meta['editorConfigure'] = '/settings/panel/display/';
       }
     }
 
     $meta['containerID'] = $detail->getID();
     $caret = phutil_tag('span', array('class' => 'caret'), '');
 
     return javelin_tag(
       'a',
       array(
         'class'   => 'button grey small dropdown',
         'meta'    => $meta,
         'href'    => idx($meta, 'detailURI', '#'),
         'target'  => '_blank',
         'sigil'   => 'differential-view-options',
       ),
       array(pht('View Options'), $caret));
   }
 
 }
diff --git a/src/applications/phriction/controller/PhrictionDiffController.php b/src/applications/phriction/controller/PhrictionDiffController.php
index dea82dcc0..7c4dbc0bc 100644
--- a/src/applications/phriction/controller/PhrictionDiffController.php
+++ b/src/applications/phriction/controller/PhrictionDiffController.php
@@ -1,306 +1,304 @@
 <?php
 
 final class PhrictionDiffController extends PhrictionController {
 
   private $id;
 
   public function shouldAllowPublic() {
     return true;
   }
 
   public function willProcessRequest(array $data) {
     $this->id = $data['id'];
   }
 
   public function processRequest() {
     $request = $this->getRequest();
     $user = $request->getUser();
 
     $document = id(new PhrictionDocumentQuery())
       ->setViewer($user)
       ->withIDs(array($this->id))
       ->needContent(true)
       ->executeOne();
     if (!$document) {
       return new Aphront404Response();
     }
 
     $current = $document->getContent();
 
     $l = $request->getInt('l');
     $r = $request->getInt('r');
 
     $ref = $request->getStr('ref');
     if ($ref) {
       list($l, $r) = explode(',', $ref);
     }
 
     $content = id(new PhrictionContent())->loadAllWhere(
       'documentID = %d AND version IN (%Ld)',
       $document->getID(),
       array($l, $r));
     $content = mpull($content, null, 'getVersion');
 
     $content_l = idx($content, $l, null);
     $content_r = idx($content, $r, null);
 
     if (!$content_l || !$content_r) {
       return new Aphront404Response();
     }
 
     $text_l = $content_l->getContent();
     $text_r = $content_r->getContent();
 
     $text_l = phutil_utf8_hard_wrap($text_l, 80);
     $text_l = implode("\n", $text_l);
     $text_r = phutil_utf8_hard_wrap($text_r, 80);
     $text_r = implode("\n", $text_r);
 
     $engine = new PhabricatorDifferenceEngine();
     $changeset = $engine->generateChangesetFromFileContent($text_l, $text_r);
 
     $changeset->setFilename($content_r->getTitle());
 
     $changeset->setOldProperties(
       array(
         'Title'   => $content_l->getTitle(),
       ));
     $changeset->setNewProperties(
       array(
         'Title'   => $content_r->getTitle(),
       ));
 
     $whitespace_mode = DifferentialChangesetParser::WHITESPACE_SHOW_ALL;
 
     $parser = new DifferentialChangesetParser();
     $parser->setUser($user);
     $parser->setChangeset($changeset);
     $parser->setRenderingReference("{$l},{$r}");
     $parser->setWhitespaceMode($whitespace_mode);
 
     $engine = new PhabricatorMarkupEngine();
     $engine->setViewer($user);
     $engine->process();
     $parser->setMarkupEngine($engine);
 
     $spec = $request->getStr('range');
     list($range_s, $range_e, $mask) =
       DifferentialChangesetParser::parseRangeSpecification($spec);
 
     $output = $parser->render($range_s, $range_e, $mask);
 
     if ($request->isAjax()) {
       return id(new PhabricatorChangesetResponse())
         ->setRenderedChangeset($output);
     }
 
     $output = id(new DifferentialChangesetDetailView())
       ->setUser($this->getViewer())
       ->setChangeset($changeset)
+      ->setRenderingRef("{$l},{$r}")
+      ->setRenderURI('/phriction/diff/'.$document->getID().'/')
       ->appendChild($output);
 
     require_celerity_resource('differential-changeset-view-css');
     require_celerity_resource('syntax-highlighting-css');
     require_celerity_resource('phriction-document-css');
 
     Javelin::initBehavior('differential-populate', array(
       'changesetViewIDs' => array($output->getID()),
     ));
-
-    Javelin::initBehavior('differential-show-more', array(
-      'uri'         => '/phriction/diff/'.$document->getID().'/',
-      'whitespace'  => $whitespace_mode,
-    ));
+    Javelin::initBehavior('differential-show-more');
 
     $slug = $document->getSlug();
 
     $revert_l = $this->renderRevertButton($content_l, $current);
     $revert_r = $this->renderRevertButton($content_r, $current);
 
     $crumbs = $this->buildApplicationCrumbs();
     $crumb_views = $this->renderBreadcrumbs($slug);
     foreach ($crumb_views as $view) {
       $crumbs->addCrumb($view);
     }
 
     $crumbs->addTextCrumb(
       pht('History'),
       PhrictionDocument::getSlugURI($slug, 'history'));
 
     $title = pht('Version %s vs %s', $l, $r);
 
     $header = id(new PHUIHeaderView())
       ->setHeader($title);
 
     $crumbs->addTextCrumb($title, $request->getRequestURI());
 
 
     $comparison_table = $this->renderComparisonTable(
       array(
         $content_r,
         $content_l,
       ));
 
     $navigation_table = null;
     if ($l + 1 == $r) {
       $nav_l = ($l > 1);
       $nav_r = ($r != $current->getVersion());
 
       $uri = $request->getRequestURI();
 
       if ($nav_l) {
         $link_l = phutil_tag(
           'a',
           array(
             'href' => $uri->alter('l', $l - 1)->alter('r', $r - 1),
             'class' => 'button',
           ),
           pht("\xC2\xAB Previous Change"));
       } else {
         $link_l = phutil_tag(
           'a',
           array(
             'href' => '#',
             'class' => 'button grey disabled',
           ),
           pht('Original Change'));
       }
 
       $link_r = null;
       if ($nav_r) {
         $link_r = phutil_tag(
           'a',
           array(
             'href' => $uri->alter('l', $l + 1)->alter('r', $r + 1),
             'class' => 'button',
           ),
           pht("Next Change \xC2\xBB"));
       } else {
         $link_r = phutil_tag(
           'a',
           array(
             'href' => '#',
             'class' => 'button grey disabled',
           ),
           pht('Most Recent Change'));
       }
 
       $navigation_table = phutil_tag(
         'table',
         array('class' => 'phriction-history-nav-table'),
         phutil_tag('tr', array(), array(
           phutil_tag('td', array('class' => 'nav-prev'), $link_l),
           phutil_tag('td', array('class' => 'nav-next'), $link_r),
         )));
     }
 
 
     $output = hsprintf(
       '<div class="phriction-document-history-diff">'.
         '%s%s'.
         '<table class="phriction-revert-table">'.
           '<tr><td>%s</td><td>%s</td>'.
         '</table>'.
         '%s'.
       '</div>',
       $comparison_table->render(),
       $navigation_table,
       $revert_l,
       $revert_r,
       $output);
 
 
     $object_box = id(new PHUIObjectBoxView())
       ->setHeader($header)
       ->appendChild($output);
 
     return $this->buildApplicationPage(
       array(
         $crumbs,
         $object_box,
       ),
       array(
         'title'     => pht('Document History'),
       ));
 
   }
 
   private function renderRevertButton(
     PhrictionContent $content,
     PhrictionContent $current) {
 
     $document_id = $content->getDocumentID();
     $version = $content->getVersion();
 
     $hidden_statuses = array(
       PhrictionChangeType::CHANGE_DELETE    => true, // Silly
       PhrictionChangeType::CHANGE_MOVE_AWAY => true, // Plain silly
       PhrictionChangeType::CHANGE_STUB      => true, // Utterly silly
     );
     if (isset($hidden_statuses[$content->getChangeType()])) {
       // Don't show an edit/revert button for changes which deleted, moved or
       // stubbed the content since it's silly.
       return null;
     }
 
     if ($content->getID() == $current->getID()) {
       return phutil_tag(
         'a',
         array(
           'href'  => '/phriction/edit/'.$document_id.'/',
           'class' => 'button grey',
         ),
         pht('Edit Current Version'));
     }
 
 
     return phutil_tag(
       'a',
       array(
         'href'  => '/phriction/edit/'.$document_id.'/?revert='.$version,
         'class' => 'button grey',
       ),
       pht('Revert to Version %s...', $version));
   }
 
   private function renderComparisonTable(array $content) {
     assert_instances_of($content, 'PhrictionContent');
 
     $user = $this->getRequest()->getUser();
 
     $phids = mpull($content, 'getAuthorPHID');
     $handles = $this->loadViewerHandles($phids);
 
     $list = new PHUIObjectItemListView();
     $list->setFlush(true);
 
     $first = true;
     foreach ($content as $c) {
       $author = $handles[$c->getAuthorPHID()]->renderLink();
       $item = id(new PHUIObjectItemView())
         ->setHeader(pht('%s by %s, %s',
           PhrictionChangeType::getChangeTypeLabel($c->getChangeType()),
           $author,
           pht('Version %s', $c->getVersion())))
         ->addAttribute(pht('%s %s',
           phabricator_date($c->getDateCreated(), $user),
           phabricator_time($c->getDateCreated(), $user)));
 
       if ($c->getDescription()) {
         $item->addAttribute($c->getDescription());
       }
 
       if ($first == true) {
         $item->setBarColor('green');
         $first = false;
       } else {
         $item->setBarColor('red');
       }
 
       $list->addItem($item);
     }
 
     return $list;
   }
 
 }
diff --git a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php
index a2ffe7863..31881310d 100644
--- a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php
+++ b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php
@@ -1,973 +1,973 @@
 <?php
 
 final class PhabricatorUSEnglishTranslation
   extends PhutilTranslation {
 
   public function getLocaleCode() {
     return 'en_US';
   }
 
   protected function getTranslations() {
     return array(
       'No daemon(s) with id(s) "%s" exist!' => array(
         'No daemon with id %s exists!',
         'No daemons with ids %s exist!',
       ),
       'These %d configuration value(s) are related:' => array(
         'This configuration value is related:',
         'These configuration values are related:',
       ),
       '%s Task(s)' => array('Task', 'Tasks'),
 
       '%s ERROR(S)' => array('ERROR', 'ERRORS'),
       '%d Error(s)' => array('%d Error', '%d Errors'),
       '%d Warning(s)' => array('%d Warning', '%d Warnings'),
       '%d Auto-Fix(es)' => array('%d Auto-Fix', '%d Auto-Fixes'),
       '%d Advice(s)' => array('%d Advice', '%d Pieces of Advice'),
       '%d Detail(s)' => array('%d Detail', '%d Details'),
 
       '(%d line(s))' => array('(%d line)', '(%d lines)'),
 
       '%d line(s)' => array('%d line', '%d lines'),
       '%d path(s)' => array('%d path', '%d paths'),
       '%d diff(s)' => array('%d diff', '%d diffs'),
 
       '%s DIFF LINK(S)' => array('DIFF LINK', 'DIFF LINKS'),
       'You successfully created %d diff(s).' => array(
         'You successfully created %d diff.',
         'You successfully created %d diffs.',
       ),
       'Diff creation failed; see body for %s error(s).' => array(
         'Diff creation failed; see body for error.',
         'Diff creation failed; see body for errors.',
       ),
 
       'There are %d raw fact(s) in storage.' => array(
         'There is %d raw fact in storage.',
         'There are %d raw facts in storage.',
       ),
 
       'There are %d aggregate fact(s) in storage.' => array(
         'There is %d aggregate fact in storage.',
         'There are %d aggregate facts in storage.',
       ),
 
       '%d Commit(s) Awaiting Audit' => array(
         '%d Commit Awaiting Audit',
         '%d Commits Awaiting Audit',
       ),
 
       '%d Problem Commit(s)' => array(
         '%d Problem Commit',
         '%d Problem Commits',
       ),
 
       '%d Review(s) Blocking Others' => array(
         '%d Review Blocking Others',
         '%d Reviews Blocking Others',
       ),
 
       '%d Review(s) Need Attention' => array(
         '%d Review Needs Attention',
         '%d Reviews Need Attention',
       ),
 
       '%d Review(s) Waiting on Others' => array(
         '%d Review Waiting on Others',
         '%d Reviews Waiting on Others',
       ),
 
       '%d Active Review(s)' => array(
         '%d Active Review',
         '%d Active Reviews',
       ),
 
       '%d Flagged Object(s)' => array(
         '%d Flagged Object',
         '%d Flagged Objects',
       ),
 
       '%d Object(s) Tracked' => array(
         '%d Object Tracked',
         '%d Objects Tracked',
       ),
 
       '%d Assigned Task(s)' => array(
         '%d Assigned Task',
         '%d Assigned Tasks',
       ),
 
       'Show %d Lint Message(s)' => array(
         'Show %d Lint Message',
         'Show %d Lint Messages',
       ),
       'Hide %d Lint Message(s)' => array(
         'Hide %d Lint Message',
         'Hide %d Lint Messages',
       ),
 
       'This is a binary file. It is %s byte(s) in length.' => array(
         'This is a binary file. It is %s byte in length.',
         'This is a binary file. It is %s bytes in length.',
       ),
 
       '%d Action(s) Have No Effect' => array(
         'Action Has No Effect',
         'Actions Have No Effect',
       ),
 
       '%d Action(s) With No Effect' => array(
         'Action With No Effect',
         'Actions With No Effect',
       ),
 
       'Some of your %d action(s) have no effect:' => array(
         'One of your actions has no effect:',
         'Some of your actions have no effect:',
       ),
 
       'Apply remaining %d action(s)?' => array(
         'Apply remaining action?',
         'Apply remaining actions?',
       ),
 
       'Apply %d Other Action(s)' => array(
         'Apply Remaining Action',
         'Apply Remaining Actions',
       ),
 
       'The %d action(s) you are taking have no effect:' => array(
         'The action you are taking has no effect:',
         'The actions you are taking have no effect:',
       ),
 
       '%s edited member(s), added %d: %s; removed %d: %s.' =>
         '%s edited members, added: %3$s; removed: %5$s.',
 
       '%s added %s member(s): %s.' => array(
         array(
           '%s added a member: %3$s.',
           '%s added members: %3$s.',
         ),
       ),
 
       '%s removed %s member(s): %s.' => array(
         array(
           '%s removed a member: %3$s.',
           '%s removed members: %3$s.',
         ),
       ),
 
       '%s edited project(s), added %s: %s; removed %s: %s.' =>
         '%s edited projects, added: %3$s; removed: %5$s.',
 
       '%s added %s project(s): %s.' => array(
         array(
           '%s added a project: %3$s.',
           '%s added projects: %3$s.',
         ),
       ),
 
       '%s removed %s project(s): %s.' => array(
         array(
           '%s removed a project: %3$s.',
           '%s removed projects: %3$s.',
         ),
       ),
 
       '%s merged %d task(s): %s.' => array(
         array(
           '%s merged a task: %3$s.',
           '%s merged tasks: %3$s.',
         ),
       ),
 
       '%s merged %d task(s) %s into %s.' => array(
         array(
           '%s merged %3$s into %4$s.',
           '%s merged tasks %3$s into %4$s.',
         ),
       ),
 
       '%s added %s voting user(s): %s.' => array(
         array(
           '%s added a voting user: %3$s.',
           '%s added voting users: %3$s.',
         ),
       ),
 
       '%s removed %s voting user(s): %s.' => array(
         array(
           '%s removed a voting user: %3$s.',
           '%s removed voting users: %3$s.',
         ),
       ),
 
       '%s added %s blocking task(s): %s.' => array(
         array(
           '%s added a blocking task: %3$s.',
           '%s added blocking tasks: %3$s.',
         ),
       ),
 
       '%s added %s blocked task(s): %s.' => array(
         array(
           '%s added a blocked task: %3$s.',
           '%s added blocked tasks: %3$s.',
         ),
       ),
 
       '%s removed %s blocking task(s): %s.' => array(
         array(
           '%s removed a blocking task: %3$s.',
           '%s removed blocking tasks: %3$s.',
         ),
       ),
 
       '%s removed %s blocked task(s): %s.' => array(
         array(
           '%s removed a blocked task: %3$s.',
           '%s removed blocked tasks: %3$s.',
         ),
       ),
 
       '%s added %s blocking task(s) for %s: %s.' => array(
         array(
           '%s added a blocking task for %3$s: %4$s.',
           '%s added blocking tasks for %3$s: %4$s.',
         ),
       ),
 
       '%s added %s blocked task(s) for %s: %s.' => array(
         array(
           '%s added a blocked task for %3$s: %4$s.',
           '%s added blocked tasks for %3$s: %4$s.',
         ),
       ),
 
       '%s removed %s blocking task(s) for %s: %s.' => array(
         array(
           '%s removed a blocking task for %3$s: %4$s.',
           '%s removed blocking tasks for %3$s: %4$s.',
         ),
       ),
 
       '%s removed %s blocked task(s) for %s: %s.' => array(
         array(
           '%s removed a blocked task for %3$s: %4$s.',
           '%s removed blocked tasks for %3$s: %4$s.',
         ),
       ),
 
       '%s edited blocking task(s), added %s: %s; removed %s: %s.' =>
         '%s edited blocking tasks, added: %3$s; removed: %5$s.',
 
       '%s edited blocking task(s) for %s, added %s: %s; removed %s: %s.' =>
         '%s edited blocking tasks for %s, added: %4$s; removed: %6$s.',
 
       '%s edited blocked task(s), added %s: %s; removed %s: %s.' =>
         '%s edited blocked tasks, added: %3$s; removed: %5$s.',
 
       '%s edited blocked task(s) for %s, added %s: %s; removed %s: %s.' =>
         '%s edited blocked tasks for %s, added: %4$s; removed: %6$s.',
 
       '%s edited answer(s), added %s: %s; removed %d: %s.' =>
         '%s edited answers, added: %3$s; removed: %5$s.',
 
       '%s added %s answer(s): %s.' => array(
         array(
           '%s added an answer: %3$s.',
           '%s added answers: %3$s.',
         ),
       ),
 
       '%s removed %s answer(s): %s.' => array(
         array(
           '%s removed a answer: %3$s.',
           '%s removed answers: %3$s.',
         ),
       ),
 
      '%s edited question(s), added %s: %s; removed %s: %s.' =>
         '%s edited questions, added: %3$s; removed: %5$s.',
 
       '%s added %s question(s): %s.' => array(
         array(
           '%s added a question: %3$s.',
           '%s added questions: %3$s.',
         ),
       ),
 
       '%s removed %s question(s): %s.' => array(
         array(
           '%s removed a question: %3$s.',
           '%s removed questions: %3$s.',
         ),
       ),
 
       '%s edited mock(s), added %s: %s; removed %s: %s.' =>
         '%s edited mocks, added: %3$s; removed: %5$s.',
 
       '%s added %s mock(s): %s.' => array(
         array(
           '%s added a mock: %3$s.',
           '%s added mocks: %3$s.',
         ),
       ),
 
       '%s removed %s mock(s): %s.' => array(
         array(
           '%s removed a mock: %3$s.',
           '%s removed mocks: %3$s.',
         ),
       ),
 
       '%s added %s task(s): %s.' => array(
         array(
           '%s added a task: %3$s.',
           '%s added tasks: %3$s.',
         ),
       ),
 
       '%s removed %s task(s): %s.' => array(
         array(
           '%s removed a task: %3$s.',
           '%s removed tasks: %3$s.',
         ),
       ),
 
       '%s edited file(s), added %s: %s; removed %s: %s.' =>
         '%s edited files, added: %3$s; removed: %5$s.',
 
       '%s added %s file(s): %s.' => array(
         array(
           '%s added a file: %3$s.',
           '%s added files: %3$s.',
         ),
       ),
 
       '%s removed %s file(s): %s.' => array(
         array(
           '%s removed a file: %3$s.',
           '%s removed files: %3$s.',
         ),
       ),
 
       '%s edited contributor(s), added %s: %s; removed %s: %s.' =>
         '%s edited contributors, added: %3$s; removed: %5$s.',
 
       '%s added %s contributor(s): %s.' => array(
         array(
           '%s added a contributor: %3$s.',
           '%s added contributors: %3$s.',
         ),
       ),
 
       '%s removed %s contributor(s): %s.' => array(
         array(
           '%s removed a contributor: %3$s.',
           '%s removed contributors: %3$s.',
         ),
       ),
 
       '%s edited %s reviewer(s), added %s: %s; removed %s: %s.' =>
         '%s edited reviewers, added: %4$s; removed: %6$s.',
 
       '%s edited %s reviewer(s) for %s, added %s: %s; removed %s: %s.' =>
         '%s edited reviewers for %3$s, added: %5$s; removed: %7$s.',
 
       '%s added %s reviewer(s): %s.' => array(
         array(
           '%s added a reviewer: %3$s.',
           '%s added reviewers: %3$s.',
         ),
       ),
 
       '%s removed %s reviewer(s): %s.' => array(
         array(
           '%s removed a reviewer: %3$s.',
           '%s removed reviewers: %3$s.',
         ),
       ),
 
       '%d other(s)' => array(
         '1 other',
         '%d others',
       ),
 
       '%s edited subscriber(s), added %d: %s; removed %d: %s.' =>
         '%s edited subscribers, added: %3$s; removed: %5$s.',
 
       '%s added %d subscriber(s): %s.' => array(
         array(
           '%s added a subscriber: %3$s.',
           '%s added subscribers: %3$s.',
         ),
       ),
 
       '%s removed %d subscriber(s): %s.' => array(
         array(
           '%s removed a subscriber: %3$s.',
           '%s removed subscribers: %3$s.',
         ),
       ),
 
       '%s edited watcher(s), added %s: %s; removed %d: %s.' =>
         '%s edited watchers, added: %3$s; removed: %5$s.',
 
       '%s added %s watcher(s): %s.' => array(
         array(
           '%s added a watcher: %3$s.',
           '%s added watchers: %3$s.',
         ),
       ),
 
       '%s removed %s watcher(s): %s.' => array(
         array(
           '%s removed a watcher: %3$s.',
           '%s removed watchers: %3$s.',
         ),
       ),
 
       '%s edited participant(s), added %d: %s; removed %d: %s.' =>
         '%s edited participants, added: %3$s; removed: %5$s.',
 
       '%s added %d participant(s): %s.' => array(
         array(
           '%s added a participant: %3$s.',
           '%s added participants: %3$s.',
         ),
       ),
 
       '%s removed %d participant(s): %s.' => array(
         array(
           '%s removed a participant: %3$s.',
           '%s removed participants: %3$s.',
         ),
       ),
 
       '%s edited image(s), added %d: %s; removed %d: %s.' =>
         '%s edited images, added: %3$s; removed: %5$s',
 
       '%s added %d image(s): %s.' => array(
         array(
           '%s added an image: %3$s.',
           '%s added images: %3$s.',
         ),
       ),
 
       '%s removed %d image(s): %s.' => array(
         array(
           '%s removed an image: %3$s.',
           '%s removed images: %3$s.',
         ),
       ),
 
       '%s Line(s)' => array(
         '%s Line',
         '%s Lines',
       ),
 
       'Indexing %d object(s) of type %s.' => array(
         'Indexing %d object of type %s.',
         'Indexing %d object of type %s.',
       ),
 
       'Run these %d command(s):' => array(
         'Run this command:',
         'Run these commands:',
       ),
 
       'Install these %d PHP extension(s):' => array(
         'Install this PHP extension:',
         'Install these PHP extensions:',
       ),
 
       'The current Phabricator configuration has these %d value(s):' => array(
         'The current Phabricator configuration has this value:',
         'The current Phabricator configuration has these values:',
       ),
 
       'The current MySQL configuration has these %d value(s):' => array(
         'The current MySQL configuration has this value:',
         'The current MySQL configuration has these values:',
       ),
 
       'You can update these %d value(s) here:' => array(
         'You can update this value here:',
         'You can update these values here:',
       ),
 
       'The current PHP configuration has these %d value(s):' => array(
         'The current PHP configuration has this value:',
         'The current PHP configuration has these values:',
       ),
 
       'To update these %d value(s), edit your PHP configuration file.' => array(
         'To update this %d value, edit your PHP configuration file.',
         'To update these %d values, edit your PHP configuration file.',
       ),
 
       'To update these %d value(s), edit your PHP configuration file, located '.
       'here:' => array(
         'To update this value, edit your PHP configuration file, located '.
         'here:',
         'To update these values, edit your PHP configuration file, located '.
         'here:',
       ),
 
       'PHP also loaded these %s configuration file(s):' => array(
         'PHP also loaded this configuration file:',
         'PHP also loaded these configuration files:',
       ),
 
       'You have %d unresolved setup issue(s)...' => array(
         'You have an unresolved setup issue...',
         'You have %d unresolved setup issues...',
       ),
 
       '%s added %d inline comment(s).' => array(
         array(
           '%s added an inline comment.',
           '%s added inline comments.',
         ),
       ),
 
       '%d comment(s)' => array('%d comment', '%d comments'),
       '%d rejection(s)' => array('%d rejection', '%d rejections'),
       '%d update(s)' => array('%d update', '%d updates'),
 
       'This configuration value is defined in these %d '.
       'configuration source(s): %s.' => array(
         'This configuration value is defined in this '.
         'configuration source: %2$s.',
         'This configuration value is defined in these %d '.
         'configuration sources: %s.',
       ),
 
       '%d Open Pull Request(s)' => array(
         '%d Open Pull Request',
         '%d Open Pull Requests',
       ),
 
       'Stale (%s day(s))' => array(
         'Stale (%s day)',
         'Stale (%s days)',
       ),
 
       'Old (%s day(s))' => array(
         'Old (%s day)',
         'Old (%s days)',
       ),
 
       '%s Commit(s)' => array(
         '%s Commit',
         '%s Commits',
       ),
 
       '%s attached %d file(s): %s.' => array(
         array(
           '%s attached a file: %3$s.',
           '%s attached files: %3$s.',
         ),
       ),
 
       '%s detached %d file(s): %s.' => array(
         array(
           '%s detached a file: %3$s.',
           '%s detached files: %3$s.',
         ),
       ),
 
       '%s changed file(s), attached %d: %s; detached %d: %s.' =>
         '%s changed files, attached: %3$s; detached: %5$s.',
 
 
       '%s added %s dependencie(s): %s.' => array(
         array(
           '%s added a dependency: %3$s.',
           '%s added dependencies: %3$s.',
         ),
       ),
 
       '%s removed %s dependencie(s): %s.' => array(
         array(
           '%s removed a dependency: %3$s.',
           '%s removed dependencies: %3$s.',
         ),
       ),
 
       '%s added %s dependent revision(s): %s.' => array(
         array(
           '%s added a dependent revision: %3$s.',
           '%s added dependent revisions: %3$s.',
         ),
       ),
 
       '%s removed %s dependent revision(s): %s.' => array(
         array(
           '%s removed a dependent revision: %3$s.',
           '%s removed dependent revisions: %3$s.',
         ),
       ),
 
       '%s added %s commit(s): %s.' => array(
         array(
           '%s added a commit: %3$s.',
           '%s added commits: %3$s.',
         ),
       ),
 
       '%s removed %s commit(s): %s.' => array(
         array(
           '%s removed a commit: %3$s.',
           '%s removed commits: %3$s.',
         ),
       ),
 
       '%s edited commit(s), added %s: %s; removed %s: %s.' =>
         '%s edited commits, added %3$s; removed %5$s.',
 
       '%s added %s reverted commit(s): %s.' => array(
         array(
           '%s added a reverted commit: %3$s.',
           '%s added reverted commits: %3$s.',
         ),
       ),
 
       '%s removed %s reverted commit(s): %s.' => array(
         array(
           '%s removed a reverted commit: %3$s.',
           '%s removed reverted commits: %3$s.',
         ),
       ),
 
       '%s edited reverted commit(s), added %s: %s; removed %s: %s.' =>
         '%s edited reverted commits, added %3$s; removed %5$s.',
 
       '%s added %s reverting commit(s): %s.' => array(
         array(
           '%s added a reverting commit: %3$s.',
           '%s added reverting commits: %3$s.',
         ),
       ),
 
       '%s removed %s reverting commit(s): %s.' => array(
         array(
           '%s removed a reverting commit: %3$s.',
           '%s removed reverting commits: %3$s.',
         ),
       ),
 
       '%s edited reverting commit(s), added %s: %s; removed %s: %s.' =>
         '%s edited reverting commits, added %3$s; removed %5$s.',
 
       '%s changed project member(s), added %d: %s; removed %d: %s.' =>
         '%s changed project members, added %3$s; removed %5$s.',
 
       '%s added %d project member(s): %s.' => array(
         array(
           '%s added a member: %3$s.',
           '%s added members: %3$s.',
         ),
       ),
 
       '%s removed %d project member(s): %s.' => array(
         array(
           '%s removed a member: %3$s.',
           '%s removed members: %3$s.',
         ),
       ),
 
       '%d project hashtag(s) are already used: %s.' => array(
           'Project hashtag %2$s is already used.',
           '%d project hashtags are already used: %2$s.',
       ),
 
       '%s changed project hashtag(s), added %d: %s; removed %d: %s.' =>
         '%s changed project hashtags, added %3$s; removed %5$s.',
 
       '%s added %d project hashtag(s): %s.' => array(
         array(
           '%s added a hashtag: %3$s.',
           '%s added hashtags: %3$s.',
         ),
       ),
 
       '%s removed %d project hashtag(s): %s.' => array(
         array(
           '%s removed a hashtag: %3$s.',
           '%s removed hashtags: %3$s.',
         ),
       ),
 
       '%d User(s) Need Approval' => array(
         '%d User Needs Approval',
         '%d Users Need Approval',
       ),
 
       '%s older changes(s) are hidden.' => array(
         '%d older change is hidden.',
         '%d older changes are hidden.',
       ),
 
       '%s, %s line(s)' => array(
         '%s, %s line',
         '%s, %s lines',
       ),
 
       '%s pushed %d commit(s) to %s.' => array(
         array(
           '%s pushed a commit to %3$s.',
           '%s pushed %d commits to %s.',
         ),
       ),
 
       '%s commit(s)' => array(
         '1 commit',
         '%s commits',
       ),
 
       '%s removed %s JIRA issue(s): %s.' => array(
         array(
           '%s removed a JIRA issue: %3$s.',
           '%s removed JIRA issues: %3$s.',
         ),
       ),
 
       '%s added %s JIRA issue(s): %s.' => array(
         array(
           '%s added a JIRA issue: %3$s.',
           '%s added JIRA issues: %3$s.',
         ),
       ),
 
       '%s added %s required legal document(s): %s.' => array(
         array(
           '%s added a required legal document: %3$s.',
           '%s added required legal documents: %3$s.',
         ),
       ),
 
       '%s updated JIRA issue(s): added %s %s; removed %d %s.' =>
         '%s updated JIRA issues: added %3$s; removed %5$s.',
 
       '%s edited %s task(s), added %s: %s; removed %s: %s.' =>
         '%s edited tasks, added %4$s; removed %6$s.',
 
       '%s added %s task(s) to %s: %s.' => array(
         array(
           '%s added a task to %3$s: %4$s.',
           '%s added tasks to %3$s: %4$s.',
         ),
       ),
 
       '%s removed %s task(s) from %s: %s.' => array(
         array(
           '%s removed a task from %3$s: %4$s.',
           '%s removed tasks from %3$s: %4$s.',
         ),
       ),
 
       '%s edited %s task(s) for %s, added %s: %s; removed %s: %s.' =>
         '%s edited tasks for %3$s, added: %5$s; removed %7$s.',
 
       '%s edited %s commit(s), added %s: %s; removed %s: %s.' =>
         '%s edited commits, added %4$s; removed %6$s.',
 
       '%s added %s commit(s) to %s: %s.' => array(
         array(
           '%s added a commit to %3$s: %4$s.',
           '%s added commits to %3$s: %4$s.',
         ),
       ),
 
       '%s removed %s commit(s) from %s: %s.' => array(
         array(
           '%s removed a commit from %3$s: %4$s.',
           '%s removed commits from %3$s: %4$s.',
         ),
       ),
 
       '%s edited %s commit(s) for %s, added %s: %s; removed %s: %s.' =>
         '%s edited commits for %3$s, added: %5$s; removed %7$s.',
 
       '%s added %s revision(s): %s.' => array(
         array(
           '%s added a revision: %3$s.',
           '%s added revisions: %3$s.',
         ),
       ),
 
       '%s removed %s revision(s): %s.' => array(
         array(
           '%s removed a revision: %3$s.',
           '%s removed revisions: %3$s.',
         ),
       ),
 
       '%s edited %s revision(s), added %s: %s; removed %s: %s.' =>
         '%s edited revisions, added %4$s; removed %6$s.',
 
       '%s added %s revision(s) to %s: %s.' => array(
         array(
           '%s added a revision to %3$s: %4$s.',
           '%s added revisions to %3$s: %4$s.',
         ),
       ),
 
       '%s removed %s revision(s) from %s: %s.' => array(
         array(
           '%s removed a revision from %3$s: %4$s.',
           '%s removed revisions from %3$s: %4$s.',
         ),
       ),
 
       '%s edited %s revision(s) for %s, added %s: %s; removed %s: %s.' =>
         '%s edited revisions for %3$s, added: %5$s; removed %7$s.',
 
       '%s edited %s project(s), added %s: %s; removed %s: %s.' =>
         '%s edited projects, added %4$s; removed %6$s.',
 
       '%s added %s project(s) to %s: %s.' => array(
         array(
           '%s added a project to %3$s: %4$s.',
           '%s added projects to %3$s: %4$s.',
         ),
       ),
 
       '%s removed %s project(s) from %s: %s.' => array(
         array(
           '%s removed a project from %3$s: %4$s.',
           '%s removed projects from %3$s: %4$s.',
         ),
       ),
 
       '%s edited %s project(s) for %s, added %s: %s; removed %s: %s.' =>
         '%s edited projects for %3$s, added: %5$s; removed %7$s.',
 
       '%s added %s panel(s): %s.' => array(
         array(
           '%s added a panel: %3$s.',
           '%s added panels: %3$s.',
         ),
       ),
 
       '%s removed %s panel(s): %s.' => array(
         array(
           '%s removed a panel: %3$s.',
           '%s removed panels: %3$s.',
         ),
       ),
 
       '%s edited %s panel(s), added %s: %s; removed %s: %s.' =>
         '%s edited panels, added %4$s; removed %6$s.',
 
       '%s added %s dashboard(s): %s.' => array(
         array(
           '%s added a dashboard: %3$s.',
           '%s added dashboards: %3$s.',
         ),
       ),
 
       '%s removed %s dashboard(s): %s.' => array(
         array(
           '%s removed a dashboard: %3$s.',
           '%s removed dashboards: %3$s.',
         ),
       ),
 
       '%s edited %s dashboard(s), added %s: %s; removed %s: %s.' =>
         '%s edited dashboards, added %4$s; removed %6$s.',
 
       '%s added %s edge(s): %s.' => array(
         array(
           '%s added an edge: %3$s.',
           '%s added edges: %3$s.',
         ),
       ),
 
       '%s added %s edge(s) to %s: %s.' => array(
         array(
           '%s added an edge to %3$s: %4$s.',
           '%s added edges to %3$s: %4$s.',
         ),
       ),
 
       '%s removed %s edge(s): %s.' => array(
         array(
           '%s removed an edge: %3$s.',
           '%s removed edges: %3$s.',
         ),
       ),
 
       '%s removed %s edge(s) from %s: %s.' => array(
         array(
           '%s removed an edge from %3$s: %4$s.',
           '%s removed edges from %3$s: %4$s.',
         ),
       ),
 
       '%s edited edge(s), added %s: %s; removed %s: %s.' =>
         '%s edited edges, added: %3$s; removed: %5$s.',
 
       '%s edited %s edge(s) for %s, added %s: %s; removed %s: %s.' =>
         '%s edited edges for %3$s, added: %5$s; removed %7$s.',
 
       '%d related link(s):' => array(
         'Related link:',
         'Related links:',
       ),
 
       'You have %d unpaid invoice(s).' => array(
         'You have an unpaid invoice.',
         'You have unpaid invoices.',
       ),
 
       'The configurations differ in the following %s way(s):' => array(
         'The configurations differ:',
         'The configurations differ in these ways:',
       ),
 
       'Phabricator is configured with an email domain whitelist (in %s), so '.
       'only users with a verified email address at one of these %s '.
       'allowed domain(s) will be able to register an account: %s' => array(
         array(
           'Phabricator is configured with an email domain whitelist (in %s), '.
           'so only users with a verified email address at %3$s will be '.
           'allowed to register an account.',
           'Phabricator is configured with an email domain whitelist (in %s), '.
           'so only users with a verified email address at one of these '.
           'allowed domains will be able to register an account: %3$s',
         ),
       ),
 
       'Show First %d Line(s)' => array(
         'Show First Line',
         'Show First %d Lines',
       ),
 
       "\xE2\x96\xB2 Show %d Line(s)" => array(
-        "\xE2\x96\xB2 Show %d Line(s)",
-        "\xE2\x96\xB2 Show %d Line(s)",
+        "\xE2\x96\xB2 Show Line",
+        "\xE2\x96\xB2 Show %d Lines",
       ),
 
       'Show All %d Line(s)' => array(
         'Show Line',
         'Show All %d Lines',
       ),
 
       "\xE2\x96\xBC Show %d Line(s)" => array(
         "\xE2\x96\xBC Show Line",
         "\xE2\x96\xBC Show %d Lines",
       ),
 
       'Show Last %d Line(s)' => array(
         'Show Last Line',
         'Show Last %d Lines',
       ),
 
     );
   }
 
 }
diff --git a/webroot/rsrc/js/application/differential/ChangesetViewManager.js b/webroot/rsrc/js/application/differential/ChangesetViewManager.js
index a18eeda8f..36f097cf4 100644
--- a/webroot/rsrc/js/application/differential/ChangesetViewManager.js
+++ b/webroot/rsrc/js/application/differential/ChangesetViewManager.js
@@ -1,291 +1,365 @@
 /**
  * @provides changeset-view-manager
  * @requires javelin-dom
  *           javelin-util
  *           javelin-stratcom
  *           javelin-install
  *           javelin-workflow
  *           javelin-router
  *           javelin-behavior-device
  *           javelin-vector
  */
 
 
 JX.install('ChangesetViewManager', {
 
   construct : function(node) {
     this._node = node;
 
     var data = this._getNodeData();
     this._renderURI = data.renderURI;
     this._ref = data.ref;
     this._whitespace = data.whitespace;
     this._renderer = data.renderer;
     this._highlight = data.highlight;
     this._encoding = data.encoding;
   },
 
   members: {
     _node: null,
     _loaded: false,
     _sequence: 0,
     _stabilize: false,
 
     _renderURI: null,
     _ref: null,
     _whitespace: null,
     _renderer: null,
     _highlight: null,
     _encoding: null,
 
 
     /**
      * Has the content of this changeset been loaded?
      *
      * This method returns `true` if a request has been fired, even if the
      * response has not returned yet.
      *
      * @return bool True if the content has been loaded.
      */
     isLoaded: function() {
       return this._loaded;
     },
 
 
     /**
      * Configure stabilization of the document position on content load.
      *
      * When we dump the changeset into the document, we can try to stabilize
      * the document scroll position so that the user doesn't feel like they
      * are jumping around as things load in. This is generally useful when
      * populating initial changes.
      *
      * However, if a user explicitly requests a content load by clicking a
      * "Load" link or using the dropdown menu, this stabilization generally
      * feels unnatural, so we don't use it in response to explicit user action.
      *
      * @param bool  True to stabilize the next content fill.
      * @return this
      */
     setStabilize: function(stabilize) {
       this._stabilize = stabilize;
       return this;
     },
 
 
     /**
      * Should this changeset load immediately when the page loads?
      *
      * Normally, changes load immediately, but if a diff or commit is very
      * large we stop doing this and have the user load files explicitly, or
      * choose to load everything.
      *
      * @return bool True if the changeset should load automatically when the
      *   page loads.
      */
     shouldAutoload: function() {
       return this._getNodeData().autoload;
     },
 
 
     /**
      * Load this changeset, if it isn't already loading.
      *
      * This fires a request to fill the content of this changeset, provided
      * there isn't already a request in flight. To force a reload, use
      * @{method:reload}.
      *
      * @return this
      */
     load: function() {
       if (this._loaded) {
         return this;
       }
 
       return this.reload();
     },
 
 
     /**
      * Reload the changeset content.
      *
      * This method always issues a request, even if the content is already
      * loading. To load conditionally, use @{method:load}.
      *
      * @return this
      */
     reload: function() {
       this._loaded = true;
       this._sequence++;
 
-      var params = {
-        ref: this._ref,
-        whitespace: this._whitespace || '',
-        renderer: this.getRenderer() || '',
-        highlight: this._highlight || '',
-        encoding: this._encoding || ''
-      };
+      var params = this._getViewParameters();
 
       var workflow = new JX.Workflow(this._renderURI, params)
         .setHandler(JX.bind(this, this._onresponse, this._sequence));
 
+      this._startContentWorkflow(workflow);
+
+      JX.DOM.setContent(
+        this._getContentFrame(),
+        JX.$N(
+          'div',
+          {className: 'differential-loading'},
+          'Loading...'));
+
+      return this;
+    },
+
+    /**
+     * Load missing context in a changeset.
+     *
+     * We do this when the user clicks "Show X Lines". We also expand all of
+     * the missing context when they "Show Entire File".
+     *
+     * @param string Line range specification, like "0-40/0-20".
+     * @param node Row where the context should be rendered after loading.
+     * @param bool True if this is a bulk load of multiple context blocks.
+     * @return this
+     */
+    loadContext: function(range, target, bulk) {
+      var params = this._getViewParameters();
+      params.range = range;
+
+      var container = JX.DOM.scry(target, 'td')[0];
+      // TODO: pht()
+      JX.DOM.setContent(container, 'Loading...');
+      JX.DOM.alterClass(target, 'differential-show-more-loading', true);
+
+      var workflow = new JX.Workflow(this._renderURI, params)
+        .setHandler(JX.bind(this, this._oncontext, target));
+
+      if (bulk) {
+        // If we're loading a bunch of these because the viewer clicked
+        // "Show Entire File Content" or similar, use lower-priority requests
+        // and draw a progress bar.
+        this._startContentWorkflow(workflow);
+      } else {
+        // If this is a single click on a context link, use a higher priority
+        // load without a chrome change.
+        workflow.start();
+      }
+
+      return this;
+    },
+
+    _startContentWorkflow: function(workflow) {
       var routable = workflow.getRoutable();
 
       routable
         .setPriority(500)
         .setType('content')
         .setKey(this._getRoutableKey());
 
       JX.Router.getInstance().queue(routable);
+    },
 
-      JX.DOM.setContent(
-        this._getContentFrame(),
-        JX.$N(
-          'div',
-          {className: 'differential-loading'},
-          'Loading...'));
 
-      return this;
+    /**
+     * Receive a response to a context request.
+     */
+    _oncontext: function(target, response) {
+      var table = JX.$H(response.changeset).getNode();
+      var root = target.parentNode;
+      this._moveRows(table, root, target);
+      root.removeChild(target);
+    },
+
+    _moveRows: function(src, dst, 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]);
+        }
+      }
     },
 
+    /**
+     * Get parameters which define the current rendering options.
+     */
+    _getViewParameters: function() {
+      return {
+        ref: this._ref,
+        whitespace: this._whitespace || '',
+        renderer: this.getRenderer() || '',
+        highlight: this._highlight || '',
+        encoding: this._encoding || ''
+      };
+    },
 
     /**
      * Get the active @{class:JX.Routable} for this changeset.
      *
      * After issuing a request with @{method:load} or @{method:reload}, you
      * can adjust routable settings (like priority) by querying the routable
      * with this method. Note that there may not be a current routable.
      *
      * @return JX.Routable|null Active routable, if one exists.
      */
     getRoutable: function() {
       return JX.Router.getInstance().getRoutableByKey(this._getRoutableKey());
     },
 
     setRenderer: function(renderer) {
       this._renderer = renderer;
       return this;
     },
 
     getRenderer: function() {
       if (this._renderer !== null) {
         return this._renderer;
       }
 
       // NOTE: If you load the page at one device resolution and then resize to
       // a different one we don't re-render the diffs, because it's a
       // complicated mess and you could lose inline comments, cursor positions,
       // etc.
-      var renderer = (JX.Device.getDevice() == 'desktop') ? '2up' : '1up';
-
-      return renderer;
+      return (JX.Device.getDevice() == 'desktop') ? '2up' : '1up';
     },
 
     setEncoding: function(encoding) {
       this._encoding = encoding;
       return this;
     },
 
     getEncoding: function() {
       return this._encoding;
     },
 
     setHighlight: function(highlight) {
       this._highlight = highlight;
       return this;
     },
 
     getHighlight: function() {
       return this._highlight;
     },
 
     _getNodeData: function() {
       return JX.Stratcom.getData(this._node);
     },
 
 
     _onresponse: function(sequence, response) {
       if (sequence != this._sequence) {
         // If this isn't the most recent request, ignore it. This normally
         // means the user changed view settings between the time the page loaded
         // and the content filled.
         return;
       }
 
       // As we populate the changeset list, we try to hold the document scroll
       // position steady, so that, e.g., users who want to leave a comment on a
       // diff with a large number of changes don't constantly have the text
       // area scrolled off the bottom of the screen until the entire diff loads.
       //
       // There are two three major cases here:
       //
       //  - If we're near the top of the document, never scroll.
       //  - If we're near the bottom of the document, always scroll.
       //  - Otherwise, scroll if the changes were above the midline of the
       //    viewport.
 
       var target = this._node;
 
       var old_pos = JX.Vector.getScroll();
       var old_view = JX.Vector.getViewport();
       var old_dim = JX.Vector.getDocument();
 
       // Number of pixels away from the top or bottom of the document which
       // count as "nearby".
       var sticky = 480;
 
       var near_top = (old_pos.y <= sticky);
       var near_bot = ((old_pos.y + old_view.y) >= (old_dim.y - sticky));
 
       var target_pos = JX.Vector.getPos(target);
       var target_dim = JX.Vector.getDim(target);
       var target_mid = (target_pos.y + (target_dim.y / 2));
 
       var view_mid = (old_pos.y + (old_view.y / 2));
       var above_mid = (target_mid < view_mid);
 
       var frame = this._getContentFrame();
       JX.DOM.setContent(frame, JX.$H(response.changeset));
 
       if (this._stabilize) {
         if (!near_top) {
           if (near_bot || above_mid) {
             // Figure out how much taller the document got.
             var delta = (JX.Vector.getDocument().y - old_dim.y);
             JX.DOM.scrollToPosition(old_pos.x, old_pos.y + delta);
           }
         }
         this._stabilize = false;
       }
 
       if (response.coverage) {
         for (var k in response.coverage) {
           try {
             JX.DOM.replace(JX.$(k), JX.$H(response.coverage[k]));
           } catch (ignored) {
             // Not terribly important.
           }
         }
       }
     },
 
     _getContentFrame: function() {
       return JX.DOM.find(this._node, 'div', 'changeset-view-content');
     },
 
     _getRoutableKey: function() {
       return 'changeset-view.' + this._ref + '.' + this._sequence;
     }
 
   },
 
   statics: {
     getForNode: function(node) {
       var data = JX.Stratcom.getData(node);
       if (!data.changesetViewManager) {
         data.changesetViewManager = new JX.ChangesetViewManager(node);
       }
       return data.changesetViewManager;
     }
   }
 });
diff --git a/webroot/rsrc/js/application/differential/DifferentialInlineCommentEditor.js b/webroot/rsrc/js/application/differential/DifferentialInlineCommentEditor.js
index 7d24af836..d47ec93b1 100644
--- a/webroot/rsrc/js/application/differential/DifferentialInlineCommentEditor.js
+++ b/webroot/rsrc/js/application/differential/DifferentialInlineCommentEditor.js
@@ -1,275 +1,294 @@
 /**
  * @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,
     _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(),
         length : this.getLength(),
         changeset : this.getChangeset(),
         text : this.getText() || ''
       };
     },
     _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]);
         }
       }
     },
     _undo : function() {
       this._removeUndoLink();
 
       this.setText(this._undoText);
       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());
       }
 
       // These operations remove the old row (edit adds a new row first).
       var remove_old = (op == 'edit' || op == 'delete');
       if (remove_old) {
         JX.DOM.remove(this.getRow());
         var other_rows = this.getOtherRows();
         for(var i = 0; i < other_rows.length; ++i) {
           JX.DOM.remove(other_rows[i]);
         }
       }
 
       // Once the user saves something, get rid of the 'undo' option. A
       // particular case where we need this is saving a delete, when we might
       // otherwise leave around an 'undo' for an earlier edit to the same
       // comment.
       this._removeUndoLink();
 
       JX.Stratcom.invoke('differential-inline-comment-update');
       this.invoke('done');
     },
     _didCancelWorkflow : function() {
       this.invoke('done');
 
       var op = this.getOperation();
       if (op == 'delete') {
         // No undo for delete, we prompt the user explicitly.
         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;
 
       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') {
         this._setRowState('loading');
         var oncomplete = JX.bind(this, this._didCompleteWorkflow);
         var onclose = JX.bind(this, function() {
           this._setRowState('visible');
           this._didCancelWorkflow();
         });
 
         new JX.Workflow(this._uri, data)
           .setHandler(oncomplete)
           .setCloseHandler(onclose)
           .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;
     }
   },
 
   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,
     otherRows: [],
     table : null,
     onRight : null,
     ID : null,
     lineNumber : null,
     changeset : null,
     length : null,
     isNew : null,
     text : null,
     templates : null,
     originalText : null
   }
 
 });
diff --git a/webroot/rsrc/js/application/differential/behavior-dropdown-menus.js b/webroot/rsrc/js/application/differential/behavior-dropdown-menus.js
index bc2e015d5..434a057c0 100644
--- a/webroot/rsrc/js/application/differential/behavior-dropdown-menus.js
+++ b/webroot/rsrc/js/application/differential/behavior-dropdown-menus.js
@@ -1,255 +1,254 @@
 /**
  * @provides javelin-behavior-differential-dropdown-menus
  * @requires 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
  */
 
 JX.behavior('differential-dropdown-menus', function(config) {
   var pht = JX.phtize(config.pht);
 
   function show_more(container) {
+    var view = JX.ChangesetViewManager.getForNode(container);
+
     var nodes = JX.DOM.scry(container, 'tr', 'context-target');
     for (var ii = 0; ii < nodes.length; ii++) {
       var show = JX.DOM.scry(nodes[ii], 'a', 'show-more');
       for (var jj = 0; jj < show.length; jj++) {
-        if (JX.Stratcom.getData(show[jj]).type != 'all') {
+        var data = JX.Stratcom.getData(show[jj]);
+        if (data.type != 'all') {
           continue;
         }
-        var event_data = {
-          context : nodes[ii],
-          show : show[jj]
-        };
-        JX.Stratcom.invoke('differential-reveal-context', null, event_data);
+        view.loadContext(data.range, nodes[ii], true);
       }
     }
   }
 
   JX.Stratcom.listen(
     'click',
     'differential-reveal-all',
     function(e) {
       var containers = JX.DOM.scry(
         JX.$('differential-review-stage'),
         'div',
         'differential-changeset');
       for (var i=0; i < containers.length; i++) {
         show_more(containers[i]);
       }
       e.kill();
     });
 
   var buildmenu = function(e) {
     var button = e.getNode('differential-view-options');
     var data = JX.Stratcom.getData(button);
     if (data.menu) {
       return;
     }
 
     e.prevent();
 
     var changeset = JX.DOM.findAbove(
       button,
       'div',
       'differential-changeset');
 
     var view = JX.ChangesetViewManager.getForNode(changeset);
     var menu = new JX.PHUIXDropdownMenu(button);
     var list = new JX.PHUIXActionListView();
 
     var add_link = function(icon, name, href, local) {
       if (!href) {
         return;
       }
 
       var link = new JX.PHUIXActionView()
         .setIcon(icon)
         .setName(name)
         .setHref(href)
         .setHandler(function(e) {
           if (local) {
             window.location.assign(href);
           } else {
             window.open(href);
           }
           menu.close();
           e.prevent();
         });
 
       list.addItem(link);
       return link;
     };
 
     var reveal_item = new JX.PHUIXActionView()
       .setIcon('fa-eye');
     list.addItem(reveal_item);
 
     var visible_item = new JX.PHUIXActionView()
       .setHandler(function(e) {
         var diff = JX.DOM.scry(
           JX.$(data.containerID),
           'table',
           'differential-diff');
 
         JX.Stratcom.invoke('differential-toggle-file', null, {diff: diff});
         e.prevent();
         menu.close();
       });
     list.addItem(visible_item);
 
     add_link('fa-file-text', pht('Browse in Diffusion'), data.diffusionURI);
     add_link('fa-file-o', pht('View Standalone'), data.standaloneURI);
 
     var up_item = new JX.PHUIXActionView()
       .setHandler(function(e) {
         if (view.isLoaded()) {
           var renderer = view.getRenderer();
           if (renderer == '1up') {
             renderer = '2up';
           } else {
             renderer = '1up';
           }
           view.setRenderer(renderer);
         }
         view.reload();
 
         e.prevent();
         menu.close();
       });
     list.addItem(up_item);
 
     var encoding_item = new JX.PHUIXActionView()
       .setIcon('fa-font')
       .setName(pht('Change Text Encoding...'))
       .setHandler(function(e) {
         var params = {
           encoding: view.getEncoding()
         };
 
         new JX.Workflow('/services/encoding/', params)
           .setHandler(function(r) {
             view.setEncoding(r.encoding);
             view.reload();
           })
           .start();
 
         e.prevent();
         menu.close();
       });
     list.addItem(encoding_item);
 
     var highlight_item = new JX.PHUIXActionView()
       .setIcon('fa-sun-o')
       .setName(pht('Highlight As...'))
       .setHandler(function(e) {
         var params = {
           highlight: view.getHighlight()
         };
 
         new JX.Workflow('/services/highlight/', params)
           .setHandler(function(r) {
             view.setHighlight(r.highlight);
             view.reload();
           })
           .start();
 
         e.prevent();
         menu.close();
       });
     list.addItem(highlight_item);
 
     add_link('fa-arrow-left', pht('Show Raw File (Left)'), data.leftURI);
     add_link('fa-arrow-right', pht('Show Raw File (Right)'), data.rightURI);
     add_link('fa-pencil', pht('Open in Editor'), data.editor, true);
     add_link('fa-wrench', pht('Configure Editor'), data.editorConfigure);
 
     menu.setContent(list.getNode());
 
     menu.listen('open', function() {
       // When the user opens the menu, check if there are any "Show More"
       // links in the changeset body. If there aren't, disable the "Show
       // Entire File" menu item since it won't change anything.
 
       var nodes = JX.DOM.scry(JX.$(data.containerID), 'a', 'show-more');
       if (nodes.length) {
         reveal_item
           .setDisabled(false)
           .setName(pht('Show Entire File'))
           .setIcon('fa-file-o')
           .setHandler(function(e) {
             show_more(JX.$(data.containerID));
             e.prevent();
             menu.close();
           });
       } else {
         reveal_item
           .setDisabled(true)
           .setIcon('fa-file')
           .setName(pht('Entire File Shown'))
           .setHandler(function(e) { e.prevent(); });
       }
 
       encoding_item.setDisabled(!view.isLoaded());
       highlight_item.setDisabled(!view.isLoaded());
 
       if (view.isLoaded()) {
         if (view.getRenderer() == '2up') {
           up_item
             .setIcon('fa-list-alt')
             .setName(pht('View Unified'));
         } else {
           up_item
             .setIcon('fa-files-o')
             .setName(pht('View Side-by-Side'));
         }
       } else {
         up_item
           .setIcon('fa-refresh')
           .setName(pht('Load Changes'));
       }
 
       visible_item
         .setDisabled(true)
         .setIcon('fa-expand')
         .setName(pht('Can\'t Toggle Unloaded File'));
       var diffs = JX.DOM.scry(
         JX.$(data.containerID),
         'table',
         'differential-diff');
 
       if (diffs.length > 1) {
         JX.$E(
           'More than one node with sigil "differential-diff" was found in "'+
           data.containerID+'."');
       } else if (diffs.length == 1) {
         var diff = diffs[0];
         visible_item.setDisabled(false);
         if (JX.Stratcom.getData(diff).hidden) {
           visible_item
             .setName(pht('Expand File'))
             .setIcon('fa-expand');
         } else {
           visible_item
             .setName(pht('Collapse File'))
             .setIcon('fa-compress');
         }
       } else {
         // Do nothing when there is no diff shown in the table. For example,
         // the file is binary.
       }
 
     });
 
     data.menu = menu;
     menu.open();
   };
 
   JX.Stratcom.listen('click', 'differential-view-options', buildmenu);
 });
diff --git a/webroot/rsrc/js/application/differential/behavior-show-more.js b/webroot/rsrc/js/application/differential/behavior-show-more.js
index 3d8567454..4ef4964f7 100644
--- a/webroot/rsrc/js/application/differential/behavior-show-more.js
+++ b/webroot/rsrc/js/application/differential/behavior-show-more.js
@@ -1,71 +1,27 @@
 /**
  * @provides javelin-behavior-differential-show-more
  * @requires javelin-behavior
  *           javelin-dom
  *           javelin-workflow
  *           javelin-util
  *           javelin-stratcom
+ *           changeset-view-manager
  */
 
-JX.behavior('differential-show-more', function(config) {
-
-  function onresponse(context, response) {
-    var table = JX.$H(response.changeset).getNode();
-    var root = context.parentNode;
-    copyRows(root, table, context);
-    root.removeChild(context);
-  }
+JX.behavior('differential-show-more', function() {
 
   JX.Stratcom.listen(
     'click',
     'show-more',
     function(e) {
-      var event_data = {
-        context :  e.getNodes()['context-target'],
-        show : e.getNodes()['show-more']
-      };
-
-      JX.Stratcom.invoke('differential-reveal-context', null, event_data);
       e.kill();
-    });
-
-  JX.Stratcom.listen(
-    'differential-reveal-context',
-    null,
-    function(e) {
-      var context = e.getData().context;
-      var data = JX.Stratcom.getData(e.getData().show);
-
-      var container = JX.DOM.scry(context, 'td')[0];
-      JX.DOM.setContent(container, 'Loading...');
-      JX.DOM.alterClass(context, 'differential-show-more-loading', true);
 
-      if (!data.whitespace) {
-        data.whitespace = config.whitespace;
-      }
+      var changeset = e.getNode('differential-changeset');
+      var view = JX.ChangesetViewManager.getForNode(changeset);
+      var data = e.getNodeData('show-more');
+      var target = e.getNode('context-target');
 
-      new JX.Workflow(config.uri, data)
-        .setHandler(JX.bind(null, onresponse, context))
-        .start();
+      view.loadContext(data.range, target);
     });
 
 });
-
-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;
-}