diff --git a/resources/sql/patches/100.projectxaction.sql b/resources/sql/patches/100.projectxaction.sql
new file mode 100644
index 0000000000..76d51f6892
--- /dev/null
+++ b/resources/sql/patches/100.projectxaction.sql
@@ -0,0 +1,11 @@
+CREATE TABLE phabricator_project.project_transaction (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ projectID INT UNSIGNED NOT NULL,
+ authorPHID VARCHAR(64) BINARY NOT NULL,
+ transactionType VARCHAR(32) NOT NULL,
+ oldValue LONGBLOB NOT NULL,
+ newValue LONGBLOB NOT NULL,
+ dateCreated INT UNSIGNED NOT NULL,
+ dateModified INT UNSIGNED NOT NULL,
+ KEY (projectID)
+) ENGINE=InnoDB;
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index 2a011014a0..548dbfc977 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -485,6 +485,7 @@ phutil_register_library_map(array(
'PhabricatorFeedStoryDifferential' => 'applications/feed/story/differential',
'PhabricatorFeedStoryManiphest' => 'applications/feed/story/maniphest',
'PhabricatorFeedStoryPhriction' => 'applications/feed/story/phriction',
+ 'PhabricatorFeedStoryProject' => 'applications/feed/story/project',
'PhabricatorFeedStoryPublisher' => 'applications/feed/publisher',
'PhabricatorFeedStoryReference' => 'applications/feed/storage/storyreference',
'PhabricatorFeedStoryStatus' => 'applications/feed/story/status',
@@ -611,6 +612,7 @@ phutil_register_library_map(array(
'PhabricatorProject' => 'applications/project/storage/project',
'PhabricatorProjectAffiliation' => 'applications/project/storage/affiliation',
'PhabricatorProjectAffiliationEditController' => 'applications/project/controller/editaffiliation',
+ 'PhabricatorProjectConstants' => 'applications/project/constants/base',
'PhabricatorProjectController' => 'applications/project/controller/base',
'PhabricatorProjectCreateController' => 'applications/project/controller/create',
'PhabricatorProjectDAO' => 'applications/project/storage/base',
@@ -623,6 +625,8 @@ phutil_register_library_map(array(
'PhabricatorProjectQuery' => 'applications/project/query/project',
'PhabricatorProjectStatus' => 'applications/project/constants/status',
'PhabricatorProjectSubproject' => 'applications/project/storage/subproject',
+ 'PhabricatorProjectTransaction' => 'applications/project/storage/transaction',
+ 'PhabricatorProjectTransactionType' => 'applications/project/constants/transaction',
'PhabricatorRedirectController' => 'applications/base/controller/redirect',
'PhabricatorRefreshCSRFController' => 'applications/auth/controller/refresh',
'PhabricatorRemarkupRuleDifferential' => 'infrastructure/markup/remarkup/markuprule/differential',
@@ -1192,6 +1196,7 @@ phutil_register_library_map(array(
'PhabricatorFeedStoryDifferential' => 'PhabricatorFeedStory',
'PhabricatorFeedStoryManiphest' => 'PhabricatorFeedStory',
'PhabricatorFeedStoryPhriction' => 'PhabricatorFeedStory',
+ 'PhabricatorFeedStoryProject' => 'PhabricatorFeedStory',
'PhabricatorFeedStoryReference' => 'PhabricatorFeedDAO',
'PhabricatorFeedStoryStatus' => 'PhabricatorFeedStory',
'PhabricatorFeedStoryTypeConstants' => 'PhabricatorFeedConstants',
@@ -1304,6 +1309,8 @@ phutil_register_library_map(array(
'PhabricatorProjectProfileController' => 'PhabricatorProjectController',
'PhabricatorProjectProfileEditController' => 'PhabricatorProjectController',
'PhabricatorProjectSubproject' => 'PhabricatorProjectDAO',
+ 'PhabricatorProjectTransaction' => 'PhabricatorProjectDAO',
+ 'PhabricatorProjectTransactionType' => 'PhabricatorProjectConstants',
'PhabricatorRedirectController' => 'PhabricatorController',
'PhabricatorRefreshCSRFController' => 'PhabricatorAuthController',
'PhabricatorRemarkupRuleDifferential' => 'PhabricatorRemarkupRuleObjectName',
diff --git a/src/applications/feed/constants/story/PhabricatorFeedStoryTypeConstants.php b/src/applications/feed/constants/story/PhabricatorFeedStoryTypeConstants.php
index 4ab63dd596..0a9de66251 100644
--- a/src/applications/feed/constants/story/PhabricatorFeedStoryTypeConstants.php
+++ b/src/applications/feed/constants/story/PhabricatorFeedStoryTypeConstants.php
@@ -1,7 +1,7 @@
getStoryData()->getAuthorPHID(),
+ $this->getStoryData()->getValue('projectPHID'),
+ );
+ }
+
+ public function getRequiredObjectPHIDs() {
+ return array(
+ $this->getStoryData()->getAuthorPHID(),
+ );
+ }
+
+ public function renderView() {
+ $data = $this->getStoryData();
+
+ $view = new PhabricatorFeedStoryView();
+
+ $type = $data->getValue('type');
+ $old = $data->getValue('old');
+ $new = $data->getValue('new');
+ $proj = $this->getHandle($data->getValue('projectPHID'));
+ $auth = $this->getHandle($data->getAuthorPHID());
+
+ switch ($type) {
+ case PhabricatorProjectTransactionType::TYPE_NAME:
+ if (strlen($old)) {
+ $action = 'renamed project '.
+ ''.$proj->renderLink().''.
+ ' from '.
+ ''.phutil_escape_html($old).''.
+ ' to '.
+ ''.phutil_escape_html($new).'.';
+ } else {
+ $action = 'created project '.
+ ''.$proj->renderLink().''.
+ ' (as '.
+ ''.phutil_escape_html($new).').';
+ }
+ break;
+ default:
+ $action = 'updated project '.$proj->renderLink().'';
+ break;
+ }
+ $view->setTitle(''.$auth->renderLink().' '.$action);
+ $view->setOneLineStory(true);
+
+ return $view;
+ }
+
+}
diff --git a/src/applications/feed/story/project/__init__.php b/src/applications/feed/story/project/__init__.php
new file mode 100644
index 0000000000..457b290fdf
--- /dev/null
+++ b/src/applications/feed/story/project/__init__.php
@@ -0,0 +1,16 @@
+isFormPost()) {
try {
+ $xactions = array();
+ $xaction = new PhabricatorProjectTransaction();
+ $xaction->setTransactionType(
+ PhabricatorProjectTransactionType::TYPE_NAME);
+ $xaction->setNewValue($request->getStr('name'));
+ $xactions[] = $xaction;
+
$editor = new PhabricatorProjectEditor($project);
$editor->setUser($user);
- $editor->setName($request->getStr('name'));
- $editor->save();
+ $editor->applyTransactions($xactions);
} catch (PhabricatorProjectNameCollisionException $ex) {
$e_name = 'Not Unique';
$errors[] = $ex->getMessage();
diff --git a/src/applications/project/controller/create/__init__.php b/src/applications/project/controller/create/__init__.php
index e8dccac003..074eac7b97 100644
--- a/src/applications/project/controller/create/__init__.php
+++ b/src/applications/project/controller/create/__init__.php
@@ -10,11 +10,13 @@ phutil_require_module('phabricator', 'aphront/response/ajax');
phutil_require_module('phabricator', 'aphront/response/dialog');
phutil_require_module('phabricator', 'aphront/response/redirect');
phutil_require_module('phabricator', 'applications/project/constants/status');
+phutil_require_module('phabricator', 'applications/project/constants/transaction');
phutil_require_module('phabricator', 'applications/project/controller/base');
phutil_require_module('phabricator', 'applications/project/editor/project');
phutil_require_module('phabricator', 'applications/project/storage/affiliation');
phutil_require_module('phabricator', 'applications/project/storage/profile');
phutil_require_module('phabricator', 'applications/project/storage/project');
+phutil_require_module('phabricator', 'applications/project/storage/transaction');
phutil_require_module('phabricator', 'view/dialog');
phutil_require_module('phabricator', 'view/form/base');
phutil_require_module('phabricator', 'view/form/control/submit');
diff --git a/src/applications/project/controller/profileedit/PhabricatorProjectProfileEditController.php b/src/applications/project/controller/profileedit/PhabricatorProjectProfileEditController.php
index 265048a0f7..3f609692cb 100644
--- a/src/applications/project/controller/profileedit/PhabricatorProjectProfileEditController.php
+++ b/src/applications/project/controller/profileedit/PhabricatorProjectProfileEditController.php
@@ -58,10 +58,16 @@ class PhabricatorProjectProfileEditController
if ($request->isFormPost()) {
try {
+ $xactions = array();
+ $xaction = new PhabricatorProjectTransaction();
+ $xaction->setTransactionType(
+ PhabricatorProjectTransactionType::TYPE_NAME);
+ $xaction->setNewValue($request->getStr('name'));
+ $xactions[] = $xaction;
+
$editor = new PhabricatorProjectEditor($project);
$editor->setUser($user);
- $editor->setName($request->getStr('name'));
- $editor->save();
+ $editor->applyTransactions($xactions);
} catch (PhabricatorProjectNameCollisionException $ex) {
$e_name = 'Not Unique';
$errors[] = $ex->getMessage();
diff --git a/src/applications/project/controller/profileedit/__init__.php b/src/applications/project/controller/profileedit/__init__.php
index df824b9bb9..9bb8a8d434 100644
--- a/src/applications/project/controller/profileedit/__init__.php
+++ b/src/applications/project/controller/profileedit/__init__.php
@@ -12,11 +12,13 @@ phutil_require_module('phabricator', 'applications/files/storage/file');
phutil_require_module('phabricator', 'applications/files/transform');
phutil_require_module('phabricator', 'applications/phid/handle/data');
phutil_require_module('phabricator', 'applications/project/constants/status');
+phutil_require_module('phabricator', 'applications/project/constants/transaction');
phutil_require_module('phabricator', 'applications/project/controller/base');
phutil_require_module('phabricator', 'applications/project/editor/project');
phutil_require_module('phabricator', 'applications/project/storage/affiliation');
phutil_require_module('phabricator', 'applications/project/storage/profile');
phutil_require_module('phabricator', 'applications/project/storage/project');
+phutil_require_module('phabricator', 'applications/project/storage/transaction');
phutil_require_module('phabricator', 'infrastructure/celerity/api');
phutil_require_module('phabricator', 'infrastructure/javelin/api');
phutil_require_module('phabricator', 'infrastructure/javelin/markup');
diff --git a/src/applications/project/editor/project/PhabricatorProjectEditor.php b/src/applications/project/editor/project/PhabricatorProjectEditor.php
index a4338938dd..6865cd7fad 100644
--- a/src/applications/project/editor/project/PhabricatorProjectEditor.php
+++ b/src/applications/project/editor/project/PhabricatorProjectEditor.php
@@ -1,7 +1,7 @@
project = $project;
}
- public function setName($name) {
- $this->projectName = $name;
- return $this;
- }
-
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
- public function save() {
+ public function applyTransactions(array $transactions) {
if (!$this->user) {
throw new Exception('Call setUser() before save()!');
}
+ $user = $this->user;
$project = $this->project;
$is_new = !$project->getID();
if ($is_new) {
- $project->setAuthorPHID($this->user->getPHID());
+ $project->setAuthorPHID($user->getPHID());
}
- if (($this->projectName !== null) &&
- ($this->projectName !== $project->getName())) {
- $project->setName($this->projectName);
- $project->setPhrictionSlug($this->projectName);
- $this->validateName($project);
+ foreach ($transactions as $xaction) {
+ $type = $xaction->getTransactionType();
+
+ $this->setTransactionOldValue($project, $xaction);
+ $this->applyTransactionEffect($project, $xaction);
+
}
try {
$project->save();
+
+ foreach ($transactions as $xaction) {
+ $xaction->setAuthorPHID($user->getPHID());
+ $xaction->setProjectID($project->getID());
+ $xaction->save();
+ }
+
+ foreach ($this->remAffiliations as $affil) {
+ $affil->delete();
+ }
+
+ foreach ($this->addAffiliations as $affil) {
+ $affil->setProjectPHID($project->getPHID());
+ $affil->save();
+ }
+
+ foreach ($transactions as $xaction) {
+ $this->publishTransactionStory($project, $xaction);
+ }
+
} catch (AphrontQueryDuplicateKeyException $ex) {
// We already validated the slug, but might race. Try again to see if
// that's the issue. If it is, we'll throw a more specific exception. If
@@ -100,4 +119,97 @@ final class PhabricatorProjectEditor {
}
}
+ private function setTransactionOldValue(
+ PhabricatorProject $project,
+ PhabricatorProjectTransaction $xaction) {
+
+ $type = $xaction->getTransactionType();
+ switch ($type) {
+ case PhabricatorProjectTransactionType::TYPE_NAME:
+ $xaction->setOldValue($project->getName());
+ break;
+ case PhabricatorProjectTransactionType::TYPE_MEMBERS:
+ $affils = $project->loadAffiliations();
+ $project->attachAffiliations($affils);
+
+ $old_value = mpull($affils, 'getUserPHID');
+ $old_value = array_values($old_value);
+ $xaction->setOldValue($affils);
+
+ $new_value = $xaction->getNewValue();
+ $new_value = array_filter($new_value);
+ $new_value = array_unique($new_value);
+ $new_value = array_values($new_value);
+ $xaction->setNewValue($new_value);
+ break;
+ default:
+ throw new Exception("Unknown transaction type '{$type}'!");
+ }
+ }
+
+ private function applyTransactionEffect(
+ PhabricatorProject $project,
+ PhabricatorProjectTransaction $xaction) {
+
+ $type = $xaction->getTransactionType();
+ switch ($type) {
+ case PhabricatorProjectTransactionType::TYPE_NAME:
+ $project->setName($xaction->getNewValue());
+ $project->setPhrictionSlug($xaction->getNewValue());
+ $this->validateName($project);
+ break;
+ case PhabricatorProjectTransactionType::TYPE_MEMBERS:
+ $old = array_fill_keys($xaction->getOldValue(), true);
+ $new = array_fill_keys($xaction->getNewValue(), true);
+
+ $add = array();
+ $rem = array();
+
+ foreach ($project->getAffiliations() as $affil) {
+ if (empty($new[$affil->getUserPHID()])) {
+ $rem[] = $affil;
+ }
+ }
+
+ foreach ($new as $phid => $ignored) {
+ if (empty($old[$phid])) {
+ $affil = new PhabricatorProjectAffiliation();
+ $affil->setUserPHID($phid);
+ $add[] = $affil;
+ }
+ }
+
+ $this->addAffiliations = $add;
+ $this->remAffiliations = $rem;
+ break;
+ default:
+ throw new Exception("Unknown transaction type '{$type}'!");
+ }
+ }
+
+ private function publishTransactionStory(
+ PhabricatorProject $project,
+ PhabricatorProjectTransaction $xaction) {
+
+ $related_phids = array(
+ $project->getPHID(),
+ $xaction->getAuthorPHID(),
+ );
+
+ id(new PhabricatorFeedStoryPublisher())
+ ->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_PROJECT)
+ ->setStoryData(
+ array(
+ 'projectPHID' => $project->getPHID(),
+ 'transactionID' => $xaction->getID(),
+ 'type' => $xaction->getTransactionType(),
+ 'old' => $xaction->getOldValue(),
+ 'new' => $xaction->getNewValue(),
+ ))
+ ->setStoryTime(time())
+ ->setStoryAuthorPHID($xaction->getAuthorPHID())
+ ->setRelatedPHIDs($related_phids)
+ ->publish();
+ }
+
}
diff --git a/src/applications/project/editor/project/__init__.php b/src/applications/project/editor/project/__init__.php
index 03b4a3c276..062ebe358d 100644
--- a/src/applications/project/editor/project/__init__.php
+++ b/src/applications/project/editor/project/__init__.php
@@ -6,7 +6,11 @@
+phutil_require_module('phabricator', 'applications/feed/constants/story');
+phutil_require_module('phabricator', 'applications/feed/publisher');
+phutil_require_module('phabricator', 'applications/project/constants/transaction');
phutil_require_module('phabricator', 'applications/project/exception/namecollison');
+phutil_require_module('phabricator', 'applications/project/storage/affiliation');
phutil_require_module('phabricator', 'applications/project/storage/project');
phutil_require_module('phutil', 'utils');
diff --git a/src/applications/project/storage/transaction/PhabricatorProjectTransaction.php b/src/applications/project/storage/transaction/PhabricatorProjectTransaction.php
new file mode 100644
index 0000000000..fad1338d66
--- /dev/null
+++ b/src/applications/project/storage/transaction/PhabricatorProjectTransaction.php
@@ -0,0 +1,39 @@
+ array(
+ 'oldValue' => self::SERIALIZATION_JSON,
+ 'newValue' => self::SERIALIZATION_JSON,
+ ),
+ ) + parent::getConfiguration();
+ }
+
+}
diff --git a/src/applications/project/storage/transaction/__init__.php b/src/applications/project/storage/transaction/__init__.php
new file mode 100644
index 0000000000..6207a6fa8d
--- /dev/null
+++ b/src/applications/project/storage/transaction/__init__.php
@@ -0,0 +1,12 @@
+