1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-25 16:22:43 +01:00

Add behaviors to Build Plans: hold drafts, affect buildables, warn on landing, restartable, runnable

Summary:
Depends on D20219. Ref T13258. Ref T11415. Installs sometimes have long-running builds or unimportant builds which they may not want to hold up drafts, affect buildable status, or warn during `arc land`.

Some builds have side effects (like deployment or merging) and are not idempotent. They can cause problems if restarted.

In other cases, builds are isolated and idempotent and generally safe, and it's okay for marketing interns to restart them.

To address these cases, add "Behaviors" to Build Plans:

  - Hold Drafts: Controls how the build affects revision promotion from "Draft".
  - Warn on Land: Controls the "arc land" warning.
  - Affects Buildable: Controls whether we care about this build when figuring out if a buildable passed or failed overall.
  - Restartable: Controls whether this build may restart or not.
  - Runnable: Allows you to weaken the requirements to run the build if you're confident it's safe to run it on arbitrary old versions of things.

NOTE: This only implements UI, none of these options actually do anything yet.

Test Plan:
Mostly poked around the UI. I'll actually implement these behaviors next, and vet them more thoroughly.

{F6244828}

{F6244830}

Reviewers: amckinley

Reviewed By: amckinley

Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam

Maniphest Tasks: T13258, T11415

Differential Revision: https://secure.phabricator.com/D20220
This commit is contained in:
epriestley 2019-02-26 06:26:03 -08:00
parent ea6c0c9bde
commit d36d0efc35
11 changed files with 755 additions and 1 deletions

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildplan
ADD properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT};

View file

@ -0,0 +1,2 @@
UPDATE {$NAMESPACE}_harbormaster.harbormaster_buildplan
SET properties = '{}' WHERE properties = '';

View file

