From 8476ad1a281cd8d8c9707956090d7a5642a6dc46 Mon Sep 17 00:00:00 2001
From: epriestley <git@epriestley.com>
Date: Wed, 14 Dec 2016 08:14:52 -0800
Subject: [PATCH] Separate all commit message field parsing out of Differential
 custom fields

Summary:
Ref T11114. See that task for some discussion.

Overall, Differential custom fields ended up with too many responsibilities. Later work in EditEngine provides a more promising model for achieving modularity with smaller, more consistent components.

In particular, we have some custom fields like `DifferentialGitSVNIDField` and `DifferentialConflictsField` which serve //only// to support the field parser.

This starts pulling commit message responsibilities out of the core list of custom fields and into simpler dedicated parsers.

Test Plan: Created and edited revisions from the CLI. Added a bit of test coverage.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T11114

Differential Revision: https://secure.phabricator.com/D17058
---
 src/__phutil_library_map__.php                |  32 ++++++
 ...DifferentialAuditorsCommitMessageField.php |  21 ++++
 ...rentialBlameRevisionCommitMessageField.php |  22 ++++
 .../field/DifferentialCommitMessageField.php  |  92 ++++++++++++++++
 ...ifferentialConflictsCommitMessageField.php |  12 +++
 ...DifferentialGitSVNIDCommitMessageField.php |  12 +++
 ...fferentialJIRAIssuesCommitMessageField.php |  27 +++++
 ...fferentialRevertPlanCommitMessageField.php |  16 +++
 ...fferentialReviewedByCommitMessageField.php |  22 ++++
 ...ifferentialReviewersCommitMessageField.php |  64 +++++++++++
 ...fferentialRevisionIDCommitMessageField.php |  52 +++++++++
 ...ferentialSubscribersCommitMessageField.php |  30 ++++++
 .../DifferentialSummaryCommitMessageField.php |  12 +++
 .../DifferentialTagsCommitMessageField.php    |  28 +++++
 .../DifferentialTasksCommitMessageField.php   |  28 +++++
 ...DifferentialTestPlanCommitMessageField.php |  36 +++++++
 .../DifferentialTitleCommitMessageField.php   |  36 +++++++
 .../DifferentialCommitMessageParser.php       | 100 +++++++++++-------
 ...ifferentialCommitMessageParserTestCase.php |  30 ++++++
 19 files changed, 632 insertions(+), 40 deletions(-)
 create mode 100644 src/applications/differential/field/DifferentialAuditorsCommitMessageField.php
 create mode 100644 src/applications/differential/field/DifferentialBlameRevisionCommitMessageField.php
 create mode 100644 src/applications/differential/field/DifferentialCommitMessageField.php
 create mode 100644 src/applications/differential/field/DifferentialConflictsCommitMessageField.php
 create mode 100644 src/applications/differential/field/DifferentialGitSVNIDCommitMessageField.php
 create mode 100644 src/applications/differential/field/DifferentialJIRAIssuesCommitMessageField.php
 create mode 100644 src/applications/differential/field/DifferentialRevertPlanCommitMessageField.php
 create mode 100644 src/applications/differential/field/DifferentialReviewedByCommitMessageField.php
 create mode 100644 src/applications/differential/field/DifferentialReviewersCommitMessageField.php
 create mode 100644 src/applications/differential/field/DifferentialRevisionIDCommitMessageField.php
 create mode 100644 src/applications/differential/field/DifferentialSubscribersCommitMessageField.php
 create mode 100644 src/applications/differential/field/DifferentialSummaryCommitMessageField.php
 create mode 100644 src/applications/differential/field/DifferentialTagsCommitMessageField.php
 create mode 100644 src/applications/differential/field/DifferentialTasksCommitMessageField.php
 create mode 100644 src/applications/differential/field/DifferentialTestPlanCommitMessageField.php
 create mode 100644 src/applications/differential/field/DifferentialTitleCommitMessageField.php

diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index ae8760090d..a70eb1d5ae 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -353,8 +353,10 @@ phutil_register_library_map(array(
     'DifferentialAffectedPath' => 'applications/differential/storage/DifferentialAffectedPath.php',
     'DifferentialApplyPatchField' => 'applications/differential/customfield/DifferentialApplyPatchField.php',
     'DifferentialAsanaRepresentationField' => 'applications/differential/customfield/DifferentialAsanaRepresentationField.php',
+    'DifferentialAuditorsCommitMessageField' => 'applications/differential/field/DifferentialAuditorsCommitMessageField.php',
     'DifferentialAuditorsField' => 'applications/differential/customfield/DifferentialAuditorsField.php',
     'DifferentialAuthorField' => 'applications/differential/customfield/DifferentialAuthorField.php',
+    'DifferentialBlameRevisionCommitMessageField' => 'applications/differential/field/DifferentialBlameRevisionCommitMessageField.php',
     'DifferentialBlameRevisionField' => 'applications/differential/customfield/DifferentialBlameRevisionField.php',
     'DifferentialBlockHeraldAction' => 'applications/differential/herald/DifferentialBlockHeraldAction.php',
     'DifferentialBlockingReviewerDatasource' => 'applications/differential/typeahead/DifferentialBlockingReviewerDatasource.php',
@@ -383,10 +385,12 @@ phutil_register_library_map(array(
     'DifferentialCloseConduitAPIMethod' => 'applications/differential/conduit/DifferentialCloseConduitAPIMethod.php',
     'DifferentialCommentPreviewController' => 'applications/differential/controller/DifferentialCommentPreviewController.php',
     'DifferentialCommentSaveController' => 'applications/differential/controller/DifferentialCommentSaveController.php',
+    'DifferentialCommitMessageField' => 'applications/differential/field/DifferentialCommitMessageField.php',
     'DifferentialCommitMessageParser' => 'applications/differential/parser/DifferentialCommitMessageParser.php',
     'DifferentialCommitMessageParserTestCase' => 'applications/differential/parser/__tests__/DifferentialCommitMessageParserTestCase.php',
     'DifferentialCommitsField' => 'applications/differential/customfield/DifferentialCommitsField.php',
     'DifferentialConduitAPIMethod' => 'applications/differential/conduit/DifferentialConduitAPIMethod.php',
+    'DifferentialConflictsCommitMessageField' => 'applications/differential/field/DifferentialConflictsCommitMessageField.php',
     'DifferentialConflictsField' => 'applications/differential/customfield/DifferentialConflictsField.php',
     'DifferentialController' => 'applications/differential/controller/DifferentialController.php',
     'DifferentialCoreCustomField' => 'applications/differential/customfield/DifferentialCoreCustomField.php',
@@ -444,6 +448,7 @@ phutil_register_library_map(array(
     'DifferentialGetRevisionConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetRevisionConduitAPIMethod.php',
     'DifferentialGetWorkingCopy' => 'applications/differential/DifferentialGetWorkingCopy.php',
     'DifferentialGitHubLandingStrategy' => 'applications/differential/landing/DifferentialGitHubLandingStrategy.php',
+    'DifferentialGitSVNIDCommitMessageField' => 'applications/differential/field/DifferentialGitSVNIDCommitMessageField.php',
     'DifferentialGitSVNIDField' => 'applications/differential/customfield/DifferentialGitSVNIDField.php',
     'DifferentialHarbormasterField' => 'applications/differential/customfield/DifferentialHarbormasterField.php',
     'DifferentialHiddenComment' => 'applications/differential/storage/DifferentialHiddenComment.php',
@@ -461,6 +466,7 @@ phutil_register_library_map(array(
     'DifferentialInlineCommentMailView' => 'applications/differential/mail/DifferentialInlineCommentMailView.php',
     'DifferentialInlineCommentPreviewController' => 'applications/differential/controller/DifferentialInlineCommentPreviewController.php',
     'DifferentialInlineCommentQuery' => 'applications/differential/query/DifferentialInlineCommentQuery.php',
+    'DifferentialJIRAIssuesCommitMessageField' => 'applications/differential/field/DifferentialJIRAIssuesCommitMessageField.php',
     'DifferentialJIRAIssuesField' => 'applications/differential/customfield/DifferentialJIRAIssuesField.php',
     'DifferentialLandingActionMenuEventListener' => 'applications/differential/landing/DifferentialLandingActionMenuEventListener.php',
     'DifferentialLandingStrategy' => 'applications/differential/landing/DifferentialLandingStrategy.php',
@@ -493,7 +499,9 @@ phutil_register_library_map(array(
     'DifferentialResponsibleDatasource' => 'applications/differential/typeahead/DifferentialResponsibleDatasource.php',
     'DifferentialResponsibleUserDatasource' => 'applications/differential/typeahead/DifferentialResponsibleUserDatasource.php',
     'DifferentialResponsibleViewerFunctionDatasource' => 'applications/differential/typeahead/DifferentialResponsibleViewerFunctionDatasource.php',
+    'DifferentialRevertPlanCommitMessageField' => 'applications/differential/field/DifferentialRevertPlanCommitMessageField.php',
     'DifferentialRevertPlanField' => 'applications/differential/customfield/DifferentialRevertPlanField.php',
+    'DifferentialReviewedByCommitMessageField' => 'applications/differential/field/DifferentialReviewedByCommitMessageField.php',
     'DifferentialReviewedByField' => 'applications/differential/customfield/DifferentialReviewedByField.php',
     'DifferentialReviewerDatasource' => 'applications/differential/typeahead/DifferentialReviewerDatasource.php',
     'DifferentialReviewerForRevisionEdgeType' => 'applications/differential/edge/DifferentialReviewerForRevisionEdgeType.php',
@@ -503,6 +511,7 @@ phutil_register_library_map(array(
     'DifferentialReviewersAddBlockingSelfHeraldAction' => 'applications/differential/herald/DifferentialReviewersAddBlockingSelfHeraldAction.php',
     'DifferentialReviewersAddReviewersHeraldAction' => 'applications/differential/herald/DifferentialReviewersAddReviewersHeraldAction.php',
     'DifferentialReviewersAddSelfHeraldAction' => 'applications/differential/herald/DifferentialReviewersAddSelfHeraldAction.php',
+    'DifferentialReviewersCommitMessageField' => 'applications/differential/field/DifferentialReviewersCommitMessageField.php',
     'DifferentialReviewersField' => 'applications/differential/customfield/DifferentialReviewersField.php',
     'DifferentialReviewersHeraldAction' => 'applications/differential/herald/DifferentialReviewersHeraldAction.php',
     'DifferentialReviewersView' => 'applications/differential/view/DifferentialReviewersView.php',
@@ -530,6 +539,7 @@ phutil_register_library_map(array(
     'DifferentialRevisionHasTaskRelationship' => 'applications/differential/relationships/DifferentialRevisionHasTaskRelationship.php',
     'DifferentialRevisionHeraldField' => 'applications/differential/herald/DifferentialRevisionHeraldField.php',
     'DifferentialRevisionHeraldFieldGroup' => 'applications/differential/herald/DifferentialRevisionHeraldFieldGroup.php',
+    'DifferentialRevisionIDCommitMessageField' => 'applications/differential/field/DifferentialRevisionIDCommitMessageField.php',
     'DifferentialRevisionIDField' => 'applications/differential/customfield/DifferentialRevisionIDField.php',
     'DifferentialRevisionLandController' => 'applications/differential/controller/DifferentialRevisionLandController.php',
     'DifferentialRevisionListController' => 'applications/differential/controller/DifferentialRevisionListController.php',
@@ -563,9 +573,15 @@ phutil_register_library_map(array(
     'DifferentialSchemaSpec' => 'applications/differential/storage/DifferentialSchemaSpec.php',
     'DifferentialSetDiffPropertyConduitAPIMethod' => 'applications/differential/conduit/DifferentialSetDiffPropertyConduitAPIMethod.php',
     'DifferentialStoredCustomField' => 'applications/differential/customfield/DifferentialStoredCustomField.php',
+    'DifferentialSubscribersCommitMessageField' => 'applications/differential/field/DifferentialSubscribersCommitMessageField.php',
     'DifferentialSubscribersField' => 'applications/differential/customfield/DifferentialSubscribersField.php',
+    'DifferentialSummaryCommitMessageField' => 'applications/differential/field/DifferentialSummaryCommitMessageField.php',
     'DifferentialSummaryField' => 'applications/differential/customfield/DifferentialSummaryField.php',
+    'DifferentialTagsCommitMessageField' => 'applications/differential/field/DifferentialTagsCommitMessageField.php',
+    'DifferentialTasksCommitMessageField' => 'applications/differential/field/DifferentialTasksCommitMessageField.php',
+    'DifferentialTestPlanCommitMessageField' => 'applications/differential/field/DifferentialTestPlanCommitMessageField.php',
     'DifferentialTestPlanField' => 'applications/differential/customfield/DifferentialTestPlanField.php',
+    'DifferentialTitleCommitMessageField' => 'applications/differential/field/DifferentialTitleCommitMessageField.php',
     'DifferentialTitleField' => 'applications/differential/customfield/DifferentialTitleField.php',
     'DifferentialTransaction' => 'applications/differential/storage/DifferentialTransaction.php',
     'DifferentialTransactionComment' => 'applications/differential/storage/DifferentialTransactionComment.php',
@@ -4976,8 +4992,10 @@ phutil_register_library_map(array(
     'DifferentialAffectedPath' => 'DifferentialDAO',
     'DifferentialApplyPatchField' => 'DifferentialCustomField',
     'DifferentialAsanaRepresentationField' => 'DifferentialCustomField',
+    'DifferentialAuditorsCommitMessageField' => 'DifferentialCommitMessageField',
     'DifferentialAuditorsField' => 'DifferentialStoredCustomField',
     'DifferentialAuthorField' => 'DifferentialCustomField',
+    'DifferentialBlameRevisionCommitMessageField' => 'DifferentialCommitMessageField',
     'DifferentialBlameRevisionField' => 'DifferentialStoredCustomField',
     'DifferentialBlockHeraldAction' => 'HeraldAction',
     'DifferentialBlockingReviewerDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
@@ -5009,10 +5027,12 @@ phutil_register_library_map(array(
     'DifferentialCloseConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialCommentPreviewController' => 'DifferentialController',
     'DifferentialCommentSaveController' => 'DifferentialController',
+    'DifferentialCommitMessageField' => 'Phobject',
     'DifferentialCommitMessageParser' => 'Phobject',
     'DifferentialCommitMessageParserTestCase' => 'PhabricatorTestCase',
     'DifferentialCommitsField' => 'DifferentialCustomField',
     'DifferentialConduitAPIMethod' => 'ConduitAPIMethod',
+    'DifferentialConflictsCommitMessageField' => 'DifferentialCommitMessageField',
     'DifferentialConflictsField' => 'DifferentialCustomField',
     'DifferentialController' => 'PhabricatorController',
     'DifferentialCoreCustomField' => 'DifferentialCustomField',
@@ -5077,6 +5097,7 @@ phutil_register_library_map(array(
     'DifferentialGetRevisionConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialGetWorkingCopy' => 'Phobject',
     'DifferentialGitHubLandingStrategy' => 'DifferentialLandingStrategy',
+    'DifferentialGitSVNIDCommitMessageField' => 'DifferentialCommitMessageField',
     'DifferentialGitSVNIDField' => 'DifferentialCustomField',
     'DifferentialHarbormasterField' => 'DifferentialCustomField',
     'DifferentialHiddenComment' => 'DifferentialDAO',
@@ -5100,6 +5121,7 @@ phutil_register_library_map(array(
     'DifferentialInlineCommentMailView' => 'DifferentialMailView',
     'DifferentialInlineCommentPreviewController' => 'PhabricatorInlineCommentPreviewController',
     'DifferentialInlineCommentQuery' => 'PhabricatorOffsetPagedQuery',
+    'DifferentialJIRAIssuesCommitMessageField' => 'DifferentialCommitMessageField',
     'DifferentialJIRAIssuesField' => 'DifferentialStoredCustomField',
     'DifferentialLandingActionMenuEventListener' => 'PhabricatorEventListener',
     'DifferentialLandingStrategy' => 'Phobject',
@@ -5132,7 +5154,9 @@ phutil_register_library_map(array(
     'DifferentialResponsibleDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'DifferentialResponsibleUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'DifferentialResponsibleViewerFunctionDatasource' => 'PhabricatorTypeaheadDatasource',
+    'DifferentialRevertPlanCommitMessageField' => 'DifferentialCommitMessageField',
     'DifferentialRevertPlanField' => 'DifferentialStoredCustomField',
+    'DifferentialReviewedByCommitMessageField' => 'DifferentialCommitMessageField',
     'DifferentialReviewedByField' => 'DifferentialCoreCustomField',
     'DifferentialReviewerDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'DifferentialReviewerForRevisionEdgeType' => 'PhabricatorEdgeType',
@@ -5142,6 +5166,7 @@ phutil_register_library_map(array(
     'DifferentialReviewersAddBlockingSelfHeraldAction' => 'DifferentialReviewersHeraldAction',
     'DifferentialReviewersAddReviewersHeraldAction' => 'DifferentialReviewersHeraldAction',
     'DifferentialReviewersAddSelfHeraldAction' => 'DifferentialReviewersHeraldAction',
+    'DifferentialReviewersCommitMessageField' => 'DifferentialCommitMessageField',
     'DifferentialReviewersField' => 'DifferentialCoreCustomField',
     'DifferentialReviewersHeraldAction' => 'HeraldAction',
     'DifferentialReviewersView' => 'AphrontView',
@@ -5185,6 +5210,7 @@ phutil_register_library_map(array(
     'DifferentialRevisionHasTaskRelationship' => 'DifferentialRevisionRelationship',
     'DifferentialRevisionHeraldField' => 'HeraldField',
     'DifferentialRevisionHeraldFieldGroup' => 'HeraldFieldGroup',
+    'DifferentialRevisionIDCommitMessageField' => 'DifferentialCommitMessageField',
     'DifferentialRevisionIDField' => 'DifferentialCustomField',
     'DifferentialRevisionLandController' => 'DifferentialController',
     'DifferentialRevisionListController' => 'DifferentialController',
@@ -5218,9 +5244,15 @@ phutil_register_library_map(array(
     'DifferentialSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'DifferentialSetDiffPropertyConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialStoredCustomField' => 'DifferentialCustomField',
+    'DifferentialSubscribersCommitMessageField' => 'DifferentialCommitMessageField',
     'DifferentialSubscribersField' => 'DifferentialCoreCustomField',
+    'DifferentialSummaryCommitMessageField' => 'DifferentialCommitMessageField',
     'DifferentialSummaryField' => 'DifferentialCoreCustomField',
+    'DifferentialTagsCommitMessageField' => 'DifferentialCommitMessageField',
+    'DifferentialTasksCommitMessageField' => 'DifferentialCommitMessageField',
+    'DifferentialTestPlanCommitMessageField' => 'DifferentialCommitMessageField',
     'DifferentialTestPlanField' => 'DifferentialCoreCustomField',
+    'DifferentialTitleCommitMessageField' => 'DifferentialCommitMessageField',
     'DifferentialTitleField' => 'DifferentialCoreCustomField',
     'DifferentialTransaction' => 'PhabricatorModularTransaction',
     'DifferentialTransactionComment' => 'PhabricatorApplicationTransactionComment',
diff --git a/src/applications/differential/field/DifferentialAuditorsCommitMessageField.php b/src/applications/differential/field/DifferentialAuditorsCommitMessageField.php
new file mode 100644
index 0000000000..fc3bc95fd3
--- /dev/null
+++ b/src/applications/differential/field/DifferentialAuditorsCommitMessageField.php
@@ -0,0 +1,21 @@
+<?php
+
+final class DifferentialAuditorsCommitMessageField
+  extends DifferentialCommitMessageField {
+
+  const FIELDKEY = 'phabricator:auditors';
+
+  public function getFieldName() {
+    return pht('Auditors');
+  }
+
+  public function parseFieldValue($value) {
+    return $this->parseObjectList(
+      $value,
+      array(
+        PhabricatorPeopleUserPHIDType::TYPECONST,
+        PhabricatorProjectProjectPHIDType::TYPECONST,
+      ));
+  }
+
+}
diff --git a/src/applications/differential/field/DifferentialBlameRevisionCommitMessageField.php b/src/applications/differential/field/DifferentialBlameRevisionCommitMessageField.php
new file mode 100644
index 0000000000..d48088c68f
--- /dev/null
+++ b/src/applications/differential/field/DifferentialBlameRevisionCommitMessageField.php
@@ -0,0 +1,22 @@
+<?php
+
+final class DifferentialBlameRevisionCommitMessageField
+  extends DifferentialCommitMessageField {
+
+  const FIELDKEY = 'blameRevision';
+
+  public function getFieldName() {
+    return pht('Blame Revision');
+  }
+
+  public function getFieldAliases() {
+    return array(
+      'Blame Rev',
+    );
+  }
+
+  public function isFieldEnabled() {
+    return $this->isCustomFieldEnabled('phabricator:blame-revision');
+  }
+
+}
diff --git a/src/applications/differential/field/DifferentialCommitMessageField.php b/src/applications/differential/field/DifferentialCommitMessageField.php
new file mode 100644
index 0000000000..ddf5cc5b28
--- /dev/null
+++ b/src/applications/differential/field/DifferentialCommitMessageField.php
@@ -0,0 +1,92 @@
+<?php
+
+abstract class DifferentialCommitMessageField
+  extends Phobject {
+
+  private $viewer;
+
+  final public function setViewer(PhabricatorUser $viewer) {
+    $this->viewer = $viewer;
+    return $this;
+  }
+
+  final public function getViewer() {
+    return $this->viewer;
+  }
+
+  abstract public function getFieldName();
+
+  public function isFieldEnabled() {
+    return true;
+  }
+
+  public function getFieldAliases() {
+    return array();
+  }
+
+  public function validateFieldValue($value) {
+    return;
+  }
+
+  public function parseFieldValue($value) {
+    return $value;
+  }
+
+  final public function getCommitMessageFieldKey() {
+    return $this->getPhobjectClassConstant('FIELDKEY', 64);
+  }
+
+  final public static function newEnabledFields(PhabricatorUser $viewer) {
+    $fields = self::getAllFields();
+
+    $results = array();
+    foreach ($fields as $key => $field) {
+      $field = clone $field;
+      $field->setViewer($viewer);
+      if ($field->isFieldEnabled()) {
+        $results[$key] = $field;
+      }
+    }
+
+    return $results;
+  }
+
+  final public static function getAllFields() {
+    return id(new PhutilClassMapQuery())
+      ->setAncestorClass(__CLASS__)
+      ->setUniqueMethod('getCommitMessageFieldKey')
+      ->execute();
+  }
+
+  protected function raiseParseException($message) {
+    throw new DifferentialFieldParseException($message);
+  }
+
+  protected function raiseValidationException($message) {
+    throw new DifferentialFieldValidationException($message);
+  }
+
+  protected function parseObjectList(
+    $value,
+    array $types,
+    $allow_partial = false,
+    array $suffixes = array()) {
+    return id(new PhabricatorObjectListQuery())
+      ->setViewer($this->getViewer())
+      ->setAllowedTypes($types)
+      ->setObjectList($value)
+      ->setAllowPartialResults($allow_partial)
+      ->setSuffixes($suffixes)
+      ->execute();
+  }
+
+  protected function isCustomFieldEnabled($key) {
+    $field_list = PhabricatorCustomField::getObjectFields(
+      new DifferentialRevision(),
+      PhabricatorCustomField::ROLE_VIEW);
+
+    $fields = $field_list->getFields();
+    return isset($fields[$key]);
+  }
+
+}
diff --git a/src/applications/differential/field/DifferentialConflictsCommitMessageField.php b/src/applications/differential/field/DifferentialConflictsCommitMessageField.php
new file mode 100644
index 0000000000..334a7a060f
--- /dev/null
+++ b/src/applications/differential/field/DifferentialConflictsCommitMessageField.php
@@ -0,0 +1,12 @@
+<?php
+
+final class DifferentialConflictsCommitMessageField
+  extends DifferentialCommitMessageField {
+
+  const FIELDKEY = 'conflicts';
+
+  public function getFieldName() {
+    return pht('Conflicts');
+  }
+
+}
diff --git a/src/applications/differential/field/DifferentialGitSVNIDCommitMessageField.php b/src/applications/differential/field/DifferentialGitSVNIDCommitMessageField.php
new file mode 100644
index 0000000000..603b9cce22
--- /dev/null
+++ b/src/applications/differential/field/DifferentialGitSVNIDCommitMessageField.php
@@ -0,0 +1,12 @@
+<?php
+
+final class DifferentialGitSVNIDCommitMessageField
+  extends DifferentialCommitMessageField {
+
+  const FIELDKEY = 'gitSVNID';
+
+  public function getFieldName() {
+    return pht('git-svn-id');
+  }
+
+}
diff --git a/src/applications/differential/field/DifferentialJIRAIssuesCommitMessageField.php b/src/applications/differential/field/DifferentialJIRAIssuesCommitMessageField.php
new file mode 100644
index 0000000000..f57566cbde
--- /dev/null
+++ b/src/applications/differential/field/DifferentialJIRAIssuesCommitMessageField.php
@@ -0,0 +1,27 @@
+<?php
+
+final class DifferentialJIRAIssuesCommitMessageField
+  extends DifferentialCommitMessageField {
+
+  const FIELDKEY = 'jira.issues';
+
+  public function getFieldName() {
+    return pht('JIRA Issues');
+  }
+
+  public function getFieldAliases() {
+    return array(
+      'JIRA',
+      'JIRA Issue',
+    );
+  }
+
+  public function parseFieldValue($value) {
+    return preg_split('/[\s,]+/', $value, $limit = -1, PREG_SPLIT_NO_EMPTY);
+  }
+
+  public function isFieldEnabled() {
+    return (bool)PhabricatorJIRAAuthProvider::getJIRAProvider();
+  }
+
+}
diff --git a/src/applications/differential/field/DifferentialRevertPlanCommitMessageField.php b/src/applications/differential/field/DifferentialRevertPlanCommitMessageField.php
new file mode 100644
index 0000000000..9f79266d4e
--- /dev/null
+++ b/src/applications/differential/field/DifferentialRevertPlanCommitMessageField.php
@@ -0,0 +1,16 @@
+<?php
+
+final class DifferentialRevertPlanCommitMessageField
+  extends DifferentialCommitMessageField {
+
+  const FIELDKEY = 'revertPlan';
+
+  public function getFieldName() {
+    return pht('Revert Plan');
+  }
+
+  public function isFieldEnabled() {
+    return $this->isCustomFieldEnabled('phabricator:revert-plan');
+  }
+
+}
diff --git a/src/applications/differential/field/DifferentialReviewedByCommitMessageField.php b/src/applications/differential/field/DifferentialReviewedByCommitMessageField.php
new file mode 100644
index 0000000000..ea222b28aa
--- /dev/null
+++ b/src/applications/differential/field/DifferentialReviewedByCommitMessageField.php
@@ -0,0 +1,22 @@
+<?php
+
+final class DifferentialReviewedByCommitMessageField
+  extends DifferentialCommitMessageField {
+
+  const FIELDKEY = 'reviewedByPHIDs';
+
+  public function getFieldName() {
+    return pht('Reviewed By');
+  }
+
+  public function parseFieldValue($value) {
+    return $this->parseObjectList(
+      $value,
+      array(
+        PhabricatorPeopleUserPHIDType::TYPECONST,
+        PhabricatorProjectProjectPHIDType::TYPECONST,
+      ),
+      $allow_partial = true);
+  }
+
+}
diff --git a/src/applications/differential/field/DifferentialReviewersCommitMessageField.php b/src/applications/differential/field/DifferentialReviewersCommitMessageField.php
new file mode 100644
index 0000000000..e3c275b471
--- /dev/null
+++ b/src/applications/differential/field/DifferentialReviewersCommitMessageField.php
@@ -0,0 +1,64 @@
+<?php
+
+final class DifferentialReviewersCommitMessageField
+  extends DifferentialCommitMessageField {
+
+  const FIELDKEY = 'reviewerPHIDs';
+
+  public function getFieldName() {
+    return pht('Reviewers');
+  }
+
+  public function getFieldAliases() {
+    return array(
+      'Reviewer',
+    );
+  }
+
+  public function parseFieldValue($value) {
+    $results = $this->parseObjectList(
+      $value,
+      array(
+        PhabricatorPeopleUserPHIDType::TYPECONST,
+        PhabricatorProjectProjectPHIDType::TYPECONST,
+        PhabricatorOwnersPackagePHIDType::TYPECONST,
+      ),
+      false,
+      array('!'));
+
+    return $this->flattenReviewers($results);
+  }
+
+  private function flattenReviewers(array $values) {
+    // NOTE: For now, `arc` relies on this field returning only scalars, so we
+    // need to reduce the results into scalars. See T10981.
+    $result = array();
+
+    foreach ($values as $value) {
+      $result[] = $value['phid'].implode('', array_keys($value['suffixes']));
+    }
+
+    return $result;
+  }
+
+  private function inflateReviewers(array $values) {
+    $result = array();
+
+    foreach ($values as $value) {
+      if (substr($value, -1) == '!') {
+        $value = substr($value, 0, -1);
+        $suffixes = array('!' => '!');
+      } else {
+        $suffixes = array();
+      }
+
+      $result[] = array(
+        'phid' => $value,
+        'suffixes' => $suffixes,
+      );
+    }
+
+    return $result;
+  }
+
+}
diff --git a/src/applications/differential/field/DifferentialRevisionIDCommitMessageField.php b/src/applications/differential/field/DifferentialRevisionIDCommitMessageField.php
new file mode 100644
index 0000000000..5e2b378e12
--- /dev/null
+++ b/src/applications/differential/field/DifferentialRevisionIDCommitMessageField.php
@@ -0,0 +1,52 @@
+<?php
+
+final class DifferentialRevisionIDCommitMessageField
+  extends DifferentialCommitMessageField {
+
+  const FIELDKEY = 'revisionID';
+
+  public function getFieldName() {
+    return pht('Differential Revision');
+  }
+
+  public function parseFieldValue($value) {
+    // If the value is just "D123" or similar, parse the ID from it directly.
+    $value = trim($value);
+    $matches = null;
+    if (preg_match('/^[dD]([1-9]\d*)\z/', $value, $matches)) {
+      return (int)$matches[1];
+    }
+
+    // Otherwise, try to extract a URI value.
+    return self::parseRevisionIDFromURI($value);
+  }
+
+  private static function parseRevisionIDFromURI($uri_string) {
+    $uri = new PhutilURI($uri_string);
+    $path = $uri->getPath();
+
+    $matches = null;
+    if (preg_match('#^/D(\d+)$#', $path, $matches)) {
+      $id = (int)$matches[1];
+
+      $prod_uri = new PhutilURI(PhabricatorEnv::getProductionURI('/D'.$id));
+
+      // Make sure the URI is the same as our URI. Basically, we want to ignore
+      // commits from other Phabricator installs.
+      if ($uri->getDomain() == $prod_uri->getDomain()) {
+        return $id;
+      }
+
+      $allowed_uris = PhabricatorEnv::getAllowedURIs('/D'.$id);
+
+      foreach ($allowed_uris as $allowed_uri) {
+        if ($uri_string == $allowed_uri) {
+          return $id;
+        }
+      }
+    }
+
+    return null;
+  }
+
+}
diff --git a/src/applications/differential/field/DifferentialSubscribersCommitMessageField.php b/src/applications/differential/field/DifferentialSubscribersCommitMessageField.php
new file mode 100644
index 0000000000..ba693a3493
--- /dev/null
+++ b/src/applications/differential/field/DifferentialSubscribersCommitMessageField.php
@@ -0,0 +1,30 @@
+<?php
+
+final class DifferentialSubscribersCommitMessageField
+  extends DifferentialCommitMessageField {
+
+  const FIELDKEY = 'ccPHIDs';
+
+  public function getFieldName() {
+    return pht('Subscribers');
+  }
+
+  public function getFieldAliases() {
+    return array(
+      'CC',
+      'CCs',
+      'Subscriber',
+    );
+  }
+
+  public function parseFieldValue($value) {
+    return $this->parseObjectList(
+      $value,
+      array(
+        PhabricatorPeopleUserPHIDType::TYPECONST,
+        PhabricatorProjectProjectPHIDType::TYPECONST,
+        PhabricatorOwnersPackagePHIDType::TYPECONST,
+      ));
+  }
+
+}
diff --git a/src/applications/differential/field/DifferentialSummaryCommitMessageField.php b/src/applications/differential/field/DifferentialSummaryCommitMessageField.php
new file mode 100644
index 0000000000..531d542633
--- /dev/null
+++ b/src/applications/differential/field/DifferentialSummaryCommitMessageField.php
@@ -0,0 +1,12 @@
+<?php
+
+final class DifferentialSummaryCommitMessageField
+  extends DifferentialCommitMessageField {
+
+  const FIELDKEY = 'summary';
+
+  public function getFieldName() {
+    return pht('Summary');
+  }
+
+}
diff --git a/src/applications/differential/field/DifferentialTagsCommitMessageField.php b/src/applications/differential/field/DifferentialTagsCommitMessageField.php
new file mode 100644
index 0000000000..7b22df06aa
--- /dev/null
+++ b/src/applications/differential/field/DifferentialTagsCommitMessageField.php
@@ -0,0 +1,28 @@
+<?php
+
+final class DifferentialTagsCommitMessageField
+  extends DifferentialCommitMessageField {
+
+  const FIELDKEY = 'phabricator:projects';
+
+  public function getFieldName() {
+    return pht('Tags');
+  }
+
+  public function getFieldAliases() {
+    return array(
+      'Tag',
+      'Project',
+      'Projects',
+    );
+  }
+
+  public function parseFieldValue($value) {
+    return $this->parseObjectList(
+      $value,
+      array(
+        PhabricatorProjectProjectPHIDType::TYPECONST,
+      ));
+  }
+
+}
diff --git a/src/applications/differential/field/DifferentialTasksCommitMessageField.php b/src/applications/differential/field/DifferentialTasksCommitMessageField.php
new file mode 100644
index 0000000000..652de0ffe6
--- /dev/null
+++ b/src/applications/differential/field/DifferentialTasksCommitMessageField.php
@@ -0,0 +1,28 @@
+<?php
+
+final class DifferentialTasksCommitMessageField
+  extends DifferentialCommitMessageField {
+
+  const FIELDKEY = 'maniphestTaskPHIDs';
+
+  public function getFieldName() {
+    return pht('Maniphest Tasks');
+  }
+
+  public function getFieldAliases() {
+    return array(
+      'Task',
+      'Tasks',
+      'Maniphest Task',
+    );
+  }
+
+  public function parseFieldValue($value) {
+    return $this->parseObjectList(
+      $value,
+      array(
+        ManiphestTaskPHIDType::TYPECONST,
+      ));
+  }
+
+}
diff --git a/src/applications/differential/field/DifferentialTestPlanCommitMessageField.php b/src/applications/differential/field/DifferentialTestPlanCommitMessageField.php
new file mode 100644
index 0000000000..d9f1a3e3b6
--- /dev/null
+++ b/src/applications/differential/field/DifferentialTestPlanCommitMessageField.php
@@ -0,0 +1,36 @@
+<?php
+
+final class DifferentialTestPlanCommitMessageField
+  extends DifferentialCommitMessageField {
+
+  const FIELDKEY = 'testPlan';
+
+  public function getFieldName() {
+    return pht('Test Plan');
+  }
+
+  public function getFieldAliases() {
+    return array(
+      'Testplan',
+      'Tested',
+      'Tests',
+    );
+  }
+
+  public function isFieldEnabled() {
+    return $this->isCustomFieldEnabled('differential:test-plan');
+  }
+
+  public function validateFieldValue($value) {
+    $is_required = PhabricatorEnv::getEnvConfig(
+      'differential.require-test-plan-field');
+
+    if ($is_required && !strlen($value)) {
+      $this->raiseValidationException(
+        pht(
+          'You must provide a test plan. Describe the actions you performed '.
+          'to verify the behavior of this change.'));
+    }
+  }
+
+}
diff --git a/src/applications/differential/field/DifferentialTitleCommitMessageField.php b/src/applications/differential/field/DifferentialTitleCommitMessageField.php
new file mode 100644
index 0000000000..af5ed58545
--- /dev/null
+++ b/src/applications/differential/field/DifferentialTitleCommitMessageField.php
@@ -0,0 +1,36 @@
+<?php
+
+final class DifferentialTitleCommitMessageField
+  extends DifferentialCommitMessageField {
+
+  const FIELDKEY = 'title';
+
+  public function getFieldName() {
+    return pht('Title');
+  }
+
+  public static function getDefaultTitle() {
+    return pht('<<Replace this line with your revision title>');
+  }
+
+  public function parseFieldValue($value) {
+    if ($value === self::getDefaultTitle()) {
+      $this->raiseParseException(
+        pht(
+          'Replace the default title line with a human-readable revision '.
+          'title which describes the changes you are making.'));
+    }
+
+    return parent::parseFieldValue($value);
+  }
+
+  public function validateFieldValue($value) {
+    if (!strlen($value)) {
+      $this->raiseValidationException(
+        pht(
+          'You must provide a revision title in the first line '.
+          'of your commit message.'));
+    }
+  }
+
+}
diff --git a/src/applications/differential/parser/DifferentialCommitMessageParser.php b/src/applications/differential/parser/DifferentialCommitMessageParser.php
index 8cd508ef5f..6cf126bd0f 100644
--- a/src/applications/differential/parser/DifferentialCommitMessageParser.php
+++ b/src/applications/differential/parser/DifferentialCommitMessageParser.php
@@ -26,43 +26,18 @@ final class DifferentialCommitMessageParser extends Phobject {
   private $titleKey;
   private $summaryKey;
   private $errors;
+  private $commitMessageFields;
   private $raiseMissingFieldErrors = true;
 
   public static function newStandardParser(PhabricatorUser $viewer) {
+    $key_title = DifferentialTitleCommitMessageField::FIELDKEY;
+    $key_summary = DifferentialSummaryCommitMessageField::FIELDKEY;
 
-    $key_title = id(new DifferentialTitleField())->getFieldKeyForConduit();
-    $key_summary = id(new DifferentialSummaryField())->getFieldKeyForConduit();
-
-    $field_list = PhabricatorCustomField::getObjectFields(
-      new DifferentialRevision(),
-      DifferentialCustomField::ROLE_COMMITMESSAGE);
-    $field_list->setViewer($viewer);
-
-    $label_map = array();
-
-    foreach ($field_list->getFields() as $field) {
-      $labels = $field->getCommitMessageLabels();
-      $key = $field->getFieldKeyForConduit();
-
-      foreach ($labels as $label) {
-        $normal_label = self::normalizeFieldLabel(
-          $label);
-        if (!empty($label_map[$normal_label])) {
-          throw new Exception(
-            pht(
-              'Field label "%s" is parsed by two custom fields: "%s" and '.
-              '"%s". Each label must be parsed by only one field.',
-              $label,
-              $key,
-              $label_map[$normal_label]));
-        }
-        $label_map[$normal_label] = $key;
-      }
-    }
+    $field_list = DifferentialCommitMessageField::newEnabledFields($viewer);
 
     return id(new self())
       ->setViewer($viewer)
-      ->setLabelMap($label_map)
+      ->setCommitMessageFields($field_list)
       ->setTitleKey($key_title)
       ->setSummaryKey($key_summary);
   }
@@ -88,6 +63,25 @@ final class DifferentialCommitMessageParser extends Phobject {
   }
 
 
+  /**
+   * @task config
+   */
+  public function setCommitMessageFields($fields) {
+    assert_instances_of($fields, 'DifferentialCommitMessageField');
+    $fields = mpull($fields, null, 'getCommitMessageFieldKey');
+    $this->commitMessageFields = $fields;
+    return $this;
+  }
+
+
+  /**
+   * @task config
+   */
+  public function getCommitMessageFields() {
+    return $this->commitMessageFields;
+  }
+
+
   /**
    * @task config
    */
@@ -141,7 +135,7 @@ final class DifferentialCommitMessageParser extends Phobject {
   public function parseCorpus($corpus) {
     $this->errors = array();
 
-    $label_map = $this->labelMap;
+    $label_map = $this->getLabelMap();
     $key_title = $this->titleKey;
     $key_summary = $this->summaryKey;
 
@@ -258,13 +252,7 @@ final class DifferentialCommitMessageParser extends Phobject {
     $viewer = $this->getViewer();
     $text_map = $this->parseCorpus($corpus);
 
-    $field_list = PhabricatorCustomField::getObjectFields(
-      new DifferentialRevision(),
-      DifferentialCustomField::ROLE_COMMITMESSAGE);
-    $field_list->setViewer($viewer);
-
-    $field_map = $field_list->getFields();
-    $field_map = mpull($field_map, null, 'getFieldKeyForConduit');
+    $field_map = $this->getCommitMessageFields();
 
     $result_map = array();
     foreach ($text_map as $field_key => $text_value) {
@@ -281,7 +269,7 @@ final class DifferentialCommitMessageParser extends Phobject {
       }
 
       try {
-        $result = $field->parseValueFromCommitMessage($text_value);
+        $result = $field->parseFieldValue($text_value);
         $result_map[$field_key] = $result;
       } catch (DifferentialFieldParseException $ex) {
         $this->errors[] = pht(
@@ -294,7 +282,7 @@ final class DifferentialCommitMessageParser extends Phobject {
     if ($this->getRaiseMissingFieldErrors()) {
       foreach ($field_map as $key => $field) {
         try {
-          $field->validateCommitMessageValue(idx($result_map, $key));
+          $field->validateFieldValue(idx($result_map, $key));
         } catch (DifferentialFieldValidationException $ex) {
           $this->errors[] = pht(
             'Invalid or missing field "%s": %s',
@@ -330,6 +318,38 @@ final class DifferentialCommitMessageParser extends Phobject {
 /* -(  Internals  )---------------------------------------------------------- */
 
 
+  private function getLabelMap() {
+    if ($this->labelMap === null) {
+      $field_list = $this->getCommitMessageFields();
+
+      $label_map = array();
+      foreach ($field_list as $field_key => $field) {
+        $labels = $field->getFieldAliases();
+        $labels[] = $field->getFieldName();
+
+        foreach ($labels as $label) {
+          $normal_label = self::normalizeFieldLabel($label);
+          if (!empty($label_map[$normal_label])) {
+            throw new Exception(
+              pht(
+                'Field label "%s" is parsed by two custom fields: "%s" and '.
+                '"%s". Each label must be parsed by only one field.',
+                $label,
+                $field_key,
+                $label_map[$normal_label]));
+          }
+
+          $label_map[$normal_label] = $field_key;
+        }
+      }
+
+      $this->labelMap = $label_map;
+    }
+
+    return $this->labelMap;
+  }
+
+
   /**
    * @task internal
    */
diff --git a/src/applications/differential/parser/__tests__/DifferentialCommitMessageParserTestCase.php b/src/applications/differential/parser/__tests__/DifferentialCommitMessageParserTestCase.php
index 669cfed220..320445fd0d 100644
--- a/src/applications/differential/parser/__tests__/DifferentialCommitMessageParserTestCase.php
+++ b/src/applications/differential/parser/__tests__/DifferentialCommitMessageParserTestCase.php
@@ -41,6 +41,36 @@ final class DifferentialCommitMessageParserTestCase
     }
   }
 
+
+  public function testDifferentialCommitMessageFieldParser() {
+    $message = <<<EOMESSAGE
+This is the title.
+
+Summary: This is the summary.
+EOMESSAGE;
+
+    $fields = array(
+      new DifferentialTitleCommitMessageField(),
+      new DifferentialSummaryCommitMessageField(),
+    );
+
+    $expect = array(
+      DifferentialTitleCommitMessageField::FIELDKEY =>
+        'This is the title.',
+      DifferentialSummaryCommitMessageField::FIELDKEY =>
+        'This is the summary.',
+    );
+
+    $parser = id(new DifferentialCommitMessageParser())
+      ->setCommitMessageFields($fields)
+      ->setTitleKey(DifferentialTitleCommitMessageField::FIELDKEY)
+      ->setSummaryKey(DifferentialSummaryCommitMessageField::FIELDKEY);
+
+    $actual = $parser->parseFields($message);
+
+    $this->assertEqual($expect, $actual);
+  }
+
   public function testDifferentialCommitMessageParserNormalization() {
     $map = array(
       'Test Plan' => 'test plan',