diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index 7647a8ef85..9d9939698c 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1,284 +1,296 @@
 <?php
 
 /**
  * This file is automatically generated. Use 'phutil_mapper.php' to rebuild it.
  * @generated
  */
 
 phutil_register_library_map(array(
   'class' =>
   array(
     'Aphront404Response' => 'aphront/response/404',
     'AphrontAjaxResponse' => 'aphront/response/ajax',
     'AphrontApplicationConfiguration' => 'aphront/applicationconfiguration',
     'AphrontController' => 'aphront/controller',
     'AphrontDatabaseConnection' => 'storage/connection/base',
     'AphrontDefaultApplicationConfiguration' => 'aphront/default/configuration',
     'AphrontDefaultApplicationController' => 'aphront/default/controller',
     'AphrontDialogResponse' => 'aphront/response/dialog',
     'AphrontDialogView' => 'view/dialog',
     'AphrontErrorView' => 'view/form/error',
     'AphrontException' => 'aphront/exception/base',
     'AphrontFileResponse' => 'aphront/response/file',
     'AphrontFormCheckboxControl' => 'view/form/control/checkbox',
     'AphrontFormControl' => 'view/form/control/base',
     'AphrontFormFileControl' => 'view/form/control/file',
     'AphrontFormMarkupControl' => 'view/form/control/markup',
     'AphrontFormSelectControl' => 'view/form/control/select',
     'AphrontFormStaticControl' => 'view/form/control/static',
     'AphrontFormSubmitControl' => 'view/form/control/submit',
     'AphrontFormTextAreaControl' => 'view/form/control/textarea',
     'AphrontFormTextControl' => 'view/form/control/text',
     'AphrontFormTokenizerControl' => 'view/form/control/tokenizer',
     'AphrontFormView' => 'view/form/base',
     'AphrontMySQLDatabaseConnection' => 'storage/connection/mysql',
     'AphrontNullView' => 'view/null',
     'AphrontPageView' => 'view/page/base',
     'AphrontPanelView' => 'view/layout/panel',
     'AphrontQueryConnectionException' => 'storage/exception/connection',
     'AphrontQueryConnectionLostException' => 'storage/exception/connectionlost',
     'AphrontQueryCountException' => 'storage/exception/count',
     'AphrontQueryException' => 'storage/exception/base',
     'AphrontQueryObjectMissingException' => 'storage/exception/objectmissing',
     'AphrontQueryParameterException' => 'storage/exception/parameter',
     'AphrontQueryRecoverableException' => 'storage/exception/recoverable',
     'AphrontRedirectException' => 'aphront/exception/redirect',
     'AphrontRedirectResponse' => 'aphront/response/redirect',
     'AphrontRequest' => 'aphront/request',
     'AphrontResponse' => 'aphront/response/base',
     'AphrontSideNavView' => 'view/layout/sidenav',
     'AphrontTableView' => 'view/control/table',
     'AphrontURIMapper' => 'aphront/mapper',
     'AphrontView' => 'view/base',
     'AphrontWebpageResponse' => 'aphront/response/webpage',
     'CelerityAPI' => 'infratructure/celerity/api',
     'CelerityResourceController' => 'infratructure/celerity/controller',
     'CelerityResourceMap' => 'infratructure/celerity/map',
     'CelerityStaticResourceResponse' => 'infratructure/celerity/response',
     'ConduitAPIMethod' => 'applications/conduit/method/base',
     'ConduitAPIRequest' => 'applications/conduit/protocol/request',
     'ConduitAPI_conduit_connect_Method' => 'applications/conduit/method/conduit/connect',
     'ConduitAPI_differential_creatediff_Method' => 'applications/conduit/method/differential/creatediff',
     'ConduitAPI_differential_setdiffproperty_Method' => 'applications/conduit/method/differential/setdiffproperty',
     'ConduitAPI_file_upload_Method' => 'applications/conduit/method/file/upload',
     'ConduitAPI_user_find_Method' => 'applications/conduit/method/user/find',
     'ConduitException' => 'applications/conduit/protocol/exception',
     'DifferentialAction' => 'applications/differential/constants/action',
+    'DifferentialCCWelcomeMail' => 'applications/differential/mail/ccwelcome',
     'DifferentialChangeType' => 'applications/differential/constants/changetype',
     'DifferentialChangeset' => 'applications/differential/storage/changeset',
     'DifferentialChangesetDetailView' => 'applications/differential/view/changesetdetailview',
     'DifferentialChangesetListView' => 'applications/differential/view/changesetlistview',
     'DifferentialChangesetParser' => 'applications/differential/parser/changeset',
     'DifferentialChangesetViewController' => 'applications/differential/controller/changesetview',
     'DifferentialController' => 'applications/differential/controller/base',
     'DifferentialDAO' => 'applications/differential/storage/base',
     'DifferentialDiff' => 'applications/differential/storage/diff',
+    'DifferentialDiffContentMail' => 'applications/differential/mail/diffcontent',
     'DifferentialDiffProperty' => 'applications/differential/storage/diffproperty',
     'DifferentialDiffTableOfContentsView' => 'applications/differential/view/difftableofcontents',
     'DifferentialDiffViewController' => 'applications/differential/controller/diffview',
+    'DifferentialFeedbackMail' => 'applications/differential/mail/feedback',
     'DifferentialHunk' => 'applications/differential/storage/hunk',
     'DifferentialLintStatus' => 'applications/differential/constants/lintstatus',
+    'DifferentialMail' => 'applications/differential/mail/base',
+    'DifferentialNewDiffMail' => 'applications/differential/mail/newdiff',
+    'DifferentialReviewRequestMail' => 'applications/differential/mail/reviewrequest',
     'DifferentialRevision' => 'applications/differential/storage/revision',
     'DifferentialRevisionControlSystem' => 'applications/differential/constants/revisioncontrolsystem',
     'DifferentialRevisionEditController' => 'applications/differential/controller/revisionedit',
+    'DifferentialRevisionEditor' => 'applications/differential/editor/revision',
     'DifferentialRevisionListController' => 'applications/differential/controller/revisionlist',
     'DifferentialRevisionStatus' => 'applications/differential/constants/revisionstatus',
     'DifferentialUnitStatus' => 'applications/differential/constants/unitstatus',
     'Javelin' => 'infratructure/javelin/api',
     'LiskDAO' => 'storage/lisk/dao',
     'PhabricatorAuthController' => 'applications/auth/controlller/base',
     'PhabricatorConduitAPIController' => 'applications/conduit/controller/api',
     'PhabricatorConduitConnectionLog' => 'applications/conduit/storage/connectionlog',
     'PhabricatorConduitConsoleController' => 'applications/conduit/controller/console',
     'PhabricatorConduitController' => 'applications/conduit/controller/base',
     'PhabricatorConduitDAO' => 'applications/conduit/storage/base',
     'PhabricatorConduitLogController' => 'applications/conduit/controller/log',
     'PhabricatorConduitMethodCallLog' => 'applications/conduit/storage/methodcalllog',
     'PhabricatorController' => 'applications/base/controller/base',
     'PhabricatorDirectoryCategory' => 'applications/directory/storage/category',
     'PhabricatorDirectoryCategoryDeleteController' => 'applications/directory/controller/categorydelete',
     'PhabricatorDirectoryCategoryEditController' => 'applications/directory/controller/categoryedit',
     'PhabricatorDirectoryCategoryListController' => 'applications/directory/controller/categorylist',
     'PhabricatorDirectoryController' => 'applications/directory/controller/base',
     'PhabricatorDirectoryDAO' => 'applications/directory/storage/base',
     'PhabricatorDirectoryItem' => 'applications/directory/storage/item',
     'PhabricatorDirectoryItemDeleteController' => 'applications/directory/controller/itemdelete',
     'PhabricatorDirectoryItemEditController' => 'applications/directory/controller/itemedit',
     'PhabricatorDirectoryItemListController' => 'applications/directory/controller/itemlist',
     'PhabricatorDirectoryMainController' => 'applications/directory/controller/main',
     'PhabricatorFile' => 'applications/files/storage/file',
     'PhabricatorFileController' => 'applications/files/controller/base',
     'PhabricatorFileDAO' => 'applications/files/storage/base',
     'PhabricatorFileListController' => 'applications/files/controller/list',
     'PhabricatorFileStorageBlob' => 'applications/files/storage/storageblob',
     'PhabricatorFileURI' => 'applications/files/uri',
     'PhabricatorFileUploadController' => 'applications/files/controller/upload',
     'PhabricatorFileViewController' => 'applications/files/controller/view',
     'PhabricatorLiskDAO' => 'applications/base/storage/lisk',
     'PhabricatorLoginController' => 'applications/auth/controlller/login',
     'PhabricatorMailImplementationAdapter' => 'applications/metamta/adapter/base',
     'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'applications/metamta/adapter/phpmailerlite',
     'PhabricatorMetaMTAController' => 'applications/metamta/controller/base',
     'PhabricatorMetaMTADAO' => 'applications/metamta/storage/base',
     'PhabricatorMetaMTAListController' => 'applications/metamta/controller/list',
     'PhabricatorMetaMTAMail' => 'applications/metamta/storage/mail',
     'PhabricatorMetaMTAMailingList' => 'applications/metamta/storage/mailinglist',
     'PhabricatorMetaMTAMailingListEditController' => 'applications/metamta/controller/mailinglistedit',
     'PhabricatorMetaMTAMailingListsController' => 'applications/metamta/controller/mailinglists',
     'PhabricatorMetaMTASendController' => 'applications/metamta/controller/send',
     'PhabricatorMetaMTAViewController' => 'applications/metamta/controller/view',
     'PhabricatorObjectHandle' => 'applications/phid/handle',
     'PhabricatorObjectHandleData' => 'applications/phid/handle/data',
     'PhabricatorPHID' => 'applications/phid/storage/phid',
     'PhabricatorPHIDAllocateController' => 'applications/phid/controller/allocate',
     'PhabricatorPHIDController' => 'applications/phid/controller/base',
     'PhabricatorPHIDDAO' => 'applications/phid/storage/base',
     'PhabricatorPHIDListController' => 'applications/phid/controller/list',
     'PhabricatorPHIDLookupController' => 'applications/phid/controller/lookup',
     'PhabricatorPHIDType' => 'applications/phid/storage/type',
     'PhabricatorPHIDTypeEditController' => 'applications/phid/controller/typeedit',
     'PhabricatorPHIDTypeListController' => 'applications/phid/controller/typelist',
     'PhabricatorPeopleController' => 'applications/people/controller/base',
     'PhabricatorPeopleEditController' => 'applications/people/controller/edit',
     'PhabricatorPeopleListController' => 'applications/people/controller/list',
     'PhabricatorPeopleProfileController' => 'applications/people/controller/profile',
     'PhabricatorStandardPageView' => 'view/page/standard',
     'PhabricatorTypeaheadCommonDatasourceController' => 'applications/typeahead/controller/common',
     'PhabricatorTypeaheadDatasourceController' => 'applications/typeahead/controller/base',
     'PhabricatorUser' => 'applications/people/storage/user',
     'PhabricatorUserDAO' => 'applications/people/storage/base',
   ),
   'function' =>
   array(
     '_qsprintf_check_scalar_type' => 'storage/qsprintf',
     '_qsprintf_check_type' => 'storage/qsprintf',
     'celerity_generate_unique_node_id' => 'infratructure/celerity/api',
     'celerity_register_resource_map' => 'infratructure/celerity/map',
     'javelin_render_tag' => 'infratructure/javelin/markup',
     'qsprintf' => 'storage/qsprintf',
     'queryfx' => 'storage/queryfx',
     'queryfx_all' => 'storage/queryfx',
     'queryfx_one' => 'storage/queryfx',
     'require_celerity_resource' => 'infratructure/celerity/api',
     'vqsprintf' => 'storage/qsprintf',
     'vqueryfx' => 'storage/queryfx',
     'xsprintf_query' => 'storage/qsprintf',
   ),
   'requires_class' =>
   array(
     'Aphront404Response' => 'AphrontResponse',
     'AphrontAjaxResponse' => 'AphrontResponse',
     'AphrontDefaultApplicationConfiguration' => 'AphrontApplicationConfiguration',
     'AphrontDefaultApplicationController' => 'AphrontController',
     'AphrontDialogResponse' => 'AphrontResponse',
     'AphrontDialogView' => 'AphrontView',
     'AphrontErrorView' => 'AphrontView',
     'AphrontFileResponse' => 'AphrontResponse',
     'AphrontFormCheckboxControl' => 'AphrontFormControl',
     'AphrontFormControl' => 'AphrontView',
     'AphrontFormFileControl' => 'AphrontFormControl',
     'AphrontFormMarkupControl' => 'AphrontFormControl',
     'AphrontFormSelectControl' => 'AphrontFormControl',
     'AphrontFormStaticControl' => 'AphrontFormControl',
     'AphrontFormSubmitControl' => 'AphrontFormControl',
     'AphrontFormTextAreaControl' => 'AphrontFormControl',
     'AphrontFormTextControl' => 'AphrontFormControl',
     'AphrontFormTokenizerControl' => 'AphrontFormControl',
     'AphrontFormView' => 'AphrontView',
     'AphrontMySQLDatabaseConnection' => 'AphrontDatabaseConnection',
     'AphrontNullView' => 'AphrontView',
     'AphrontPageView' => 'AphrontView',
     'AphrontPanelView' => 'AphrontView',
     'AphrontQueryConnectionException' => 'AphrontQueryException',
     'AphrontQueryConnectionLostException' => 'AphrontQueryRecoverableException',
     'AphrontQueryCountException' => 'AphrontQueryException',
     'AphrontQueryObjectMissingException' => 'AphrontQueryException',
     'AphrontQueryParameterException' => 'AphrontQueryException',
     'AphrontQueryRecoverableException' => 'AphrontQueryException',
     'AphrontRedirectException' => 'AphrontException',
     'AphrontRedirectResponse' => 'AphrontResponse',
     'AphrontSideNavView' => 'AphrontView',
     'AphrontTableView' => 'AphrontView',
     'AphrontWebpageResponse' => 'AphrontResponse',
     'CelerityResourceController' => 'AphrontController',
     'ConduitAPI_conduit_connect_Method' => 'ConduitAPIMethod',
     'ConduitAPI_differential_creatediff_Method' => 'ConduitAPIMethod',
     'ConduitAPI_differential_setdiffproperty_Method' => 'ConduitAPIMethod',
     'ConduitAPI_file_upload_Method' => 'ConduitAPIMethod',
     'ConduitAPI_user_find_Method' => 'ConduitAPIMethod',
+    'DifferentialCCWelcomeMail' => 'DifferentialReviewRequestMail',
     'DifferentialChangeset' => 'DifferentialDAO',
     'DifferentialChangesetDetailView' => 'AphrontView',
     'DifferentialChangesetListView' => 'AphrontView',
     'DifferentialChangesetViewController' => 'DifferentialController',
     'DifferentialController' => 'PhabricatorController',
     'DifferentialDAO' => 'PhabricatorLiskDAO',
     'DifferentialDiff' => 'DifferentialDAO',
+    'DifferentialDiffContentMail' => 'DifferentialMail',
     'DifferentialDiffProperty' => 'DifferentialDAO',
     'DifferentialDiffTableOfContentsView' => 'AphrontView',
     'DifferentialDiffViewController' => 'DifferentialController',
+    'DifferentialFeedbackMail' => 'DifferentialMail',
     'DifferentialHunk' => 'DifferentialDAO',
+    'DifferentialNewDiffMail' => 'DifferentialReviewRequestMail',
+    'DifferentialReviewRequestMail' => 'DifferentialMail',
     'DifferentialRevision' => 'DifferentialDAO',
     'DifferentialRevisionEditController' => 'DifferentialController',
     'DifferentialRevisionListController' => 'DifferentialController',
     'PhabricatorAuthController' => 'PhabricatorController',
     'PhabricatorConduitAPIController' => 'PhabricatorConduitController',
     'PhabricatorConduitConnectionLog' => 'PhabricatorConduitDAO',
     'PhabricatorConduitConsoleController' => 'PhabricatorConduitController',
     'PhabricatorConduitController' => 'PhabricatorController',
     'PhabricatorConduitDAO' => 'PhabricatorLiskDAO',
     'PhabricatorConduitLogController' => 'PhabricatorConduitController',
     'PhabricatorConduitMethodCallLog' => 'PhabricatorConduitDAO',
     'PhabricatorController' => 'AphrontController',
     'PhabricatorDirectoryCategory' => 'PhabricatorDirectoryDAO',
     'PhabricatorDirectoryCategoryDeleteController' => 'PhabricatorDirectoryController',
     'PhabricatorDirectoryCategoryEditController' => 'PhabricatorDirectoryController',
     'PhabricatorDirectoryCategoryListController' => 'PhabricatorDirectoryController',
     'PhabricatorDirectoryController' => 'PhabricatorController',
     'PhabricatorDirectoryDAO' => 'PhabricatorLiskDAO',
     'PhabricatorDirectoryItem' => 'PhabricatorDirectoryDAO',
     'PhabricatorDirectoryItemDeleteController' => 'PhabricatorDirectoryController',
     'PhabricatorDirectoryItemEditController' => 'PhabricatorDirectoryController',
     'PhabricatorDirectoryItemListController' => 'PhabricatorDirectoryController',
     'PhabricatorDirectoryMainController' => 'PhabricatorDirectoryController',
     'PhabricatorFile' => 'PhabricatorFileDAO',
     'PhabricatorFileController' => 'PhabricatorController',
     'PhabricatorFileDAO' => 'PhabricatorLiskDAO',
     'PhabricatorFileListController' => 'PhabricatorFileController',
     'PhabricatorFileStorageBlob' => 'PhabricatorFileDAO',
     'PhabricatorFileUploadController' => 'PhabricatorFileController',
     'PhabricatorFileViewController' => 'PhabricatorFileController',
     'PhabricatorLiskDAO' => 'LiskDAO',
     'PhabricatorLoginController' => 'PhabricatorAuthController',
     'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'PhabricatorMailImplementationAdapter',
     'PhabricatorMetaMTAController' => 'PhabricatorController',
     'PhabricatorMetaMTADAO' => 'PhabricatorLiskDAO',
     'PhabricatorMetaMTAListController' => 'PhabricatorMetaMTAController',
     'PhabricatorMetaMTAMail' => 'PhabricatorMetaMTADAO',
     'PhabricatorMetaMTAMailingList' => 'PhabricatorMetaMTADAO',
     'PhabricatorMetaMTAMailingListEditController' => 'PhabricatorMetaMTAController',
     'PhabricatorMetaMTAMailingListsController' => 'PhabricatorMetaMTAController',
     'PhabricatorMetaMTASendController' => 'PhabricatorMetaMTAController',
     'PhabricatorMetaMTAViewController' => 'PhabricatorMetaMTAController',
     'PhabricatorPHID' => 'PhabricatorPHIDDAO',
     'PhabricatorPHIDAllocateController' => 'PhabricatorPHIDController',
     'PhabricatorPHIDController' => 'PhabricatorController',
     'PhabricatorPHIDDAO' => 'PhabricatorLiskDAO',
     'PhabricatorPHIDListController' => 'PhabricatorPHIDController',
     'PhabricatorPHIDLookupController' => 'PhabricatorPHIDController',
     'PhabricatorPHIDType' => 'PhabricatorPHIDDAO',
     'PhabricatorPHIDTypeEditController' => 'PhabricatorPHIDController',
     'PhabricatorPHIDTypeListController' => 'PhabricatorPHIDController',
     'PhabricatorPeopleController' => 'PhabricatorController',
     'PhabricatorPeopleEditController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleListController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleProfileController' => 'PhabricatorPeopleController',
     'PhabricatorStandardPageView' => 'AphrontPageView',
     'PhabricatorTypeaheadCommonDatasourceController' => 'PhabricatorTypeaheadDatasourceController',
     'PhabricatorTypeaheadDatasourceController' => 'PhabricatorController',
     'PhabricatorUser' => 'PhabricatorUserDAO',
     'PhabricatorUserDAO' => 'PhabricatorLiskDAO',
   ),
   'requires_interface' =>
   array(
   ),
 ));
