diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php
index 181012c0dc..83f42b1e8f 100644
--- a/src/__celerity_resource_map__.php
+++ b/src/__celerity_resource_map__.php
@@ -1,1904 +1,1905 @@
 <?php
 
 /**
  * This file is automatically generated. Use 'celerity_mapper.php' to rebuild
  * it.
  * @generated
  */
 
 celerity_register_resource_map(array(
   'aphront-attached-file-view-css' =>
   array(
     'uri' => '/res/a6ca5487/rsrc/css/aphront/attached-file-view.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/aphront/attached-file-view.css',
   ),
   'aphront-calendar-view-css' =>
   array(
     'uri' => '/res/c86d9a4b/rsrc/css/aphront/calendar-view.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/aphront/calendar-view.css',
   ),
   'aphront-contextbar-view-css' =>
   array(
     'uri' => '/res/3e2f3045/rsrc/css/aphront/context-bar.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/aphront/context-bar.css',
   ),
   'aphront-crumbs-view-css' =>
   array(
     'uri' => '/res/9009e6bd/rsrc/css/aphront/crumbs-view.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/aphront/crumbs-view.css',
   ),
   'aphront-dark-console-css' =>
   array(
     'uri' => '/res/1a9f84bb/rsrc/css/aphront/dark-console.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/aphront/dark-console.css',
   ),
   'aphront-dialog-view-css' =>
   array(
     'uri' => '/res/1c0a5f75/rsrc/css/aphront/dialog-view.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/aphront/dialog-view.css',
   ),
   'aphront-error-view-css' =>
   array(
     'uri' => '/res/e4c5e4ed/rsrc/css/aphront/error-view.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/aphront/error-view.css',
   ),
   'aphront-form-view-css' =>
   array(
     'uri' => '/res/4d1d9d08/rsrc/css/aphront/form-view.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/aphront/form-view.css',
   ),
   'aphront-headsup-action-list-view-css' =>
   array(
     'uri' => '/res/84743e20/rsrc/css/aphront/headsup-action-list-view.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/aphront/headsup-action-list-view.css',
   ),
   'aphront-list-filter-view-css' =>
   array(
     'uri' => '/res/0f5ddaba/rsrc/css/aphront/list-filter-view.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/aphront/list-filter-view.css',
   ),
   'aphront-pager-view-css' =>
   array(
     'uri' => '/res/43fb79f0/rsrc/css/aphront/pager-view.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/aphront/pager-view.css',
   ),
   'aphront-panel-view-css' =>
   array(
     'uri' => '/res/58da9c70/rsrc/css/aphront/panel-view.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/aphront/panel-view.css',
   ),
   'aphront-request-failure-view-css' =>
   array(
     'uri' => '/res/c9a43002/rsrc/css/aphront/request-failure-view.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/aphront/request-failure-view.css',
   ),
   'aphront-side-nav-view-css' =>
   array(
     'uri' => '/res/42f70a69/rsrc/css/aphront/side-nav-view.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/aphront/side-nav-view.css',
   ),
   'aphront-table-view-css' =>
   array(
     'uri' => '/res/f4f39a2e/rsrc/css/aphront/table-view.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/aphront/table-view.css',
   ),
   'aphront-tokenizer-control-css' =>
   array(
     'uri' => '/res/f530af47/rsrc/css/aphront/tokenizer.css',
     'type' => 'css',
     'requires' =>
     array(
       0 => 'aphront-typeahead-control-css',
     ),
     'disk' => '/rsrc/css/aphront/tokenizer.css',
   ),
   'aphront-typeahead-control-css' =>
   array(
     'uri' => '/res/a05236a6/rsrc/css/aphront/typeahead.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/aphront/typeahead.css',
   ),
   'differential-changeset-view-css' =>
   array(
     'uri' => '/res/bc78a228/rsrc/css/application/differential/changeset-view.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/differential/changeset-view.css',
   ),
   'differential-core-view-css' =>
   array(
     'uri' => '/res/584d40e8/rsrc/css/application/differential/core.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/differential/core.css',
   ),
   'differential-inline-comment-editor' =>
   array(
     'uri' => '/res/ff5f42a9/rsrc/js/application/differential/DifferentialInlineCommentEditor.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-dom',
       1 => 'javelin-workflow',
       2 => 'javelin-util',
       3 => 'javelin-stratcom',
       4 => 'javelin-install',
     ),
     'disk' => '/rsrc/js/application/differential/DifferentialInlineCommentEditor.js',
   ),
   'differential-local-commits-view-css' =>
   array(
     'uri' => '/res/8cdacd82/rsrc/css/application/differential/local-commits-view.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/differential/local-commits-view.css',
   ),
   'differential-revision-add-comment-css' =>
   array(
     'uri' => '/res/849748d3/rsrc/css/application/differential/add-comment.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/differential/add-comment.css',
   ),
   'differential-revision-comment-css' =>
   array(
     'uri' => '/res/e4b350b2/rsrc/css/application/differential/revision-comment.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/differential/revision-comment.css',
   ),
   'differential-revision-comment-list-css' =>
   array(
     'uri' => '/res/3b31faa3/rsrc/css/application/differential/revision-comment-list.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/differential/revision-comment-list.css',
   ),
   'differential-revision-detail-css' =>
   array(
     'uri' => '/res/33592453/rsrc/css/application/differential/revision-detail.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/differential/revision-detail.css',
   ),
   'differential-revision-history-css' =>
   array(
     'uri' => '/res/0d7d515d/rsrc/css/application/differential/revision-history.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/differential/revision-history.css',
   ),
   'differential-table-of-contents-css' =>
   array(
     'uri' => '/res/d173445b/rsrc/css/application/differential/table-of-contents.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/differential/table-of-contents.css',
   ),
   'diffusion-commit-view-css' =>
   array(
     'uri' => '/res/bc39d876/rsrc/css/application/diffusion/commit-view.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/diffusion/commit-view.css',
   ),
   'diffusion-source-css' =>
   array(
     'uri' => '/res/db4566b6/rsrc/css/application/diffusion/diffusion-source.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/diffusion/diffusion-source.css',
   ),
   'files-css' =>
   array(
     'uri' => '/res/a265a77d/rsrc/css/application/files/files.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/files/files.css',
   ),
   'herald-css' =>
   array(
     'uri' => '/res/ed5556e6/rsrc/css/application/herald/herald.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/herald/herald.css',
   ),
   'herald-rule-editor' =>
   array(
     'uri' => '/res/ec9eea63/rsrc/js/application/herald/HeraldRuleEditor.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'multirow-row-manager',
       1 => 'javelin-install',
       2 => 'javelin-typeahead',
       3 => 'javelin-util',
       4 => 'javelin-dom',
       5 => 'javelin-tokenizer',
       6 => 'javelin-typeahead-preloaded-source',
       7 => 'javelin-stratcom',
       8 => 'javelin-json',
       9 => 'phabricator-prefab',
     ),
     'disk' => '/rsrc/js/application/herald/HeraldRuleEditor.js',
   ),
   'herald-test-css' =>
   array(
     'uri' => '/res/c0cd6bdb/rsrc/css/application/herald/herald-test.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/herald/herald-test.css',
   ),
   'javelin-behavior' =>
   array(
     'uri' => '/res/0017f840/rsrc/js/javelin/lib/behavior.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-magical-init',
     ),
     'disk' => '/rsrc/js/javelin/lib/behavior.js',
   ),
