1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-16 11:52:40 +01:00
phorge-phorge/src/applications/differential/field/specification/base/DifferentialFieldSpecification.php
epriestley ae7488f710 Drive commit message rendering from field specifications
Summary:
When rendering commit messages, drive all the logic through field specification
classes instead of the hard-coded DifferentialCommitMessageData class. This
removes DifferentialCommitMessageData and support classes.

Note that this effectively reverts D546, and will cause a minor break for
Facebook (Task IDs will no longer render in commit messages generated by "arc
amend", and will not be editable via "arc diff --edit"). This can be resolved by
implementing the feature as a custom field. While I've been able to preserve the
task ID functionality elsewhere, I felt this implementation was too complex to
reasonably leave hooks for, and the break is pretty minor.

Test Plan:
  - Made numerous calls to differential.getcommitmessage across many diffs in
various states, with and without 'edit' and with and without various field
overrides.
  - General behavior seems correct (messages look accurate, and have the
expected information). Special fields like "Reviewed By" and "git-svn-id" seem
to work correctly.
  - Edit behavior seems correct (edit mode shows all editable fields, hides
fields like "Reviewed By").
  - Field overwrite behavior seems correct (overwritable fields show the correct
values when overwritten, ignore provided values otherwise).

Reviewed By: jungejason
Reviewers: jungejason, tuomaspelkonen, aran
CC: aran, jungejason
Differential Revision: 814
2011-08-18 07:20:20 -07:00

627 lines
20 KiB
PHP

<?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.
*/
/**
* 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 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).
*
* 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);
}
/* -( 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).
*
* 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.
*
* @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);
}
/* -( Loading Additional Data )-------------------------------------------- */
/**
* Specify which @{class:PhabricatorObjectHandles} 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:PhabricatorObjectHandles} 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:PhabricatorObjectHandles} 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:PhabricatorObjectHandles} 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();
}
/* -( 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) {
$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];
}
}