diff --git a/src/applications/differential/editor/revision/DifferentialRevisionEditor.php b/src/applications/differential/editor/revision/DifferentialRevisionEditor.php
index a8a9769d00..d0c2153ec8 100644
--- a/src/applications/differential/editor/revision/DifferentialRevisionEditor.php
+++ b/src/applications/differential/editor/revision/DifferentialRevisionEditor.php
@@ -1,903 +1,907 @@
 <?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;
 
   private $auxiliaryFields = array();
   private $contentSource;
 
   public function __construct(DifferentialRevision $revision, $actor_phid) {
     $this->revision = $revision;
     $this->actorPHID = $actor_phid;
   }
 
   public static function newRevisionFromConduitWithDiff(
     array $fields,
     DifferentialDiff $diff,
     $user_phid) {
 
     $revision = new DifferentialRevision();
     $revision->setPHID($revision->generatePHID());
 
     $revision->setAuthorPHID($user_phid);
     $revision->setStatus(DifferentialRevisionStatus::NEEDS_REVIEW);
 
     $editor = new DifferentialRevisionEditor($revision, $user_phid);
 
     $editor->copyFieldsFromConduit($fields);
 
     $editor->addDiff($diff, null);
     $editor->save();
 
     return $revision;
   }
 
   public function copyFieldsFromConduit(array $fields) {
 
     $revision = $this->revision;
     $revision->loadRelationships();
 
     $aux_fields = DifferentialFieldSelector::newSelector()
       ->getFieldSpecifications();
 
     foreach ($aux_fields as $key => $aux_field) {
       $aux_field->setRevision($revision);
       if (!$aux_field->shouldAppearOnCommitMessage()) {
         unset($aux_fields[$key]);
       }
     }
 
     $aux_fields = mpull($aux_fields, null, 'getCommitMessageKey');
 
     foreach ($fields as $field => $value) {
       if (empty($aux_fields[$field])) {
         throw new Exception(
           "Parsed commit message contains unrecognized field '{$field}'.");
       }
       $aux_fields[$field]->setValueFromParsedCommitMessage($value);
     }
 
+    foreach ($aux_fields as $aux_field) {
+      $aux_field->validateField();
+    }
+
     $aux_fields = array_values($aux_fields);
     $this->setAuxiliaryFields($aux_fields);
   }
 
   public function setAuxiliaryFields(array $auxiliary_fields) {
     $this->auxiliaryFields = $auxiliary_fields;
     return $this;
   }
 
   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 setContentSource(PhabricatorContentSource $content_source) {
     $this->contentSource = $content_source;
     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->getAuthorPHID() === null) {
         $revision->setAuthorPHID($this->getActorPHID());
       }
       if ($revision->getSummary() === null) {
         $revision->setSummary('');
       }
       if ($revision->getTestPlan() === null) {
         $revision->setTestPlan('');
       }
       $revision->save();
     }
 
     $revision->loadRelationships();
 
     $this->willWriteRevision();
 
     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());
 
       $adapter = new HeraldDifferentialRevisionAdapter(
         $revision,
         $diff);
       $adapter->setExplicitCCs($new['ccs']);
       $adapter->setExplicitReviewers($new['rev']);
       $adapter->setForbiddenCCs($revision->getUnsubscribedPHIDs());
 
       $xscript = HeraldEngine::loadAndApplyRules($adapter);
       $xscript_uri = PhabricatorEnv::getProductionURI(
         '/herald/transcript/'.$xscript->getID().'/');
       $xscript_phid = $xscript->getPHID();
       $xscript_header = $xscript->getXHeraldRulesHeader();
 
       $xscript_header = HeraldTranscript::saveXHeraldRulesHeader(
         $revision->getPHID(),
         $xscript_header);
 
       $sub = array(
         'rev' => array(),
         'ccs' => $adapter->getCCsAddedByHerald(),
       );
       $rem_ccs = $adapter->getCCsRemovedByHerald();
     } 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::alterReviewers(
       $revision,
       $this->reviewers,
       array_keys($rem['rev']),
       array_keys($add['rev']),
       $this->actorPHID);
 
 /*
 
     // TODO: When Herald is brought over, run through this stuff to figure
     // out which adds are Herald's fault.
 
     // TODO: Still need to do this.
 
     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();
         }
       }
       foreach (array_keys($rem['ccs']) as $id) {
         if (empty($new['ccs'][$id])) {
           $reason_phid = $this->getActorPHID();
         } else {
           $reason_phid = 'TODO';//$xscript_phid;
         }
       }
     }
 */
     self::alterCCs(
       $revision,
       $this->cc,
       array_keys($rem['ccs']),
       array_keys($add['ccs']),
       $this->actorPHID);
 
     $this->updateAuxiliaryFields();
 
     // Add the author and users included from Herald rules 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;
         if ($diff) {
           $add['rev'] += $adapter->getEmailPHIDsAddedByHerald();
         }
       } else {
         $stable['rev'][$this->getActorPHID()] = true;
         if ($diff) {
           $stable['rev'] += $adapter->getEmailPHIDsAddedByHerald();
         }
       }
     }
 
     $mail = array();
 
     $phids = array($this->getActorPHID());
 
     $handles = id(new PhabricatorObjectHandleData($phids))
       ->loadHandles();
     $actor_handle = $handles[$this->getActorPHID()];
 
     $changesets = null;
     $comment = null;
     if ($diff) {
       $changesets = $diff->loadChangesets();
       // TODO: This should probably be in DifferentialFeedbackEditor?
       if (!$is_new) {
         $comment = $this->createComment();
       }
       if ($comment) {
         $mail[] = id(new DifferentialNewDiffMail(
             $revision,
             $actor_handle,
             $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.
 
       $diff->setDescription(substr($this->getComments(), 0, 80));
       $diff->save();
 
       $this->updateAffectedPathTable($revision, $diff, $changesets);
       $this->updateRevisionHashTable($revision, $diff);
 
       // 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->loadActiveDiff();
       if ($diff) {
         $changesets = $diff->loadChangesets();
       } else {
         $changesets = array();
       }
     }
 
     $revision->save();
 
     $this->didWriteRevision();
 
     $event_data = array(
       'revision_id'          => $revision->getID(),
       'revision_phid'        => $revision->getPHID(),
       'revision_name'        => $revision->getTitle(),
       'revision_author_phid' => $revision->getAuthorPHID(),
       'action'               => $is_new
         ? DifferentialAction::ACTION_CREATE
         : DifferentialAction::ACTION_UPDATE,
       'feedback_content'     => $is_new
         ? phutil_utf8_shorten($revision->getSummary(), 140)
         : $this->getComments(),
       'actor_phid'           => $revision->getAuthorPHID(),
     );
     id(new PhabricatorTimelineEvent('difx', $event_data))
       ->recordEvent();
 
     id(new PhabricatorFeedStoryPublisher())
       ->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_DIFFERENTIAL)
       ->setStoryData($event_data)
       ->setStoryTime(time())
       ->setStoryAuthorPHID($revision->getAuthorPHID())
       ->setRelatedPHIDs(
         array(
           $revision->getPHID(),
           $revision->getAuthorPHID(),
         ))
       ->publish();
 
 // TODO
 //    $revision->saveTransaction();
 
 //  TODO: Move this into a worker task thing.
     PhabricatorSearchDifferentialIndexer::indexRevision($revision);
 
     if ($this->silentUpdate) {
       return;
     }
 
     $revision->loadRelationships();
 
     if ($add['rev']) {
       $message = id(new DifferentialNewDiffMail(
           $revision,
           $actor_handle,
           $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,
           $actor_handle,
           $changesets))
         ->setIsFirstMailToRecipients(true)
         ->setToPHIDs(array_keys($add['ccs']));
     }
 
     foreach ($mail as $message) {
       $message->setHeraldTranscriptURI($xscript_uri);
       $message->setXHeraldRulesHeader($xscript_header);
       $message->send();
     }
   }
 
   public static function addCCAndUpdateRevision(
     $revision,
     $phid,
     $reason) {
 
     self::addCC($revision, $phid, $reason);
 
     $unsubscribed = $revision->getUnsubscribed();
     if (isset($unsubscribed[$phid])) {
       unset($unsubscribed[$phid]);
       $revision->setUnsubscribed($unsubscribed);
       $revision->save();
     }
   }
 
   public static function removeCCAndUpdateRevision(
     $revision,
     $phid,
     $reason) {
 
     self::removeCC($revision, $phid, $reason);
 
     $unsubscribed = $revision->getUnsubscribed();
     if (empty($unsubscribed[$phid])) {
       $unsubscribed[$phid] = true;
       $revision->setUnsubscribed($unsubscribed);
       $revision->save();
     }
   }
 
   public static function addCC(
     DifferentialRevision $revision,
     $phid,
     $reason) {
     return self::alterCCs(
       $revision,
       $revision->getCCPHIDs(),
       $rem = array(),
       $add = array($phid),
       $reason);
   }
 
   public static function removeCC(
     DifferentialRevision $revision,
     $phid,
     $reason) {
     return self::alterCCs(
       $revision,
       $revision->getCCPHIDs(),
       $rem = array($phid),
       $add = array(),
       $reason);
   }
 
   protected static function alterCCs(
     DifferentialRevision $revision,
     array $stable_phids,
     array $rem_phids,
     array $add_phids,
     $reason_phid) {
 
     return self::alterRelationships(
       $revision,
       $stable_phids,
       $rem_phids,
       $add_phids,
       $reason_phid,
       DifferentialRevision::RELATION_SUBSCRIBED);
   }
 
 
   public static function alterReviewers(
     DifferentialRevision $revision,
     array $stable_phids,
     array $rem_phids,
     array $add_phids,
     $reason_phid) {
 
     return self::alterRelationships(
       $revision,
       $stable_phids,
       $rem_phids,
       $add_phids,
       $reason_phid,
       DifferentialRevision::RELATION_REVIEWER);
   }
 
   private static function alterRelationships(
     DifferentialRevision $revision,
     array $stable_phids,
     array $rem_phids,
     array $add_phids,
     $reason_phid,
     $relation_type) {
 
     $rem_map = array_fill_keys($rem_phids, true);
     $add_map = array_fill_keys($add_phids, true);
 
     $seq_map = array_values($stable_phids);
     $seq_map = array_flip($seq_map);
     foreach ($rem_map as $phid => $ignored) {
       if (!isset($seq_map[$phid])) {
         $seq_map[$phid] = count($seq_map);
       }
     }
     foreach ($add_map as $phid => $ignored) {
       if (!isset($seq_map[$phid])) {
         $seq_map[$phid] = count($seq_map);
       }
     }
 
     $raw = $revision->getRawRelations($relation_type);
     $raw = ipull($raw, null, 'objectPHID');
 
     $sequence = count($seq_map);
     foreach ($raw as $phid => $ignored) {
       if (isset($seq_map[$phid])) {
         $raw[$phid]['sequence'] = $seq_map[$phid];
       } else {
         $raw[$phid]['sequence'] = $sequence++;
       }
     }
     $raw = isort($raw, 'sequence');
 
     foreach ($raw as $phid => $ignored) {
       if (isset($rem_map[$phid])) {
         unset($raw[$phid]);
       }
     }
 
     foreach ($add_phids as $add) {
       $raw[$add] = array(
         'objectPHID'  => $add,
         'sequence'    => idx($seq_map, $add, $sequence++),
         'reasonPHID'  => $reason_phid,
       );
     }
 
     $conn_w = $revision->establishConnection('w');
 
     $sql = array();
     foreach ($raw as $relation) {
       $sql[] = qsprintf(
         $conn_w,
         '(%d, %s, %s, %d, %s)',
         $revision->getID(),
         $relation_type,
         $relation['objectPHID'],
         $relation['sequence'],
         $relation['reasonPHID']);
     }
 
     $conn_w->openTransaction();
       queryfx(
         $conn_w,
         'DELETE FROM %T WHERE revisionID = %d AND relation = %s',
         DifferentialRevision::RELATIONSHIP_TABLE,
         $revision->getID(),
         $relation_type);
       if ($sql) {
         queryfx(
           $conn_w,
           'INSERT INTO %T
             (revisionID, relation, objectPHID, sequence, reasonPHID)
           VALUES %Q',
           DifferentialRevision::RELATIONSHIP_TABLE,
           implode(', ', $sql));
       }
     $conn_w->saveTransaction();
 
     $revision->loadRelationships();
   }
 
 
   private function createComment() {
     $revision_id = $this->revision->getID();
     $comment = id(new DifferentialComment())
       ->setAuthorPHID($this->getActorPHID())
       ->setRevisionID($revision_id)
       ->setContent($this->getComments())
       ->setAction('update');
 
     if ($this->contentSource) {
       $comment->setContentSource($this->contentSource);
     }
 
     $comment->save();
 
     return $comment;
   }
 
   private function updateAuxiliaryFields() {
     $aux_map = array();
     foreach ($this->auxiliaryFields as $aux_field) {
       $key = $aux_field->getStorageKey();
       if ($key !== null) {
         $val = $aux_field->getValueForStorage();
         $aux_map[$key] = $val;
       }
     }
 
     if (!$aux_map) {
       return;
     }
 
     $revision = $this->revision;
 
     $fields = id(new DifferentialAuxiliaryField())->loadAllWhere(
       'revisionPHID = %s AND name IN (%Ls)',
       $revision->getPHID(),
       array_keys($aux_map));
     $fields = mpull($fields, null, 'getName');
 
     foreach ($aux_map as $key => $val) {
       $obj = idx($fields, $key);
       if (!strlen($val)) {
         // If the new value is empty, just delete the old row if one exists and
         // don't add a new row if it doesn't.
         if ($obj) {
           $obj->delete();
         }
       } else {
         if (!$obj) {
           $obj = new DifferentialAuxiliaryField();
           $obj->setRevisionPHID($revision->getPHID());
           $obj->setName($key);
         }
 
         if ($obj->getValue() !== $val) {
           $obj->setValue($val);
           $obj->save();
         }
       }
     }
   }
 
   private function willWriteRevision() {
     foreach ($this->auxiliaryFields as $aux_field) {
       $aux_field->willWriteRevision($this);
     }
   }
 
   private function didWriteRevision() {
     foreach ($this->auxiliaryFields as $aux_field) {
       $aux_field->didWriteRevision($this);
     }
   }
 
   /**
    * Update the table which links Differential revisions to paths they affect,
    * so Diffusion can efficiently find pending revisions for a given file.
    */
   private function updateAffectedPathTable(
     DifferentialRevision $revision,
     DifferentialDiff $diff,
     array $changesets) {
 
     $project = $diff->loadArcanistProject();
     if (!$project) {
       // Probably an old revision from before projects.
       return;
     }
 
     $repository = $project->loadRepository();
     if (!$repository) {
       // Probably no project <-> repository link, or the repository where the
       // project lives is untracked.
       return;
     }
 
     $path_prefix = null;
 
     $local_root = $diff->getSourceControlPath();
     if ($local_root) {
       // We're in a working copy which supports subdirectory checkouts (e.g.,
       // SVN) so we need to figure out what prefix we should add to each path
       // (e.g., trunk/projects/example/) to get the absolute path from the
       // root of the repository. DVCS systems like Git and Mercurial are not
       // affected.
 
       // Normalize both paths and check if the repository root is a prefix of
       // the local root. If so, throw it away. Note that this correctly handles
       // the case where the remote path is "/".
       $local_root = id(new PhutilURI($local_root))->getPath();
       $local_root = rtrim($local_root, '/');
 
       $repo_root = id(new PhutilURI($repository->getRemoteURI()))->getPath();
       $repo_root = rtrim($repo_root, '/');
 
       if (!strncmp($repo_root, $local_root, strlen($repo_root))) {
         $path_prefix = substr($local_root, strlen($repo_root));
       }
     }
 
     $paths = array();
     foreach ($changesets as $changeset) {
       $paths[] = $path_prefix.'/'.$changeset->getFileName();
     }
 
     // Mark this as also touching all parent paths, so you can see all pending
     // changes to any file within a directory.
     $all_paths = array();
     foreach ($paths as $local) {
       foreach (DiffusionPathIDQuery::expandPathToRoot($local) as $path) {
         $all_paths[$path] = true;
       }
     }
     $all_paths = array_keys($all_paths);
 
     $path_map = id(new DiffusionPathIDQuery($all_paths))->loadPathIDs();
 
     $table = new DifferentialAffectedPath();
     $conn_w = $table->establishConnection('w');
 
     $sql = array();
     foreach ($all_paths as $path) {
       $path_id = idx($path_map, $path);
       if (!$path_id) {
         // Don't bother creating these, it probably means we're either adding
         // a file (in which case having this row is irrelevant since Diffusion
         // won't be querying for it) or something is misconfigured (in which
         // case we'd just be writing garbage).
         continue;
       }
       $sql[] = qsprintf(
         $conn_w,
         '(%d, %d, %d, %d)',
         $repository->getID(),
         $path_id,
         time(),
         $revision->getID());
     }
 
     queryfx(
       $conn_w,
       'DELETE FROM %T WHERE revisionID = %d',
       $table->getTableName(),
       $revision->getID());
     foreach (array_chunk($sql, 256) as $chunk) {
       queryfx(
         $conn_w,
         'INSERT INTO %T (repositoryID, pathID, epoch, revisionID) VALUES %Q',
         $table->getTableName(),
         implode(', ', $chunk));
     }
   }
 
 
   /**
    * Update the table connecting revisions to DVCS local hashes, so we can
    * identify revisions by commit/tree hashes.
    */
   private function updateRevisionHashTable(
     DifferentialRevision $revision,
     DifferentialDiff $diff) {
 
     $vcs = $diff->getSourceControlSystem();
     if ($vcs == DifferentialRevisionControlSystem::SVN) {
       // Subversion has no local commit or tree hash information, so we don't
       // have to do anything.
       return;
     }
 
     $property = id(new DifferentialDiffProperty())->loadOneWhere(
       'diffID = %d AND name = %s',
       $diff->getID(),
       'local:commits');
     if (!$property) {
       return;
     }
 
     $hashes = array();
 
     $data = $property->getData();
     switch ($vcs) {
       case DifferentialRevisionControlSystem::GIT:
         foreach ($data as $commit) {
           $hashes[] = array(
             DifferentialRevisionHash::HASH_GIT_COMMIT,
             $commit['commit'],
           );
           $hashes[] = array(
             DifferentialRevisionHash::HASH_GIT_TREE,
             $commit['tree'],
           );
         }
         break;
       case DifferentialRevisionControlSystem::MERCURIAL:
         foreach ($data as $commit) {
           $hashes[] = array(
             DifferentialRevisionHash::HASH_MERCURIAL_COMMIT,
             $commit['rev'],
           );
         }
         break;
     }
 
     $conn_w = $revision->establishConnection('w');
 
     $sql = array();
     foreach ($hashes as $info) {
       list($type, $hash) = $info;
       $sql[] = qsprintf(
         $conn_w,
         '(%d, %s, %s)',
         $revision->getID(),
         $type,
         $hash);
     }
 
     queryfx(
       $conn_w,
       'DELETE FROM %T WHERE revisionID = %d',
       DifferentialRevisionHash::TABLE_NAME,
       $revision->getID());
 
     if ($sql) {
       queryfx(
         $conn_w,
         'INSERT INTO %T (revisionID, type, hash) VALUES %Q',
         DifferentialRevisionHash::TABLE_NAME,
         implode(', ', $sql));
     }
   }
 
 }
 