-  0 =>
-  array(
-    'uri' => '/res/14c48a9f/rsrc/js/javelin/lib/__tests__/behavior.js',
-    'type' => 'js',
-    'requires' =>
-    array(
-      0 => 'javelin-behavior',
-    ),
-    'disk' => '/rsrc/js/javelin/lib/__tests__/behavior.js',
-  ),
   'javelin-behavior-aphront-basic-tokenizer' =>
   array(
     'uri' => '/res/9be30797/rsrc/js/application/core/behavior-tokenizer.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-typeahead',
       2 => 'javelin-tokenizer',
       3 => 'javelin-typeahead-preloaded-source',
       4 => 'javelin-typeahead-ondemand-source',
       5 => 'javelin-dom',
       6 => 'javelin-stratcom',
     ),
     'disk' => '/rsrc/js/application/core/behavior-tokenizer.js',
   ),
   'javelin-behavior-aphront-drag-and-drop' =>
   array(
     'uri' => '/res/ac21045a/rsrc/js/application/core/behavior-drag-and-drop.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-dom',
       2 => 'javelin-util',
       3 => 'phabricator-drag-and-drop-file-upload',
     ),
     'disk' => '/rsrc/js/application/core/behavior-drag-and-drop.js',
   ),
   'javelin-behavior-aphront-drag-and-drop-textarea' =>
   array(
     'uri' => '/res/fa7527f9/rsrc/js/application/core/behavior-drag-and-drop-textarea.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-dom',
       2 => 'phabricator-drag-and-drop-file-upload',
     ),
     'disk' => '/rsrc/js/application/core/behavior-drag-and-drop-textarea.js',
   ),
   'javelin-behavior-aphront-form-disable-on-submit' =>
   array(
     'uri' => '/res/6c659ede/rsrc/js/application/core/behavior-form.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-stratcom',
       2 => 'javelin-dom',
     ),
     'disk' => '/rsrc/js/application/core/behavior-form.js',
   ),
   'javelin-behavior-countdown-timer' =>
   array(
     'uri' => '/res/5ee9cb13/rsrc/js/application/countdown/timer.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-dom',
       2 => 'javelin-util',
     ),
     'disk' => '/rsrc/js/application/countdown/timer.js',
   ),
   'javelin-behavior-dark-console' =>
   array(
     'uri' => '/res/c80156c4/rsrc/js/application/core/behavior-dark-console.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-stratcom',
       2 => 'javelin-util',
       3 => 'javelin-dom',
       4 => 'javelin-request',
       5 => 'phabricator-keyboard-shortcut',
     ),
     'disk' => '/rsrc/js/application/core/behavior-dark-console.js',
   ),
   'javelin-behavior-differential-accept-with-errors' =>
   array(
     'uri' => '/res/41c4685b/rsrc/js/application/differential/behavior-accept-with-errors.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-dom',
     ),
     'disk' => '/rsrc/js/application/differential/behavior-accept-with-errors.js',
   ),
   'javelin-behavior-differential-add-reviewers-and-ccs' =>
   array(
     'uri' => '/res/99e1b311/rsrc/js/application/differential/behavior-add-reviewers-and-ccs.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-dom',
       2 => 'javelin-tokenizer',
       3 => 'javelin-typeahead',
       4 => 'javelin-typeahead-preloaded-source',
     ),
     'disk' => '/rsrc/js/application/differential/behavior-add-reviewers-and-ccs.js',
   ),
   'javelin-behavior-differential-comment-jump' =>
   array(
     'uri' => '/res/be77fced/rsrc/js/application/differential/behavior-comment-jump.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-util',
       2 => 'javelin-dom',
     ),
     'disk' => '/rsrc/js/application/differential/behavior-comment-jump.js',
   ),
   'javelin-behavior-differential-diff-radios' =>
   array(
     'uri' => '/res/004cb66f/rsrc/js/application/differential/behavior-diff-radios.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-stratcom',
       2 => 'javelin-dom',
     ),
     'disk' => '/rsrc/js/application/differential/behavior-diff-radios.js',
   ),
   'javelin-behavior-differential-dropdown-menus' =>
   array(
-    'uri' => '/res/acba60ad/rsrc/js/application/differential/behavior-dropdown-menus.js',
+    'uri' => '/res/7bfb2fdb/rsrc/js/application/differential/behavior-dropdown-menus.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-dom',
       2 => 'javelin-util',
       3 => 'javelin-stratcom',
       4 => 'phabricator-dropdown-menu',
       5 => 'phabricator-menu-item',
     ),
     'disk' => '/rsrc/js/application/differential/behavior-dropdown-menus.js',
   ),
   'javelin-behavior-differential-edit-inline-comments' =>
   array(
     'uri' => '/res/c24338ce/rsrc/js/application/differential/behavior-edit-inline-comments.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-stratcom',
       2 => 'javelin-dom',
       3 => 'javelin-util',
       4 => 'javelin-vector',
       5 => 'differential-inline-comment-editor',
     ),
     'disk' => '/rsrc/js/application/differential/behavior-edit-inline-comments.js',
   ),
   'javelin-behavior-differential-feedback-preview' =>
   array(
     'uri' => '/res/768b60c9/rsrc/js/application/differential/behavior-comment-preview.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-stratcom',
       2 => 'javelin-dom',
       3 => 'javelin-request',
       4 => 'javelin-util',
       5 => 'phabricator-shaped-request',
     ),
     'disk' => '/rsrc/js/application/differential/behavior-comment-preview.js',
   ),
   'javelin-behavior-differential-keyboard-navigation' =>
   array(
     'uri' => '/res/f5ce5987/rsrc/js/application/differential/behavior-keyboard-nav.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-dom',
       2 => 'javelin-stratcom',
       3 => 'phabricator-keyboard-shortcut',
     ),
     'disk' => '/rsrc/js/application/differential/behavior-keyboard-nav.js',
   ),
   'javelin-behavior-differential-populate' =>
   array(
     'uri' => '/res/6efe5cd2/rsrc/js/application/differential/behavior-populate.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-workflow',
       2 => 'javelin-util',
       3 => 'javelin-dom',
     ),
     'disk' => '/rsrc/js/application/differential/behavior-populate.js',
   ),
   'javelin-behavior-differential-show-all-comments' =>
   array(
     'uri' => '/res/bcc990f0/rsrc/js/application/differential/behavior-show-all-comments.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-stratcom',
       2 => 'javelin-dom',
     ),
     'disk' => '/rsrc/js/application/differential/behavior-show-all-comments.js',
   ),
   'javelin-behavior-differential-show-more' =>
   array(
     'uri' => '/res/68a8e485/rsrc/js/application/differential/behavior-show-more.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-dom',
       2 => 'javelin-workflow',
       3 => 'javelin-util',
       4 => 'javelin-stratcom',
     ),
     'disk' => '/rsrc/js/application/differential/behavior-show-more.js',
   ),
   'javelin-behavior-diffusion-jump-to' =>
   array(
     'uri' => '/res/7c42e1ba/rsrc/js/application/diffusion/behavior-jump-to.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-util',
       2 => 'javelin-vector',
       3 => 'javelin-dom',
     ),
     'disk' => '/rsrc/js/application/diffusion/behavior-jump-to.js',
   ),
   'javelin-behavior-diffusion-pull-lastmodified' =>
   array(
     'uri' => '/res/29fe2790/rsrc/js/application/diffusion/behavior-pull-lastmodified.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-dom',
       2 => 'javelin-util',
       3 => 'javelin-request',
     ),
     'disk' => '/rsrc/js/application/diffusion/behavior-pull-lastmodified.js',
   ),
   'javelin-behavior-error-log' =>
   array(
     'uri' => '/res/a5cb42a5/rsrc/js/application/core/behavior-error-log.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-dom',
     ),
     'disk' => '/rsrc/js/application/core/behavior-error-log.js',
   ),
   'javelin-behavior-files-drag-and-drop' =>
   array(
     'uri' => '/res/0e84cc42/rsrc/js/application/core/behavior-files-drag-and-drop.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-dom',
       2 => 'javelin-uri',
       3 => 'phabricator-drag-and-drop-file-upload',
     ),
     'disk' => '/rsrc/js/application/core/behavior-files-drag-and-drop.js',
   ),
   'javelin-behavior-herald-rule-editor' =>
   array(
     'uri' => '/res/77a0c945/rsrc/js/application/herald/herald-rule-editor.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'herald-rule-editor',
       1 => 'javelin-behavior',
     ),
     'disk' => '/rsrc/js/application/herald/herald-rule-editor.js',
   ),
   'javelin-behavior-maniphest-project-create' =>
   array(
     'uri' => '/res/85a0eaf9/rsrc/js/application/maniphest/behavior-project-create.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-dom',
       2 => 'javelin-stratcom',
       3 => 'javelin-workflow',
     ),
     'disk' => '/rsrc/js/application/maniphest/behavior-project-create.js',
   ),
   'javelin-behavior-maniphest-transaction-controls' =>
   array(
     'uri' => '/res/33bd237a/rsrc/js/application/maniphest/behavior-transaction-controls.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-dom',
       2 => 'javelin-tokenizer',
       3 => 'javelin-typeahead',
       4 => 'javelin-typeahead-preloaded-source',
     ),
     'disk' => '/rsrc/js/application/maniphest/behavior-transaction-controls.js',
   ),
   'javelin-behavior-maniphest-transaction-expand' =>
   array(
     'uri' => '/res/966410de/rsrc/js/application/maniphest/behavior-transaction-expand.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-dom',
       2 => 'javelin-workflow',
       3 => 'javelin-stratcom',
     ),
     'disk' => '/rsrc/js/application/maniphest/behavior-transaction-expand.js',
   ),
   'javelin-behavior-maniphest-transaction-preview' =>
   array(
     'uri' => '/res/44e86555/rsrc/js/application/maniphest/behavior-transaction-preview.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-dom',
       2 => 'javelin-util',
       3 => 'phabricator-shaped-request',
     ),
     'disk' => '/rsrc/js/application/maniphest/behavior-transaction-preview.js',
   ),
   'javelin-behavior-owners-path-editor' =>
   array(
     'uri' => '/res/9cf78ffc/rsrc/js/application/owners/owners-path-editor.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'owners-path-editor',
       1 => 'javelin-behavior',
     ),
     'disk' => '/rsrc/js/application/owners/owners-path-editor.js',
   ),
   'javelin-behavior-phabricator-keyboard-pager' =>
   array(
     'uri' => '/res/56d64eff/rsrc/js/application/core/behavior-keyboard-pager.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-uri',
       2 => 'phabricator-keyboard-shortcut',
     ),
     'disk' => '/rsrc/js/application/core/behavior-keyboard-pager.js',
   ),
   'javelin-behavior-phabricator-keyboard-shortcuts' =>
   array(
     'uri' => '/res/94b009e2/rsrc/js/application/core/behavior-keyboard-shortcuts.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-workflow',
       2 => 'javelin-json',
       3 => 'phabricator-keyboard-shortcut',
     ),
     'disk' => '/rsrc/js/application/core/behavior-keyboard-shortcuts.js',
   ),
   'javelin-behavior-phabricator-object-selector' =>
   array(
     'uri' => '/res/1f7867ca/rsrc/js/application/core/behavior-object-selector.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-dom',
       2 => 'javelin-request',
       3 => 'javelin-util',
       4 => 'javelin-stratcom',
     ),
     'disk' => '/rsrc/js/application/core/behavior-object-selector.js',
   ),
   'javelin-behavior-phabricator-watch-anchor' =>
   array(
     'uri' => '/res/880e3de4/rsrc/js/application/core/behavior-watch-anchor.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-stratcom',
       2 => 'javelin-dom',
       3 => 'javelin-vector',
     ),
     'disk' => '/rsrc/js/application/core/behavior-watch-anchor.js',
   ),
   'javelin-behavior-phriction-document-preview' =>
   array(
     'uri' => '/res/f1665ecd/rsrc/js/application/phriction/phriction-document-preview.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-dom',
       2 => 'javelin-util',
       3 => 'phabricator-shaped-request',
     ),
     'disk' => '/rsrc/js/application/phriction/phriction-document-preview.js',
   ),
   'javelin-behavior-projects-resource-editor' =>
   array(
     'uri' => '/res/ffdde7d9/rsrc/js/application/projects/projects-resource-editor.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'phabricator-prefab',
       2 => 'multirow-row-manager',
       3 => 'javelin-tokenizer',
       4 => 'javelin-typeahead-preloaded-source',
       5 => 'javelin-typeahead',
       6 => 'javelin-dom',
       7 => 'javelin-json',
       8 => 'javelin-util',
     ),
     'disk' => '/rsrc/js/application/projects/projects-resource-editor.js',
   ),
   'javelin-behavior-refresh-csrf' =>
   array(
     'uri' => '/res/88beba4c/rsrc/js/application/core/behavior-refresh-csrf.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-request',
       1 => 'javelin-behavior',
       2 => 'javelin-dom',
     ),
     'disk' => '/rsrc/js/application/core/behavior-refresh-csrf.js',
   ),
   'javelin-behavior-repository-crossreference' =>
   array(
     'uri' => '/res/49472f48/rsrc/js/application/repository/repository-crossreference.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-dom',
       2 => 'javelin-uri',
     ),
     'disk' => '/rsrc/js/application/repository/repository-crossreference.js',
   ),
   'javelin-behavior-view-placeholder' =>
   array(
     'uri' => '/res/5b89bdf5/rsrc/js/javelin/ext/view/ViewPlaceholder.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-dom',
       2 => 'javelin-view-renderer',
     ),
     'disk' => '/rsrc/js/javelin/ext/view/ViewPlaceholder.js',
   ),
   'javelin-behavior-workflow' =>
   array(
     'uri' => '/res/079f49c3/rsrc/js/application/core/behavior-workflow.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-behavior',
       1 => 'javelin-stratcom',
       2 => 'javelin-workflow',
     ),
     'disk' => '/rsrc/js/application/core/behavior-workflow.js',
   ),
   'javelin-color' =>
   array(
     'uri' => '/res/b0439fc9/rsrc/js/javelin/ext/fx/Color.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
     ),
     'disk' => '/rsrc/js/javelin/ext/fx/Color.js',
   ),
   'javelin-cookie' =>
   array(
     'uri' => '/res/a9cddab0/rsrc/js/javelin/lib/Cookie.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-util',
     ),
     'disk' => '/rsrc/js/javelin/lib/Cookie.js',
   ),
   'javelin-dom' =>
   array(
     'uri' => '/res/b2e8a5b6/rsrc/js/javelin/lib/DOM.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-magical-init',
       1 => 'javelin-install',
       2 => 'javelin-util',
       3 => 'javelin-vector',
       4 => 'javelin-stratcom',
     ),
     'disk' => '/rsrc/js/javelin/lib/DOM.js',
   ),
   'javelin-dynval' =>
   array(
     'uri' => '/res/d89c6f88/rsrc/js/javelin/ext/reactor/core/DynVal.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-reactornode',
       2 => 'javelin-util',
       3 => 'javelin-reactor',
     ),
     'disk' => '/rsrc/js/javelin/ext/reactor/core/DynVal.js',
   ),
   'javelin-event' =>
   array(
     'uri' => '/res/f42fa6ea/rsrc/js/javelin/core/Event.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
     ),
     'disk' => '/rsrc/js/javelin/core/Event.js',
   ),
   'javelin-fx' =>
   array(
     'uri' => '/res/97e25a7f/rsrc/js/javelin/ext/fx/FX.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-color',
       1 => 'javelin-install',
       2 => 'javelin-util',
     ),
     'disk' => '/rsrc/js/javelin/ext/fx/FX.js',
   ),
   'javelin-history' =>
   array(
     'uri' => '/res/9bb36651/rsrc/js/javelin/lib/History.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-stratcom',
       1 => 'javelin-install',
       2 => 'javelin-uri',
       3 => 'javelin-util',
     ),
     'disk' => '/rsrc/js/javelin/lib/History.js',
   ),
   'javelin-install' =>
   array(
     'uri' => '/res/cab679ff/rsrc/js/javelin/core/install.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-util',
       1 => 'javelin-magical-init',
     ),
     'disk' => '/rsrc/js/javelin/core/install.js',
   ),
   'javelin-json' =>
   array(
     'uri' => '/res/561b8056/rsrc/js/javelin/lib/JSON.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
     ),
     'disk' => '/rsrc/js/javelin/lib/JSON.js',
   ),
   'javelin-magical-init' =>
   array(
     'uri' => '/res/0e72d59b/rsrc/js/javelin/core/init.js',
     'type' => 'js',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/js/javelin/core/init.js',
   ),
   'javelin-mask' =>
   array(
     'uri' => '/res/03ef78b8/rsrc/js/javelin/lib/Mask.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-vector',
       2 => 'javelin-dom',
     ),
     'disk' => '/rsrc/js/javelin/lib/Mask.js',
   ),
   'javelin-reactor' =>
   array(
     'uri' => '/res/dfd87f3c/rsrc/js/javelin/ext/reactor/core/Reactor.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-util',
     ),
     'disk' => '/rsrc/js/javelin/ext/reactor/core/Reactor.js',
   ),
   'javelin-reactor-dom' =>
   array(
     'uri' => '/res/701b6f39/rsrc/js/javelin/ext/reactor/dom/RDOM.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-dom',
       1 => 'javelin-dynval',
       2 => 'javelin-reactornode',
       3 => 'javelin-install',
       4 => 'javelin-util',
     ),
     'disk' => '/rsrc/js/javelin/ext/reactor/dom/RDOM.js',
   ),
   'javelin-reactor-node-calmer' =>
   array(
     'uri' => '/res/5a35920a/rsrc/js/javelin/ext/reactor/core/ReactorNodeCalmer.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-reactor',
       2 => 'javelin-util',
     ),
     'disk' => '/rsrc/js/javelin/ext/reactor/core/ReactorNodeCalmer.js',
   ),
   'javelin-reactornode' =>
   array(
     'uri' => '/res/f278cc27/rsrc/js/javelin/ext/reactor/core/ReactorNode.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-reactor',
       2 => 'javelin-util',
       3 => 'javelin-reactor-node-calmer',
     ),
     'disk' => '/rsrc/js/javelin/ext/reactor/core/ReactorNode.js',
   ),
   'javelin-request' =>
   array(
     'uri' => '/res/b3257b7d/rsrc/js/javelin/lib/Request.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-stratcom',
       2 => 'javelin-util',
       3 => 'javelin-behavior',
       4 => 'javelin-json',
       5 => 'javelin-dom',
     ),
     'disk' => '/rsrc/js/javelin/lib/Request.js',
   ),
   'javelin-resource' =>
   array(
     'uri' => '/res/1ebc5a0d/rsrc/js/javelin/lib/Resource.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-magical-init',
       1 => 'javelin-stratcom',
       2 => 'javelin-util',
       3 => 'javelin-uri',
     ),
     'disk' => '/rsrc/js/javelin/lib/Resource.js',
   ),
   'javelin-stratcom' =>
   array(
     'uri' => '/res/d7a3d1e9/rsrc/js/javelin/core/Stratcom.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-event',
       2 => 'javelin-util',
       3 => 'javelin-magical-init',
     ),
     'disk' => '/rsrc/js/javelin/core/Stratcom.js',
   ),
   'javelin-tokenizer' =>
   array(
     'uri' => '/res/1b1c2148/rsrc/js/javelin/lib/control/tokenizer/Tokenizer.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-dom',
       1 => 'javelin-util',
       2 => 'javelin-stratcom',
       3 => 'javelin-install',
     ),
     'disk' => '/rsrc/js/javelin/lib/control/tokenizer/Tokenizer.js',
   ),
   'javelin-typeahead' =>
   array(
     'uri' => '/res/7dea2b98/rsrc/js/javelin/lib/control/typeahead/Typeahead.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-dom',
       2 => 'javelin-vector',
       3 => 'javelin-util',
     ),
     'disk' => '/rsrc/js/javelin/lib/control/typeahead/Typeahead.js',
   ),
   'javelin-typeahead-composite-source' =>
   array(
     'uri' => '/res/7c0d631f/rsrc/js/javelin/lib/control/typeahead/source/TypeaheadCompositeSource.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-typeahead-source',
       2 => 'javelin-util',
     ),
     'disk' => '/rsrc/js/javelin/lib/control/typeahead/source/TypeaheadCompositeSource.js',
   ),
   'javelin-typeahead-normalizer' =>
   array(
     'uri' => '/res/a9e97c0d/rsrc/js/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
     ),
     'disk' => '/rsrc/js/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js',
   ),
   'javelin-typeahead-ondemand-source' =>
   array(
     'uri' => '/res/81e531aa/rsrc/js/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-util',
       2 => 'javelin-stratcom',
       3 => 'javelin-request',
       4 => 'javelin-typeahead-source',
     ),
     'disk' => '/rsrc/js/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js',
   ),
   'javelin-typeahead-preloaded-source' =>
   array(
     'uri' => '/res/d464efd2/rsrc/js/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-util',
       2 => 'javelin-stratcom',
       3 => 'javelin-request',
       4 => 'javelin-typeahead-source',
     ),
     'disk' => '/rsrc/js/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js',
   ),
   'javelin-typeahead-source' =>
   array(
     'uri' => '/res/8606f519/rsrc/js/javelin/lib/control/typeahead/source/TypeaheadSource.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-util',
       2 => 'javelin-dom',
       3 => 'javelin-typeahead-normalizer',
     ),
     'disk' => '/rsrc/js/javelin/lib/control/typeahead/source/TypeaheadSource.js',
   ),
   'javelin-uri' =>
   array(
     'uri' => '/res/393ace00/rsrc/js/javelin/lib/URI.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-util',
       2 => 'javelin-stratcom',
     ),
     'disk' => '/rsrc/js/javelin/lib/URI.js',
   ),
   'javelin-util' =>
   array(
     'uri' => '/res/2180bc95/rsrc/js/javelin/core/util.js',
     'type' => 'js',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/js/javelin/core/util.js',
   ),
   'javelin-vector' =>
   array(
     'uri' => '/res/50535cb8/rsrc/js/javelin/lib/Vector.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-event',
     ),
     'disk' => '/rsrc/js/javelin/lib/Vector.js',
   ),
   'javelin-view' =>
   array(
     'uri' => '/res/b98657a7/rsrc/js/javelin/ext/view/View.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-util',
     ),
     'disk' => '/rsrc/js/javelin/ext/view/View.js',
   ),
   'javelin-view-html' =>
   array(
     'uri' => '/res/7e5a2122/rsrc/js/javelin/ext/view/HTMLView.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-view',
     ),
     'disk' => '/rsrc/js/javelin/ext/view/HTMLView.js',
   ),
   'javelin-view-interpreter' =>
   array(
     'uri' => '/res/17e911ca/rsrc/js/javelin/ext/view/ViewInterpreter.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-view',
       1 => 'javelin-install',
     ),
     'disk' => '/rsrc/js/javelin/ext/view/ViewInterpreter.js',
   ),
   'javelin-view-renderer' =>
   array(
     'uri' => '/res/db4ed5a2/rsrc/js/javelin/ext/view/ViewRenderer.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
     ),
     'disk' => '/rsrc/js/javelin/ext/view/ViewRenderer.js',
   ),
   'javelin-view-visitor' =>
   array(
     'uri' => '/res/0ef9dc43/rsrc/js/javelin/ext/view/ViewVisitor.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-util',
     ),
     'disk' => '/rsrc/js/javelin/ext/view/ViewVisitor.js',
   ),
   'javelin-workflow' =>
   array(
     'uri' => '/res/519c4e1a/rsrc/js/javelin/lib/Workflow.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-stratcom',
       1 => 'javelin-request',
       2 => 'javelin-dom',
       3 => 'javelin-vector',
       4 => 'javelin-install',
       5 => 'javelin-util',
       6 => 'javelin-mask',
       7 => 'javelin-uri',
     ),
     'disk' => '/rsrc/js/javelin/lib/Workflow.js',
   ),
   'mainphest-task-detail-css' =>
   array(
     'uri' => '/res/8cf452e0/rsrc/css/application/maniphest/task-detail.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/maniphest/task-detail.css',
   ),
   'maniphest-task-summary-css' =>
   array(
     'uri' => '/res/14cb4b5d/rsrc/css/application/maniphest/task-summary.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/maniphest/task-summary.css',
   ),
   'maniphest-transaction-detail-css' =>
   array(
     'uri' => '/res/149fccab/rsrc/css/application/maniphest/transaction-detail.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/maniphest/transaction-detail.css',
   ),
   'multirow-row-manager' =>
   array(
     'uri' => '/res/0a9b3dee/rsrc/js/application/core/MultirowRowManager.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-stratcom',
       2 => 'javelin-dom',
       3 => 'javelin-util',
     ),
     'disk' => '/rsrc/js/application/core/MultirowRowManager.js',
   ),
   'owners-path-editor' =>
   array(
     'uri' => '/res/e6c51eb6/rsrc/js/application/owners/OwnersPathEditor.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'multirow-row-manager',
       1 => 'javelin-install',
       2 => 'path-typeahead',
       3 => 'javelin-dom',
       4 => 'javelin-util',
     ),
     'disk' => '/rsrc/js/application/owners/OwnersPathEditor.js',
   ),
   'owners-path-editor-css' =>
   array(
     'uri' => '/res/9bc5332c/rsrc/css/application/owners/owners-path-editor.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/owners/owners-path-editor.css',
   ),
   'path-typeahead' =>
   array(
     'uri' => '/res/50246fb6/rsrc/js/application/herald/PathTypeahead.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-typeahead',
       2 => 'javelin-dom',
       3 => 'javelin-request',
       4 => 'javelin-typeahead-ondemand-source',
       5 => 'javelin-util',
     ),
     'disk' => '/rsrc/js/application/herald/PathTypeahead.js',
   ),
   'phabricator-content-source-view-css' =>
   array(
     'uri' => '/res/8c738a93/rsrc/css/application/contentsource/content-source-view.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/contentsource/content-source-view.css',
   ),
   'phabricator-core-buttons-css' =>
   array(
     'uri' => '/res/89b939ae/rsrc/css/core/buttons.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/core/buttons.css',
   ),
   'phabricator-core-css' =>
   array(
     'uri' => '/res/f912ffab/rsrc/css/core/core.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/core/core.css',
   ),
   'phabricator-countdown-css' =>
   array(
     'uri' => '/res/0f646281/rsrc/css/application/countdown/timer.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/countdown/timer.css',
   ),
   'phabricator-directory-css' =>
   array(
     'uri' => '/res/a3d307c5/rsrc/css/application/directory/phabricator-directory.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/directory/phabricator-directory.css',
   ),
   'phabricator-drag-and-drop-file-upload' =>
   array(
     'uri' => '/res/63a06ad9/rsrc/js/application/core/DragAndDropFileUpload.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-util',
       2 => 'javelin-request',
       3 => 'javelin-dom',
       4 => 'javelin-uri',
     ),
     'disk' => '/rsrc/js/application/core/DragAndDropFileUpload.js',
   ),
   'phabricator-dropdown-menu' =>
   array(
     'uri' => '/res/d55c3771/rsrc/js/application/core/DropdownMenu.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-util',
       2 => 'javelin-dom',
       3 => 'javelin-vector',
       4 => 'javelin-stratcom',
       5 => 'phabricator-menu-item',
     ),
     'disk' => '/rsrc/js/application/core/DropdownMenu.js',
   ),
   'phabricator-feed-css' =>
   array(
     'uri' => '/res/7d1d0015/rsrc/css/application/feed/feed.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/feed/feed.css',
   ),
   'phabricator-keyboard-shortcut' =>
   array(
     'uri' => '/res/beed38cd/rsrc/js/application/core/KeyboardShortcut.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-util',
       2 => 'phabricator-keyboard-shortcut-manager',
     ),
     'disk' => '/rsrc/js/application/core/KeyboardShortcut.js',
   ),
   'phabricator-keyboard-shortcut-manager' =>
   array(
     'uri' => '/res/04767571/rsrc/js/application/core/KeyboardShortcutManager.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-util',
       2 => 'javelin-stratcom',
       3 => 'javelin-dom',
       4 => 'javelin-vector',
     ),
     'disk' => '/rsrc/js/application/core/KeyboardShortcutManager.js',
   ),
   'phabricator-menu-item' =>
   array(
     'uri' => '/res/32fc2325/rsrc/js/application/core/DropdownMenuItem.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-dom',
     ),
     'disk' => '/rsrc/js/application/core/DropdownMenuItem.js',
   ),
   'phabricator-object-selector-css' =>
   array(
     'uri' => '/res/608461d2/rsrc/css/application/objectselector/object-selector.css',
     'type' => 'css',
     'requires' =>
     array(
       0 => 'aphront-dialog-view-css',
     ),
     'disk' => '/rsrc/css/application/objectselector/object-selector.css',
   ),
