mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-18 19:40:55 +01:00
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
This commit is contained in:
parent
735120b842
commit
0be3db03ee
19 changed files with 322 additions and 487 deletions
|
@ -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',
|
||||
|
|
|
@ -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>'.$field_labels.'):(?P<text>.*)$/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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -16,6 +16,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class DifferentialCommitMessageParserException extends Exception {
|
||||
final class DifferentialFieldParseException extends Exception {
|
||||
|
||||
}
|
|
@ -7,4 +7,4 @@
|
|||
|
||||
|
||||
|
||||
phutil_require_source('DifferentialCommitMessageParserException.php');
|
||||
phutil_require_source('DifferentialFieldParseException.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 )---------------------------------------------------- */
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -46,4 +46,8 @@ final class DifferentialGitSVNIDFieldSpecification
|
|||
return null;
|
||||
}
|
||||
|
||||
public function parseValueFromCommitMessage($value) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -89,4 +89,8 @@ final class DifferentialReviewedByFieldSpecification
|
|||
return implode(', ', $names);
|
||||
}
|
||||
|
||||
public function parseValueFromCommitMessage($value) {
|
||||
return $this->parseCommitMessageUserList($value);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -46,5 +46,8 @@ final class DifferentialRevisionIDFieldSpecification
|
|||
return $this->id;
|
||||
}
|
||||
|
||||
public function parseValueFromCommitMessage($value) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -70,4 +70,8 @@ final class DifferentialSummaryFieldSpecification
|
|||
return $this->summary;
|
||||
}
|
||||
|
||||
public function parseValueFromCommitMessage($value) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -81,5 +81,8 @@ final class DifferentialTestPlanFieldSpecification
|
|||
return $this->plan;
|
||||
}
|
||||
|
||||
public function parseValueFromCommitMessage($value) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -82,4 +82,8 @@ final class DifferentialTitleFieldSpecification
|
|||
return $this->title;
|
||||
}
|
||||
|
||||
public function parseValueFromCommitMessage($value) {
|
||||
return preg_replace('/\s*\n\s*/', ' ', $value);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,445 +0,0 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright 2011 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class DifferentialCommitMessage {
|
||||
|
||||
protected $rawCorpus;
|
||||
|
||||
protected $title;
|
||||
protected $summary;
|
||||
protected $testPlan;
|
||||
|
||||
protected $blameRevision;
|
||||
protected $revertPlan;
|
||||
|
||||
protected $reviewerNames = array();
|
||||
protected $reviewerPHIDs;
|
||||
|
||||
protected $reviewedByNames = array();
|
||||
protected $reviewedByPHIDs;
|
||||
|
||||
protected $ccNames = array();
|
||||
protected $ccPHIDs;
|
||||
|
||||
protected $revisionID;
|
||||
protected $gitSVNID;
|
||||
|
||||
protected $tasks = array();
|
||||
|
||||
protected function __construct() {
|
||||
|
||||
}
|
||||
|
||||
public function getReviewerPHIDs() {
|
||||
return $this->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>'.$field_names.'):(?P<text>.*)$/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);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is automatically generated. Lint this module to rebuild it.
|
||||
* @generated
|
||||
*/
|
||||
|
||||
|
||||
|
||||
phutil_require_module('phabricator', 'applications/differential/parser/commitmessage/exception');
|
||||
phutil_require_module('phabricator', 'applications/metamta/storage/mailinglist');
|
||||
phutil_require_module('phabricator', 'applications/people/storage/user');
|
||||
|
||||
phutil_require_module('phutil', 'utils');
|
||||
|
||||
|
||||
phutil_require_source('DifferentialCommitMessage.php');
|
Loading…
Reference in a new issue