diff --git a/src/applications/differential/field/specification/base/DifferentialFieldSpecification.php b/src/applications/differential/field/specification/base/DifferentialFieldSpecification.php
index 7b0b1be529..65b59dc558 100644
--- a/src/applications/differential/field/specification/base/DifferentialFieldSpecification.php
+++ b/src/applications/differential/field/specification/base/DifferentialFieldSpecification.php
@@ -1,747 +1,748 @@
 <?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.
  */
 
 /**
  * Describes and implements the behavior for a custom field on Differential
  * revisions. Along with other configuration, you can extend this class to add
  * custom fields to Differential revisions and commit messages.
  *
  * Generally, you should implement all methods from the storage task and then
  * the methods from one or more interface tasks.
  *
  * @task storage Field Storage
  * @task edit Extending the Revision Edit Interface
  * @task view Extending the Revision View Interface
  * @task conduit Extending the Conduit View Interface
  * @task commit Extending Commit Messages
  * @task load Loading Additional Data
  * @task context Contextual Data
  */
 abstract class DifferentialFieldSpecification {
 
   private $revision;
   private $diff;
   private $handles;
   private $diffProperties;
   private $user;
 
 
 /* -(  Storage  )------------------------------------------------------------ */
 
 
   /**
    * Return a unique string used to key storage of this field's value, like
    * "mycompany.fieldname" or similar. You can return null (the default) to
    * indicate that this field does not use any storage. This is appropriate for
    * display fields, like @{class:DifferentialLinesFieldSpecification}. If you
    * implement this, you must also implement @{method:getValueForStorage} and
    * @{method:setValueFromStorage}.
    *
    * @return string|null  Unique key which identifies this field in auxiliary
    *                      field storage. Maximum length is 32. Alternatively,
    *                      null (default) to indicate that this field does not
    *                      use auxiliary field storage.
    * @task storage
    */
   public function getStorageKey() {
     return null;
   }
 
 
   /**
    * Return a serialized representation of the field value, appropriate for
    * storing in auxiliary field storage. You must implement this method if
    * you implement @{method:getStorageKey}.
    *
    * @return string Serialized field value.
    * @task storage
    */
   public function getValueForStorage() {
     throw new DifferentialFieldSpecificationIncompleteException($this);
   }
 
 
   /**
    * Set the field's value given a serialized storage value. This is called
    * when the field is loaded; if no data is available, the value will be
    * null. You must implement this method if you implement
    * @{method:getStorageKey}.
    *
    * @param string|null Serialized field representation (from
    *                    @{method:getValueForStorage}) or null if no value has
    *                    ever been stored.
    * @return this
    * @task storage
    */
   public function setValueFromStorage($value) {
     throw new DifferentialFieldSpecificationIncompleteException($this);
   }
 
 
 /* -(  Extending the Revision Edit Interface  )------------------------------ */
 
 
   /**
    * Determine if this field should appear on the "Edit Revision" interface. If
    * you return true from this method, you must implement
    * @{method:setValueFromRequest}, @{method:renderEditControl} and
    * @{method:validateField}.
    *
    * For a concrete example of a field which implements an edit interface, see
    * @{class:DifferentialRevertPlanFieldSpecification}.
    *
    * @return bool True to indicate that this field implements an edit interface.
    * @task edit
    */
   public function shouldAppearOnEdit() {
     return false;
   }
 
 
   /**
    * Set the field's value from an HTTP request. Generally, you should read
    * the value of some field name you emitted in @{method:renderEditControl}
    * and save it into the object, e.g.:
    *
    *   $this->value = $request->getStr('my-custom-field');
    *
    * If you have some particularly complicated field, you may need to read
    * more data; this is why you have access to the entire request.
    *
    * You must implement this if you implement @{method:shouldAppearOnEdit}.
    *
    * You should not perform field validation here; instead, you should implement
    * @{method:validateField}.
    *
    * @param AphrontRequest HTTP request representing a user submitting a form
    *                       with this field in it.
    * @return this
    * @task edit
    */
   public function setValueFromRequest(AphrontRequest $request) {
     throw new DifferentialFieldSpecificationIncompleteException($this);
   }
 
 
   /**
    * Build a renderable object (generally, some @{class:AphrontFormControl})
    * which can be appended to a @{class:AphrontFormView} and represents the
    * interface the user sees on the "Edit Revision" screen when interacting
    * with this field.
    *
    * For example:
    *
    *   return id(new AphrontFormTextControl())
    *     ->setLabel('Custom Field')
    *     ->setName('my-custom-key')
    *     ->setValue($this->value);
    *
    * You must implement this if you implement @{method:shouldAppearOnEdit}.
    *
    * @return AphrontView|string Something renderable.
    * @task edit
    */
   public function renderEditControl() {
     throw new DifferentialFieldSpecificationIncompleteException($this);
   }
 
 
   /**
    * This method will be called after @{method:setValueFromRequest} but before
    * the field is saved. It gives you an opportunity to inspect the field value
    * and throw a @{class:DifferentialFieldValidationException} if there is a
    * problem with the value the user has provided (for example, the value the
-   * user entered is not correctly formatted).
+   * user entered is not correctly formatted). This method is also called after
+   * @{method:setValueFromParsedCommitMessage} before the revision is saved.
    *
    * By default, fields are not validated.
    *
    * @return void
    * @task edit
    */
   public function validateField() {
     return;
   }
 
   /**
    * Hook for applying revision changes via the editor. Normally, you should
    * not implement this, but a number of builtin fields use the revision object
    * itself as storage. If you need to do something similar for whatever reason,
    * this method gives you an opportunity to interact with the editor or
    * revision before changes are saved (for example, you can write the field's
    * value into some property of the revision).
    *
    * @param DifferentialRevisionEditor  Active editor which is applying changes
    *                                    to the revision.
    * @return void
    * @task edit
    */
   public function willWriteRevision(DifferentialRevisionEditor $editor) {
     return;
   }
 
   /**
    * Hook after an edit operation has completed. This allows you to update
    * link tables or do other write operations which should happen after the
    * revision is saved. Normally you don't need to implement this.
    *
    *
    * @param DifferentialRevisionEditor  Active editor which has just applied
    *                                    changes to the revision.
    * @return void
    * @task edit
    */
   public function didWriteRevision(DifferentialRevisionEditor $editor) {
     return;
   }
 
 
 /* -(  Extending the Revision View Interface  )------------------------------ */
 
 
   /**
    * Determine if this field should appear on the revision detail view
    * interface. One use of this interface is to add purely informational
    * fields to the revision view, without any sort of backing storage.
    *
    * If you return true from this method, you must implement the methods
    * @{method:renderLabelForRevisionView} and
    * @{method:renderValueForRevisionView}.
    *
    * @return bool True if this field should appear when viewing a revision.
    * @task view
    */
   public function shouldAppearOnRevisionView() {
     return false;
   }
 
 
   /**
    * Return a string field label which will appear in the revision detail
    * table.
    *
    * You must implement this method if you return true from
    * @{method:shouldAppearOnRevisionView}.
    *
    * @return string Label for field in revision detail view.
    * @task view
    */
   public function renderLabelForRevisionView() {
     throw new DifferentialFieldSpecificationIncompleteException($this);
   }
 
 
   /**
    * Return a markup block representing the field for the revision detail
    * view. Note that you can return null to suppress display (for instance,
    * if the field shows related objects of some type and the revision doesn't
    * have any related objects).
    *
    * You must implement this method if you return true from
    * @{method:shouldAppearOnRevisionView}.
    *
    * @return string|null Display markup for field value, or null to suppress
    *                     field rendering.
    * @task view
    */
   public function renderValueForRevisionView() {
     throw new DifferentialFieldSpecificationIncompleteException($this);
   }
 
 
 /* -(  Extending the Conduit Interface  )------------------------------------ */
 
 
   /**
    * @task conduit
    */
   public function shouldAppearOnConduitView() {
     return false;
   }
 
   /**
    * @task conduit
    */
   public function getValueForConduit() {
     throw new DifferentialFieldSpecificationIncompleteException($this);
   }
 
   /**
    * @task conduit
    */
   public function getKeyForConduit() {
     $key = $this->getStorageKey();
     if ($key === null) {
       throw new DifferentialFieldSpecificationIncompleteException($this);
     }
     return $key;
   }
 
 
 /* -(  Extending Commit Messages  )------------------------------------------ */
 
 
   /**
    * Determine if this field should appear in commit messages. You should return
    * true if this field participates in any part of the commit message workflow,
    * even if it is not rendered by default.
    *
    * If you implement this method, you must implement
    * @{method:getCommitMessageKey} and
    * @{method:setValueFromParsedCommitMessage}.
    *
    * @return bool True if this field appears in commit messages in any capacity.
    * @task commit
    */
   public function shouldAppearOnCommitMessage() {
     return false;
   }
 
   /**
    * Key which identifies this field in parsed commit messages. Commit messages
    * exist in two forms: raw textual commit messages and parsed dictionaries of
    * fields. This method must return a unique string which identifies this field
    * in dictionaries. Principally, this dictionary is shipped to and from arc
    * over Conduit. Keys should be appropriate property names, like "testPlan"
    * (not "Test Plan") and must be globally unique.
    *
    * You must implement this method if you return true from
    * @{method:shouldAppearOnCommitMessage}.
    *
    * @return string Key which identifies the field in dictionaries.
    * @task commit
    */
   public function getCommitMessageKey() {
     throw new DifferentialFieldSpecificationIncompleteException($this);
   }
 
   /**
    * Set this field's value from a value in a parsed commit message dictionary.
    * Afterward, this field will go through the normal write workflows and the
    * change will be permanently stored via either the storage mechanisms (if
    * your field implements them), revision write hooks (if your field implements
    * them) or discarded (if your field implements neither, e.g. is just a
    * display field).
    *
    * The value you receive will either be null or something you originally
    * returned from @{method:parseValueFromCommitMessage}.
    *
    * You must implement this method if you return true from
    * @{method:shouldAppearOnCommitMessage}.
    *
    * @param mixed Field value from a parsed commit message dictionary.
    * @return this
    * @task commit
    */
   public function setValueFromParsedCommitMessage($value) {
     throw new DifferentialFieldSpecificationIncompleteException($this);
   }
 
   /**
    * In revision control systems which read revision information from the
    * working copy, the user may edit the commit message outside of invoking
    * "arc diff --edit". When they do this, only some fields (those fields which
    * can not be edited by other users) are safe to overwrite. For instance, it
    * is fine to overwrite "Summary" because no one else can edit it, but not
    * to overwrite "Reviewers" because reviewers may have been added or removed
    * via the web interface.
    *
    * If a field is safe to overwrite when edited in a working copy commit
    * message, return true. If the authoritative value should always be used,
    * return false. By default, fields can not be overwritten.
    *
    * @return bool True to indicate the field is save to overwrite.
    * @task commit
    */
   public function shouldOverwriteWhenCommitMessageIsEdited() {
     return false;
   }
 
   /**
    * Return true if this field should be suggested to the user during
    * "arc diff --edit". Basicially, return true if the field is something the
    * user might want to fill out (like "Summary"), and false if it's a
    * system/display/readonly field (like "Differential Revision"). If this
    * method returns true, the field will be rendered even if it has no value
    * during edit and update operations.
    *
    * @return bool True to indicate the field should appear in the edit template.
    * @task commit
    */
   public function shouldAppearOnCommitMessageTemplate() {
     return true;
   }
 
   /**
    * Render a human-readable label for this field, like "Summary" or
    * "Test Plan". This is distinct from the commit message key, but generally
    * they should be similar.
    *
    * @return string Human-readable field label for commit messages.
    * @task commit
    */
   public function renderLabelForCommitMessage() {
     throw new DifferentialFieldSpecificationIncompleteException($this);
   }
 
   /**
    * Render a human-readable value for this field when it appears in commit
    * messages (for instance, lists of users should be rendered as user names).
    *
    * The ##$is_edit## parameter allows you to distinguish between commit
    * messages being rendered for editing and those being rendered for amending
    * or commit. Some fields may decline to render a value in one mode (for
    * example, "Reviewed By" appears only when doing commit/amend, not while
    * editing).
    *
    * @param bool True if the message is being edited.
    * @return string Human-readable field value.
    * @task commit
    */
   public function renderValueForCommitMessage($is_edit) {
     throw new DifferentialFieldSpecificationIncompleteException($this);
   }
 
   /**
    * Return one or more labels which this field parses in commit messages. For
    * example, you might parse all of "Task", "Tasks" and "Task Numbers" or
    * similar. This is just to make it easier to get commit messages to parse
    * when users are typing in the fields manually as opposed to using a
    * template, by accepting alternate spellings / pluralizations / etc. By
    * default, only the label returned from @{method:renderLabelForCommitMessage}
    * is parsed.
    *
    * @return list List of supported labels that this field can parse from commit
    *              messages.
    * @task commit
    */
   public function getSupportedCommitMessageLabels() {
     return array($this->renderLabelForCommitMessage());
   }
 
   /**
    * Parse a raw text block from a commit message into a canonical
    * representation of the field value. For example, the "CC" field accepts a
    * comma-delimited list of usernames and emails and parses them into valid
    * PHIDs, emitting a PHID list.
    *
    * If you encounter errors (like a nonexistent username) while parsing,
    * you should throw a @{class:DifferentialFieldParseException}.
    *
    * Generally, this method should accept whatever you return from
    * @{method:renderValueForCommitMessage} and parse it back into a sensible
    * representation.
    *
    * You must implement this method if you return true from
    * @{method:shouldAppearOnCommitMessage}.
    *
    * @param string
    * @return mixed The canonical representation of the field value. For example,
    *               you should lookup usernames and object references.
    * @task commit
    */
   public function parseValueFromCommitMessage($value) {
     throw new DifferentialFieldSpecificationIncompleteException($this);
   }
 
 
 /* -(  Loading Additional Data  )-------------------------------------------- */
 
 
   /**
    * Specify which @{class:PhabricatorObjectHandles} need to be loaded for your
    * field to render correctly.
    *
    * This is a convenience method which makes the handles available on all
    * interfaces where the field appears. If your field needs handles on only
    * some interfaces (or needs different handles on different interfaces) you
    * can overload the more specific methods to customize which interfaces you
    * retrieve handles for. Requesting only the handles you need will improve
    * the performance of your field.
    *
    * You can later retrieve these handles by calling @{method:getHandle}.
    *
    * @return list List of PHIDs to load handles for.
    * @task load
    */
   protected function getRequiredHandlePHIDs() {
     return array();
   }
 
   /**
    * Specify which @{class:PhabricatorObjectHandles} need to be loaded for your
    * field to render correctly on the view interface.
    *
    * This is a more specific version of @{method:getRequiredHandlePHIDs} which
    * can be overridden to improve field performance by loading only data you
    * need.
    *
    * @return list List of PHIDs to load handles for.
    * @task load
    */
   public function getRequiredHandlePHIDsForRevisionView() {
     return $this->getRequiredHandlePHIDs();
   }
 
   /**
    * Specify which @{class:PhabricatorObjectHandles} need to be loaded for your
    * field to render correctly on the edit interface.
    *
    * This is a more specific version of @{method:getRequiredHandlePHIDs} which
    * can be overridden to improve field performance by loading only data you
    * need.
    *
    * @return list List of PHIDs to load handles for.
    * @task load
    */
   public function getRequiredHandlePHIDsForRevisionEdit() {
     return $this->getRequiredHandlePHIDs();
   }
 
   /**
    * Specify which @{class:PhabricatorObjectHandles} need to be loaded for your
    * field to render correctly on the commit message interface.
    *
    * This is a more specific version of @{method:getRequiredHandlePHIDs} which
    * can be overridden to improve field performance by loading only data you
    * need.
    *
    * @return list List of PHIDs to load handles for.
    * @task load
    */
   public function getRequiredHandlePHIDsForCommitMessage() {
     return $this->getRequiredHandlePHIDs();
   }
 
   /**
    * Specify which diff properties this field needs to load.
    *
    * @return list List of diff property keys this field requires.
    * @task load
    */
   public function getRequiredDiffProperties() {
     return array();
   }
 
   /**
    * Parse a list of users into a canonical PHID list.
    *
    * @param string Raw list of comma-separated user names.
    * @return list List of corresponding PHIDs.
    * @task load
    */
   protected function parseCommitMessageUserList($value) {
     return $this->parseCommitMessageObjectList($value, $mailables = false);
   }
 
   /**
    * Parse a list of mailable objects into a canonical PHID list.
    *
    * @param string Raw list of comma-separated mailable names.
    * @return list List of corresponding PHIDs.
    * @task load
    */
   protected function parseCommitMessageMailableList($value) {
     return $this->parseCommitMessageObjectList($value, $mailables = true);
   }
 
 
   /**
    * Parse and lookup a list of object names, converting them to PHIDs.
    *
    * @param string Raw list of comma-separated object names.
    * @return list List of corresponding PHIDs.
    * @task load
    */
   private function parseCommitMessageObjectList($value, $include_mailables) {
     $value = array_unique(array_filter(preg_split('/[\s,]+/', $value)));
     if (!$value) {
       return array();
     }
 
     $object_map = array();
 
     $users = id(new PhabricatorUser())->loadAllWhere(
       '(username IN (%Ls)) OR (email IN (%Ls))',
       $value,
       $value);
     $object_map += mpull($users, 'getPHID', 'getUsername');
     $object_map += mpull($users, 'getPHID', 'getEmail');
 
     if ($include_mailables) {
       $mailables = id(new PhabricatorMetaMTAMailingList())->loadAllWhere(
         '(email IN (%Ls)) OR (name IN (%Ls))',
         $value,
         $value);
       $object_map += mpull($mailables, 'getPHID', 'getName');
       $object_map += mpull($mailables, 'getPHID', 'getEmail');
     }
 
     $invalid = array();
     $results = array();
     foreach ($value as $name) {
       if (empty($object_map[$name])) {
         $invalid[] = $name;
       } else {
         $results[] = $object_map[$name];
       }
     }
 
     if ($invalid) {
       $invalid = implode(', ', $invalid);
       $what = $include_mailables
         ? "users and mailing lists"
         : "users";
       throw new DifferentialFieldParseException(
         "Commit message references nonexistent {$what}: {$invalid}.");
     }
 
     return array_unique($results);
   }
 
 
 /* -(  Contextual Data  )---------------------------------------------------- */
 
 
   /**
    * @task context
    */
   final public function setRevision(DifferentialRevision $revision) {
     $this->revision = $revision;
     $this->didSetRevision();
     return $this;
   }
 
   /**
    * @task context
    */
   protected function didSetRevision() {
     return;
   }
 
 
   /**
    * @task context
    */
   final public function setDiff(DifferentialDiff $diff) {
     $this->diff = $diff;
     return $this;
   }
 
   /**
    * @task context
    */
   final public function setHandles(array $handles) {
     $this->handles = $handles;
     return $this;
   }
 
   /**
    * @task context
    */
   final public function setDiffProperties(array $diff_properties) {
     $this->diffProperties = $diff_properties;
     return $this;
   }
 
   /**
    * @task context
    */
   final public function setUser(PhabricatorUser $user) {
     $this->user = $user;
     return $this;
   }
 
   /**
    * @task context
    */
   final protected function getRevision() {
     if (empty($this->revision)) {
       throw new DifferentialFieldDataNotAvailableException($this);
     }
     return $this->revision;
   }
 
   /**
    * @task context
    */
   final protected function getDiff() {
     if (empty($this->diff)) {
       throw new DifferentialFieldDataNotAvailableException($this);
     }
     return $this->diff;
   }
 
   /**
    * @task context
    */
   final protected function getUser() {
     if (empty($this->user)) {
       throw new DifferentialFieldDataNotAvailableException($this);
     }
     return $this->user;
   }
 
   /**
    * Get the handle for an object PHID. You must overload
    * @{method:getRequiredHandlePHIDs} (or a more specific version thereof)
    * and include the PHID you want in the list for it to be available here.
    *
    * @return PhabricatorObjectHandle Handle to the object.
    * @task context
    */
   final protected function getHandle($phid) {
     if ($this->handles === null) {
       throw new DifferentialFieldDataNotAvailableException($this);
     }
     if (empty($this->handles[$phid])) {
       $class = get_class($this);
       throw new Exception(
         "A differential field (of class '{$class}') is attempting to retrieve ".
         "a handle ('{$phid}') which it did not request. Return all handle ".
         "PHIDs you need from getRequiredHandlePHIDs().");
     }
     return $this->handles[$phid];
   }
 
   /**
    * Get a diff property which this field previously requested by returning
    * the key from @{method:getRequiredDiffProperties}.
    *
    * @param  string      Diff property key.
    * @return string|null Diff property, or null if the property does not have
    *                     a value.
    * @task context
    */
   final public function getDiffProperty($key) {
     if ($this->diffProperties === null) {
       // This will be set to some (possibly empty) array if we've loaded
       // properties, so null means diff properties aren't available in this
       // context.
       throw new DifferentialFieldDataNotAvailableException($this);
     }
     if (!array_key_exists($key, $this->diffProperties)) {
       $class = get_class($this);
       throw new Exception(
         "A differential field (of class '{$class}') is attempting to retrieve ".
         "a diff property ('{$key}') which it did not request. Return all ".
         "diff property keys you need from getRequiredDiffProperties().");
     }
     return $this->diffProperties[$key];
   }
 
 }