diff --git a/src/applications/conduit/controller/api/PhabricatorConduitAPIController.php b/src/applications/conduit/controller/api/PhabricatorConduitAPIController.php
index e3bd8e0e5a..9cd95681c4 100644
--- a/src/applications/conduit/controller/api/PhabricatorConduitAPIController.php
+++ b/src/applications/conduit/controller/api/PhabricatorConduitAPIController.php
@@ -1,185 +1,189 @@
 <?php
 
 /*
  * Copyright 2011 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 PhabricatorConduitAPIController
   extends PhabricatorConduitController {
 
+  public function shouldRequireLogin() {
+    return false;
+  }
+
   private $method;
 
   public function willProcessRequest(array $data) {
     $this->method = $data['method'];
     return $this;
   }
 
   public function processRequest() {
     $time_start = microtime(true);
     $request = $this->getRequest();
 
     $method = $this->method;
 
     $method_class = ConduitAPIMethod::getClassNameFromAPIMethodName($method);
     $api_request = null;
 
     $log = new PhabricatorConduitMethodCallLog();
     $log->setMethod($method);
     $metadata = array();
 
     try {
 
       if (!class_exists($method_class)) {
         throw new Exception(
           "Unable to load the implementation class for method '{$method}'. ".
           "You may have misspelled the method, need to define ".
           "'{$method_class}', or need to run 'arc build'.");
       }
 
       // Fake out checkModule, the class has already been autoloaded by the
       // class_exists() call above.
       $method_handler = newv($method_class, array());
 
       if (isset($_REQUEST['params']) && is_array($_REQUEST['params'])) {
         $params_post = $request->getArr('params');
         foreach ($params_post as $key => $value) {
           $params_post[$key] = json_decode($value, true);
         }
         $params = $params_post;
       } else {
         $params_json = $request->getStr('params');
         if (!strlen($params_json)) {
           $params = array();
         } else {
           $params = json_decode($params_json, true);
           if (!is_array($params)) {
             throw new Exception(
               "Invalid parameter information was passed to method ".
               "'{$method}', could not decode JSON serialization.");
           }
         }
       }
 
       $metadata = idx($params, '__conduit__', array());
       unset($params['__conduit__']);
 
       $api_request = new ConduitAPIRequest($params);
 
       try {
         $result = $method_handler->executeMethod($api_request);
         $error_code = null;
         $error_info = null;
       } catch (ConduitException $ex) {
         $result = null;
         $error_code = $ex->getMessage();
         $error_info = $method_handler->getErrorDescription($error_code);
       }
     } catch (Exception $ex) {
       $result = null;
       $error_code = 'ERR-CONDUIT-CORE';
       $error_info = $ex->getMessage();
     }
 
     $time_end = microtime(true);
 
     $connection_id = null;
     if (idx($metadata, 'connectionID')) {
       $connection_id = $metadata['connectionID'];
     } else if (($method == 'conduit.connect') && $result) {
       $connection_id = idx($result, 'connectionID');
     }
 
     $log->setConnectionID($connection_id);
     $log->setError((string)$error_code);
     $log->setDuration(1000000 * ($time_end - $time_start));
     $log->save();
 
     $result = array(
       'result'      => $result,
       'error_code'  => $error_code,
       'error_info'  => $error_info,
     );
 
     switch ($request->getStr('output')) {
       case 'human':
         return $this->buildHumanReadableResponse(
           $method,
           $api_request,
           $result);
       case 'json':
       default:
         return id(new AphrontFileResponse())
           ->setMimeType('application/json')
           ->setContent(json_encode($result));
     }
   }
 
   private function buildHumanReadableResponse(
     $method,
     ConduitAPIRequest $request = null,
     $result = null) {
 
     $param_rows = array();
     $param_rows[] = array('Method', phutil_escape_html($method));
     if ($request) {
       foreach ($request->getAllParameters() as $key => $value) {
         $param_rows[] = array(
           phutil_escape_html($key),
           phutil_escape_html(json_encode($value)),
         );
       }
     }
 
     $param_table = new AphrontTableView($param_rows);
     $param_table->setColumnClasses(
       array(
         'header',
         'wide',
       ));
 
     $result_rows = array();
     foreach ($result as $key => $value) {
       $result_rows[] = array(
         phutil_escape_html($key),
         phutil_escape_html(json_encode($value)),
       );
     }
 
     $result_table = new AphrontTableView($result_rows);
     $result_table->setColumnClasses(
       array(
         'header',
         'wide',
       ));
 
     $param_panel = new AphrontPanelView();
     $param_panel->setHeader('Method Parameters');
     $param_panel->appendChild($param_table);
 
     $result_panel = new AphrontPanelView();
     $result_panel->setHeader('Method Result');
     $result_panel->appendChild($result_table);
 
     return $this->buildStandardPageResponse(
       array(
         $param_panel,
         $result_panel,
       ),
       array(
         'title' => 'Method Call Result',
       ));
   }
 
 }
diff --git a/src/applications/differential/controller/diffview/DifferentialDiffViewController.php b/src/applications/differential/controller/diffview/DifferentialDiffViewController.php
index 67829ec10b..2e686d1a16 100644
--- a/src/applications/differential/controller/diffview/DifferentialDiffViewController.php
+++ b/src/applications/differential/controller/diffview/DifferentialDiffViewController.php
@@ -1,84 +1,86 @@
 <?php
 
 /*
  * Copyright 2011 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() {
     $diff = id(new DifferentialDiff())->load($this->id);
     if (!$diff) {
       return new Aphront404Response();
     }
 
     $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>.');
 
     $action_form = new AphrontFormView();
     $action_form
       ->setAction('/differential/revision/edit/')
+      ->addHiddenInput('diffID', $diff->getID())
+      ->addHiddenInput('viaDiffView', 1)
       ->appendChild(
         id(new AphrontFormSelectControl())
           ->setLabel('Attach To')
           ->setName('revisionID')
           ->setValue('')
           ->setOptions(array(
             '' => "Create a new Revision...",
           )))
       ->appendChild(
         id(new AphrontFormSubmitControl())
           ->setValue('Continue'));
 
     $action_panel->appendChild($action_form);
 
 
 
     $changesets = $diff->loadChangesets();
     $changesets = msort($changesets, 'getSortKey');
 
     $table_of_contents = id(new DifferentialDiffTableOfContentsView())
       ->setChangesets($changesets);
 
     $details = id(new DifferentialChangesetListView())
       ->setChangesets($changesets);
 
     return $this->buildStandardPageResponse(
       '<div class="differential-primary-pane">'.
         implode(
           "\n",
           array(
             $action_panel->render(),
             $table_of_contents->render(),
             $details->render(),
           )).
       '</div>',
       array(
         'title' => 'Diff View',
       ));
   }
 
 }
diff --git a/src/applications/differential/controller/revisionedit/DifferentialRevisionEditController.php b/src/applications/differential/controller/revisionedit/DifferentialRevisionEditController.php
index 1f9a1679dc..4e38ac654e 100644
--- a/src/applications/differential/controller/revisionedit/DifferentialRevisionEditController.php
+++ b/src/applications/differential/controller/revisionedit/DifferentialRevisionEditController.php
@@ -1,145 +1,192 @@
 <?php
 
 /*
  * Copyright 2011 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 DifferentialRevisionEditController extends DifferentialController {
 
   private $id;
 
   public function willProcessRequest(array $data) {
     $this->id = idx($data, 'id');
   }
 
   public function processRequest() {
 
     if ($this->id) {
       $revision = id(new DifferentialRevision())->load($this->id);
       if (!$revision) {
         return new Aphront404Response();
       }
     } else {
       $revision = new DifferentialRevision();
     }
-/*
-    $e_name = true;
-    $errors = array();
 
     $request = $this->getRequest();
-    if ($request->isFormPost()) {
-      $category->setName($request->getStr('name'));
-      $category->setSequence($request->getStr('sequence'));
+    $diff_id = $request->getInt('diffID');
+    if ($diff_id) {
+      $diff = id(new DifferentialDiff())->load($diff_id);
+      if (!$diff) {
+        return new Aphront404Response();
+      }
+      if ($diff->getRevisionID()) {
+        // TODO: Redirect?
+        throw new Exception("This diff is already attached to a revision!");
+      }
+    } else {
+      $diff = null;
+    }
+
+    $e_title = true;
+    $e_testplan = true;
+    $errors = array();
 
-      if (!strlen($category->getName())) {
-        $errors[] = 'Category name is required.';
-        $e_name = 'Required';
+    if ($request->isFormPost() && !$request->getStr('viaDiffView')) {
+      $revision->setTitle($request->getStr('title'));
+      $revision->setSummary($request->getStr('summary'));
+      $revision->setTestPlan($request->getStr('testplan'));
+      $revision->setBlameRevision($request->getStr('blame'));
+      $revision->setRevertPlan($request->getStr('revert'));
+
+      if (!strlen(trim($revision->getTitle()))) {
+        $errors[] = 'You must provide a title.';
+        $e_title = 'Required';
+      }
+
+      if (!strlen(trim($revision->getTestPlan()))) {
+        $errors[] = 'You must provide a test plan.';
+        $e_testplan = 'Required';
+      }
+
+      $user_phid = $request->getUser()->getPHID();
+
+      if (in_array($user_phid, $request->getArr('reviewers'))) {
+        $errors[] = 'You may not review your own revision.';
       }
 
       if (!$errors) {
-        $category->save();
-        return id(new AphrontRedirectResponse())
-          ->setURI('/directory/category/');
+        $editor = new DifferentialRevisionEditor($revision, $user_phid);
+        if ($diff) {
+          $editor->addDiff($diff, $request->getStr('comments'));
+        }
+        $editor->setCCPHIDs($request->getArr('cc'));
+        $editor->setReviewers($request->getArr('reviewers'));
+        $editor->save();
+
+        $response = id(new AphrontRedirectResponse())
+          ->setURI('/D'.$revision->getID());
       }
-    }
 
-    $error_view = null;
-    if ($errors) {
-      $error_view = id(new AphrontErrorView())
-        ->setTitle('Form Errors')
-        ->setErrors($errors);
+      $reviewer_phids = $request->getArr('reviewers');
+      $cc_phids = $request->getArr('cc');
+    } else {
+//      $reviewer_phids = $revision->getReviewers();
+//      $cc_phids = $revision->getCCPHIDs();
+      $reviewer_phids = array();
+      $cc_phids = array();
     }
-*/
-    $e_name = true;
-    $e_testplan = true;
 
     $form = new AphrontFormView();