-  'phabricator-prefab' =>
-  array(
-    'uri' => '/res/5784a112/rsrc/js/application/core/Prefab.js',
-    'type' => 'js',
-    'requires' =>
-    array(
-      0 => 'javelin-install',
-      1 => 'javelin-util',
-      2 => 'javelin-dom',
-    ),
-    'disk' => '/rsrc/js/application/core/Prefab.js',
-  ),
   'phabricator-profile-css' =>
   array(
     'uri' => '/res/9869d10b/rsrc/css/application/profile/profile-view.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/profile/profile-view.css',
   ),
   'phabricator-profile-header-css' =>
   array(
     'uri' => '/res/d39ef6a4/rsrc/css/application/profile/profile-header-view.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/profile/profile-header-view.css',
   ),
+  0 =>
+  array(
+    'uri' => '/res/b6096fdd/rsrc/js/javelin/lib/__tests__/URI.js',
+    'type' => 'js',
+    'requires' =>
+    array(
+      0 => 'javelin-uri',
+      1 => 'javelin-php-serializer',
+    ),
+    'disk' => '/rsrc/js/javelin/lib/__tests__/URI.js',
+  ),
+  'phabricator-prefab' =>
+  array(
+    'uri' => '/res/5784a112/rsrc/js/application/core/Prefab.js',
+    'type' => 'js',
+    'requires' =>
+    array(
+      0 => 'javelin-install',
+      1 => 'javelin-util',
+      2 => 'javelin-dom',
+    ),
+    'disk' => '/rsrc/js/application/core/Prefab.js',
+  ),
   'phabricator-remarkup-css' =>
   array(
     'uri' => '/res/39f358b8/rsrc/css/core/remarkup.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/core/remarkup.css',
   ),
   'phabricator-search-results-css' =>
   array(
     'uri' => '/res/f8a86e27/rsrc/css/application/search/search-results.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/search/search-results.css',
   ),
   'phabricator-shaped-request' =>
   array(
     'uri' => '/res/dcd87f90/rsrc/js/application/core/ShapedRequest.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-util',
       2 => 'javelin-request',
     ),
     'disk' => '/rsrc/js/application/core/ShapedRequest.js',
   ),
   'phabricator-slowvote-css' =>
   array(
     'uri' => '/res/94d20443/rsrc/css/application/slowvote/slowvote.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/slowvote/slowvote.css',
   ),
   'phabricator-standard-page-view' =>
   array(
     'uri' => '/res/ec5acaed/rsrc/css/application/base/standard-page-view.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/base/standard-page-view.css',
   ),
   'phabricator-ui-example-css' =>
   array(
     'uri' => '/res/0cef078b/rsrc/css/application/uiexample/example.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/uiexample/example.css',
   ),
   'phabricator-uiexample-javelin-view' =>
   array(
     'uri' => '/res/a2ce2cfc/rsrc/js/application/uiexample/JavelinViewExample.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-view',
       2 => 'javelin-util',
       3 => 'javelin-dom',
     ),
     'disk' => '/rsrc/js/application/uiexample/JavelinViewExample.js',
   ),
   'phabricator-uiexample-reactor-button' =>
   array(
     'uri' => '/res/142127f6/rsrc/js/application/uiexample/ReactorButtonExample.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-view',
       2 => 'javelin-util',
       3 => 'javelin-dom',
       4 => 'javelin-reactor-dom',
     ),
     'disk' => '/rsrc/js/application/uiexample/ReactorButtonExample.js',
   ),
   'phabricator-uiexample-reactor-checkbox' =>
   array(
     'uri' => '/res/c75cb9e9/rsrc/js/application/uiexample/ReactorCheckboxExample.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-view',
       2 => 'javelin-util',
       3 => 'javelin-dom',
       4 => 'javelin-reactor-dom',
     ),
     'disk' => '/rsrc/js/application/uiexample/ReactorCheckboxExample.js',
   ),
   'phabricator-uiexample-reactor-focus' =>
   array(
     'uri' => '/res/3cc992eb/rsrc/js/application/uiexample/ReactorFocusExample.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-view',
       2 => 'javelin-util',
       3 => 'javelin-dom',
       4 => 'javelin-reactor-dom',
     ),
     'disk' => '/rsrc/js/application/uiexample/ReactorFocusExample.js',
   ),
   'phabricator-uiexample-reactor-input' =>
   array(
     'uri' => '/res/4953da16/rsrc/js/application/uiexample/ReactorInputExample.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-view',
       2 => 'javelin-util',
       3 => 'javelin-dom',
       4 => 'javelin-reactor-dom',
       5 => 'javelin-view-html',
       6 => 'javelin-view-interpreter',
       7 => 'javelin-view-renderer',
     ),
     'disk' => '/rsrc/js/application/uiexample/ReactorInputExample.js',
   ),
   'phabricator-uiexample-reactor-mouseover' =>
   array(
     'uri' => '/res/52a355b6/rsrc/js/application/uiexample/ReactorMouseoverExample.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-view',
       2 => 'javelin-util',
       3 => 'javelin-dom',
       4 => 'javelin-reactor-dom',
     ),
     'disk' => '/rsrc/js/application/uiexample/ReactorMouseoverExample.js',
   ),
   'phabricator-uiexample-reactor-radio' =>
   array(
     'uri' => '/res/ae87f3af/rsrc/js/application/uiexample/ReactorRadioExample.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-view',
       2 => 'javelin-util',
       3 => 'javelin-dom',
       4 => 'javelin-reactor-dom',
     ),
     'disk' => '/rsrc/js/application/uiexample/ReactorRadioExample.js',
   ),
   'phabricator-uiexample-reactor-select' =>
   array(
     'uri' => '/res/23cb448a/rsrc/js/application/uiexample/ReactorSelectExample.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-view',
       2 => 'javelin-util',
       3 => 'javelin-dom',
       4 => 'javelin-reactor-dom',
     ),
     'disk' => '/rsrc/js/application/uiexample/ReactorSelectExample.js',
   ),
   'phabricator-uiexample-reactor-sendclass' =>
   array(
     'uri' => '/res/8cd34264/rsrc/js/application/uiexample/ReactorSendClassExample.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-view',
       2 => 'javelin-util',
       3 => 'javelin-dom',
       4 => 'javelin-reactor-dom',
     ),
     'disk' => '/rsrc/js/application/uiexample/ReactorSendClassExample.js',
   ),
   'phabricator-uiexample-reactor-sendproperties' =>
   array(
     'uri' => '/res/18af54aa/rsrc/js/application/uiexample/ReactorSendPropertiesExample.js',
     'type' => 'js',
     'requires' =>
     array(
       0 => 'javelin-install',
       1 => 'javelin-view',
       2 => 'javelin-util',
       3 => 'javelin-dom',
       4 => 'javelin-reactor-dom',
     ),
     'disk' => '/rsrc/js/application/uiexample/ReactorSendPropertiesExample.js',
   ),
   'phriction-document-css' =>
   array(
     'uri' => '/res/8d09bd7f/rsrc/css/application/phriction/phriction-document-css.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/phriction/phriction-document-css.css',
   ),
   'project-edit-css' =>
   array(
     'uri' => '/res/c192b5f9/rsrc/css/application/projects/project-edit.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/application/projects/project-edit.css',
   ),
   'syntax-highlighting-css' =>
   array(
     'uri' => '/res/5669beb6/rsrc/css/core/syntax.css',
     'type' => 'css',
     'requires' =>
     array(
     ),
     'disk' => '/rsrc/css/core/syntax.css',
   ),
 ), array(
   'packages' =>
   array(
     '03ef179e' =>
     array(
       'name' => 'diffusion.pkg.css',
       'symbols' =>
       array(
         0 => 'diffusion-commit-view-css',
       ),
       'uri' => '/res/pkg/03ef179e/diffusion.pkg.css',
       'type' => 'css',
     ),
     '46547a92' =>
     array(
       'name' => 'core.pkg.js',
       'symbols' =>
       array(
         0 => 'javelin-mask',
         1 => 'javelin-workflow',
         2 => 'javelin-behavior-workflow',
         3 => 'javelin-behavior-aphront-form-disable-on-submit',
         4 => 'phabricator-keyboard-shortcut-manager',
         5 => 'phabricator-keyboard-shortcut',
         6 => 'javelin-behavior-phabricator-keyboard-shortcuts',
         7 => 'javelin-behavior-refresh-csrf',
         8 => 'javelin-behavior-phabricator-watch-anchor',
       ),
       'uri' => '/res/pkg/46547a92/core.pkg.js',
       'type' => 'js',
     ),
     '540effd7' =>
     array(
       'name' => 'typeahead.pkg.js',
       'symbols' =>
       array(
         0 => 'javelin-typeahead',
         1 => 'javelin-typeahead-normalizer',
         2 => 'javelin-typeahead-source',
         3 => 'javelin-typeahead-preloaded-source',
         4 => 'javelin-typeahead-ondemand-source',
         5 => 'javelin-tokenizer',
         6 => 'javelin-behavior-aphront-basic-tokenizer',
       ),
       'uri' => '/res/pkg/540effd7/typeahead.pkg.js',
       'type' => 'js',
     ),
     'a6562582' =>
     array(
       'name' => 'differential.pkg.js',
       'symbols' =>
       array(
         0 => 'phabricator-drag-and-drop-file-upload',
         1 => 'phabricator-shaped-request',
         2 => 'javelin-behavior-differential-feedback-preview',
         3 => 'javelin-behavior-differential-edit-inline-comments',
         4 => 'javelin-behavior-differential-populate',
         5 => 'javelin-behavior-differential-show-more',
         6 => 'javelin-behavior-differential-diff-radios',
         7 => 'javelin-behavior-differential-accept-with-errors',
         8 => 'javelin-behavior-differential-comment-jump',
         9 => 'javelin-behavior-differential-add-reviewers-and-ccs',
         10 => 'javelin-behavior-differential-keyboard-navigation',
         11 => 'javelin-behavior-aphront-drag-and-drop',
         12 => 'javelin-behavior-aphront-drag-and-drop-textarea',
         13 => 'javelin-behavior-phabricator-object-selector',
         14 => 'differential-inline-comment-editor',
       ),
       'uri' => '/res/pkg/a6562582/differential.pkg.js',
       'type' => 'js',
     ),
     'b164acea' =>
     array(
       'name' => 'javelin.pkg.js',
       'symbols' =>
       array(
         0 => 'javelin-util',
         1 => 'javelin-install',
         2 => 'javelin-event',
         3 => 'javelin-stratcom',
         4 => 'javelin-behavior',
         5 => 'javelin-request',
         6 => 'javelin-vector',
         7 => 'javelin-dom',
         8 => 'javelin-json',
         9 => 'javelin-uri',
       ),
       'uri' => '/res/pkg/b164acea/javelin.pkg.js',
       'type' => 'js',
     ),
     'fdcba95b' =>
     array(
       'name' => 'differential.pkg.css',
       'symbols' =>
       array(
         0 => 'differential-core-view-css',
         1 => 'differential-changeset-view-css',
         2 => 'differential-revision-detail-css',
         3 => 'differential-revision-history-css',
         4 => 'differential-table-of-contents-css',
         5 => 'differential-revision-comment-css',
         6 => 'differential-revision-add-comment-css',
         7 => 'differential-revision-comment-list-css',
         8 => 'phabricator-object-selector-css',
         9 => 'aphront-headsup-action-list-view-css',
         10 => 'phabricator-content-source-view-css',
         11 => 'differential-local-commits-view-css',
       ),
       'uri' => '/res/pkg/fdcba95b/differential.pkg.css',
       'type' => 'css',
     ),
     16378540 =>
     array(
       'name' => 'core.pkg.css',
       'symbols' =>
       array(
         0 => 'phabricator-core-css',
         1 => 'phabricator-core-buttons-css',
         2 => 'phabricator-standard-page-view',
         3 => 'aphront-dialog-view-css',
         4 => 'aphront-form-view-css',
         5 => 'aphront-panel-view-css',
         6 => 'aphront-side-nav-view-css',
         7 => 'aphront-table-view-css',
         8 => 'aphront-crumbs-view-css',
         9 => 'aphront-tokenizer-control-css',
         10 => 'aphront-typeahead-control-css',
         11 => 'aphront-list-filter-view-css',
         12 => 'phabricator-directory-css',
         13 => 'phabricator-remarkup-css',
         14 => 'syntax-highlighting-css',
       ),
       'uri' => '/res/pkg/16378540/core.pkg.css',
       'type' => 'css',
     ),
   ),
   'reverse' =>
   array(
     'aphront-crumbs-view-css' => '16378540',
     'aphront-dialog-view-css' => '16378540',
     'aphront-form-view-css' => '16378540',
     'aphront-headsup-action-list-view-css' => 'fdcba95b',
     'aphront-list-filter-view-css' => '16378540',
     'aphront-panel-view-css' => '16378540',
     'aphront-side-nav-view-css' => '16378540',
     'aphront-table-view-css' => '16378540',
     'aphront-tokenizer-control-css' => '16378540',
     'aphront-typeahead-control-css' => '16378540',
     'differential-changeset-view-css' => 'fdcba95b',
     'differential-core-view-css' => 'fdcba95b',
     'differential-inline-comment-editor' => 'a6562582',
     'differential-local-commits-view-css' => 'fdcba95b',
     'differential-revision-add-comment-css' => 'fdcba95b',
     'differential-revision-comment-css' => 'fdcba95b',
     'differential-revision-comment-list-css' => 'fdcba95b',
     'differential-revision-detail-css' => 'fdcba95b',
     'differential-revision-history-css' => 'fdcba95b',
     'differential-table-of-contents-css' => 'fdcba95b',
     'diffusion-commit-view-css' => '03ef179e',
     'javelin-behavior' => 'b164acea',
     'javelin-behavior-aphront-basic-tokenizer' => '540effd7',
     'javelin-behavior-aphront-drag-and-drop' => 'a6562582',
     'javelin-behavior-aphront-drag-and-drop-textarea' => 'a6562582',
     'javelin-behavior-aphront-form-disable-on-submit' => '46547a92',
     'javelin-behavior-differential-accept-with-errors' => 'a6562582',
     'javelin-behavior-differential-add-reviewers-and-ccs' => 'a6562582',
     'javelin-behavior-differential-comment-jump' => 'a6562582',
     'javelin-behavior-differential-diff-radios' => 'a6562582',
     'javelin-behavior-differential-edit-inline-comments' => 'a6562582',
     'javelin-behavior-differential-feedback-preview' => 'a6562582',
     'javelin-behavior-differential-keyboard-navigation' => 'a6562582',
     'javelin-behavior-differential-populate' => 'a6562582',
     'javelin-behavior-differential-show-more' => 'a6562582',
     'javelin-behavior-phabricator-keyboard-shortcuts' => '46547a92',
     'javelin-behavior-phabricator-object-selector' => 'a6562582',
     'javelin-behavior-phabricator-watch-anchor' => '46547a92',
     'javelin-behavior-refresh-csrf' => '46547a92',
     'javelin-behavior-workflow' => '46547a92',
     'javelin-dom' => 'b164acea',
     'javelin-event' => 'b164acea',
     'javelin-install' => 'b164acea',
     'javelin-json' => 'b164acea',
     'javelin-mask' => '46547a92',
     'javelin-request' => 'b164acea',
     'javelin-stratcom' => 'b164acea',
     'javelin-tokenizer' => '540effd7',
     'javelin-typeahead' => '540effd7',
     'javelin-typeahead-normalizer' => '540effd7',
     'javelin-typeahead-ondemand-source' => '540effd7',
     'javelin-typeahead-preloaded-source' => '540effd7',
     'javelin-typeahead-source' => '540effd7',
     'javelin-uri' => 'b164acea',
     'javelin-util' => 'b164acea',
     'javelin-vector' => 'b164acea',
     'javelin-workflow' => '46547a92',
     'phabricator-content-source-view-css' => 'fdcba95b',
     'phabricator-core-buttons-css' => '16378540',
     'phabricator-core-css' => '16378540',
     'phabricator-directory-css' => '16378540',
     'phabricator-drag-and-drop-file-upload' => 'a6562582',
     'phabricator-keyboard-shortcut' => '46547a92',
     'phabricator-keyboard-shortcut-manager' => '46547a92',
     'phabricator-object-selector-css' => 'fdcba95b',
     'phabricator-remarkup-css' => '16378540',
     'phabricator-shaped-request' => 'a6562582',
     'phabricator-standard-page-view' => '16378540',
     'syntax-highlighting-css' => '16378540',
   ),
 ));
