Use transactions to show edit history for Configuration

Summary: Use ApplicationTransactions in Config to create an edit history. Resolves T2256.

Test Plan: {F28477}

Reviewers: btrahan, codeblock

Reviewed By: codeblock

CC: aran

Maniphest Tasks: T2256

Differential Revision: https://secure.phabricator.com/D4314
This commit is contained in:
epriestley 2013-01-01 18:14:41 -08:00
parent 25ca17da46
commit 32e4a7a37f
10 changed files with 281 additions and 17 deletions

View file

@ -0,0 +1,20 @@
CREATE TABLE {$NAMESPACE}_config.config_transaction (
id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
phid VARCHAR(64) NOT NULL COLLATE utf8_bin,
authorPHID VARCHAR(64) NOT NULL COLLATE utf8_bin,
objectPHID VARCHAR(64) NOT NULL COLLATE utf8_bin,
viewPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin,
editPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin,
commentPHID VARCHAR(64) COLLATE utf8_bin,
commentVersion INT UNSIGNED NOT NULL,
transactionType VARCHAR(32) NOT NULL COLLATE utf8_bin,
oldValue LONGTEXT NOT NULL COLLATE utf8_bin,
newValue LONGTEXT NOT NULL COLLATE utf8_bin,
contentSource LONGTEXT NOT NULL COLLATE utf8_bin,
dateCreated INT UNSIGNED NOT NULL,
dateModified INT UNSIGNED NOT NULL,
UNIQUE KEY `key_phid` (phid),
KEY `key_object` (objectPHID)
) ENGINE=InnoDB, COLLATE utf8_general_ci;

View file