+    if ($diff) {
+      $form->addHiddenInput('diffID', $diff->getID());
+    }
+
     if ($revision->getID()) {
       $form->setAction('/differential/revision/edit/'.$revision->getID().'/');
     } else {
       $form->setAction('/differential/revision/edit/');
     }
 
-    $reviewer_map = array(
-      1 => 'A Zebra',
-      2 => 'Pie Messenger',
-    );
+    $error_view = null;
+    if ($errors) {
+      $error_view = id(new AphrontErrorView())
+        ->setTitle('Form Errors')
+        ->setErrors($errors);
+    }
 
     $form
       ->appendChild(
         id(new AphrontFormTextAreaControl())
-          ->setLabel('Name')
-          ->setName('name')
-          ->setValue($revision->getName())
-          ->setError($e_name))
+          ->setLabel('Title')
+          ->setName('title')
+          ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT)
+          ->setValue($revision->getTitle())
+          ->setError($e_title))
       ->appendChild(
         id(new AphrontFormTextAreaControl())
           ->setLabel('Summary')
           ->setName('summary')
           ->setValue($revision->getSummary()))
       ->appendChild(
         id(new AphrontFormTextAreaControl())
           ->setLabel('Test Plan')
           ->setName('testplan')
           ->setValue($revision->getTestPlan())
           ->setError($e_testplan))
       ->appendChild(
         id(new AphrontFormTokenizerControl())
           ->setLabel('Reviewers')
           ->setName('reviewers')
-          ->setDatasource('/typeahead/common/user/')
+          ->setDatasource('/typeahead/common/users/')
           ->setValue($reviewer_map))
       ->appendChild(
         id(new AphrontFormTokenizerControl())
           ->setLabel('CC')
           ->setName('cc')
-          ->setDatasource('/typeahead/common/user/')
+          ->setDatasource('/typeahead/common/mailable/')
           ->setValue($reviewer_map))
       ->appendChild(
         id(new AphrontFormTextControl())
           ->setLabel('Blame Revision')
           ->setName('blame')
           ->setValue($revision->getBlameRevision())
           ->setCaption('Revision which broke the stuff which this '.
                        'change fixes.'))
       ->appendChild(
         id(new AphrontFormTextAreaControl())
-          ->setLabel('Revert')
+          ->setLabel('Revert Plan')
           ->setName('revert')
           ->setValue($revision->getRevertPlan())
-          ->setCaption('Special steps required to safely revert this change.'))
-      ->appendChild(
-        id(new AphrontFormSubmitControl())
-          ->setValue('Save'));
+          ->setCaption('Special steps required to safely revert this change.'));
+
+    $submit = id(new AphrontFormSubmitControl())
+      ->setValue('Save');
+    if ($diff) {
+      $submit->addCancelButton('/differential/diff/'.$diff->getID().'/');
+    } else {
+      $submit->addCancelButton('/D'.$revision->getID());
+    }
+
+    $form->appendChild($submit);
 
     $panel = new AphrontPanelView();
     if ($revision->getID()) {
       $panel->setHeader('Edit Differential Revision');
     } else {
       $panel->setHeader('Create New Differential Revision');
     }
 
     $panel->appendChild($form);
     $panel->setWidth(AphrontPanelView::WIDTH_FORM);
 
