Skip to content

Commit b8b7bf8

Browse files
committed
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
1 parent 270c8d2 commit b8b7bf8

File tree

7 files changed

+342
-21
lines changed

7 files changed

+342
-21
lines changed

src/__phutil_library_map__.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,9 @@
207207
'ConduitAPI_phpast_Method' => 'applications/phpast/conduit/ConduitAPI_phpast_Method.php',
208208
'ConduitAPI_phpast_getast_Method' => 'applications/phpast/conduit/ConduitAPI_phpast_getast_Method.php',
209209
'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',
210213
'ConduitAPI_phriction_Method' => 'applications/phriction/conduit/ConduitAPI_phriction_Method.php',
211214
'ConduitAPI_phriction_edit_Method' => 'applications/phriction/conduit/ConduitAPI_phriction_edit_Method.php',
212215
'ConduitAPI_phriction_history_Method' => 'applications/phriction/conduit/ConduitAPI_phriction_history_Method.php',
@@ -2588,6 +2591,9 @@
25882591
'ConduitAPI_phpast_Method' => 'ConduitAPIMethod',
25892592
'ConduitAPI_phpast_getast_Method' => 'ConduitAPI_phpast_Method',
25902593
'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',
25912597
'ConduitAPI_phriction_Method' => 'ConduitAPIMethod',
25922598
'ConduitAPI_phriction_edit_Method' => 'ConduitAPI_phriction_Method',
25932599
'ConduitAPI_phriction_history_Method' => 'ConduitAPI_phriction_Method',

src/applications/files/query/PhabricatorFileQuery.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ final class PhabricatorFileQuery
1313
private $transforms;
1414
private $dateCreatedAfter;
1515
private $dateCreatedBefore;
16+
private $contentHashes;
1617

1718
public function withIDs(array $ids) {
1819
$this->ids = $ids;
@@ -39,6 +40,11 @@ public function withDateCreatedAfter($date_created_after) {
3940
return $this;
4041
}
4142

43+
public function withContentHashes(array $content_hashes) {
44+
$this->contentHashes = $content_hashes;
45+
return $this;
46+
}
47+
4248
/**
4349
* Select files which are transformations of some other file. For example,
4450
* you can use this query to find previously generated thumbnails of an image
@@ -228,6 +234,13 @@ private function buildWhereClause(AphrontDatabaseConnection $conn_r) {
228234
$this->dateCreatedBefore);
229235
}
230236

237+
if ($this->contentHashes) {
238+
$where[] = qsprintf(
239+
$conn_r,
240+
'f.contentHash IN (%Ls)',
241+
$this->contentHashes);
242+
}
243+
231244
return $this->formatWhereClause($where);
232245
}
233246

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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+
}

src/applications/phragment/controller/PhragmentZIPController.php

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -111,29 +111,18 @@ public function processRequest() {
111111
* Returns a list of mappings like array('some/path.txt' => 'file PHID');
112112
*/
113113
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();
120-
121-
if (count($children) === 0) {
122-
$path = substr($current->getPath(), strlen($base_path) + 1);
123-
if ($this->getVersion($current) === null) {
124-
return array();
125-
}
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-
}
114+
$mappings = $current->getFragmentMappings(
115+
$this->getRequest()->getUser(),
116+
$base_path);
117+
118+
$result = array();
119+
foreach ($mappings as $path => $fragment) {
120+
$version = $this->getVersion($fragment);
121+
if ($version !== null) {
122+
$result[$path] = $version->getFilePHID();
134123
}
135-
return $mappings;
136124
}
125+
return $result;
137126
}
138127

139128
private function getVersion($fragment) {

0 commit comments

Comments
 (0)