Index: resources/sql/patches/20131208.phragmentsnapshot.sql =================================================================== --- /dev/null +++ resources/sql/patches/20131208.phragmentsnapshot.sql @@ -0,0 +1,21 @@ +CREATE TABLE {$NAMESPACE}_phragment.phragment_snapshot ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + primaryFragmentPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + name VARCHAR(192) NOT NULL COLLATE utf8_bin, + description LONGTEXT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid), + UNIQUE KEY `key_name` (primaryFragmentPHID, name) +) ENGINE=InnoDB, COLLATE utf8_general_ci; + +CREATE TABLE {$NAMESPACE}_phragment.phragment_snapshotchild ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + snapshotPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + fragmentPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + fragmentVersionPHID VARCHAR(64) NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_child` (snapshotPHID, fragmentPHID, fragmentVersionPHID) +) ENGINE=InnoDB, COLLATE utf8_general_ci; Index: src/__phutil_library_map__.php =================================================================== --- src/__phutil_library_map__.php +++ src/__phutil_library_map__.php @@ -2186,9 +2186,18 @@ 'PhragmentHistoryController' => 'applications/phragment/controller/PhragmentHistoryController.php', 'PhragmentPHIDTypeFragment' => 'applications/phragment/phid/PhragmentPHIDTypeFragment.php', 'PhragmentPHIDTypeFragmentVersion' => 'applications/phragment/phid/PhragmentPHIDTypeFragmentVersion.php', + 'PhragmentPHIDTypeSnapshot' => 'applications/phragment/phid/PhragmentPHIDTypeSnapshot.php', 'PhragmentPatchController' => 'applications/phragment/controller/PhragmentPatchController.php', 'PhragmentPatchUtil' => 'applications/phragment/util/PhragmentPatchUtil.php', 'PhragmentRevertController' => 'applications/phragment/controller/PhragmentRevertController.php', + 'PhragmentSnapshot' => 'applications/phragment/storage/PhragmentSnapshot.php', + 'PhragmentSnapshotChild' => 'applications/phragment/storage/PhragmentSnapshotChild.php', + 'PhragmentSnapshotChildQuery' => 'applications/phragment/query/PhragmentSnapshotChildQuery.php', + 'PhragmentSnapshotCreateController' => 'applications/phragment/controller/PhragmentSnapshotCreateController.php', + 'PhragmentSnapshotDeleteController' => 'applications/phragment/controller/PhragmentSnapshotDeleteController.php', + 'PhragmentSnapshotPromoteController' => 'applications/phragment/controller/PhragmentSnapshotPromoteController.php', + 'PhragmentSnapshotQuery' => 'applications/phragment/query/PhragmentSnapshotQuery.php', + 'PhragmentSnapshotViewController' => 'applications/phragment/controller/PhragmentSnapshotViewController.php', 'PhragmentUpdateController' => 'applications/phragment/controller/PhragmentUpdateController.php', 'PhragmentVersionController' => 'applications/phragment/controller/PhragmentVersionController.php', 'PhragmentZIPController' => 'applications/phragment/controller/PhragmentZIPController.php', @@ -4789,9 +4798,26 @@ 'PhragmentHistoryController' => 'PhragmentController', 'PhragmentPHIDTypeFragment' => 'PhabricatorPHIDType', 'PhragmentPHIDTypeFragmentVersion' => 'PhabricatorPHIDType', + 'PhragmentPHIDTypeSnapshot' => 'PhabricatorPHIDType', 'PhragmentPatchController' => 'PhragmentController', 'PhragmentPatchUtil' => 'Phobject', 'PhragmentRevertController' => 'PhragmentController', + 'PhragmentSnapshot' => + array( + 0 => 'PhragmentDAO', + 1 => 'PhabricatorPolicyInterface', + ), + 'PhragmentSnapshotChild' => + array( + 0 => 'PhragmentDAO', + 1 => 'PhabricatorPolicyInterface', + ), + 'PhragmentSnapshotChildQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhragmentSnapshotCreateController' => 'PhragmentController', + 'PhragmentSnapshotDeleteController' => 'PhragmentController', + 'PhragmentSnapshotPromoteController' => 'PhragmentController', + 'PhragmentSnapshotQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhragmentSnapshotViewController' => 'PhragmentController', 'PhragmentUpdateController' => 'PhragmentController', 'PhragmentVersionController' => 'PhragmentController', 'PhragmentZIPController' => 'PhragmentController', Index: src/applications/phragment/application/PhabricatorApplicationPhragment.php =================================================================== --- src/applications/phragment/application/PhabricatorApplicationPhragment.php +++ src/applications/phragment/application/PhabricatorApplicationPhragment.php @@ -39,9 +39,19 @@ 'update/(?P.*)' => 'PhragmentUpdateController', 'history/(?P.*)' => 'PhragmentHistoryController', 'zip/(?P.*)' => 'PhragmentZIPController', + 'zip@(?P[^/]+)/(?P.*)' => 'PhragmentZIPController', 'version/(?P[0-9]*)/' => 'PhragmentVersionController', 'patch/(?P[0-9x]*)/(?P[0-9]*)/' => 'PhragmentPatchController', 'revert/(?P[0-9]*)/(?P.*)' => 'PhragmentRevertController', + 'snapshot/' => array( + 'create/(?P.*)' => 'PhragmentSnapshotCreateController', + 'view/(?P[0-9]*)/' => 'PhragmentSnapshotViewController', + 'delete/(?P[0-9]*)/' => 'PhragmentSnapshotDeleteController', + 'promote/' => array( + 'latest/(?P.*)' => 'PhragmentSnapshotPromoteController', + '(?P[0-9]*)/' => 'PhragmentSnapshotPromoteController', + ), + ), ), ); } Index: src/applications/phragment/controller/PhragmentBrowseController.php =================================================================== --- src/applications/phragment/controller/PhragmentBrowseController.php +++ src/applications/phragment/controller/PhragmentBrowseController.php @@ -56,7 +56,7 @@ foreach ($fragments as $fragment) { $item = id(new PHUIObjectItemView()); $item->setHeader($fragment->getName()); - $item->setHref($this->getApplicationURI('/browse/'.$fragment->getPath())); + $item->setHref($fragment->getURI()); if (!$fragment->isDirectory()) { $item->addAttribute(pht( 'Last Updated %s', Index: src/applications/phragment/controller/PhragmentController.php =================================================================== --- src/applications/phragment/controller/PhragmentController.php +++ src/applications/phragment/controller/PhragmentController.php @@ -64,6 +64,16 @@ $phids = array(); $phids[] = $fragment->getLatestVersionPHID(); + $snapshot_phids = array(); + $snapshots = id(new PhragmentSnapshotQuery()) + ->setViewer($viewer) + ->withPrimaryFragmentPHIDs(array($fragment->getPHID())) + ->execute(); + foreach ($snapshots as $snapshot) { + $phids[] = $snapshot->getPHID(); + $snapshot_phids[] = $snapshot->getPHID(); + } + $this->loadHandles($phids); $file = null; @@ -127,6 +137,21 @@ ->setHref($this->getApplicationURI("history/".$fragment->getPath())) ->setIcon('history')); } + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Create Snapshot')) + ->setHref($this->getApplicationURI( + "snapshot/create/".$fragment->getPath())) + ->setDisabled(false) // TODO: Policy + ->setIcon('snapshot')); + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Promote Snapshot to Here')) + ->setHref($this->getApplicationURI( + "snapshot/promote/latest/".$fragment->getPath())) + ->setWorkflow(true) + ->setDisabled(false) // TODO: Policy + ->setIcon('promote')); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) @@ -152,6 +177,12 @@ pht('Directory')); } + if (count($snapshot_phids) > 0) { + $properties->addProperty( + pht('Snapshots'), + $this->renderHandlesForPHIDs($snapshot_phids)); + } + return id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); Index: src/applications/phragment/controller/PhragmentSnapshotCreateController.php =================================================================== --- /dev/null +++ src/applications/phragment/controller/PhragmentSnapshotCreateController.php @@ -0,0 +1,170 @@ +dblob = idx($data, "dblob", ""); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $parents = $this->loadParentFragments($this->dblob); + if ($parents === null) { + return new Aphront404Response(); + } + $fragment = nonempty(last($parents), null); + if ($fragment === null) { + return new Aphront404Response(); + } + + $children = id(new PhragmentFragmentQuery()) + ->setViewer($viewer) + ->needLatestVersion(true) + ->withLeadingPath($fragment->getPath().'/') + ->execute(); + + $error_view = null; + + if ($request->isFormPost()) { + $errors = array(); + + $v_name = $request->getStr('name'); + if (strlen($v_name) === 0) { + $errors[] = pht('You must specify a name.'); + } + if (strpos($v_name, '/') !== false) { + $errors[] = pht('Snapshot names can not contain "/".'); + } + + if (!count($errors)) { + $snapshot = null; + + try { + // Create the snapshot. + $snapshot = id(new PhragmentSnapshot()) + ->setPrimaryFragmentPHID($fragment->getPHID()) + ->setName($v_name) + ->save(); + } catch (AphrontQueryDuplicateKeyException $e) { + $errors[] = pht('A snapshot with this name already exists.'); + } + + if (!count($errors)) { + // Add the primary fragment. + id(new PhragmentSnapshotChild()) + ->setSnapshotPHID($snapshot->getPHID()) + ->setFragmentPHID($fragment->getPHID()) + ->setFragmentVersionPHID($fragment->getLatestVersionPHID()) + ->save(); + + // Add all of the child fragments. + foreach ($children as $child) { + id(new PhragmentSnapshotChild()) + ->setSnapshotPHID($snapshot->getPHID()) + ->setFragmentPHID($child->getPHID()) + ->setFragmentVersionPHID($child->getLatestVersionPHID()) + ->save(); + } + + return id(new AphrontRedirectResponse()) + ->setURI('/phragment/snapshot/view/'.$snapshot->getID()); + } + } + + $error_view = id(new AphrontErrorView()) + ->setErrors($errors) + ->setTitle(pht('Errors while creating snapshot')); + } + + $fragment_sequence = "-"; + if ($fragment->getLatestVersion() !== null) { + $fragment_sequence = $fragment->getLatestVersion()->getSequence(); + } + + $rows = array(); + $rows[] = phutil_tag( + 'tr', + array(), + array( + phutil_tag('th', array(), 'Fragment'), + phutil_tag('th', array(), 'Version'))); + $rows[] = phutil_tag( + 'tr', + array(), + array( + phutil_tag('td', array(), $fragment->getPath()), + phutil_tag('td', array(), $fragment_sequence))); + foreach ($children as $child) { + $sequence = "-"; + if ($child->getLatestVersion() !== null) { + $sequence = $child->getLatestVersion()->getSequence(); + } + $rows[] = phutil_tag( + 'tr', + array(), + array( + phutil_tag('td', array(), $child->getPath()), + phutil_tag('td', array(), $sequence))); + } + + $table = phutil_tag( + 'table', + array('class' => 'remarkup-table'), + $rows); + + $container = phutil_tag( + 'div', + array('class' => 'phabricator-remarkup'), + array( + phutil_tag( + 'p', + array(), + pht( + "The snapshot will contain the following fragments at ". + "the specified versions: ")), + $table)); + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Fragment Path')) + ->setDisabled(true) + ->setValue('/'.$fragment->getPath())) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Snapshot Name')) + ->setName('name')) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Create Snapshot')) + ->addCancelButton( + $this->getApplicationURI('browse/'.$fragment->getPath()))) + ->appendChild( + id(new PHUIFormDividerControl())) + ->appendInstructions($container); + + $crumbs = $this->buildApplicationCrumbsWithPath($parents); + $crumbs->addCrumb( + id(new PhabricatorCrumbView()) + ->setName(pht('Create Snapshot'))); + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Create Snapshot of %s', $fragment->getName())) + ->setFormError($error_view) + ->setForm($form); + + return $this->buildApplicationPage( + array( + $crumbs, + $box), + array( + 'title' => pht('Create Fragment'), + 'device' => true)); + } + +} Index: src/applications/phragment/controller/PhragmentSnapshotDeleteController.php =================================================================== --- /dev/null +++ src/applications/phragment/controller/PhragmentSnapshotDeleteController.php @@ -0,0 +1,50 @@ +id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $snapshot = id(new PhragmentSnapshotQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->executeOne(); + if ($snapshot === null) { + return new Aphront404Response(); + } + + if ($request->isDialogFormPost()) { + $fragment_uri = $snapshot->getPrimaryFragment()->getURI(); + + $snapshot->delete(); + + return id(new AphrontRedirectResponse()) + ->setURI($fragment_uri); + } + + return $this->createDialog(); + } + + function createDialog() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $dialog = id(new AphrontDialogView()) + ->setTitle(pht('Really delete this snapshot?')) + ->setUser($request->getUser()) + ->addSubmitButton(pht('Delete')) + ->addCancelButton(pht('Cancel')) + ->appendParagraph(pht( + "Deleting this snapshot is a permanent operation. You can not ". + "recover the state of the snapshot.")); + return id(new AphrontDialogResponse())->setDialog($dialog); + } + +} Index: src/applications/phragment/controller/PhragmentSnapshotPromoteController.php =================================================================== --- /dev/null +++ src/applications/phragment/controller/PhragmentSnapshotPromoteController.php @@ -0,0 +1,180 @@ +dblob = idx($data, 'dblob', null); + $this->id = idx($data, 'id', null); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + // When the user is promoting a snapshot to the latest version, the + // identifier is a fragment path. + if ($this->dblob !== null) { + $this->targetFragment = id(new PhragmentFragmentQuery()) + ->setViewer($viewer) + ->withPaths(array($this->dblob)) + ->executeOne(); + if ($this->targetFragment === null) { + return new Aphront404Response(); + } + + $this->snapshots = id(new PhragmentSnapshotQuery()) + ->setViewer($viewer) + ->withPrimaryFragmentPHIDs(array($this->targetFragment->getPHID())) + ->execute(); + } + + // When the user is promoting a snapshot to another snapshot, the + // identifier is another snapshot ID. + if ($this->id !== null) { + $this->targetSnapshot = id(new PhragmentSnapshotQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->executeOne(); + if ($this->targetSnapshot === null) { + return new Aphront404Response(); + } + + $this->snapshots = id(new PhragmentSnapshotQuery()) + ->setViewer($viewer) + ->withPrimaryFragmentPHIDs(array( + $this->targetSnapshot->getPrimaryFragmentPHID())) + ->execute(); + } + + // If there's no identifier, just 404. + if ($this->snapshots === null) { + return new Aphront404Response(); + } + + // Work out what options the user has. + $this->options = mpull( + $this->snapshots, + 'getName', + 'getID'); + if ($this->id !== null) { + unset($this->options[$this->id]); + } + + // If there's no options, show a dialog telling the + // user there are no snapshots to promote. + if (count($this->options) === 0) { + return id(new AphrontDialogResponse())->setDialog( + id(new AphrontDialogView()) + ->setTitle(pht('No snapshots to promote')) + ->appendParagraph(pht( + "There are no snapshots available to promote.")) + ->setUser($request->getUser()) + ->addCancelButton(pht('Cancel'))); + } + + // Handle snapshot promotion. + if ($request->isDialogFormPost()) { + $snapshot = id(new PhragmentSnapshotQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getStr('snapshot'))) + ->executeOne(); + if ($snapshot === null) { + return new Aphront404Response(); + } + + $snapshot->openTransaction(); + // Delete all existing child entries. + $children = id(new PhragmentSnapshotChildQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withSnapshotPHIDs(array($snapshot->getPHID())) + ->execute(); + foreach ($children as $child) { + $child->delete(); + } + + if ($this->id === null) { + // The user is promoting the snapshot to the latest version. + $children = id(new PhragmentFragmentQuery()) + ->setViewer($viewer) + ->needLatestVersion(true) + ->withLeadingPath($this->targetFragment->getPath().'/') + ->execute(); + + // Add the primary fragment. + id(new PhragmentSnapshotChild()) + ->setSnapshotPHID($snapshot->getPHID()) + ->setFragmentPHID($this->targetFragment->getPHID()) + ->setFragmentVersionPHID( + $this->targetFragment->getLatestVersionPHID()) + ->save(); + + // Add all of the child fragments. + foreach ($children as $child) { + id(new PhragmentSnapshotChild()) + ->setSnapshotPHID($snapshot->getPHID()) + ->setFragmentPHID($child->getPHID()) + ->setFragmentVersionPHID($child->getLatestVersionPHID()) + ->save(); + } + } else { + // The user is promoting the snapshot to another snapshot. We just + // copy the other snapshot's child entries and change the snapshot + // PHID to make it identical. + $children = id(new PhragmentSnapshotChildQuery()) + ->setViewer($viewer) + ->withSnapshotPHIDs(array($this->targetSnapshot->getPHID())) + ->execute(); + foreach ($children as $child) { + id(new PhragmentSnapshotChild()) + ->setSnapshotPHID($snapshot->getPHID()) + ->setFragmentPHID($child->getFragmentPHID()) + ->setFragmentVersionPHID($child->getFragmentVersionPHID()) + ->save(); + } + } + $snapshot->saveTransaction(); + + return id(new AphrontRedirectResponse()); + } + + return $this->createDialog(); + } + + function createDialog() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $dialog = id(new AphrontDialogView()) + ->setTitle(pht('Promote which snapshot?')) + ->setUser($request->getUser()) + ->addSubmitButton(pht('Promote')) + ->addCancelButton(pht('Cancel')); + + if ($this->id === null) { + // The user is promoting a snapshot to the latest version. + $dialog->appendParagraph(pht( + "Select the snapshot you want to promote to the latest version:")); + } else { + // The user is promoting a snapshot to another snapshot. + $dialog->appendParagraph(pht( + "Select the snapshot you want to promote to '%s':", + $this->targetSnapshot->getName())); + } + + $dialog->appendChild( + id(new AphrontFormSelectControl()) + ->setUser($viewer) + ->setName('snapshot') + ->setOptions($this->options)); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + +} Index: src/applications/phragment/controller/PhragmentSnapshotViewController.php =================================================================== --- /dev/null +++ src/applications/phragment/controller/PhragmentSnapshotViewController.php @@ -0,0 +1,146 @@ +id = idx($data, "id", ""); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $snapshot = id(new PhragmentSnapshotQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->executeOne(); + if ($snapshot === null) { + return new Aphront404Response(); + } + + $box = $this->createSnapshotView($snapshot); + + $fragment = id(new PhragmentFragmentQuery()) + ->setViewer($viewer) + ->withPHIDs(array($snapshot->getPrimaryFragmentPHID())) + ->executeOne(); + if ($fragment === null) { + return new Aphront404Response(); + } + + $parents = $this->loadParentFragments($fragment->getPath()); + if ($parents === null) { + return new Aphront404Response(); + } + + $crumbs = $this->buildApplicationCrumbsWithPath($parents); + $crumbs->addCrumb( + id(new PhabricatorCrumbView()) + ->setName(pht('"%s" Snapshot', $snapshot->getName()))); + + $children = id(new PhragmentSnapshotChildQuery()) + ->setViewer($viewer) + ->needFragments(true) + ->needFragmentVersions(true) + ->withSnapshotPHIDs(array($snapshot->getPHID())) + ->execute(); + + $list = id(new PHUIObjectItemListView()) + ->setUser($viewer); + + foreach ($children as $child) { + $item = id(new PHUIObjectItemView()) + ->setHeader($child->getFragment()->getPath()); + + if ($child->getFragmentVersion() !== null) { + $item + ->setHref($child->getFragmentVersion()->getURI()) + ->addAttribute(pht( + 'Version %s', + $child->getFragmentVersion()->getSequence())); + } else { + $item + ->setHref($child->getFragment()->getURI()) + ->addAttribute(pht('Directory')); + } + + $list->addItem($item); + } + + return $this->buildApplicationPage( + array( + $crumbs, + $box, + $list), + array( + 'title' => pht('View Snapshot'), + 'device' => true)); + } + + protected function createSnapshotView($snapshot) { + if ($snapshot === null) { + return null; + } + + $viewer = $this->getRequest()->getUser(); + + $phids = array(); + $phids[] = $snapshot->getPrimaryFragmentPHID(); + + $this->loadHandles($phids); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('"%s" Snapshot', $snapshot->getName())) + ->setPolicyObject($snapshot) + ->setUser($viewer); + + $zip_uri = $this->getApplicationURI( + "zip@".$snapshot->getName(). + "/".$snapshot->getPrimaryFragment()->getPath()); + + $actions = id(new PhabricatorActionListView()) + ->setUser($viewer) + ->setObject($snapshot) + ->setObjectURI($snapshot->getURI()); + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Download Snapshot as ZIP')) + ->setHref($zip_uri) + ->setDisabled(false) // TODO: Policy + ->setIcon('zip')); + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Delete Snapshot')) + ->setHref($this->getApplicationURI( + "snapshot/delete/".$snapshot->getID()."/")) + ->setWorkflow(true) + ->setDisabled(false) // TODO: Policy + ->setIcon('delete')); + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Promote Another Snapshot to Here')) + ->setHref($this->getApplicationURI( + "snapshot/promote/".$snapshot->getID()."/")) + ->setWorkflow(true) + ->setDisabled(false) // TODO: Policy + ->setIcon('promote')); + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer) + ->setObject($snapshot) + ->setActionList($actions); + + $properties->addProperty( + pht('Name'), + $snapshot->getName()); + $properties->addProperty( + pht('Fragment'), + $this->renderHandlesForPHIDs(array($snapshot->getPrimaryFragmentPHID()))); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->addPropertyList($properties); + } +} Index: src/applications/phragment/controller/PhragmentZIPController.php =================================================================== --- src/applications/phragment/controller/PhragmentZIPController.php +++ src/applications/phragment/controller/PhragmentZIPController.php @@ -3,9 +3,13 @@ final class PhragmentZIPController extends PhragmentController { private $dblob; + private $snapshot; + + private $snapshotCache; public function willProcessRequest(array $data) { $this->dblob = idx($data, "dblob", ""); + $this->snapshot = idx($data, "snapshot", null); } public function processRequest() { @@ -18,6 +22,27 @@ } $fragment = idx($parents, count($parents) - 1, null); + if ($this->snapshot !== null) { + $snapshot = id(new PhragmentSnapshotQuery()) + ->setViewer($viewer) + ->withPrimaryFragmentPHIDs(array($fragment->getPHID())) + ->withNames(array($this->snapshot)) + ->executeOne(); + if ($snapshot === null) { + return new Aphront404Response(); + } + + $cache = id(new PhragmentSnapshotChildQuery()) + ->setViewer($viewer) + ->needFragmentVersions(true) + ->withSnapshotPHIDs(array($snapshot->getPHID())) + ->execute(); + $this->snapshotCache = mpull( + $cache, + 'getFragmentVersion', + 'getFragmentPHID'); + } + $temp = new TempFile(); $zip = null; @@ -95,10 +120,10 @@ if (count($children) === 0) { $path = substr($current->getPath(), strlen($base_path) + 1); - if ($current->getLatestVersion() === null) { + if ($this->getVersion($current) === null) { return array(); } - return array($path => $current->getLatestVersion()->getFilePHID()); + return array($path => $this->getVersion($current)->getFilePHID()); } else { $mappings = array(); foreach ($children as $child) { @@ -111,4 +136,12 @@ } } + private function getVersion($fragment) { + if ($this->snapshot === null) { + return $fragment->getLatestVersion(); + } else { + return idx($this->snapshotCache, $fragment->getPHID(), null); + } + } + } Index: src/applications/phragment/phid/PhragmentPHIDTypeFragment.php =================================================================== --- src/applications/phragment/phid/PhragmentPHIDTypeFragment.php +++ src/applications/phragment/phid/PhragmentPHIDTypeFragment.php @@ -34,7 +34,10 @@ foreach ($handles as $phid => $handle) { $fragment = $objects[$phid]; - $handle->setName($fragment->getID()); + $handle->setName(pht( + "Fragment %s: %s", + $fragment->getID(), + $fragment->getName())); $handle->setURI($fragment->getURI()); } } Index: src/applications/phragment/phid/PhragmentPHIDTypeFragmentVersion.php =================================================================== --- src/applications/phragment/phid/PhragmentPHIDTypeFragmentVersion.php +++ src/applications/phragment/phid/PhragmentPHIDTypeFragmentVersion.php @@ -34,7 +34,10 @@ foreach ($handles as $phid => $handle) { $version = $objects[$phid]; - $handle->setName($version->getSequence()); + $handle->setName(pht( + "Fragment Version %d: %s", + $version->getSequence(), + $version->getFragment()->getName())); $handle->setURI($version->getURI()); } } Index: src/applications/phragment/phid/PhragmentPHIDTypeSnapshot.php =================================================================== --- src/applications/phragment/phid/PhragmentPHIDTypeSnapshot.php +++ src/applications/phragment/phid/PhragmentPHIDTypeSnapshot.php @@ -1,27 +1,27 @@ withPHIDs($phids); } @@ -32,10 +32,12 @@ $viewer = $query->getViewer(); foreach ($handles as $phid => $handle) { - $fragment = $objects[$phid]; + $snapshot = $objects[$phid]; - $handle->setName($fragment->getID()); - $handle->setURI($fragment->getURI()); + $handle->setName(pht( + 'Snapshot: %s', + $snapshot->getName())); + $handle->setURI($snapshot->getURI()); } } Index: src/applications/phragment/query/PhragmentFragmentQuery.php =================================================================== --- src/applications/phragment/query/PhragmentFragmentQuery.php +++ src/applications/phragment/query/PhragmentFragmentQuery.php @@ -8,7 +8,7 @@ private $paths; private $leadingPath; private $depths; - private $needsLatestVersion; + private $needLatestVersion; public function withIDs(array $ids) { $this->ids = $ids; @@ -36,7 +36,7 @@ } public function needLatestVersion($need_latest_version) { - $this->needsLatestVersion = $need_latest_version; + $this->needLatestVersion = $need_latest_version; return $this; } @@ -99,7 +99,7 @@ } protected function didFilterPage(array $page) { - if ($this->needsLatestVersion) { + if ($this->needLatestVersion) { $versions = array(); $version_phids = array_filter(mpull($page, 'getLatestVersionPHID')); Index: src/applications/phragment/query/PhragmentSnapshotChildQuery.php =================================================================== --- /dev/null +++ src/applications/phragment/query/PhragmentSnapshotChildQuery.php @@ -0,0 +1,174 @@ +ids = $ids; + return $this; + } + + public function withSnapshotPHIDs(array $snapshot_phids) { + $this->snapshotPHIDs = $snapshot_phids; + return $this; + } + + public function withFragmentPHIDs(array $fragment_phids) { + $this->fragmentPHIDs = $fragment_phids; + return $this; + } + + public function withFragmentVersionPHIDs(array $fragment_version_phids) { + $this->fragmentVersionPHIDs = $fragment_version_phids; + return $this; + } + + public function needFragments($need_fragments) { + $this->needFragments = $need_fragments; + return $this; + } + + public function needFragmentVersions($need_fragment_versions) { + $this->needFragmentVersions = $need_fragment_versions; + return $this; + } + + public function loadPage() { + $table = new PhragmentSnapshotChild(); + $conn_r = $table->establishConnection('r'); + + $data = queryfx_all( + $conn_r, + 'SELECT * FROM %T %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn_r), + $this->buildOrderClause($conn_r), + $this->buildLimitClause($conn_r)); + + return $table->loadAllFromArray($data); + } + + protected function buildWhereClause($conn_r) { + $where = array(); + + if ($this->ids) { + $where[] = qsprintf( + $conn_r, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->snapshotPHIDs) { + $where[] = qsprintf( + $conn_r, + 'snapshotPHID IN (%Ls)', + $this->snapshotPHIDs); + } + + if ($this->fragmentPHIDs) { + $where[] = qsprintf( + $conn_r, + 'fragmentPHID IN (%Ls)', + $this->fragmentPHIDs); + } + + if ($this->fragmentVersionPHIDs) { + $where[] = qsprintf( + $conn_r, + 'fragmentVersionPHID IN (%Ls)', + $this->fragmentVersionPHIDs); + } + + $where[] = $this->buildPagingClause($conn_r); + + return $this->formatWhereClause($where); + } + + protected function willFilterPage(array $page) { + $snapshots = array(); + + $snapshot_phids = array_filter(mpull($page, 'getSnapshotPHID')); + if ($snapshot_phids) { + $snapshots = id(new PhabricatorObjectQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($snapshot_phids) + ->setParentQuery($this) + ->execute(); + $snapshots = mpull($snapshots, null, 'getPHID'); + } + + foreach ($page as $key => $child) { + $snapshot_phid = $child->getSnapshotPHID(); + if (empty($snapshots[$snapshot_phid])) { + unset($page[$key]); + continue; + } + $child->attachSnapshot($snapshots[$snapshot_phid]); + } + + return $page; + } + + protected function didFilterPage(array $page) { + if ($this->needFragments) { + $fragments = array(); + + $fragment_phids = array_filter(mpull($page, 'getFragmentPHID')); + if ($fragment_phids) { + $fragments = id(new PhabricatorObjectQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($fragment_phids) + ->setParentQuery($this) + ->execute(); + $fragments = mpull($fragments, null, 'getPHID'); + } + + foreach ($page as $key => $child) { + $fragment_phid = $child->getFragmentPHID(); + if (empty($fragments[$fragment_phid])) { + unset($page[$key]); + continue; + } + $child->attachFragment($fragments[$fragment_phid]); + } + } + + if ($this->needFragmentVersions) { + $fragment_versions = array(); + + $fragment_version_phids = array_filter(mpull( + $page, + 'getFragmentVersionPHID')); + if ($fragment_version_phids) { + $fragment_versions = id(new PhabricatorObjectQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($fragment_version_phids) + ->setParentQuery($this) + ->execute(); + $fragment_versions = mpull($fragment_versions, null, 'getPHID'); + } + + foreach ($page as $key => $child) { + $fragment_version_phid = $child->getFragmentVersionPHID(); + if (empty($fragment_versions[$fragment_version_phid])) { + continue; + } + $child->attachFragmentVersion( + $fragment_versions[$fragment_version_phid]); + } + } + + return $page; + } + + public function getQueryApplicationClass() { + return 'PhabricatorApplicationPhragment'; + } +} Index: src/applications/phragment/query/PhragmentSnapshotQuery.php =================================================================== --- /dev/null +++ src/applications/phragment/query/PhragmentSnapshotQuery.php @@ -0,0 +1,110 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withPrimaryFragmentPHIDs(array $primary_fragment_phids) { + $this->primaryFragmentPHIDs = $primary_fragment_phids; + return $this; + } + + public function withNames(array $names) { + $this->names = $names; + return $this; + } + + public function loadPage() { + $table = new PhragmentSnapshot(); + $conn_r = $table->establishConnection('r'); + + $data = queryfx_all( + $conn_r, + 'SELECT * FROM %T %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn_r), + $this->buildOrderClause($conn_r), + $this->buildLimitClause($conn_r)); + + return $table->loadAllFromArray($data); + } + + protected function buildWhereClause($conn_r) { + $where = array(); + + if ($this->ids) { + $where[] = qsprintf( + $conn_r, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids) { + $where[] = qsprintf( + $conn_r, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->primaryFragmentPHIDs) { + $where[] = qsprintf( + $conn_r, + 'primaryFragmentPHID IN (%Ls)', + $this->primaryFragmentPHIDs); + } + + if ($this->names) { + $where[] = qsprintf( + $conn_r, + 'name IN (%Ls)', + $this->names); + } + + $where[] = $this->buildPagingClause($conn_r); + + return $this->formatWhereClause($where); + } + + protected function willFilterPage(array $page) { + $fragments = array(); + + $fragment_phids = array_filter(mpull($page, 'getPrimaryFragmentPHID')); + if ($fragment_phids) { + $fragments = id(new PhabricatorObjectQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($fragment_phids) + ->setParentQuery($this) + ->execute(); + $fragments = mpull($fragments, null, 'getPHID'); + } + + foreach ($page as $key => $snapshot) { + $fragment_phid = $snapshot->getPrimaryFragmentPHID(); + if (empty($fragments[$fragment_phid])) { + unset($page[$key]); + continue; + } + $snapshot->attachPrimaryFragment($fragments[$fragment_phid]); + } + + return $page; + } + + public function getQueryApplicationClass() { + return 'PhabricatorApplicationPhragment'; + } +} Index: src/applications/phragment/storage/PhragmentFragment.php =================================================================== --- src/applications/phragment/storage/PhragmentFragment.php +++ src/applications/phragment/storage/PhragmentFragment.php @@ -23,7 +23,7 @@ } public function getURI() { - return '/phragment/fragment/'.$this->getID().'/'; + return '/phragment/browse/'.$this->getPath(); } public function getName() { Index: src/applications/phragment/storage/PhragmentSnapshot.php =================================================================== --- /dev/null +++ src/applications/phragment/storage/PhragmentSnapshot.php @@ -0,0 +1,69 @@ + true, + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PhragmentPHIDTypeSnapshot::TYPECONST); + } + + public function getURI() { + return '/phragment/snapshot/view/'.$this->getID().'/'; + } + + public function getPrimaryFragment() { + return $this->assertAttached($this->primaryFragment); + } + + public function attachPrimaryFragment(PhragmentFragment $fragment) { + return $this->primaryFragment = $fragment; + } + + public function delete() { + $children = id(new PhragmentSnapshotChild()) + ->loadAllWhere('snapshotPHID = %s', $this->getPHID()); + $this->openTransaction(); + foreach ($children as $child) { + $child->delete(); + } + $result = parent::delete(); + $this->saveTransaction(); + return $result; + } + + +/* -( Policy Interface )--------------------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW + ); + } + + public function getPolicy($capability) { + return $this->getPrimaryFragment()->getPolicy($capability); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return $this->getPrimaryFragment() + ->hasAutomaticCapability($capability, $viewer); + } + + public function describeAutomaticCapability($capability) { + return $this->getPrimaryFragment() + ->describeAutomaticCapability($capability); + } +} Index: src/applications/phragment/storage/PhragmentSnapshotChild.php =================================================================== --- /dev/null +++ src/applications/phragment/storage/PhragmentSnapshotChild.php @@ -0,0 +1,64 @@ +assertAttached($this->snapshot); + } + + public function attachSnapshot(PhragmentSnapshot $snapshot) { + return $this->snapshot = $snapshot; + } + + public function getFragment() { + return $this->assertAttached($this->fragment); + } + + public function attachFragment(PhragmentFragment $fragment) { + return $this->fragment = $fragment; + } + + public function getFragmentVersion() { + if ($this->fragmentVersionPHID === null) { + return null; + } + return $this->assertAttached($this->fragmentVersion); + } + + public function attachFragmentVersion(PhragmentFragmentVersion $version) { + return $this->fragmentVersion = $version; + } + + +/* -( Policy Interface )--------------------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW + ); + } + + public function getPolicy($capability) { + return $this->getSnapshot()->getPolicy($capability); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return $this->getSnapshot() + ->hasAutomaticCapability($capability, $viewer); + } + + public function describeAutomaticCapability($capability) { + return $this->getSnapshot() + ->describeAutomaticCapability($capability); + } +} Index: src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php =================================================================== --- src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php +++ src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php @@ -1824,6 +1824,10 @@ 'type' => 'sql', 'name' => $this->getPatchPath('20131206.phragmentnull.sql'), ), + '20131208.phragmentsnapshot.sql' => array( + 'type' => 'sql', + 'name' => $this->getPatchPath('20131208.phragmentsnapshot.sql'), + ), ); } }