-    $error_view = null;
     return $this->buildStandardPageResponse(
       array($error_view, $panel),
       array(
         'title' => 'Edit Differential Revision',
       ));
   }
 
 }
diff --git a/src/applications/differential/controller/revisionedit/__init__.php b/src/applications/differential/controller/revisionedit/__init__.php
index 4c5fa1f85e..872135cb60 100644
--- a/src/applications/differential/controller/revisionedit/__init__.php
+++ b/src/applications/differential/controller/revisionedit/__init__.php
@@ -1,19 +1,24 @@
 <?php
 /**
  * This file is automatically generated. Lint this module to rebuild it.
  * @generated
  */
 
 
 
 phutil_require_module('phabricator', 'aphront/response/404');
+phutil_require_module('phabricator', 'aphront/response/redirect');
 phutil_require_module('phabricator', 'applications/differential/controller/base');
+phutil_require_module('phabricator', 'applications/differential/editor/revision');
+phutil_require_module('phabricator', 'applications/differential/storage/diff');
 phutil_require_module('phabricator', 'applications/differential/storage/revision');
 phutil_require_module('phabricator', 'view/form/base');
 phutil_require_module('phabricator', 'view/form/control/submit');
+phutil_require_module('phabricator', 'view/form/control/textarea');
+phutil_require_module('phabricator', 'view/form/error');
 phutil_require_module('phabricator', 'view/layout/panel');
 
 phutil_require_module('phutil', 'utils');
 
 
 phutil_require_source('DifferentialRevisionEditController.php');