diff --git a/src/applications/differential/controller/diffview/DifferentialDiffViewController.php b/src/applications/differential/controller/diffview/DifferentialDiffViewController.php
index 6a81b7d756..bcc78e6801 100644
--- a/src/applications/differential/controller/diffview/DifferentialDiffViewController.php
+++ b/src/applications/differential/controller/diffview/DifferentialDiffViewController.php
@@ -1,133 +1,134 @@
 <?php
 
 /*
- * Copyright 2011 Facebook, Inc.
+ * Copyright 2012 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 class DifferentialDiffViewController extends DifferentialController {
 
   private $id;
 
   public function willProcessRequest(array $data) {
     $this->id = $data['id'];
   }
 
   public function processRequest() {
     $request = $this->getRequest();
 
     $diff = id(new DifferentialDiff())->load($this->id);
     if (!$diff) {
       return new Aphront404Response();
     }
 
     if ($diff->getRevisionID()) {
       $top_panel = new AphrontPanelView();
       $top_panel->setWidth(AphrontPanelView::WIDTH_WIDE);
       $link = phutil_render_tag(
         'a',
         array(
           'href' => PhabricatorEnv::getURI('/D'.$diff->getRevisionID()),
         ),
         phutil_escape_html('D'.$diff->getRevisionID()));
       $top_panel->appendChild("<h1>This diff belongs to revision {$link}</h1>");
     } else {
       $action_panel = new AphrontPanelView();
       $action_panel->setHeader('Preview Diff');
       $action_panel->setWidth(AphrontPanelView::WIDTH_WIDE);
       $action_panel->appendChild(
         '<p class="aphront-panel-instructions">Review the diff for '.
         'correctness. When you are satisfied, either <strong>create a new '.
         'revision</strong> or <strong>update an existing revision</strong>.');
 
       // TODO: implmenent optgroup support in AphrontFormSelectControl?
       $select = array();
       $select[] = '<optgroup label="Create New Revision">';
       $select[] = '<option value="">Create a new Revision...</option>';
       $select[] = '</optgroup>';
 
       $revision_data = new DifferentialRevisionListData(
         DifferentialRevisionListData::QUERY_OPEN_OWNED,
         array($request->getUser()->getPHID()));
       $revisions = $revision_data->loadRevisions();
 
       if ($revisions) {
         $select[] = '<optgroup label="Update Existing Revision">';
         foreach ($revisions as $revision) {
           $select[] = phutil_render_tag(
             'option',
             array(
               'value' => $revision->getID(),
             ),
             phutil_escape_html($revision->getTitle()));
         }
         $select[] = '</optgroup>';
       }
 
       $select =
         '<select name="revisionID">'.
         implode("\n", $select).
         '</select>';
 
       $action_form = new AphrontFormView();
       $action_form
         ->setUser($request->getUser())
         ->setAction('/differential/revision/edit/')
         ->addHiddenInput('diffID', $diff->getID())
         ->addHiddenInput('viaDiffView', 1)
         ->appendChild(
           id(new AphrontFormMarkupControl())
           ->setLabel('Attach To')
           ->setValue($select))
         ->appendChild(
           id(new AphrontFormSubmitControl())
           ->setValue('Continue'));
 
       $action_panel->appendChild($action_form);
 
       $top_panel = $action_panel;
     }
 
 
 
     $changesets = $diff->loadChangesets();
     $changesets = msort($changesets, 'getSortKey');
 
     $table_of_contents = id(new DifferentialDiffTableOfContentsView())
       ->setChangesets($changesets);
 
     $refs = array();
     foreach ($changesets as $changeset) {
       $refs[$changeset->getID()] = $changeset->getID();
     }
 
     $details = id(new DifferentialChangesetListView())
       ->setChangesets($changesets)
-      ->setRenderingReferences($refs);
+      ->setRenderingReferences($refs)
+      ->setUser($request->getUser());
 
     return $this->buildStandardPageResponse(
       id(new DifferentialPrimaryPaneView())
         ->setLineWidthFromChangesets($changesets)
         ->appendChild(
           array(
             $top_panel->render(),
             $table_of_contents->render(),
             $details->render(),
           )),
       array(
         'title' => 'Diff View',
       ));
   }
 
 }
diff --git a/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php b/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php
index 719c04e22c..d0338c5b45 100644
--- a/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php
+++ b/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php
@@ -1,670 +1,671 @@
 <?php
 
 /*
  * Copyright 2012 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 class DifferentialRevisionViewController extends DifferentialController {
 
   private $revisionID;
 
   public function shouldRequireLogin() {
     return !$this->allowsAnonymousAccess();
   }
 
   public function willProcessRequest(array $data) {
     $this->revisionID = $data['id'];
   }
 
   public function processRequest() {
 
     $request = $this->getRequest();
     $user = $request->getUser();
     $viewer_is_anonymous = !$user->isLoggedIn();
 
     $revision = id(new DifferentialRevision())->load($this->revisionID);
     if (!$revision) {
       return new Aphront404Response();
     }
 
     $revision->loadRelationships();
 
     $diffs = $revision->loadDiffs();
 
     if (!$diffs) {
       throw new Exception(
         "This revision has no diffs. Something has gone quite wrong.");
     }
 
     $diff_vs = $request->getInt('vs');
 
     $target = end($diffs);
     $target_id = $request->getInt('id');
     if ($target_id) {
       if (isset($diffs[$target_id])) {
         $target = $diffs[$target_id];
       }
     }
 
     $diffs = mpull($diffs, null, 'getID');
     if (empty($diffs[$diff_vs])) {
       $diff_vs = null;
     }
 
     list($aux_fields, $props) = $this->loadAuxiliaryFieldsAndProperties(
       $revision,
       $target,
       array(
         'local:commits',
       ));
 
     list($changesets, $vs_map, $rendering_references) =
       $this->loadChangesetsAndVsMap($diffs, $diff_vs, $target);
 
     $comments = $revision->loadComments();
     $comments = array_merge(
       $this->getImplicitComments($revision),
       $comments);
 
     $all_changesets = $changesets;
     $inlines = $this->loadInlineComments($comments, $all_changesets);
 
     $object_phids = array_merge(
       $revision->getReviewers(),
       $revision->getCCPHIDs(),
       $revision->loadCommitPHIDs(),
       array(
         $revision->getAuthorPHID(),
         $user->getPHID(),
       ),
       mpull($comments, 'getAuthorPHID'));
 
     foreach ($comments as $comment) {
       $metadata = $comment->getMetadata();
       $added_reviewers = idx(
         $metadata,
         DifferentialComment::METADATA_ADDED_REVIEWERS);
       if ($added_reviewers) {
         foreach ($added_reviewers as $phid) {
           $object_phids[] = $phid;
         }
       }
       $added_ccs = idx(
         $metadata,
         DifferentialComment::METADATA_ADDED_CCS);
       if ($added_ccs) {
         foreach ($added_ccs as $phid) {
           $object_phids[] = $phid;
         }
       }
     }
 
     foreach ($revision->getAttached() as $type => $phids) {
       foreach ($phids as $phid => $info) {
         $object_phids[] = $phid;
       }
     }
 
     $aux_phids = array();
     foreach ($aux_fields as $key => $aux_field) {
       $aux_phids[$key] = $aux_field->getRequiredHandlePHIDsForRevisionView();
     }
     $object_phids = array_merge($object_phids, array_mergev($aux_phids));
     $object_phids = array_unique($object_phids);
 
     $handles = id(new PhabricatorObjectHandleData($object_phids))
       ->loadHandles();
 
     foreach ($aux_fields as $key => $aux_field) {
       // Make sure each field only has access to handles it specifically
       // requested, not all handles. Otherwise you can get a field which works
       // only in the presence of other fields.
       $aux_field->setHandles(array_select_keys($handles, $aux_phids[$key]));
     }
 
     $reviewer_warning = null;
     $has_live_reviewer = false;
     foreach ($revision->getReviewers() as $reviewer) {
       if (!$handles[$reviewer]->isDisabled()) {
         $has_live_reviewer = true;
       }
     }
     if (!$has_live_reviewer) {
       $reviewer_warning = new AphrontErrorView();
       $reviewer_warning->setSeverity(AphrontErrorView::SEVERITY_WARNING);
       $reviewer_warning->setTitle('No Active Reviewers');
       if ($revision->getReviewers()) {
         $reviewer_warning->appendChild(
           '<p>All specified reviewers are disabled. You may want to add '.
           'some new reviewers.</p>');
       } else {
         $reviewer_warning->appendChild(
           '<p>This revision has no specified reviewers. You may want to '.
           'add some.</p>');
       }
     }
 
     $request_uri = $request->getRequestURI();
 
     $limit = 100;
     $large = $request->getStr('large');
     if (count($changesets) > $limit && !$large) {
       $count = number_format(count($changesets));
       $warning = new AphrontErrorView();
       $warning->setTitle('Very Large Diff');
       $warning->setSeverity(AphrontErrorView::SEVERITY_WARNING);
       $warning->setWidth(AphrontErrorView::WIDTH_WIDE);
       $warning->appendChild(
         "<p>This diff is very large and affects {$count} files. Use ".
         "Table of Contents to open files in a standalone view. ".
         "<strong>".
           phutil_render_tag(
             'a',
             array(
               'href' => $request_uri->alter('large', 'true'),
             ),
             'Show All Files Inline').
         "</strong>");
       $warning = $warning->render();
 
       $visible_changesets = array();
     } else {
       $warning = null;
       $visible_changesets = $changesets;
     }
 
     $revision_detail = new DifferentialRevisionDetailView();
     $revision_detail->setRevision($revision);
     $revision_detail->setAuxiliaryFields($aux_fields);
 
     $actions = $this->getRevisionActions($revision);
 
     $custom_renderer_class = PhabricatorEnv::getEnvConfig(
       'differential.revision-custom-detail-renderer');
     if ($custom_renderer_class) {
 
       // TODO: build a better version of the action links and deprecate the
       // whole DifferentialRevisionDetailRenderer class.
       PhutilSymbolLoader::loadClass($custom_renderer_class);
       $custom_renderer =
         newv($custom_renderer_class, array());
       $actions = array_merge(
         $actions,
         $custom_renderer->generateActionLinks($revision, $target));
     }
 
     $whitespace = $request->getStr(
       'whitespace',
       DifferentialChangesetParser::WHITESPACE_IGNORE_ALL);
 
     $arc_project = $target->loadArcanistProject();
 
     if ($arc_project) {
       $symbol_indexes = $this->buildSymbolIndexes(
         $target,
         $arc_project,
         $visible_changesets);
       $repository = $arc_project->loadRepository();
     } else {
       $symbol_indexes = array();
       $repository = null;
     }
 
     $revision_detail->setActions($actions);
     $revision_detail->setUser($user);
 
     $comment_view = new DifferentialRevisionCommentListView();
     $comment_view->setComments($comments);
     $comment_view->setHandles($handles);
     $comment_view->setInlineComments($inlines);
     $comment_view->setChangesets($all_changesets);
     $comment_view->setUser($user);
     $comment_view->setTargetDiff($target);
     $comment_view->setVersusDiffID($diff_vs);
 
     $changeset_view = new DifferentialChangesetListView();
     $changeset_view->setChangesets($visible_changesets);
     $changeset_view->setEditable(!$viewer_is_anonymous);
     $changeset_view->setStandaloneViews(true);
+    $changeset_view->setUser($user);
     $changeset_view->setRevision($revision);
     $changeset_view->setDiff($target);
     $changeset_view->setRenderingReferences($rendering_references);
     $changeset_view->setWhitespace($whitespace);
     if ($repository) {
       $changeset_view->setRepository($repository, $target);
     }
     $changeset_view->setSymbolIndexes($symbol_indexes);
 
     $diff_history = new DifferentialRevisionUpdateHistoryView();
     $diff_history->setDiffs($diffs);
     $diff_history->setSelectedVersusDiffID($diff_vs);
     $diff_history->setSelectedDiffID($target->getID());
     $diff_history->setSelectedWhitespace($whitespace);
     $diff_history->setUser($user);
 
     $local_view = new DifferentialLocalCommitsView();
     $local_view->setUser($user);
     $local_view->setLocalCommits(idx($props, 'local:commits'));
 
     $toc_view = new DifferentialDiffTableOfContentsView();
     $toc_view->setChangesets($changesets);
     $toc_view->setStandaloneViewLink(empty($visible_changesets));
     $toc_view->setVsMap($vs_map);
     $toc_view->setRevisionID($revision->getID());
     $toc_view->setWhitespace($whitespace);
 
     if (!$viewer_is_anonymous) {
       $draft = id(new PhabricatorDraft())->loadOneWhere(
         'authorPHID = %s AND draftKey = %s',
         $user->getPHID(),
         'differential-comment-'.$revision->getID());
       if ($draft) {
         $draft = $draft->getDraft();
       } else {
         $draft = null;
       }
 
       $comment_form = new DifferentialAddCommentView();
       $comment_form->setRevision($revision);
       $comment_form->setActions($this->getRevisionCommentActions($revision));
       $comment_form->setActionURI('/differential/comment/save/');
       $comment_form->setUser($user);
       $comment_form->setDraft($draft);
     }
 
     $pane_id = celerity_generate_unique_node_id();
     Javelin::initBehavior(
       'differential-keyboard-navigation',
       array(
         'haunt' => $pane_id,
       ));
 
     $page_pane = id(new DifferentialPrimaryPaneView())
       ->setLineWidthFromChangesets($changesets)
       ->setID($pane_id)
       ->appendChild($reviewer_warning)
       ->appendChild(
         $revision_detail->render().
         $comment_view->render().
         $diff_history->render().
         $warning.
         $local_view->render().
         $toc_view->render().
         $changeset_view->render());
     if ($comment_form) {
       $page_pane->appendChild($comment_form->render());
     }
     return $this->buildStandardPageResponse(
       $page_pane,
       array(
         'title' => 'D'.$revision->getID().' '.$revision->getTitle(),
       ));
   }
 
   private function getImplicitComments(DifferentialRevision $revision) {
 
     $template = new DifferentialComment();
     $template->setAuthorPHID($revision->getAuthorPHID());
     $template->setRevisionID($revision->getID());
     $template->setDateCreated($revision->getDateCreated());
 
     $comments = array();
 
     if (strlen($revision->getSummary())) {
       $summary_comment = clone $template;
       $summary_comment->setContent($revision->getSummary());
       $summary_comment->setAction(DifferentialAction::ACTION_SUMMARIZE);
       $comments[] = $summary_comment;
     }
 
     if (strlen($revision->getTestPlan())) {
       $testplan_comment = clone $template;
       $testplan_comment->setContent($revision->getTestPlan());
       $testplan_comment->setAction(DifferentialAction::ACTION_TESTPLAN);
       $comments[] = $testplan_comment;
     }
 
     return $comments;
   }
 
   private function getRevisionActions(DifferentialRevision $revision) {
     $viewer_phid = $this->getRequest()->getUser()->getPHID();
     $viewer_is_owner = ($revision->getAuthorPHID() == $viewer_phid);
     $viewer_is_reviewer = in_array($viewer_phid, $revision->getReviewers());
     $viewer_is_cc = in_array($viewer_phid, $revision->getCCPHIDs());
     $viewer_is_anonymous = !$this->getRequest()->getUser()->isLoggedIn();
     $status = $revision->getStatus();
     $revision_id = $revision->getID();
     $revision_phid = $revision->getPHID();
 
     $links = array();
 
     if ($viewer_is_owner) {
       $links[] = array(
         'class' => 'revision-edit',
         'href'  => "/differential/revision/edit/{$revision_id}/",
         'name'  => 'Edit Revision',
       );
     }
 
     if (!$viewer_is_anonymous) {
       if (!$viewer_is_owner && !$viewer_is_reviewer) {
         $action = $viewer_is_cc ? 'rem' : 'add';
         $links[] = array(
           'class'   => $viewer_is_cc ? 'subscribe-rem' : 'subscribe-add',
           'href'    => "/differential/subscribe/{$action}/{$revision_id}/",
           'name'    => $viewer_is_cc ? 'Unsubscribe' : 'Subscribe',
           'instant' => true,
         );
       } else {
         $links[] = array(
           'class' => 'subscribe-rem unavailable',
           'name'  => 'Automatically Subscribed',
         );
       }
 
       require_celerity_resource('phabricator-object-selector-css');
       require_celerity_resource('javelin-behavior-phabricator-object-selector');
 
       $links[] = array(
         'class' => 'action-dependencies',
         'name'  => 'Edit Dependencies',
         'href'  => "/search/attach/{$revision_phid}/DREV/dependencies/",
         'sigil' => 'workflow',
       );
 
       if (PhabricatorEnv::getEnvConfig('maniphest.enabled')) {
         $links[] = array(
           'class' => 'attach-maniphest',
           'name'  => 'Edit Maniphest Tasks',
           'href'  => "/search/attach/{$revision_phid}/TASK/",
           'sigil' => 'workflow',
         );
       }
 
       $links[] = array(
         'class' => 'transcripts-metamta',
         'name'  => 'MetaMTA Transcripts',
         'href'  => "/mail/?phid={$revision_phid}",
       );
 
       $links[] = array(
         'class' => 'transcripts-herald',
         'name'  => 'Herald Transcripts',
         'href'  => "/herald/transcript/?phid={$revision_phid}",
       );
     }
 
     return $links;
   }
 
   private function getRevisionCommentActions(DifferentialRevision $revision) {
 
     $actions = array(
       DifferentialAction::ACTION_COMMENT => true,
     );
     $admin_actions = array();
 
     $viewer = $this->getRequest()->getUser();
     $viewer_phid = $viewer->getPHID();
     $viewer_is_admin =  $viewer->getIsAdmin();
     $viewer_is_owner = ($viewer_phid == $revision->getAuthorPHID());
     $viewer_is_reviewer = in_array($viewer_phid, $revision->getReviewers());
     $viewer_did_accept = ($viewer_phid === $revision->loadReviewedBy());
 
     if ($viewer_is_owner) {
       switch ($revision->getStatus()) {
         case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
           $actions[DifferentialAction::ACTION_ABANDON] = true;
           $actions[DifferentialAction::ACTION_RETHINK] = true;
           break;
         case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
           $actions[DifferentialAction::ACTION_ABANDON] = true;
           $actions[DifferentialAction::ACTION_REQUEST] = true;
           break;
         case ArcanistDifferentialRevisionStatus::ACCEPTED:
           $actions[DifferentialAction::ACTION_ABANDON] = true;
           $actions[DifferentialAction::ACTION_REQUEST] = true;
           $actions[DifferentialAction::ACTION_RETHINK] = true;
           break;
         case ArcanistDifferentialRevisionStatus::COMMITTED:
           break;
         case ArcanistDifferentialRevisionStatus::ABANDONED:
           $actions[DifferentialAction::ACTION_RECLAIM] = true;
           break;
       }
     } else {
       switch ($revision->getStatus()) {
         case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
           $admin_actions[DifferentialAction::ACTION_ABANDON] = $viewer_is_admin;
           $actions[DifferentialAction::ACTION_ACCEPT] = true;
           $actions[DifferentialAction::ACTION_REJECT] = true;
           $actions[DifferentialAction::ACTION_RESIGN] = $viewer_is_reviewer;
           break;
         case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
           $admin_actions[DifferentialAction::ACTION_ABANDON] = $viewer_is_admin;
           $actions[DifferentialAction::ACTION_ACCEPT] = true;
           $actions[DifferentialAction::ACTION_RESIGN] = $viewer_is_reviewer;
           break;
         case ArcanistDifferentialRevisionStatus::ACCEPTED:
           $admin_actions[DifferentialAction::ACTION_ABANDON] = $viewer_is_admin;
           $actions[DifferentialAction::ACTION_REJECT] = true;
           $actions[DifferentialAction::ACTION_RESIGN] =
             $viewer_is_reviewer && !$viewer_did_accept;
           break;
         case ArcanistDifferentialRevisionStatus::COMMITTED:
         case ArcanistDifferentialRevisionStatus::ABANDONED:
           break;
       }
     }
 
     $actions[DifferentialAction::ACTION_ADDREVIEWERS] = true;
     $actions[DifferentialAction::ACTION_ADDCCS] = true;
 
     $actions = array_keys(array_filter($actions));
     $admin_actions = array_keys(array_filter($admin_actions));
     $actions_dict = array();
 
     foreach ($actions as $action) {
       $actions_dict[$action] = DifferentialAction::getActionVerb($action);
     }
     foreach ($admin_actions as $action) {
       $actions_dict[$action] =
         '(Admin) ' . DifferentialAction::getActionVerb($action);
     }
 
     return $actions_dict;
   }
 
   private function loadInlineComments(array $comments, array &$changesets) {
 
     $inline_comments = array();
 
     $comment_ids = array_filter(mpull($comments, 'getID'));
     if (!$comment_ids) {
       return $inline_comments;
     }
 
     $inline_comments = id(new DifferentialInlineComment())
       ->loadAllWhere(
         'commentID in (%Ld)',
         $comment_ids);
 
     $load_changesets = array();
     foreach ($inline_comments as $inline) {
       $changeset_id = $inline->getChangesetID();
       if (isset($changesets[$changeset_id])) {
         continue;
       }
       $load_changesets[$changeset_id] = true;
     }
 
     $more_changesets = array();
     if ($load_changesets) {
       $changeset_ids = array_keys($load_changesets);
       $more_changesets += id(new DifferentialChangeset())
         ->loadAllWhere(
           'id IN (%Ld)',
           $changeset_ids);
     }
 
     if ($more_changesets) {
       $changesets += $more_changesets;
       $changesets = msort($changesets, 'getSortKey');
     }
 
     return $inline_comments;
   }
 
   private function loadChangesetsAndVsMap(array $diffs, $diff_vs, $target) {
     $load_ids = array();
     if ($diff_vs) {
       $load_ids[] = $diff_vs;
     }
     $load_ids[] = $target->getID();
 
     $raw_changesets = id(new DifferentialChangeset())
       ->loadAllWhere(
         'diffID IN (%Ld)',
         $load_ids);
     $changeset_groups = mgroup($raw_changesets, 'getDiffID');
 
     $changesets = idx($changeset_groups, $target->getID(), array());
     $changesets = mpull($changesets, null, 'getID');
 
     $refs = array();
     foreach ($changesets as $changeset) {
       $refs[$changeset->getID()] = $changeset->getID();
     }
 
     $vs_map = array();
     if ($diff_vs) {
       $vs_changesets = idx($changeset_groups, $diff_vs, array());
       $vs_changesets = mpull($vs_changesets, null, 'getFilename');
       foreach ($changesets as $key => $changeset) {
         $file = $changeset->getFilename();
         if (isset($vs_changesets[$file])) {
           $vs_map[$changeset->getID()] = $vs_changesets[$file]->getID();
           $refs[$changeset->getID()] =
             $changeset->getID().'/'.$vs_changesets[$file]->getID();
           unset($vs_changesets[$file]);
         } else {
           $refs[$changeset->getID()] = $changeset->getID();
         }
       }
       foreach ($vs_changesets as $changeset) {
         $changesets[$changeset->getID()] = $changeset;
         $vs_map[$changeset->getID()] = -1;
         $refs[$changeset->getID()] = $changeset->getID().'/-1';
       }
     }
 
     $changesets = msort($changesets, 'getSortKey');
 
     return array($changesets, $vs_map, $refs);
   }
 
   private function loadAuxiliaryFieldsAndProperties(
     DifferentialRevision $revision,
     DifferentialDiff $diff,
     array $special_properties) {
 
     $aux_fields = DifferentialFieldSelector::newSelector()
       ->getFieldSpecifications();
     foreach ($aux_fields as $key => $aux_field) {
       if (!$aux_field->shouldAppearOnRevisionView()) {
         unset($aux_fields[$key]);
       }
     }
 
     $aux_fields = DifferentialAuxiliaryField::loadFromStorage(
       $revision,
       $aux_fields);
 
     $aux_props = array();
     foreach ($aux_fields as $key => $aux_field) {
       $aux_field->setDiff($diff);
       $aux_props[$key] = $aux_field->getRequiredDiffProperties();
     }
 
     $required_properties = array_mergev($aux_props);
     $required_properties = array_merge(
       $required_properties,
       $special_properties);
 
     $property_map = array();
     if ($required_properties) {
       $properties = id(new DifferentialDiffProperty())->loadAllWhere(
         'diffID = %d AND name IN (%Ls)',
         $diff->getID(),
         $required_properties);
       $property_map = mpull($properties, 'getData', 'getName');
     }
 
     foreach ($aux_fields as $key => $aux_field) {
       // Give each field only the properties it specifically required, and
       // set 'null' for each requested key which we didn't actually load a
       // value for (otherwise, getDiffProperty() will throw).
       if ($aux_props[$key]) {
         $props = array_select_keys($property_map, $aux_props[$key]) +
                  array_fill_keys($aux_props[$key], null);
       } else {
         $props = array();
       }
 
       $aux_field->setDiffProperties($props);
     }
 
     return array(
       $aux_fields,
       array_select_keys(
         $property_map,
         $special_properties));
   }
 
   private function buildSymbolIndexes(
     DifferentialDiff $target,
     PhabricatorRepositoryArcanistProject $arc_project,
     array $visible_changesets) {
 
     $engine = PhabricatorSyntaxHighlighter::newEngine();
 
     $langs = $arc_project->getSymbolIndexLanguages();
     if (!$langs) {
       return array();
     }
 
     $symbol_indexes = array();
 
     $project_phids = array_merge(
       array($arc_project->getPHID()),
       nonempty($arc_project->getSymbolIndexProjects(), array()));
 
     $indexed_langs = array_fill_keys($langs, true);
     foreach ($visible_changesets as $key => $changeset) {
       $lang = $engine->getLanguageFromFilename($changeset->getFilename());
       if (isset($indexed_langs[$lang])) {
         $symbol_indexes[$key] = array(
           'lang'      => $lang,
           'projects'  => $project_phids,
         );
       }
     }
 
     return $symbol_indexes;
   }
 
 
 }
diff --git a/src/applications/differential/view/changesetlistview/DifferentialChangesetListView.php b/src/applications/differential/view/changesetlistview/DifferentialChangesetListView.php
index bb7e717327..020703f8a4 100644
--- a/src/applications/differential/view/changesetlistview/DifferentialChangesetListView.php
+++ b/src/applications/differential/view/changesetlistview/DifferentialChangesetListView.php
@@ -1,227 +1,245 @@
 <?php
 
 /*
  * Copyright 2012 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 class DifferentialChangesetListView extends AphrontView {
 
   private $changesets = array();
   private $references = array();
   private $editable;
   private $revision;
   private $renderURI = '/differential/changeset/';
   private $whitespace;
   private $standaloneViews;
+  private $user;
   private $symbolIndexes = array();
   private $repository;
   private $diff;
 
   public function setChangesets($changesets) {
     $this->changesets = $changesets;
     return $this;
   }
 
   public function setEditable($editable) {
     $this->editable = $editable;
     return $this;
   }
 
   public function setStandaloneViews($has_standalone_views) {
     $this->standaloneViews = $has_standalone_views;
     return $this;
   }
 
+  public function setUser(PhabricatorUser $user) {
+    $this->user = $user;
+    return $this;
+  }
+
   public function setRevision(DifferentialRevision $revision) {
     $this->revision = $revision;
     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 render() {
     require_celerity_resource('differential-changeset-view-css');
 
     $changesets = $this->changesets;
 
     if ($this->standaloneViews) {
       Javelin::initBehavior(
         'differential-dropdown-menus',
         array());
     }
 
     $output = array();
     $mapping = array();
     $repository = $this->repository;
     foreach ($changesets as $key => $changeset) {
       $file = $changeset->getFilename();
       $class = 'differential-changeset';
       if (!$this->editable) {
         $class .= ' differential-changeset-noneditable';
       }
 
       $ref = $this->references[$key];
 
       $detail = new DifferentialChangesetDetailView();
 
       $detail_button = null;
       if ($this->standaloneViews) {
         $detail_uri = new PhutilURI($this->renderURI);
         $detail_uri->setQueryParams(
           array(
             'ref'         => $ref,
             'whitespace'  => $this->whitespace,
           ));
 
         $diffusion_uri = null;
         if ($repository) {
           $diffusion_uri = $repository->getDiffusionBrowseURIForPath(
             $changeset->getAbsoluteRepositoryPath($this->diff, $repository));
         }
 
         $meta = array(
           'detailURI'     => (string)$detail_uri,
           'diffusionURI'  => $diffusion_uri,
           'containerID'   => $detail->getID(),
         );
         $change = $changeset->getChangeType();
         if ($change != DifferentialChangeType::TYPE_ADD) {
           $meta['leftURI'] = (string)$detail_uri->alter('view', 'old');
         }
         if ($change != DifferentialChangeType::TYPE_DELETE &&
             $change != DifferentialChangeType::TYPE_MULTICOPY) {
           $meta['rightURI'] = (string)$detail_uri->alter('view', 'new');
         }
 
+        if ($this->user) {
+          $path = ltrim(
+            $changeset->getAbsoluteRepositoryPath($this->diff, $repository),
+            '/');
+          $line = 1; // TODO: get first changed line
+          $editor_link = $this->user->loadEditorLink($path, $line, $repository);
+          if ($editor_link) {
+            $meta['editor'] = $editor_link;
+          } else {
+            $meta['editorConfigure'] = '/settings/page/preferences/';
+          }
+        }
+
         $detail_button = javelin_render_tag(
           'a',
           array(
             'class'   => 'button small grey',
             'meta'    => $meta,
             'href'    => $detail_uri,
             'target'  => '_blank',
             'sigil'   => 'differential-view-options',
           ),
           "View Options \xE2\x96\xBC");
       }
 
-
       $detail->setChangeset($changeset);
       $detail->addButton($detail_button);
       $detail->setSymbolIndex(idx($this->symbolIndexes, $key));
 
       $uniq_id = celerity_generate_unique_node_id();
       $detail->appendChild(
         phutil_render_tag(
           'div',
           array(
             'id' => $uniq_id,
           ),
           '<div class="differential-loading">Loading...</div>'));
       $output[] = $detail->render();
 
       $mapping[$uniq_id] = $ref;
     }
 
     Javelin::initBehavior('differential-populate', array(
       'registry'    => $mapping,
       'whitespace'  => $this->whitespace,
       'uri'         => $this->renderURI,
     ));
 
     Javelin::initBehavior('differential-show-more', array(
       'uri' => $this->renderURI,
       'whitespace' => $this->whitespace,
     ));
 
     Javelin::initBehavior('differential-comment-jump', array());
 
     if ($this->editable) {
 
       $undo_templates = $this->renderUndoTemplates();
 
       $revision = $this->revision;
       Javelin::initBehavior('differential-edit-inline-comments', array(
         'uri' => '/differential/comment/inline/edit/'.$revision->getID().'/',
         'undo_templates' => $undo_templates,
       ));
     }
 
     return
       '<div class="differential-review-stage" id="differential-review-stage">'.
         implode("\n", $output).
       '</div>';
   }
 
   /**
    * Render the "Undo" markup for the inline comment undo feature.
    */
   private function renderUndoTemplates() {
     $link = javelin_render_tag(
       'a',
       array(
         'href'  => '#',
         'sigil' => 'differential-inline-comment-undo',
       ),
       'Undo');
 
     $div = phutil_render_tag(
       'div',
       array(
         'class' => 'differential-inline-undo',
       ),
       'Changes discarded. '.$link);
 
     $content = '<th></th><td>'.$div.'</td>';
     $empty   = '<th></th><td></td>';
 
     $left = array($content, $empty);
     $right = array($empty, $content);
 
     return array(
       'l' => '<table><tr>'.implode('', $left).'</tr></table>',
       'r' => '<table><tr>'.implode('', $right).'</tr></table>',
     );
   }
 
 }
