diff --git a/src/applications/differential/field/selector/DifferentialDefaultFieldSelector.php b/src/applications/differential/field/selector/DifferentialDefaultFieldSelector.php index b5ec522fe7..98bb0f630b 100644 --- a/src/applications/differential/field/selector/DifferentialDefaultFieldSelector.php +++ b/src/applications/differential/field/selector/DifferentialDefaultFieldSelector.php @@ -1,92 +1,113 @@ <?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 DifferentialDefaultFieldSelector extends DifferentialFieldSelector { public function getFieldSpecifications() { $fields = array( new DifferentialTitleFieldSpecification(), new DifferentialSummaryFieldSpecification(), ); if (PhabricatorEnv::getEnvConfig('differential.show-test-plan-field')) { $fields[] = new DifferentialTestPlanFieldSpecification(); } $fields = array_merge( $fields, array( new DifferentialRevisionStatusFieldSpecification(), new DifferentialAuthorFieldSpecification(), new DifferentialReviewersFieldSpecification(), new DifferentialReviewedByFieldSpecification(), new DifferentialCCsFieldSpecification(), new DifferentialUnitFieldSpecification(), new DifferentialLintFieldSpecification(), new DifferentialCommitsFieldSpecification(), new DifferentialDependenciesFieldSpecification(), new DifferentialManiphestTasksFieldSpecification(), )); if (PhabricatorEnv::getEnvConfig('differential.show-host-field')) { $fields[] = new DifferentialHostFieldSpecification(); $fields[] = new DifferentialPathFieldSpecification(); } $fields = array_merge( $fields, array( new DifferentialBranchFieldSpecification(), new DifferentialArcanistProjectFieldSpecification(), new DifferentialApplyPatchFieldSpecification(), new DifferentialRevisionIDFieldSpecification(), new DifferentialGitSVNIDFieldSpecification(), new DifferentialDateModifiedFieldSpecification(), new DifferentialDateCreatedFieldSpecification(), new DifferentialAuditorsFieldSpecification(), )); return $fields; } public function sortFieldsForRevisionList(array $fields) { assert_instances_of($fields, 'DifferentialFieldSpecification'); $map = array(); foreach ($fields as $field) { $map[get_class($field)] = $field; } $map = array_select_keys( $map, array( 'DifferentialRevisionIDFieldSpecification', 'DifferentialTitleFieldSpecification', 'DifferentialRevisionStatusFieldSpecification', 'DifferentialAuthorFieldSpecification', 'DifferentialReviewersFieldSpecification', 'DifferentialDateModifiedFieldSpecification', 'DifferentialDateCreatedFieldSpecification', )) + $map; return array_values($map); } + public function sortFieldsForMail(array $fields) { + assert_instances_of($fields, 'DifferentialFieldSpecification'); + + $map = array(); + foreach ($fields as $field) { + $map[get_class($field)] = $field; + } + + $map = array_select_keys( + $map, + array( + 'DifferentialSummaryFieldSpecification', + 'DifferentialTestPlanFieldSpecification', + 'DifferentialRevisionIDFieldSpecification', + 'DifferentialBranchFieldSpecification', + 'DifferentialCommitsFieldSpecification', + )) + $map; + + return array_values($map); + } + } diff --git a/src/applications/differential/field/selector/DifferentialFieldSelector.php b/src/applications/differential/field/selector/DifferentialFieldSelector.php index d4070e5564..523b5e829e 100644 --- a/src/applications/differential/field/selector/DifferentialFieldSelector.php +++ b/src/applications/differential/field/selector/DifferentialFieldSelector.php @@ -1,36 +1,41 @@ <?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. */ abstract class DifferentialFieldSelector { final public function __construct() { // <empty> } final public static function newSelector() { return PhabricatorEnv::newObjectFromConfig('differential.field-selector'); } abstract public function getFieldSpecifications(); public function sortFieldsForRevisionList(array $fields) { assert_instances_of($fields, 'DifferentialFieldSpecification'); return $fields; } + public function sortFieldsForMail(array $fields) { + assert_instances_of($fields, 'DifferentialFieldSpecification'); + return $fields; + } + } diff --git a/src/applications/differential/field/specification/DifferentialBranchFieldSpecification.php b/src/applications/differential/field/specification/DifferentialBranchFieldSpecification.php index ec8ceabc93..af1cbe89ca 100644 --- a/src/applications/differential/field/specification/DifferentialBranchFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialBranchFieldSpecification.php @@ -1,41 +1,58 @@ <?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 DifferentialBranchFieldSpecification extends DifferentialFieldSpecification { public function shouldAppearOnRevisionView() { return true; } public function renderLabelForRevisionView() { return 'Branch:'; } public function renderValueForRevisionView() { $diff = $this->getDiff(); $branch = $diff->getBranch(); if ($branch == '') { return null; } return phutil_escape_html($branch); } + public function renderValueForMail() { + $status = $this->getRevision()->getStatus(); + + if ($status != ArcanistDifferentialRevisionStatus::NEEDS_REVISION && + $status != ArcanistDifferentialRevisionStatus::ACCEPTED) { + return null; + } + + $diff = $this->getRevision()->loadActiveDiff(); + if ($diff) { + $branch = $diff->getBranch(); + if ($branch) { + return "BRANCH\n $branch"; + } + } + } + } diff --git a/src/applications/differential/field/specification/DifferentialCommitsFieldSpecification.php b/src/applications/differential/field/specification/DifferentialCommitsFieldSpecification.php index 245bf6049c..41958397cf 100644 --- a/src/applications/differential/field/specification/DifferentialCommitsFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialCommitsFieldSpecification.php @@ -1,53 +1,83 @@ <?php /* - * Copyright 2011 Facebook, Inc. + * 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 DifferentialCommitsFieldSpecification extends DifferentialFieldSpecification { public function shouldAppearOnRevisionView() { return true; } public function getRequiredHandlePHIDsForRevisionView() { return $this->getCommitPHIDs(); } public function renderLabelForRevisionView() { return 'Commits:'; } public function renderValueForRevisionView() { $commit_phids = $this->getCommitPHIDs(); if (!$commit_phids) { return null; } $links = array(); foreach ($commit_phids as $commit_phid) { $links[] = $this->getHandle($commit_phid)->renderLink(); } return implode('<br />', $links); } private function getCommitPHIDs() { $revision = $this->getRevision(); return $revision->getCommitPHIDs(); } + public function renderValueForMail() { + $revision = $this->getRevision(); + + if ($revision->getStatus() != ArcanistDifferentialRevisionStatus::CLOSED) { + return null; + } + + $phids = $revision->loadCommitPHIDs(); + if (!$phids) { + return null; + } + + $body = array(); + $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); + if (count($handles) == 1) { + $body[] = "COMMIT"; + } else { + // This is unlikely to ever happen since we'll send this mail the + // first time we discover a commit, but it's not impossible if data + // was migrated, etc. + $body[] = "COMMITS"; + } + + foreach ($handles as $handle) { + $body[] = ' '.PhabricatorEnv::getProductionURI($handle->getURI()); + } + + return implode("\n", $body); + } + } diff --git a/src/applications/differential/field/specification/DifferentialFieldSpecification.php b/src/applications/differential/field/specification/DifferentialFieldSpecification.php index 334eab01e4..1edd7fc27f 100644 --- a/src/applications/differential/field/specification/DifferentialFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialFieldSpecification.php @@ -1,892 +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 string|null Plain text, or null for no message. + * + * @task mail + */ + public function renderValueForMail() { + 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)); } 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/DifferentialRevisionIDFieldSpecification.php b/src/applications/differential/field/specification/DifferentialRevisionIDFieldSpecification.php index 96e8c956ce..0e2c23e046 100644 --- a/src/applications/differential/field/specification/DifferentialRevisionIDFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialRevisionIDFieldSpecification.php @@ -1,107 +1,112 @@ <?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 DifferentialRevisionIDFieldSpecification extends DifferentialFieldSpecification { private $id; protected function didSetRevision() { $this->id = $this->getRevision()->getID(); } public function shouldAppearOnCommitMessage() { return true; } public function shouldAppearOnCommitMessageTemplate() { return false; } public function getCommitMessageKey() { return 'revisionID'; } public function setValueFromParsedCommitMessage($value) { $this->id = $value; return $this; } public function renderLabelForCommitMessage() { return 'Differential Revision'; } public function renderValueForCommitMessage($is_edit) { if (!$this->id) { return null; } return PhabricatorEnv::getProductionURI('/D'.$this->id); } public function parseValueFromCommitMessage($value) { $rev = trim($value); if (!strlen($rev)) { return null; } if (is_numeric($rev)) { // TODO: Eventually, remove support for bare revision numbers. return (int)$rev; } $rev = self::parseRevisionIDFromURI($rev); if ($rev !== null) { return $rev; } $example_uri = PhabricatorEnv::getProductionURI('/D123'); throw new DifferentialFieldParseException( "Commit references invalid 'Differential Revision'. Expected a ". "Phabricator URI like '{$example_uri}', got '{$value}'."); } public static function parseRevisionIDFromURI($uri) { $path = id(new PhutilURI($uri))->getPath(); $matches = null; if (preg_match('#^/D(\d+)$#', $path, $matches)) { $id = (int)$matches[1]; // Make sure the URI is the same as our URI. Basically, we want to ignore // commits from other Phabricator installs. if ($uri == PhabricatorEnv::getProductionURI('/D'.$id)) { return $id; } } return null; } public function shouldAppearOnRevisionList() { return true; } public function renderHeaderForRevisionList() { return 'ID'; } public function renderValueForRevisionList(DifferentialRevision $revision) { return 'D'.$revision->getID(); } + public function renderValueForMail() { + $uri = PhabricatorEnv::getProductionURI('/D'.$this->id); + return "REVISION DETAIL\n {$uri}"; + } + } diff --git a/src/applications/differential/mail/DifferentialCommentMail.php b/src/applications/differential/mail/DifferentialCommentMail.php index 478e7b635c..7fec3f4973 100644 --- a/src/applications/differential/mail/DifferentialCommentMail.php +++ b/src/applications/differential/mail/DifferentialCommentMail.php @@ -1,204 +1,179 @@ <?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 DifferentialCommentMail extends DifferentialMail { protected $changedByCommit; public function setChangedByCommit($changed_by_commit) { $this->changedByCommit = $changed_by_commit; return $this; } public function getChangedByCommit() { return $this->changedByCommit; } public function __construct( DifferentialRevision $revision, PhabricatorObjectHandle $actor, DifferentialComment $comment, array $changesets, array $inline_comments) { assert_instances_of($changesets, 'DifferentialChangeset'); assert_instances_of($inline_comments, 'PhabricatorInlineCommentInterface'); $this->setRevision($revision); $this->setActorHandle($actor); $this->setComment($comment); $this->setChangesets($changesets); $this->setInlineComments($inline_comments); } protected function getMailTags() { $comment = $this->getComment(); $action = $comment->getAction(); $tags = array(); switch ($action) { case DifferentialAction::ACTION_ADDCCS: $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_CC; break; case DifferentialAction::ACTION_CLOSE: $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_CLOSED; break; } if (strlen(trim($comment->getContent()))) { switch ($action) { case DifferentialAction::ACTION_CLOSE: // Commit comments are auto-generated and not especially interesting, // so don't tag them as having a comment. break; default: $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_COMMENT; break; } } return $tags; } protected function renderVarySubject() { $verb = ucwords($this->getVerb()); return "[{$verb}] ".$this->renderSubject(); } protected function getVerb() { $comment = $this->getComment(); $action = $comment->getAction(); $verb = DifferentialAction::getActionPastTenseVerb($action); return $verb; } protected function renderBody() { $comment = $this->getComment(); $actor = $this->getActorName(); $name = $this->getRevision()->getTitle(); $verb = $this->getVerb(); $body = array(); $body[] = "{$actor} has {$verb} the revision \"{$name}\"."; // If the commented added reviewers or CCs, list them explicitly. $meta = $comment->getMetadata(); $m_reviewers = idx( $meta, DifferentialComment::METADATA_ADDED_REVIEWERS, array()); $m_cc = idx( $meta, DifferentialComment::METADATA_ADDED_CCS, array()); $load = array_merge($m_reviewers, $m_cc); if ($load) { $handles = id(new PhabricatorObjectHandleData($load))->loadHandles(); if ($m_reviewers) { $body[] = 'Added Reviewers: '.$this->renderHandleList( $handles, $m_reviewers); } if ($m_cc) { $body[] = 'Added CCs: '.$this->renderHandleList( $handles, $m_cc); } } $body[] = null; $content = $comment->getContent(); if (strlen($content)) { $body[] = $this->formatText($content); $body[] = null; } if ($this->getChangedByCommit()) { $body[] = 'CHANGED PRIOR TO COMMIT'; $body[] = ' '.$this->getChangedByCommit(); $body[] = null; } $inlines = $this->getInlineComments(); if ($inlines) { $body[] = 'INLINE COMMENTS'; $changesets = $this->getChangesets(); foreach ($inlines as $inline) { $changeset = $changesets[$inline->getChangesetID()]; if (!$changeset) { throw new Exception('Changeset missing!'); } $file = $changeset->getFilename(); $start = $inline->getLineNumber(); $len = $inline->getLineLength(); if ($len) { $range = $start.'-'.($start + $len); } else { $range = $start; } $content = $inline->getContent(); $body[] = $this->formatText("{$file}:{$range} {$content}"); } $body[] = null; } - $body[] = $this->renderRevisionDetailLink(); - $body[] = null; - - $revision = $this->getRevision(); - $status = $revision->getStatus(); - - if ($status == ArcanistDifferentialRevisionStatus::NEEDS_REVISION || - $status == ArcanistDifferentialRevisionStatus::ACCEPTED) { - $diff = $revision->loadActiveDiff(); - if ($diff) { - $branch = $diff->getBranch(); - if ($branch) { - $body[] = "BRANCH\n $branch"; - $body[] = null; - } - } - } - - if ($status == ArcanistDifferentialRevisionStatus::CLOSED) { - $phids = $revision->loadCommitPHIDs(); - if ($phids) { - $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); - if (count($handles) == 1) { - $body[] = "COMMIT"; - } else { - // This is unlikely to ever happen since we'll send this mail the - // first time we discover a commit, but it's not impossible if data - // was migrated, etc. - $body[] = "COMMITS"; - } - - foreach ($handles as $handle) { - $body[] = ' '.PhabricatorEnv::getProductionURI($handle->getURI()); - } + $selector = DifferentialFieldSelector::newSelector(); + $aux_fields = $selector->sortFieldsForMail( + $selector->getFieldSpecifications()); + + foreach ($aux_fields as $field) { + $field->setRevision($this->getRevision()); + $text = $field->renderValueForMail(); + if ($text !== null) { + $body[] = $text; $body[] = null; } } return implode("\n", $body); } }