Page Menu
Home
GnuPG
Search
Configure Global Search
Log In
Files
F36624019
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Size
17 KB
Subscribers
None
View Options
diff --git a/src/applications/differential/conduit/DifferentialParseCommitMessageConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialParseCommitMessageConduitAPIMethod.php
index 107d22d2a..452798331 100644
--- a/src/applications/differential/conduit/DifferentialParseCommitMessageConduitAPIMethod.php
+++ b/src/applications/differential/conduit/DifferentialParseCommitMessageConduitAPIMethod.php
@@ -1,142 +1,105 @@
<?php
final class DifferentialParseCommitMessageConduitAPIMethod
extends DifferentialConduitAPIMethod {
private $errors;
public function getAPIMethodName() {
return 'differential.parsecommitmessage';
}
public function getMethodDescription() {
return pht('Parse commit messages for Differential fields.');
}
protected function defineParamTypes() {
return array(
'corpus' => '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');
- $revision = new DifferentialRevision();
-
$field_list = PhabricatorCustomField::getObjectFields(
- $revision,
+ new DifferentialRevision(),
DifferentialCustomField::ROLE_COMMITMESSAGE);
$field_list->setViewer($viewer);
- $field_map = mpull($field_list->getFields(), null, 'getFieldKeyForConduit');
-
- $this->errors = array();
+ $field_map = mpull($field_list, null, 'getFieldKeyForConduit');
- $label_map = $this->buildLabelMap($field_list);
- $corpus_map = $this->parseCommitMessage($corpus, $label_map);
+ $corpus_map = $this->parseCommitMessage($corpus);
$values = array();
foreach ($corpus_map as $field_key => $text_value) {
$field = idx($field_map, $field_key);
if (!$field) {
throw new Exception(
pht(
'Parser emitted text value for field key "%s", but no such '.
'field exists.',
$field_key));
}
try {
$values[$field_key] = $field->parseValueFromCommitMessage($text_value);
} catch (DifferentialFieldParseException $ex) {
$this->errors[] = pht(
'Error parsing field "%s": %s',
$field->renderCommitMessageLabel(),
$ex->getMessage());
}
}
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());
}
}
}
// grab some extra information about the Differential Revision: field...
$revision_id_field = new DifferentialRevisionIDField();
$revision_id_value = idx(
$corpus_map,
$revision_id_field->getFieldKeyForConduit());
$revision_id_valid_domain = PhabricatorEnv::getProductionURI('');
return array(
'errors' => $this->errors,
'fields' => $values,
'revisionIDFieldInfo' => array(
'value' => $revision_id_value,
'validDomain' => $revision_id_valid_domain,
),
);
}
- private function buildLabelMap(PhabricatorCustomFieldList $field_list) {
- $label_map = array();
-
- foreach ($field_list->getFields() as $key => $field) {
- $labels = $field->getCommitMessageLabels();
- $key = $field->getFieldKeyForConduit();
-
- foreach ($labels as $label) {
- $normal_label = DifferentialCommitMessageParser::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 $label_map;
- }
-
-
- private function parseCommitMessage($corpus, array $label_map) {
- $key_title = id(new DifferentialTitleField())->getFieldKeyForConduit();
- $key_summary = id(new DifferentialSummaryField())->getFieldKeyForConduit();
-
- $parser = id(new DifferentialCommitMessageParser())
- ->setLabelMap($label_map)
- ->setTitleKey($key_title)
- ->setSummaryKey($key_summary);
-
+ 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/customfield/DifferentialCoreCustomField.php b/src/applications/differential/customfield/DifferentialCoreCustomField.php
index 87d18553a..7b6d02276 100644
--- a/src/applications/differential/customfield/DifferentialCoreCustomField.php
+++ b/src/applications/differential/customfield/DifferentialCoreCustomField.php
@@ -1,133 +1,176 @@
<?php
/**
* Base class for Differential fields with storage on the revision object
* itself. This mostly wraps reading/writing field values to and from the
* object.
*/
abstract class DifferentialCoreCustomField
extends DifferentialCustomField {
private $value;
private $fieldError;
+ private $fieldParser;
abstract protected function readValueFromRevision(
DifferentialRevision $revision);
protected function writeValueToRevision(
DifferentialRevision $revision,
$value) {
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
protected function isCoreFieldRequired() {
return false;
}
protected function isCoreFieldValueEmpty($value) {
if (is_array($value)) {
return !$value;
}
return !strlen(trim($value));
}
protected function getCoreFieldRequiredErrorString() {
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
public function validateApplicationTransactions(
PhabricatorApplicationTransactionEditor $editor,
$type,
array $xactions) {
$this->setFieldError(null);
$errors = parent::validateApplicationTransactions(
$editor,
$type,
$xactions);
$transaction = null;
foreach ($xactions as $xaction) {
$value = $xaction->getNewValue();
if ($this->isCoreFieldRequired()) {
if ($this->isCoreFieldValueEmpty($value)) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Required'),
$this->getCoreFieldRequiredErrorString(),
$xaction);
$error->setIsMissingFieldError(true);
$errors[] = $error;
$this->setFieldError(pht('Required'));
+ continue;
+ }
+ }
+
+ if (is_string($value)) {
+ $parser = $this->getFieldParser();
+ $result = $parser->parseCorpus($value);
+
+ unset($result['__title__']);
+ unset($result['__summary__']);
+
+ if ($result) {
+ $error = new PhabricatorApplicationTransactionValidationError(
+ $type,
+ pht('Invalid'),
+ pht(
+ 'The value you have entered in "%s" can not be parsed '.
+ 'unambiguously when rendered in a commit message. Edit the '.
+ 'message so that keywords like "Summary:" and "Test Plan:" do '.
+ 'not appear at the beginning of lines. Parsed keys: %s.',
+ $this->getFieldName(),
+ implode(', ', array_keys($result))),
+ $xaction);
+ $errors[] = $error;
+ $this->setFieldError(pht('Invalid'));
+ continue;
}
}
}
return $errors;
}
+ private function getFieldParser() {
+ if (!$this->fieldParser) {
+ $viewer = $this->getViewer();
+ $parser = DifferentialCommitMessageParser::newStandardParser($viewer);
+
+ // Set custom title and summary keys so we can detect the presence of
+ // "Summary:" in, e.g., a test plan.
+ $parser->setTitleKey('__title__');
+ $parser->setSummaryKey('__summary__');
+
+ $this->fieldParser = $parser;
+ }
+
+ return $this->fieldParser;
+ }
+
public function canDisableField() {
return false;
}
public function shouldAppearInApplicationTransactions() {
return true;
}
public function shouldAppearInEditView() {
return true;
}
public function readValueFromObject(PhabricatorCustomFieldInterface $object) {
if ($this->isCoreFieldRequired()) {
$this->setFieldError(true);
}
$this->setValue($this->readValueFromRevision($object));
}
public function getOldValueForApplicationTransactions() {
return $this->readValueFromRevision($this->getObject());
}
public function getNewValueForApplicationTransactions() {
return $this->getValue();
}
public function applyApplicationTransactionInternalEffects(
PhabricatorApplicationTransaction $xaction) {
$this->writeValueToRevision($this->getObject(), $xaction->getNewValue());
}
public function setFieldError($field_error) {
$this->fieldError = $field_error;
return $this;
}
public function getFieldError() {
return $this->fieldError;
}
public function setValue($value) {
$this->value = $value;
return $this;
}
public function getValue() {
return $this->value;
}
public function readValueFromCommitMessage($value) {
$this->setValue($value);
return $this;
}
public function renderCommitMessageValue(array $handles) {
return $this->getValue();
}
public function getConduitDictionaryValue() {
return $this->getValue();
}
}
diff --git a/src/applications/differential/parser/DifferentialCommitMessageParser.php b/src/applications/differential/parser/DifferentialCommitMessageParser.php
index fda7ae05b..ff76b30e5 100644
--- a/src/applications/differential/parser/DifferentialCommitMessageParser.php
+++ b/src/applications/differential/parser/DifferentialCommitMessageParser.php
@@ -1,216 +1,255 @@
<?php
/**
* Parses commit messages (containing relatively freeform text with textual
* field labels) into a dictionary of fields.
*
* $parser = id(new DifferentialCommitMessageParser())
* ->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 $labelMap;
private $titleKey;
private $summaryKey;
private $errors;
+ 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())
+ ->setLabelMap($label_map)
+ ->setTitleKey($key_title)
+ ->setSummaryKey($key_summary);
+ }
+
+
/* -( Configuring the Parser )--------------------------------------------- */
/**
* @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 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>'.$field_labels.'):(?P<text>.*)$/i';
return $field_pattern;
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Feb 26, 7:08 PM (1 d, 2 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
6c/7c/1c7ce5a328d81bc3c7fbcca62ab4
Attached To
rPHAB Phabricator
Event Timeline
Log In to Comment