diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 3647f3116f..91f516ff78 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -153,8 +153,6 @@ phutil_register_library_map(array( 'DifferentialCommentMail' => 'applications/differential/mail/comment', 'DifferentialCommentPreviewController' => 'applications/differential/controller/commentpreview', 'DifferentialCommentSaveController' => 'applications/differential/controller/commentsave', - 'DifferentialCommitMessage' => 'applications/differential/parser/commitmessage', - 'DifferentialCommitMessageParserException' => 'applications/differential/parser/commitmessage/exception', 'DifferentialCommitsFieldSpecification' => 'applications/differential/field/specification/commits', 'DifferentialController' => 'applications/differential/controller/base', 'DifferentialDAO' => 'applications/differential/storage/base', @@ -169,6 +167,7 @@ phutil_register_library_map(array( 'DifferentialExceptionMail' => 'applications/differential/mail/exception', 'DifferentialExportPatchFieldSpecification' => 'applications/differential/field/specification/exportpatch', 'DifferentialFieldDataNotAvailableException' => 'applications/differential/field/exception/notavailable', + 'DifferentialFieldParseException' => 'applications/differential/field/exception/parse', 'DifferentialFieldSelector' => 'applications/differential/field/selector/base', 'DifferentialFieldSpecification' => 'applications/differential/field/specification/base', 'DifferentialFieldSpecificationIncompleteException' => 'applications/differential/field/exception/incomplete', diff --git a/src/applications/conduit/method/differential/parsecommitmessage/ConduitAPI_differential_parsecommitmessage_Method.php b/src/applications/conduit/method/differential/parsecommitmessage/ConduitAPI_differential_parsecommitmessage_Method.php index b3feedabbb..924449d6b5 100644 --- a/src/applications/conduit/method/differential/parsecommitmessage/ConduitAPI_differential_parsecommitmessage_Method.php +++ b/src/applications/conduit/method/differential/parsecommitmessage/ConduitAPI_differential_parsecommitmessage_Method.php @@ -44,29 +44,132 @@ class ConduitAPI_differential_parsecommitmessage_Method protected function execute(ConduitAPIRequest $request) { $corpus = $request->getValue('corpus'); - try { - $message = DifferentialCommitMessage::newFromRawCorpus($corpus); - } catch (DifferentialCommitMessageParserException $ex) { - return array( - 'error' => $ex->getMessage(), - ); + $aux_fields = DifferentialFieldSelector::newSelector() + ->getFieldSpecifications(); + + foreach ($aux_fields as $key => $aux_field) { + if (!$aux_field->shouldAppearOnCommitMessage()) { + unset($aux_fields[$key]); + } } + $aux_fields = mpull($aux_fields, null, 'getCommitMessageKey'); + + // Build a map from labels (like "Test Plan") to field keys + // (like "testPlan"). + $label_map = $this->buildLabelMap($aux_fields); + $field_map = $this->parseCommitMessage($corpus, $label_map); + + $fields = array(); + $errors = array(); + foreach ($field_map as $field_key => $field_value) { + $field = $aux_fields[$field_key]; + try { + $fields[$field_key] = $field->parseValueFromCommitMessage($field_value); + } catch (DifferentialFieldParseException $ex) { + $field_label = $field->renderLabelForCommitMessage(); + $errors[] = "Error parsing field '{$field_label}': ".$ex->getMessage(); + } + } + + // TODO: This is for backcompat only, remove once Arcanist gets updated. + $error = head($errors); + return array( - 'error' => null, - 'fields' => array( - 'title' => $message->getTitle(), - 'summary' => $message->getSummary(), - 'testPlan' => $message->getTestPlan(), - 'blameRevision' => $message->getBlameRevision(), - 'revertPlan' => $message->getRevertPlan(), - 'reviewerPHIDs' => $message->getReviewerPHIDs(), - 'reviewedByPHIDs' => $message->getReviewedByPHIDs(), - 'ccPHIDs' => $message->getCCPHIDs(), - 'revisionID' => $message->getRevisionID(), - 'gitSVNID' => $message->getGitSVNID(), - 'tasks' => $message->getTasks(), - ), + 'error' => $error, + 'errors' => $errors, + 'fields' => $fields, ); } + + private function buildLabelMap(array $aux_fields) { + $label_map = array(); + foreach ($aux_fields as $key => $aux_field) { + $labels = $aux_field->getSupportedCommitMessageLabels(); + foreach ($labels as $label) { + $normal_label = strtolower($label); + if (!empty($label_map[$normal_label])) { + $previous = $label_map[$normal_label]; + throw new Exception( + "Field label '{$label}' is parsed by two fields: '{$key}' and ". + "'{$previous}'. Each label must be parsed by only one field."); + } + $label_map[$normal_label] = $key; + } + } + return $label_map; + } + + private function buildLabelRegexp(array $label_map) { + $field_labels = array_keys($label_map); + foreach ($field_labels as $key => $label) { + $field_labels[$key] = preg_quote($label, '/'); + } + $field_labels = implode('|', $field_labels); + + $field_pattern = '/^(?P'.$field_labels.'):(?P.*)$/i'; + + return $field_pattern; + } + + private function parseCommitMessage($corpus, array $label_map) { + $label_regexp = $this->buildLabelRegexp($label_map); + + // Note, deliberately not populating $seen with 'title' because it is + // optional to include the 'Title:' label. We're doing a little special + // casing to consume the first line as the title regardless of whether you + // label it as such or not. + $field = 'title'; + + $seen = array(); + $lines = explode("\n", trim($corpus)); + $field_map = array(); + foreach ($lines as $key => $line) { + $match = null; + if (preg_match($label_regexp, $line, $match)) { + $lines[$key] = trim($match['text']); + $field = $label_map[strtolower($match['field'])]; + if (!empty($seen[$field])) { + throw new Exception( + "Field '{$field}' occurs twice in commit message!"); + } + $seen[$field] = true; + } + $field_map[$key] = $field; + } + + $fields = array(); + foreach ($lines as $key => $line) { + $fields[$field_map[$key]][] = $line; + } + + // This is a piece of special-cased magic which allows you to omit the + // field labels for "title" and "summary". If the user enters a large block + // of text at the beginning of the commit message with an empty line in it, + // treat everything before the blank line as "title" and everything after + // as "summary". + if (isset($fields['title']) && empty($fields['summary'])) { + $lines = $fields['title']; + for ($ii = 0; $ii < count($lines); $ii++) { + if (strlen(trim($lines[$ii])) == 0) { + break; + } + } + if ($ii != count($lines)) { + $fields['title'] = array_slice($lines, 0, $ii); + $fields['summary'] = array_slice($lines, $ii); + } + } + + // Implode all the lines back into chunks of text. + foreach ($fields as $name => $lines) { + $data = rtrim(implode("\n", $lines)); + $data = ltrim($data, "\n"); + $fields[$name] = $data; + } + + return $fields; + } + + } diff --git a/src/applications/conduit/method/differential/parsecommitmessage/__init__.php b/src/applications/conduit/method/differential/parsecommitmessage/__init__.php index 605cd16f25..ea78e07134 100644 --- a/src/applications/conduit/method/differential/parsecommitmessage/__init__.php +++ b/src/applications/conduit/method/differential/parsecommitmessage/__init__.php @@ -7,7 +7,9 @@ phutil_require_module('phabricator', 'applications/conduit/method/base'); -phutil_require_module('phabricator', 'applications/differential/parser/commitmessage'); +phutil_require_module('phabricator', 'applications/differential/field/selector/base'); + +phutil_require_module('phutil', 'utils'); phutil_require_source('ConduitAPI_differential_parsecommitmessage_Method.php'); diff --git a/src/applications/differential/parser/commitmessage/exception/DifferentialCommitMessageParserException.php b/src/applications/differential/field/exception/parse/DifferentialFieldParseException.php similarity index 90% rename from src/applications/differential/parser/commitmessage/exception/DifferentialCommitMessageParserException.php rename to src/applications/differential/field/exception/parse/DifferentialFieldParseException.php index e82636a503..49610947e4 100644 --- a/src/applications/differential/parser/commitmessage/exception/DifferentialCommitMessageParserException.php +++ b/src/applications/differential/field/exception/parse/DifferentialFieldParseException.php @@ -16,6 +16,6 @@ * limitations under the License. */ -class DifferentialCommitMessageParserException extends Exception { +final class DifferentialFieldParseException extends Exception { } diff --git a/src/applications/differential/parser/commitmessage/exception/__init__.php b/src/applications/differential/field/exception/parse/__init__.php similarity index 59% rename from src/applications/differential/parser/commitmessage/exception/__init__.php rename to src/applications/differential/field/exception/parse/__init__.php index 1bba8b66ab..3a925359d7 100644 --- a/src/applications/differential/parser/commitmessage/exception/__init__.php +++ b/src/applications/differential/field/exception/parse/__init__.php @@ -7,4 +7,4 @@ -phutil_require_source('DifferentialCommitMessageParserException.php'); +phutil_require_source('DifferentialFieldParseException.php'); diff --git a/src/applications/differential/field/specification/base/DifferentialFieldSpecification.php b/src/applications/differential/field/specification/base/DifferentialFieldSpecification.php index bf2acb8faf..7b0b1be529 100644 --- a/src/applications/differential/field/specification/base/DifferentialFieldSpecification.php +++ b/src/applications/differential/field/specification/base/DifferentialFieldSpecification.php @@ -337,6 +337,9 @@ abstract class DifferentialFieldSpecification { * 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}. * @@ -413,6 +416,48 @@ abstract class DifferentialFieldSpecification { 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 )-------------------------------------------- */ @@ -482,7 +527,6 @@ abstract class DifferentialFieldSpecification { return $this->getRequiredHandlePHIDs(); } - /** * Specify which diff properties this field needs to load. * @@ -493,6 +537,82 @@ abstract class DifferentialFieldSpecification { 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 )---------------------------------------------------- */ diff --git a/src/applications/differential/field/specification/base/__init__.php b/src/applications/differential/field/specification/base/__init__.php index 6dcca1a4da..db188c4711 100644 --- a/src/applications/differential/field/specification/base/__init__.php +++ b/src/applications/differential/field/specification/base/__init__.php @@ -8,6 +8,11 @@ phutil_require_module('phabricator', 'applications/differential/field/exception/incomplete'); phutil_require_module('phabricator', 'applications/differential/field/exception/notavailable'); +phutil_require_module('phabricator', 'applications/differential/field/exception/parse'); +phutil_require_module('phabricator', 'applications/metamta/storage/mailinglist'); +phutil_require_module('phabricator', 'applications/people/storage/user'); + +phutil_require_module('phutil', 'utils'); phutil_require_source('DifferentialFieldSpecification.php'); diff --git a/src/applications/differential/field/specification/blamerev/DifferentialBlameRevisionFieldSpecification.php b/src/applications/differential/field/specification/blamerev/DifferentialBlameRevisionFieldSpecification.php index 5fdcea13be..d425476532 100644 --- a/src/applications/differential/field/specification/blamerev/DifferentialBlameRevisionFieldSpecification.php +++ b/src/applications/differential/field/specification/blamerev/DifferentialBlameRevisionFieldSpecification.php @@ -103,4 +103,15 @@ final class DifferentialBlameRevisionFieldSpecification return $this->value; } + public function getSupportedCommitMessageLabels() { + return array( + 'Blame Revision', + 'Blame Rev', + ); + } + + public function parseValueFromCommitMessage($value) { + return $value; + } + } diff --git a/src/applications/differential/field/specification/ccs/DifferentialCCsFieldSpecification.php b/src/applications/differential/field/specification/ccs/DifferentialCCsFieldSpecification.php index 8799bf40fb..55a62bacb3 100644 --- a/src/applications/differential/field/specification/ccs/DifferentialCCsFieldSpecification.php +++ b/src/applications/differential/field/specification/ccs/DifferentialCCsFieldSpecification.php @@ -118,4 +118,15 @@ final class DifferentialCCsFieldSpecification return implode(', ', $names); } + public function getSupportedCommitMessageLabels() { + return array( + 'CC', + 'CCs', + ); + } + + public function parseValueFromCommitMessage($value) { + return $this->parseCommitMessageMailableList($value); + } + } diff --git a/src/applications/differential/field/specification/gitsvnid/DifferentialGitSVNIDFieldSpecification.php b/src/applications/differential/field/specification/gitsvnid/DifferentialGitSVNIDFieldSpecification.php index 1fcff06fbf..0b684aeab8 100644 --- a/src/applications/differential/field/specification/gitsvnid/DifferentialGitSVNIDFieldSpecification.php +++ b/src/applications/differential/field/specification/gitsvnid/DifferentialGitSVNIDFieldSpecification.php @@ -46,4 +46,8 @@ final class DifferentialGitSVNIDFieldSpecification return null; } + public function parseValueFromCommitMessage($value) { + return $value; + } + } diff --git a/src/applications/differential/field/specification/revertplan/DifferentialRevertPlanFieldSpecification.php b/src/applications/differential/field/specification/revertplan/DifferentialRevertPlanFieldSpecification.php index 790758e4a0..3f9cda19ac 100644 --- a/src/applications/differential/field/specification/revertplan/DifferentialRevertPlanFieldSpecification.php +++ b/src/applications/differential/field/specification/revertplan/DifferentialRevertPlanFieldSpecification.php @@ -99,8 +99,20 @@ final class DifferentialRevertPlanFieldSpecification return 'Revert Plan'; } + public function renderValueForCommitMessage($is_edit) { return $this->value; } + public function getSupportedCommitMessageLabels() { + return array( + 'Revert Plan', + 'Revert', + ); + } + + public function parseValueFromCommitMessage($value) { + return $value; + } + } diff --git a/src/applications/differential/field/specification/reviewedby/DifferentialReviewedByFieldSpecification.php b/src/applications/differential/field/specification/reviewedby/DifferentialReviewedByFieldSpecification.php index 78158e4d57..59feb8013c 100644 --- a/src/applications/differential/field/specification/reviewedby/DifferentialReviewedByFieldSpecification.php +++ b/src/applications/differential/field/specification/reviewedby/DifferentialReviewedByFieldSpecification.php @@ -89,4 +89,8 @@ final class DifferentialReviewedByFieldSpecification return implode(', ', $names); } + public function parseValueFromCommitMessage($value) { + return $this->parseCommitMessageUserList($value); + } + } diff --git a/src/applications/differential/field/specification/reviewers/DifferentialReviewersFieldSpecification.php b/src/applications/differential/field/specification/reviewers/DifferentialReviewersFieldSpecification.php index f609494eb5..a40f961e72 100644 --- a/src/applications/differential/field/specification/reviewers/DifferentialReviewersFieldSpecification.php +++ b/src/applications/differential/field/specification/reviewers/DifferentialReviewersFieldSpecification.php @@ -129,4 +129,15 @@ final class DifferentialReviewersFieldSpecification return implode(', ', $names); } + public function getSupportedCommitMessageLabels() { + return array( + 'Reviewer', + 'Reviewers', + ); + } + + public function parseValueFromCommitMessage($value) { + return $this->parseCommitMessageUserList($value); + } + } diff --git a/src/applications/differential/field/specification/revisionid/DifferentialRevisionIDFieldSpecification.php b/src/applications/differential/field/specification/revisionid/DifferentialRevisionIDFieldSpecification.php index 02eade8f06..190547c942 100644 --- a/src/applications/differential/field/specification/revisionid/DifferentialRevisionIDFieldSpecification.php +++ b/src/applications/differential/field/specification/revisionid/DifferentialRevisionIDFieldSpecification.php @@ -46,5 +46,8 @@ final class DifferentialRevisionIDFieldSpecification return $this->id; } + public function parseValueFromCommitMessage($value) { + return $value; + } } diff --git a/src/applications/differential/field/specification/summary/DifferentialSummaryFieldSpecification.php b/src/applications/differential/field/specification/summary/DifferentialSummaryFieldSpecification.php index a7e5ad50ed..b3afbe7dfb 100644 --- a/src/applications/differential/field/specification/summary/DifferentialSummaryFieldSpecification.php +++ b/src/applications/differential/field/specification/summary/DifferentialSummaryFieldSpecification.php @@ -70,4 +70,8 @@ final class DifferentialSummaryFieldSpecification return $this->summary; } + public function parseValueFromCommitMessage($value) { + return $value; + } + } diff --git a/src/applications/differential/field/specification/testplan/DifferentialTestPlanFieldSpecification.php b/src/applications/differential/field/specification/testplan/DifferentialTestPlanFieldSpecification.php index 14b46a7a9c..c069ee0fdd 100644 --- a/src/applications/differential/field/specification/testplan/DifferentialTestPlanFieldSpecification.php +++ b/src/applications/differential/field/specification/testplan/DifferentialTestPlanFieldSpecification.php @@ -81,5 +81,8 @@ final class DifferentialTestPlanFieldSpecification return $this->plan; } + public function parseValueFromCommitMessage($value) { + return $value; + } } diff --git a/src/applications/differential/field/specification/title/DifferentialTitleFieldSpecification.php b/src/applications/differential/field/specification/title/DifferentialTitleFieldSpecification.php index b4ba293935..9a55539144 100644 --- a/src/applications/differential/field/specification/title/DifferentialTitleFieldSpecification.php +++ b/src/applications/differential/field/specification/title/DifferentialTitleFieldSpecification.php @@ -82,4 +82,8 @@ final class DifferentialTitleFieldSpecification return $this->title; } + public function parseValueFromCommitMessage($value) { + return preg_replace('/\s*\n\s*/', ' ', $value); + } + } diff --git a/src/applications/differential/parser/commitmessage/DifferentialCommitMessage.php b/src/applications/differential/parser/commitmessage/DifferentialCommitMessage.php deleted file mode 100644 index f0c1dbdc6e..0000000000 --- a/src/applications/differential/parser/commitmessage/DifferentialCommitMessage.php +++ /dev/null @@ -1,445 +0,0 @@ -reviewerPHIDs; - } - - public function getReviewedByPHIDs() { - return $this->reviewedByPHIDs; - } - - public function getCCPHIDs() { - return $this->ccPHIDs; - } - - public function setTitle($title) { - $this->title = $title; - return $this; - } - - public function getTitle() { - return $this->title; - } - - public function setRevisionID($revision_id) { - $this->revisionID = $revision_id; - return $this; - } - - public function getRevisionID() { - return $this->revisionID; - } - - public function setSummary($summary) { - $this->summary = $summary; - return $this; - } - - public function getSummary() { - return $this->summary; - } - - public function setTestPlan($test_plan) { - $this->testPlan = $test_plan; - return $this; - } - - public function getTestPlan() { - return $this->testPlan; - } - - public function setBlameRevision($blame_revision) { - $this->blameRevision = $blame_revision; - return $this; - } - - public function getBlameRevision() { - return $this->blameRevision; - } - - public function setRevertPlan($revert_plan) { - $this->revertPlan = $revert_plan; - return $this; - } - - public function getRevertPlan() { - return $this->revertPlan; - } - - public function setReviewerNames($reviewer_names) { - $this->reviewerNames = $reviewer_names; - return $this; - } - - public function getReviewerNames() { - return $this->reviewerNames; - } - - public function setCCNames($cc_names) { - $this->ccNames = $cc_names; - return $this; - } - - public function getCCNames() { - return $this->ccNames; - } - - public function setReviewedByNames($reviewed_by_names) { - $this->reviewedByNames = $reviewed_by_names; - return $this; - } - - public function getReviewedByNames() { - return $this->reviewedByNames; - } - - public function setGitSVNID($git_svn_id) { - $this->gitSVNID = $git_svn_id; - return $this; - } - - public function getGitSVNID() { - return $this->gitSVNID; - } - - public function setReviewerPHIDs(array $phids) { - $this->reviewerPHIDs = $phids; - return $this; - } - - public function setReviewedByPHIDs(array $phids) { - $this->reviewedByPHIDs = $phids; - return $this; - } - - public function setCCPHIDs(array $phids) { - $this->ccPHIDs = $phids; - return $this; - } - - public function setTasks(array $tasks) { - $this->tasks = $tasks; - return $this; - } - - public function getTasks() { - return $this->tasks; - } - - public static function newFromRawCorpus($raw_corpus) { - $message = new DifferentialCommitMessage(); - $message->setRawCorpus($raw_corpus); - - $fields = $message->parseFields($raw_corpus); - - foreach ($fields as $field => $data) { - switch ($field) { - case 'Title': - $message->setTitle($data); - break; - case 'Differential Revision': - $message->setRevisionID($data); - break; - case 'Summary': - $message->setSummary($data); - break; - case 'Test Plan': - $message->setTestPlan($data); - break; - case 'Blame Revision': - $message->setBlameRevision($data); - break; - case 'Revert Plan'; - $message->setRevertPlan($data); - break; - case 'Reviewers': - $message->setReviewerNames($data); - break; - case 'Reviewed By': - $message->setReviewedByNames($data); - break; - case 'CC': - $message->setCCNames($data); - break; - case 'git-svn-id': - $message->setGitSVNID($data); - break; - case 'Commenters': - // Just drop this. - break; - case 'Tasks': - $message->setTasks($data); - break; - default: - throw new Exception("Unrecognized field '{$field}'."); - } - } - - $need_users = array_merge( - $message->getReviewerNames(), - $message->getReviewedByNames(), - $message->getCCNames()); - $need_mail = $message->getCCNames(); - - if ($need_users) { - $users = id(new PhabricatorUser())->loadAllWhere( - '(username IN (%Ls)) OR (email IN (%Ls))', - $need_users, - $need_users); - $users = mpull($users, 'getPHID', 'getUsername') + - mpull($users, 'getPHID', 'getEmail'); - } else { - $users = array(); - } - - if ($need_mail) { - $mail = id(new PhabricatorMetaMTAMailingList())->loadAllWhere( - '(email in (%Ls)) OR (name IN (%Ls))', - $need_mail, - $need_mail); - $mail = mpull($mail, 'getPHID', 'getName') + - mpull($mail, 'getPHID', 'getEmail'); - } else { - $mail = array(); - } - - $reviewer_phids = array(); - foreach ($message->getReviewerNames() as $name) { - $phid = idx($users, $name); - if (!$phid) { - throw new DifferentialCommitMessageParserException( - "Commit message references nonexistent 'Reviewer' value '".$name."'"); - } - $reviewer_phids[] = $phid; - } - $message->setReviewerPHIDs($reviewer_phids); - - $reviewed_by_phids = array(); - foreach ($message->getReviewedByNames() as $name) { - $phid = idx($users, $name); - if (!$phid) { - throw new DifferentialCommitMessageParserException( - "Commit message references nonexistent 'Reviewed by' value '". - $name."'"); - } - $reviewed_by_phids[] = $phid; - } - $message->setReviewedByPHIDs($reviewed_by_phids); - - $cc_phids = array(); - foreach ($message->getCCNames() as $name) { - $phid = idx($users, $name); - if (!$phid) { - $phid = idx($mail, $name); - } - if (!$phid) { - throw new DifferentialCommitMessageParserException( - "Commit message references nonexistent 'CC' value '".$name."'"); - } - $cc_phids[] = $phid; - } - $message->setCCPHIDs($cc_phids); - - return $message; - } - - public function setRawCorpus($raw_corpus) { - $this->rawCorpus = $raw_corpus; - return $this; - } - - public function getRawCorpus() { - return $this->rawCorpus; - } - - protected function parseFields($message) { - - $field_spec = array( - 'Differential Revision' => 'Differential Revision', - 'Title' => 'Title', - 'Summary' => 'Summary', - 'Test Plan' => 'Test Plan', - 'Blame Rev' => 'Blame Revision', - 'Blame Revision' => 'Blame Revision', - 'Reviewed By' => 'Reviewed By', - 'Reviewers' => 'Reviewers', - 'CC' => 'CC', - 'Revert' => 'Revert Plan', - 'Revert Plan' => 'Revert Plan', - 'Task ID' => 'Tasks', - 'Task IDs' => 'Tasks', - 'Tasks' => 'Tasks', - 'git-svn-id' => 'git-svn-id', - - // This appears only in "arc amend"-ed messages, just discard it. - 'Commenters' => 'Commenters', - ); - - $field_names = array_keys($field_spec); - foreach ($field_names as $key => $name) { - $field_names[$key] = preg_quote($name, '/'); - } - $field_names = implode('|', $field_names); - $field_pattern = '/^(?P'.$field_names.'):(?P.*)$/i'; - - foreach ($field_spec as $key => $value) { - $field_spec[strtolower($key)] = $value; - } - - $message = trim($message); - $lines = explode("\n", $message); - $this->rawInput = $lines; - - if (!$message) { - $this->fail( - null, - "Your commit message is empty."); - } - - $field = 'Title'; - // Note, deliberately not populating $seen with 'Title' because it is - // optional to include the 'Title:' header. - $seen = array(); - $field_map = array(); - foreach ($lines as $key => $line) { - $match = null; - if (preg_match($field_pattern, $line, $match)) { - $lines[$key] = trim($match['text']); - $field = $field_spec[strtolower($match['field'])]; - if (!empty($seen[$field])) { - $this->fail( - $key, - "Field '{$field}' occurs twice in commit message."); - } - $seen[$field] = true; - } - - $field_map[$key] = $field; - } - - $fields = array(); - foreach ($lines as $key => $line) { - $fields[$field_map[$key]][] = $line; - } - - foreach ($fields as $name => $lines) { - if ($name == 'Title') { - // If the user enters a title and then a blank line without a summary, - // treat the first line as the title and the rest as the summary. - if (!isset($fields['Summary'])) { - $ii = 0; - for ($ii = 0; $ii < count($lines); $ii++) { - if (strlen(trim($lines[$ii])) == 0) { - break; - } - } - if ($ii != count($lines)) { - $fields['Title'] = array_slice($lines, 0, $ii); - $fields['Summary'] = array_slice($lines, $ii); - } - } - } - } - - foreach ($fields as $name => $lines) { - $data = rtrim(implode("\n", $lines)); - $data = ltrim($data, "\n"); - switch ($name) { - case 'Title': - $data = preg_replace('/\s*\n\s*/', ' ', $data); - break; - case 'Tasks': - list($pre_comment) = split(' -- ', $data); - $data = array_filter(preg_split('/[^\d]+/', $pre_comment)); - foreach ($data as $k => $v) { - $data[$k] = (int)$v; - } - $data = array_unique($data); - break; - case 'Blame Revision': - case 'Differential Revision': - $data = (int)preg_replace('/[^\d]/', '', $data); - break; - case 'CC': - case 'Reviewers': - case 'Reviewed By': - $data = array_filter(preg_split('/[\s,]+/', $data)); - break; - } - if (is_array($data)) { - $data = array_values($data); - } - if ($data) { - $fields[$name] = $data; - } else { - unset($fields[$name]); - } - } - - return $fields; - } - - protected function fail($line, $reason) { - if ($line !== null) { - $lines = $this->rawInput; - $min = max(0, $line - 3); - $max = min(count($lines) - 1, $line + 3); - $reason .= "\n\n"; - $len = strlen($max); - for ($ii = $min; $ii <= $max; $ii++) { - $reason .= sprintf( - "%8.8s % {$len}d %s\n", - $ii == $line ? '>>>' : '', - $ii + 1, - $lines[$ii]); - } - } - throw new DifferentialCommitMessageParserException($reason); - } - -} diff --git a/src/applications/differential/parser/commitmessage/__init__.php b/src/applications/differential/parser/commitmessage/__init__.php deleted file mode 100644 index 6727ce72b8..0000000000 --- a/src/applications/differential/parser/commitmessage/__init__.php +++ /dev/null @@ -1,16 +0,0 @@ -