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
This commit is contained in:
		| @@ -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', | ||||
|   | ||||
| @@ -39,6 +39,8 @@ final class PhabricatorApplicationPhragment extends PhabricatorApplication { | ||||
|         'update/(?P<dblob>.*)' => 'PhragmentUpdateController', | ||||
|         'history/(?P<dblob>.*)' => 'PhragmentHistoryController', | ||||
|         'zip/(?P<dblob>.*)' => 'PhragmentZIPController', | ||||
|         'version/(?P<id>[0-9]*)/' => 'PhragmentVersionController', | ||||
|         'patch/(?P<aid>[0-9x]*)/(?P<bid>[0-9]*)/' => 'PhragmentPatchController', | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -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)); | ||||
|   | ||||
| @@ -0,0 +1,88 @@ | ||||
| <?php | ||||
|  | ||||
| final class PhragmentPatchController extends PhragmentController { | ||||
|  | ||||
|   private $aid; | ||||
|   private $bid; | ||||
|  | ||||
|   public function willProcessRequest(array $data) { | ||||
|     $this->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()); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,179 @@ | ||||
| <?php | ||||
|  | ||||
| final class PhragmentVersionController extends PhragmentController { | ||||
|  | ||||
|   private $id; | ||||
|  | ||||
|   public function willProcessRequest(array $data) { | ||||
|     $this->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; | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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() { | ||||
|   | ||||
| @@ -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) { | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 James Rhodes
					James Rhodes