diff --git a/src/applications/diffusion/controller/change/DiffusionChangeController.php b/src/applications/diffusion/controller/change/DiffusionChangeController.php
index 80f2d754a9..79b01e8183 100644
--- a/src/applications/diffusion/controller/change/DiffusionChangeController.php
+++ b/src/applications/diffusion/controller/change/DiffusionChangeController.php
@@ -1,73 +1,74 @@
 <?php
 
 /*
- * Copyright 2011 Facebook, Inc.
+ * Copyright 2012 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 class DiffusionChangeController extends DiffusionController {
 
   public function processRequest() {
     $drequest = $this->diffusionRequest;
 
     $content = array();
 
     $diff_query = DiffusionDiffQuery::newFromDiffusionRequest($drequest);
     $changeset = $diff_query->loadChangeset();
 
     if (!$changeset) {
       // TODO: Refine this.
       return new Aphront404Response();
     }
 
     $changeset_view = new DifferentialChangesetListView();
     $changeset_view->setChangesets(
       array(
         0 => $changeset,
       ));
     $changeset_view->setRenderingReferences(
       array(
         0 => $diff_query->getRenderingReference(),
       ));
     $changeset_view->setRenderURI(
       '/diffusion/'.$drequest->getRepository()->getCallsign().'/diff/');
     $changeset_view->setWhitespace(
       DifferentialChangesetParser::WHITESPACE_SHOW_ALL);
+    $changeset_view->setUser($this->getRequest()->getUser());
 
     $content[] = $this->buildCrumbs(
       array(
         'branch' => true,
         'path'   => true,
         'view'   => 'change',
       ));
 
     // TODO: This is pretty awkward, unify the CSS between Diffusion and
     // Differential better.
     require_celerity_resource('differential-core-view-css');
     $content[] =
       '<div class="differential-primary-pane">'.
         $changeset_view->render().
       '</div>';
 
     $nav = $this->buildSideNav('change', true);
     $nav->appendChild($content);
 
     return $this->buildStandardPageResponse(
       $nav,
       array(
         'title' => 'Change',
       ));
   }
 
 }
diff --git a/src/applications/diffusion/controller/commit/DiffusionCommitController.php b/src/applications/diffusion/controller/commit/DiffusionCommitController.php
index cdb6abe232..97696eb5c9 100644
--- a/src/applications/diffusion/controller/commit/DiffusionCommitController.php
+++ b/src/applications/diffusion/controller/commit/DiffusionCommitController.php
@@ -1,292 +1,293 @@
 <?php
 
 /*
  * Copyright 2012 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 class DiffusionCommitController extends DiffusionController {
 
   const CHANGES_LIMIT = 100;
 
   public function processRequest() {
     $drequest = $this->getDiffusionRequest();
     $request = $this->getRequest();
     $user = $request->getUser();
 
     $callsign = $drequest->getRepository()->getCallsign();
 
     $content = array();
     $content[] = $this->buildCrumbs(array(
       'commit' => true,
     ));
 
     $detail_panel = new AphrontPanelView();
 
     $repository = $drequest->getRepository();
     $commit = $drequest->loadCommit();
 
     if (!$commit) {
       // TODO: Make more user-friendly.
       throw new Exception('This commit has not parsed yet.');
     }
 
     $commit_data = $drequest->loadCommitData();
 
     $is_foreign = $commit_data->getCommitDetail('foreign-svn-stub');
     if ($is_foreign) {
       $subpath = $commit_data->getCommitDetail('svn-subpath');
 
       $error_panel = new AphrontErrorView();
       $error_panel->setWidth(AphrontErrorView::WIDTH_WIDE);
       $error_panel->setTitle('Commit Not Tracked');
       $error_panel->setSeverity(AphrontErrorView::SEVERITY_WARNING);
       $error_panel->appendChild(
         "This Diffusion repository is configured to track only one ".
         "subdirectory of the entire Subversion repository, and this commit ".
         "didn't affect the tracked subdirectory ('".
         phutil_escape_html($subpath)."'), so no information is available.");
       $content[] = $error_panel;
     } else {
       $engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine();
 
       require_celerity_resource('diffusion-commit-view-css');
       require_celerity_resource('phabricator-remarkup-css');
 
       $property_table = $this->renderPropertyTable($commit, $commit_data);
 
       $detail_panel->appendChild(
         '<div class="diffusion-commit-view">'.
           '<div class="diffusion-commit-dateline">'.
             'r'.$callsign.$commit->getCommitIdentifier().
             ' &middot; '.
             phabricator_datetime($commit->getEpoch(), $user).
           '</div>'.
           '<h1>Revision Detail</h1>'.
           '<div class="diffusion-commit-details">'.
             $property_table.
             '<hr />'.
             '<div class="diffusion-commit-message phabricator-remarkup">'.
               $engine->markupText($commit_data->getCommitMessage()).
             '</div>'.
           '</div>'.
         '</div>');
 
       $content[] = $detail_panel;
     }
 
     $change_query = DiffusionPathChangeQuery::newFromDiffusionRequest(
       $drequest);
     $changes = $change_query->loadChanges();
 
     $original_changes_count = count($changes);
     if ($request->getStr('show_all') !== 'true' &&
         $original_changes_count > self::CHANGES_LIMIT) {
       $changes = array_slice($changes, 0, self::CHANGES_LIMIT);
     }
 
     $change_table = new DiffusionCommitChangeTableView();
     $change_table->setDiffusionRequest($drequest);
     $change_table->setPathChanges($changes);
 
     $count = count($changes);
 
     $bad_commit = null;
     if ($count == 0) {
       $bad_commit = queryfx_one(
         id(new PhabricatorRepository())->establishConnection('r'),
         'SELECT * FROM %T WHERE fullCommitName = %s',
         PhabricatorRepository::TABLE_BADCOMMIT,
         'r'.$callsign.$commit->getCommitIdentifier());
     }
 
     if ($bad_commit) {
       $error_panel = new AphrontErrorView();
       $error_panel->setWidth(AphrontErrorView::WIDTH_WIDE);
       $error_panel->setTitle('Bad Commit');
       $error_panel->appendChild(
         phutil_escape_html($bad_commit['description']));
 
       $content[] = $error_panel;
     } else if ($is_foreign) {
       // Don't render anything else.
     } else if (!count($changes)) {
       $no_changes = new AphrontErrorView();
       $no_changes->setWidth(AphrontErrorView::WIDTH_WIDE);
       $no_changes->setSeverity(AphrontErrorView::SEVERITY_WARNING);
       $no_changes->setTitle('Not Yet Parsed');
       // TODO: This can also happen with weird SVN changes that don't do
       // anything (or only alter properties?), although the real no-changes case
       // is extremely rare and might be impossible to produce organically. We
       // should probably write some kind of "Nothing Happened!" change into the
       // DB once we parse these changes so we can distinguish between
       // "not parsed yet" and "no changes".
       $no_changes->appendChild(
         "This commit hasn't been fully parsed yet (or doesn't affect any ".
         "paths).");
       $content[] = $no_changes;
     } else {
       $change_panel = new AphrontPanelView();
       $change_panel->setHeader("Changes (".number_format($count).")");
 
       if ($count !== $original_changes_count) {
         $show_all_button = phutil_render_tag(
           'a',
           array(
             'class'   => 'button green',
             'href'    => '?show_all=true',
           ),
           phutil_escape_html('Show All Changes'));
         $warning_view = id(new AphrontErrorView())
           ->setSeverity(AphrontErrorView::SEVERITY_WARNING)
           ->setTitle(sprintf(
                        "Showing only the first %d changes out of %s!",
                        self::CHANGES_LIMIT,
                        number_format($original_changes_count)));
 
         $change_panel->appendChild($warning_view);
         $change_panel->addButton($show_all_button);
       }
 
       $change_panel->appendChild($change_table);
 
       $content[] = $change_panel;
 
       $changesets = DiffusionPathChange::convertToDifferentialChangesets(
         $changes);
 
       $vcs = $repository->getVersionControlSystem();
       switch ($vcs) {
         case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
           $vcs_supports_directory_changes = true;
           break;
         case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
         case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
           $vcs_supports_directory_changes = false;
           break;
         default:
           throw new Exception("Unknown VCS.");
       }
 
       $references = array();
       foreach ($changesets as $key => $changeset) {
         $file_type = $changeset->getFileType();
         if ($file_type == DifferentialChangeType::FILE_DIRECTORY) {
           if (!$vcs_supports_directory_changes) {
             unset($changesets[$key]);
             continue;
           }
         }
 
         $branch = $drequest->getBranchURIComponent(
           $drequest->getBranch());
         $filename = $changeset->getFilename();
         $reference = "{$branch}{$filename};".$drequest->getCommit();
         $references[$key] = $reference;
       }
 
       $change_list = new DifferentialChangesetListView();
       $change_list->setChangesets($changesets);
       $change_list->setRenderingReferences($references);
       $change_list->setRenderURI('/diffusion/'.$callsign.'/diff/');
+      $change_list->setUser($user);
 
       // TODO: This is pretty awkward, unify the CSS between Diffusion and
       // Differential better.
       require_celerity_resource('differential-core-view-css');
       $change_list =
         '<div class="differential-primary-pane">'.
           $change_list->render().
         '</div>';
 
       $content[] = $change_list;
     }
 
     return $this->buildStandardPageResponse(
       $content,
       array(
         'title' => 'r'.$callsign.$commit->getCommitIdentifier(),
       ));
   }
 
   private function renderPropertyTable(
     PhabricatorRepositoryCommit $commit,
     PhabricatorRepositoryCommitData $data) {
 
     $phids = array();
     if ($data->getCommitDetail('authorPHID')) {
       $phids[] = $data->getCommitDetail('authorPHID');
     }
     if ($data->getCommitDetail('reviewerPHID')) {
       $phids[] = $data->getCommitDetail('reviewerPHID');
     }
     if ($data->getCommitDetail('differential.revisionPHID')) {
       $phids[] = $data->getCommitDetail('differential.revisionPHID');
     }
 
     $handles = array();
     if ($phids) {
       $handles = id(new PhabricatorObjectHandleData($phids))
         ->loadHandles();
     }
 
     $props = array();
 
     $author_phid = $data->getCommitDetail('authorPHID');
     if ($data->getCommitDetail('authorPHID')) {
       $props['Author'] = $handles[$author_phid]->renderLink();
     } else {
       $props['Author'] = phutil_escape_html($data->getAuthorName());
     }
 
     $reviewer_phid = $data->getCommitDetail('reviewerPHID');
     $reviewer_name = $data->getCommitDetail('reviewerName');
     if ($reviewer_phid) {
       $props['Reviewer'] = $handles[$reviewer_phid]->renderLink();
     } else if ($reviewer_name) {
       $props['Reviewer'] = phutil_escape_html($reviewer_name);
     }
 
     $revision_phid = $data->getCommitDetail('differential.revisionPHID');
     if ($revision_phid) {
       $props['Differential Revision'] = $handles[$revision_phid]->renderLink();
     }
 
     $request = $this->getDiffusionRequest();
 
     $contains = DiffusionContainsQuery::newFromDiffusionRequest($request);
     $branches = $contains->loadContainingBranches();
 
     if ($branches) {
       // TODO: Separate these into 'tracked' and other; link tracked branches.
       $branches = implode(', ', array_keys($branches));
       $branches = phutil_escape_html($branches);
       $props['Branches'] = $branches;
     }
 
     $rows = array();
     foreach ($props as $key => $value) {
       $rows[] =
         '<tr>'.
           '<th>'.$key.':</th>'.
           '<td>'.$value.'</td>'.
         '</tr>';
     }
 
     return
       '<table class="diffusion-commit-properties">'.
         implode("\n", $rows).
       '</table>';
   }
 
 }
diff --git a/src/applications/diffusion/controller/file/DiffusionBrowseFileController.php b/src/applications/diffusion/controller/file/DiffusionBrowseFileController.php
index 859420fa64..3b3fa1a1ca 100644
--- a/src/applications/diffusion/controller/file/DiffusionBrowseFileController.php
+++ b/src/applications/diffusion/controller/file/DiffusionBrowseFileController.php
@@ -1,439 +1,458 @@
 <?php
 
 /*
- * Copyright 2011 Facebook, Inc.
+ * Copyright 2012 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 class DiffusionBrowseFileController extends DiffusionController {
 
   // Image types we want to display inline using <img> tags
   protected $imageTypes = array(
     'png' => 'image/png',
     'gif' => 'image/gif',
     'ico' => 'image/png',
     'jpg' => 'image/jpeg',
     'jpeg'=> 'image/jpeg'
   );
 
   // Document types that should trigger link to ?view=raw
   protected $documentTypes = array(
     'pdf'=> 'application/pdf',
     'ps' => 'application/postscript',
   );
 
   public function processRequest() {
 
     // Build the view selection form.
     $select_map = array(
       'highlighted' => 'View as Highlighted Text',
       'blame' => 'View as Highlighted Text with Blame',
       'plain' => 'View as Plain Text',
       'plainblame' => 'View as Plain Text with Blame',
       'raw' => 'View as raw document',
     );
 
     $request = $this->getRequest();
 
     $drequest = $this->getDiffusionRequest();
     $path = $drequest->getPath();
     $selected = $request->getStr('view');
     $needs_blame = ($selected == 'blame' || $selected == 'plainblame');
     $file_query = DiffusionFileContentQuery::newFromDiffusionRequest(
       $this->diffusionRequest);
     $file_query->setNeedsBlame($needs_blame);
     $file_query->loadFileContent();
     $data = $file_query->getRawData();
     if ($selected === 'raw') {
       $response = new AphrontFileResponse();
       $response->setContent($data);
       $mime_type = $this->getDocumentType($path);
       if ($mime_type) {
         $response->setMimeType($mime_type);
       } else {
         $as_filename = idx(pathinfo($path), 'basename');
         $response->setDownload($as_filename);
       }
       return $response;
     }
 
     $select = '<select name="view">';
     foreach ($select_map as $k => $v) {
       $option = phutil_render_tag(
         'option',
         array(
           'value' => $k,
           'selected' => ($k == $selected) ? 'selected' : null,
         ),
         phutil_escape_html($v));
 
       $select .= $option;
     }
     $select .= '</select>';
 
     require_celerity_resource('diffusion-source-css');
 
     $view_select_panel = new AphrontPanelView();
     $view_select_form = phutil_render_tag(
       'form',
       array(
         'action' => $request->getRequestURI(),
         'method' => 'get',
         'class'  => 'diffusion-browse-type-form',
       ),
       $select.
       '<button>View</button>');
     $view_select_panel->appendChild($view_select_form);
+
+    $user = $request->getUser();
+    if ($user) {
+      $line = 1;
+      $repository = $this->getDiffusionRequest()->getRepository();
+      $editor_link = $user->loadEditorLink($path, $line, $repository);
+      if ($editor_link) {
+        $view_select_panel->addButton(
+          phutil_render_tag(
+            'a',
+            array(
+              'href' => $editor_link,
+              'class' => 'button',
+            ),
+            'Edit'
+          ));
+      }
+    }
+
     $view_select_panel->appendChild('<div style="clear: both;"></div>');
 
     // Build the content of the file.
     $corpus = $this->buildCorpus(
       $selected,
       $file_query,
       $needs_blame,
       $drequest,
       $path,
       $data
     );
 
     // Render the page.
     $content = array();
     $content[] = $this->buildCrumbs(
       array(
         'branch' => true,
         'path'   => true,
         'view'   => 'browse',
       ));
     $content[] = $view_select_panel;
     $content[] = $corpus;
     $content[] = $this->buildOpenRevisions();
 
     $nav = $this->buildSideNav('browse', true);
     $nav->appendChild($content);
 
 
     $basename = basename($this->getDiffusionRequest()->getPath());
 
     return $this->buildStandardPageResponse(
       $nav,
       array(
         'title' => $basename,
       ));
   }
 
 
   /**
    * Returns a content-type corrsponding to an image file extension
    *
    * @param string $path File path
    * @return mixed A content-type string or NULL if path doesn't end with a
    *               recognized image extension
    */
   public function getImageType($path) {
     $ext = pathinfo($path);
     $ext = idx($ext, 'extension');
     return idx($this->imageTypes, $ext);
   }
 
   /**
    * Returns a content-type corresponding to an document file extension
    *
    * @param string $path File path
    * @return mixed A content-type string or NULL if path doesn't end with a
    *               recognized document extension
    */
   public function getDocumentType($path) {
     $ext = pathinfo($path);
     $ext = idx($ext, 'extension');
     return idx($this->documentTypes, $ext);
   }
 
 
   private function buildCorpus($selected,
                                $file_query,
                                $needs_blame,
                                $drequest,
                                $path,
                                $data) {
     $image_type = $this->getImageType($path);
     if ($image_type && !$selected) {
       $corpus = phutil_render_tag(
         'img',
         array(
           'style' => 'padding-bottom: 10px',
           'src' => 'data:'.$image_type.';base64,'.base64_encode($data),
         )
       );
       return $corpus;
     }
 
     $document_type = $this->getDocumentType($path);
     if (($document_type && !$selected) || !phutil_is_utf8($data)) {
       $data = $file_query->getRawData();
       $document_type_description = $document_type ? $document_type : 'binary';
       $corpus = phutil_render_tag(
         'p',
         array(
           'style' => 'text-align: center;'
         ),
         phutil_render_tag(
           'a',
           array(
             'href' => '?view=raw',
             'class' => 'button'
           ),
           "View $document_type_description"
         )
       );
       return $corpus;
     }
 
 
     // TODO: blame of blame.
     switch ($selected) {
       case 'plain':
         $style =
           "margin: 1em 2em; width: 90%; height: 80em; font-family: monospace";
         $corpus = phutil_render_tag(
           'textarea',
           array(
             'style' => $style,
           ),
           phutil_escape_html($file_query->getRawData()));
 
           break;
 
       case 'plainblame':
         $style =
           "margin: 1em 2em; width: 90%; height: 80em; font-family: monospace";
         list($text_list, $rev_list, $blame_dict) =
           $file_query->getBlameData();
 
         $rows = array();
         foreach ($text_list as $k => $line) {
           $rev = $rev_list[$k];
           if (isset($blame_dict[$rev]['handle'])) {
             $author = $blame_dict[$rev]['handle']->getName();
           } else {
             $author = $blame_dict[$rev]['author'];
           }
           $rows[] =
             sprintf("%-10s %-20s %s", substr($rev, 0, 7), $author, $line);
         }
 
         $corpus = phutil_render_tag(
           'textarea',
           array(
             'style' => $style,
           ),
           phutil_escape_html(implode("\n", $rows)));
 
         break;
 
       case 'highlighted':
       case 'blame':
       default:
         require_celerity_resource('syntax-highlighting-css');
 
         list($text_list, $rev_list, $blame_dict) = $file_query->getBlameData();
 
         $text_list = implode("\n", $text_list);
         $text_list = PhabricatorSyntaxHighlighter::highlightWithFilename(
           $path,
           $text_list);
         $text_list = explode("\n", $text_list);
 
         $rows = $this->buildDisplayRows($text_list, $rev_list, $blame_dict,
           $needs_blame, $drequest, $file_query, $selected);
 
         $corpus_table = phutil_render_tag(
           'table',
           array(
             'class' => "diffusion-source remarkup-code PhabricatorMonospaced",
           ),
           implode("\n", $rows));
         $corpus = phutil_render_tag(
           'div',
           array(
             'style' => 'padding: 0pt 2em;',
           ),
           $corpus_table);
 
         break;
     }
 
     return $corpus;
   }
 
 
   private function buildDisplayRows($text_list, $rev_list, $blame_dict,
     $needs_blame, DiffusionRequest $drequest, $file_query, $selected) {
     $last_rev = null;
     $color = null;
     $rows = array();
     $n = 1;
     $view = $this->getRequest()->getStr('view');
 
     if ($blame_dict) {
       $epoch_list = ipull($blame_dict, 'epoch');
       $max = max($epoch_list);
       $min = min($epoch_list);
       $range = $max - $min + 1;
     } else {
       $range = 1;
     }
 
     foreach ($text_list as $k => $line) {
       if ($needs_blame) {
         // If the line's rev is same as the line above, show empty content
         // with same color; otherwise generate blame info. The newer a change
         // is, the darker the color.
         $rev = $rev_list[$k];
         if ($last_rev == $rev) {
           $blame_info =
             ($file_query->getSupportsBlameOnBlame() ?
               '<th style="background: '.$color.'; width: 2em;"></th>' : '').
             '<th style="background: '.$color.'; width: 9em;"></th>'.
             '<th style="background: '.$color.'"></th>';
         } else {
 
           $color_number = (int)(0xEE -
             0xEE * ($blame_dict[$rev]['epoch'] - $min) / $range);
           $color = sprintf('#%02xee%02x', $color_number, $color_number);
 
           $revision_link = self::renderRevision(
             $drequest,
             substr($rev, 0, 7));
 
           if (!$file_query->getSupportsBlameOnBlame()) {
             $prev_link = '';
           } else {
             $prev_rev = $file_query->getPrevRev($rev);
             $path = $drequest->getPath();
             $prev_link = self::renderBrowse(
               $drequest,
               $path,
               "\xC2\xAB",
               $prev_rev,
               $n,
               $selected);
             $prev_link = '<th style="background: ' . $color .
               '; width: 2em;">' . $prev_link . '</th>';
           }
 
           if (isset($blame_dict[$rev]['handle'])) {
             $author_link = $blame_dict[$rev]['handle']->renderLink();
           } else {
             $author_link = phutil_escape_html($blame_dict[$rev]['author']);
           }
           $blame_info =
             $prev_link .
             '<th style="background: '.$color.
               '; width: 12em;">'.$revision_link.'</th>'.
             '<th style="background: '.$color.'; width: 12em'.
               '; font-weight: normal; color: #333;">'.$author_link.'</th>';
           $last_rev = $rev;
         }
       } else {
         $blame_info = null;
       }
 
       // Highlight the line of interest if needed.
       if ($n == $drequest->getLine()) {
         $tr = '<tr style="background: #ffff00;">';
         $targ = '<a id="scroll_target"></a>';
         Javelin::initBehavior('diffusion-jump-to',
           array('target' => 'scroll_target'));
       } else {
         $tr = '<tr>';
         $targ = null;
       }
 
       // Create the row display.
       $uri_path = $drequest->getUriPath();
       $uri_rev  = $drequest->getStableCommitName();
       $uri_view = $view
         ? '?view='.$view
         : null;
 
       $l = phutil_render_tag(
         'a',
         array(
           'class' => 'diffusion-line-link',
           'href' => $uri_path.';'.$uri_rev.'$'.$n.$uri_view,
         ),
         $n);
 
       $rows[] = $tr.$blame_info.'<th>'.$l.'</th><td>'.$targ.$line.'</td></tr>';
       ++$n;
     }
 
     return $rows;
   }
 
 
   private static function renderRevision(DiffusionRequest $drequest,
     $revision) {
 
     $callsign = $drequest->getCallsign();
 
     $name = 'r'.$callsign.$revision;
     return phutil_render_tag(
       'a',
       array(
            'href' => '/'.$name,
       ),
       $name
     );
   }
 
 
   private static function renderBrowse(
     DiffusionRequest $drequest,
     $path,
     $name = null,
     $rev = null,
     $line = null,
     $view = null) {
 
     $callsign = $drequest->getCallsign();
 
     if ($name === null) {
       $name = $path;
     }
 
     $at = null;
     if ($rev) {
       $at = ';'.$rev;
     }
 
     if ($view) {
       $view = '?view='.$view;
     }
 
     if ($line) {
       $line = '$'.$line;
     }
 
     return phutil_render_tag(
       'a',
       array(
         'href' => "/diffusion/{$callsign}/browse/{$path}{$at}{$line}{$view}",
       ),
       $name
     );
   }
 
 
 }
