Skip to content

Commit ec77c00

Browse files
authored
Merge pull request #79 from eeditiones/fix/betmas-collections
Make tuttle work with very active repos that are deployed into a subcollection
2 parents ca3f9d8 + f643e25 commit ec77c00

File tree

6 files changed

+150
-26
lines changed

6 files changed

+150
-26
lines changed

src/index.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ <h1><img class="tuttle" src="images/HPTuttle-1866.png"></img>
8989
<fx-submission id="fetch"
9090
method="get"
9191
replace="none"
92-
url="./git/{instance('default')/repos/repo[index('list')]/@collection}"
92+
url="./git/{instance('default')/repos/repo[index('list')]/@collection => encode-for-uri()}"
9393
serialization="none">
9494

9595
<fx-action event="submit">
@@ -109,7 +109,7 @@ <h1><img class="tuttle" src="images/HPTuttle-1866.png"></img>
109109
method="post"
110110
replace="instance"
111111
instance="response"
112-
url="./git/{instance('default')/repos/repo[index('list')]/@collection}"
112+
url="./git/{instance('default')/repos/repo[index('list')]/@collection => encode-for-uri()}"
113113
serialization="none">
114114

115115
<fx-action event="submit-done">
@@ -122,7 +122,7 @@ <h1><img class="tuttle" src="images/HPTuttle-1866.png"></img>
122122
</fx-submission>
123123

124124
<fx-submission id="update"
125-
url="./git/{instance('default')/repos/repo[index('list')]/@collection}/incremental"
125+
url="./git/{instance('default')/repos/repo[index('list')]/@collection => encode-for-uri()}/incremental"
126126
replace="instance"
127127
instance="response"
128128
method="post">
@@ -138,7 +138,7 @@ <h1><img class="tuttle" src="images/HPTuttle-1866.png"></img>
138138
<fx-send submission="config"></fx-send>
139139
</fx-action>
140140
<fx-action event="submit-error">
141-
<fx-message>Udpate Failed!</fx-message>
141+
<fx-message>Update Failed!</fx-message>
142142
<fx-setvalue ref="instance('vars')/inprogress"></fx-setvalue>
143143
</fx-action>
144144
</fx-submission>
@@ -213,4 +213,4 @@ <h3>Git Repositories</h3>
213213
<script type="module" src="js/fore-all.js"></script>
214214
</div>
215215
</body>
216-
</html>
216+
</html>

src/modules/api.xq

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,7 @@ declare %private function api:get-collection-config($collection as xs:string?) a
551551
if (empty($git-collection))
552552
then error((), "git collection not found!")
553553
else if (empty($collection-config))
554-
then error((), "collection config not found!")
554+
then error((), "collection config " || $git-collection || " not found!")
555555
else $collection-config
556556
};
557557

src/modules/github.xqm

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import module namespace config="http://e-editiones.org/tuttle/config" at "config
99

1010
declare namespace http="http://expath.org/ns/http-client";
1111

12+
declare variable $github:max-page-size := 100;
13+
declare variable $github:max-total-result-size := 500;
14+
1215
declare function github:repo-url($config as map(*)) as xs:string {
1316
``[`{$config?baseurl}`repos/`{$config?owner}`/`{$config?repo}`]``
1417
};
@@ -74,7 +77,7 @@ declare function github:get-last-commit($config as map(*)) as map(*) {
7477
: Get all commits
7578
:)
7679
declare function github:get-commits($config as map(*)) as array(*)* {
77-
github:get-commits($config, 100)
80+
github:get-commits($config, $github:max-page-size)
7881
};
7982

8083
(:~
@@ -107,23 +110,50 @@ declare %private function github:short-commit-info ($commit-info as map(*)) as a
107110
: Get commits in full
108111
:)
109112
declare function github:get-raw-commits($config as map(*), $count as xs:integer) as array(*)? {
110-
github:request-json-ignore-pages(
111-
github:commit-ref-url($config, $count), $config?token)
113+
github:get-raw-commits($config, $count, ())
114+
};
115+
116+
(:~
117+
: Get commits in full, going over pages until we find the commit with the correct hash
118+
:)
119+
declare function github:get-raw-commits (
120+
$config as map(*),
121+
$count as xs:integer,
122+
$stop-at-commit-id as xs:string?
123+
) as array(*)? {
124+
let $stop-condition := if (empty($stop-at-commit-id)) then
125+
function ($_) {
126+
(: We are not looking for any SHA. Prevent going over all of the commits in a possibly big repo :)
127+
true()
128+
}
129+
else
130+
function ($results-on-page) {
131+
(: We can stop searching once we have all the commits between 'now' and the commit we looked for :)
132+
let $found-commits := $results-on-page?*?sha
133+
return $found-commits = $stop-at-commit-id
134+
}
135+
136+
let $results := github:request-json-all-pages(
137+
github:commit-ref-url($config, $count),
138+
$config?token,
139+
$stop-condition
140+
)
141+
return array { $results?* }
112142
};
113143

