@agentshouse/kit 0.2.1-alpha.8 → 0.2.1-c02dacffca87c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -24
- package/dist/abort.js +9 -0
- package/dist/acp.js +102 -88
- package/dist/agents.js +356 -52
- package/dist/api.js +40 -4
- package/dist/app-git.js +48 -0
- package/dist/authority-main.js +2 -2
- package/dist/bridge.js +93 -78
- package/dist/building.js +50 -0
- package/dist/calls.js +71 -0
- package/dist/chatgpt.js +32 -0
- package/dist/chunks.js +49 -0
- package/dist/clis.js +224 -88
- package/dist/codex-main.js +58 -0
- package/dist/configure-main.js +6 -0
- package/dist/connection.js +105 -0
- package/dist/conversations.js +749 -268
- package/dist/copies.js +475 -260
- package/dist/end.js +33 -0
- package/dist/enrol-main.js +7 -1
- package/dist/events.js +546 -0
- package/dist/files.js +140 -17
- package/dist/git-main.js +2 -2
- package/dist/git.js +62 -22
- package/dist/home.js +134 -10
- package/dist/house-cli.js +299 -0
- package/dist/house-main.js +5 -102
- package/dist/instructions.js +17 -18
- package/dist/kit-cli.js +68 -0
- package/dist/kit-main.js +8 -16
- package/dist/login.js +130 -95
- package/dist/logout.js +19 -0
- package/dist/mcp.js +43 -0
- package/dist/operation.js +7 -4
- package/dist/parts.js +82 -0
- package/dist/placement.js +31 -0
- package/dist/refusals.js +53 -0
- package/dist/relay.js +41 -0
- package/dist/resident.js +145 -51
- package/dist/scope.js +214 -0
- package/dist/scripts.js +255 -0
- package/dist/secret-input.js +7 -2
- package/dist/service-main.js +9 -0
- package/dist/service.js +55 -0
- package/dist/shell.js +3 -0
- package/dist/sign-in.js +52 -32
- package/dist/silence.js +28 -0
- package/dist/skills.js +92 -0
- package/dist/transcription.js +210 -0
- package/dist/translate.js +2 -8
- package/dist/update.js +127 -0
- package/dist/wire.js +40 -0
- package/dist/work.js +346 -0
- package/package.json +20 -21
- package/dist/blocks.js +0 -51
- package/dist/bootstrap.json +0 -28
- package/dist/stream.js +0 -47
package/dist/copies.js
CHANGED
|
@@ -1,33 +1,79 @@
|
|
|
1
1
|
import { parse } from '@agentshouse/mdmodel';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { mkdir, readFile, readdir, realpath, rename, rm, rmdir, stat, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
|
-
import { HouseRefusal } from './api.js';
|
|
6
|
-
import {
|
|
5
|
+
import { finalRefusal, HouseRefusal, retrying } from './api.js';
|
|
6
|
+
import { chunkTexts, stagedChunks } from './chunks.js';
|
|
7
|
+
import { blobId, blobText, git, isText, HOUSE_REF, importCommits, localCopies, revision, run, succeeded, tree, } from './git.js';
|
|
8
|
+
import { paused } from './files.js';
|
|
7
9
|
import { kitHome } from './home.js';
|
|
8
10
|
import { operationId } from './operation.js';
|
|
11
|
+
import { refusalOf } from './refusals.js';
|
|
9
12
|
import { changed } from './translate.js';
|
|
10
13
|
const BRANCH = 'main';
|
|
11
|
-
const ROOM_HOMES = ['capture', 'library', 'collaboration'];
|
|
12
14
|
const PROVENANCE = '_provenance';
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
+
const IDENTITY = { GIT_COMMITTER_NAME: 'Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
|
|
16
|
+
const ROOMS = 'rooms/';
|
|
17
|
+
const ENUMERATE = '/kit/door/enumerate';
|
|
18
|
+
const NOT_READY = 'not ready yet; Kit copies it once House has built it';
|
|
19
|
+
const STAGED_CHUNKS = 'house-chunks';
|
|
20
|
+
// A Room answered not ready is asked again on this schedule: its chunk build usually catches up within seconds, so the first asks come soon and the last repeats until it does.
|
|
21
|
+
const ASKED_AGAIN_MS = [1000, 2000, 3000, 5000, 8000, 11_000, 15_000];
|
|
22
|
+
class NotReady extends Error {
|
|
23
|
+
rooms;
|
|
24
|
+
constructor(rooms) {
|
|
25
|
+
super(NOT_READY);
|
|
26
|
+
this.rooms = rooms;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
15
29
|
function causeOf(error) {
|
|
16
30
|
return error instanceof Error ? error.message : String(error);
|
|
17
31
|
}
|
|
32
|
+
function houseCause(error) {
|
|
33
|
+
return error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
|
|
34
|
+
}
|
|
18
35
|
function logged(error) {
|
|
19
36
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
20
37
|
}
|
|
21
|
-
|
|
38
|
+
const REBASED = new Set([
|
|
39
|
+
'field_base_stale',
|
|
40
|
+
'section_base_stale',
|
|
41
|
+
'preamble_base_stale',
|
|
42
|
+
'base_revision_stale',
|
|
43
|
+
'path_exists',
|
|
44
|
+
'path_absent',
|
|
45
|
+
]);
|
|
46
|
+
function rebaseFixes(refusal) {
|
|
47
|
+
return refusal.code === 'edit_conflict' && refusal.conflicts.some((code) => REBASED.has(code));
|
|
48
|
+
}
|
|
49
|
+
function textOnly(paths) {
|
|
50
|
+
const [named, them] = paths.length === 1 ? [`${paths[0]} is`, 'it'] : [`${paths.join(', ')} are`, 'them'];
|
|
51
|
+
return `${named} not text, and house push sends text only; remove ${them} from the commit and place ${them} with house upload`;
|
|
52
|
+
}
|
|
53
|
+
function outside(path) {
|
|
54
|
+
return { refused: true, text: `${path} is not a House Local copy; run house push inside one` };
|
|
55
|
+
}
|
|
56
|
+
function short(commit) {
|
|
57
|
+
return commit.slice(0, 12);
|
|
58
|
+
}
|
|
59
|
+
function moved(before, after) {
|
|
60
|
+
if (before.house !== after.house)
|
|
61
|
+
return [`${rootOf(after.room_handle)}: ${HOUSE_REF} is at ${short(after.house)}`];
|
|
62
|
+
return before.position === after.position ? [] : [`${rootOf(after.room_handle)}: read-only files refreshed`];
|
|
63
|
+
}
|
|
64
|
+
function expiredAuthority(error) {
|
|
65
|
+
if (!(error instanceof HouseRefusal))
|
|
66
|
+
return undefined;
|
|
22
67
|
try {
|
|
23
|
-
|
|
68
|
+
const { error: refused } = JSON.parse(error.text);
|
|
69
|
+
return refused?.code === 'expired' && typeof refused.authority === 'string' ? refused.authority : undefined;
|
|
24
70
|
}
|
|
25
71
|
catch {
|
|
26
|
-
return
|
|
72
|
+
return undefined;
|
|
27
73
|
}
|
|
28
74
|
}
|
|
29
|
-
function
|
|
30
|
-
return
|
|
75
|
+
function grouped(items, size) {
|
|
76
|
+
return items.flatMap((_, index) => (index % size === 0 ? [items.slice(index, index + size)] : []));
|
|
31
77
|
}
|
|
32
78
|
function covers(path, prefix) {
|
|
33
79
|
return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
|
|
@@ -37,14 +83,17 @@ function sidecar(path) {
|
|
|
37
83
|
segments.splice(-1, 0, PROVENANCE);
|
|
38
84
|
return segments.join('/');
|
|
39
85
|
}
|
|
40
|
-
function
|
|
41
|
-
return
|
|
86
|
+
function recordOf(key) {
|
|
87
|
+
return join(kitHome(), 'local-copies', `${key}.json`);
|
|
42
88
|
}
|
|
43
|
-
function
|
|
44
|
-
return
|
|
89
|
+
function stagingPrefix() {
|
|
90
|
+
return `.${createHash('sha256').update(kitHome()).digest('hex').slice(0, 16)}-`;
|
|
91
|
+
}
|
|
92
|
+
function rootOf(handle) {
|
|
93
|
+
return `/rooms/${handle}`;
|
|
45
94
|
}
|
|
46
|
-
function
|
|
47
|
-
return
|
|
95
|
+
function relative(path) {
|
|
96
|
+
return path.replace(/^\/rooms\/[^/]+\//, '');
|
|
48
97
|
}
|
|
49
98
|
function declaredWritable(path, copy) {
|
|
50
99
|
const deepest = (scopes) => Math.max(-1, ...scopes.filter((scope) => covers(path, scope)).map((scope) => scope.length));
|
|
@@ -67,32 +116,59 @@ async function removeFile(repository, path) {
|
|
|
67
116
|
return;
|
|
68
117
|
}
|
|
69
118
|
}
|
|
70
|
-
async function
|
|
119
|
+
async function readOnlyFiles(repository, held) {
|
|
71
120
|
const tracked = new Set((await git(repository, ['ls-files', '-z'])).split('\0'));
|
|
72
121
|
const holding = new Set(held);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
122
|
+
return {
|
|
123
|
+
remove: async (path) => {
|
|
124
|
+
if (!holding.has(path))
|
|
125
|
+
return;
|
|
126
|
+
if (!tracked.has(path))
|
|
127
|
+
await removeFile(repository, path);
|
|
128
|
+
holding.delete(path);
|
|
129
|
+
},
|
|
130
|
+
place: async (path, content) => {
|
|
131
|
+
if (tracked.has(path))
|
|
132
|
+
return;
|
|
133
|
+
if (!holding.has(path) && (await exists(join(repository, path))))
|
|
134
|
+
return;
|
|
135
|
+
await writeText(join(repository, path), content);
|
|
136
|
+
holding.add(path);
|
|
137
|
+
},
|
|
138
|
+
held: () => [...holding].sort(),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
async function readOnlyPlaced(repository, held, written, removed) {
|
|
142
|
+
const files = await readOnlyFiles(repository, held);
|
|
143
|
+
for (const path of removed)
|
|
144
|
+
await files.remove(path);
|
|
145
|
+
for (const [path, content] of written)
|
|
146
|
+
await files.place(path, content);
|
|
147
|
+
return files.held();
|
|
148
|
+
}
|
|
149
|
+
async function importedWhole(repository, scope, texts, commit, readOnly) {
|
|
150
|
+
const revisions = {};
|
|
151
|
+
async function* tracked() {
|
|
152
|
+
for await (const { path, content, revision: held, provenance } of texts) {
|
|
153
|
+
if (declaredWritable(path, scope)) {
|
|
154
|
+
revisions[path] = held;
|
|
155
|
+
yield [path, content];
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
await readOnly.place(path, content);
|
|
159
|
+
}
|
|
160
|
+
if (provenance !== undefined)
|
|
161
|
+
await readOnly.place(sidecar(path), provenance);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { house: await importCommits(repository, [{ ...commit, files: tracked() }]), revisions };
|
|
89
165
|
}
|
|
90
166
|
async function ruled(path, rules) {
|
|
91
167
|
if ((await readFile(path, 'utf8').catch(() => null)) !== rules)
|
|
92
168
|
await writeFile(path, rules);
|
|
93
169
|
}
|
|
94
170
|
async function ignoreRules(repository, writable, copy) {
|
|
95
|
-
const readOnly =
|
|
171
|
+
const readOnly = copy.read_only.filter((path) => !path.split('/').includes(PROVENANCE));
|
|
96
172
|
const protectedPath = (path) => copy.protected.some((prefix) => prefix !== '' && covers(path, prefix));
|
|
97
173
|
const whole = copy.protected.filter((prefix) => prefix !== '' && !writable.some((path) => covers(path, prefix)));
|
|
98
174
|
const house = [
|
|
@@ -101,7 +177,7 @@ async function ignoreRules(repository, writable, copy) {
|
|
|
101
177
|
].map(pattern);
|
|
102
178
|
const granted = readOnly.filter((path) => !protectedPath(path)).map(pattern);
|
|
103
179
|
await ruled(join(repository, '.gitignore'), [
|
|
104
|
-
'#
|
|
180
|
+
'# Kit writes this file and .ignore. The paths below are read-only House',
|
|
105
181
|
'# content: Git does not track them, and House refuses a change to them even',
|
|
106
182
|
'# when this file is edited or a path is force-added.',
|
|
107
183
|
'/.gitignore',
|
|
@@ -116,7 +192,7 @@ async function ignoreRules(repository, writable, copy) {
|
|
|
116
192
|
'',
|
|
117
193
|
].join('\n'));
|
|
118
194
|
await ruled(join(repository, '.ignore'), [
|
|
119
|
-
'#
|
|
195
|
+
'# Kit writes this file so native search still reads the read-only House',
|
|
120
196
|
'# content that .gitignore keeps out of Git.',
|
|
121
197
|
`!${PROVENANCE}`,
|
|
122
198
|
...[...house, ...granted].map((rule) => `!${rule}`),
|
|
@@ -147,34 +223,36 @@ function movedPaths(moves, before, after) {
|
|
|
147
223
|
.filter((pair) => pairs.filter((other) => other.from === pair.from || other.to === pair.to).length === 1)
|
|
148
224
|
.map(({ from, to }) => [from, to]));
|
|
149
225
|
}
|
|
150
|
-
export class
|
|
226
|
+
export class LocalCopies {
|
|
151
227
|
owner;
|
|
152
228
|
house;
|
|
153
229
|
copies = new Map();
|
|
154
230
|
queues = new Map();
|
|
155
|
-
|
|
156
|
-
|
|
231
|
+
waiting = new Set();
|
|
232
|
+
answering = Promise.resolve();
|
|
157
233
|
constructor(house) {
|
|
158
234
|
this.house = house;
|
|
159
235
|
this.owner = {
|
|
160
236
|
key: 'owner',
|
|
161
237
|
submit: async (operation, changes) => {
|
|
162
238
|
try {
|
|
163
|
-
const outcome = await house.post('/kit/door/batch', { operation_id: operation, changes });
|
|
239
|
+
const outcome = await retrying(() => house.post('/kit/door/batch', { operation_id: operation, changes }));
|
|
164
240
|
return { accepted: outcome.contents };
|
|
165
241
|
}
|
|
166
242
|
catch (error) {
|
|
167
|
-
if (!(error
|
|
243
|
+
if (!finalRefusal(error))
|
|
168
244
|
throw error;
|
|
169
|
-
return {
|
|
245
|
+
return {
|
|
246
|
+
refused: refusalOf(error.text) ?? { code: '', text: `House refused it with ${error.status}`, conflicts: [] },
|
|
247
|
+
};
|
|
170
248
|
}
|
|
171
249
|
},
|
|
172
250
|
};
|
|
173
251
|
}
|
|
174
252
|
async load() {
|
|
175
|
-
const home = join(kitHome(), '
|
|
253
|
+
const home = join(kitHome(), 'local-copies');
|
|
176
254
|
const names = (await readdir(join(home, 'rooms')).catch(() => [])).filter((name) => name.endsWith('.json'));
|
|
177
|
-
for (const path of
|
|
255
|
+
for (const path of names.map((name) => join(home, 'rooms', name))) {
|
|
178
256
|
const text = await readFile(path, 'utf8').catch(() => null);
|
|
179
257
|
if (text === null)
|
|
180
258
|
continue;
|
|
@@ -182,48 +260,128 @@ export class WorkingCopies {
|
|
|
182
260
|
this.copies.set(copy.key, copy);
|
|
183
261
|
}
|
|
184
262
|
}
|
|
263
|
+
async swept() {
|
|
264
|
+
const rooms = join(localCopies(), 'rooms');
|
|
265
|
+
for (const name of await readdir(rooms).catch(() => [])) {
|
|
266
|
+
if (name.startsWith(stagingPrefix()))
|
|
267
|
+
await rm(join(rooms, name), { recursive: true, force: true });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
185
270
|
async mapping() {
|
|
186
|
-
await this.
|
|
271
|
+
await this.answering;
|
|
187
272
|
return [...this.copies.values()]
|
|
188
|
-
.filter((copy) => copy.
|
|
189
|
-
.map((copy) => `${rootOf(copy)}: ${copy.repository}`)
|
|
273
|
+
.filter((copy) => copy.unfollowed === undefined)
|
|
274
|
+
.map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
|
|
190
275
|
.sort();
|
|
191
276
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
277
|
+
connected() {
|
|
278
|
+
return this.selected();
|
|
279
|
+
}
|
|
280
|
+
async changed(rooms) {
|
|
281
|
+
if (rooms.length > 0)
|
|
282
|
+
await this.selected(rooms);
|
|
283
|
+
}
|
|
284
|
+
fetch() {
|
|
285
|
+
return this.answered(() => this.fetched(true));
|
|
286
|
+
}
|
|
287
|
+
async selected(rooms) {
|
|
288
|
+
const copied = await this.answered(() => this.fetched(false, rooms));
|
|
289
|
+
if (copied.refused)
|
|
290
|
+
logged(copied.text);
|
|
291
|
+
}
|
|
292
|
+
answered(work) {
|
|
293
|
+
const answered = this.answering.then(work);
|
|
294
|
+
this.answering = answered.catch(() => undefined);
|
|
295
|
+
return answered;
|
|
296
|
+
}
|
|
297
|
+
async fetched(refreshing, rooms) {
|
|
298
|
+
const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', rooms === undefined ? {} : { rooms });
|
|
299
|
+
const synced = new Map(answer.rooms.map((room) => [`${ROOMS}${room.room_ref}`, room.room_handle]));
|
|
300
|
+
const named = rooms === undefined ? null : new Set(rooms.map((room) => `${ROOMS}${room}`));
|
|
301
|
+
const held = () => [...this.copies.values()].filter((copy) => named === null || named.has(copy.key));
|
|
302
|
+
return this.holding([...new Set([...held().map((copy) => copy.key), ...synced.keys()])].sort(), async () => {
|
|
303
|
+
const said = [];
|
|
304
|
+
let refused = false;
|
|
305
|
+
const mark = refreshing ? 'said' : 'unsaid';
|
|
306
|
+
for (const copy of held()) {
|
|
307
|
+
if (synced.has(copy.key) || copy.unfollowed === 'said' || copy.unfollowed === mark)
|
|
308
|
+
continue;
|
|
309
|
+
await this.save({ ...copy, unfollowed: mark });
|
|
310
|
+
if (refreshing)
|
|
311
|
+
said.push(`${rootOf(copy.room_handle)}: no longer followed; kept as it is`);
|
|
312
|
+
}
|
|
313
|
+
const current = [];
|
|
314
|
+
const wanted = new Map();
|
|
315
|
+
for (const [key, handle] of synced) {
|
|
316
|
+
try {
|
|
317
|
+
const held = this.copies.get(key);
|
|
318
|
+
if (held === undefined || !(await exists(held.repository))) {
|
|
319
|
+
wanted.set(key, handle);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const { unfollowed, ...followed } = held;
|
|
323
|
+
current.push(unfollowed === undefined && held.room_handle === handle ? held : await this.save({ ...followed, room_handle: handle }));
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
refused = true;
|
|
327
|
+
said.push(`${rootOf(handle)}: ${causeOf(error)}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const unready = [];
|
|
331
|
+
const unsettled = [];
|
|
332
|
+
if (wanted.size > 0) {
|
|
333
|
+
const { authorities } = await this.discovered([...wanted.values()]);
|
|
334
|
+
for (const [key, handle] of wanted) {
|
|
335
|
+
const authority = authorities.find((entry) => `${ROOMS}${entry.room_ref}` === key);
|
|
336
|
+
try {
|
|
337
|
+
if (authority === undefined)
|
|
338
|
+
throw new Error('House serves nothing of it to copy');
|
|
339
|
+
said.push(await this.copied({ key, handle, authority }));
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
if (error instanceof NotReady) {
|
|
343
|
+
unready.push(...error.rooms);
|
|
344
|
+
said.push(`${rootOf(handle)}: ${NOT_READY}`);
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
refused = true;
|
|
348
|
+
unsettled.push(key.slice(ROOMS.length));
|
|
349
|
+
said.push(`${rootOf(handle)}: ${causeOf(error)}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const reading = refreshing ? current : current.filter((copy) => this.waiting.has(copy.room_ref));
|
|
355
|
+
if (reading.length > 0) {
|
|
356
|
+
const failed = await this.refreshed(reading, unready).then(() => null, houseCause);
|
|
357
|
+
for (const copy of reading) {
|
|
358
|
+
said.push(...(unready.includes(copy.room_ref) ? [`${rootOf(copy.room_handle)}: ${NOT_READY}`] : moved(copy, this.copies.get(copy.key))));
|
|
359
|
+
}
|
|
360
|
+
if (failed !== null) {
|
|
361
|
+
refused = true;
|
|
362
|
+
unsettled.push(...reading.map((copy) => copy.room_ref));
|
|
363
|
+
said.push(failed);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
for (const room of rooms ?? [...this.waiting]) {
|
|
367
|
+
if (!unready.includes(room) && !unsettled.includes(room))
|
|
368
|
+
this.waiting.delete(room);
|
|
369
|
+
}
|
|
370
|
+
const due = unready.filter((room) => !this.waiting.has(room));
|
|
371
|
+
for (const room of due)
|
|
372
|
+
this.waiting.add(room);
|
|
373
|
+
if (due.length > 0)
|
|
374
|
+
void this.askedAgain(due);
|
|
375
|
+
return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
async askedAgain(rooms) {
|
|
379
|
+
let asking = rooms;
|
|
380
|
+
for (let step = 0; asking.length > 0; step++) {
|
|
381
|
+
await paused(ASKED_AGAIN_MS[Math.min(step, ASKED_AGAIN_MS.length - 1)]);
|
|
382
|
+
await this.selected().catch(logged);
|
|
383
|
+
asking = asking.filter((room) => this.waiting.has(room));
|
|
384
|
+
}
|
|
227
385
|
}
|
|
228
386
|
async observed(report) {
|
|
229
387
|
const key = await this.keyAt(dirname(report.common));
|
|
@@ -253,11 +411,11 @@ export class WorkingCopies {
|
|
|
253
411
|
async push(cwd, selector, caller) {
|
|
254
412
|
const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
|
|
255
413
|
if (located.status !== 0)
|
|
256
|
-
return
|
|
414
|
+
return outside(cwd);
|
|
257
415
|
const [common, worktree] = located.stdout.toString('utf8').split('\n');
|
|
258
416
|
const key = await this.keyAt(dirname(common));
|
|
259
417
|
if (key === undefined)
|
|
260
|
-
return
|
|
418
|
+
return outside(worktree);
|
|
261
419
|
return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
|
|
262
420
|
}
|
|
263
421
|
async keyAt(repository) {
|
|
@@ -273,19 +431,12 @@ export class WorkingCopies {
|
|
|
273
431
|
this.queues.set(key, queued.catch(() => undefined));
|
|
274
432
|
return queued;
|
|
275
433
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
this.delivering.add(key);
|
|
280
|
-
void this.exclusive(key, async () => {
|
|
281
|
-
this.delivering.delete(key);
|
|
282
|
-
const copy = this.copies.get(key);
|
|
283
|
-
if (copy.active)
|
|
284
|
-
await this.save(await this.caughtUp(copy));
|
|
285
|
-
}).catch(logged);
|
|
434
|
+
holding(keys, work) {
|
|
435
|
+
const [key, ...rest] = keys;
|
|
436
|
+
return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
|
|
286
437
|
}
|
|
287
438
|
async save(copy) {
|
|
288
|
-
const path =
|
|
439
|
+
const path = recordOf(copy.key);
|
|
289
440
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
290
441
|
const temporary = `${path}.${process.pid}`;
|
|
291
442
|
await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
|
|
@@ -293,163 +444,210 @@ export class WorkingCopies {
|
|
|
293
444
|
this.copies.set(copy.key, copy);
|
|
294
445
|
return copy;
|
|
295
446
|
}
|
|
296
|
-
async
|
|
297
|
-
const
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
for (const file of view.manifest.files) {
|
|
318
|
-
const content = contents.get(file.path);
|
|
319
|
-
if (file.writable) {
|
|
320
|
-
read.writable.set(file.path, content);
|
|
321
|
-
read.revisions[file.path] = file.revision;
|
|
322
|
-
}
|
|
323
|
-
else {
|
|
324
|
-
read.readOnly.set(file.path, content);
|
|
447
|
+
async discovered(handles) {
|
|
448
|
+
const discover = (group) => this.house.post('/kit/door/discover', { prefixes: group.map(rootOf) });
|
|
449
|
+
const [first, ...rest] = handles;
|
|
450
|
+
const { bounds, authorities } = await discover([first]);
|
|
451
|
+
for (const group of grouped(rest, bounds.max_prefixes))
|
|
452
|
+
authorities.push(...(await discover(group)).authorities);
|
|
453
|
+
return { bounds, authorities };
|
|
454
|
+
}
|
|
455
|
+
async chunked(root, room, directory) {
|
|
456
|
+
await mkdir(directory, { recursive: true });
|
|
457
|
+
for (let renewed = false;; renewed = true) {
|
|
458
|
+
const { rooms } = await this.house.post('/kit/door/room-chunks/list', { prefixes: [root] });
|
|
459
|
+
const list = rooms.find((entry) => entry.room_ref === room);
|
|
460
|
+
if (list === undefined)
|
|
461
|
+
throw new Error('House serves nothing of it to copy');
|
|
462
|
+
if (!list.ready)
|
|
463
|
+
throw new NotReady([room]);
|
|
464
|
+
const held = new Set(await readdir(directory));
|
|
465
|
+
const failure = await stagedChunks(directory, list.chunks.filter(({ hash }) => !held.has(hash)));
|
|
466
|
+
if (failure === undefined) {
|
|
467
|
+
return { position: list.position, log_epoch: list.log_epoch, texts: chunkTexts(directory, list.chunks, list.models) };
|
|
325
468
|
}
|
|
326
|
-
if (
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
return read;
|
|
330
|
-
}
|
|
331
|
-
async apply(scope) {
|
|
332
|
-
let copy = this.copies.get(scope.key);
|
|
333
|
-
if (copy === undefined || !(await exists(copy.repository))) {
|
|
334
|
-
await this.materialize(scope);
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
if (copy.room_handle !== scope.handle)
|
|
338
|
-
copy = await this.save({ ...copy, room_handle: scope.handle });
|
|
339
|
-
if (!copy.active) {
|
|
340
|
-
await this.save(await this.received({ ...copy, active: true }));
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
const discovery = await this.house.post('/kit/door/discover', {
|
|
344
|
-
prefixes: prefixesOf(copy),
|
|
345
|
-
});
|
|
346
|
-
const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
|
|
347
|
-
if (authority === undefined)
|
|
348
|
-
return;
|
|
349
|
-
if (authority.managed_replication !== 'admitted') {
|
|
350
|
-
logged(`operation_denied: ${rootOf(copy)} is not synchronized to this Environment`);
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
if (JSON.stringify([authority.operations.write, authority.protected_paths]) !==
|
|
354
|
-
JSON.stringify([copy.write, copy.protected])) {
|
|
355
|
-
await this.save(await this.received(copy));
|
|
356
|
-
return;
|
|
469
|
+
if (renewed && (await readdir(directory)).length === held.size)
|
|
470
|
+
throw failure;
|
|
357
471
|
}
|
|
358
|
-
await this.save(await this.caughtUp(copy));
|
|
359
472
|
}
|
|
360
|
-
async
|
|
361
|
-
const
|
|
362
|
-
if (
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
}
|
|
366
|
-
const repository = scope.private ? join(WORKING_COPIES, 'private') : join(WORKING_COPIES, 'rooms', read.anchor);
|
|
367
|
-
if (await exists(repository)) {
|
|
368
|
-
logged(`${repository} already exists and House Kit leaves it as it is`);
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
const staging = join(dirname(repository), `.${randomUUID()}`);
|
|
473
|
+
async materialized({ key, handle, authority }, whole) {
|
|
474
|
+
const repository = join(localCopies(), 'rooms', authority.room_ref);
|
|
475
|
+
if (await exists(repository))
|
|
476
|
+
throw new Error(`${repository} already exists and Kit leaves it as it is`);
|
|
477
|
+
const staging = join(dirname(repository), `${stagingPrefix()}${randomUUID()}`);
|
|
478
|
+
const scope = { write: authority.operations.write, protected: authority.protected_paths };
|
|
372
479
|
await mkdir(staging, { recursive: true });
|
|
373
|
-
let
|
|
374
|
-
let readOnly;
|
|
480
|
+
let copy;
|
|
375
481
|
try {
|
|
376
482
|
await git(staging, ['init', '-q', '-b', BRANCH]);
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
483
|
+
const read = await whole(join(staging, '.git', STAGED_CHUNKS));
|
|
484
|
+
const readOnly = await readOnlyFiles(staging, []);
|
|
485
|
+
const { house, revisions } = await importedWhole(staging, scope, read.texts, { message: `House ${rootOf(handle)}`, from: null }, readOnly);
|
|
380
486
|
await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
|
|
381
487
|
await git(staging, ['reset', '-q', '--hard']);
|
|
382
|
-
|
|
383
|
-
|
|
488
|
+
copy = {
|
|
489
|
+
key,
|
|
490
|
+
room_ref: authority.room_ref,
|
|
491
|
+
room_handle: handle,
|
|
492
|
+
repository,
|
|
493
|
+
house,
|
|
494
|
+
position: read.position,
|
|
495
|
+
log_epoch: read.log_epoch,
|
|
496
|
+
...scope,
|
|
497
|
+
read_only: readOnly.held(),
|
|
498
|
+
manifests: { [house]: { parent: null, files: revisions } },
|
|
499
|
+
moves: [],
|
|
500
|
+
};
|
|
501
|
+
await ignoreRules(staging, Object.keys(revisions), copy);
|
|
502
|
+
await rm(join(staging, '.git', STAGED_CHUNKS), { recursive: true, force: true });
|
|
384
503
|
await rename(staging, repository);
|
|
385
504
|
}
|
|
386
505
|
catch (error) {
|
|
387
506
|
await rm(staging, { recursive: true, force: true });
|
|
388
507
|
throw error;
|
|
389
508
|
}
|
|
390
|
-
|
|
391
|
-
key: scope.key,
|
|
392
|
-
private: scope.private,
|
|
393
|
-
room_ref: read.anchor,
|
|
394
|
-
room_handle: scope.handle,
|
|
395
|
-
repository,
|
|
396
|
-
active: true,
|
|
397
|
-
house,
|
|
398
|
-
position: read.position,
|
|
399
|
-
log_epoch: read.log_epoch,
|
|
400
|
-
write: read.write,
|
|
401
|
-
protected: read.protected,
|
|
402
|
-
read_only: readOnly,
|
|
403
|
-
manifests: { [house]: { parent: null, files: read.revisions } },
|
|
404
|
-
moves: [],
|
|
405
|
-
});
|
|
509
|
+
return this.save(copy);
|
|
406
510
|
}
|
|
407
|
-
async
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
511
|
+
async copied(wanted) {
|
|
512
|
+
const { handle, authority } = wanted;
|
|
513
|
+
const root = rootOf(handle);
|
|
514
|
+
let answer = null;
|
|
515
|
+
try {
|
|
516
|
+
if (!this.waiting.has(authority.room_ref)) {
|
|
517
|
+
answer = await this.house.post(ENUMERATE, {
|
|
518
|
+
prefixes: [root],
|
|
519
|
+
positions: [
|
|
520
|
+
{ authority: authority.room_ref, position: '0', log_epoch: authority.log_epoch, page: { through: authority.position, read: 0 } },
|
|
521
|
+
],
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
if (expiredAuthority(error) !== authority.room_ref)
|
|
527
|
+
throw error;
|
|
528
|
+
}
|
|
529
|
+
const view = answer?.authorities.find((entry) => entry.authority === authority.room_ref);
|
|
530
|
+
if (answer !== null && view === undefined)
|
|
531
|
+
throw new Error('House serves nothing of it to copy');
|
|
532
|
+
if (view !== undefined && view.next === undefined) {
|
|
533
|
+
const texts = view.changes.filter((change) => !('removed' in change));
|
|
534
|
+
const copy = await this.materialized(wanted, async () => ({ position: view.position, log_epoch: view.log_epoch, texts }));
|
|
535
|
+
return `${root}: copied to ${copy.repository}`;
|
|
536
|
+
}
|
|
537
|
+
const copy = await this.materialized(wanted, (directory) => this.chunked(root, authority.room_ref, directory));
|
|
538
|
+
const [caught] = await this.caughtUp([copy], true);
|
|
539
|
+
await succeeded(copy.repository, ['merge', '--ff-only', '-q', caught.house]);
|
|
540
|
+
return `${root}: copied to ${copy.repository}`;
|
|
541
|
+
}
|
|
542
|
+
async received(copy, authority) {
|
|
543
|
+
const root = rootOf(copy.room_handle);
|
|
544
|
+
const directory = join(copy.repository, '.git', STAGED_CHUNKS);
|
|
545
|
+
const scope = { write: authority.operations.write, protected: authority.protected_paths };
|
|
546
|
+
try {
|
|
547
|
+
const read = await this.chunked(root, copy.room_ref, directory);
|
|
548
|
+
const readOnly = await readOnlyFiles(copy.repository, copy.read_only);
|
|
549
|
+
for (const path of copy.read_only)
|
|
550
|
+
await readOnly.remove(path);
|
|
551
|
+
const commit = { message: `House ${root} at position ${read.position}`, from: copy.house, replaces: true };
|
|
552
|
+
const { house, revisions } = await importedWhole(copy.repository, scope, read.texts, commit, readOnly);
|
|
553
|
+
const next = {
|
|
554
|
+
...copy,
|
|
555
|
+
...scope,
|
|
556
|
+
house,
|
|
557
|
+
position: read.position,
|
|
558
|
+
log_epoch: read.log_epoch,
|
|
559
|
+
read_only: readOnly.held(),
|
|
560
|
+
manifests: { ...copy.manifests, [house]: { parent: null, files: revisions } },
|
|
561
|
+
};
|
|
562
|
+
await ignoreRules(copy.repository, Object.keys(revisions), next);
|
|
563
|
+
return next;
|
|
564
|
+
}
|
|
565
|
+
finally {
|
|
566
|
+
await rm(directory, { recursive: true, force: true });
|
|
567
|
+
}
|
|
428
568
|
}
|
|
429
|
-
async
|
|
430
|
-
|
|
431
|
-
|
|
569
|
+
async reread(copy) {
|
|
570
|
+
const { authorities } = await this.discovered([copy.room_handle]);
|
|
571
|
+
const authority = authorities.find((entry) => entry.room_ref === copy.room_ref);
|
|
572
|
+
if (authority === undefined)
|
|
573
|
+
throw new NotReady([copy.room_ref]);
|
|
574
|
+
return this.received(copy, authority);
|
|
575
|
+
}
|
|
576
|
+
async refreshed(copies, unready) {
|
|
577
|
+
const { bounds, authorities } = await this.discovered(copies.map((copy) => copy.room_handle));
|
|
578
|
+
const read = [];
|
|
579
|
+
const unchanged = [];
|
|
580
|
+
for (const copy of copies) {
|
|
581
|
+
const authority = authorities.find((entry) => entry.room_ref === copy.room_ref);
|
|
582
|
+
if (authority !== undefined &&
|
|
583
|
+
JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
|
|
584
|
+
try {
|
|
585
|
+
read.push(await this.save(await this.received(copy, authority)));
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
if (!(error instanceof NotReady))
|
|
589
|
+
throw error;
|
|
590
|
+
unready.push(...error.rooms);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
unchanged.push(copy);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
for (const group of grouped(unchanged, bounds.max_prefixes))
|
|
598
|
+
await this.caughtUp(group, false, unready);
|
|
599
|
+
for (const group of grouped(read, bounds.max_prefixes))
|
|
600
|
+
await this.caughtUp(group, true, unready);
|
|
601
|
+
}
|
|
602
|
+
async caughtUp(copies, whole, unready) {
|
|
603
|
+
const caught = new Map(copies.map((copy) => [copy.key, copy]));
|
|
604
|
+
const read = new Set();
|
|
605
|
+
const waiting = unready ?? [];
|
|
606
|
+
let asking = [...copies];
|
|
607
|
+
while (asking.length > 0) {
|
|
432
608
|
let answer;
|
|
433
609
|
try {
|
|
434
|
-
answer = await this.house.post(
|
|
435
|
-
prefixes:
|
|
436
|
-
positions:
|
|
610
|
+
answer = await this.house.post(ENUMERATE, {
|
|
611
|
+
prefixes: asking.map((copy) => rootOf(copy.room_handle)),
|
|
612
|
+
positions: asking.map((copy) => ({ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch })),
|
|
437
613
|
});
|
|
438
614
|
}
|
|
439
615
|
catch (error) {
|
|
440
|
-
const
|
|
441
|
-
if (
|
|
442
|
-
return this.received(copy);
|
|
443
|
-
if (code !== 'replica_behind')
|
|
616
|
+
const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
|
|
617
|
+
if (expired === undefined)
|
|
444
618
|
throw error;
|
|
445
|
-
|
|
446
|
-
|
|
619
|
+
asking = asking.filter((copy) => copy !== expired);
|
|
620
|
+
if (read.has(expired.key)) {
|
|
621
|
+
waiting.push(expired.room_ref);
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
read.add(expired.key);
|
|
625
|
+
try {
|
|
626
|
+
const next = await this.save(await this.reread(expired));
|
|
627
|
+
caught.set(expired.key, next);
|
|
628
|
+
asking.push(next);
|
|
629
|
+
}
|
|
630
|
+
catch (refused) {
|
|
631
|
+
if (!(refused instanceof NotReady))
|
|
632
|
+
throw refused;
|
|
633
|
+
waiting.push(expired.room_ref);
|
|
634
|
+
}
|
|
635
|
+
continue;
|
|
447
636
|
}
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
637
|
+
const advanced = [];
|
|
638
|
+
for (const copy of asking) {
|
|
639
|
+
const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
|
|
640
|
+
if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
|
|
641
|
+
continue;
|
|
642
|
+
const next = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
|
|
643
|
+
caught.set(copy.key, next);
|
|
644
|
+
advanced.push(next);
|
|
645
|
+
}
|
|
646
|
+
asking = advanced.filter((copy) => whole || read.has(copy.key));
|
|
452
647
|
}
|
|
648
|
+
if (unready === undefined && waiting.length > 0)
|
|
649
|
+
throw new NotReady(waiting);
|
|
650
|
+
return copies.map((copy) => caught.get(copy.key));
|
|
453
651
|
}
|
|
454
652
|
async incorporated(copy, changes, position, logEpoch) {
|
|
455
653
|
const houseTree = await tree(copy.repository, copy.house);
|
|
@@ -482,7 +680,7 @@ export class WorkingCopies {
|
|
|
482
680
|
let { house, manifests } = copy;
|
|
483
681
|
if (files.size > 0) {
|
|
484
682
|
house = await importCommits(copy.repository, [
|
|
485
|
-
{ message: `House ${rootOf(copy)} at position ${position}`, from: copy.house, files },
|
|
683
|
+
{ message: `House ${rootOf(copy.room_handle)} at position ${position}`, from: copy.house, files },
|
|
486
684
|
]);
|
|
487
685
|
manifests = { ...manifests, [house]: { parent: copy.house, files: revisions } };
|
|
488
686
|
}
|
|
@@ -512,8 +710,6 @@ export class WorkingCopies {
|
|
|
512
710
|
}
|
|
513
711
|
copy = await this.save(without(copy));
|
|
514
712
|
}
|
|
515
|
-
if (!copy.active)
|
|
516
|
-
return { refused: true, text: `${rootOf(copy)} is no longer kept in this Environment` };
|
|
517
713
|
const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
|
|
518
714
|
const base = listed.find((ancestor) => ancestor in copy.manifests);
|
|
519
715
|
if (base === undefined) {
|
|
@@ -524,7 +720,7 @@ export class WorkingCopies {
|
|
|
524
720
|
changes = await this.derived(copy, base, commit);
|
|
525
721
|
}
|
|
526
722
|
catch (error) {
|
|
527
|
-
return { refused: true, text:
|
|
723
|
+
return { refused: true, text: houseCause(error) };
|
|
528
724
|
}
|
|
529
725
|
if (changes.length === 0)
|
|
530
726
|
return { refused: false, text: `Nothing to submit: ${short(commit)} matches House.` };
|
|
@@ -546,12 +742,23 @@ export class WorkingCopies {
|
|
|
546
742
|
return this.settle(submitted, caller);
|
|
547
743
|
}
|
|
548
744
|
async derived(copy, base, commit) {
|
|
549
|
-
const root = rootOf(copy);
|
|
745
|
+
const root = rootOf(copy.room_handle);
|
|
550
746
|
const before = await tree(copy.repository, base);
|
|
551
747
|
const after = await tree(copy.repository, commit);
|
|
552
748
|
const moved = movedPaths(copy.moves, before, after);
|
|
553
749
|
const arrived = new Set(moved.values());
|
|
554
|
-
const
|
|
750
|
+
const arriving = [...after].filter(([path, entry]) => before.get(path) !== entry);
|
|
751
|
+
if (arriving.length > 0) {
|
|
752
|
+
const { bounds } = await this.house.post('/kit/door/discover', { prefixes: [root] });
|
|
753
|
+
const binary = [];
|
|
754
|
+
for (const [path, entry] of arriving) {
|
|
755
|
+
if (!(await isText(copy.repository, entry, bounds.max_authored_file_bytes)))
|
|
756
|
+
binary.push(path);
|
|
757
|
+
}
|
|
758
|
+
if (binary.length > 0)
|
|
759
|
+
throw new Error(textOnly(binary));
|
|
760
|
+
}
|
|
761
|
+
const text = (entry) => blobText(copy.repository, entry);
|
|
555
762
|
const changes = [];
|
|
556
763
|
for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
|
|
557
764
|
if (arrived.has(path))
|
|
@@ -564,7 +771,7 @@ export class WorkingCopies {
|
|
|
564
771
|
changes.push({ op: 'rename', path: `${root}/${path}`, to: `${root}/${to}`, base: known });
|
|
565
772
|
const moving = after.get(to);
|
|
566
773
|
if (moving !== prior) {
|
|
567
|
-
changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving
|
|
774
|
+
changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving) });
|
|
568
775
|
}
|
|
569
776
|
continue;
|
|
570
777
|
}
|
|
@@ -573,18 +780,17 @@ export class WorkingCopies {
|
|
|
573
780
|
if (next === undefined)
|
|
574
781
|
changes.push({ op: 'remove', path: `${root}/${path}`, base: known });
|
|
575
782
|
else if (prior === undefined)
|
|
576
|
-
changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next
|
|
783
|
+
changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next) });
|
|
577
784
|
else
|
|
578
|
-
changes.push(...changed(`${root}/${path}`, path, known, await text(prior
|
|
785
|
+
changes.push(...changed(`${root}/${path}`, path, known, await text(prior), await text(next)));
|
|
579
786
|
}
|
|
580
787
|
return changes;
|
|
581
788
|
}
|
|
582
789
|
async settle(start, caller, restaged = false) {
|
|
583
790
|
let copy = start;
|
|
584
791
|
const push = copy.push;
|
|
585
|
-
if (push.caller !== caller.key)
|
|
586
|
-
return this.recovered(copy
|
|
587
|
-
}
|
|
792
|
+
if (push.caller !== caller.key)
|
|
793
|
+
return this.recovered(copy);
|
|
588
794
|
let outcome;
|
|
589
795
|
try {
|
|
590
796
|
outcome = await caller.submit(push.operation_id, push.changes, push.upload, async (upload) => {
|
|
@@ -592,9 +798,10 @@ export class WorkingCopies {
|
|
|
592
798
|
});
|
|
593
799
|
}
|
|
594
800
|
catch (error) {
|
|
801
|
+
const refusal = error instanceof HouseRefusal ? refusalOf(error.text) : null;
|
|
595
802
|
return {
|
|
596
803
|
refused: true,
|
|
597
|
-
text: `House's answer to ${short(push.commit)} did not arrive
|
|
804
|
+
text: refusal?.text ?? `House's answer to ${short(push.commit)} did not arrive; run house push again`,
|
|
598
805
|
};
|
|
599
806
|
}
|
|
600
807
|
if ('refused' in outcome) {
|
|
@@ -602,32 +809,40 @@ export class WorkingCopies {
|
|
|
602
809
|
const { upload, ...unstaged } = copy.push;
|
|
603
810
|
return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
|
|
604
811
|
}
|
|
605
|
-
if (outcome.refused.code === '
|
|
606
|
-
return this.recovered(copy
|
|
812
|
+
if (outcome.refused.code === 'expired')
|
|
813
|
+
return this.recovered(copy);
|
|
607
814
|
let settled = await this.save(without(copy));
|
|
815
|
+
if (settled.unfollowed !== undefined)
|
|
816
|
+
return { refused: true, text: outcome.refused.text };
|
|
817
|
+
const rebased = rebaseFixes(outcome.refused);
|
|
608
818
|
try {
|
|
609
|
-
settled =
|
|
819
|
+
settled = (await this.caughtUp([settled], true))[0];
|
|
610
820
|
}
|
|
611
|
-
catch
|
|
612
|
-
return {
|
|
821
|
+
catch {
|
|
822
|
+
return {
|
|
823
|
+
refused: true,
|
|
824
|
+
text: rebased ? `${outcome.refused.text}\nHouse's current state did not arrive; push again later` : outcome.refused.text,
|
|
825
|
+
};
|
|
613
826
|
}
|
|
614
827
|
return {
|
|
615
828
|
refused: true,
|
|
616
|
-
text:
|
|
829
|
+
text: rebased
|
|
830
|
+
? `${outcome.refused.text}\n${HOUSE_REF} holds House's current state; rebase onto it and push again`
|
|
831
|
+
: outcome.refused.text,
|
|
617
832
|
};
|
|
618
833
|
}
|
|
619
834
|
return this.finished(await this.save({ ...copy, push: { ...copy.push, state: 'accepted', written: outcome.accepted } }));
|
|
620
835
|
}
|
|
621
|
-
async recovered(copy
|
|
836
|
+
async recovered(copy) {
|
|
622
837
|
const push = copy.push;
|
|
623
|
-
const current =
|
|
838
|
+
const current = (await this.caughtUp([copy], true))[0];
|
|
624
839
|
const house = await tree(copy.repository, current.house);
|
|
625
|
-
const root = rootOf(current);
|
|
840
|
+
const root = rootOf(current.room_handle);
|
|
626
841
|
if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
|
|
627
842
|
await this.save(without(current));
|
|
628
843
|
return {
|
|
629
844
|
refused: true,
|
|
630
|
-
text:
|
|
845
|
+
text: `House does not hold ${short(push.commit)}; run house push again`,
|
|
631
846
|
};
|
|
632
847
|
}
|
|
633
848
|
const written = [];
|
|
@@ -636,7 +851,7 @@ export class WorkingCopies {
|
|
|
636
851
|
written.push({
|
|
637
852
|
path: `${root}/${path}`,
|
|
638
853
|
revision: entry === undefined ? null : (revisionAt(current, current.house, path) ?? null),
|
|
639
|
-
content: entry === undefined ? null : await blobText(copy.repository, entry
|
|
854
|
+
content: entry === undefined ? null : await blobText(copy.repository, entry),
|
|
640
855
|
});
|
|
641
856
|
}
|
|
642
857
|
return this.finished(await this.save({ ...current, push: { ...push, state: 'accepted', written } }));
|
|
@@ -652,10 +867,10 @@ export class WorkingCopies {
|
|
|
652
867
|
await this.save(without(copy));
|
|
653
868
|
return { refused: false, text: integrated.text };
|
|
654
869
|
}
|
|
655
|
-
catch
|
|
870
|
+
catch {
|
|
656
871
|
return {
|
|
657
872
|
refused: false,
|
|
658
|
-
text: `House accepted ${short(commit)};
|
|
873
|
+
text: `House accepted ${short(commit)}; run house push again to finish integrating it here`,
|
|
659
874
|
};
|
|
660
875
|
}
|
|
661
876
|
}
|
|
@@ -664,7 +879,7 @@ export class WorkingCopies {
|
|
|
664
879
|
const push = copy.push;
|
|
665
880
|
let written = push.written;
|
|
666
881
|
if (push.changes.some((change) => change.op === 'rename')) {
|
|
667
|
-
copy =
|
|
882
|
+
copy = (await this.caughtUp([copy], true))[0];
|
|
668
883
|
const house = await tree(copy.repository, copy.house);
|
|
669
884
|
const sent = await tree(copy.repository, push.house);
|
|
670
885
|
if (push.changes.some((change) => change.op === 'rename' && !house.has(relative(change.to)))) {
|
|
@@ -678,9 +893,9 @@ export class WorkingCopies {
|
|
|
678
893
|
written = [
|
|
679
894
|
...written,
|
|
680
895
|
{
|
|
681
|
-
path: `${rootOf(copy)}/${path}`,
|
|
896
|
+
path: `${rootOf(copy.room_handle)}/${path}`,
|
|
682
897
|
revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
|
|
683
|
-
content: entry === undefined ? null : await blobText(copy.repository, entry
|
|
898
|
+
content: entry === undefined ? null : await blobText(copy.repository, entry),
|
|
684
899
|
},
|
|
685
900
|
];
|
|
686
901
|
}
|
|
@@ -698,7 +913,7 @@ export class WorkingCopies {
|
|
|
698
913
|
}
|
|
699
914
|
const edits = new Map([...files].filter(([path, content]) => (content === null ? selected.has(path) : selected.get(path) !== blobId(content))));
|
|
700
915
|
const received = [...files.keys()].filter((path) => revisionAt(copy, copy.house, path) === revisionAt(copy, push.house, path));
|
|
701
|
-
const accepted = `House ${rootOf(copy)} with ${push.commit} accepted`;
|
|
916
|
+
const accepted = `House ${rootOf(copy.room_handle)} with ${push.commit} accepted`;
|
|
702
917
|
const house = await importCommits(copy.repository, [
|
|
703
918
|
...(edits.size === 0 ? [] : [{ message: accepted, from: push.commit, files: edits }]),
|
|
704
919
|
{
|
|
@@ -725,7 +940,7 @@ export class WorkingCopies {
|
|
|
725
940
|
async landed(repository, changes, house, selected) {
|
|
726
941
|
const text = async (path) => {
|
|
727
942
|
const entry = house.get(relative(path));
|
|
728
|
-
return entry === undefined ? undefined : blobText(repository, entry
|
|
943
|
+
return entry === undefined ? undefined : blobText(repository, entry);
|
|
729
944
|
};
|
|
730
945
|
for (const change of changes) {
|
|
731
946
|
const current = await text(change.path);
|