diff --git a/src/applications/people/controller/settings/panels/preferences/PhabricatorUserPreferenceSettingsPanelController.php b/src/applications/people/controller/settings/panels/preferences/PhabricatorUserPreferenceSettingsPanelController.php
index 189351c6eb..9a5e390972 100644
--- a/src/applications/people/controller/settings/panels/preferences/PhabricatorUserPreferenceSettingsPanelController.php
+++ b/src/applications/people/controller/settings/panels/preferences/PhabricatorUserPreferenceSettingsPanelController.php
@@ -1,107 +1,118 @@
 <?php
 
 /*
  * Copyright 2012 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 class PhabricatorUserPreferenceSettingsPanelController
   extends PhabricatorUserSettingsPanelController {
 
   public function processRequest() {
 
     $request = $this->getRequest();
     $user = $request->getUser();
     $preferences = $user->loadPreferences();
 
     $pref_monospaced  = PhabricatorUserPreferences::PREFERENCE_MONOSPACED;
+    $pref_editor      = PhabricatorUserPreferences::PREFERENCE_EDITOR;
     $pref_titles      = PhabricatorUserPreferences::PREFERENCE_TITLES;
 
     if ($request->isFormPost()) {
       $monospaced = $request->getStr($pref_monospaced);
 
       // Prevent the user from doing stupid things.
       $monospaced = preg_replace('/[^a-z0-9 ,"]+/i', '', $monospaced);
 
       $preferences->setPreference($pref_titles, $request->getStr($pref_titles));
+      $preferences->setPreference($pref_editor, $request->getStr($pref_editor));
       $preferences->setPreference($pref_monospaced, $monospaced);
 
       $preferences->save();
       return id(new AphrontRedirectResponse())
         ->setURI('/settings/page/preferences/?saved=true');
     }
 
     $example_string = <<<EXAMPLE
 // This is what your monospaced font currently looks like.
 function helloWorld() {
   alert("Hello world!");
 }
 EXAMPLE;
 
     $form = id(new AphrontFormView())
       ->setUser($user)
       ->setAction('/settings/page/preferences/')
       ->appendChild(
         id(new AphrontFormSelectControl())
           ->setLabel('Page Titles')
           ->setName($pref_titles)
           ->setValue($preferences->getPreference($pref_titles))
           ->setOptions(
             array(
               'glyph' =>
               "In page titles, show Tool names as unicode glyphs: \xE2\x9A\x99",
               'text' =>
               'In page titles, show Tool names as plain text: [Differential]',
             )))
+      ->appendChild(
+        id(new AphrontFormTextControl())
+        ->setLabel('Editor Link')
+        ->setName($pref_editor)
+        ->setCaption(
+          'Link to edit files in external editor. '.
+          '%f is replaced by filename, %l by line number, %r by repository. '.
+          'Example: editor://open/?file=%f&line=%l&repository=%r')
+        ->setValue($preferences->getPreference($pref_editor)))
       ->appendChild(
         id(new AphrontFormTextControl())
         ->setLabel('Monospaced Font')
         ->setName($pref_monospaced)
         ->setCaption(
           'Overrides default fonts in tools like Differential. '.
           '(Default: 10px "Menlo", "Consolas", "Monaco", '.
           'monospace)')
         ->setValue($preferences->getPreference($pref_monospaced)))
       ->appendChild(
         id(new AphrontFormMarkupControl())
         ->setValue(
           '<pre class="PhabricatorMonospaced">'.
           phutil_escape_html($example_string).
           '</pre>'))
       ->appendChild(
         id(new AphrontFormSubmitControl())
           ->setValue('Save Preferences'));
 
     $panel = new AphrontPanelView();
     $panel->setWidth(AphrontPanelView::WIDTH_WIDE);
     $panel->setHeader('Display Preferences');
     $panel->appendChild($form);
 
     $error_view = null;
     if ($request->getStr('saved') === 'true') {
       $error_view = id(new AphrontErrorView())
         ->setTitle('Preferences Saved')
         ->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
         ->setErrors(array('Your preferences have been saved.'));
     }
 
     return id(new AphrontNullView())
       ->appendChild(
         array(
           $error_view,
           $panel,
         ));
   }
 }
 
diff --git a/src/applications/people/storage/preferences/PhabricatorUserPreferences.php b/src/applications/people/storage/preferences/PhabricatorUserPreferences.php
index 06c47de689..193aca39e4 100644
--- a/src/applications/people/storage/preferences/PhabricatorUserPreferences.php
+++ b/src/applications/people/storage/preferences/PhabricatorUserPreferences.php
@@ -1,53 +1,54 @@
 <?php
 
 /*
  * Copyright 2012 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 class PhabricatorUserPreferences extends PhabricatorUserDAO {
 
   const PREFERENCE_MONOSPACED     = 'monospaced';
+  const PREFERENCE_EDITOR         = 'editor';
   const PREFERENCE_TITLES         = 'titles';
 
   const PREFERENCE_RE_PREFIX      = 're-prefix';
   const PREFERENCE_NO_SELF_MAIL   = 'self-mail';
 
   protected $userPHID;
   protected $preferences = array();
 
   public function getConfiguration() {
     return array(
       self::CONFIG_SERIALIZATION => array(
         'preferences' => self::SERIALIZATION_JSON,
       ),
       self::CONFIG_TIMESTAMPS => false,
     ) + parent::getConfiguration();
   }
 
   public function getPreference($key, $default = null) {
     return idx($this->preferences, $key, $default);
   }
 
   public function setPreference($key, $value) {
     $this->preferences[$key] = $value;
     return $this;
   }
 
   public function unsetPreference($key) {
     unset($this->preferences[$key]);
     return $this;
   }
 
 }
diff --git a/src/applications/people/storage/user/PhabricatorUser.php b/src/applications/people/storage/user/PhabricatorUser.php
index 12a8429856..0d932d705f 100644
--- a/src/applications/people/storage/user/PhabricatorUser.php
+++ b/src/applications/people/storage/user/PhabricatorUser.php
@@ -1,511 +1,526 @@
 <?php
 
 /*
  * Copyright 2012 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 class PhabricatorUser extends PhabricatorUserDAO {
 
   const SESSION_TABLE = 'phabricator_session';
   const NAMETOKEN_TABLE = 'user_nametoken';
 
   protected $phid;
   protected $userName;
   protected $realName;
   protected $email;
   protected $passwordSalt;
   protected $passwordHash;
   protected $profileImagePHID;
   protected $timezoneIdentifier = '';
 
   protected $consoleEnabled = 0;
   protected $consoleVisible = 0;
   protected $consoleTab = '';
 
   protected $conduitCertificate;
 
   protected $isSystemAgent = 0;
   protected $isAdmin = 0;
   protected $isDisabled = 0;
 
   private $preferences = null;
 
   protected function readField($field) {
     switch ($field) {
       case 'profileImagePHID':
         return nonempty(
           $this->profileImagePHID,
           PhabricatorEnv::getEnvConfig('user.default-profile-image-phid'));
       case 'timezoneIdentifier':
         // If the user hasn't set one, guess the server's time.
         return nonempty(
           $this->timezoneIdentifier,
           date_default_timezone_get());
       // Make sure these return booleans.
       case 'isAdmin':
         return (bool)$this->isAdmin;
       case 'isDisabled':
         return (bool)$this->isDisabled;
       case 'isSystemAgent':
         return (bool)$this->isSystemAgent;
       default:
         return parent::readField($field);
     }
   }
 
   public function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_PARTIAL_OBJECTS => true,
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       PhabricatorPHIDConstants::PHID_TYPE_USER);
   }
 
   public function setPassword($password) {
     if (!$this->getPHID()) {
       throw new Exception(
         "You can not set a password for an unsaved user because their PHID ".
         "is a salt component in the password hash.");
     }
 
     if (!strlen($password)) {
       $this->setPasswordHash('');
     } else {
       $this->setPasswordSalt(md5(mt_rand()));
       $hash = $this->hashPassword($password);
       $this->setPasswordHash($hash);
     }
     return $this;
   }
 
   public function isLoggedIn() {
     return !($this->getPHID() === null);
   }
 
   public function save() {
     if (!$this->getConduitCertificate()) {
       $this->setConduitCertificate($this->generateConduitCertificate());
     }
     $result = parent::save();
 
     $this->updateNameTokens();
     PhabricatorSearchUserIndexer::indexUser($this);
 
     return $result;
   }
 
   private function generateConduitCertificate() {
     return Filesystem::readRandomCharacters(255);
   }
 
   public function comparePassword($password) {
     if (!strlen($password)) {
       return false;
     }
     if (!strlen($this->getPasswordHash())) {
       return false;
     }
     $password = $this->hashPassword($password);
     return ($password === $this->getPasswordHash());
   }
 
   private function hashPassword($password) {
     $password = $this->getUsername().
                 $password.
                 $this->getPHID().
                 $this->getPasswordSalt();
     for ($ii = 0; $ii < 1000; $ii++) {
       $password = md5($password);
     }
     return $password;
   }
 
   const CSRF_CYCLE_FREQUENCY  = 3600;
   const CSRF_TOKEN_LENGTH     = 16;
 
   const EMAIL_CYCLE_FREQUENCY = 86400;
   const EMAIL_TOKEN_LENGTH    = 24;
 
   public function getCSRFToken($offset = 0) {
     return $this->generateToken(
       time() + (self::CSRF_CYCLE_FREQUENCY * $offset),
       self::CSRF_CYCLE_FREQUENCY,
       PhabricatorEnv::getEnvConfig('phabricator.csrf-key'),
       self::CSRF_TOKEN_LENGTH);
   }
 
   public function validateCSRFToken($token) {
 
     if (!$this->getPHID()) {
       return true;
     }
 
     // When the user posts a form, we check that it contains a valid CSRF token.
     // Tokens cycle each hour (every CSRF_CYLCE_FREQUENCY seconds) and we accept
     // either the current token, the next token (users can submit a "future"
     // token if you have two web frontends that have some clock skew) or any of
     // the last 6 tokens. This means that pages are valid for up to 7 hours.
     // There is also some Javascript which periodically refreshes the CSRF
     // tokens on each page, so theoretically pages should be valid indefinitely.
     // However, this code may fail to run (if the user loses their internet
     // connection, or there's a JS problem, or they don't have JS enabled).
     // Choosing the size of the window in which we accept old CSRF tokens is
     // an issue of balancing concerns between security and usability. We could
     // choose a very narrow (e.g., 1-hour) window to reduce vulnerability to
     // attacks using captured CSRF tokens, but it's also more likely that real
     // users will be affected by this, e.g. if they close their laptop for an
     // hour, open it back up, and try to submit a form before the CSRF refresh
     // can kick in. Since the user experience of submitting a form with expired
     // CSRF is often quite bad (you basically lose data, or it's a big pain to
     // recover at least) and I believe we gain little additional protection
     // by keeping the window very short (the overwhelming value here is in
     // preventing blind attacks, and most attacks which can capture CSRF tokens
     // can also just capture authentication information [sniffing networks]
     // or act as the user [xss]) the 7 hour default seems like a reasonable
     // balance. Other major platforms have much longer CSRF token lifetimes,
     // like Rails (session duration) and Django (forever), which suggests this
     // is a reasonable analysis.
     $csrf_window = 6;
 
     for ($ii = -$csrf_window; $ii <= 1; $ii++) {
       $valid = $this->getCSRFToken($ii);
       if ($token == $valid) {
         return true;
       }
     }
 
     return false;
   }
 
   private function generateToken($epoch, $frequency, $key, $len) {
     $time_block = floor($epoch / $frequency);
     $vec = $this->getPHID().$this->getPasswordHash().$key.$time_block;
     return substr(PhabricatorHash::digest($vec), 0, $len);
   }
 
   /**
    * Issue a new session key to this user. Phabricator supports different
    * types of sessions (like "web" and "conduit") and each session type may
    * have multiple concurrent sessions (this allows a user to be logged in on
    * multiple browsers at the same time, for instance).
    *
    * Note that this method is transport-agnostic and does not set cookies or
    * issue other types of tokens, it ONLY generates a new session key.
    *
    * You can configure the maximum number of concurrent sessions for various
    * session types in the Phabricator configuration.
    *
    * @param   string  Session type, like "web".
    * @return  string  Newly generated session key.
    */
   public function establishSession($session_type) {
     $conn_w = $this->establishConnection('w');
 
     if (strpos($session_type, '-') !== false) {
       throw new Exception("Session type must not contain hyphen ('-')!");
     }
 
     // We allow multiple sessions of the same type, so when a caller requests
     // a new session of type "web", we give them the first available session in
     // "web-1", "web-2", ..., "web-N", up to some configurable limit. If none
     // of these sessions is available, we overwrite the oldest session and
     // reissue a new one in its place.
 
     $session_limit = 1;
     switch ($session_type) {
       case 'web':
         $session_limit = PhabricatorEnv::getEnvConfig('auth.sessions.web');
         break;
       case 'conduit':
         $session_limit = PhabricatorEnv::getEnvConfig('auth.sessions.conduit');
         break;
       default:
         throw new Exception("Unknown session type '{$session_type}'!");
     }
 
     $session_limit = (int)$session_limit;
     if ($session_limit <= 0) {
       throw new Exception(
         "Session limit for '{$session_type}' must be at least 1!");
     }
 
     // NOTE: Session establishment is sensitive to race conditions, as when
     // piping `arc` to `arc`:
     //
     //   arc export ... | arc paste ...
     //
     // To avoid this, we overwrite an old session only if it hasn't been
     // re-established since we read it.
 
     // Consume entropy to generate a new session key, forestalling the eventual
     // heat death of the universe.
     $session_key = Filesystem::readRandomCharacters(40);
 
     // Load all the currently active sessions.
     $sessions = queryfx_all(
       $conn_w,
       'SELECT type, sessionKey, sessionStart FROM %T
         WHERE userPHID = %s AND type LIKE %>',
       PhabricatorUser::SESSION_TABLE,
       $this->getPHID(),
       $session_type.'-');
     $sessions = ipull($sessions, null, 'type');
     $sessions = isort($sessions, 'sessionStart');
 
     $existing_sessions = array_keys($sessions);
 
     // UNGUARDED WRITES: Logging-in users don't have CSRF stuff yet.
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
 
     $retries = 0;
     while (true) {
 
 
       // Choose which 'type' we'll actually establish, i.e. what number we're
       // going to append to the basic session type. To do this, just check all
       // the numbers sequentially until we find an available session.
       $establish_type = null;
       for ($ii = 1; $ii <= $session_limit; $ii++) {
         $try_type = $session_type.'-'.$ii;
         if (!in_array($try_type, $existing_sessions)) {
           $establish_type = $try_type;
           $expect_key = $session_key;
           $existing_sessions[] = $try_type;
 
           // Ensure the row exists so we can issue an update below. We don't
           // care if we race here or not.
           queryfx(
             $conn_w,
             'INSERT IGNORE INTO %T (userPHID, type, sessionKey, sessionStart)
               VALUES (%s, %s, %s, 0)',
             self::SESSION_TABLE,
             $this->getPHID(),
             $establish_type,
             $session_key);
           break;
         }
       }
 
       // If we didn't find an available session, choose the oldest session and
       // overwrite it.
       if (!$establish_type) {
         $oldest = reset($sessions);
         $establish_type = $oldest['type'];
         $expect_key = $oldest['sessionKey'];
       }
 
       // This is so that we'll only overwrite the session if it hasn't been
       // refreshed since we read it. If it has, the session key will be
       // different and we know we're racing other processes. Whichever one
       // won gets the session, we go back and try again.
 
       queryfx(
         $conn_w,
         'UPDATE %T SET sessionKey = %s, sessionStart = UNIX_TIMESTAMP()
           WHERE userPHID = %s AND type = %s AND sessionKey = %s',
         self::SESSION_TABLE,
         $session_key,
         $this->getPHID(),
         $establish_type,
         $expect_key);
 
       if ($conn_w->getAffectedRows()) {
         // The update worked, so the session is valid.
         break;
       } else {
         // We know this just got grabbed, so don't try it again.
         unset($sessions[$establish_type]);
       }
 
       if (++$retries > $session_limit) {
         throw new Exception("Failed to establish a session!");
       }
     }
 
     $log = PhabricatorUserLog::newLog(
       $this,
       $this,
       PhabricatorUserLog::ACTION_LOGIN);
     $log->setDetails(
       array(
         'session_type' => $session_type,
         'session_issued' => $establish_type,
       ));
     $log->setSession($session_key);
     $log->save();
 
     return $session_key;
   }
 
   public function destroySession($session_key) {
     $conn_w = $this->establishConnection('w');
     queryfx(
       $conn_w,
       'DELETE FROM %T WHERE userPHID = %s AND sessionKey = %s',
       self::SESSION_TABLE,
       $this->getPHID(),
       $session_key);
   }
 
   private function generateEmailToken($offset = 0) {
     return $this->generateToken(
       time() + ($offset * self::EMAIL_CYCLE_FREQUENCY),
       self::EMAIL_CYCLE_FREQUENCY,
       PhabricatorEnv::getEnvConfig('phabricator.csrf-key').$this->getEmail(),
       self::EMAIL_TOKEN_LENGTH);
   }
 
   public function validateEmailToken($token) {
     for ($ii = -1; $ii <= 1; $ii++) {
       $valid = $this->generateEmailToken($ii);
       if ($token == $valid) {
         return true;
       }
     }
     return false;
   }
 
   public function getEmailLoginURI() {
     $token = $this->generateEmailToken();
     $uri = PhabricatorEnv::getProductionURI('/login/etoken/'.$token.'/');
     $uri = new PhutilURI($uri);
     return $uri->alter('email', $this->getEmail());
   }
 
   public function loadPreferences() {
     if ($this->preferences) {
       return $this->preferences;
     }
 
     $preferences = id(new PhabricatorUserPreferences())->loadOneWhere(
       'userPHID = %s',
       $this->getPHID());
 
     if (!$preferences) {
       $preferences = new PhabricatorUserPreferences();
       $preferences->setUserPHID($this->getPHID());
 
       $default_dict = array(
         PhabricatorUserPreferences::PREFERENCE_TITLES => 'glyph',
+        PhabricatorUserPreferences::PREFERENCE_EDITOR => '',
         PhabricatorUserPreferences::PREFERENCE_MONOSPACED => '');
 
       $preferences->setPreferences($default_dict);
     }
 
     $this->preferences = $preferences;
     return $preferences;
   }
 