114144
(:~
115145
: Get diff between production collection and github-newest
116146
:)
117147
declare function github:get-newest-commits($config as map(*)) as xs:string* {
118148
let $deployed := $config?deployed
119-
let $commits := github:get-raw-commits($config, 100)
149+
let $commits := github:get-raw-commits($config, $github:max-page-size, $deployed)
120150
let $sha := $commits?*?sha
121151
let $how-many := index-of($sha, $deployed) - 1
122152
return
123153
if (empty($how-many)) then (
124154
error(
125155
xs:QName("github:commit-not-found"),
126-
'The deployed commit hash ' || $deployed || ' was not found in the list of commits on the remote.')
156+
'The deployed commit hash ' || $deployed || ' was not found in the list of commits on the remote. Tuttle can only process incremental upgrades of ' || $github:max-total-result-size || '.')
127157
) else (
128158
reverse(subsequence($sha, 1, $how-many))
129159
)
@@ -218,7 +248,7 @@ declare function github:incremental($config as map(*)) {
218248
:)
219249
declare function github:get-commit-files($config as map(*), $sha as xs:string) as array(*) {
220250
let $url := github:repo-url($config) || "/commits/" || $sha
221-
let $commit := github:request-json-all-pages($url, $config?token, ())
251+
let $commit := github:request-json-all-pages($url, $config?token)
222252

223253
return array { $commit?files?* }
224254
};
@@ -361,7 +391,32 @@ declare %private function github:request-json($url as xs:string, $token as xs:st
361391
)
362392
};
363393

