From 0be3db03eeb506ded4218a6cc915bdb68bd88591 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 18 Aug 2011 12:08:18 -0700 Subject: [PATCH] Drive Differential commit message parsing through extensible fields Summary: I think this is the last major step -- use the fields to parse commit messages, not a hard-coded list of stuff. This adds two primary methods to fields, one to get all the labels they'll parse (so we can do "CC" and "CCs" and treat them as the same field) and one to parse the string into a canonical representation (e.g., lookup reviewers and such). You'll need to impelement the one block of task-specific stuff I removed in Facebook's task field: 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; Otherwise I think this is clean. Test Plan: - Called the conduit method with various commit messages, parsed fields/errors seemed correct. - "arc diff"'d this diff onto localhost, then updated it. - "arc amend"'d this diff. Reviewers: jungejason, tuomaspelkonen, aran Reviewed By: jungejason CC: aran, jungejason, epriestley Differential Revision: 829 --- src/__phutil_library_map__.php | 3 +- ...differential_parsecommitmessage_Method.php | 143 +++++- .../parsecommitmessage/__init__.php | 4 +- .../DifferentialFieldParseException.php} | 2 +- .../exception/parse}/__init__.php | 2 +- .../base/DifferentialFieldSpecification.php | 122 ++++- .../field/specification/base/__init__.php | 5 + ...rentialBlameRevisionFieldSpecification.php | 11 + .../ccs/DifferentialCCsFieldSpecification.php | 11 + ...DifferentialGitSVNIDFieldSpecification.php | 4 + ...fferentialRevertPlanFieldSpecification.php | 12 + ...fferentialReviewedByFieldSpecification.php | 4 + ...ifferentialReviewersFieldSpecification.php | 11 + ...fferentialRevisionIDFieldSpecification.php | 3 + .../DifferentialSummaryFieldSpecification.php | 4 + ...DifferentialTestPlanFieldSpecification.php | 3 + .../DifferentialTitleFieldSpecification.php | 4 + .../DifferentialCommitMessage.php | 445 ------------------ .../parser/commitmessage/__init__.php | 16 - 19 files changed, 322 insertions(+), 487 deletions(-) rename src/applications/differential/{parser/commitmessage/exception/DifferentialCommitMessageParserException.php => field/exception/parse/DifferentialFieldParseException.php} (90%) rename src/applications/differential/{parser/commitmessage/exception => field/exception/parse}/__init__.php (59%) delete mode 100644 src/applications/differential/parser/commitmessage/DifferentialCommitMessage.php delete mode 100644 src/applications/differential/parser/commitmessage/__init__.php 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 @@ -