+  public function loadEditorLink($path,
+                                 $line,
+                                 PhabricatorRepository $repository) {
+    $editor = $this->loadPreferences()->getPreference(
+      PhabricatorUserPreferences::PREFERENCE_EDITOR);
+    if ($editor) {
+      return strtr($editor, array(
+        '%f' => phutil_escape_uri($path),
+        '%l' => phutil_escape_uri($line),
+        '%r' => phutil_escape_uri($repository->getCallsign()),
+      ));
+    }
+  }
+
   private static function tokenizeName($name) {
     if (function_exists('mb_strtolower')) {
       $name = mb_strtolower($name, 'UTF-8');
     } else {
       $name = strtolower($name);
     }
     $name = trim($name);
     if (!strlen($name)) {
       return array();
     }
     return preg_split('/\s+/', $name);
   }
 
   /**
    * Populate the nametoken table, which used to fetch typeahead results. When
    * a user types "linc", we want to match "Abraham Lincoln" from on-demand
    * typeahead sources. To do this, we need a separate table of name fragments.
    */
   public function updateNameTokens() {
     $tokens = array_merge(
       self::tokenizeName($this->getRealName()),
       self::tokenizeName($this->getUserName()));
     $tokens = array_unique($tokens);
     $table  = self::NAMETOKEN_TABLE;
     $conn_w = $this->establishConnection('w');
 
     $sql = array();
     foreach ($tokens as $token) {
       $sql[] = qsprintf(
         $conn_w,
         '(%d, %s)',
         $this->getID(),
         $token);
     }
 
     queryfx(
       $conn_w,
       'DELETE FROM %T WHERE userID = %d',
       $table,
       $this->getID());
     if ($sql) {
       queryfx(
         $conn_w,
         'INSERT INTO %T (userID, token) VALUES %Q',
         $table,
         implode(', ', $sql));
     }
   }
 
   public function sendWelcomeEmail(PhabricatorUser $admin) {
     $admin_username = $admin->getUserName();
     $admin_realname = $admin->getRealName();
     $user_username = $this->getUserName();
     $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
 
     $base_uri = PhabricatorEnv::getProductionURI('/');
 
     $uri = $this->getEmailLoginURI();
     $body = <<<EOBODY
 Welcome to Phabricator!
 
 {$admin_username} ({$admin_realname}) has created an account for you.
 
   Username: {$user_username}
 
 To login to Phabricator, follow this link and set a password:
 
   {$uri}
 
 After you have set a password, you can login in the future by going here:
 
   {$base_uri}
 
 EOBODY;
 
     if (!$is_serious) {
       $body .= <<<EOBODY
 
 Love,
 Phabricator
 
 EOBODY;
     }
 
     $mail = id(new PhabricatorMetaMTAMail())
       ->addTos(array($this->getPHID()))
       ->setSubject('[Phabricator] Welcome to Phabricator')
       ->setBody($body)
       ->setFrom($admin->getPHID())
       ->saveAndSend();
   }
 
   public static function validateUsername($username) {
     return (bool)preg_match('/^[a-zA-Z0-9]+$/', $username);
   }
 
 }
