diff --git a/src/applications/differential/conduit/DifferentialParseCommitMessageConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialParseCommitMessageConduitAPIMethod.php index dc29ff07a9..1e82dc935b 100644 --- a/src/applications/differential/conduit/DifferentialParseCommitMessageConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialParseCommitMessageConduitAPIMethod.php @@ -1,105 +1,57 @@ 'required string', 'partial' => 'optional bool', ); } protected function defineReturnType() { return 'nonempty dict'; } protected function execute(ConduitAPIRequest $request) { - $viewer = $request->getUser(); - $corpus = $request->getValue('corpus'); - $is_partial = $request->getValue('partial'); - - $field_list = PhabricatorCustomField::getObjectFields( - new DifferentialRevision(), - DifferentialCustomField::ROLE_COMMITMESSAGE); - $field_list->setViewer($viewer); - $field_map = mpull($field_list->getFields(), null, 'getFieldKeyForConduit'); - - $corpus_map = $this->parseCommitMessage($corpus); - - $values = array(); - foreach ($corpus_map as $field_key => $text_value) { - $field = idx($field_map, $field_key); + $viewer = $this->getViewer(); - if (!$field) { - throw new Exception( - pht( - 'Parser emitted text value for field key "%s", but no such '. - 'field exists.', - $field_key)); - } + $parser = DifferentialCommitMessageParser::newStandardParser($viewer); - try { - $values[$field_key] = $field->parseValueFromCommitMessage($text_value); - } catch (DifferentialFieldParseException $ex) { - $this->errors[] = pht( - 'Error parsing field "%s": %s', - $field->renderCommitMessageLabel(), - $ex->getMessage()); - } + $is_partial = $request->getValue('partial'); + if ($is_partial) { + $parser->setRaiseMissingFieldErrors(false); } - if (!$is_partial) { - foreach ($field_map as $key => $field) { - try { - $field->validateCommitMessageValue(idx($values, $key)); - } catch (DifferentialFieldValidationException $ex) { - $this->errors[] = pht( - 'Invalid or missing field "%s": %s', - $field->renderCommitMessageLabel(), - $ex->getMessage()); - } - } - } + $corpus = $request->getValue('corpus'); + $field_map = $parser->parseFields($corpus); + + $errors = $parser->getErrors(); // grab some extra information about the Differential Revision: field... $revision_id_field = new DifferentialRevisionIDField(); $revision_id_value = idx( - $corpus_map, + $field_map, $revision_id_field->getFieldKeyForConduit()); $revision_id_valid_domain = PhabricatorEnv::getProductionURI(''); return array( - 'errors' => $this->errors, - 'fields' => $values, + 'errors' => $errors, + 'fields' => $field_map, 'revisionIDFieldInfo' => array( 'value' => $revision_id_value, 'validDomain' => $revision_id_valid_domain, ), ); } - private function parseCommitMessage($corpus) { - $viewer = $this->getViewer(); - $parser = DifferentialCommitMessageParser::newStandardParser($viewer); - $result = $parser->parseCorpus($corpus); - - $this->errors = array(); - foreach ($parser->getErrors() as $error) { - $this->errors[] = $error; - } - - return $result; - } - } diff --git a/src/applications/differential/parser/DifferentialCommitMessageParser.php b/src/applications/differential/parser/DifferentialCommitMessageParser.php index ff76b30e51..8cd508ef5f 100644 --- a/src/applications/differential/parser/DifferentialCommitMessageParser.php +++ b/src/applications/differential/parser/DifferentialCommitMessageParser.php @@ -1,255 +1,348 @@ setLabelMap($label_map) * ->setTitleKey($key_title) * ->setSummaryKey($key_summary); * * $fields = $parser->parseCorpus($corpus); * $errors = $parser->getErrors(); * * This is used by Differential to parse messages entered from the command line. * * @task config Configuring the Parser * @task parse Parsing Messages * @task support Support Methods * @task internal Internals */ final class DifferentialCommitMessageParser extends Phobject { + private $viewer; private $labelMap; private $titleKey; private $summaryKey; private $errors; - + private $raiseMissingFieldErrors = true; public static function newStandardParser(PhabricatorUser $viewer) { $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; } } return id(new self()) + ->setViewer($viewer) ->setLabelMap($label_map) ->setTitleKey($key_title) ->setSummaryKey($key_summary); } /* -( Configuring the Parser )--------------------------------------------- */ + /** + * @task config + */ + public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; + return $this; + } + + + /** + * @task config + */ + public function getViewer() { + return $this->viewer; + } + + + /** + * @task config + */ + public function setRaiseMissingFieldErrors($raise) { + $this->raiseMissingFieldErrors = $raise; + return $this; + } + + + /** + * @task config + */ + public function getRaiseMissingFieldErrors() { + return $this->raiseMissingFieldErrors; + } + + /** * @task config */ public function setLabelMap(array $label_map) { $this->labelMap = $label_map; return $this; } /** * @task config */ public function setTitleKey($title_key) { $this->titleKey = $title_key; return $this; } /** * @task config */ public function setSummaryKey($summary_key) { $this->summaryKey = $summary_key; return $this; } /* -( Parsing Messages )--------------------------------------------------- */ /** * @task parse */ public function parseCorpus($corpus) { $this->errors = array(); $label_map = $this->labelMap; $key_title = $this->titleKey; $key_summary = $this->summaryKey; if (!$key_title || !$key_summary || ($label_map === null)) { throw new Exception( pht( 'Expected %s, %s and %s to be set before parsing a corpus.', 'labelMap', 'summaryKey', 'titleKey')); } $label_regexp = $this->buildLabelRegexp($label_map); // NOTE: We're special casing things here to make the "Title:" label // optional in the message. $field = $key_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[self::normalizeFieldLabel($match['field'])]; if (!empty($seen[$field])) { $this->errors[] = pht( 'Field "%s" occurs twice in commit message!', $field); } $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[$key_title]) && empty($fields[$key_summary])) { $lines = $fields[$key_title]; for ($ii = 0; $ii < count($lines); $ii++) { if (strlen(trim($lines[$ii])) == 0) { break; } } if ($ii != count($lines)) { $fields[$key_title] = array_slice($lines, 0, $ii); $summary = array_slice($lines, $ii); if (strlen(trim(implode("\n", $summary)))) { $fields[$key_summary] = $summary; } } } // 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; } // This is another piece of special-cased magic which allows you to // enter a ridiculously long title, or just type a big block of stream // of consciousness text, and have some sort of reasonable result conjured // from it. if (isset($fields[$key_title])) { $terminal = '...'; $title = $fields[$key_title]; $short = id(new PhutilUTF8StringTruncator()) ->setMaximumBytes(250) ->setTerminator($terminal) ->truncateString($title); if ($short != $title) { // If we shortened the title, split the rest into the summary, so // we end up with a title like: // // Title title tile title title... // // ...and a summary like: // // ...title title title. // // Summary summary summary summary. $summary = idx($fields, $key_summary, ''); $offset = strlen($short) - strlen($terminal); $remainder = ltrim(substr($fields[$key_title], $offset)); $summary = '...'.$remainder."\n\n".$summary; $summary = rtrim($summary, "\n"); $fields[$key_title] = $short; $fields[$key_summary] = $summary; } } return $fields; } + /** + * @task parse + */ + public function parseFields($corpus) { + $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'); + + $result_map = array(); + foreach ($text_map as $field_key => $text_value) { + $field = idx($field_map, $field_key); + if (!$field) { + // This is a strict error, since we only parse fields which we have + // been told are valid. The caller probably handed us an invalid label + // map. + throw new Exception( + pht( + 'Parser emitted a field with key "%s", but no corresponding '. + 'field definition exists.', + $field_key)); + } + + try { + $result = $field->parseValueFromCommitMessage($text_value); + $result_map[$field_key] = $result; + } catch (DifferentialFieldParseException $ex) { + $this->errors[] = pht( + 'Error parsing field "%s": %s', + $field->renderCommitMessageLabel(), + $ex->getMessage()); + } + } + + if ($this->getRaiseMissingFieldErrors()) { + foreach ($field_map as $key => $field) { + try { + $field->validateCommitMessageValue(idx($result_map, $key)); + } catch (DifferentialFieldValidationException $ex) { + $this->errors[] = pht( + 'Invalid or missing field "%s": %s', + $field->renderCommitMessageLabel(), + $ex->getMessage()); + } + } + } + + return $result_map; + } + + /** * @task parse */ public function getErrors() { return $this->errors; } /* -( Support Methods )---------------------------------------------------- */ /** * @task support */ public static function normalizeFieldLabel($label) { return phutil_utf8_strtolower($label); } /* -( Internals )---------------------------------------------------------- */ /** * @task internal */ 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; } }