From 787eb778b1c0fc640d22459686a78b826880d3b3 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 6 Feb 2011 13:42:00 -0800 Subject: [PATCH] differential.parsecommitmessage Test plan: meta --- resources/git/commit-template.txt | 11 + src/__phutil_library_map__.php | 4 + ...differential_parsecommitmessage_Method.php | 68 +++ .../parsecommitmessage/__init__.php | 13 + .../DifferentialCommitMessage.php | 432 ++++++++++++++++++ .../parser/commitmessage/__init__.php | 16 + ...fferentialCommitMessageParserException.php | 21 + .../commitmessage/exception/__init__.php | 10 + 8 files changed, 575 insertions(+) create mode 100644 resources/git/commit-template.txt create mode 100644 src/applications/conduit/method/differential/parsecommitmessage/ConduitAPI_differential_parsecommitmessage_Method.php create mode 100644 src/applications/conduit/method/differential/parsecommitmessage/__init__.php create mode 100755 src/applications/differential/parser/commitmessage/DifferentialCommitMessage.php create mode 100644 src/applications/differential/parser/commitmessage/__init__.php create mode 100755 src/applications/differential/parser/commitmessage/exception/DifferentialCommitMessageParserException.php create mode 100644 src/applications/differential/parser/commitmessage/exception/__init__.php diff --git a/resources/git/commit-template.txt b/resources/git/commit-template.txt new file mode 100644 index 0000000000..568dc357be --- /dev/null +++ b/resources/git/commit-template.txt @@ -0,0 +1,11 @@ +<> + +Summary: + +Test Plan: + +Reviewers: + +CC: + +############################################################################### diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 6366705e02..b2377277d4 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -65,6 +65,7 @@ phutil_register_library_map(array( 'ConduitAPIRequest' => 'applications/conduit/protocol/request', 'ConduitAPI_conduit_connect_Method' => 'applications/conduit/method/conduit/connect', 'ConduitAPI_differential_creatediff_Method' => 'applications/conduit/method/differential/creatediff', + 'ConduitAPI_differential_parsecommitmessage_Method' => 'applications/conduit/method/differential/parsecommitmessage', 'ConduitAPI_differential_setdiffproperty_Method' => 'applications/conduit/method/differential/setdiffproperty', 'ConduitAPI_file_upload_Method' => 'applications/conduit/method/file/upload', 'ConduitAPI_user_find_Method' => 'applications/conduit/method/user/find', @@ -94,6 +95,8 @@ 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', 'DifferentialController' => 'applications/differential/controller/base', 'DifferentialDAO' => 'applications/differential/storage/base', 'DifferentialDiff' => 'applications/differential/storage/diff', @@ -268,6 +271,7 @@ phutil_register_library_map(array( 'CelerityResourceController' => 'AphrontController', 'ConduitAPI_conduit_connect_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_creatediff_Method' => 'ConduitAPIMethod', + 'ConduitAPI_differential_parsecommitmessage_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_setdiffproperty_Method' => 'ConduitAPIMethod', 'ConduitAPI_file_upload_Method' => 'ConduitAPIMethod', 'ConduitAPI_user_find_Method' => 'ConduitAPIMethod', 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 new file mode 100644 index 0000000000..82bbc78603 --- /dev/null +++ b/src/applications/conduit/method/differential/parsecommitmessage/ConduitAPI_differential_parsecommitmessage_Method.php @@ -0,0 +1,68 @@ + 'required string', + ); + } + + public function defineReturnType() { + return 'nonempty dict'; + } + + public function defineErrorTypes() { + return array( + ); + } + + protected function execute(ConduitAPIRequest $request) { + $corpus = $request->getValue('corpus'); + + try { + $message = DifferentialCommitMessage::newFromRawCorpus($corpus); + } catch (DifferentialCommitMessageParserException $ex) { + return array( + 'error' => $ex->getMessage(), + ); + } + + 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(), + ), + ); + } +} diff --git a/src/applications/conduit/method/differential/parsecommitmessage/__init__.php b/src/applications/conduit/method/differential/parsecommitmessage/__init__.php new file mode 100644 index 0000000000..605cd16f25 --- /dev/null +++ b/src/applications/conduit/method/differential/parsecommitmessage/__init__.php @@ -0,0 +1,13 @@ +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 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; + 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); + } else { + $users = array(); + } + + if ($need_mail) { + $mail = id(new PhabricatorMetaMTAMailingList())->loadAllWhere( + 'email in (%Ls)', + $need_mail); + } 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': ".$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': ".$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': ".$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', + + '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 = '/^(?'.$field_names.'):(?.*)$/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': + $data = preg_replace('/\s+/', ' ', $data); + break; + 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]); + } + } + + if (isset($fields['Reviewers']) && isset($fields['Reviewed By'])) { + $this->fail( + null, + "Commit message contains both 'Reviewers:' and 'Reviewed By:' ". + "fields."); + } + + 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 new file mode 100644 index 0000000000..6727ce72b8 --- /dev/null +++ b/src/applications/differential/parser/commitmessage/__init__.php @@ -0,0 +1,16 @@ +