diff --git a/src/applications/people/storage/user/__init__.php b/src/applications/people/storage/user/__init__.php
index 29e3e21402..910e09c634 100644
--- a/src/applications/people/storage/user/__init__.php
+++ b/src/applications/people/storage/user/__init__.php
@@ -1,27 +1,28 @@
 <?php
 /**
  * This file is automatically generated. Lint this module to rebuild it.
  * @generated
  */
 
 
 
 phutil_require_module('phabricator', 'aphront/writeguard');
 phutil_require_module('phabricator', 'applications/metamta/storage/mail');
 phutil_require_module('phabricator', 'applications/people/storage/base');
 phutil_require_module('phabricator', 'applications/people/storage/log');
 phutil_require_module('phabricator', 'applications/people/storage/preferences');
 phutil_require_module('phabricator', 'applications/phid/constants');
 phutil_require_module('phabricator', 'applications/phid/storage/phid');
 phutil_require_module('phabricator', 'applications/search/index/indexer/user');
 phutil_require_module('phabricator', 'infrastructure/env');
 phutil_require_module('phabricator', 'infrastructure/util/hash');
 phutil_require_module('phabricator', 'storage/qsprintf');
 phutil_require_module('phabricator', 'storage/queryfx');
 
 phutil_require_module('phutil', 'filesystem');
+phutil_require_module('phutil', 'markup');
 phutil_require_module('phutil', 'parser/uri');
 phutil_require_module('phutil', 'utils');
 
 
 phutil_require_source('PhabricatorUser.php');
diff --git a/webroot/rsrc/js/application/differential/behavior-dropdown-menus.js b/webroot/rsrc/js/application/differential/behavior-dropdown-menus.js
index 3afe7b4598..73dba5650b 100644
--- a/webroot/rsrc/js/application/differential/behavior-dropdown-menus.js
+++ b/webroot/rsrc/js/application/differential/behavior-dropdown-menus.js
@@ -1,84 +1,93 @@
 /**
  * @provides javelin-behavior-differential-dropdown-menus
  * @requires javelin-behavior
  *           javelin-dom
  *           javelin-util
  *           javelin-stratcom
  *           phabricator-dropdown-menu
  *           phabricator-menu-item
  */
 
 JX.behavior('differential-dropdown-menus', function(config) {
 
   function build_menu(button, data) {
 
     function show_more() {
       var container = JX.$(data.containerID);
       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') {
             continue;
           }
           var event_data = {
             context : nodes[ii],
             show : show[jj]
           };
           JX.Stratcom.invoke('differential-reveal-context', null, event_data);
         }
       }
     }
 
     function link_to(name, uri) {
       var item = new JX.PhabricatorMenuItem(
         name,
         JX.bind(null, window.open, uri),
         uri);
       item.setDisabled(!uri);
       return item;
     }
 
     var reveal_item = new JX.PhabricatorMenuItem('', show_more);
 
     var diffusion_item = link_to('Browse in Diffusion', data.diffusionURI);
     if (!data.diffusionURI) {
       diffusion_item.setDisabled(true);
     }
 
     var menu = new JX.PhabricatorDropdownMenu(buttons[ii])
       .addItem(reveal_item)
       .addItem(diffusion_item)
       .addItem(link_to('View Standalone', data.detailURI));
     if (data.leftURI) {
       menu.addItem(link_to('Show Raw File (Left)', data.leftURI));
     }
     if (data.rightURI) {
       menu.addItem(link_to('Show Raw File (Right)', data.rightURI));
     }
+    if (data.editor) {
+      menu.addItem(new JX.PhabricatorMenuItem(
+        'Open in Editor',
+        JX.bind(null, location.assign, data.editor), // Open in the same window.
+        data.editor));
+    }
+    if (data.editorConfigure) {
+      menu.addItem(link_to('Configure Editor', data.editorConfigure));
+    }
 
     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);
           reveal_item.setName('Show Entire File');
         } else {
           reveal_item.setDisabled(true);
           reveal_item.setName('Entire File Shown');
         }
       });
   }
 
   var buttons = JX.DOM.scry(window.document, 'a', 'differential-view-options');
   for (var ii = 0; ii < buttons.length; ii++) {
     build_menu(buttons[ii], JX.Stratcom.getData(buttons[ii]));
   }
 
 });