Page MenuHomePhabricator

D7739.id17493.diff

D7739.id17493.diff

diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -207,6 +207,9 @@
'ConduitAPI_phpast_Method' => 'applications/phpast/conduit/ConduitAPI_phpast_Method.php',
'ConduitAPI_phpast_getast_Method' => 'applications/phpast/conduit/ConduitAPI_phpast_getast_Method.php',
'ConduitAPI_phpast_version_Method' => 'applications/phpast/conduit/ConduitAPI_phpast_version_Method.php',
+ 'ConduitAPI_phragment_Method' => 'applications/phragment/conduit/ConduitAPI_phragment_Method.php',
+ 'ConduitAPI_phragment_getpatch_Method' => 'applications/phragment/conduit/ConduitAPI_phragment_getpatch_Method.php',
+ 'ConduitAPI_phragment_getstate_Method' => 'applications/phragment/conduit/ConduitAPI_phragment_getstate_Method.php',
'ConduitAPI_phriction_Method' => 'applications/phriction/conduit/ConduitAPI_phriction_Method.php',
'ConduitAPI_phriction_edit_Method' => 'applications/phriction/conduit/ConduitAPI_phriction_edit_Method.php',
'ConduitAPI_phriction_history_Method' => 'applications/phriction/conduit/ConduitAPI_phriction_history_Method.php',
@@ -2573,6 +2576,9 @@
'ConduitAPI_phpast_Method' => 'ConduitAPIMethod',
'ConduitAPI_phpast_getast_Method' => 'ConduitAPI_phpast_Method',
'ConduitAPI_phpast_version_Method' => 'ConduitAPI_phpast_Method',
+ 'ConduitAPI_phragment_Method' => 'ConduitAPIMethod',
+ 'ConduitAPI_phragment_getpatch_Method' => 'ConduitAPI_phragment_Method',
+ 'ConduitAPI_phragment_getstate_Method' => 'ConduitAPI_phragment_Method',
'ConduitAPI_phriction_Method' => 'ConduitAPIMethod',
'ConduitAPI_phriction_edit_Method' => 'ConduitAPI_phriction_Method',
'ConduitAPI_phriction_history_Method' => 'ConduitAPI_phriction_Method',
diff --git a/src/applications/phragment/conduit/ConduitAPI_phragment_Method.php b/src/applications/phragment/conduit/ConduitAPI_phragment_Method.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phragment/conduit/ConduitAPI_phragment_Method.php
@@ -0,0 +1,13 @@
+<?php
+
+/**
+ * @group conduit
+ */
+abstract class ConduitAPI_phragment_Method extends ConduitAPIMethod {
+
+ public function getApplication() {
+ return PhabricatorApplication::getByClass(
+ 'PhabricatorApplicationPhragment');
+ }
+
+}
diff --git a/src/applications/phragment/conduit/ConduitAPI_phragment_getpatch_Method.php b/src/applications/phragment/conduit/ConduitAPI_phragment_getpatch_Method.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phragment/conduit/ConduitAPI_phragment_getpatch_Method.php
@@ -0,0 +1,196 @@
+<?php
+
+/**
+ * @group conduit
+ */
+final class ConduitAPI_phragment_getpatch_Method
+ extends ConduitAPI_phragment_Method {
+
+ public function getMethodDescription() {
+ return "Retrieve the patches to apply for a given set of files.";
+ }
+
+ public function defineParamTypes() {
+ return array(
+ 'path' => 'required string',
+ 'state' => 'required dict<string, string>',
+ );
+ }
+
+ public function defineReturnType() {
+ return 'nonempty dict';
+ }
+
+ public function defineErrorTypes() {
+ return array(
+ 'ERR_BAD_FRAGMENT' => 'No such fragment exists',
+ );
+ }
+
+ protected function execute(ConduitAPIRequest $request) {
+ $path = $request->getValue('path');
+ $state = $request->getValue('state');
+ // The state is an array mapping file paths to hashes.
+
+ $patches = array();
+
+ // We need to get all of the mappings (like phragment.getstate) first
+ // so that we can detect deletions and creations of files.
+ $fragment = id(new PhragmentFragmentQuery())
+ ->setViewer($request->getUser())
+ ->withPaths(array($path))
+ ->executeOne();
+ if ($fragment === null) {
+ throw new ConduitException('ERR_BAD_FRAGMENT');
+ }
+
+ $mappings = $this->getFragmentMappings(
+ $request->getUser(),
+ $fragment,
+ $fragment->getPath());
+
+ $file_phids = mpull(mpull($mappings, 'getLatestVersion'), 'getFilePHID');
+ $files = id(new PhabricatorFileQuery())
+ ->setViewer($request->getUser())
+ ->withPHIDs($file_phids)
+ ->execute();
+ $files = mpull($files, null, 'getPHID');
+
+ // Scan all of the files that the caller currently has and iterate
+ // over that.
+ foreach ($state as $path => $hash) {
+ // If $mappings[$path] exists, then the user has the file and it's
+ // also a fragment.
+ if (array_key_exists($path, $mappings)) {
+ $file_phid = $mappings[$path]->getLatestVersion()->getFilePHID();
+ if ($file_phid !== null) {
+ // If the file PHID is present, then we need to check the
+ // hashes to see if they are the same.
+ $hash_caller = strtolower($state[$path]);
+ $hash_current = $files[$file_phid]->getContentHash();
+ if ($hash_caller === $hash_current) {
+ // The user's version is identical to our version, so
+ // there is no update needed.
+ } else {
+ // The hash differs, and the user needs to update.
+ $patches[] = array(
+ 'path' => $path,
+ 'file_old' => null,
+ 'file_new' => $files[$file_phid],
+ 'hash_old' => $hash_caller,
+ 'hash_new' => $hash_current,
+ 'patch' => null);
+ }
+ } else {
+ // We have a record of this as a file, but there is no file
+ // attached to the latest version, so we consider this to be
+ // a deletion.
+ $patches[] = array(
+ 'path' => $path,
+ 'file_old' => null,
+ 'file_new' => null,
+ 'hash_old' => $hash_caller,
+ 'hash_new' => PhragmentPatchUtil::EMPTY_HASH,
+ 'patch' => null);
+ }
+ } else {
+ // If $mappings[$path] does not exist, then the user has a file,
+ // and we have absolutely no record of it what-so-ever (we haven't
+ // even recorded a deletion). Assuming most applications will store
+ // some form of data near their own files, this is probably a data
+ // file relevant for the application that is not versioned, so we
+ // don't tell the client to do anything with it.
+ }
+ }
+
+ // Check the remaining files that we know about but the caller has
+ // not reported.
+ foreach ($mappings as $path => $child) {
+ if (array_key_exists($path, $state)) {
+ // We have already evaluated this above.
+ } else {
+ $file_phid = $mappings[$path]->getLatestVersion()->getFilePHID();
+ if ($file_phid !== null) {
+ // If the file PHID is present, then this is a new file that
+ // we know about, but the caller does not. We need to tell
+ // the caller to create the file.
+ $hash_current = $files[$file_phid]->getContentHash();
+ $patches[] = array(
+ 'path' => $path,
+ 'file_old' => null,
+ 'file_new' => $files[$file_phid],
+ 'hash_old' => PhragmentPatchUtil::EMPTY_HASH,
+ 'hash_new' => $hash_current,
+ 'patch' => null);
+ } else {
+ // We have a record of deleting this file, and the caller hasn't
+ // reported it, so they've probably deleted it in a previous
+ // update.
+ }
+ }
+ }
+
+ // Before we can calculate patches, we need to resolve the old versions
+ // of files so we can draw diffs on them.
+ $hashes = array();
+ foreach ($patches as $patch) {
+ if ($patch["hash_old"] !== PhragmentPatchUtil::EMPTY_HASH) {
+ $hashes[] = $patch["hash_old"];
+ }
+ }
+ $old_files = array();
+ if (count($hashes) !== 0) {
+ $old_files = id(new PhabricatorFile())->loadAllWhere(
+ 'contentHash IN (%Ls) LIMIT 1', $hashes);
+ }
+ $old_files = mpull($old_files, null, 'getContentHash');
+ foreach ($patches as $patch) {
+ if ($patch["hash_old"] !== PhragmentPatchUtil::EMPTY_HASH) {
+ $patch['file_old'] = $old_files[$patch['hash_old']];
+ }
+ }
+
+ // Now run through all of the patch entries, calculate the patches
+ // and return the results.
+ foreach ($patches as $key => $patch) {
+ $patches[$key]['patch'] = PhragmentPatchUtil::calculatePatch(
+ $patches[$key]['file_old'],
+ $patches[$key]['file_new']);
+ unset($patches[$key]['file_old']);
+ unset($patches[$key]['file_new']);
+ }
+
+ return $patches;
+ }
+
+ private function getFragmentMappings(
+ PhabricatorUser $viewer,
+ PhragmentFragment $current,
+ $base_path) {
+
+ $children = id(new PhragmentFragmentQuery())
+ ->setViewer($viewer)
+ ->needLatestVersion(true)
+ ->withLeadingPath($current->getPath().'/')
+ ->withDepths(array($current->getDepth() + 1))
+ ->execute();
+
+ if (count($children) === 0) {
+ $path = substr($current->getPath(), strlen($base_path) + 1);
+ return array($path => $current);
+ } else {
+ $mappings = array();
+ foreach ($children as $child) {
+ $child_mappings = $this->getFragmentMappings(
+ $viewer,
+ $child,
+ $base_path);
+ foreach ($child_mappings as $key => $value) {
+ $mappings[$key] = $value;
+ }
+ }
+ return $mappings;
+ }
+ }
+
+}
diff --git a/src/applications/phragment/conduit/ConduitAPI_phragment_getstate_Method.php b/src/applications/phragment/conduit/ConduitAPI_phragment_getstate_Method.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phragment/conduit/ConduitAPI_phragment_getstate_Method.php
@@ -0,0 +1,103 @@
+<?php
+
+/**
+ * @group conduit
+ */
+final class ConduitAPI_phragment_getstate_Method
+ extends ConduitAPI_phragment_Method {
+
+ public function getMethodDescription() {
+ return "Retrieve the current state of a fragment by its path.";
+ }
+
+ public function defineParamTypes() {
+ return array(
+ 'path' => 'required string',
+ );
+ }
+
+ public function defineReturnType() {
+ return 'nonempty dict';
+ }
+
+ public function defineErrorTypes() {
+ return array(
+ 'ERR_BAD_FRAGMENT' => 'No such fragment exists',
+ );
+ }
+
+ protected function execute(ConduitAPIRequest $request) {
+ $path = $request->getValue('path');
+
+ $fragment = id(new PhragmentFragmentQuery())
+ ->setViewer($request->getUser())
+ ->withPaths(array($path))
+ ->executeOne();
+ if ($fragment === null) {
+ throw new ConduitException('ERR_BAD_FRAGMENT');
+ }
+
+ $mappings = $this->getFragmentMappings(
+ $request->getUser(),
+ $fragment,
+ $fragment->getPath());
+
+ $file_phids = mpull(mpull($mappings, 'getLatestVersion'), 'getFilePHID');
+ $files = id(new PhabricatorFileQuery())
+ ->setViewer($request->getUser())
+ ->withPHIDs($file_phids)
+ ->execute();
+ $files = mpull($files, null, 'getPHID');
+
+ $results = array();
+ foreach ($mappings as $path => $child) {
+ $file_phid = $child->getLatestVersion()->getFilePHID();
+ if (!isset($files[$file_phid])) {
+ // Skip any files we don't have permission to access.
+ continue;
+ }
+
+ $file = $files[$file_phid];
+ $path = substr($child->getPath(), strlen($fragment->getPath()) + 1);
+ $results[] = array(
+ 'phid' => $child->getPHID(),
+ 'phid_version' => $child->getLatestVersionPHID(),
+ 'path' => $path,
+ 'hash' => $file->getContentHash(),
+ 'version' => $child->getLatestVersion()->getSequence(),
+ 'uri' => $file->getViewURI());
+ }
+ return $results;
+ }
+
+ private function getFragmentMappings(
+ PhabricatorUser $viewer,
+ PhragmentFragment $current,
+ $base_path) {
+
+ $children = id(new PhragmentFragmentQuery())
+ ->setViewer($viewer)
+ ->needLatestVersion(true)
+ ->withLeadingPath($current->getPath().'/')
+ ->withDepths(array($current->getDepth() + 1))
+ ->execute();
+
+ if (count($children) === 0) {
+ $path = substr($current->getPath(), strlen($base_path) + 1);
+ return array($path => $current);
+ } else {
+ $mappings = array();
+ foreach ($children as $child) {
+ $child_mappings = $this->getFragmentMappings(
+ $viewer,
+ $child,
+ $base_path);
+ foreach ($child_mappings as $key => $value) {
+ $mappings[$key] = $value;
+ }
+ }
+ return $mappings;
+ }
+ }
+
+}
diff --git a/src/applications/phragment/util/PhragmentPatchUtil.php b/src/applications/phragment/util/PhragmentPatchUtil.php
--- a/src/applications/phragment/util/PhragmentPatchUtil.php
+++ b/src/applications/phragment/util/PhragmentPatchUtil.php
@@ -9,7 +9,7 @@
*
* @phutil-external-symbol class diff_match_patch
*/
- public function calculatePatch(
+ public static function calculatePatch(
PhabricatorFile $old = null,
PhabricatorFile $new = null) {

File Metadata

Mime Type
text/x-diff
Storage Engine
amazon-s3
Storage Format
Raw Data
Storage Handle
phabricator/sf/b5/7unttbaeofrdcysn
Default Alt Text
D7739.id17493.diff (12 KB)

Event Timeline