pushwork 2.0.0-preview → 2.0.0-preview.3
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/dist/branches.d.ts +1 -0
- package/dist/branches.d.ts.map +1 -1
- package/dist/cli/commands.d.ts +71 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/cli/commands.js +794 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +19 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli.js +67 -112
- package/dist/cli.js.map +1 -1
- package/dist/commands.d.ts +58 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +975 -0
- package/dist/commands.js.map +1 -0
- package/dist/config/index.d.ts +71 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +314 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config.d.ts +1 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -2
- package/dist/config.js.map +1 -1
- package/dist/core/change-detection.d.ts +80 -0
- package/dist/core/change-detection.d.ts.map +1 -0
- package/dist/core/change-detection.js +560 -0
- package/dist/core/change-detection.js.map +1 -0
- package/dist/core/config.d.ts +81 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +304 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +22 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/move-detection.d.ts +34 -0
- package/dist/core/move-detection.d.ts.map +1 -0
- package/dist/core/move-detection.js +128 -0
- package/dist/core/move-detection.js.map +1 -0
- package/dist/core/snapshot.d.ts +105 -0
- package/dist/core/snapshot.d.ts.map +1 -0
- package/dist/core/snapshot.js +254 -0
- package/dist/core/snapshot.js.map +1 -0
- package/dist/core/sync-engine.d.ts +177 -0
- package/dist/core/sync-engine.d.ts.map +1 -0
- package/dist/core/sync-engine.js +1471 -0
- package/dist/core/sync-engine.js.map +1 -0
- package/dist/index.d.ts +2 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -14
- package/dist/index.js.map +1 -1
- package/dist/pushwork.d.ts +28 -61
- package/dist/pushwork.d.ts.map +1 -1
- package/dist/pushwork.js +127 -445
- package/dist/pushwork.js.map +1 -1
- package/dist/shapes/types.d.ts +1 -0
- package/dist/shapes/types.d.ts.map +1 -1
- package/dist/shapes/types.js.map +1 -1
- package/dist/shapes/vfs.d.ts.map +1 -1
- package/dist/shapes/vfs.js +6 -2
- package/dist/shapes/vfs.js.map +1 -1
- package/dist/snarf.d.ts +21 -0
- package/dist/snarf.d.ts.map +1 -0
- package/dist/snarf.js +117 -0
- package/dist/snarf.js.map +1 -0
- package/dist/stash.d.ts +0 -2
- package/dist/stash.d.ts.map +1 -1
- package/dist/stash.js +0 -1
- package/dist/stash.js.map +1 -1
- package/dist/types/config.d.ts +102 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +10 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/documents.d.ts +88 -0
- package/dist/types/documents.d.ts.map +1 -0
- package/dist/types/documents.js +23 -0
- package/dist/types/documents.js.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +20 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/snapshot.d.ts +64 -0
- package/dist/types/snapshot.d.ts.map +1 -0
- package/dist/types/snapshot.js +3 -0
- package/dist/types/snapshot.js.map +1 -0
- package/dist/utils/content-similarity.d.ts +53 -0
- package/dist/utils/content-similarity.d.ts.map +1 -0
- package/dist/utils/content-similarity.js +155 -0
- package/dist/utils/content-similarity.js.map +1 -0
- package/dist/utils/content.d.ts +10 -0
- package/dist/utils/content.d.ts.map +1 -0
- package/dist/utils/content.js +35 -0
- package/dist/utils/content.js.map +1 -0
- package/dist/utils/directory.d.ts +24 -0
- package/dist/utils/directory.d.ts.map +1 -0
- package/dist/utils/directory.js +56 -0
- package/dist/utils/directory.js.map +1 -0
- package/dist/utils/fs.d.ts +74 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +298 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +21 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/mime-types.d.ts +13 -0
- package/dist/utils/mime-types.d.ts.map +1 -0
- package/dist/utils/mime-types.js +247 -0
- package/dist/utils/mime-types.js.map +1 -0
- package/dist/utils/network-sync.d.ts +30 -0
- package/dist/utils/network-sync.d.ts.map +1 -0
- package/dist/utils/network-sync.js +391 -0
- package/dist/utils/network-sync.js.map +1 -0
- package/dist/utils/node-polyfills.d.ts +9 -0
- package/dist/utils/node-polyfills.d.ts.map +1 -0
- package/dist/utils/node-polyfills.js +9 -0
- package/dist/utils/node-polyfills.js.map +1 -0
- package/dist/utils/output.d.ts +129 -0
- package/dist/utils/output.d.ts.map +1 -0
- package/dist/utils/output.js +375 -0
- package/dist/utils/output.js.map +1 -0
- package/dist/utils/repo-factory.d.ts +15 -0
- package/dist/utils/repo-factory.d.ts.map +1 -0
- package/dist/utils/repo-factory.js +156 -0
- package/dist/utils/repo-factory.js.map +1 -0
- package/dist/utils/string-similarity.d.ts +14 -0
- package/dist/utils/string-similarity.d.ts.map +1 -0
- package/dist/utils/string-similarity.js +43 -0
- package/dist/utils/string-similarity.js.map +1 -0
- package/dist/utils/text-diff.d.ts +37 -0
- package/dist/utils/text-diff.d.ts.map +1 -0
- package/dist/utils/text-diff.js +131 -0
- package/dist/utils/text-diff.js.map +1 -0
- package/dist/utils/trace.d.ts +19 -0
- package/dist/utils/trace.d.ts.map +1 -0
- package/dist/utils/trace.js +68 -0
- package/dist/utils/trace.js.map +1 -0
- package/dist/version.d.ts +11 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +93 -0
- package/dist/version.js.map +1 -0
- package/package.json +5 -1
- package/.prettierrc +0 -9
- package/flake.lock +0 -128
- package/flake.nix +0 -66
- package/pnpm-workspace.yaml +0 -5
- package/src/branches.ts +0 -93
- package/src/cli.ts +0 -292
- package/src/config.ts +0 -64
- package/src/fs-tree.ts +0 -70
- package/src/ignore.ts +0 -33
- package/src/index.ts +0 -38
- package/src/log.ts +0 -8
- package/src/pushwork.ts +0 -1055
- package/src/repo.ts +0 -76
- package/src/shapes/custom.ts +0 -29
- package/src/shapes/file.ts +0 -115
- package/src/shapes/index.ts +0 -19
- package/src/shapes/patchwork-folder.ts +0 -156
- package/src/shapes/types.ts +0 -79
- package/src/shapes/vfs.ts +0 -93
- package/src/stash.ts +0 -106
- package/test/integration/branches.test.ts +0 -389
- package/test/integration/pushwork.test.ts +0 -547
- package/test/setup.ts +0 -29
- package/test/unit/doc-shape.test.ts +0 -612
- package/tsconfig.json +0 -22
- package/vitest.config.ts +0 -14
|
@@ -1,612 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* White-box tests: import src/ directly and verify the on-disk Automerge doc
|
|
3
|
-
* structure (BranchesDoc wrapping, file-doc indirection, artifact pinning,
|
|
4
|
-
* branch isolation). All tests run fully offline.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import * as fs from "fs/promises";
|
|
8
|
-
import * as path from "path";
|
|
9
|
-
import * as tmp from "tmp";
|
|
10
|
-
|
|
11
|
-
async function pathExists(p: string): Promise<boolean> {
|
|
12
|
-
try {
|
|
13
|
-
await fs.access(p);
|
|
14
|
-
return true;
|
|
15
|
-
} catch {
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
import {
|
|
20
|
-
initSubduction,
|
|
21
|
-
parseAutomergeUrl,
|
|
22
|
-
isImmutableString,
|
|
23
|
-
type DocHandle,
|
|
24
|
-
type Repo,
|
|
25
|
-
} from "@automerge/automerge-repo";
|
|
26
|
-
import { NodeFSStorageAdapter } from "@automerge/automerge-repo-storage-nodefs";
|
|
27
|
-
import { Repo as RepoCtor } from "@automerge/automerge-repo";
|
|
28
|
-
|
|
29
|
-
import {
|
|
30
|
-
init,
|
|
31
|
-
save,
|
|
32
|
-
createBranch,
|
|
33
|
-
switchBranch,
|
|
34
|
-
currentBranch,
|
|
35
|
-
listBranches,
|
|
36
|
-
mergeBranch,
|
|
37
|
-
previewMerge,
|
|
38
|
-
cutWorkdir,
|
|
39
|
-
pasteStash,
|
|
40
|
-
showStashes,
|
|
41
|
-
type BranchesDoc,
|
|
42
|
-
isBranchesDoc,
|
|
43
|
-
detectDocType,
|
|
44
|
-
type UnixFileEntry,
|
|
45
|
-
} from "../../src/index.js";
|
|
46
|
-
import { readConfig } from "../../src/config.js";
|
|
47
|
-
|
|
48
|
-
async function openOfflineRepo(storage: string): Promise<Repo> {
|
|
49
|
-
await initSubduction();
|
|
50
|
-
const adapter = new NodeFSStorageAdapter(storage);
|
|
51
|
-
return new RepoCtor({ storage: adapter, network: [] });
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function withRepo<T>(
|
|
55
|
-
storage: string,
|
|
56
|
-
fn: (repo: Repo) => Promise<T>,
|
|
57
|
-
): Promise<T> {
|
|
58
|
-
const repo = await openOfflineRepo(storage);
|
|
59
|
-
try {
|
|
60
|
-
return await fn(repo);
|
|
61
|
-
} finally {
|
|
62
|
-
await repo.shutdown();
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function readDoc<T>(handle: DocHandle<T>): T {
|
|
67
|
-
const d = handle.doc();
|
|
68
|
-
if (!d) throw new Error(`empty doc at ${handle.url}`);
|
|
69
|
-
return d;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
describe("doc shape", () => {
|
|
73
|
-
let workRoot: string;
|
|
74
|
-
let cleanup: () => void;
|
|
75
|
-
|
|
76
|
-
beforeEach(() => {
|
|
77
|
-
const t = tmp.dirSync({ unsafeCleanup: true });
|
|
78
|
-
workRoot = t.name;
|
|
79
|
-
cleanup = t.removeCallback;
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
afterEach(() => cleanup());
|
|
83
|
-
|
|
84
|
-
const storageOf = (root: string) => path.join(root, ".pushwork", "storage");
|
|
85
|
-
|
|
86
|
-
it("init wraps the folder doc in a BranchesDoc by default", async () => {
|
|
87
|
-
await fs.writeFile(path.join(workRoot, "a.txt"), "hi\n");
|
|
88
|
-
await init({
|
|
89
|
-
dir: workRoot,
|
|
90
|
-
backend: "subduction",
|
|
91
|
-
shape: "vfs",
|
|
92
|
-
online: false,
|
|
93
|
-
});
|
|
94
|
-
const cfg = await readConfig(workRoot);
|
|
95
|
-
expect(cfg.branches).toBe(true);
|
|
96
|
-
|
|
97
|
-
await withRepo(storageOf(workRoot), async (repo) => {
|
|
98
|
-
const root = await repo.find(cfg.rootUrl);
|
|
99
|
-
expect(detectDocType(root.doc())).toBe("branches");
|
|
100
|
-
const doc = readDoc(root) as BranchesDoc;
|
|
101
|
-
expect(isBranchesDoc(doc)).toBe(true);
|
|
102
|
-
expect(Object.keys(doc.branches)).toEqual(["default"]);
|
|
103
|
-
const folderUrl = doc.branches.default;
|
|
104
|
-
const folder = await repo.find(folderUrl);
|
|
105
|
-
expect(detectDocType(folder.doc())).toBe("directory");
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("init --no-branches skips the BranchesDoc wrapper", async () => {
|
|
110
|
-
await fs.writeFile(path.join(workRoot, "a.txt"), "hi\n");
|
|
111
|
-
const rootUrl = await init({
|
|
112
|
-
dir: workRoot,
|
|
113
|
-
backend: "subduction",
|
|
114
|
-
shape: "vfs",
|
|
115
|
-
branches: false,
|
|
116
|
-
online: false,
|
|
117
|
-
});
|
|
118
|
-
const cfg = await readConfig(workRoot);
|
|
119
|
-
expect(cfg.branches).toBe(false);
|
|
120
|
-
expect(cfg.rootUrl).toBe(rootUrl);
|
|
121
|
-
await withRepo(storageOf(workRoot), async (repo) => {
|
|
122
|
-
const root = await repo.find(rootUrl);
|
|
123
|
-
expect(detectDocType(root.doc())).toBe("directory");
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("file content is stored in separate UnixFileEntry docs (indirection)", async () => {
|
|
128
|
-
await fs.writeFile(path.join(workRoot, "a.txt"), "hello world\n");
|
|
129
|
-
await init({
|
|
130
|
-
dir: workRoot,
|
|
131
|
-
backend: "subduction",
|
|
132
|
-
shape: "vfs",
|
|
133
|
-
online: false,
|
|
134
|
-
});
|
|
135
|
-
const cfg = await readConfig(workRoot);
|
|
136
|
-
await withRepo(storageOf(workRoot), async (repo) => {
|
|
137
|
-
const root = await repo.find(cfg.rootUrl);
|
|
138
|
-
const doc = readDoc(root) as BranchesDoc;
|
|
139
|
-
const folder = await repo.find(doc.branches.default);
|
|
140
|
-
const folderDoc = readDoc(folder) as Record<string, unknown>;
|
|
141
|
-
const fileUrl = folderDoc["a.txt"];
|
|
142
|
-
expect(typeof fileUrl).toBe("string");
|
|
143
|
-
const file = await repo.find(fileUrl as `automerge:${string}`);
|
|
144
|
-
const fd = readDoc(file) as UnixFileEntry;
|
|
145
|
-
expect(fd["@patchwork"].type).toBe("file");
|
|
146
|
-
expect(fd.content).toBe("hello world\n");
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("artifact files store ImmutableString content and pin URL with heads", async () => {
|
|
151
|
-
await fs.mkdir(path.join(workRoot, "dist"));
|
|
152
|
-
await fs.writeFile(path.join(workRoot, "dist", "main.js"), "console.log(1)\n");
|
|
153
|
-
await fs.writeFile(path.join(workRoot, "src.ts"), "export {}\n");
|
|
154
|
-
await init({
|
|
155
|
-
dir: workRoot,
|
|
156
|
-
backend: "subduction",
|
|
157
|
-
shape: "vfs",
|
|
158
|
-
online: false,
|
|
159
|
-
});
|
|
160
|
-
const cfg = await readConfig(workRoot);
|
|
161
|
-
await withRepo(storageOf(workRoot), async (repo) => {
|
|
162
|
-
const root = await repo.find(cfg.rootUrl);
|
|
163
|
-
const doc = readDoc(root) as BranchesDoc;
|
|
164
|
-
const folder = await repo.find(doc.branches.default);
|
|
165
|
-
const folderDoc = readDoc(folder) as Record<string, unknown>;
|
|
166
|
-
|
|
167
|
-
const artifactUrl = folderDoc["dist/main.js"] as string;
|
|
168
|
-
const sourceUrl = folderDoc["src.ts"] as string;
|
|
169
|
-
expect(parseAutomergeUrl(artifactUrl).heads).toBeTruthy();
|
|
170
|
-
expect(parseAutomergeUrl(sourceUrl).heads).toBeFalsy();
|
|
171
|
-
|
|
172
|
-
const artifactDoc = readDoc(await repo.find(artifactUrl)) as UnixFileEntry;
|
|
173
|
-
expect(isImmutableString(artifactDoc.content)).toBe(true);
|
|
174
|
-
|
|
175
|
-
const sourceDoc = readDoc(await repo.find(sourceUrl)) as UnixFileEntry;
|
|
176
|
-
expect(typeof sourceDoc.content).toBe("string");
|
|
177
|
-
expect(isImmutableString(sourceDoc.content)).toBe(false);
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("binary files store content as Uint8Array", async () => {
|
|
182
|
-
const bytes = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0x01, 0xff]);
|
|
183
|
-
await fs.writeFile(path.join(workRoot, "img.png"), bytes);
|
|
184
|
-
await init({
|
|
185
|
-
dir: workRoot,
|
|
186
|
-
backend: "subduction",
|
|
187
|
-
shape: "vfs",
|
|
188
|
-
online: false,
|
|
189
|
-
});
|
|
190
|
-
const cfg = await readConfig(workRoot);
|
|
191
|
-
await withRepo(storageOf(workRoot), async (repo) => {
|
|
192
|
-
const root = await repo.find(cfg.rootUrl);
|
|
193
|
-
const doc = readDoc(root) as BranchesDoc;
|
|
194
|
-
const folder = await repo.find(doc.branches.default);
|
|
195
|
-
const folderDoc = readDoc(folder) as Record<string, unknown>;
|
|
196
|
-
const file = await repo.find(folderDoc["img.png"] as `automerge:${string}`);
|
|
197
|
-
const fd = readDoc(file) as UnixFileEntry;
|
|
198
|
-
expect(fd.content instanceof Uint8Array).toBe(true);
|
|
199
|
-
expect(Array.from(fd.content as Uint8Array)).toEqual(Array.from(bytes));
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
describe("file-doc lifecycle", () => {
|
|
205
|
-
let workRoot: string;
|
|
206
|
-
let cleanup: () => void;
|
|
207
|
-
|
|
208
|
-
beforeEach(() => {
|
|
209
|
-
const t = tmp.dirSync({ unsafeCleanup: true });
|
|
210
|
-
workRoot = t.name;
|
|
211
|
-
cleanup = t.removeCallback;
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
afterEach(() => cleanup());
|
|
215
|
-
|
|
216
|
-
const storageOf = (root: string) => path.join(root, ".pushwork", "storage");
|
|
217
|
-
|
|
218
|
-
it("file URLs stay stable within a branch across edits (mutation, not clone)", async () => {
|
|
219
|
-
// pushFiles mutates the existing UnixFileEntry doc in place, which
|
|
220
|
-
// keeps the file URL stable within a branch. (Branch isolation comes
|
|
221
|
-
// from createBranch deep-cloning, not from per-edit cloning.)
|
|
222
|
-
await fs.writeFile(path.join(workRoot, "stable.txt"), "stable\n");
|
|
223
|
-
await fs.writeFile(path.join(workRoot, "edited.txt"), "v1\n");
|
|
224
|
-
await init({
|
|
225
|
-
dir: workRoot,
|
|
226
|
-
backend: "subduction",
|
|
227
|
-
shape: "vfs",
|
|
228
|
-
online: false,
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
async function urlFor(filePath: string): Promise<string> {
|
|
232
|
-
const cfg = await readConfig(workRoot);
|
|
233
|
-
return withRepo(storageOf(workRoot), async (repo) => {
|
|
234
|
-
const root = await repo.find(cfg.rootUrl);
|
|
235
|
-
const doc = readDoc(root) as BranchesDoc;
|
|
236
|
-
const folder = await repo.find(doc.branches.default);
|
|
237
|
-
const folderDoc = readDoc(folder) as Record<string, unknown>;
|
|
238
|
-
return folderDoc[filePath] as string;
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const stableUrl1 = await urlFor("stable.txt");
|
|
243
|
-
const editedUrl1 = await urlFor("edited.txt");
|
|
244
|
-
|
|
245
|
-
await fs.writeFile(path.join(workRoot, "edited.txt"), "v2\n");
|
|
246
|
-
await save(workRoot);
|
|
247
|
-
|
|
248
|
-
const stableUrl2 = await urlFor("stable.txt");
|
|
249
|
-
const editedUrl2 = await urlFor("edited.txt");
|
|
250
|
-
|
|
251
|
-
expect(stableUrl2).toBe(stableUrl1);
|
|
252
|
-
expect(editedUrl2).toBe(editedUrl1);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it("save does not stamp lastSyncAt; sync would (offline test of negative case)", async () => {
|
|
256
|
-
await fs.writeFile(path.join(workRoot, "x.txt"), "x\n");
|
|
257
|
-
await init({
|
|
258
|
-
dir: workRoot,
|
|
259
|
-
backend: "subduction",
|
|
260
|
-
shape: "vfs",
|
|
261
|
-
online: false,
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
async function lastSyncAt(): Promise<number | undefined> {
|
|
265
|
-
const cfg = await readConfig(workRoot);
|
|
266
|
-
return withRepo(storageOf(workRoot), async (repo) => {
|
|
267
|
-
const root = await repo.find(cfg.rootUrl);
|
|
268
|
-
const doc = readDoc(root) as BranchesDoc;
|
|
269
|
-
const folder = await repo.find(doc.branches.default);
|
|
270
|
-
return (readDoc(folder) as { lastSyncAt?: number }).lastSyncAt;
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// init with online:false skips the lastSyncAt stamp too
|
|
275
|
-
expect(await lastSyncAt()).toBeUndefined();
|
|
276
|
-
|
|
277
|
-
await fs.writeFile(path.join(workRoot, "x.txt"), "y\n");
|
|
278
|
-
await save(workRoot);
|
|
279
|
-
|
|
280
|
-
expect(await lastSyncAt()).toBeUndefined();
|
|
281
|
-
});
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
describe("branch isolation", () => {
|
|
285
|
-
let workRoot: string;
|
|
286
|
-
let cleanup: () => void;
|
|
287
|
-
|
|
288
|
-
beforeEach(() => {
|
|
289
|
-
const t = tmp.dirSync({ unsafeCleanup: true });
|
|
290
|
-
workRoot = t.name;
|
|
291
|
-
cleanup = t.removeCallback;
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
afterEach(() => cleanup());
|
|
295
|
-
|
|
296
|
-
const storageOf = (root: string) => path.join(root, ".pushwork", "storage");
|
|
297
|
-
|
|
298
|
-
it("branch <name> creates a new branch with independent file-doc URLs", async () => {
|
|
299
|
-
// `createBranch` deep-clones every file doc the source folder
|
|
300
|
-
// references so editing on one branch can never alias the other.
|
|
301
|
-
await fs.writeFile(path.join(workRoot, "a.txt"), "hi\n");
|
|
302
|
-
await init({
|
|
303
|
-
dir: workRoot,
|
|
304
|
-
backend: "subduction",
|
|
305
|
-
shape: "vfs",
|
|
306
|
-
online: false,
|
|
307
|
-
});
|
|
308
|
-
await createBranch(workRoot, "feat");
|
|
309
|
-
|
|
310
|
-
const cfg = await readConfig(workRoot);
|
|
311
|
-
await withRepo(storageOf(workRoot), async (repo) => {
|
|
312
|
-
const root = await repo.find(cfg.rootUrl);
|
|
313
|
-
const doc = readDoc(root) as BranchesDoc;
|
|
314
|
-
const defaultUrl = doc.branches.default;
|
|
315
|
-
const featUrl = doc.branches.feat;
|
|
316
|
-
expect(defaultUrl).not.toBe(featUrl);
|
|
317
|
-
|
|
318
|
-
const def = readDoc(await repo.find(defaultUrl)) as Record<
|
|
319
|
-
string,
|
|
320
|
-
unknown
|
|
321
|
-
>;
|
|
322
|
-
const feat = readDoc(await repo.find(featUrl)) as Record<
|
|
323
|
-
string,
|
|
324
|
-
unknown
|
|
325
|
-
>;
|
|
326
|
-
// File URLs differ (clone), but content matches.
|
|
327
|
-
expect(feat["a.txt"]).not.toBe(def["a.txt"]);
|
|
328
|
-
const defFile = readDoc(
|
|
329
|
-
await repo.find(def["a.txt"] as `automerge:${string}`),
|
|
330
|
-
) as UnixFileEntry;
|
|
331
|
-
const featFile = readDoc(
|
|
332
|
-
await repo.find(feat["a.txt"] as `automerge:${string}`),
|
|
333
|
-
) as UnixFileEntry;
|
|
334
|
-
expect(featFile.content).toBe(defFile.content);
|
|
335
|
-
});
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
it("editing on a branch does not change the source branch's folder doc", async () => {
|
|
339
|
-
await fs.writeFile(path.join(workRoot, "a.txt"), "hi\n");
|
|
340
|
-
await init({
|
|
341
|
-
dir: workRoot,
|
|
342
|
-
backend: "subduction",
|
|
343
|
-
shape: "vfs",
|
|
344
|
-
online: false,
|
|
345
|
-
});
|
|
346
|
-
await createBranch(workRoot, "feat");
|
|
347
|
-
|
|
348
|
-
const cfg = await readConfig(workRoot);
|
|
349
|
-
const beforeDefault = await withRepo(storageOf(workRoot), async (repo) => {
|
|
350
|
-
const root = await repo.find(cfg.rootUrl);
|
|
351
|
-
const doc = readDoc(root) as BranchesDoc;
|
|
352
|
-
const folder = await repo.find(doc.branches.default);
|
|
353
|
-
return (readDoc(folder) as Record<string, unknown>)["a.txt"];
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
await switchBranch(workRoot, "feat");
|
|
357
|
-
await fs.writeFile(path.join(workRoot, "a.txt"), "edited on feat\n");
|
|
358
|
-
await save(workRoot);
|
|
359
|
-
|
|
360
|
-
const afterDefault = await withRepo(storageOf(workRoot), async (repo) => {
|
|
361
|
-
const root = await repo.find(cfg.rootUrl);
|
|
362
|
-
const doc = readDoc(root) as BranchesDoc;
|
|
363
|
-
const folder = await repo.find(doc.branches.default);
|
|
364
|
-
return (readDoc(folder) as Record<string, unknown>)["a.txt"];
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
expect(afterDefault).toBe(beforeDefault); // default's folder doc unchanged
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
it("merge brings non-conflicting source changes into target", async () => {
|
|
371
|
-
await fs.writeFile(path.join(workRoot, "shared.txt"), "shared\n");
|
|
372
|
-
await fs.writeFile(path.join(workRoot, "target.txt"), "T\n");
|
|
373
|
-
await init({
|
|
374
|
-
dir: workRoot,
|
|
375
|
-
backend: "subduction",
|
|
376
|
-
shape: "vfs",
|
|
377
|
-
online: false,
|
|
378
|
-
});
|
|
379
|
-
await createBranch(workRoot, "feat");
|
|
380
|
-
await switchBranch(workRoot, "feat");
|
|
381
|
-
|
|
382
|
-
// Edit shared.txt and add a new file on feat.
|
|
383
|
-
await fs.writeFile(path.join(workRoot, "shared.txt"), "shared edited on feat\n");
|
|
384
|
-
await fs.writeFile(path.join(workRoot, "feat-only.txt"), "F\n");
|
|
385
|
-
await save(workRoot);
|
|
386
|
-
|
|
387
|
-
// Switch back to default and edit target.txt only.
|
|
388
|
-
await switchBranch(workRoot, "default");
|
|
389
|
-
await fs.writeFile(path.join(workRoot, "target.txt"), "T edited on default\n");
|
|
390
|
-
await save(workRoot);
|
|
391
|
-
|
|
392
|
-
const report = await mergeBranch(workRoot, "feat");
|
|
393
|
-
expect(report.source).toBe("feat");
|
|
394
|
-
expect(report.target).toBe("default");
|
|
395
|
-
expect(report.merged.sort()).toEqual(["shared.txt", "target.txt"]);
|
|
396
|
-
expect(report.added).toEqual(["feat-only.txt"]);
|
|
397
|
-
|
|
398
|
-
expect(await fs.readFile(path.join(workRoot, "shared.txt"), "utf8")).toBe(
|
|
399
|
-
"shared edited on feat\n",
|
|
400
|
-
);
|
|
401
|
-
expect(await fs.readFile(path.join(workRoot, "target.txt"), "utf8")).toBe(
|
|
402
|
-
"T edited on default\n",
|
|
403
|
-
);
|
|
404
|
-
expect(await fs.readFile(path.join(workRoot, "feat-only.txt"), "utf8")).toBe(
|
|
405
|
-
"F\n",
|
|
406
|
-
);
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
it("merge refuses on dirty working tree", async () => {
|
|
410
|
-
await fs.writeFile(path.join(workRoot, "a.txt"), "a\n");
|
|
411
|
-
await init({
|
|
412
|
-
dir: workRoot,
|
|
413
|
-
backend: "subduction",
|
|
414
|
-
shape: "vfs",
|
|
415
|
-
online: false,
|
|
416
|
-
});
|
|
417
|
-
await createBranch(workRoot, "feat");
|
|
418
|
-
await fs.writeFile(path.join(workRoot, "dirty.txt"), "uncommitted\n");
|
|
419
|
-
await expect(mergeBranch(workRoot, "feat")).rejects.toThrow(
|
|
420
|
-
/uncommitted changes/,
|
|
421
|
-
);
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
it("merge does character-level CRDT merge on text content", async () => {
|
|
425
|
-
await fs.writeFile(path.join(workRoot, "x.txt"), "line one\nline two\nline three\n");
|
|
426
|
-
await init({
|
|
427
|
-
dir: workRoot,
|
|
428
|
-
backend: "subduction",
|
|
429
|
-
shape: "vfs",
|
|
430
|
-
online: false,
|
|
431
|
-
});
|
|
432
|
-
await createBranch(workRoot, "other");
|
|
433
|
-
await switchBranch(workRoot, "other");
|
|
434
|
-
await fs.writeFile(
|
|
435
|
-
path.join(workRoot, "x.txt"),
|
|
436
|
-
"line one\nline two\nline three EDITED ON OTHER\n",
|
|
437
|
-
);
|
|
438
|
-
await save(workRoot);
|
|
439
|
-
await switchBranch(workRoot, "default");
|
|
440
|
-
await fs.writeFile(
|
|
441
|
-
path.join(workRoot, "x.txt"),
|
|
442
|
-
"line one EDITED ON DEFAULT\nline two\nline three\n",
|
|
443
|
-
);
|
|
444
|
-
await save(workRoot);
|
|
445
|
-
await mergeBranch(workRoot, "other");
|
|
446
|
-
|
|
447
|
-
const merged = await fs.readFile(path.join(workRoot, "x.txt"), "utf8");
|
|
448
|
-
expect(merged).toContain("line one EDITED ON DEFAULT");
|
|
449
|
-
expect(merged).toContain("line three EDITED ON OTHER");
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
it("previewMerge shows entries without mutating", async () => {
|
|
453
|
-
await fs.writeFile(path.join(workRoot, "a.txt"), "v1\n");
|
|
454
|
-
await init({
|
|
455
|
-
dir: workRoot,
|
|
456
|
-
backend: "subduction",
|
|
457
|
-
shape: "vfs",
|
|
458
|
-
online: false,
|
|
459
|
-
});
|
|
460
|
-
await createBranch(workRoot, "feat");
|
|
461
|
-
await switchBranch(workRoot, "feat");
|
|
462
|
-
await fs.writeFile(path.join(workRoot, "a.txt"), "v2\n");
|
|
463
|
-
await fs.writeFile(path.join(workRoot, "new.txt"), "n\n");
|
|
464
|
-
await save(workRoot);
|
|
465
|
-
await switchBranch(workRoot, "default");
|
|
466
|
-
|
|
467
|
-
const preview = await previewMerge(workRoot, "feat");
|
|
468
|
-
const paths = preview.entries.map((e) => e.path).sort();
|
|
469
|
-
expect(paths).toEqual(["a.txt", "new.txt"]);
|
|
470
|
-
expect(preview.entries.find((e) => e.path === "a.txt")?.kind).toBe(
|
|
471
|
-
"merged",
|
|
472
|
-
);
|
|
473
|
-
expect(preview.entries.find((e) => e.path === "new.txt")?.kind).toBe(
|
|
474
|
-
"added",
|
|
475
|
-
);
|
|
476
|
-
// Working tree unchanged on disk:
|
|
477
|
-
expect(await fs.readFile(path.join(workRoot, "a.txt"), "utf8")).toBe("v1\n");
|
|
478
|
-
expect(await pathExists(path.join(workRoot, "new.txt"))).toBe(false);
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
it("merge errors on missing source branch", async () => {
|
|
482
|
-
await fs.writeFile(path.join(workRoot, "a.txt"), "a\n");
|
|
483
|
-
await init({
|
|
484
|
-
dir: workRoot,
|
|
485
|
-
backend: "subduction",
|
|
486
|
-
shape: "vfs",
|
|
487
|
-
online: false,
|
|
488
|
-
});
|
|
489
|
-
await expect(mergeBranch(workRoot, "ghost")).rejects.toThrow(/does not exist/);
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
it("cut/paste round-trips modifications, additions, and deletions", async () => {
|
|
493
|
-
await fs.writeFile(path.join(workRoot, "mod.txt"), "v1\n");
|
|
494
|
-
await fs.writeFile(path.join(workRoot, "doomed.txt"), "remove me\n");
|
|
495
|
-
await init({
|
|
496
|
-
dir: workRoot,
|
|
497
|
-
backend: "subduction",
|
|
498
|
-
shape: "vfs",
|
|
499
|
-
online: false,
|
|
500
|
-
});
|
|
501
|
-
await fs.writeFile(path.join(workRoot, "mod.txt"), "v2\n");
|
|
502
|
-
await fs.writeFile(path.join(workRoot, "added.txt"), "new\n");
|
|
503
|
-
await fs.unlink(path.join(workRoot, "doomed.txt"));
|
|
504
|
-
|
|
505
|
-
const cut = await cutWorkdir(workRoot, { name: "wip" });
|
|
506
|
-
expect(cut.entries).toBe(3);
|
|
507
|
-
|
|
508
|
-
// Working tree restored to clean state:
|
|
509
|
-
expect(await fs.readFile(path.join(workRoot, "mod.txt"), "utf8")).toBe(
|
|
510
|
-
"v1\n",
|
|
511
|
-
);
|
|
512
|
-
expect(
|
|
513
|
-
await fs.readFile(path.join(workRoot, "doomed.txt"), "utf8"),
|
|
514
|
-
).toBe("remove me\n");
|
|
515
|
-
expect(await pathExists(path.join(workRoot, "added.txt"))).toBe(false);
|
|
516
|
-
|
|
517
|
-
const stashes = await showStashes(workRoot);
|
|
518
|
-
expect(stashes.length).toBe(1);
|
|
519
|
-
expect(stashes[0].name).toBe("wip");
|
|
520
|
-
expect(stashes[0].branch).toBe("default");
|
|
521
|
-
|
|
522
|
-
await pasteStash(workRoot);
|
|
523
|
-
|
|
524
|
-
expect(await fs.readFile(path.join(workRoot, "mod.txt"), "utf8")).toBe(
|
|
525
|
-
"v2\n",
|
|
526
|
-
);
|
|
527
|
-
expect(await fs.readFile(path.join(workRoot, "added.txt"), "utf8")).toBe(
|
|
528
|
-
"new\n",
|
|
529
|
-
);
|
|
530
|
-
expect(await pathExists(path.join(workRoot, "doomed.txt"))).toBe(false);
|
|
531
|
-
|
|
532
|
-
expect((await showStashes(workRoot)).length).toBe(0);
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
it("cut refuses on a clean working tree", async () => {
|
|
536
|
-
await fs.writeFile(path.join(workRoot, "a.txt"), "a\n");
|
|
537
|
-
await init({
|
|
538
|
-
dir: workRoot,
|
|
539
|
-
backend: "subduction",
|
|
540
|
-
shape: "vfs",
|
|
541
|
-
online: false,
|
|
542
|
-
});
|
|
543
|
-
await expect(cutWorkdir(workRoot)).rejects.toThrow(/working tree clean/);
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
it("paste refuses on a dirty working tree", async () => {
|
|
547
|
-
await fs.writeFile(path.join(workRoot, "a.txt"), "a\n");
|
|
548
|
-
await init({
|
|
549
|
-
dir: workRoot,
|
|
550
|
-
backend: "subduction",
|
|
551
|
-
shape: "vfs",
|
|
552
|
-
online: false,
|
|
553
|
-
});
|
|
554
|
-
await fs.writeFile(path.join(workRoot, "a.txt"), "edited\n");
|
|
555
|
-
await cutWorkdir(workRoot);
|
|
556
|
-
// dirty the working tree again
|
|
557
|
-
await fs.writeFile(path.join(workRoot, "b.txt"), "extra\n");
|
|
558
|
-
await expect(pasteStash(workRoot)).rejects.toThrow(/uncommitted/);
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
it("paste with no stashes errors", async () => {
|
|
562
|
-
await fs.writeFile(path.join(workRoot, "a.txt"), "a\n");
|
|
563
|
-
await init({
|
|
564
|
-
dir: workRoot,
|
|
565
|
-
backend: "subduction",
|
|
566
|
-
shape: "vfs",
|
|
567
|
-
online: false,
|
|
568
|
-
});
|
|
569
|
-
await expect(pasteStash(workRoot)).rejects.toThrow(/no stashes/);
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
it("paste with id selects a specific stash", async () => {
|
|
573
|
-
await fs.writeFile(path.join(workRoot, "a.txt"), "a\n");
|
|
574
|
-
await init({
|
|
575
|
-
dir: workRoot,
|
|
576
|
-
backend: "subduction",
|
|
577
|
-
shape: "vfs",
|
|
578
|
-
online: false,
|
|
579
|
-
});
|
|
580
|
-
await fs.writeFile(path.join(workRoot, "first.txt"), "1\n");
|
|
581
|
-
const c1 = await cutWorkdir(workRoot, { name: "first" });
|
|
582
|
-
await fs.writeFile(path.join(workRoot, "second.txt"), "2\n");
|
|
583
|
-
const c2 = await cutWorkdir(workRoot, { name: "second" });
|
|
584
|
-
|
|
585
|
-
const out = await pasteStash(workRoot, String(c1.id));
|
|
586
|
-
expect(out.id).toBe(c1.id);
|
|
587
|
-
expect(await pathExists(path.join(workRoot, "first.txt"))).toBe(true);
|
|
588
|
-
// Second stash is still there
|
|
589
|
-
const stashes = await showStashes(workRoot);
|
|
590
|
-
expect(stashes.length).toBe(1);
|
|
591
|
-
expect(stashes[0].id).toBe(c2.id);
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
it("listBranches reports current and all names", async () => {
|
|
595
|
-
await fs.writeFile(path.join(workRoot, "a.txt"), "hi\n");
|
|
596
|
-
await init({
|
|
597
|
-
dir: workRoot,
|
|
598
|
-
backend: "subduction",
|
|
599
|
-
shape: "vfs",
|
|
600
|
-
online: false,
|
|
601
|
-
});
|
|
602
|
-
await createBranch(workRoot, "feat");
|
|
603
|
-
await createBranch(workRoot, "bugfix");
|
|
604
|
-
const out = await listBranches(workRoot);
|
|
605
|
-
expect(out.current).toBe("default");
|
|
606
|
-
expect(out.names.sort()).toEqual(["bugfix", "default", "feat"]);
|
|
607
|
-
|
|
608
|
-
expect(await currentBranch(workRoot)).toBe("default");
|
|
609
|
-
await switchBranch(workRoot, "feat");
|
|
610
|
-
expect(await currentBranch(workRoot)).toBe("feat");
|
|
611
|
-
});
|
|
612
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"module": "commonjs",
|
|
5
|
-
"lib": ["ES2020"],
|
|
6
|
-
"outDir": "./dist",
|
|
7
|
-
"rootDir": "./src",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"forceConsistentCasingInFileNames": true,
|
|
12
|
-
"declaration": true,
|
|
13
|
-
"declarationMap": true,
|
|
14
|
-
"sourceMap": true,
|
|
15
|
-
"resolveJsonModule": true,
|
|
16
|
-
"types": ["node"],
|
|
17
|
-
"noUnusedLocals": true,
|
|
18
|
-
"noUnusedParameters": true
|
|
19
|
-
},
|
|
20
|
-
"include": ["src/**/*"],
|
|
21
|
-
"exclude": ["node_modules", "dist", "test"]
|
|
22
|
-
}
|
package/vitest.config.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from "vitest/config";
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
test: {
|
|
5
|
-
globals: true,
|
|
6
|
-
environment: "node",
|
|
7
|
-
include: ["src/**/*.{test,spec}.ts", "test/**/*.{test,spec}.ts"],
|
|
8
|
-
setupFiles: ["./test/setup.ts"],
|
|
9
|
-
// Many integration tests spawn the CLI as a subprocess; allow time
|
|
10
|
-
// for build server / sync server roundtrips.
|
|
11
|
-
testTimeout: 60000,
|
|
12
|
-
hookTimeout: 60000,
|
|
13
|
-
},
|
|
14
|
-
});
|