From c48cfb31bc76ab4d325c0f6ae90b9bffbf84032f Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Sun, 8 Dec 2013 12:14:34 +1100 Subject: [PATCH] Implement viewing versions and downloading patches in Phragment Summary: This adds support for viewing individual versions on a fragment as well as comparing versions and downloading diff_match_patch-based patches. It does not use the side-by-side diff format as while it works for small changes, it quickly becomes impossible to distingush what changes have been made due to the diff_match_patch format. Test Plan: Clicked on versions and downloaded patches. Reviewers: epriestley, #blessed_reviewers Reviewed By: epriestley CC: Korvin, epriestley, aran Maniphest Tasks: T4205 Differential Revision: https://secure.phabricator.com/D7734 --- src/__phutil_library_map__.php | 4 + .../PhabricatorApplicationPhragment.php | 2 + .../controller/PhragmentHistoryController.php | 1 + .../controller/PhragmentPatchController.php | 88 +++++++++ .../controller/PhragmentVersionController.php | 179 ++++++++++++++++++ .../query/PhragmentFragmentVersionQuery.php | 26 +++ .../storage/PhragmentFragmentVersion.php | 2 +- .../phragment/util/PhragmentPatchUtil.php | 2 +- 8 files changed, 302 insertions(+), 2 deletions(-) create mode 100644 src/applications/phragment/controller/PhragmentPatchController.php create mode 100644 src/applications/phragment/controller/PhragmentVersionController.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 0af987871e..8af18cc750 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2186,8 +2186,10 @@ phutil_register_library_map(array( 'PhragmentHistoryController' => 'applications/phragment/controller/PhragmentHistoryController.php', 'PhragmentPHIDTypeFragment' => 'applications/phragment/phid/PhragmentPHIDTypeFragment.php', 'PhragmentPHIDTypeFragmentVersion' => 'applications/phragment/phid/PhragmentPHIDTypeFragmentVersion.php', + 'PhragmentPatchController' => 'applications/phragment/controller/PhragmentPatchController.php', 'PhragmentPatchUtil' => 'applications/phragment/util/PhragmentPatchUtil.php', 'PhragmentUpdateController' => 'applications/phragment/controller/PhragmentUpdateController.php', + 'PhragmentVersionController' => 'applications/phragment/controller/PhragmentVersionController.php', 'PhragmentZIPController' => 'applications/phragment/controller/PhragmentZIPController.php', 'PhrequentController' => 'applications/phrequent/controller/PhrequentController.php', 'PhrequentDAO' => 'applications/phrequent/storage/PhrequentDAO.php', @@ -4786,8 +4788,10 @@ phutil_register_library_map(array( 'PhragmentHistoryController' => 'PhragmentController', 'PhragmentPHIDTypeFragment' => 'PhabricatorPHIDType', 'PhragmentPHIDTypeFragmentVersion' => 'PhabricatorPHIDType', + 'PhragmentPatchController' => 'PhragmentController', 'PhragmentPatchUtil' => 'Phobject', 'PhragmentUpdateController' => 'PhragmentController', + 'PhragmentVersionController' => 'PhragmentController', 'PhragmentZIPController' => 'PhragmentController', 'PhrequentController' => 'PhabricatorController', 'PhrequentDAO' => 'PhabricatorLiskDAO', diff --git a/src/applications/phragment/application/PhabricatorApplicationPhragment.php b/src/applications/phragment/application/PhabricatorApplicationPhragment.php index 7b4772d59a..01e42b331f 100644 --- a/src/applications/phragment/application/PhabricatorApplicationPhragment.php +++ b/src/applications/phragment/application/PhabricatorApplicationPhragment.php @@ -39,6 +39,8 @@ final class PhabricatorApplicationPhragment extends PhabricatorApplication { 'update/(?P.*)' => 'PhragmentUpdateController', 'history/(?P.*)' => 'PhragmentHistoryController', 'zip/(?P.*)' => 'PhragmentZIPController', + 'version/(?P[0-9]*)/' => 'PhragmentVersionController', + 'patch/(?P[0-9x]*)/(?P[0-9]*)/' => 'PhragmentPatchController', ), ); } diff --git a/src/applications/phragment/controller/PhragmentHistoryController.php b/src/applications/phragment/controller/PhragmentHistoryController.php index 00a812b5ee..4ca50c0f3a 100644 --- a/src/applications/phragment/controller/PhragmentHistoryController.php +++ b/src/applications/phragment/controller/PhragmentHistoryController.php @@ -47,6 +47,7 @@ final class PhragmentHistoryController extends PhragmentController { foreach ($versions as $version) { $item = id(new PHUIObjectItemView()); $item->setHeader('Version '.$version->getSequence()); + $item->setHref($version->getURI()); $item->addAttribute(phabricator_datetime( $version->getDateCreated(), $viewer)); diff --git a/src/applications/phragment/controller/PhragmentPatchController.php b/src/applications/phragment/controller/PhragmentPatchController.php new file mode 100644 index 0000000000..41bc2de963 --- /dev/null +++ b/src/applications/phragment/controller/PhragmentPatchController.php @@ -0,0 +1,88 @@ +aid = idx($data, "aid", 0); + $this->bid = idx($data, "bid", 0); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + // If "aid" is "x", then it means the user wants to generate + // a patch of an empty file to the version specified by "bid". + + $ids = array($this->aid, $this->bid); + if ($this->aid === "x") { + $ids = array($this->bid); + } + + $versions = id(new PhragmentFragmentVersionQuery()) + ->setViewer($viewer) + ->withIDs($ids) + ->execute(); + + $version_a = null; + if ($this->aid !== "x") { + $version_a = idx($versions, $this->aid, null); + if ($version_a === null) { + return new Aphront404Response(); + } + } + + $version_b = idx($versions, $this->bid, null); + if ($version_b === null) { + return new Aphront404Response(); + } + + $file_phids = array(); + if ($version_a !== null) { + $file_phids[] = $version_a->getFilePHID(); + } + $file_phids[] = $version_b->getFilePHID(); + + $files = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs($file_phids) + ->execute(); + $files = mpull($files, null, 'getPHID'); + + $file_a = null; + if ($version_a != null) { + $file_a = idx($files, $version_a->getFilePHID(), null); + } + $file_b = idx($files, $version_b->getFilePHID(), null); + + $patch = PhragmentPatchUtil::calculatePatch($file_a, $file_b); + + if ($patch === null) { + throw new Exception("Unable to compute patch!"); + } + + $a_sequence = 'x'; + if ($version_a !== null) { + $a_sequence = $version_a->getSequence(); + } + + $name = + $version_b->getFragment()->getName().'.'. + $a_sequence.'.'. + $version_b->getSequence().'.patch'; + + $result = PhabricatorFile::buildFromFileDataOrHash( + $patch, + array( + 'name' => $name, + 'mime-type' => 'text/plain', + 'ttl' => time() + 60 * 60 * 24, + )); + return id(new AphrontRedirectResponse()) + ->setURI($result->getBestURI()); + } + +} diff --git a/src/applications/phragment/controller/PhragmentVersionController.php b/src/applications/phragment/controller/PhragmentVersionController.php new file mode 100644 index 0000000000..0579e96e4c --- /dev/null +++ b/src/applications/phragment/controller/PhragmentVersionController.php @@ -0,0 +1,179 @@ +id = idx($data, "id", 0); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $version = id(new PhragmentFragmentVersionQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->executeOne(); + if ($version === null) { + return new Aphront404Response(); + } + + $parents = $this->loadParentFragments($version->getFragment()->getPath()); + if ($parents === null) { + return new Aphront404Response(); + } + $current = idx($parents, count($parents) - 1, null); + + $crumbs = $this->buildApplicationCrumbsWithPath($parents); + $crumbs->addCrumb( + id(new PhabricatorCrumbView()) + ->setName(pht('View Version %d', $version->getSequence()))); + + $phids = array(); + $phids[] = $version->getFilePHID(); + + $this->loadHandles($phids); + + $file = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($version->getFilePHID())) + ->executeOne(); + if ($file !== null) { + $file_uri = $file->getBestURI(); + } + + $header = id(new PHUIHeaderView()) + ->setHeader(pht( + "%s at version %d", + $version->getFragment()->getName(), + $version->getSequence())) + ->setPolicyObject($version) + ->setUser($viewer); + + $actions = id(new PhabricatorActionListView()) + ->setUser($viewer) + ->setObject($version) + ->setObjectURI($version->getURI()); + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Download Version')) + ->setHref($file_uri) + ->setDisabled($file === null) + ->setIcon('download')); + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer) + ->setObject($version) + ->setActionList($actions); + $properties->addProperty( + pht('File'), + $this->renderHandlesForPHIDs(array($version->getFilePHID()))); + + $box = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->addPropertyList($properties); + + return $this->buildApplicationPage( + array( + $crumbs, + $box, + $this->renderPatchFromPreviousVersion($version, $file), + $this->renderPreviousVersionList($version)), + array( + 'title' => pht('View Version'), + 'device' => true)); + } + + private function renderPatchFromPreviousVersion( + PhragmentFragmentVersion $version, + PhabricatorFile $file) { + + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $previous_file = null; + $previous = id(new PhragmentFragmentVersionQuery()) + ->setViewer($viewer) + ->withFragmentPHIDs(array($version->getFragmentPHID())) + ->withSequences(array($version->getSequence() - 1)) + ->executeOne(); + if ($previous !== null) { + $previous_file = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($previous->getFilePHID())) + ->executeOne(); + } + + $patch = PhragmentPatchUtil::calculatePatch($previous_file, $file); + + if ($patch === null) { + return id(new AphrontErrorView()) + ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) + ->setTitle(pht("Identical Version")) + ->appendChild(phutil_tag( + 'p', + array(), + pht("This version is identical to the previous version."))); + } + + if (strlen($patch) > 20480) { + // Patch is longer than 20480 characters. Trim it and let the user know. + $patch = substr($patch, 0, 20480)."\n...\n"; + $patch .= pht( + "This patch is longer than 20480 characters. Use the link ". + "in the action list to download the full patch."); + } + + return id(new PHUIObjectBoxView()) + ->setHeader(id(new PHUIHeaderView()) + ->setHeader(pht('Differences since previous version'))) + ->appendChild(id(new PhabricatorSourceCodeView()) + ->setLines(phutil_split_lines($patch))); + } + + private function renderPreviousVersionList( + PhragmentFragmentVersion $version) { + + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $previous_versions = id(new PhragmentFragmentVersionQuery()) + ->setViewer($viewer) + ->withFragmentPHIDs(array($version->getFragmentPHID())) + ->withSequenceBefore($version->getSequence()) + ->execute(); + + $list = id(new PHUIObjectItemListView()) + ->setUser($viewer); + + foreach ($previous_versions as $previous_version) { + $item = id(new PHUIObjectItemView()); + $item->setHeader('Version '.$previous_version->getSequence()); + $item->setHref($previous_version->getURI()); + $item->addAttribute(phabricator_datetime( + $previous_version->getDateCreated(), + $viewer)); + $item->addAction(id(new PHUIListItemView()) + ->setIcon('patch') + ->setName(pht("Get Patch")) + ->setHref($this->getApplicationURI( + 'patch/'.$previous_version->getID().'/'.$version->getID()))); + $list->addItem($item); + } + + $item = id(new PHUIObjectItemView()); + $item->setHeader('Prior to Version 0'); + $item->addAttribute('Prior to any content (empty file)'); + $item->addAction(id(new PHUIListItemView()) + ->setIcon('patch') + ->setName(pht("Get Patch")) + ->setHref($this->getApplicationURI( + 'patch/x/'.$version->getID()))); + $list->addItem($item); + + return $list; + } + +} diff --git a/src/applications/phragment/query/PhragmentFragmentVersionQuery.php b/src/applications/phragment/query/PhragmentFragmentVersionQuery.php index d1dcad8056..264a89cf89 100644 --- a/src/applications/phragment/query/PhragmentFragmentVersionQuery.php +++ b/src/applications/phragment/query/PhragmentFragmentVersionQuery.php @@ -6,6 +6,8 @@ final class PhragmentFragmentVersionQuery private $ids; private $phids; private $fragmentPHIDs; + private $sequences; + private $sequenceBefore; public function withIDs(array $ids) { $this->ids = $ids; @@ -22,6 +24,16 @@ final class PhragmentFragmentVersionQuery return $this; } + public function withSequences(array $sequences) { + $this->sequences = $sequences; + return $this; + } + + public function withSequenceBefore($current) { + $this->sequenceBefore = $current; + return $this; + } + public function loadPage() { $table = new PhragmentFragmentVersion(); $conn_r = $table->establishConnection('r'); @@ -61,6 +73,20 @@ final class PhragmentFragmentVersionQuery $this->fragmentPHIDs); } + if ($this->sequences) { + $where[] = qsprintf( + $conn_r, + 'sequence IN (%Ld)', + $this->sequences); + } + + if ($this->sequenceBefore !== null) { + $where[] = qsprintf( + $conn_r, + 'sequence < %d', + $this->sequenceBefore); + } + $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); diff --git a/src/applications/phragment/storage/PhragmentFragmentVersion.php b/src/applications/phragment/storage/PhragmentFragmentVersion.php index ec93d738a4..ff848e3270 100644 --- a/src/applications/phragment/storage/PhragmentFragmentVersion.php +++ b/src/applications/phragment/storage/PhragmentFragmentVersion.php @@ -22,7 +22,7 @@ final class PhragmentFragmentVersion extends PhragmentDAO } public function getURI() { - return '/phragment/patch/'.$this->getID().'/'; + return '/phragment/version/'.$this->getID().'/'; } public function getFragment() { diff --git a/src/applications/phragment/util/PhragmentPatchUtil.php b/src/applications/phragment/util/PhragmentPatchUtil.php index e5849b2757..c395a14e35 100644 --- a/src/applications/phragment/util/PhragmentPatchUtil.php +++ b/src/applications/phragment/util/PhragmentPatchUtil.php @@ -9,7 +9,7 @@ final class PhragmentPatchUtil extends Phobject { * * @phutil-external-symbol class diff_match_patch */ - public function calculatePatch( + public static function calculatePatch( PhabricatorFile $old = null, PhabricatorFile $new = null) {