364-
declare %private function github:request-json-all-pages($url as xs:string, $token as xs:string?, $acc) {
394+
(:~
395+
: Get all pages of a specified URL. Github has some paginated endpoints, This function traverses all of
396+
: those and joins the results.
397+
:)
398+
declare %private function github:request-json-all-pages($url as xs:string, $token as xs:string?) {
399+
github:request-json-all-pages($url, $token, function ($_) { (: Traverse all pages :) false() }, ())
400+
};
401+
402+
(:~
403+
: Overload, adds the $stop-condition callback which is given the contents of the current page
404+
: return `true()` to indicate there are sufficient results and we can stop
405+
:)
406+
declare %private function github:request-json-all-pages(
407+
$url as xs:string,
408+
$token as xs:string?,
409+
$stop-condition as function(map(*)) as xs:boolean
410+
) {
411+
github:request-json-all-pages($url, $token, $stop-condition, ())
412+
};
413+
414+
declare %private function github:request-json-all-pages(
415+
$url as xs:string,
416+
$token as xs:string?,
417+
$stop-condition as function(map(*)) as xs:boolean,
418+
$acc
419+
) {
365420
let $response :=
366421
app:request-json(
367422
github:build-request($url, (
@@ -374,13 +429,16 @@ declare %private function github:request-json-all-pages($url as xs:string, $toke
374429
github:parse-link-header($response[1]/http:header[@name="link"]/@value)?next
375430
) else ()
376431

377-
let $all := ($acc, $response[2])
432+
let $results-in-this-page := $response[2]
433+
let $all := ($acc, $results-in-this-page)
434+
435+
let $should-stop := $stop-condition($results-in-this-page)
378436

379437
return (
380-
if (exists($next-url)) then (
381-
github:request-json-all-pages($next-url, $token, $all)
438+
if (not($should-stop) and count($all?*) < $github:max-total-result-size and exists($next-url)) then (
439+
github:request-json-all-pages($next-url, $token, $stop-condition, $all)
382440
) else (
383-
$all
441+
$all
384442
)
385443
)
386444
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<tuttle>
2+
<repos>
3+
<!-- A version of tuttle sample data with tons of commits so updates go over multiple pages -->
4+
<collection name="tuttle-sample-data">
5+
<default>true</default>
6+
<type>github</type>
7+
<baseurl>https://api.github.com/</baseurl>
8+
<repo>tuttle-sample-data</repo>
9+
<owner>eeditiones</owner>
10+
<token>XXX</token>
11+
<ref>long-history</ref>
12+
<hookuser>admin</hookuser>
13+
<hookpasswd />
14+
</collection>
15+
</repos>
16+
<ignore>
17+
<file>existdb.json</file>
18+
<file>build.xml</file>
19+
<file>README.md</file>
20+
<file>.gitignore</file>
21+
<file>expath-pkg.xml.tmpl</file>
22+
<file>repo.xml.tmpl</file>
23+
<file>build.properties.xml</file>
24+
</ignore>
25+
<config
26+
apikeys="/db/system/auth/tuttle-token.xml"
27+
lock="git-lock.xml"
28+
prefix="/db/apps/"
29+
suffix="-stage"
30+
><sm group="nogroup" mode="rw-r--r--" user="nobody" /></config>
31+
</tuttle>

test/fixtures/alt-tuttle.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,4 @@
3636
<config prefix="/db/apps/" suffix="-stage" lock="git-lock.xml" apikeys="/db/system/auth/tuttle-token.xml">
3737
<sm user="nobody" group="nogroup" mode="rw-r--r--"/>
3838
</config>
39-
</tuttle>
39+
</tuttle>

test/tuttle.js

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export default () =>
110110
await assert.doesNotReject(resultPromise, 'The request should succeed');
111111
res = await resultPromise;
112112
})
113-
113+
114114
it('returns status 200', async function () {
115115
assert.strictEqual(res.status, 200);
116116
});
@@ -176,27 +176,62 @@ export default () =>
176176
it('can also write hashes to repo.xml', async () => {
177177
await remove();
178178
await install();
179-
179+
180180
// Set up tuttle with a repo where repo.xml is used to store the git sha info
181181
const buffer = await readFile('./test/fixtures/alt-repoxml-tuttle.xml');
182182
await putResource(buffer, '/db/apps/tuttle/data/tuttle.xml');
183-
183+
184184
const resultPromise = axios.get('git/status', { auth });
185185
await assert.doesNotReject(resultPromise);
186-
186+
187187
const stagingPromise = axios.get('git/tuttle-sample-data', { auth });
188188
await assert.doesNotReject(stagingPromise, 'The request should succeed');
189189

190190
const deployPromise = axios.post('git/tuttle-sample-data', {}, { auth });
191191
await assert.doesNotReject(deployPromise, 'The request should succeed');
192-
192+
193193
const repoXML = await getResource('/db/apps/tuttle-sample-data/repo.xml');
194-
194+
195195
const repo = new DOMParser().parseFromString(repoXML.toString(), 'text/xml').documentElement;
196196
assert.ok(repo.getAttribute('commit-id'), 'The commit id should be set');
197197
assert.ok(repo.getAttribute('commit-time'), 'The commit time should be set');
198198
assert.ok(repo.getAttribute('commit-date'), 'The commit date should be set');
199199
});
200-
201-
});
202200

201+
describe('large histories', async () => {
202+
before(async () => {
203+
await remove();
204+
await install();
205+
});
206+
207+
await it('can upgrade over a few hundred commits', async () => {
208+
// Set up tuttle with a repo with a ton of commits that it can upgrade over
209+
const buffer = await readFile('./test/fixtures/alt-big-repo-tuttle.xml');
210+
await putResource(buffer, '/db/apps/tuttle/data/tuttle.xml');
211+
212+
const OLD_HASH = '41188098f120b6e70d1b0c3bb704a422eba43dfa';
213+
const stageOldVersionPromise = axios.get(`git/tuttle-sample-data?hash=${OLD_HASH}`, { auth });
214+
await assert.doesNotReject(stageOldVersionPromise);
215+
const deployOldVersionPromise = axios.post('git/tuttle-sample-data', {}, { auth });
216+
await assert.doesNotReject(deployOldVersionPromise, 'The request should succeed');
217+
218+
const beforeString = await getResource('/db/apps/tuttle-sample-data/data/regular-changing-document.xml');
219+
220+
const before = new DOMParser().parseFromString(beforeString.toString(), 'text/xml').documentElement;
221+
assert.strictEqual(before.textContent, 'Initial version');
222+
223+
console.log('deployed older version of the sample data on the long-history branch')
224+
225+
const resultPromise = axios.get('git/status', { auth });
226+
await assert.doesNotReject(resultPromise);
227+
228+
const incrementalPromise = axios.post('git/tuttle-sample-data/incremental', {}, { auth });
229+
await assert.doesNotReject(incrementalPromise, 'The incremental request should succeed');
230+
231+
const afterString = await getResource('/db/apps/tuttle-sample-data/data/regular-changing-document.xml');
232+
233+
const after = new DOMParser().parseFromString(afterString.toString(), 'text/xml').documentElement;
234+
assert.strictEqual(after.textContent, 'change for 200');
235+
});
236+
});
237+
});

0 commit comments

Comments
 (0)