@ -687,6 +687,7 @@ phutil_register_library_map(array(
'PhabricatorConfigDefaultSource' => 'infrastructure/env/PhabricatorConfigDefaultSource.php', 'PhabricatorConfigDefaultSource' => 'infrastructure/env/PhabricatorConfigDefaultSource.php',
'PhabricatorConfigDictionarySource' => 'infrastructure/env/PhabricatorConfigDictionarySource.php', 'PhabricatorConfigDictionarySource' => 'infrastructure/env/PhabricatorConfigDictionarySource.php',
'PhabricatorConfigEditController' => 'applications/config/controller/PhabricatorConfigEditController.php', 'PhabricatorConfigEditController' => 'applications/config/controller/PhabricatorConfigEditController.php',
'PhabricatorConfigEditor' => 'applications/config/editor/PhabricatorConfigEditor.php',
'PhabricatorConfigEntry' => 'applications/config/storage/PhabricatorConfigEntry.php', 'PhabricatorConfigEntry' => 'applications/config/storage/PhabricatorConfigEntry.php',
'PhabricatorConfigEntryDAO' => 'applications/config/storage/PhabricatorConfigEntryDAO.php', 'PhabricatorConfigEntryDAO' => 'applications/config/storage/PhabricatorConfigEntryDAO.php',
'PhabricatorConfigFileSource' => 'infrastructure/env/PhabricatorConfigFileSource.php', 'PhabricatorConfigFileSource' => 'infrastructure/env/PhabricatorConfigFileSource.php',
@ -701,6 +702,8 @@ phutil_register_library_map(array(
'PhabricatorConfigProxySource' => 'infrastructure/env/PhabricatorConfigProxySource.php', 'PhabricatorConfigProxySource' => 'infrastructure/env/PhabricatorConfigProxySource.php',
'PhabricatorConfigSource' => 'infrastructure/env/PhabricatorConfigSource.php', 'PhabricatorConfigSource' => 'infrastructure/env/PhabricatorConfigSource.php',
'PhabricatorConfigStackSource' => 'infrastructure/env/PhabricatorConfigStackSource.php', 'PhabricatorConfigStackSource' => 'infrastructure/env/PhabricatorConfigStackSource.php',
'PhabricatorConfigTransaction' => 'applications/config/storage/PhabricatorConfigTransaction.php',
'PhabricatorConfigTransactionQuery' => 'applications/config/query/PhabricatorConfigTransactionQuery.php',
'PhabricatorContentSource' => 'applications/metamta/contentsource/PhabricatorContentSource.php', 'PhabricatorContentSource' => 'applications/metamta/contentsource/PhabricatorContentSource.php',
'PhabricatorContentSourceView' => 'applications/metamta/contentsource/PhabricatorContentSourceView.php', 'PhabricatorContentSourceView' => 'applications/metamta/contentsource/PhabricatorContentSourceView.php',
'PhabricatorController' => 'applications/base/controller/PhabricatorController.php', 'PhabricatorController' => 'applications/base/controller/PhabricatorController.php',
@ -2023,6 +2026,7 @@ phutil_register_library_map(array(
'PhabricatorConfigDefaultSource' => 'PhabricatorConfigProxySource', 'PhabricatorConfigDefaultSource' => 'PhabricatorConfigProxySource',
'PhabricatorConfigDictionarySource' => 'PhabricatorConfigSource', 'PhabricatorConfigDictionarySource' => 'PhabricatorConfigSource',
'PhabricatorConfigEditController' => 'PhabricatorConfigController', 'PhabricatorConfigEditController' => 'PhabricatorConfigController',
'PhabricatorConfigEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorConfigEntry' => 'PhabricatorConfigEntryDAO', 'PhabricatorConfigEntry' => 'PhabricatorConfigEntryDAO',
'PhabricatorConfigEntryDAO' => 'PhabricatorLiskDAO', 'PhabricatorConfigEntryDAO' => 'PhabricatorLiskDAO',
'PhabricatorConfigFileSource' => 'PhabricatorConfigProxySource', 'PhabricatorConfigFileSource' => 'PhabricatorConfigProxySource',
@ -2040,6 +2044,8 @@ phutil_register_library_map(array(
), ),
'PhabricatorConfigProxySource' => 'PhabricatorConfigSource', 'PhabricatorConfigProxySource' => 'PhabricatorConfigSource',
'PhabricatorConfigStackSource' => 'PhabricatorConfigSource', 'PhabricatorConfigStackSource' => 'PhabricatorConfigSource',
'PhabricatorConfigTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorContentSourceView' => 'AphrontView', 'PhabricatorContentSourceView' => 'AphrontView',
'PhabricatorController' => 'AphrontController', 'PhabricatorController' => 'AphrontController',
'PhabricatorCountdownController' => 'PhabricatorController', 'PhabricatorCountdownController' => 'PhabricatorController',

View file

@ -61,15 +61,26 @@ final class PhabricatorConfigEditController
$errors = array(); $errors = array();
if ($request->isFormPost()) { if ($request->isFormPost()) {
list($e_value, $value_errors, $display_value) = $this->readRequest( $result = $this->readRequest(
$option, $option,
$config_entry,
$request); $request);
list($e_value, $value_errors, $display_value, $xaction) = $result;
$errors = array_merge($errors, $value_errors); $errors = array_merge($errors, $value_errors);
if (!$errors) { if (!$errors) {
$config_entry->save();
$editor = id(new PhabricatorConfigEditor())
->setActor($user)
->setContinueOnNoEffect(true)
->setContentSource(
PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_WEB,
array(
'ip' => $request->getRemoteAddr(),
)))
->applyTransactions($config_entry, array($xaction));
return id(new AphrontRedirectResponse())->setURI($done_uri); return id(new AphrontRedirectResponse())->setURI($done_uri);
} }
} else { } else {
@ -148,12 +159,23 @@ final class PhabricatorConfigEditController
->setName($this->key) ->setName($this->key)
->setHref('/config/edit/'.$this->key)); ->setHref('/config/edit/'.$this->key));
$xactions = id(new PhabricatorConfigTransactionQuery())
->withObjectPHIDs(array($config_entry->getPHID()))
->setViewer($user)
->execute();
$xaction_view = id(new PhabricatorApplicationTransactionView())
->setUser($user)
->setTransactions($xactions);
return $this->buildApplicationPage( return $this->buildApplicationPage(
array( array(
$crumbs, $crumbs,
id(new PhabricatorHeaderView())->setHeader($title), id(new PhabricatorHeaderView())->setHeader($title),
$error_view, $error_view,
$form, $form,
$xaction_view,
), ),
array( array(
'title' => $title, 'title' => $title,
@ -163,41 +185,49 @@ final class PhabricatorConfigEditController
private function readRequest( private function readRequest(
PhabricatorConfigOption $option, PhabricatorConfigOption $option,
PhabricatorConfigEntry $entry,
AphrontRequest $request) { AphrontRequest $request) {
$xaction = new PhabricatorConfigTransaction();
$xaction->setTransactionType(PhabricatorConfigTransaction::TYPE_EDIT);
$e_value = null; $e_value = null;
$errors = array(); $errors = array();
$value = $request->getStr('value'); $value = $request->getStr('value');
if (!strlen($value)) { if (!strlen($value)) {
$value = null; $value = null;
$entry->setValue($value);
$entry->setIsDeleted(true); $xaction->setNewValue(
return array($e_value, $errors, $value); array(
} else { 'deleted' => true,
$entry->setIsDeleted(false); 'value' => null,
));
return array($e_value, $errors, $value, $xaction);
} }
$type = $option->getType(); $type = $option->getType();
$set_value = null;
switch ($type) { switch ($type) {
case 'int': case 'int':
if (preg_match('/^-?[0-9]+$/', trim($value))) { if (preg_match('/^-?[0-9]+$/', trim($value))) {
$entry->setValue((int)$value); $set_value = (int)$value;
} else { } else {
$e_value = pht('Invalid'); $e_value = pht('Invalid');
$errors[] = pht('Value must be an integer.'); $errors[] = pht('Value must be an integer.');
} }
break; break;
case 'string': case 'string':
$set_value = (string)$value;
break; break;
case 'bool': case 'bool':
switch ($value) { switch ($value) {
case 'true': case 'true':
$entry->setValue(true); $set_value = true;
break; break;
case 'false': case 'false':
$entry->setValue(false); $set_value = false;
break; break;
default: default:
$e_value = pht('Invalid'); $e_value = pht('Invalid');
@ -212,12 +242,22 @@ final class PhabricatorConfigEditController
$errors[] = pht( $errors[] = pht(
'The given value must be valid JSON. This means, among '. 'The given value must be valid JSON. This means, among '.
'other things, that you must wrap strings in double-quotes.'); 'other things, that you must wrap strings in double-quotes.');
$entry->setValue($json); $set_value = $json;
} }
break; break;
} }
return array($e_value, $errors, $value); if (!$errors) {
$xaction->setNewValue(
array(
'deleted' => false,
'value' => $set_value,
));
} else {
$xaction = null;
}
return array($e_value, $errors, $value, $xaction);
} }
private function getDisplayValue( private function getDisplayValue(

View file

@ -0,0 +1,90 @@
<?php
final class PhabricatorConfigEditor
extends PhabricatorApplicationTransactionEditor {
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorConfigTransaction::TYPE_EDIT;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorConfigTransaction::TYPE_EDIT:
return array(
'deleted' => (bool)$object->getIsDeleted(),
'value' => $object->getValue(),
);
}
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorConfigTransaction::TYPE_EDIT:
return $xaction->getNewValue();
}
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorConfigTransaction::TYPE_EDIT:
$v = $xaction->getNewValue();
$object->setIsDeleted($v['deleted']);
$object->setValue($v['value']);
break;
}
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
return;
}
protected function mergeTransactions(
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
$type = $u->getTransactionType();
switch ($type) {
case PhabricatorConfigTransaction::TYPE_EDIT:
return $v;
}
return parent::mergeTransactions($u, $v);
}
protected function transactionHasEffect(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$type = $xaction->getTransactionType();
switch ($type) {
case PhabricatorConfigTransaction::TYPE_EDIT:
// If an edit deletes an already-deleted entry, no-op it.
if (idx($old, 'deleted') && idx($new, 'deleted')) {
return false;
}
break;
}
return parent::transactionHasEffect($object, $xaction);
}
}

View file

@ -0,0 +1,10 @@
<?php
final class PhabricatorConfigTransactionQuery
extends PhabricatorApplicationTransactionQuery {
protected function getTemplateApplicationTransaction() {
return new PhabricatorConfigTransaction();
}
}

View file

@ -0,0 +1,94 @@
<?php
final class PhabricatorConfigTransaction
extends PhabricatorApplicationTransaction {
const TYPE_EDIT = 'config:edit';
public function getApplicationName() {
return 'config';
}
public function getApplicationTransactionType() {
return PhabricatorPHIDConstants::PHID_TYPE_CONF;
}
public function getApplicationTransactionCommentObject() {
return null;
}
public function getApplicationObjectTypeName() {
return pht('config');
}
public function getTitle() {
$author_phid = $this->getAuthorPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_EDIT:
// TODO: After T2213 show the actual values too; for now, we don't
// have the tools to do it without making a bit of a mess of it.
$old_del = idx($old, 'deleted');
$new_del = idx($new, 'deleted');
if ($old_del && !$new_del) {
return pht(
'%s created this configuration entry.',
$this->renderHandleLink($author_phid));
} else if (!$old_del && $new_del) {
return pht(
'%s deleted this configuration entry.',
$this->renderHandleLink($author_phid));
} else if ($old_del && $new_del) {
// This is a bug.
return pht(
'%s deleted this configuration entry (again?).',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s edited this configuration entry.',
$this->renderHandleLink($author_phid));
}
break;
}
return parent::getTitle();
}
public function getIcon() {
switch ($this->getTransactionType()) {
case self::TYPE_EDIT:
return 'edit';
}
return parent::getIcon();
}
public function getColor() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_EDIT:
$old_del = idx($old, 'deleted');
$new_del = idx($new, 'deleted');
if ($old_del && !$new_del) {
return PhabricatorTransactions::COLOR_GREEN;
} else if (!$old_del && $new_del) {
return PhabricatorTransactions::COLOR_RED;
} else {
return PhabricatorTransactions::COLOR_BLUE;
}
break;
}
}
}

View file

@ -6,6 +6,7 @@ final class PhabricatorMacroEditor
public function getTransactionTypes() { public function getTransactionTypes() {
$types = parent::getTransactionTypes(); $types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorMacroTransactionType::TYPE_NAME; $types[] = PhabricatorMacroTransactionType::TYPE_NAME;
$types[] = PhabricatorMacroTransactionType::TYPE_DISABLED; $types[] = PhabricatorMacroTransactionType::TYPE_DISABLED;
$types[] = PhabricatorMacroTransactionType::TYPE_FILE; $types[] = PhabricatorMacroTransactionType::TYPE_FILE;

View file

@ -8,6 +8,7 @@ final class PholioMockEditor extends PhabricatorApplicationTransactionEditor {
public function getTransactionTypes() { public function getTransactionTypes() {
$types = parent::getTransactionTypes(); $types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;

View file

@ -58,9 +58,7 @@ abstract class PhabricatorApplicationTransactionEditor
} }
public function getTransactionTypes() { public function getTransactionTypes() {
$types = array( $types = array();
PhabricatorTransactions::TYPE_COMMENT,
);
if ($this->object instanceof PhabricatorSubscribableInterface) { if ($this->object instanceof PhabricatorSubscribableInterface) {
$types[] = PhabricatorTransactions::TYPE_SUBSCRIBERS; $types[] = PhabricatorTransactions::TYPE_SUBSCRIBERS;

View file

@ -1072,6 +1072,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList {
'type' => 'sql', 'type' => 'sql',
'name' => $this->getPatchPath('20121226.config.sql'), 'name' => $this->getPatchPath('20121226.config.sql'),
), ),
'20130101.confxaction.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('20130101.confxaction.sql'),
),
); );
} }