@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Provide `phragment.getstate` and `phragment.getpatch` Conduit methods

Summary:
This provides a `phragment.getstate` and a `phragment.getpatch` Conduit method.

`phragment.getstate` - This returns the current state of the fragment and all of it's children.

`phragment.getpatch` - This accepts a base path and a mapping of paths to hashes. The mapping is for the caller to specify the current state of the files it has. This returns a list of patches that the caller needs to apply to it's files to get to the latest version.

Test Plan:
Ran the following script in a folder which had content matching a fragment and it's children:

```
#!/bin/bash

STATE=""
for i in $(find ./ -type f); do
HASH=$(cat $i | sha1sum | awk '{ print $1 }')
BASE=${i:2}
STATE="$STATE,\"$BASE\":\"$HASH\""
done
STATE=${STATE:1}
STATE="{$STATE}"

echo '{"path":"tychaia3.zip","state":'$STATE'}' | arc --conduit-uri=http://phabricator.local/ call-conduit phragment.getpatch
```

and I got:

```
{"error":null,"errorMessage":null,"response":[]}
```

I updated one of the child fragments with a new file and ran the script again (patch has been omitted due to it's size):

```
{"error":null,"errorMessage":null,"response":[{"path":"Content\/TitleFont.xnb","hash_old":"4a927d7b90582e50cdd330de9f4b59b0cc5eb5c7","hash_new":"25867504642a3a403102274c68fbb9b430c1980f","patch":"..."}]}
```

Reviewers: epriestley, #blessed_reviewers

Reviewed By: epriestley

CC: Korvin, epriestley, aran, staticshock

Maniphest Tasks: T4205

Differential Revision: https://secure.phabricator.com/D7739

+341 -20
+6
src/__phutil_library_map__.php
··· 207 207 'ConduitAPI_phpast_Method' => 'applications/phpast/conduit/ConduitAPI_phpast_Method.php', 208 208 'ConduitAPI_phpast_getast_Method' => 'applications/phpast/conduit/ConduitAPI_phpast_getast_Method.php', 209 209 'ConduitAPI_phpast_version_Method' => 'applications/phpast/conduit/ConduitAPI_phpast_version_Method.php', 210 + 'ConduitAPI_phragment_Method' => 'applications/phragment/conduit/ConduitAPI_phragment_Method.php', 211 + 'ConduitAPI_phragment_getpatch_Method' => 'applications/phragment/conduit/ConduitAPI_phragment_getpatch_Method.php', 212 + 'ConduitAPI_phragment_queryfragments_Method' => 'applications/phragment/conduit/ConduitAPI_phragment_queryfragments_Method.php', 210 213 'ConduitAPI_phriction_Method' => 'applications/phriction/conduit/ConduitAPI_phriction_Method.php', 211 214 'ConduitAPI_phriction_edit_Method' => 'applications/phriction/conduit/ConduitAPI_phriction_edit_Method.php', 212 215 'ConduitAPI_phriction_history_Method' => 'applications/phriction/conduit/ConduitAPI_phriction_history_Method.php', ··· 2588 2591 'ConduitAPI_phpast_Method' => 'ConduitAPIMethod', 2589 2592 'ConduitAPI_phpast_getast_Method' => 'ConduitAPI_phpast_Method', 2590 2593 'ConduitAPI_phpast_version_Method' => 'ConduitAPI_phpast_Method', 2594 + 'ConduitAPI_phragment_Method' => 'ConduitAPIMethod', 2595 + 'ConduitAPI_phragment_getpatch_Method' => 'ConduitAPI_phragment_Method', 2596 + 'ConduitAPI_phragment_queryfragments_Method' => 'ConduitAPI_phragment_Method', 2591 2597 'ConduitAPI_phriction_Method' => 'ConduitAPIMethod', 2592 2598 'ConduitAPI_phriction_edit_Method' => 'ConduitAPI_phriction_Method', 2593 2599 'ConduitAPI_phriction_history_Method' => 'ConduitAPI_phriction_Method',
+13
src/applications/files/query/PhabricatorFileQuery.php
··· 13 13 private $transforms; 14 14 private $dateCreatedAfter; 15 15 private $dateCreatedBefore; 16 + private $contentHashes; 16 17 17 18 public function withIDs(array $ids) { 18 19 $this->ids = $ids; ··· 36 37 37 38 public function withDateCreatedAfter($date_created_after) { 38 39 $this->dateCreatedAfter = $date_created_after; 40 + return $this; 41 + } 42 + 43 + public function withContentHashes(array $content_hashes) { 44 + $this->contentHashes = $content_hashes; 39 45 return $this; 40 46 } 41 47 ··· 226 232 $conn_r, 227 233 'f.dateCreated <= %d', 228 234 $this->dateCreatedBefore); 235 + } 236 + 237 + if ($this->contentHashes) { 238 + $where[] = qsprintf( 239 + $conn_r, 240 + 'f.contentHash IN (%Ls)', 241 + $this->contentHashes); 229 242 } 230 243 231 244 return $this->formatWhereClause($where);
+13
src/applications/phragment/conduit/ConduitAPI_phragment_Method.php
··· 1 + <?php 2 + 3 + /** 4 + * @group conduit 5 + */ 6 + abstract class ConduitAPI_phragment_Method extends ConduitAPIMethod { 7 + 8 + public function getApplication() { 9 + return PhabricatorApplication::getByClass( 10 + 'PhabricatorApplicationPhragment'); 11 + } 12 + 13 + }
+185
src/applications/phragment/conduit/ConduitAPI_phragment_getpatch_Method.php
··· 1 + <?php 2 + 3 + /** 4 + * @group conduit 5 + */ 6 + final class ConduitAPI_phragment_getpatch_Method 7 + extends ConduitAPI_phragment_Method { 8 + 9 + public function getMethodStatus() { 10 + return self::METHOD_STATUS_UNSTABLE; 11 + } 12 + 13 + public function getMethodDescription() { 14 + return pht("Retrieve the patches to apply for a given set of files."); 15 + } 16 + 17 + public function defineParamTypes() { 18 + return array( 19 + 'path' => 'required string', 20 + 'state' => 'required dict<string, string>', 21 + ); 22 + } 23 + 24 + public function defineReturnType() { 25 + return 'nonempty dict'; 26 + } 27 + 28 + public function defineErrorTypes() { 29 + return array( 30 + 'ERR_BAD_FRAGMENT' => 'No such fragment exists', 31 + ); 32 + } 33 + 34 + protected function execute(ConduitAPIRequest $request) { 35 + $path = $request->getValue('path'); 36 + $state = $request->getValue('state'); 37 + // The state is an array mapping file paths to hashes. 38 + 39 + $patches = array(); 40 + 41 + // We need to get all of the mappings (like phragment.getstate) first 42 + // so that we can detect deletions and creations of files. 43 + $fragment = id(new PhragmentFragmentQuery()) 44 + ->setViewer($request->getUser()) 45 + ->withPaths(array($path)) 46 + ->executeOne(); 47 + if ($fragment === null) { 48 + throw new ConduitException('ERR_BAD_FRAGMENT'); 49 + } 50 + 51 + $mappings = $fragment->getFragmentMappings( 52 + $request->getUser(), 53 + $fragment->getPath()); 54 + 55 + $file_phids = mpull(mpull($mappings, 'getLatestVersion'), 'getFilePHID'); 56 + $files = id(new PhabricatorFileQuery()) 57 + ->setViewer($request->getUser()) 58 + ->withPHIDs($file_phids) 59 + ->execute(); 60 + $files = mpull($files, null, 'getPHID'); 61 + 62 + // Scan all of the files that the caller currently has and iterate 63 + // over that. 64 + foreach ($state as $path => $hash) { 65 + // If $mappings[$path] exists, then the user has the file and it's 66 + // also a fragment. 67 + if (array_key_exists($path, $mappings)) { 68 + $file_phid = $mappings[$path]->getLatestVersion()->getFilePHID(); 69 + if ($file_phid !== null) { 70 + // If the file PHID is present, then we need to check the 71 + // hashes to see if they are the same. 72 + $hash_caller = strtolower($state[$path]); 73 + $hash_current = $files[$file_phid]->getContentHash(); 74 + if ($hash_caller === $hash_current) { 75 + // The user's version is identical to our version, so 76 + // there is no update needed. 77 + } else { 78 + // The hash differs, and the user needs to update. 79 + $patches[] = array( 80 + 'path' => $path, 81 + 'fileOld' => null, 82 + 'fileNew' => $files[$file_phid], 83 + 'hashOld' => $hash_caller, 84 + 'hashNew' => $hash_current, 85 + 'patchURI' => null); 86 + } 87 + } else { 88 + // We have a record of this as a file, but there is no file 89 + // attached to the latest version, so we consider this to be 90 + // a deletion. 91 + $patches[] = array( 92 + 'path' => $path, 93 + 'fileOld' => null, 94 + 'fileNew' => null, 95 + 'hashOld' => $hash_caller, 96 + 'hashNew' => PhragmentPatchUtil::EMPTY_HASH, 97 + 'patchURI' => null); 98 + } 99 + } else { 100 + // If $mappings[$path] does not exist, then the user has a file, 101 + // and we have absolutely no record of it what-so-ever (we haven't 102 + // even recorded a deletion). Assuming most applications will store 103 + // some form of data near their own files, this is probably a data 104 + // file relevant for the application that is not versioned, so we 105 + // don't tell the client to do anything with it. 106 + } 107 + } 108 + 109 + // Check the remaining files that we know about but the caller has 110 + // not reported. 111 + foreach ($mappings as $path => $child) { 112 + if (array_key_exists($path, $state)) { 113 + // We have already evaluated this above. 114 + } else { 115 + $file_phid = $mappings[$path]->getLatestVersion()->getFilePHID(); 116 + if ($file_phid !== null) { 117 + // If the file PHID is present, then this is a new file that 118 + // we know about, but the caller does not. We need to tell 119 + // the caller to create the file. 120 + $hash_current = $files[$file_phid]->getContentHash(); 121 + $patches[] = array( 122 + 'path' => $path, 123 + 'fileOld' => null, 124 + 'fileNew' => $files[$file_phid], 125 + 'hashOld' => PhragmentPatchUtil::EMPTY_HASH, 126 + 'hashNew' => $hash_current, 127 + 'patchURI' => null); 128 + } else { 129 + // We have a record of deleting this file, and the caller hasn't 130 + // reported it, so they've probably deleted it in a previous 131 + // update. 132 + } 133 + } 134 + } 135 + 136 + // Before we can calculate patches, we need to resolve the old versions 137 + // of files so we can draw diffs on them. 138 + $hashes = array(); 139 + foreach ($patches as $patch) { 140 + if ($patch["hashOld"] !== PhragmentPatchUtil::EMPTY_HASH) { 141 + $hashes[] = $patch["hashOld"]; 142 + } 143 + } 144 + $old_files = array(); 145 + if (count($hashes) !== 0) { 146 + $old_files = id(new PhabricatorFileQuery()) 147 + ->setViewer($request->getUser()) 148 + ->withContentHashes($hashes) 149 + ->execute(); 150 + } 151 + $old_files = mpull($old_files, null, 'getContentHash'); 152 + foreach ($patches as $key => $patch) { 153 + if ($patch["hashOld"] !== PhragmentPatchUtil::EMPTY_HASH) { 154 + if (array_key_exists($patch['hashOld'], $old_files)) { 155 + $patches[$key]['fileOld'] = $old_files[$patch['hashOld']]; 156 + } else { 157 + // We either can't see or can't read the old file. 158 + $patches[$key]['hashOld'] = PhragmentPatchUtil::EMPTY_HASH; 159 + $patches[$key]['fileOld'] = null; 160 + } 161 + } 162 + } 163 + 164 + // Now run through all of the patch entries, calculate the patches 165 + // and return the results. 166 + foreach ($patches as $key => $patch) { 167 + $data = PhragmentPatchUtil::calculatePatch( 168 + $patches[$key]['fileOld'], 169 + $patches[$key]['fileNew']); 170 + unset($patches[$key]['fileOld']); 171 + unset($patches[$key]['fileNew']); 172 + 173 + $file = PhabricatorFile::buildFromFileDataOrHash( 174 + $data, 175 + array( 176 + 'name' => 'patch.dmp', 177 + 'ttl' => time() + 60 * 60 * 24, 178 + )); 179 + $patches[$key]['patchURI'] = $file->getDownloadURI(); 180 + } 181 + 182 + return $patches; 183 + } 184 + 185 + }
+83
src/applications/phragment/conduit/ConduitAPI_phragment_queryfragments_Method.php
··· 1 + <?php 2 + 3 + /** 4 + * @group conduit 5 + */ 6 + final class ConduitAPI_phragment_queryfragments_Method 7 + extends ConduitAPI_phragment_Method { 8 + 9 + public function getMethodStatus() { 10 + return self::METHOD_STATUS_UNSTABLE; 11 + } 12 + 13 + public function getMethodDescription() { 14 + return pht("Query fragments based on their paths."); 15 + } 16 + 17 + public function defineParamTypes() { 18 + return array( 19 + 'paths' => 'required list<string>', 20 + ); 21 + } 22 + 23 + public function defineReturnType() { 24 + return 'nonempty dict'; 25 + } 26 + 27 + public function defineErrorTypes() { 28 + return array( 29 + 'ERR_BAD_FRAGMENT' => 'No such fragment exists', 30 + ); 31 + } 32 + 33 + protected function execute(ConduitAPIRequest $request) { 34 + $paths = $request->getValue('paths'); 35 + 36 + $fragments = id(new PhragmentFragmentQuery()) 37 + ->setViewer($request->getUser()) 38 + ->withPaths($paths) 39 + ->execute(); 40 + $fragments = mpull($fragments, null, 'getPath'); 41 + foreach ($paths as $path) { 42 + if (!array_key_exists($path, $fragments)) { 43 + throw new ConduitException('ERR_BAD_FRAGMENT'); 44 + } 45 + } 46 + 47 + $results = array(); 48 + foreach ($fragments as $path => $fragment) { 49 + $mappings = $fragment->getFragmentMappings( 50 + $request->getUser(), 51 + $fragment->getPath()); 52 + 53 + $file_phids = mpull(mpull($mappings, 'getLatestVersion'), 'getFilePHID'); 54 + $files = id(new PhabricatorFileQuery()) 55 + ->setViewer($request->getUser()) 56 + ->withPHIDs($file_phids) 57 + ->execute(); 58 + $files = mpull($files, null, 'getPHID'); 59 + 60 + $result = array(); 61 + foreach ($mappings as $cpath => $child) { 62 + $file_phid = $child->getLatestVersion()->getFilePHID(); 63 + if (!isset($files[$file_phid])) { 64 + // Skip any files we don't have permission to access. 65 + continue; 66 + } 67 + 68 + $file = $files[$file_phid]; 69 + $cpath = substr($child->getPath(), strlen($fragment->getPath()) + 1); 70 + $result[] = array( 71 + 'phid' => $child->getPHID(), 72 + 'phidVersion' => $child->getLatestVersionPHID(), 73 + 'path' => $cpath, 74 + 'hash' => $file->getContentHash(), 75 + 'version' => $child->getLatestVersion()->getSequence(), 76 + 'uri' => $file->getViewURI()); 77 + } 78 + $results[$path] = $result; 79 + } 80 + return $results; 81 + } 82 + 83 + }
+9 -20
src/applications/phragment/controller/PhragmentZIPController.php
··· 111 111 * Returns a list of mappings like array('some/path.txt' => 'file PHID'); 112 112 */ 113 113 private function getFragmentMappings(PhragmentFragment $current, $base_path) { 114 - $children = id(new PhragmentFragmentQuery()) 115 - ->setViewer($this->getRequest()->getUser()) 116 - ->needLatestVersion(true) 117 - ->withLeadingPath($current->getPath().'/') 118 - ->withDepths(array($current->getDepth() + 1)) 119 - ->execute(); 114 + $mappings = $current->getFragmentMappings( 115 + $this->getRequest()->getUser(), 116 + $base_path); 120 117 121 - if (count($children) === 0) { 122 - $path = substr($current->getPath(), strlen($base_path) + 1); 123 - if ($this->getVersion($current) === null) { 124 - return array(); 118 + $result = array(); 119 + foreach ($mappings as $path => $fragment) { 120 + $version = $this->getVersion($fragment); 121 + if ($version !== null) { 122 + $result[$path] = $version->getFilePHID(); 125 123 } 126 - return array($path => $this->getVersion($current)->getFilePHID()); 127 - } else { 128 - $mappings = array(); 129 - foreach ($children as $child) { 130 - $child_mappings = $this->getFragmentMappings($child, $base_path); 131 - foreach ($child_mappings as $key => $value) { 132 - $mappings[$key] = $value; 133 - } 134 - } 135 - return $mappings; 136 124 } 125 + return $result; 137 126 } 138 127 139 128 private function getVersion($fragment) {
+32
src/applications/phragment/storage/PhragmentFragment.php
··· 276 276 } 277 277 278 278 279 + /* -( Utility ) ---------------------------------------------------------- */ 280 + 281 + 282 + public function getFragmentMappings( 283 + PhabricatorUser $viewer, 284 + $base_path) { 285 + 286 + $children = id(new PhragmentFragmentQuery()) 287 + ->setViewer($viewer) 288 + ->needLatestVersion(true) 289 + ->withLeadingPath($this->getPath().'/') 290 + ->withDepths(array($this->getDepth() + 1)) 291 + ->execute(); 292 + 293 + if (count($children) === 0) { 294 + $path = substr($this->getPath(), strlen($base_path) + 1); 295 + return array($path => $this); 296 + } else { 297 + $mappings = array(); 298 + foreach ($children as $child) { 299 + $child_mappings = $child->getFragmentMappings( 300 + $viewer, 301 + $base_path); 302 + foreach ($child_mappings as $key => $value) { 303 + $mappings[$key] = $value; 304 + } 305 + } 306 + return $mappings; 307 + } 308 + } 309 + 310 + 279 311 /* -( Policy Interface )--------------------------------------------------- */ 280 312 281 313