diff --git a/src/applications/differential/editor/revision/DifferentialRevisionEditor.php b/src/applications/differential/editor/revision/DifferentialRevisionEditor.php
new file mode 100644
index 0000000000..3f9e91d010
--- /dev/null
+++ b/src/applications/differential/editor/revision/DifferentialRevisionEditor.php
@@ -0,0 +1,550 @@
+<?php
+
+/*
+ * Copyright 2011 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.
+ */
+
+/**
+ * Handle major edit operations to DifferentialRevision -- adding and removing
+ * reviewers, diffs, and CCs. Unlike simple edits, these changes trigger
+ * complicated email workflows.
+ */
+class DifferentialRevisionEditor {
+
+  protected $revision;
+  protected $actorPHID;
+
+  protected $cc         = null;
+  protected $reviewers  = null;
+  protected $diff;
+  protected $comments;
+  protected $silentUpdate;
+
+  public function __construct(DifferentialRevision $revision, $actor_phid) {
+    $this->revision = $revision;
+    $this->actorPHID = $actor_phid;
+  }
+
+/*
+  public static function newRevisionFromRawMessageWithDiff(
+    DifferentialRawMessage $message,
+    Diff $diff,
+    $user) {
+
+    if ($message->getRevisionID()) {
+      throw new Exception(
+        "The provided commit message is already associated with a ".
+        "Differential revision.");
+    }
+
+    if ($message->getReviewedByNames()) {
+      throw new Exception(
+        "The provided commit message contains a 'Reviewed By:' field.");
+    }
+
+    $revision = new DifferentialRevision();
+    $revision->setPHID($revision->generatePHID());
+
+    $revision->setOwnerID($user);
+    $revision->setStatus(DifferentialRevisionStatus::NEEDS_REVIEW);
+    $revision->attachReviewers(array());
+    $revision->attachCCPHIDs(array());
+
+    $editor = new DifferentialRevisionEditor($revision, $user);
+
+    self::copyFields($editor, $revision, $message, $user);
+
+    $editor->addDiff($diff, null);
+    $editor->save();
+
+    return $revision;
+  }
+
+  public static function newRevisionFromConduitWithDiff(
+    array $fields,
+    Diff $diff,
+    $user) {
+
+    $revision = new DifferentialRevision();
+    $revision->setPHID($revision->generatePHID());
+
+    $revision->setOwnerID($user);
+    $revision->setStatus(DifferentialRevisionStatus::NEEDS_REVIEW);
+    $revision->attachReviewers(array());
+    $revision->attachCCPHIDs(array());
+
+    $editor = new DifferentialRevisionEditor($revision, $user);
+
+    $editor->copyFieldFromConduit($fields);
+
+    $editor->addDiff($diff, null);
+    $editor->save();
+
+    return $revision;
+  }
+
+
+  public static function copyFields(
+    DifferentialRevisionEditor $editor,
+    DifferentialRevision $revision,
+    DifferentialRawMessage $message,
+    $user) {
+
+    $revision->setName($message->getTitle());
+    $revision->setSummary($message->getSummary());
+    $revision->setTestPlan($message->getTestPlan());
+    $revision->setSVNBlameRevision($message->getBlameRevision());
+    $revision->setRevert($message->getRevertPlan());
+    $revision->setPlatformImpact($message->getPlatformImpact());
+    $revision->setBugzillaID($message->getBugzillaID());
+
+    $editor->setReviewers($message->getReviewerPHIDs());
+    $editor->setCCPHIDs($message->getCCPHIDs());
+  }
+
+  public function copyFieldFromConduit(array $fields) {
+
+    $user = $this->actorPHID;
+    $revision = $this->revision;
+
+    $revision->setName($fields['title']);
+    $revision->setSummary($fields['summary']);
+    $revision->setTestPlan($fields['testPlan']);
+    $revision->setSVNBlameRevision($fields['blameRevision']);
+    $revision->setRevert($fields['revertPlan']);
+    $revision->setPlatformImpact($fields['platformImpact']);
+    $revision->setBugzillaID($fields['bugzillaID']);
+
+    $this->setReviewers($fields['reviewerGUIDs']);
+    $this->setCCPHIDs($fields['ccGUIDs']);
+  }
+*/
+
+  public function getRevision() {
+    return $this->revision;
+  }
+
+  public function setReviewers(array $reviewers) {
+    $this->reviewers = $reviewers;
+    return $this;
+  }
+
+  public function setCCPHIDs(array $cc) {
+    $this->cc = $cc;
+    return $this;
+  }
+
+  public function addDiff(DifferentialDiff $diff, $comments) {
+    if ($diff->getRevisionID() &&
+        $diff->getRevisionID() != $this->getRevision()->getID()) {
+      $diff_id = (int)$diff->getID();
+      $targ_id = (int)$this->getRevision()->getID();
+      $real_id = (int)$diff->getRevisionID();
+      throw new Exception(
+        "Can not attach diff #{$diff_id} to Revision D{$targ_id}, it is ".
+        "already attached to D{$real_id}.");
+    }
+    $this->diff = $diff;
+    $this->comments = $comments;
+    return $this;
+  }
+
+  protected function getDiff() {
+    return $this->diff;
+  }
+
+  protected function getComments() {
+    return $this->comments;
+  }
+
+  protected function getActorPHID() {
+    return $this->actorPHID;
+  }
+
+  public function isNewRevision() {
+    return !$this->getRevision()->getID();
+  }
+
+  /**
+   * A silent update does not trigger Herald rules or send emails. This is used
+   * for auto-amends at commit time.
+   */
+  public function setSilentUpdate($silent) {
+    $this->silentUpdate = $silent;
+    return $this;
+  }
+
+  public function save() {
+    $revision = $this->getRevision();
+
+// TODO
+//    $revision->openTransaction();
+
+    $is_new = $this->isNewRevision();
+    if ($is_new) {
+      // These fields aren't nullable; set them to sensible defaults if they
+      // haven't been configured. We're just doing this so we can generate an
+      // ID for the revision if we don't have one already.
+      $revision->setLineCount(0);
+      if ($revision->getStatus() === null) {
+        $revision->setStatus(DifferentialRevisionStatus::NEEDS_REVIEW);
+      }
+      if ($revision->getTitle() === null) {
+        $revision->setTitle('Untitled Revision');
+      }
+      if ($revision->getOwnerPHID() === null) {
+        $revision->setOwnerPHID($this->getActorPHID());
+      }
+
+      $revision->save();
+    }
+
+    $revision->loadRelationships();
+
+    if ($this->reviewers === null) {
+      $this->reviewers = $revision->getReviewers();
+    }
+
+    if ($this->cc === null) {
+      $this->cc = $revision->getCCPHIDs();
+    }
+
+    // We're going to build up three dictionaries: $add, $rem, and $stable. The
+    // $add dictionary has added reviewers/CCs. The $rem dictionary has
+    // reviewers/CCs who have been removed, and the $stable array is
+    // reviewers/CCs who haven't changed. We're going to send new reviewers/CCs
+    // a different ("welcome") email than we send stable reviewers/CCs.
+
+    $old = array(
+      'rev' => array_fill_keys($revision->getReviewers(), true),
+      'ccs' => array_fill_keys($revision->getCCPHIDs(), true),
+    );
+
+    $diff = $this->getDiff();
+
+    $xscript_header = null;
+    $xscript_uri = null;
+
+    $new = array(
+      'rev' => array_fill_keys($this->reviewers, true),
+      'ccs' => array_fill_keys($this->cc, true),
+    );
+
+
+    $rem_ccs = array();
+    if ($diff) {
+      $diff->setRevisionID($revision->getID());
+      $revision->setLineCount($diff->getLineCount());
+
+// TODO!
+//      $revision->setRepositoryID($diff->getRepositoryID());
+
+/*
+      $iface = new DifferentialRevisionHeraldable($revision);
+      $iface->setExplicitCCs($new['ccs']);
+      $iface->setExplicitReviewers($new['rev']);
+      $iface->setForbiddenCCs($revision->getForbiddenCCPHIDs());
+      $iface->setForbiddenReviewers($revision->getForbiddenReviewers());
+      $iface->setDiff($diff);
+
+      $xscript = HeraldEngine::loadAndApplyRules($iface);
+      $xscript_uri = $xscript->getURI();
+      $xscript_phid = $xscript->getPHID();
+      $xscript_header = $xscript->getXHeraldRulesHeader();
+
+
+      $sub = array(
+        'rev' => array(),
+        'ccs' => $iface->getCCsAddedByHerald(),
+      );
+      $rem_ccs = $iface->getCCsRemovedByHerald();
+*/
+  // TODO!
+      $sub = array(
+        'rev' => array(),
+        'ccs' => array(),
+      );
+
+
+    } else {
+      $sub = array(
+        'rev' => array(),
+        'ccs' => array(),
+      );
+    }
+
+    // Remove any CCs which are prevented by Herald rules.
+    $sub['ccs'] = array_diff_key($sub['ccs'], $rem_ccs);
+    $new['ccs'] = array_diff_key($new['ccs'], $rem_ccs);
+
+    $add = array();
+    $rem = array();
+    $stable = array();
+    foreach (array('rev', 'ccs') as $key) {
+      $add[$key] = array();
+      if ($new[$key] !== null) {
+        $add[$key] += array_diff_key($new[$key], $old[$key]);
+      }
+      $add[$key] += array_diff_key($sub[$key], $old[$key]);
+
+      $combined = $sub[$key];
+      if ($new[$key] !== null) {
+        $combined += $new[$key];
+      }
+      $rem[$key] = array_diff_key($old[$key], $combined);
+
+      $stable[$key] = array_diff_key($old[$key], $add[$key] + $rem[$key]);
+    }
+
+    self::removeReviewers(
+      $revision,
+      array_keys($rem['rev']),
+      $this->actorPHID);
+    self::addReviewers(
+      $revision,
+      array_keys($add['rev']),
+      $this->actorPHID);
+
+    // Add the owner to the relevant set of users so they get a copy of the
+    // email.
+    if (!$this->silentUpdate) {
+      if ($is_new) {
+        $add['rev'][$this->getActorPHID()] = true;
+      } else {
+        $stable['rev'][$this->getActorPHID()] = true;
+      }
+    }
+
+    $mail = array();
+
+    $changesets = null;
+    $feedback = null;
+    if ($diff) {
+      $changesets = $diff->loadChangesets();
+      // TODO: move to DifferentialFeedbackEditor
+      if (!$is_new) {
+        // TODO
+//        $feedback = $this->createFeedback();
+      }
+      if ($feedback) {
+        $mail[] = id(new DifferentialNewDiffMail(
+            $revision,
+            $this->getActorPHID(),
+            $changesets))
+          ->setIsFirstMailAboutRevision($is_new)
+          ->setIsFirstMailToRecipients($is_new)
+          ->setComments($this->getComments())
+          ->setToPHIDs(array_keys($stable['rev']))
+          ->setCCPHIDs(array_keys($stable['ccs']));
+      }
+
+      // Save the changes we made above.
+
+// TODO
+//      $diff->setDescription(substr($this->getComments(), 0, 80));
+      $diff->save();
+
+      // An updated diff should require review, as long as it's not committed
+      // or accepted. The "accepted" status is "sticky" to encourage courtesy
+      // re-diffs after someone accepts with minor changes/suggestions.
+
+      $status = $revision->getStatus();
+      if ($status != DifferentialRevisionStatus::COMMITTED &&
+          $status != DifferentialRevisionStatus::ACCEPTED) {
+        $revision->setStatus(DifferentialRevisionStatus::NEEDS_REVIEW);
+      }
+
+    } else {
+      $diff = $revision->getActiveDiff();
+      if ($diff) {
+        $changesets = id(new DifferentialChangeset())->loadAllWithDiff($diff);
+      } else {
+        $changesets = array();
+      }
+    }
+
+    $revision->save();
+
+// TODO
+//    $revision->saveTransaction();
+
+    $event = array(
+      'revision_id' => $revision->getID(),
+      'PHID'        => $revision->getPHID(),
+      'action'      => $is_new ? 'create' : 'update',
+      'actor'       => $this->getActorPHID(),
+    );
+
+//  TODO
+//    id(new ToolsTimelineEvent('difx', fb_json_encode($event)))->record();
+
+    if ($this->silentUpdate) {
+      return;
+    }
+
+// TODO
+//    $revision->attachReviewers(array_keys($new['rev']));
+//    $revision->attachCCPHIDs(array_keys($new['ccs']));
+
+    if ($add['ccs'] || $rem['ccs']) {
+      foreach (array_keys($add['ccs']) as $id) {
+        if (empty($new['ccs'][$id])) {
+          $reason_phid = 'TODO';//$xscript_phid;
+        } else {
+          $reason_phid = $this->getActorPHID();
+        }
+        self::addCCPHID($revision, $id, $reason_phid);
+      }
+      foreach (array_keys($rem['ccs']) as $id) {
+        if (empty($new['ccs'][$id])) {
+          $reason_phid = $this->getActorPHID();
+        } else {
+          $reason_phid = 'TODO';//$xscript_phid;
+        }
+        self::removeCCPHID($revision, $id, $reason_phid);
+      }
+    }
+
+    if ($add['rev']) {
+      $message = id(new DifferentialNewDiffMail(
+          $revision,
+          $this->getActorPHID(),
+          $changesets))
+        ->setIsFirstMailAboutRevision($is_new)
+        ->setIsFirstMailToRecipients(true)
+        ->setToPHIDs(array_keys($add['rev']));
+
+      if ($is_new) {
+        // The first time we send an email about a revision, put the CCs in
+        // the "CC:" field of the same "Review Requested" email that reviewers
+        // get, so you don't get two initial emails if you're on a list that
+        // is CC'd.
+        $message->setCCPHIDs(array_keys($add['ccs']));
+      }
+
+      $mail[] = $message;
+    }
+
+    // If you were added as a reviewer and a CC, just give you the reviewer
+    // email. We could go to greater lengths to prevent this, but there's
+    // bunch of stuff with list subscriptions anyway. You can still get two
+    // emails, but only if a revision is updated and you are added as a reviewer
+    // at the same time a list you are on is added as a CC, which is rare and
+    // reasonable.
+    $add['ccs'] = array_diff_key($add['ccs'], $add['rev']);
+
+    if (!$is_new && $add['ccs']) {
+      $mail[] = id(new DifferentialCCWelcomeMail(
+          $revision,
+          $this->getActorPHID(),
+          $changesets))
+        ->setIsFirstMailToRecipients(true)
+        ->setToPHIDs(array_keys($add['ccs']));
+    }
+
+    foreach ($mail as $message) {
+// TODO
+//      $message->setHeraldTranscriptURI($xscript_uri);
+//      $message->setXHeraldRulesHeader($xscript_header);
+      $message->send();
+    }
+  }
+
+  public function addCCPHID(
+    DifferentialRevision $revision,
+    $phid,
+    $reason_phid) {
+    self::alterCCPHID($revision, $phid, true, $reason_phid);
+  }
+
+  public function removeCCPHID(
+    DifferentialRevision $revision,
+    $phid,
+    $reason_phid) {
+    self::alterCCPHID($revision, $phid, false, $reason_phid);
+  }
+
+  protected static function alterCCPHID(
+    DifferentialRevision $revision,
+    $phid,
+    $add,
+    $reason_phid) {
+/*
+    $relationship = new DifferentialRelationship();
+    $relationship->setRevisionID($revision->getID());
+    $relationship->setRelation(DifferentialRelationship::RELATION_SUBSCRIBED);
+    $relationship->setRelatedPHID($phid);
+    $relationship->setForbidden(!$add);
+    $relationship->setReasonPHID($reason_phid);
+    $relationship->replace();
+*/
+  }
+
+
+  public static function addReviewers(
+    DifferentialRevision $revision,
+    array $reviewer_ids,
+    $reason_phid) {
+/*
+    foreach ($reviewer_ids as $reviewer_id) {
+      $relationship = new DifferentialRelationship();
+      $relationship->setRevisionID($revision->getID());
+      $relationship->setRelatedPHID($reviewer_id);
+      $relationship->setForbidden(false);
+      $relationship->setReasonPHID($reason_phid);
+      $relationship->setRelation(DifferentialRelationship::RELATION_REVIEWER);
+      $relationship->replace();
+    }
+*/
+  }
+
+  public static function removeReviewers(
+    DifferentialRevision $revision,
+    array $reviewer_ids,
+    $reason_phid) {
+/*
+    if (!$reviewer_ids) {
+      return;
+    }
+
+    foreach ($reviewer_ids as $reviewer_id) {
+      $relationship = new DifferentialRelationship();
+      $relationship->setRevisionID($revision->getID());
+      $relationship->setRelatedPHID($reviewer_id);
+      $relationship->setForbidden(true);
+      $relationship->setReasonPHID($reason_phid);
+      $relationship->setRelation(DifferentialRelationship::RELATION_REVIEWER);
+      $relationship->replace();
+    }
+*/
+  }
+
+/*
+  protected function createFeedback() {
+    $revision = $this->getRevision();
+    $feedback = id(new DifferentialFeedback())
+      ->setUserID($this->getActorPHID())
+      ->setRevision($revision)
+      ->setContent($this->getComments())
+      ->setAction('update');
+
+    $feedback->save();
+
+    return $feedback;
+  }
+*/
+
+}
+
diff --git a/src/applications/differential/editor/revision/__init__.php b/src/applications/differential/editor/revision/__init__.php
new file mode 100644
index 0000000000..1be89fe9a6
--- /dev/null
+++ b/src/applications/differential/editor/revision/__init__.php
@@ -0,0 +1,17 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('phabricator', 'applications/differential/constants/revisionstatus');
+phutil_require_module('phabricator', 'applications/differential/mail/ccwelcome');
+phutil_require_module('phabricator', 'applications/differential/mail/newdiff');
+phutil_require_module('phabricator', 'applications/differential/storage/changeset');
+
+phutil_require_module('phutil', 'utils');
+
+
+phutil_require_source('DifferentialRevisionEditor.php');
diff --git a/src/applications/differential/mail/base/DifferentialMail.php b/src/applications/differential/mail/base/DifferentialMail.php
new file mode 100755
index 0000000000..6b30df6fb8
--- /dev/null
+++ b/src/applications/differential/mail/base/DifferentialMail.php
@@ -0,0 +1,311 @@
+<?php
+
+/*
+ * Copyright 2011 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.
+ */
+
+abstract class DifferentialMail {
+
+  const SUBJECT_PREFIX  = '[Differential]';
+
+  protected $to = array();
+  protected $cc = array();
+
+  protected $actorName;
+  protected $actorID;
+
+  protected $revision;
+  protected $feedback;
+  protected $changesets;
+  protected $inlineComments;
+  protected $isFirstMailAboutRevision;
+  protected $isFirstMailToRecipients;
+  protected $heraldTranscriptURI;
+  protected $heraldRulesHeader;
+
+  public function getActorName() {
+    return $this->actorName;
+  }
+
+  public function setActorName($actor_name) {
+    $this->actorName = $actor_name;
+    return $this;
+  }
+
+  abstract protected function renderSubject();
+  abstract protected function renderBody();
+
+  public function setXHeraldRulesHeader($header) {
+    $this->heraldRulesHeader = $header;
+    return $this;
+  }
+
+  public function send() {
+    $to_phids = $this->getToPHIDs();
+    if (!$to_phids) {
+      throw new Exception('No "To:" users provided!');
+    }
+
+    $message_id = $this->getMessageID();
+
+    $cc_phids = $this->getCCPHIDs();
+    $subject  = $this->buildSubject();
+    $body     = $this->buildBody();
+
+    $mail = new PhabricatorMetaMTAMail();
+    if ($this->getActorID()) {
+      $mail->setFrom($this->getActorID());
+      $mail->setReplyTo($this->getReplyHandlerEmailAddress());
+    } else {
+      $mail->setFrom($this->getReplyHandlerEmailAddress());
+    }
+
+    $mail
+      ->addTos($to_phids)
+      ->addCCs($cc_phids)
+      ->setSubject($subject)
+      ->setBody($body)
+      ->setIsHTML($this->shouldMarkMailAsHTML())
+      ->addHeader('Thread-Topic', $this->getRevision()->getTitle())
+      ->addHeader('Thread-Index', $this->generateThreadIndex());
+
+    if ($this->isFirstMailAboutRevision()) {
+      $mail->addHeader('Message-ID',  $message_id);
+    } else {
+      $mail->addHeader('In-Reply-To', $message_id);
+      $mail->addHeader('References',  $message_id);
+    }
+
+    if ($this->heraldRulesHeader) {
+      $mail->addHeader('X-Herald-Rules', $this->heraldRulesHeader);
+    }
+
+    $mail->setRelatedPHID($this->getRevision()->getPHID());
+
+    // Save this to the MetaMTA queue for later delivery to the MTA.
+    $mail->save();
+  }
+
+  protected function buildSubject() {
+    return self::SUBJECT_PREFIX.' '.$this->renderSubject();
+  }
+
+  protected function shouldMarkMailAsHTML() {
+    return false;
+  }
+
+  protected function buildBody() {
+
+    $actions = array();
+    $body = $this->renderBody();
+    $body .= <<<EOTEXT
+
+ACTIONS
+  Reply to comment, or !accept, !reject, !abandon, !resign, or !showdiff.
+
+EOTEXT;
+
+    if ($this->getHeraldTranscriptURI() && $this->isFirstMailToRecipients()) {
+      $xscript_uri = $this->getHeraldTranscriptURI();
+      $body .= <<<EOTEXT
+
+MANAGE HERALD RULES
+  http://todo.com/herald/
+
+WHY DID I GET THIS EMAIL?
+  {$xscript_uri}
+
+Tip: use the X-Herald-Rules header to filter Herald messages in your client.
+
+EOTEXT;
+    }
+
+    return $body;
+  }
+
+  protected function getReplyHandlerEmailAddress() {
+    // TODO
+    $phid = $this->getRevision()->getPHID();
+    $server = 'todo.example.com';
+    return "differential+{$phid}@{$server}";
+  }
+
+  protected function formatText($text) {
+    $text = explode("\n", $text);
+    foreach ($text as &$line) {
+      $line = rtrim('  '.$line);
+    }
+    unset($line);
+    return implode("\n", $text);
+  }
+
+  public function setToPHIDs(array $to) {
+    $this->to = $this->filterContactPHIDs($to);
+    return $this;
+  }
+
+  public function setCCPHIDs(array $cc) {
+    $this->cc = $this->filterContactPHIDs($cc);
+    return $this;
+  }
+
+  protected function filterContactPHIDs(array $phids) {
+    return $phids;
+
+    // TODO: actually do this?
+
+    // Differential revisions use Subscriptions for CCs, so any arbitrary
+    // PHID can end up CC'd to them. Only try to actually send email PHIDs
+    // which have ToolsHandle types that are marked emailable. If we don't
+    // filter here, sending the email will fail.
+/*
+    $handles = array();
+    prep(new ToolsHandleData($phids, $handles));
+    foreach ($handles as $phid => $handle) {
+      if (!$handle->isEmailable()) {
+        unset($handles[$phid]);
+      }
+    }
+    return array_keys($handles);
+*/
+  }
+
+  protected function getToPHIDs() {
+    return $this->to;
+  }
+
+  protected function getCCPHIDs() {
+    return $this->cc;
+  }
+
+  public function setActorID($actor_id) {
+    $this->actorID = $actor_id;
+    return $this;
+  }
+
+  public function getActorID() {
+    return $this->actorID;
+  }
+
+  public function setRevision($revision) {
+    $this->revision = $revision;
+    return $this;
+  }
+
+  public function getRevision() {
+    return $this->revision;
+  }
+
+  protected function getMessageID() {
+    $phid = $this->getRevision()->getPHID();
+    // TODO
+    return "<differential-rev-{$phid}-req@TODO.com>";
+  }
+
+  public function setFeedback($feedback) {
+    $this->feedback = $feedback;
+    return $this;
+  }
+
+  public function getFeedback() {
+    return $this->feedback;
+  }
+
+  public function setChangesets($changesets) {
+    $this->changesets = $changesets;
+    return $this;
+  }
+
+  public function getChangesets() {
+    return $this->changesets;
+  }
+
+  public function setInlineComments(array $inline_comments) {
+    $this->inlineComments = $inline_comments;
+    return $this;
+  }
+
+  public function getInlineComments() {
+    return $this->inlineComments;
+  }
+
+  public function renderRevisionDetailLink() {
+    $uri = $this->getRevisionURI();
+    return "REVISION DETAIL\n  {$uri}";
+  }
+
+  public function getRevisionURI() {
+    // TODO
+    return 'http://local.aphront.com/D'.$this->getRevision()->getID();
+  }
+
+  public function setIsFirstMailToRecipients($first) {
+    $this->isFirstMailToRecipients = $first;
+    return $this;
+  }
+
+  public function isFirstMailToRecipients() {
+    return $this->isFirstMailToRecipients;
+  }
+
+  public function setIsFirstMailAboutRevision($first) {
+    $this->isFirstMailAboutRevision = $first;
+    return $this;
+  }
+
+  public function isFirstMailAboutRevision() {
+    return $this->isFirstMailAboutRevision;
+  }
+
+  protected function generateThreadIndex() {
+    // When threading, Outlook ignores the 'References' and 'In-Reply-To'
+    // headers that most clients use. Instead, it uses a custom 'Thread-Index'
+    // header. The format of this header is something like this (from
+    // camel-exchange-folder.c in Evolution Exchange):
+
+    /* A new post to a folder gets a 27-byte-long thread index. (The value
+     * is apparently unique but meaningless.) Each reply to a post gets a
+     * 32-byte-long thread index whose first 27 bytes are the same as the
+     * parent's thread index. Each reply to any of those gets a
+     * 37-byte-long thread index, etc. The Thread-Index header contains a
+     * base64 representation of this value.
+     */
+
+    // The specific implementation uses a 27-byte header for the first email
+    // a recipient receives, and a random 5-byte suffix (32 bytes total)
+    // thereafter. This means that all the replies are (incorrectly) siblings,
+    // but it would be very difficult to keep track of the entire tree and this
+    // gets us reasonable client behavior.
+
+    $base = substr(md5($this->getRevision()->getPHID()), 0, 27);
+    if (!$this->isFirstMailAboutRevision()) {
+      // not totally sure, but it seems like outlook orders replies by
+      // thread-index rather than timestamp, so to get these to show up in the
+      // right order we use the time as the last 4 bytes.
+      $base .= ' ' . pack("N", time());
+    }
+    return base64_encode($base);
+  }
+
+  public function setHeraldTranscriptURI($herald_transcript_uri) {
+    $this->heraldTranscriptURI = $herald_transcript_uri;
+    return $this;
+  }
+
+  public function getHeraldTranscriptURI() {
+    return $this->heraldTranscriptURI;
+  }
+
+}
diff --git a/src/applications/differential/mail/base/__init__.php b/src/applications/differential/mail/base/__init__.php
new file mode 100644
index 0000000000..e37c404c5a
--- /dev/null
+++ b/src/applications/differential/mail/base/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('phabricator', 'applications/metamta/storage/mail');
+
+
+phutil_require_source('DifferentialMail.php');
diff --git a/src/view/form/control/textarea/AphrontFormTextAreaControl.php b/src/applications/differential/mail/ccwelcome/DifferentialCCWelcomeMail.php
similarity index 54%
copy from src/view/form/control/textarea/AphrontFormTextAreaControl.php
copy to src/applications/differential/mail/ccwelcome/DifferentialCCWelcomeMail.php
index 2668b825b1..1cf42fd5a1 100755
--- a/src/view/form/control/textarea/AphrontFormTextAreaControl.php
+++ b/src/applications/differential/mail/ccwelcome/DifferentialCCWelcomeMail.php
@@ -1,35 +1,39 @@
 <?php
 
 /*
  * Copyright 2011 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 AphrontFormTextAreaControl extends AphrontFormControl {
+class DifferentialCCWelcomeMail extends DifferentialReviewRequestMail {
 
-  protected function getCustomControlClass() {
-    return 'aphront-form-control-textarea';
+  protected function renderSubject() {
+    $revision = $this->getRevision();
+    return 'Added to CC: '.$revision->getName();
   }
 
-  protected function renderInput() {
-    return phutil_render_tag(
-      'textarea',
-      array(
-        'name'      => $this->getName(),
-        'disabled'  => $this->getDisabled() ? 'disabled' : null,
-      ),
-      phutil_escape_html($this->getValue()));
-  }
+  protected function renderBody() {
+
+    $actor = $this->getActorName();
+    $name  = $this->getRevision()->getName();
+    $body = array();
+
+    $body[] = "{$actor} added you to the CC list for the revision \"{$name}\".";
+    $body[] = null;
 
+    $body[] = $this->renderReviewRequestBody();
+
+    return implode("\n", $body);
+  }
 }
diff --git a/src/applications/differential/mail/ccwelcome/__init__.php b/src/applications/differential/mail/ccwelcome/__init__.php
new file mode 100644
index 0000000000..0a59f308d4
--- /dev/null
+++ b/src/applications/differential/mail/ccwelcome/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('phabricator', 'applications/differential/mail/reviewrequest');
+
+
+phutil_require_source('DifferentialCCWelcomeMail.php');
diff --git a/src/applications/differential/storage/revision/DifferentialRevision.php b/src/applications/differential/mail/diffcontent/DifferentialDiffContentMail.php
similarity index 60%
copy from src/applications/differential/storage/revision/DifferentialRevision.php
copy to src/applications/differential/mail/diffcontent/DifferentialDiffContentMail.php
index 4a24911c63..002569be64 100755
--- a/src/applications/differential/storage/revision/DifferentialRevision.php
+++ b/src/applications/differential/mail/diffcontent/DifferentialDiffContentMail.php
@@ -1,36 +1,35 @@
 <?php
 
 /*
  * Copyright 2011 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 DifferentialRevision extends DifferentialDAO {
+class DifferentialDiffContentMail extends DifferentialMail {
 
-  protected $name;
-  protected $status;
+  protected $content;
 
-  protected $summary;
-  protected $testPlan;
-  protected $revertPlan;
-  protected $blameRevision;
+  public function __construct(DifferentialRevision $revision, $content) {
+    $this->setRevision($revision);
+    $this->content = $content;
+  }
 
-  protected $phid;
-  protected $ownerPHID;
-
-  protected $dateCommitted;
-
-  protected $lineCount;
+  protected function renderSubject() {
+    return "Content: ".$this->getRevision()->getName();
+  }
 
+  protected function renderBody() {
+    return $this->content;
+  }
 }
diff --git a/src/applications/differential/mail/diffcontent/__init__.php b/src/applications/differential/mail/diffcontent/__init__.php
new file mode 100644
index 0000000000..2398125b9a
--- /dev/null
+++ b/src/applications/differential/mail/diffcontent/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('phabricator', 'applications/differential/mail/base');
+
+
+phutil_require_source('DifferentialDiffContentMail.php');
diff --git a/src/applications/differential/mail/feedback/DifferentialFeedbackMail.php b/src/applications/differential/mail/feedback/DifferentialFeedbackMail.php
new file mode 100755
index 0000000000..c3715126fd
--- /dev/null
+++ b/src/applications/differential/mail/feedback/DifferentialFeedbackMail.php
@@ -0,0 +1,114 @@
+<?php
+
+/*
+ * Copyright 2011 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 DifferentialFeedbackMail extends DifferentialMail {
+
+  protected $changedByCommit;
+
+  public function setChangedByCommit($changed_by_commit) {
+    $this->changedByCommit = $changed_by_commit;
+    return $this;
+  }
+
+  public function getChangedByCommit() {
+    return $this->changedByCommit;
+  }
+
+  public function __construct(
+    DifferentialRevision $revision,
+    $actor_id,
+    DifferentialFeedback $feedback,
+    array $changesets,
+    array $inline_comments) {
+
+    $this->setRevision($revision);
+    $this->setActorID($actor_id);
+    $this->setFeedback($feedback);
+    $this->setChangesets($changesets);
+    $this->setInlineComments($inline_comments);
+
+  }
+
+  protected function renderSubject() {
+    $revision = $this->getRevision();
+    $verb = $this->getVerb();
+    return ucwords($verb).': '.$revision->getName();
+  }
+
+  protected function getVerb() {
+    $feedback = $this->getFeedback();
+    $action = $feedback->getAction();
+    $verb = DifferentialAction::getActionVerb($action);
+    return $verb;
+  }
+
+  protected function renderBody() {
+
+    $feedback = $this->getFeedback();
+
+    $actor = $this->getActorName();
+    $name  = $this->getRevision()->getName();
+    $verb  = $this->getVerb();
+
+    $body  = array();
+
+    $body[] = "{$actor} has {$verb} the revision \"{$name}\".";
+    $body[] = null;
+
+    $content = $feedback->getContent();
+    if (strlen($content)) {
+      $body[] = $this->formatText($content);
+      $body[] = null;
+    }
+
+    if ($this->getChangedByCommit()) {
+      $body[] = 'CHANGED PRIOR TO COMMIT';
+      $body[] = '  This revision was updated prior to commit.';
+      $body[] = null;
+    }
+
+    $inlines = $this->getInlineComments();
+    if ($inlines) {
+      $body[] = 'INLINE COMMENTS';
+      $changesets = $this->getChangesets();
+      foreach ($inlines as $inline) {
+        $changeset = $changesets[$inline->getChangesetID()];
+        if (!$changeset) {
+          throw new Exception('Changeset missing!');
+        }
+        $file = $changeset->getFilename();
+        $line = $inline->renderLineRange();
+        $content = $inline->getContent();
+        $body[] = $this->formatText("{$file}:{$line} {$content}");
+      }
+      $body[] = null;
+    }
+
+    $body[] = $this->renderRevisionDetailLink();
+    $revision = $this->getRevision();
+    if ($revision->getStatus() == DifferentialRevisionStatus::COMMITTED) {
+      $rev_ref = $revision->getRevisionRef();
+      if ($rev_ref) {
+        $body[] = "  Detail URL: ".$rev_ref->getDetailURL();
+      }
+    }
+    $body[] = null;
+
+    return implode("\n", $body);
+  }
+}
diff --git a/src/applications/differential/mail/feedback/__init__.php b/src/applications/differential/mail/feedback/__init__.php
new file mode 100644
index 0000000000..dd323e6309
--- /dev/null
+++ b/src/applications/differential/mail/feedback/__init__.php
@@ -0,0 +1,14 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('phabricator', 'applications/differential/constants/action');
+phutil_require_module('phabricator', 'applications/differential/constants/revisionstatus');
+phutil_require_module('phabricator', 'applications/differential/mail/base');
+
+
+phutil_require_source('DifferentialFeedbackMail.php');
diff --git a/src/applications/differential/mail/newdiff/DifferentialNewDiffMail.php b/src/applications/differential/mail/newdiff/DifferentialNewDiffMail.php
new file mode 100755
index 0000000000..82fcbe9b1a
--- /dev/null
+++ b/src/applications/differential/mail/newdiff/DifferentialNewDiffMail.php
@@ -0,0 +1,65 @@
+<?php
+
+/*
+ * Copyright 2011 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 DifferentialNewDiffMail extends DifferentialReviewRequestMail {
+
+  protected function renderSubject() {
+    $revision = $this->getRevision();
+    $line_count = $revision->getLineCount();
+    $lines = ($line_count == 1 ? "1 line" : "{$line_count} lines");
+
+    if ($this->isFirstMailToRecipients()) {
+      $verb = 'Request';
+    } else {
+      $verb = 'Updated';
+    }
+
+    return "{$verb} ({$lines}): ".$revision->getTitle();
+  }
+
+  protected function buildSubject() {
+    if (!$this->isFirstMailToRecipients()) {
+      return parent::buildSubject();
+    }
+
+    $prefix = self::SUBJECT_PREFIX;
+
+    $subject = $this->renderSubject();
+
+    return "{$prefix} {$subject}";
+  }
+
+  protected function renderBody() {
+    $actor = $this->getActorName();
+
+    $name  = $this->getRevision()->getTitle();
+
+    $body = array();
+
+    if ($this->isFirstMailToRecipients()) {
+      $body[] = "{$actor} requested code review of \"{$name}\".";
+    } else {
+      $body[] = "{$actor} updated the revision \"{$name}\".";
+    }
+    $body[] = null;
+
+    $body[] = $this->renderReviewRequestBody();
+
+    return implode("\n", $body);
+  }
+}
diff --git a/src/applications/differential/mail/newdiff/__init__.php b/src/applications/differential/mail/newdiff/__init__.php
new file mode 100644
index 0000000000..f915929352
--- /dev/null
+++ b/src/applications/differential/mail/newdiff/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('phabricator', 'applications/differential/mail/reviewrequest');
+
+
+phutil_require_source('DifferentialNewDiffMail.php');
diff --git a/src/applications/differential/mail/reviewrequest/DifferentialReviewRequestMail.php b/src/applications/differential/mail/reviewrequest/DifferentialReviewRequestMail.php
new file mode 100755
index 0000000000..5589a9b4a9
--- /dev/null
+++ b/src/applications/differential/mail/reviewrequest/DifferentialReviewRequestMail.php
@@ -0,0 +1,74 @@
+<?php
+
+/*
+ * Copyright 2011 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.
+ */
+
+abstract class DifferentialReviewRequestMail extends DifferentialMail {
+
+  protected $comments;
+
+  public function setComments($comments) {
+    $this->comments = $comments;
+    return $this;
+  }
+
+  public function getComments() {
+    return $this->comments;
+  }
+
+  public function __construct(
+    DifferentialRevision $revision,
+    $actor_id,
+    array $changesets) {
+
+    $this->setRevision($revision);
+    $this->setActorID($actor_id);
+    $this->setChangesets($changesets);
+  }
+
+  protected function renderReviewRequestBody() {
+    $revision = $this->getRevision();
+
+    $body = array();
+    if ($this->isFirstMailToRecipients()) {
+      $body[] = $this->formatText($revision->getSummary());
+      $body[] = null;
+
+      $body[] = 'TEST PLAN';
+      $body[] = $this->formatText($revision->getTestPlan());
+      $body[] = null;
+    } else {
+      if (strlen($this->getComments())) {
+        $body[] = $this->formatText($this->getComments());
+        $body[] = null;
+      }
+    }
+
+    $body[] = $this->renderRevisionDetailLink();
+    $body[] = null;
+
+    $changesets = $this->getChangesets();
+    if ($changesets) {
+      $body[] = 'AFFECTED FILES';
+      foreach ($changesets as $changeset) {
+        $body[] = '  '.$changeset->getFilename();
+      }
+      $body[] = null;
+    }
+
+    return implode("\n", $body);
+  }
+}
diff --git a/src/applications/differential/mail/reviewrequest/__init__.php b/src/applications/differential/mail/reviewrequest/__init__.php
new file mode 100644
index 0000000000..2411be0156
--- /dev/null
+++ b/src/applications/differential/mail/reviewrequest/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('phabricator', 'applications/differential/mail/base');
+
+
+phutil_require_source('DifferentialReviewRequestMail.php');
diff --git a/src/applications/differential/storage/revision/DifferentialRevision.php b/src/applications/differential/storage/revision/DifferentialRevision.php
index 4a24911c63..5f3b6008ca 100755
--- a/src/applications/differential/storage/revision/DifferentialRevision.php
+++ b/src/applications/differential/storage/revision/DifferentialRevision.php
@@ -1,36 +1,58 @@
 <?php
 
 /*
  * Copyright 2011 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 DifferentialRevision extends DifferentialDAO {
 
-  protected $name;
+  protected $title;
   protected $status;
 
   protected $summary;
   protected $testPlan;
   protected $revertPlan;
   protected $blameRevision;
 
   protected $phid;
   protected $ownerPHID;
 
   protected $dateCommitted;
 
   protected $lineCount;
+  
+  public function getConfiguration() {
+    return array(
+      self::CONFIG_AUX_PHID => true,
+    ) + parent::getConfiguration();
+  }
+
+  public function generatePHID() {
+    return PhabricatorPHID::generateNewPHID('DREV');
+  }
+  
+  public function loadRelationships() {
+    
+  }
+  
+  public function getReviewers() {
+    return array();
+  }
+  
+  public function getCCPHIDs() {
+    return array();
+  }
 
 }
diff --git a/src/view/form/base/AphrontFormView.php b/src/view/form/base/AphrontFormView.php
index 25a051f5c3..77aa14f64d 100755
--- a/src/view/form/base/AphrontFormView.php
+++ b/src/view/form/base/AphrontFormView.php
@@ -1,73 +1,81 @@
 <?php
 
 /*
  * Copyright 2011 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.
  */
 
 final class AphrontFormView extends AphrontView {
 
   private $action;
   private $method = 'POST';
   private $header;
   private $data = array();
   private $encType;
 
   public function setAction($action) {
     $this->action = $action;
     return $this;
   }
 
   public function setMethod($method) {
     $this->method = $method;
     return $this;
   }
 
   public function setEncType($enc_type) {
     $this->encType = $enc_type;
     return $this;
   }
 
+  public function addHiddenInput($key, $value) {
+    $this->data[$key] = $value;
+    return $this;
+  }
+
   public function render() {
     require_celerity_resource('aphront-form-view-css');
     return phutil_render_tag(
       'form',
       array(
         'action'  => $this->action,
         'method'  => $this->method,
         'class'   => 'aphront-form-view',
         'enctype' => $this->encType,
       ),
       $this->renderDataInputs().
       $this->renderChildren());
   }
 
   private function renderDataInputs() {
     $data = $this->data + array(
       '__form__' => 1,
     );
     $inputs = array();
     foreach ($data as $key => $value) {
+      if ($value === null) {
+        continue;
+      }
       $inputs[] = phutil_render_tag(
         'input',
         array(
           'type'  => 'hidden',
           'name'  => $key,
           'value' => $value,
         ));
     }
     return implode("\n", $inputs);
   }
 
 }
