diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_parsecommitmessage_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_parsecommitmessage_Method.php index b7b85deed..a25d97765 100644 --- a/src/applications/conduit/method/differential/ConduitAPI_differential_parsecommitmessage_Method.php +++ b/src/applications/conduit/method/differential/ConduitAPI_differential_parsecommitmessage_Method.php @@ -1,190 +1,189 @@ <?php /* * Copyright 2012 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. */ /** * @group conduit */ final class ConduitAPI_differential_parsecommitmessage_Method extends ConduitAPIMethod { public function getMethodDescription() { return "Parse commit messages for Differential fields."; } public function defineParamTypes() { return array( 'corpus' => 'required string', 'partial' => 'optional bool', ); } public function defineReturnType() { return 'nonempty dict'; } public function defineErrorTypes() { return array( ); } protected function execute(ConduitAPIRequest $request) { $corpus = $request->getValue('corpus'); $is_partial = $request->getValue('partial'); $aux_fields = DifferentialFieldSelector::newSelector() ->getFieldSpecifications(); foreach ($aux_fields as $key => $aux_field) { if (!$aux_field->shouldAppearOnCommitMessage()) { unset($aux_fields[$key]); } $aux_field->setUser($request->getUser()); } $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); + $field->setValueFromParsedCommitMessage($fields[$field_key]); } catch (DifferentialFieldParseException $ex) { $field_label = $field->renderLabelForCommitMessage(); $errors[] = "Error parsing field '{$field_label}': ".$ex->getMessage(); - $fields[$field_key] = $ex->getPartialParse(); } - $field->setValueFromParsedCommitMessage($fields[$field_key]); } if (!$is_partial) { foreach ($aux_fields as $field_key => $aux_field) { try { $aux_field->validateField(); } catch (DifferentialFieldValidationException $ex) { $field_label = $aux_field->renderLabelForCommitMessage(); $errors[] = "Invalid or missing field '{$field_label}': ". $ex->getMessage(); } } } return array( 'errors' => $errors, 'fields' => $fields, ); } private function buildLabelMap(array $aux_fields) { assert_instances_of($aux_fields, 'DifferentialFieldSpecification'); $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; } } diff --git a/src/applications/differential/field/exception/DifferentialFieldParseException.php b/src/applications/differential/field/exception/DifferentialFieldParseException.php index 992fb7216..33254675a 100644 --- a/src/applications/differential/field/exception/DifferentialFieldParseException.php +++ b/src/applications/differential/field/exception/DifferentialFieldParseException.php @@ -1,31 +1,21 @@ <?php /* * Copyright 2012 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. */ final class DifferentialFieldParseException extends Exception { - private $partialParse; - - public function __construct($message, $partial_parse = null) { - parent::__construct($message); - $this->partialParse = $partial_parse; - } - - public function getPartialParse() { - return $this->partialParse; - } } diff --git a/src/applications/differential/field/specification/DifferentialFieldSpecification.php b/src/applications/differential/field/specification/DifferentialFieldSpecification.php index 728be2d07..ce13cb222 100644 --- a/src/applications/differential/field/specification/DifferentialFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialFieldSpecification.php @@ -1,910 +1,909 @@ <?php /* * Copyright 2012 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. */ /** * Describes and implements the behavior for a custom field on Differential * revisions. Along with other configuration, you can extend this class to add * custom fields to Differential revisions and commit messages. * * Generally, you should implement all methods from the storage task and then * the methods from one or more interface tasks. * * @task storage Field Storage * @task edit Extending the Revision Edit Interface * @task view Extending the Revision View Interface * @task list Extending the Revision List Interface * @task mail Extending the E-mail Interface * @task conduit Extending the Conduit View Interface * @task commit Extending Commit Messages * @task load Loading Additional Data * @task context Contextual Data */ abstract class DifferentialFieldSpecification { private $revision; private $diff; private $handles; private $diffProperties; private $user; /* -( Storage )------------------------------------------------------------ */ /** * Return a unique string used to key storage of this field's value, like * "mycompany.fieldname" or similar. You can return null (the default) to * indicate that this field does not use any storage. This is appropriate for * display fields, like @{class:DifferentialLinesFieldSpecification}. If you * implement this, you must also implement @{method:getValueForStorage} and * @{method:setValueFromStorage}. * * @return string|null Unique key which identifies this field in auxiliary * field storage. Maximum length is 32. Alternatively, * null (default) to indicate that this field does not * use auxiliary field storage. * @task storage */ public function getStorageKey() { return null; } /** * Return a serialized representation of the field value, appropriate for * storing in auxiliary field storage. You must implement this method if * you implement @{method:getStorageKey}. * * @return string Serialized field value. * @task storage */ public function getValueForStorage() { throw new DifferentialFieldSpecificationIncompleteException($this); } /** * Set the field's value given a serialized storage value. This is called * when the field is loaded; if no data is available, the value will be * null. You must implement this method if you implement * @{method:getStorageKey}. * * @param string|null Serialized field representation (from * @{method:getValueForStorage}) or null if no value has * ever been stored. * @return this * @task storage */ public function setValueFromStorage($value) { throw new DifferentialFieldSpecificationIncompleteException($this); } /* -( Extending the Revision Edit Interface )------------------------------ */ /** * Determine if this field should appear on the "Edit Revision" interface. If * you return true from this method, you must implement * @{method:setValueFromRequest}, @{method:renderEditControl} and * @{method:validateField}. * * For a concrete example of a field which implements an edit interface, see * @{class:DifferentialRevertPlanFieldSpecification}. * * @return bool True to indicate that this field implements an edit interface. * @task edit */ public function shouldAppearOnEdit() { return false; } /** * Set the field's value from an HTTP request. Generally, you should read * the value of some field name you emitted in @{method:renderEditControl} * and save it into the object, e.g.: * * $this->value = $request->getStr('my-custom-field'); * * If you have some particularly complicated field, you may need to read * more data; this is why you have access to the entire request. * * You must implement this if you implement @{method:shouldAppearOnEdit}. * * You should not perform field validation here; instead, you should implement * @{method:validateField}. * * @param AphrontRequest HTTP request representing a user submitting a form * with this field in it. * @return this * @task edit */ public function setValueFromRequest(AphrontRequest $request) { throw new DifferentialFieldSpecificationIncompleteException($this); } /** * Build a renderable object (generally, some @{class:AphrontFormControl}) * which can be appended to a @{class:AphrontFormView} and represents the * interface the user sees on the "Edit Revision" screen when interacting * with this field. * * For example: * * return id(new AphrontFormTextControl()) * ->setLabel('Custom Field') * ->setName('my-custom-key') * ->setValue($this->value); * * You must implement this if you implement @{method:shouldAppearOnEdit}. * * @return AphrontView|string Something renderable. * @task edit */ public function renderEditControl() { throw new DifferentialFieldSpecificationIncompleteException($this); } /** * This method will be called after @{method:setValueFromRequest} but before * the field is saved. It gives you an opportunity to inspect the field value * and throw a @{class:DifferentialFieldValidationException} if there is a * problem with the value the user has provided (for example, the value the * user entered is not correctly formatted). This method is also called after * @{method:setValueFromParsedCommitMessage} before the revision is saved. * * By default, fields are not validated. * * @return void * @task edit */ public function validateField() { return; } /** * Hook for applying revision changes via the editor. Normally, you should * not implement this, but a number of builtin fields use the revision object * itself as storage. If you need to do something similar for whatever reason, * this method gives you an opportunity to interact with the editor or * revision before changes are saved (for example, you can write the field's * value into some property of the revision). * * @param DifferentialRevisionEditor Active editor which is applying changes * to the revision. * @return void * @task edit */ public function willWriteRevision(DifferentialRevisionEditor $editor) { return; } /** * Hook after an edit operation has completed. This allows you to update * link tables or do other write operations which should happen after the * revision is saved. Normally you don't need to implement this. * * * @param DifferentialRevisionEditor Active editor which has just applied * changes to the revision. * @return void * @task edit */ public function didWriteRevision(DifferentialRevisionEditor $editor) { return; } /* -( Extending the Revision View Interface )------------------------------ */ /** * Determine if this field should appear on the revision detail view * interface. One use of this interface is to add purely informational * fields to the revision view, without any sort of backing storage. * * If you return true from this method, you must implement the methods * @{method:renderLabelForRevisionView} and * @{method:renderValueForRevisionView}. * * @return bool True if this field should appear when viewing a revision. * @task view */ public function shouldAppearOnRevisionView() { return false; } /** * Return a string field label which will appear in the revision detail * table. * * You must implement this method if you return true from * @{method:shouldAppearOnRevisionView}. * * @return string Label for field in revision detail view. * @task view */ public function renderLabelForRevisionView() { throw new DifferentialFieldSpecificationIncompleteException($this); } /** * Return a markup block representing the field for the revision detail * view. Note that you can return null to suppress display (for instance, * if the field shows related objects of some type and the revision doesn't * have any related objects). * * You must implement this method if you return true from * @{method:shouldAppearOnRevisionView}. * * @return string|null Display markup for field value, or null to suppress * field rendering. * @task view */ public function renderValueForRevisionView() { throw new DifferentialFieldSpecificationIncompleteException($this); } /** * Load users, their current statuses and return a markup with links to the * user profiles and information about their current status. * * @return string Display markup. * @task view */ public function renderUserList(array $user_phids) { if (!$user_phids) { return '<em>None</em>'; } $statuses = id(new PhabricatorUserStatus())->loadCurrentStatuses( $user_phids); $links = array(); foreach ($user_phids as $user_phid) { $handle = $this->getHandle($user_phid); $extra = null; $status = idx($statuses, $handle->getPHID()); if ($handle->isDisabled()) { $extra = ' <strong>(disabled)</strong>'; } else if ($status) { $until = phabricator_date($status->getDateTo(), $this->getUser()); if ($status->getStatus() == PhabricatorUserStatus::STATUS_SPORADIC) { $extra = ' <strong title="until '.$until.'">(sporadic)</strong>'; } else { $extra = ' <strong title="until '.$until.'">(away)</strong>'; } } $links[] = $handle->renderLink().$extra; } return implode(', ', $links); } /** * Return a markup block representing a warning to display with the comment * box when preparing to accept a diff. A return value of null indicates no * warning box should be displayed for this field. * * @return string|null Display markup for warning box, or null for no warning */ public function renderWarningBoxForRevisionAccept() { return null; } /* -( Extending the Revision List Interface )------------------------------ */ /** * Determine if this field should appear in the table on the revision list * interface. * * @return bool True if this field should appear in the table. * * @task list */ public function shouldAppearOnRevisionList() { return false; } /** * Return a column header for revision list tables. * * @return string Column header. * * @task list */ public function renderHeaderForRevisionList() { throw new DifferentialFieldSpecificationIncompleteException($this); } /** * Optionally, return a column class for revision list tables. * * @return string CSS class for table cells. * * @task list */ public function getColumnClassForRevisionList() { return null; } /** * Return a table cell value for revision list tables. * * @param DifferentialRevision The revision to render a value for. * @return string Table cell value. * * @task list */ public function renderValueForRevisionList(DifferentialRevision $revision) { throw new DifferentialFieldSpecificationIncompleteException($this); } /* -( Extending the E-mail Interface )------------------------------------- */ /** * Return plain text to render in e-mail messages. The text may span * multiple lines. * * @return int One of DifferentialMailPhase constants. * @return string|null Plain text, or null for no message. * * @task mail */ public function renderValueForMail($phase) { return null; } /* -( Extending the Conduit Interface )------------------------------------ */ /** * @task conduit */ public function shouldAppearOnConduitView() { return false; } /** * @task conduit */ public function getValueForConduit() { throw new DifferentialFieldSpecificationIncompleteException($this); } /** * @task conduit */ public function getKeyForConduit() { $key = $this->getStorageKey(); if ($key === null) { throw new DifferentialFieldSpecificationIncompleteException($this); } return $key; } /* -( Extending Commit Messages )------------------------------------------ */ /** * Determine if this field should appear in commit messages. You should return * true if this field participates in any part of the commit message workflow, * even if it is not rendered by default. * * If you implement this method, you must implement * @{method:getCommitMessageKey} and * @{method:setValueFromParsedCommitMessage}. * * @return bool True if this field appears in commit messages in any capacity. * @task commit */ public function shouldAppearOnCommitMessage() { return false; } /** * Key which identifies this field in parsed commit messages. Commit messages * exist in two forms: raw textual commit messages and parsed dictionaries of * fields. This method must return a unique string which identifies this field * in dictionaries. Principally, this dictionary is shipped to and from arc * over Conduit. Keys should be appropriate property names, like "testPlan" * (not "Test Plan") and must be globally unique. * * You must implement this method if you return true from * @{method:shouldAppearOnCommitMessage}. * * @return string Key which identifies the field in dictionaries. * @task commit */ public function getCommitMessageKey() { throw new DifferentialFieldSpecificationIncompleteException($this); } /** * Set this field's value from a value in a parsed commit message dictionary. * Afterward, this field will go through the normal write workflows and the * change will be permanently stored via either the storage mechanisms (if * your field implements them), revision write hooks (if your field implements * 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}. * * @param mixed Field value from a parsed commit message dictionary. * @return this * @task commit */ public function setValueFromParsedCommitMessage($value) { throw new DifferentialFieldSpecificationIncompleteException($this); } /** * In revision control systems which read revision information from the * working copy, the user may edit the commit message outside of invoking * "arc diff --edit". When they do this, only some fields (those fields which * can not be edited by other users) are safe to overwrite. For instance, it * is fine to overwrite "Summary" because no one else can edit it, but not * to overwrite "Reviewers" because reviewers may have been added or removed * via the web interface. * * If a field is safe to overwrite when edited in a working copy commit * message, return true. If the authoritative value should always be used, * return false. By default, fields can not be overwritten. * * arc will only attempt to overwrite field values if run with "--verbatim". * * @return bool True to indicate the field is save to overwrite. * @task commit */ public function shouldOverwriteWhenCommitMessageIsEdited() { return false; } /** * Return true if this field should be suggested to the user during * "arc diff --edit". Basicially, return true if the field is something the * user might want to fill out (like "Summary"), and false if it's a * system/display/readonly field (like "Differential Revision"). If this * method returns true, the field will be rendered even if it has no value * during edit and update operations. * * @return bool True to indicate the field should appear in the edit template. * @task commit */ public function shouldAppearOnCommitMessageTemplate() { return true; } /** * Render a human-readable label for this field, like "Summary" or * "Test Plan". This is distinct from the commit message key, but generally * they should be similar. * * @return string Human-readable field label for commit messages. * @task commit */ public function renderLabelForCommitMessage() { throw new DifferentialFieldSpecificationIncompleteException($this); } /** * Render a human-readable value for this field when it appears in commit * messages (for instance, lists of users should be rendered as user names). * * The ##$is_edit## parameter allows you to distinguish between commit * messages being rendered for editing and those being rendered for amending * or commit. Some fields may decline to render a value in one mode (for * example, "Reviewed By" appears only when doing commit/amend, not while * editing). * * @param bool True if the message is being edited. * @return string Human-readable field value. * @task commit */ public function renderValueForCommitMessage($is_edit) { 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 )-------------------------------------------- */ /** * Specify which @{class:PhabricatorObjectHandle}s need to be loaded for your * field to render correctly. * * This is a convenience method which makes the handles available on all * interfaces where the field appears. If your field needs handles on only * some interfaces (or needs different handles on different interfaces) you * can overload the more specific methods to customize which interfaces you * retrieve handles for. Requesting only the handles you need will improve * the performance of your field. * * You can later retrieve these handles by calling @{method:getHandle}. * * @return list List of PHIDs to load handles for. * @task load */ protected function getRequiredHandlePHIDs() { return array(); } /** * Specify which @{class:PhabricatorObjectHandle}s need to be loaded for your * field to render correctly on the view interface. * * This is a more specific version of @{method:getRequiredHandlePHIDs} which * can be overridden to improve field performance by loading only data you * need. * * @return list List of PHIDs to load handles for. * @task load */ public function getRequiredHandlePHIDsForRevisionView() { return $this->getRequiredHandlePHIDs(); } /** * Specify which @{class:PhabricatorObjectHandle}s need to be loaded for your * field to render correctly on the list interface. * * This is a more specific version of @{method:getRequiredHandlePHIDs} which * can be overridden to improve field performance by loading only data you * need. * * @param DifferentialRevision The revision to pull PHIDs for. * @return list List of PHIDs to load handles for. * @task load */ public function getRequiredHandlePHIDsForRevisionList( DifferentialRevision $revision) { return array(); } /** * Specify which @{class:PhabricatorObjectHandle}s need to be loaded for your * field to render correctly on the edit interface. * * This is a more specific version of @{method:getRequiredHandlePHIDs} which * can be overridden to improve field performance by loading only data you * need. * * @return list List of PHIDs to load handles for. * @task load */ public function getRequiredHandlePHIDsForRevisionEdit() { return $this->getRequiredHandlePHIDs(); } /** * Specify which @{class:PhabricatorObjectHandle}s need to be loaded for your * field to render correctly on the commit message interface. * * This is a more specific version of @{method:getRequiredHandlePHIDs} which * can be overridden to improve field performance by loading only data you * need. * * @return list List of PHIDs to load handles for. * @task load */ public function getRequiredHandlePHIDsForCommitMessage() { return $this->getRequiredHandlePHIDs(); } /** * Specify which diff properties this field needs to load. * * @return list List of diff property keys this field requires. * @task load */ public function getRequiredDiffProperties() { 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. * @param bool True to include mailing lists. * @param bool True to make a best effort. By default, an exception is * thrown if any item is invalid. * @return list List of corresponding PHIDs. * @task load */ public static function parseCommitMessageObjectList( $value, $include_mailables, $allow_partial = false) { $value = array_unique(array_filter(preg_split('/[\s,]+/', $value))); if (!$value) { return array(); } $object_map = array(); $users = id(new PhabricatorUser())->loadAllWhere( '(username IN (%Ls))', $value); $user_map = mpull($users, 'getPHID', 'getUsername'); foreach ($user_map as $username => $phid) { // Usernames may have uppercase letters in them. Put both names in the // map so we can try the original case first, so that username *always* // works in weird edge cases where some other mailable object collides. $object_map[$username] = $phid; $object_map[strtolower($username)] = $phid; } 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])) { if (empty($object_map[strtolower($name)])) { $invalid[] = $name; } else { $results[] = $object_map[strtolower($name)]; } } else { $results[] = $object_map[$name]; } } if ($invalid && !$allow_partial) { $invalid = implode(', ', $invalid); $what = $include_mailables ? "users and mailing lists" : "users"; throw new DifferentialFieldParseException( - "Commit message references nonexistent {$what}: {$invalid}.", - array_unique($results)); + "Commit message references nonexistent {$what}: {$invalid}."); } return array_unique($results); } /* -( Contextual Data )---------------------------------------------------- */ /** * @task context */ final public function setRevision(DifferentialRevision $revision) { $this->revision = $revision; $this->didSetRevision(); return $this; } /** * @task context */ protected function didSetRevision() { return; } /** * @task context */ final public function setDiff(DifferentialDiff $diff) { $this->diff = $diff; return $this; } /** * @task context */ final public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } /** * @task context */ final public function setDiffProperties(array $diff_properties) { $this->diffProperties = $diff_properties; return $this; } /** * @task context */ final public function setUser(PhabricatorUser $user) { $this->user = $user; return $this; } /** * @task context */ final protected function getRevision() { if (empty($this->revision)) { throw new DifferentialFieldDataNotAvailableException($this); } return $this->revision; } /** * @task context */ final protected function getDiff() { if (empty($this->diff)) { throw new DifferentialFieldDataNotAvailableException($this); } return $this->diff; } /** * @task context */ final protected function getUser() { if (empty($this->user)) { throw new DifferentialFieldDataNotAvailableException($this); } return $this->user; } /** * Get the handle for an object PHID. You must overload * @{method:getRequiredHandlePHIDs} (or a more specific version thereof) * and include the PHID you want in the list for it to be available here. * * @return PhabricatorObjectHandle Handle to the object. * @task context */ final protected function getHandle($phid) { if ($this->handles === null) { throw new DifferentialFieldDataNotAvailableException($this); } if (empty($this->handles[$phid])) { $class = get_class($this); throw new Exception( "A differential field (of class '{$class}') is attempting to retrieve ". "a handle ('{$phid}') which it did not request. Return all handle ". "PHIDs you need from getRequiredHandlePHIDs()."); } return $this->handles[$phid]; } /** * Get a diff property which this field previously requested by returning * the key from @{method:getRequiredDiffProperties}. * * @param string Diff property key. * @return string|null Diff property, or null if the property does not have * a value. * @task context */ final public function getDiffProperty($key) { if ($this->diffProperties === null) { // This will be set to some (possibly empty) array if we've loaded // properties, so null means diff properties aren't available in this // context. throw new DifferentialFieldDataNotAvailableException($this); } if (!array_key_exists($key, $this->diffProperties)) { $class = get_class($this); throw new Exception( "A differential field (of class '{$class}') is attempting to retrieve ". "a diff property ('{$key}') which it did not request. Return all ". "diff property keys you need from getRequiredDiffProperties()."); } return $this->diffProperties[$key]; } } diff --git a/src/applications/differential/field/specification/DifferentialManiphestTasksFieldSpecification.php b/src/applications/differential/field/specification/DifferentialManiphestTasksFieldSpecification.php index 4758301a8..8f4760b78 100644 --- a/src/applications/differential/field/specification/DifferentialManiphestTasksFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialManiphestTasksFieldSpecification.php @@ -1,178 +1,177 @@ <?php /* * Copyright 2012 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. */ final class DifferentialManiphestTasksFieldSpecification extends DifferentialFieldSpecification { private $maniphestTasks = array(); public function shouldAppearOnRevisionView() { return PhabricatorEnv::getEnvConfig('maniphest.enabled'); } public function getRequiredHandlePHIDsForRevisionView() { return $this->getManiphestTaskPHIDs(); } public function renderLabelForRevisionView() { return 'Maniphest Tasks:'; } public function renderValueForRevisionView() { $task_phids = $this->getManiphestTaskPHIDs(); if (!$task_phids) { return null; } $links = array(); foreach ($task_phids as $task_phid) { $links[] = $this->getHandle($task_phid)->renderLink(); } return implode('<br />', $links); } private function getManiphestTaskPHIDs() { $revision = $this->getRevision(); return $revision->getAttachedPHIDs( PhabricatorPHIDConstants::PHID_TYPE_TASK); } /** * Attach the revision to the task(s) and the task(s) to the revision. * * @return void */ public function didWriteRevision(DifferentialRevisionEditor $editor) { $aeditor = new PhabricatorObjectAttachmentEditor( PhabricatorPHIDConstants::PHID_TYPE_DREV, $editor->getRevision()); $aeditor->setUser($this->getUser()); $aeditor->attachObjects( PhabricatorPHIDConstants::PHID_TYPE_TASK, $this->maniphestTasks, $two_way = true); } protected function didSetRevision() { $this->maniphestTasks = $this->getManiphestTaskPHIDs(); } public function getRequiredHandlePHIDsForCommitMessage() { return $this->maniphestTasks; } public function shouldAppearOnCommitMessageTemplate() { return PhabricatorEnv::getEnvConfig('maniphest.enabled'); } public function shouldAppearOnCommitMessage() { return PhabricatorEnv::getEnvConfig('maniphest.enabled'); } public function getCommitMessageKey() { return 'maniphestTaskPHIDs'; } public function setValueFromParsedCommitMessage($value) { $this->maniphestTasks = nonempty($value, array()); return $this; } public function renderLabelForCommitMessage() { return 'Maniphest Tasks'; } public function getSupportedCommitMessageLabels() { return array( 'Maniphest Task', 'Maniphest Tasks', ); } public function renderValueForCommitMessage($is_edit) { if (!$this->maniphestTasks) { return null; } $names = array(); foreach ($this->maniphestTasks as $phid) { $handle = $this->getHandle($phid); $names[] = 'T'.$handle->getAlternateID(); } return implode(', ', $names); } public function parseValueFromCommitMessage($value) { $matches = null; preg_match_all('/T(\d+)/', $value, $matches); if (empty($matches[0])) { return array(); } $task_ids = $matches[1]; $tasks = id(new ManiphestTask()) ->loadAllWhere('id in (%Ld)', $task_ids); $task_phids = array(); $invalid = array(); foreach ($task_ids as $task_id) { $task = idx($tasks, $task_id); if (empty($task)) { $invalid[] = 'T'.$task_id; } else { $task_phids[] = $task->getPHID(); } } if ($invalid) { if (count($invalid) > 1) { $what = 'Maniphest Tasks'; } else { $what = 'Maniphest Task'; } $invalid = implode(', ', $invalid); throw new DifferentialFieldParseException( - "Commit message references nonexistent {$what}: {$invalid}.", - $task_phids); + "Commit message references nonexistent {$what}: {$invalid}."); } return $task_phids; } public function renderValueForMail($phase) { if ($phase == DifferentialMailPhase::COMMENT) { return null; } if (!$this->maniphestTasks) { return null; } $handles = id(new PhabricatorObjectHandleData($this->maniphestTasks)) ->loadHandles(); $body = array(); $body[] = 'MANIPHEST TASKS'; foreach ($handles as $handle) { $body[] = ' '.PhabricatorEnv::getProductionURI($handle->getURI()); } return implode("\n", $body); } }