@ -1328,6 +1328,9 @@ phutil_register_library_map(array(
'HarbormasterBuildMessageQuery' => 'applications/harbormaster/query/HarbormasterBuildMessageQuery.php',
'HarbormasterBuildPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildPHIDType.php',
'HarbormasterBuildPlan' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php',
'HarbormasterBuildPlanBehavior' => 'applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php',
'HarbormasterBuildPlanBehaviorOption' => 'applications/harbormaster/plan/HarbormasterBuildPlanBehaviorOption.php',
'HarbormasterBuildPlanBehaviorTransaction' => 'applications/harbormaster/xaction/plan/HarbormasterBuildPlanBehaviorTransaction.php',
'HarbormasterBuildPlanDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildPlanDatasource.php',
'HarbormasterBuildPlanDefaultEditCapability' => 'applications/harbormaster/capability/HarbormasterBuildPlanDefaultEditCapability.php',
'HarbormasterBuildPlanDefaultViewCapability' => 'applications/harbormaster/capability/HarbormasterBuildPlanDefaultViewCapability.php',
@ -1424,6 +1427,7 @@ phutil_register_library_map(array(
'HarbormasterMessageType' => 'applications/harbormaster/engine/HarbormasterMessageType.php',
'HarbormasterObject' => 'applications/harbormaster/storage/HarbormasterObject.php',
'HarbormasterOtherBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterOtherBuildStepGroup.php',
'HarbormasterPlanBehaviorController' => 'applications/harbormaster/controller/HarbormasterPlanBehaviorController.php',
'HarbormasterPlanController' => 'applications/harbormaster/controller/HarbormasterPlanController.php',
'HarbormasterPlanDisableController' => 'applications/harbormaster/controller/HarbormasterPlanDisableController.php',
'HarbormasterPlanEditController' => 'applications/harbormaster/controller/HarbormasterPlanEditController.php',
@ -6942,6 +6946,9 @@ phutil_register_library_map(array(
'PhabricatorConduitResultInterface',
'PhabricatorProjectInterface',
),
'HarbormasterBuildPlanBehavior' => 'Phobject',
'HarbormasterBuildPlanBehaviorOption' => 'Phobject',
'HarbormasterBuildPlanBehaviorTransaction' => 'HarbormasterBuildPlanTransactionType',
'HarbormasterBuildPlanDatasource' => 'PhabricatorTypeaheadDatasource',
'HarbormasterBuildPlanDefaultEditCapability' => 'PhabricatorPolicyCapability',
'HarbormasterBuildPlanDefaultViewCapability' => 'PhabricatorPolicyCapability',
@ -7057,6 +7064,7 @@ phutil_register_library_map(array(
'HarbormasterMessageType' => 'Phobject',
'HarbormasterObject' => 'HarbormasterDAO',
'HarbormasterOtherBuildStepGroup' => 'HarbormasterBuildStepGroup',
'HarbormasterPlanBehaviorController' => 'HarbormasterPlanController',
'HarbormasterPlanController' => 'HarbormasterController',
'HarbormasterPlanDisableController' => 'HarbormasterPlanController',
'HarbormasterPlanEditController' => 'HarbormasterPlanController',

View file

@ -83,6 +83,8 @@ final class PhabricatorHarbormasterApplication extends PhabricatorApplication {
=> 'HarbormasterPlanEditController',
'order/(?:(?P<id>\d+)/)?' => 'HarbormasterPlanOrderController',
'disable/(?P<id>\d+)/' => 'HarbormasterPlanDisableController',
'behavior/(?P<id>\d+)/(?P<behaviorKey>[^/]+)/' =>
'HarbormasterPlanBehaviorController',
'run/(?P<id>\d+)/' => 'HarbormasterPlanRunController',
'(?P<id>\d+)/' => 'HarbormasterPlanViewController',
),

View file

@ -0,0 +1,92 @@
<?php
final class HarbormasterPlanBehaviorController
extends HarbormasterPlanController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$plan = id(new HarbormasterBuildPlanQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$plan) {
return new Aphront404Response();
}
$behavior_key = $request->getURIData('behaviorKey');
$metadata_key = HarbormasterBuildPlanBehavior::getTransactionMetadataKey();
$behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors();
$behavior = idx($behaviors, $behavior_key);
if (!$behavior) {
return new Aphront404Response();
}
$plan_uri = $plan->getURI();
$v_option = $behavior->getPlanOption($plan)->getKey();
if ($request->isFormPost()) {
$v_option = $request->getStr('option');
$xactions = array();
$xactions[] = id(new HarbormasterBuildPlanTransaction())
->setTransactionType(
HarbormasterBuildPlanBehaviorTransaction::TRANSACTIONTYPE)
->setMetadataValue($metadata_key, $behavior_key)
->setNewValue($v_option);
$editor = id(new HarbormasterBuildPlanEditor())
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setContentSourceFromRequest($request);
$editor->applyTransactions($plan, $xactions);
return id(new AphrontRedirectResponse())->setURI($plan_uri);
}
$select_control = id(new AphrontFormRadioButtonControl())
->setName('option')
->setValue($v_option)
->setLabel(pht('Option'));
foreach ($behavior->getOptions() as $option) {
$icon = id(new PHUIIconView())
->setIcon($option->getIcon());
$select_control->addButton(
$option->getKey(),
array(
$icon,
' ',
$option->getName(),
),
$option->getDescription());
}
$form = id(new AphrontFormView())
->setViewer($viewer)
->appendInstructions(
pht(
'Choose a build plan behavior for "%s".',
phutil_tag('strong', array(), $behavior->getName())))
->appendRemarkupInstructions($behavior->getEditInstructions())
->appendControl($select_control);
return $this->newDialog()
->setTitle(pht('Edit Behavior: %s', $behavior->getName()))
->appendForm($form)
->setWidth(AphrontDialogView::WIDTH_FORM)
->addSubmitButton(pht('Save Changes'))
->addCancelButton($plan_uri);
}
}

View file

@ -61,6 +61,7 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController {
}
$builds_view = $this->newBuildsView($plan);
$options_view = $this->newOptionsView($plan);
$timeline = $this->buildTransactionTimeline(
$plan,
@ -74,6 +75,7 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController {
array(
$error,
$step_list,
$options_view,
$builds_view,
$timeline,
));
@ -484,4 +486,75 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController {
->appendChild($list);
}
private function newOptionsView(HarbormasterBuildPlan $plan) {
$viewer = $this->getViewer();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$plan,
PhabricatorPolicyCapability::CAN_EDIT);
$behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors();
$rows = array();
foreach ($behaviors as $behavior) {
$option = $behavior->getPlanOption($plan);
$icon = $option->getIcon();
$icon = id(new PHUIIconView())->setIcon($icon);
$edit_uri = new PhutilURI(
$this->getApplicationURI(
urisprintf(
'plan/behavior/%d/%s/',
$plan->getID(),
$behavior->getKey())));
$edit_button = id(new PHUIButtonView())
->setTag('a')
->setColor(PHUIButtonView::GREY)
->setSize(PHUIButtonView::SMALL)
->setDisabled(!$can_edit)
->setWorkflow(true)
->setText(pht('Edit'))
->setHref($edit_uri);
$rows[] = array(
$icon,
$behavior->getName(),
$option->getName(),
$option->getDescription(),
$edit_button,
);
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
null,
pht('Name'),
pht('Behavior'),
pht('Details'),
null,
))
->setColumnClasses(
array(
null,
'pri',
null,
'wide',
null,
));
$header = id(new PHUIHeaderView())
->setHeader(pht('Plan Behaviors'));
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($table);
}
}

View file

@ -77,7 +77,7 @@ final class HarbormasterBuildPlanEditEngine
}
protected function buildCustomEditFields($object) {
return array(
$fields = array(
id(new PhabricatorTextEditField())
->setKey('name')
->setLabel(pht('Name'))
@ -89,6 +89,36 @@ final class HarbormasterBuildPlanEditEngine
->setConduitTypeDescription(pht('New plan name.'))
->setValue($object->getName()),
);
$metadata_key = HarbormasterBuildPlanBehavior::getTransactionMetadataKey();
$behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors();
foreach ($behaviors as $behavior) {
$key = $behavior->getKey();
// Get the raw key off the object so that we don't reset stuff to
// default values by mistake if a behavior goes missing somehow.
$storage_key = HarbormasterBuildPlanBehavior::getStorageKeyForBehaviorKey(
$key);
$behavior_option = $object->getPlanProperty($storage_key);
if (!strlen($behavior_option)) {
$behavior_option = $behavior->getPlanOption($object)->getKey();
}
$fields[] = id(new PhabricatorSelectEditField())
->setIsFormField(false)
->setKey(sprintf('behavior.%s', $behavior->getKey()))
->setMetadataValue($metadata_key, $behavior->getKey())
->setLabel(pht('Behavior: %s', $behavior->getName()))
->setTransactionType(
HarbormasterBuildPlanBehaviorTransaction::TRANSACTIONTYPE)
->setValue($behavior_option)
->setOptions($behavior->getOptionMap());
}
return $fields;
}
}

View file

@ -0,0 +1,348 @@
<?php
final class HarbormasterBuildPlanBehavior
extends Phobject {
private $key;
private $name;
private $options;
private $defaultKey;
private $editInstructions;
public function setKey($key) {
$this->key = $key;
return $this;
}
public function getKey() {
return $this->key;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setEditInstructions($edit_instructions) {
$this->editInstructions = $edit_instructions;
return $this;
}
public function getEditInstructions() {
return $this->editInstructions;
}
public function getOptionMap() {
return mpull($this->options, 'getName', 'getKey');
}
public function setOptions(array $options) {
assert_instances_of($options, 'HarbormasterBuildPlanBehaviorOption');
$key_map = array();
$default = null;
foreach ($options as $option) {
$key = $option->getKey();
if (isset($key_map[$key])) {
throw new Exception(
pht(
'Multiple behavior options (for behavior "%s") have the same '.
'key ("%s"). Each option must have a unique key.',
$this->getKey(),
$key));
}
$key_map[$key] = true;
if ($option->getIsDefault()) {
if ($default === null) {
$default = $key;
} else {
throw new Exception(
pht(
'Multiple behavior options (for behavior "%s") are marked as '.
'default options ("%s" and "%s"). Exactly one option must be '.
'marked as the default option.',
$this->getKey(),
$default,
$key));
}
}
}
if ($default === null) {
throw new Exception(
pht(
'No behavior option is marked as the default option (for '.
'behavior "%s"). Exactly one option must be marked as the '.
'default option.',
$this->getKey()));
}
$this->options = mpull($options, null, 'getKey');
$this->defaultKey = $default;
return $this;
}
public function getOptions() {
return $this->options;
}
public function getPlanOption(HarbormasterBuildPlan $plan) {
$behavior_key = $this->getKey();
$storage_key = self::getStorageKeyForBehaviorKey($behavior_key);
$plan_value = $plan->getPlanProperty($storage_key);
if (isset($this->options[$plan_value])) {
return $this->options[$plan_value];
}
return idx($this->options, $this->defaultKey);
}
public static function getTransactionMetadataKey() {
return 'behavior-key';
}
public static function getStorageKeyForBehaviorKey($behavior_key) {
return sprintf('behavior.%s', $behavior_key);
}
public static function newPlanBehaviors() {
$draft_options = array(
id(new HarbormasterBuildPlanBehaviorOption())
->setKey('always')
->setIcon('fa-check-circle-o green')
->setName(pht('Always'))
->setIsDefault(true)
->setDescription(
pht(
'Revisions are not sent for review until the build completes, '.
'and are returned to the author for updates if the build fails.')),
id(new HarbormasterBuildPlanBehaviorOption())
->setKey('building')
->setIcon('fa-pause-circle-o yellow')
->setName(pht('If Building'))
->setDescription(
pht(
'Revisions are not sent for review until the build completes, '.
'but they will be sent for review even if it fails.')),
id(new HarbormasterBuildPlanBehaviorOption())
->setKey('never')
->setIcon('fa-circle-o red')
->setName(pht('Never'))
->setDescription(
pht(
'Revisions are sent for review regardless of the status of the '.
'build.')),
);
$land_options = array(
id(new HarbormasterBuildPlanBehaviorOption())
->setKey('always')
->setIcon('fa-check-circle-o green')
->setName(pht('Always'))
->setIsDefault(true)
->setDescription(
pht(
'"arc land" warns if the build is still running or has '.
'failed.')),
id(new HarbormasterBuildPlanBehaviorOption())
->setKey('building')
->setIcon('fa-pause-circle-o yellow')
->setName(pht('If Building'))
->setDescription(
pht(
'"arc land" warns if the build is still running, but ignores '.
'the build if it has failed.')),
id(new HarbormasterBuildPlanBehaviorOption())
->setKey('complete')
->setIcon('fa-dot-circle-o yellow')
->setName(pht('If Complete'))
->setDescription(
pht(
'"arc land" warns if the build has failed, but ignores the '.
'build if it is still running.')),
id(new HarbormasterBuildPlanBehaviorOption())
->setKey('never')
->setIcon('fa-circle-o red')
->setName(pht('Never'))
->setDescription(
pht(
'"arc land" never warns that the build is still running or '.
'has failed.')),
);
$aggregate_options = array(
id(new HarbormasterBuildPlanBehaviorOption())
->setKey('always')
->setIcon('fa-check-circle-o green')
->setName(pht('Always'))
->setIsDefault(true)
->setDescription(
pht(
'The buildable waits for the build, and fails if the '.
'build fails.')),
id(new HarbormasterBuildPlanBehaviorOption())
->setKey('building')
->setIcon('fa-pause-circle-o yellow')
->setName(pht('If Building'))
->setDescription(
pht(
'The buildable waits for the build, but does not fail '.
'if the build fails.')),
id(new HarbormasterBuildPlanBehaviorOption())
->setKey('never')
->setIcon('fa-circle-o red')
->setName(pht('Never'))
->setDescription(
pht(
'The buildable does not wait for the build.')),
);
$restart_options = array(
id(new HarbormasterBuildPlanBehaviorOption())
->setKey('always')
->setIcon('fa-repeat green')
->setName(pht('Always'))
->setIsDefault(true)
->setDescription(
pht('The build may be restarted.')),
id(new HarbormasterBuildPlanBehaviorOption())
->setKey('never')
->setIcon('fa-times red')
->setName(pht('Never'))
->setDescription(
pht('The build may not be restarted.')),
);
$run_options = array(
id(new HarbormasterBuildPlanBehaviorOption())
->setKey('edit')
->setIcon('fa-pencil green')
->setName(pht('If Editable'))
->setIsDefault(true)
->setDescription(
pht('Only users who can edit the plan can run it manually.')),
id(new HarbormasterBuildPlanBehaviorOption())
->setKey('view')
->setIcon('fa-exclamation-triangle yellow')
->setName(pht('If Viewable'))
->setDescription(
pht(
'Any user who can view the plan can run it manually.')),
);
$behaviors = array(
id(new self())
->setKey('hold-drafts')
->setName(pht('Hold Drafts'))
->setEditInstructions(
pht(
'When users create revisions in Differential, the default '.
'behavior is to hold them in the "Draft" state until all builds '.
'pass. Once builds pass, the revisions promote and are sent for '.
'review, which notifies reviewers.'.
"\n\n".
'The general intent of this workflow is to make sure reviewers '.
'are only spending time on review once changes survive automated '.
'tests. If a change does not pass tests, it usually is not '.
'really ready for review.'.
"\n\n".
'If you want to promote revisions out of "Draft" before builds '.
'pass, or promote revisions even when builds fail, you can '.
'change the promotion behavior. This may be useful if you have '.
'very long-running builds, or some builds which are not very '.
'important.'.
"\n\n".
'Users may always use "Request Review" to promote a "Draft" '.
'revision, even if builds have failed or are still in progress.'))
->setOptions($draft_options),
id(new self())
->setKey('arc-land')
->setName(pht('Warn When Landing'))
->setEditInstructions(
pht(
'When a user attempts to `arc land` a revision and that revision '.
'has ongoing or failed builds, the default behavior of `arc` is '.
'to warn them about those builds and give them a chance to '.
'reconsider: they may want to wait for ongoing builds to '.
'complete, or fix failed builds before landing the change.'.
"\n\n".
'If you do not want to warn users about this build, you can '.
'change the warning behavior. This may be useful if the build '.
'takes a long time to run (so you do not expect users to wait '.
'for it) or the outcome is not important.'.
"\n\n".
'This warning is only advisory. Users may always elect to ignore '.
'this warning and continue, even if builds have failed.'))
->setOptions($land_options),
id(new self())
->setKey('buildable')
->setEditInstructions(
pht(
'The overall state of a buildable (like a commit or revision) is '.
'normally the aggregation of the individual states of all builds '.
'that have run against it.'.
"\n\n".
'Buildables are "building" until all builds pass (which changes '.
'them to "pass"), or any build fails (which changes them to '.
'"fail").'.
"\n\n".
'You can change this behavior if you do not want to wait for this '.
'build, or do not care if it fails.'))
->setName(pht('Affects Buildable'))
->setOptions($aggregate_options),
id(new self())
->setKey('restartable')
->setEditInstructions(
pht(
'Usually, builds may be restarted. This may be useful if you '.
'suspect a build has failed for environmental or circumstantial '.
'reasons unrelated to the actual code, and want to give it '.
'another chance at glory.'.
"\n\n".
'If you want to prevent a build from being restarted, you can '.
'change the behavior here. This may be useful to prevent '.
'accidents where a build with a dangerous side effect (like '.
'deployment) is restarted improperly.'))
->setName(pht('Restartable'))
->setOptions($restart_options),
id(new self())
->setKey('runnable')
->setEditInstructions(
pht(
'To run a build manually, you normally must have permission to '.
'edit the related build plan. If you would prefer that anyone who '.
'can see the build plan be able to run and restart the build, you '.
'can change the behavior here.'.
"\n\n".
'Note that this affects both {nav Run Plan Manually} and '.
'{nav Restart Build}, since the two actions are largely '.
'equivalent.'.
"\n\n".
'WARNING: This may be unsafe, particularly if the build has '.
'side effects like deployment.'.
"\n\n".
'If you weaken this policy, an attacker with control of an '.
'account that has "Can View" permission but not "Can Edit" '.
'permission can manually run this build against any old version '.
'of the code, including versions with known security issues.'.
"\n\n".
'If running the build has a side effect like deploying code, '.
'they can force deployment of a vulnerable version and then '.
'escalate into an attack against the deployed service.'))
->setName(pht('Runnable'))
->setOptions($run_options),
);
return mpull($behaviors, null, 'getKey');
}
}

View file

@ -0,0 +1,57 @@
<?php
final class HarbormasterBuildPlanBehaviorOption
extends Phobject {
private $name;
private $key;
private $icon;
private $description;
private $isDefault;
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setKey($key) {
$this->key = $key;
return $this;
}
public function getKey() {
return $this->key;
}
public function setDescription($description) {
$this->description = $description;
return $this;
}
public function getDescription() {
return $this->description;
}
public function setIsDefault($is_default) {
$this->isDefault = $is_default;
return $this;
}
public function getIsDefault() {
return $this->isDefault;
}
public function setIcon($icon) {
$this->icon = $icon;
return $this;
}
public function getIcon() {
return $this->icon;
}
}

View file

@ -17,6 +17,7 @@ final class HarbormasterBuildPlan extends HarbormasterDAO
protected $planAutoKey;
protected $viewPolicy;
protected $editPolicy;
protected $properties = array();
const STATUS_ACTIVE = 'active';
const STATUS_DISABLED = 'disabled';
@ -45,6 +46,9 @@ final class HarbormasterBuildPlan extends HarbormasterDAO
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort128',
'planStatus' => 'text32',
@ -94,6 +98,15 @@ final class HarbormasterBuildPlan extends HarbormasterDAO
return pht('Plan %d', $this->getID());
}
public function getPlanProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setPlanProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
/* -( Autoplans )---------------------------------------------------------- */

View file

@ -0,0 +1,127 @@
<?php
final class HarbormasterBuildPlanBehaviorTransaction
extends HarbormasterBuildPlanTransactionType {
const TRANSACTIONTYPE = 'behavior';
public function generateOldValue($object) {
$behavior = $this->getBehavior();
return $behavior->getPlanOption($object)->getKey();
}
public function applyInternalEffects($object, $value) {
$key = $this->getStorageKey();
return $object->setPlanProperty($key, $value);
}
public function getTitle() {
$old_value = $this->getOldValue();
$new_value = $this->getNewValue();
$behavior = $this->getBehavior();
if ($behavior) {
$behavior_name = $behavior->getName();
$options = $behavior->getOptions();
if (isset($options[$old_value])) {
$old_value = $options[$old_value]->getName();
}
if (isset($options[$new_value])) {
$new_value = $options[$new_value]->getName();
}
} else {
$behavior_name = $this->getBehaviorKey();
}
return pht(
'%s changed the %s behavior for this plan from %s to %s.',
$this->renderAuthor(),
$this->renderValue($behavior_name),
$this->renderValue($old_value),
$this->renderValue($new_value));
}
public function validateTransactions($object, array $xactions) {
$errors = array();
$behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors();
$behaviors = mpull($behaviors, null, 'getKey');
foreach ($xactions as $xaction) {
$key = $this->getBehaviorKeyForTransaction($xaction);
if (!isset($behaviors[$key])) {
$errors[] = $this->newInvalidError(
pht(
'No behavior with key "%s" exists. Valid keys are: %s.',
$key,
implode(', ', array_keys($behaviors))),
$xaction);
continue;
}
$behavior = $behaviors[$key];
$options = $behavior->getOptions();
$storage_key = HarbormasterBuildPlanBehavior::getStorageKeyForBehaviorKey(
$key);
$old = $object->getPlanProperty($storage_key);
$new = $xaction->getNewValue();
if ($old === $new) {
continue;
}
if (!isset($options[$new])) {
$errors[] = $this->newInvalidError(
pht(
'Value "%s" is not a valid option for behavior "%s". Valid '.
'options are: %s.',
$new,
$key,
implode(', ', array_keys($options))),
$xaction);
continue;
}
}
return $errors;
}
public function getTransactionTypeForConduit($xaction) {
return 'behavior';
}
public function getFieldValuesForConduit($xaction, $data) {
return array(
'key' => $this->getBehaviorKeyForTransaction($xaction),
'old' => $xaction->getOldValue(),
'new' => $xaction->getNewValue(),
);
}
private function getBehaviorKeyForTransaction(
PhabricatorApplicationTransaction $xaction) {
$metadata_key = HarbormasterBuildPlanBehavior::getTransactionMetadataKey();
return $xaction->getMetadataValue($metadata_key);
}
private function getBehaviorKey() {
$metadata_key = HarbormasterBuildPlanBehavior::getTransactionMetadataKey();
return $this->getMetadataValue($metadata_key);
}
private function getBehavior() {
$behavior_key = $this->getBehaviorKey();
$behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors();
return idx($behaviors, $behavior_key);
}
private function getStorageKey() {
return HarbormasterBuildPlanBehavior::getStorageKeyForBehaviorKey(
$this->getBehaviorKey());
}
}