diff --git a/src/view/form/control/textarea/AphrontFormTextAreaControl.php b/src/view/form/control/textarea/AphrontFormTextAreaControl.php
index 2668b825b1..55ec143551 100755
--- a/src/view/form/control/textarea/AphrontFormTextAreaControl.php
+++ b/src/view/form/control/textarea/AphrontFormTextAreaControl.php
@@ -1,35 +1,55 @@
 <?php
 
 /*
  * Copyright 2011 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 AphrontFormTextAreaControl extends AphrontFormControl {
 
+  const HEIGHT_VERY_SHORT = 'very-short';
+  const HEIGHT_SHORT      = 'short';
+  
+  private $height;
+
+  public function setHeight($height) {
+    $this->height = $height;
+    return $this;
+  }
+
   protected function getCustomControlClass() {
     return 'aphront-form-control-textarea';
   }
 
   protected function renderInput() {
+
+    $height_class = null;
+    switch ($this->height) {
+      case self::HEIGHT_VERY_SHORT:
+      case self::HEIGHT_SHORT:
+        $height_class = 'aphront-textarea-'.$this->height;
+        break;
+    }
+
     return phutil_render_tag(
       'textarea',
       array(
         'name'      => $this->getName(),
         'disabled'  => $this->getDisabled() ? 'disabled' : null,
+        'class'     => $height_class,
       ),
       phutil_escape_html($this->getValue()));
   }
 
 }
diff --git a/src/view/page/standard/PhabricatorStandardPageView.php b/src/view/page/standard/PhabricatorStandardPageView.php
index 8f3adddbfc..7956b4ba2b 100755
--- a/src/view/page/standard/PhabricatorStandardPageView.php
+++ b/src/view/page/standard/PhabricatorStandardPageView.php
@@ -1,151 +1,152 @@
 <?php
 
 /*
  * Copyright 2011 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 PhabricatorStandardPageView extends AphrontPageView {
 
   private $baseURI;
   private $applicationName;
   private $tabs = array();
   private $selectedTab;
   private $glyph;
   private $bodyContent;
   private $request;
 
   public function setRequest($request) {
     $this->request = $request;
     return $this;
   }
 
   public function getRequest() {
     return $this->request;
   }
 
   public function setApplicationName($application_name) {
     $this->applicationName = $application_name;
     return $this;
   }
 
   public function getApplicationName() {
     return $this->applicationName;
   }
 
   public function setBaseURI($base_uri) {
     $this->baseURI = $base_uri;
     return $this;
   }
 
   public function getBaseURI() {
     return $this->baseURI;
   }
 
   public function setTabs(array $tabs, $selected_tab) {
     $this->tabs = $tabs;
     $this->selectedTab = $selected_tab;
     return $this;
   }
 
   public function getTitle() {
     return $this->getGlyph().' '.parent::getTitle();
   }
 
 
   protected function willRenderPage() {
     require_celerity_resource('phabricator-core-css');
     require_celerity_resource('phabricator-core-buttons-css');
     require_celerity_resource('phabricator-standard-page-view');
 
     require_celerity_resource('javelin-lib-dev');
 
     $this->bodyContent = $this->renderChildren();
   }
 
 
   protected function getHead() {
     $response = CelerityAPI::getStaticResourceResponse();
     return
       $response->renderResourcesOfType('css').
       '<script type="text/javascript">window.__DEV__=1;</script>'.
       '<script type="text/javascript" src="/rsrc/js/javelin/init.dev.js">'.
       '</script>';
   }
 
   public function setGlyph($glyph) {
     $this->glyph = $glyph;
     return $this;
   }
 
   public function getGlyph() {
     return $this->glyph;
   }
 
   protected function getBody() {
 
     $tabs = array();
     foreach ($this->tabs as $name => $tab) {
       $tabs[] = phutil_render_tag(
         'a',
         array(
           'href'  => idx($tab, 'href'),
           'class' => ($name == $this->selectedTab)
             ? 'phabricator-selected-tab'
             : null,
         ),
         phutil_escape_html(idx($tab, 'name')));
     }
     $tabs = implode('', $tabs);
     if ($tabs) {
       $tabs = '<span class="phabricator-head-tabs">'.$tabs.'</span>';
     }
 
     $login_stuff = null;
     $request = $this->getRequest();
-    $user = $request->getUser();
-
-    if ($user->getPHID()) {
-      $login_stuff = 'Logged in as '.phutil_escape_html($user->getUsername());
+    if ($request) {
+      $user = $request->getUser();
+      if ($user->getPHID()) {
+        $login_stuff = 'Logged in as '.phutil_escape_html($user->getUsername());
+      }
     }
 
     return
       '<div class="phabricator-standard-page">'.
         '<div class="phabricator-standard-header">'.
           '<div class="phabricator-login-details">'.
             $login_stuff.
           '</div>'.
           '<a href="/">Phabricator</a> '.
           phutil_render_tag(
             'a',
             array(
               'href'  => $this->getBaseURI(),
               'class' => 'phabricator-head-appname',
             ),
             phutil_escape_html($this->getApplicationName())).
           $tabs.
         '</div>'.
         $this->bodyContent.
         '<div style="clear: both;"></div>'.
       '</div>';
   }
 
   protected function getTail() {
     $response = CelerityAPI::getStaticResourceResponse();
     return
       $response->renderResourcesOfType('js').
       $response->renderHTMLFooter();
   }
 
 }
diff --git a/webroot/rsrc/css/aphront/form-view.css b/webroot/rsrc/css/aphront/form-view.css
index fdf79ef46b..aded181c7d 100644
--- a/webroot/rsrc/css/aphront/form-view.css
+++ b/webroot/rsrc/css/aphront/form-view.css
@@ -1,99 +1,103 @@
 /**
  * @provides aphront-form-view-css
  */
 
 .aphront-form-view {
   background: #e7e7e7;
   border: 1px solid #c4c4c4;
   padding: 1em;
 }
 
 .aphront-form-view label.aphront-form-label {
   padding-top: 4px;
   width: 19%;
   float: left;
   text-align: right;
   font-weight: bold;
   font-size: 13px;
   color: #666666;
 }
 
 .aphront-form-input {
   margin-left: 20%;
   margin-right: 25%;
   width: 55%;
 }
 
 .aphront-form-error {
   width: 23%;
   float: right;
   color: #aa0000;
   font-weight: bold;
   padding-top: 4px;
 }
 
 .aphront-form-input input,
 .aphront-form-input textarea {
   font-size: 12px;
   width: 100%;
 }
 
 
 .aphront-form-input textarea {
   height: 12em;
 }
 
 .aphront-form-control {
   padding: 4px;
 }
 
 .aphront-form-control-submit button,
 .aphront-form-control-submit a.button {
   float: right;
   margin: 0.5em 0 0em 2%;
 }
 
+.aphront-form-control-textarea textarea.aphront-textarea-very-short {
+  height: 3em;
+}
+
 .aphront-form-control-select .aphront-form-input {
   padding-top: 2px;
 }
 
 
 .aphront-form-view .aphront-form-caption {
   font-size: 11px;
   color: #444444;
   text-align: right;
   clear: both;
   margin-right: 25%;
   margin-left: 20%;
 }
 
 .aphront-error-view {
   width: 720px;
   margin: 1em auto;
   border: 1px solid #aa0000;
   padding: 1em;
   background: #f9b9bc;
 }
 
 .aphront-form-instructions {
   margin: 0.75em 3% 1.25em;
 }
 
 .aphront-form-control-static .aphront-form-input {
   padding-top: 4px;
   font-size: 13px;
 }
 
 table.aphront-form-control-checkbox-layout {
   margin-top: 3px;
   font-size: 13px;
 }
 
 table.aphront-form-control-checkbox-layout th {
   padding-top: 2px;
   padding-left: 0.35em;
 }
 
 .aphront-form-control-checkbox-layout td input {
   width: auto;
 }