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
package/dist/pushwork.js
CHANGED
|
@@ -33,43 +33,34 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.deleteBranchFile = void 0;
|
|
37
36
|
exports.init = init;
|
|
38
37
|
exports.clone = clone;
|
|
39
38
|
exports.url = url;
|
|
40
39
|
exports.sync = sync;
|
|
40
|
+
exports.nuclearizeRepo = nuclearizeRepo;
|
|
41
41
|
exports.save = save;
|
|
42
42
|
exports.status = status;
|
|
43
43
|
exports.diff = diff;
|
|
44
|
-
exports.listBranches = listBranches;
|
|
45
|
-
exports.currentBranch = currentBranch;
|
|
46
|
-
exports.createBranch = createBranch;
|
|
47
|
-
exports.previewMerge = previewMerge;
|
|
48
|
-
exports.mergeBranch = mergeBranch;
|
|
49
44
|
exports.cutWorkdir = cutWorkdir;
|
|
50
|
-
exports.
|
|
51
|
-
exports.
|
|
52
|
-
exports.switchBranch = switchBranch;
|
|
45
|
+
exports.pasteSnarf = pasteSnarf;
|
|
46
|
+
exports.showSnarfs = showSnarfs;
|
|
53
47
|
const fs = __importStar(require("fs/promises"));
|
|
54
48
|
const path = __importStar(require("path"));
|
|
55
49
|
const Automerge = __importStar(require("@automerge/automerge"));
|
|
56
50
|
const automerge_repo_1 = require("@automerge/automerge-repo");
|
|
57
51
|
const config_js_1 = require("./config.js");
|
|
58
|
-
const branches_js_1 = require("./branches.js");
|
|
59
|
-
Object.defineProperty(exports, "deleteBranchFile", { enumerable: true, get: function () { return branches_js_1.deleteBranchFile; } });
|
|
60
52
|
const ignore_js_1 = require("./ignore.js");
|
|
61
53
|
const fs_tree_js_1 = require("./fs-tree.js");
|
|
62
54
|
const log_js_1 = require("./log.js");
|
|
63
55
|
const repo_js_1 = require("./repo.js");
|
|
64
|
-
const
|
|
56
|
+
const snarf_js_1 = require("./snarf.js");
|
|
65
57
|
const index_js_1 = require("./shapes/index.js");
|
|
66
58
|
const dlog = (0, log_js_1.log)("pushwork");
|
|
67
59
|
const DEFAULT_ARTIFACT_DIRECTORIES = ["dist"];
|
|
68
60
|
async function init(opts) {
|
|
69
61
|
const root = path.resolve(opts.dir);
|
|
70
|
-
const useBranches = opts.branches ?? true;
|
|
71
62
|
const online = opts.online ?? true;
|
|
72
|
-
dlog("init root=%s backend=%s shape=%s
|
|
63
|
+
dlog("init root=%s backend=%s shape=%s online=%s", root, opts.backend, opts.shape, online);
|
|
73
64
|
if (await (0, config_js_1.configExists)(root)) {
|
|
74
65
|
throw new Error(`pushwork already initialized at ${root}`);
|
|
75
66
|
}
|
|
@@ -82,38 +73,25 @@ async function init(opts) {
|
|
|
82
73
|
const ig = await (0, ignore_js_1.loadIgnore)(root);
|
|
83
74
|
const fsFiles = await (0, fs_tree_js_1.walkDir)(root, ig);
|
|
84
75
|
dlog("init walked %d files", fsFiles.size);
|
|
76
|
+
const title = path.basename(root) || undefined;
|
|
85
77
|
const tree = await pushFiles(repo, fsFiles, undefined, artifactDirs);
|
|
86
|
-
const folderUrl = await shape.encode({ repo, tree });
|
|
87
|
-
dlog("init encoded folder=%s", folderUrl);
|
|
78
|
+
const folderUrl = await shape.encode({ repo, tree, title });
|
|
79
|
+
dlog("init encoded folder=%s title=%s", folderUrl, title);
|
|
88
80
|
const folderHandle = await repo.find(folderUrl);
|
|
89
81
|
if (online) {
|
|
90
82
|
await (0, repo_js_1.waitForSync)(folderHandle, { minMs: 3000, idleMs: 1500, maxMs: 15000 });
|
|
91
83
|
stampLastSyncAt(folderHandle);
|
|
92
84
|
await (0, repo_js_1.waitForSync)(folderHandle, { idleMs: 1500, maxMs: 10000 });
|
|
93
85
|
}
|
|
94
|
-
let rootUrl = folderUrl;
|
|
95
|
-
if (useBranches) {
|
|
96
|
-
const branchesHandle = repo.create({
|
|
97
|
-
"@patchwork": { type: "branches" },
|
|
98
|
-
branches: { [branches_js_1.DEFAULT_BRANCH]: folderUrl },
|
|
99
|
-
});
|
|
100
|
-
if (online) {
|
|
101
|
-
await (0, repo_js_1.waitForSync)(branchesHandle, { minMs: 1500, idleMs: 1500, maxMs: 10000 });
|
|
102
|
-
}
|
|
103
|
-
rootUrl = branchesHandle.url;
|
|
104
|
-
dlog("init wrapped in BranchesDoc=%s", rootUrl);
|
|
105
|
-
await (0, branches_js_1.writeBranchFile)(root, branches_js_1.DEFAULT_BRANCH);
|
|
106
|
-
}
|
|
107
86
|
await (0, config_js_1.writeConfig)(root, {
|
|
108
87
|
version: config_js_1.CONFIG_VERSION,
|
|
109
|
-
rootUrl,
|
|
88
|
+
rootUrl: folderUrl,
|
|
110
89
|
backend: opts.backend,
|
|
111
90
|
shape: opts.shape,
|
|
112
91
|
artifactDirectories: artifactDirs,
|
|
113
|
-
branches: useBranches,
|
|
114
92
|
});
|
|
115
|
-
dlog("init complete: rootUrl=%s",
|
|
116
|
-
return
|
|
93
|
+
dlog("init complete: rootUrl=%s", folderUrl);
|
|
94
|
+
return folderUrl;
|
|
117
95
|
}
|
|
118
96
|
finally {
|
|
119
97
|
await repo.shutdown();
|
|
@@ -135,36 +113,36 @@ async function clone(opts) {
|
|
|
135
113
|
const repo = await (0, repo_js_1.openRepo)(opts.backend, (0, config_js_1.storageDir)(root), { offline: !online });
|
|
136
114
|
try {
|
|
137
115
|
const shape = await (0, index_js_1.resolveShape)(opts.shape);
|
|
138
|
-
|
|
116
|
+
let folderHandle = await repo.find(opts.url);
|
|
139
117
|
if (online) {
|
|
140
|
-
await (0, repo_js_1.waitForSync)(
|
|
118
|
+
await (0, repo_js_1.waitForSync)(folderHandle, { idleMs: 1500, maxMs: 15000 });
|
|
141
119
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
120
|
+
let storedUrl = opts.url;
|
|
121
|
+
const branchesDoc = asBranchesDoc(folderHandle.doc());
|
|
122
|
+
if (branchesDoc) {
|
|
123
|
+
if (!opts.onBranchesDoc) {
|
|
124
|
+
throw new Error(`URL ${opts.url} is a legacy branches doc; pushwork no longer supports branches. Provide an onBranchesDoc callback (or use the CLI, which will prompt you to pick a branch).`);
|
|
125
|
+
}
|
|
126
|
+
const branches = Object.entries(branchesDoc.branches).map(([name, url]) => ({ name, url }));
|
|
127
|
+
const chosenUrl = await opts.onBranchesDoc({
|
|
128
|
+
title: branchesDoc.title,
|
|
129
|
+
branches,
|
|
130
|
+
});
|
|
131
|
+
dlog("clone branches doc → chose %s", chosenUrl);
|
|
132
|
+
folderHandle = await repo.find(chosenUrl);
|
|
150
133
|
if (online) {
|
|
151
134
|
await (0, repo_js_1.waitForSync)(folderHandle, { idleMs: 1500, maxMs: 15000 });
|
|
152
135
|
}
|
|
153
|
-
|
|
154
|
-
dlog("clone branch=%s folder=%s", branchName, folderHandle.url);
|
|
155
|
-
}
|
|
156
|
-
else if (opts.branch) {
|
|
157
|
-
throw new Error(`--branch passed but root doc is not a branches doc (type=${docType})`);
|
|
136
|
+
storedUrl = chosenUrl;
|
|
158
137
|
}
|
|
159
138
|
const tree = await shape.decode({ repo, root: folderHandle });
|
|
160
139
|
await materializeTree(repo, root, tree);
|
|
161
140
|
await (0, config_js_1.writeConfig)(root, {
|
|
162
141
|
version: config_js_1.CONFIG_VERSION,
|
|
163
|
-
rootUrl:
|
|
142
|
+
rootUrl: storedUrl,
|
|
164
143
|
backend: opts.backend,
|
|
165
144
|
shape: opts.shape,
|
|
166
145
|
artifactDirectories: artifactDirs,
|
|
167
|
-
branches: useBranches,
|
|
168
146
|
});
|
|
169
147
|
dlog("clone complete");
|
|
170
148
|
}
|
|
@@ -172,41 +150,93 @@ async function clone(opts) {
|
|
|
172
150
|
await repo.shutdown();
|
|
173
151
|
}
|
|
174
152
|
}
|
|
153
|
+
function asBranchesDoc(doc) {
|
|
154
|
+
if (!doc || typeof doc !== "object")
|
|
155
|
+
return null;
|
|
156
|
+
const meta = doc["@patchwork"];
|
|
157
|
+
if (!meta || typeof meta !== "object")
|
|
158
|
+
return null;
|
|
159
|
+
if (meta.type !== "branches")
|
|
160
|
+
return null;
|
|
161
|
+
const branches = doc.branches;
|
|
162
|
+
if (!branches || typeof branches !== "object")
|
|
163
|
+
return null;
|
|
164
|
+
return {
|
|
165
|
+
title: meta.title,
|
|
166
|
+
branches: branches,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
175
169
|
async function url(cwd) {
|
|
176
170
|
const config = await (0, config_js_1.readConfig)(path.resolve(cwd));
|
|
177
171
|
return config.rootUrl;
|
|
178
172
|
}
|
|
179
|
-
async function sync(cwd) {
|
|
173
|
+
async function sync(cwd, opts = {}) {
|
|
174
|
+
if (opts.nuclear) {
|
|
175
|
+
await nuclearizeRepo(cwd);
|
|
176
|
+
}
|
|
180
177
|
await commitWorkdir(cwd, { online: true });
|
|
181
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Re-create every Automerge doc this repo references — every UnixFileEntry
|
|
181
|
+
* and the folder/directory doc — with brand-new URLs and no shared history
|
|
182
|
+
* with the originals. Updates `config.json` to point at the new root.
|
|
183
|
+
* Offline; the next sync publishes the new docs to the server.
|
|
184
|
+
*
|
|
185
|
+
* Use sparingly: this orphans the previous URLs from this repo's perspective.
|
|
186
|
+
* Anyone who had cloned the old URL keeps working from those docs; this
|
|
187
|
+
* client just stops referencing them.
|
|
188
|
+
*/
|
|
189
|
+
async function nuclearizeRepo(cwd) {
|
|
190
|
+
const root = path.resolve(cwd);
|
|
191
|
+
const config = await (0, config_js_1.readConfig)(root);
|
|
192
|
+
dlog("nuclear root=%s", root);
|
|
193
|
+
const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
|
|
194
|
+
try {
|
|
195
|
+
const shape = await (0, index_js_1.resolveShape)(config.shape);
|
|
196
|
+
const oldFolder = await repo.find(config.rootUrl);
|
|
197
|
+
const title = path.basename(root) || undefined;
|
|
198
|
+
const oldTree = await shape.decode({ repo, root: oldFolder });
|
|
199
|
+
// For each leaf: read content, create a fresh UnixFileEntry doc.
|
|
200
|
+
const newTree = (0, index_js_1.newDir)();
|
|
201
|
+
for (const [posixPath, fileUrl] of (0, index_js_1.flattenLeaves)(oldTree)) {
|
|
202
|
+
const bare = (0, index_js_1.stripHeads)(fileUrl);
|
|
203
|
+
const oldFileHandle = await repo.find(bare);
|
|
204
|
+
const oldDoc = oldFileHandle.doc();
|
|
205
|
+
const newFileHandle = repo.create({
|
|
206
|
+
"@patchwork": { type: "file" },
|
|
207
|
+
name: oldDoc.name,
|
|
208
|
+
extension: oldDoc.extension,
|
|
209
|
+
mimeType: oldDoc.mimeType,
|
|
210
|
+
content: oldDoc.content,
|
|
211
|
+
});
|
|
212
|
+
let finalUrl = newFileHandle.url;
|
|
213
|
+
if ((0, index_js_1.isInArtifactDir)(posixPath, config.artifactDirectories)) {
|
|
214
|
+
finalUrl = (0, index_js_1.pinUrl)(newFileHandle);
|
|
215
|
+
}
|
|
216
|
+
(0, index_js_1.setFileAt)(newTree, posixPath.split("/").filter(Boolean), finalUrl);
|
|
217
|
+
}
|
|
218
|
+
// Encode without previousRoot → fresh folder/directory doc URL.
|
|
219
|
+
const newRootUrl = await shape.encode({ repo, tree: newTree, title });
|
|
220
|
+
dlog("nuclear new rootUrl=%s", newRootUrl);
|
|
221
|
+
await (0, config_js_1.writeConfig)(root, { ...config, rootUrl: newRootUrl });
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
await repo.shutdown();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
182
227
|
async function save(cwd) {
|
|
183
228
|
await commitWorkdir(cwd, { online: false });
|
|
184
229
|
}
|
|
185
230
|
async function commitWorkdir(cwd, { online }) {
|
|
186
231
|
const root = path.resolve(cwd);
|
|
187
232
|
const config = await (0, config_js_1.readConfig)(root);
|
|
188
|
-
|
|
189
|
-
dlog("commit online=%s root=%s branch=%s", online, root, branchName);
|
|
233
|
+
dlog("commit online=%s root=%s", online, root);
|
|
190
234
|
const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), {
|
|
191
235
|
offline: !online,
|
|
192
236
|
});
|
|
193
237
|
try {
|
|
194
238
|
const shape = await (0, index_js_1.resolveShape)(config.shape);
|
|
195
|
-
const
|
|
196
|
-
// In branches mode + online, touch every branch's folder doc so the
|
|
197
|
-
// network adapter announces them. Without this, a branch created
|
|
198
|
-
// offline (`pushwork branch X`) is never pushed to the server, even
|
|
199
|
-
// though its entry is in the BranchesDoc.
|
|
200
|
-
const otherBranchHandles = [];
|
|
201
|
-
if (online && config.branches && (0, branches_js_1.isBranchesDoc)(rootHandle.doc())) {
|
|
202
|
-
const doc = rootHandle.doc();
|
|
203
|
-
for (const [name, url] of Object.entries(doc.branches)) {
|
|
204
|
-
if (name === branchName)
|
|
205
|
-
continue;
|
|
206
|
-
otherBranchHandles.push(await repo.find(url));
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
const folderHandle = await (0, branches_js_1.resolveEffectiveRoot)(repo, rootHandle, branchName);
|
|
239
|
+
const folderHandle = await repo.find(config.rootUrl);
|
|
210
240
|
const previousTree = await shape.decode({ repo, root: folderHandle });
|
|
211
241
|
const previousFiles = await readFileBytes(repo, previousTree);
|
|
212
242
|
const ig = await (0, ignore_js_1.loadIgnore)(root);
|
|
@@ -222,21 +252,11 @@ async function commitWorkdir(cwd, { online }) {
|
|
|
222
252
|
// working tree changed — a sync is also a checkpoint that "we
|
|
223
253
|
// reconciled with the server at this time."
|
|
224
254
|
stampLastSyncAt(folderHandle);
|
|
225
|
-
// Wait for the current branch's folder, the BranchesDoc itself
|
|
226
|
-
// (when in branches mode), and any other branch folder docs to
|
|
227
|
-
// flush. The maxMs is generous so a brand-new offline-created
|
|
228
|
-
// branch reliably propagates.
|
|
229
255
|
await (0, repo_js_1.waitForSync)(folderHandle, {
|
|
230
256
|
minMs: 3000,
|
|
231
257
|
idleMs: 1500,
|
|
232
258
|
maxMs: 15000,
|
|
233
259
|
});
|
|
234
|
-
if (config.branches) {
|
|
235
|
-
await (0, repo_js_1.waitForSync)(rootHandle, { idleMs: 1500, maxMs: 10000 });
|
|
236
|
-
}
|
|
237
|
-
for (const h of otherBranchHandles) {
|
|
238
|
-
await (0, repo_js_1.waitForSync)(h, { idleMs: 1500, maxMs: 10000 });
|
|
239
|
-
}
|
|
240
260
|
}
|
|
241
261
|
const finalTree = await shape.decode({ repo, root: folderHandle });
|
|
242
262
|
await materializeTree(repo, root, finalTree);
|
|
@@ -249,18 +269,16 @@ async function commitWorkdir(cwd, { online }) {
|
|
|
249
269
|
async function status(cwd) {
|
|
250
270
|
const root = path.resolve(cwd);
|
|
251
271
|
const config = await (0, config_js_1.readConfig)(root);
|
|
252
|
-
const branchName = config.branches ? await (0, branches_js_1.readBranchFile)(root) : null;
|
|
253
272
|
const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
|
|
254
273
|
try {
|
|
255
274
|
const shape = await (0, index_js_1.resolveShape)(config.shape);
|
|
256
|
-
const
|
|
257
|
-
const folderHandle = await (0, branches_js_1.resolveEffectiveRoot)(repo, rootHandle, branchName);
|
|
275
|
+
const folderHandle = await repo.find(config.rootUrl);
|
|
258
276
|
const previousTree = await shape.decode({ repo, root: folderHandle });
|
|
259
277
|
const previousFiles = await readFileBytes(repo, previousTree);
|
|
260
278
|
const ig = await (0, ignore_js_1.loadIgnore)(root);
|
|
261
279
|
const fsFiles = await (0, fs_tree_js_1.walkDir)(root, ig);
|
|
262
280
|
const diff = computeDiff(previousFiles, fsFiles);
|
|
263
|
-
return {
|
|
281
|
+
return { diff };
|
|
264
282
|
}
|
|
265
283
|
finally {
|
|
266
284
|
await repo.shutdown();
|
|
@@ -269,12 +287,10 @@ async function status(cwd) {
|
|
|
269
287
|
async function diff(cwd, limitToPath) {
|
|
270
288
|
const root = path.resolve(cwd);
|
|
271
289
|
const config = await (0, config_js_1.readConfig)(root);
|
|
272
|
-
const branchName = config.branches ? await (0, branches_js_1.readBranchFile)(root) : null;
|
|
273
290
|
const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
|
|
274
291
|
try {
|
|
275
292
|
const shape = await (0, index_js_1.resolveShape)(config.shape);
|
|
276
|
-
const
|
|
277
|
-
const folderHandle = await (0, branches_js_1.resolveEffectiveRoot)(repo, rootHandle, branchName);
|
|
293
|
+
const folderHandle = await repo.find(config.rootUrl);
|
|
278
294
|
const previousTree = await shape.decode({ repo, root: folderHandle });
|
|
279
295
|
const previousFiles = await readFileBytes(repo, previousTree);
|
|
280
296
|
const ig = await (0, ignore_js_1.loadIgnore)(root);
|
|
@@ -303,304 +319,19 @@ async function diff(cwd, limitToPath) {
|
|
|
303
319
|
await repo.shutdown();
|
|
304
320
|
}
|
|
305
321
|
}
|
|
306
|
-
async function listBranches(cwd) {
|
|
307
|
-
const root = path.resolve(cwd);
|
|
308
|
-
const config = await (0, config_js_1.readConfig)(root);
|
|
309
|
-
if (!config.branches) {
|
|
310
|
-
throw new Error("pushwork repo has no branches");
|
|
311
|
-
}
|
|
312
|
-
const current = await (0, branches_js_1.readBranchFile)(root);
|
|
313
|
-
const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
|
|
314
|
-
try {
|
|
315
|
-
const rootHandle = await repo.find(config.rootUrl);
|
|
316
|
-
const doc = rootHandle.doc();
|
|
317
|
-
if (!(0, branches_js_1.isBranchesDoc)(doc)) {
|
|
318
|
-
throw new Error(`root doc at ${config.rootUrl} is not a branches doc`);
|
|
319
|
-
}
|
|
320
|
-
return { current, names: (0, branches_js_1.listBranchNames)(doc) };
|
|
321
|
-
}
|
|
322
|
-
finally {
|
|
323
|
-
await repo.shutdown();
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
async function currentBranch(cwd) {
|
|
327
|
-
const root = path.resolve(cwd);
|
|
328
|
-
const config = await (0, config_js_1.readConfig)(root);
|
|
329
|
-
if (!config.branches)
|
|
330
|
-
return null;
|
|
331
|
-
return (0, branches_js_1.readBranchFile)(root);
|
|
332
|
-
}
|
|
333
|
-
async function createBranch(cwd, name) {
|
|
334
|
-
if (!name)
|
|
335
|
-
throw new Error("branch name is required");
|
|
336
|
-
if (name.includes("/") || name.includes("\\")) {
|
|
337
|
-
throw new Error("branch name may not contain slashes");
|
|
338
|
-
}
|
|
339
|
-
const root = path.resolve(cwd);
|
|
340
|
-
const config = await (0, config_js_1.readConfig)(root);
|
|
341
|
-
if (!config.branches)
|
|
342
|
-
throw new Error("pushwork repo has no branches");
|
|
343
|
-
const currentName = await (0, branches_js_1.readBranchFile)(root);
|
|
344
|
-
if (!currentName)
|
|
345
|
-
throw new Error("no current branch is set");
|
|
346
|
-
const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
|
|
347
|
-
try {
|
|
348
|
-
const shape = await (0, index_js_1.resolveShape)(config.shape);
|
|
349
|
-
const rootHandle = await repo.find(config.rootUrl);
|
|
350
|
-
const doc = rootHandle.doc();
|
|
351
|
-
if (!(0, branches_js_1.isBranchesDoc)(doc)) {
|
|
352
|
-
throw new Error(`root doc at ${config.rootUrl} is not a branches doc`);
|
|
353
|
-
}
|
|
354
|
-
if (doc.branches[name]) {
|
|
355
|
-
throw new Error(`branch "${name}" already exists`);
|
|
356
|
-
}
|
|
357
|
-
const sourceUrl = doc.branches[currentName];
|
|
358
|
-
if (!sourceUrl) {
|
|
359
|
-
throw new Error(`current branch "${currentName}" not found in branches doc`);
|
|
360
|
-
}
|
|
361
|
-
const sourceHandle = await repo.find(sourceUrl);
|
|
362
|
-
// Clone the folder doc.
|
|
363
|
-
const clonedFolder = repo.clone(sourceHandle);
|
|
364
|
-
dlog("createBranch %s cloned folder %s → %s", name, sourceUrl, clonedFolder.url);
|
|
365
|
-
// Deep-clone every file doc the source folder references, then rewrite
|
|
366
|
-
// the cloned folder's leaves to point at the new file URLs. Without
|
|
367
|
-
// this step both branches would alias the same UnixFileEntry docs and
|
|
368
|
-
// editing one branch would silently mutate the other.
|
|
369
|
-
const sourceTree = await shape.decode({ repo, root: sourceHandle });
|
|
370
|
-
const fileUrlRemap = new Map();
|
|
371
|
-
for (const [, fileUrl] of (0, index_js_1.flattenLeaves)(sourceTree)) {
|
|
372
|
-
const bare = (0, index_js_1.stripHeads)(fileUrl);
|
|
373
|
-
if (fileUrlRemap.has(bare))
|
|
374
|
-
continue;
|
|
375
|
-
const orig = await repo.find(bare);
|
|
376
|
-
const cloned = repo.clone(orig);
|
|
377
|
-
fileUrlRemap.set(bare, cloned.url);
|
|
378
|
-
dlog("createBranch cloned file %s → %s", bare, cloned.url);
|
|
379
|
-
}
|
|
380
|
-
const newTree = (0, index_js_1.newDir)();
|
|
381
|
-
for (const [posixPath, fileUrl] of (0, index_js_1.flattenLeaves)(sourceTree)) {
|
|
382
|
-
const bare = (0, index_js_1.stripHeads)(fileUrl);
|
|
383
|
-
const remappedBare = fileUrlRemap.get(bare);
|
|
384
|
-
if (!remappedBare)
|
|
385
|
-
continue;
|
|
386
|
-
const parsed = (0, automerge_repo_1.parseAutomergeUrl)(fileUrl);
|
|
387
|
-
// Preserve heads-pinning if the source URL was pinned.
|
|
388
|
-
let finalUrl = remappedBare;
|
|
389
|
-
if (parsed.heads) {
|
|
390
|
-
const newHandle = await repo.find(remappedBare);
|
|
391
|
-
finalUrl = (0, index_js_1.pinUrl)(newHandle);
|
|
392
|
-
}
|
|
393
|
-
const segments = posixPath.split("/").filter(Boolean);
|
|
394
|
-
(0, index_js_1.setFileAt)(newTree, segments, finalUrl);
|
|
395
|
-
}
|
|
396
|
-
await shape.encode({ repo, tree: newTree, previousRoot: clonedFolder });
|
|
397
|
-
rootHandle.change((d) => {
|
|
398
|
-
d.branches[name] = clonedFolder.url;
|
|
399
|
-
});
|
|
400
|
-
return clonedFolder.url;
|
|
401
|
-
}
|
|
402
|
-
finally {
|
|
403
|
-
await repo.shutdown();
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
/**
|
|
407
|
-
* Apply changes from `source` branch onto the current branch.
|
|
408
|
-
*
|
|
409
|
-
* For each path:
|
|
410
|
-
* - In both branches: their UnixFileEntry docs share Automerge history (deep
|
|
411
|
-
* cloned at branch creation), so we Automerge-merge source's content into
|
|
412
|
-
* target's. Concurrent edits are CRDT-merged inside each file doc.
|
|
413
|
-
* - Only in source: deep-clone the source's file doc into a new doc and add
|
|
414
|
-
* it to target's folder. Editing on either branch afterward stays isolated.
|
|
415
|
-
* - Only in target: untouched. We don't propagate deletions from source — the
|
|
416
|
-
* user can do that explicitly.
|
|
417
|
-
*
|
|
418
|
-
* Refuses if the working tree has uncommitted changes against the current
|
|
419
|
-
* branch (run `pushwork save` first). Offline only — propagation happens on
|
|
420
|
-
* the next `pushwork sync`.
|
|
421
|
-
*/
|
|
422
|
-
/**
|
|
423
|
-
* Compute what `merge <source>` would do without mutating any docs or the
|
|
424
|
-
* working tree. For paths in both branches we apply the merge to a *clone*
|
|
425
|
-
* of the target's file doc to learn the merged bytes; for paths only in
|
|
426
|
-
* source we just read source's bytes.
|
|
427
|
-
*/
|
|
428
|
-
async function previewMerge(cwd, source) {
|
|
429
|
-
if (!source)
|
|
430
|
-
throw new Error("source branch name is required");
|
|
431
|
-
const root = path.resolve(cwd);
|
|
432
|
-
const config = await (0, config_js_1.readConfig)(root);
|
|
433
|
-
if (!config.branches)
|
|
434
|
-
throw new Error("pushwork repo has no branches");
|
|
435
|
-
const targetName = await (0, branches_js_1.readBranchFile)(root);
|
|
436
|
-
if (!targetName)
|
|
437
|
-
throw new Error("no current branch is set");
|
|
438
|
-
if (source === targetName) {
|
|
439
|
-
throw new Error(`cannot merge "${source}" into itself`);
|
|
440
|
-
}
|
|
441
|
-
const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
|
|
442
|
-
try {
|
|
443
|
-
const shape = await (0, index_js_1.resolveShape)(config.shape);
|
|
444
|
-
const rootHandle = await repo.find(config.rootUrl);
|
|
445
|
-
const branchesDoc = rootHandle.doc();
|
|
446
|
-
if (!(0, branches_js_1.isBranchesDoc)(branchesDoc)) {
|
|
447
|
-
throw new Error(`root doc at ${config.rootUrl} is not a branches doc`);
|
|
448
|
-
}
|
|
449
|
-
if (!branchesDoc.branches[source]) {
|
|
450
|
-
throw new Error(`source branch "${source}" does not exist`);
|
|
451
|
-
}
|
|
452
|
-
const targetFolder = await repo.find(branchesDoc.branches[targetName]);
|
|
453
|
-
const sourceFolder = await repo.find(branchesDoc.branches[source]);
|
|
454
|
-
const tTree = await shape.decode({ repo, root: targetFolder });
|
|
455
|
-
const sTree = await shape.decode({ repo, root: sourceFolder });
|
|
456
|
-
const tLeaves = (0, index_js_1.flattenLeaves)(tTree);
|
|
457
|
-
const sLeaves = (0, index_js_1.flattenLeaves)(sTree);
|
|
458
|
-
const entries = [];
|
|
459
|
-
for (const [posixPath, sUrl] of sLeaves) {
|
|
460
|
-
const tUrl = tLeaves.get(posixPath);
|
|
461
|
-
const sBare = (0, index_js_1.stripHeads)(sUrl);
|
|
462
|
-
const sHandle = await repo.find(sBare);
|
|
463
|
-
if (!tUrl) {
|
|
464
|
-
entries.push({
|
|
465
|
-
path: posixPath,
|
|
466
|
-
kind: "added",
|
|
467
|
-
after: (0, index_js_1.contentToBytes)(sHandle.doc().content),
|
|
468
|
-
});
|
|
469
|
-
continue;
|
|
470
|
-
}
|
|
471
|
-
const tBare = (0, index_js_1.stripHeads)(tUrl);
|
|
472
|
-
if (tBare === sBare)
|
|
473
|
-
continue;
|
|
474
|
-
const tHandle = await repo.find(tBare);
|
|
475
|
-
const before = (0, index_js_1.contentToBytes)(tHandle.doc().content);
|
|
476
|
-
// Compute merge result without touching the target doc.
|
|
477
|
-
const merged = Automerge.merge(Automerge.clone(tHandle.doc()), Automerge.clone(sHandle.doc()));
|
|
478
|
-
const after = (0, index_js_1.contentToBytes)(merged.content);
|
|
479
|
-
if ((0, fs_tree_js_1.byteEq)(before, after))
|
|
480
|
-
continue;
|
|
481
|
-
entries.push({ path: posixPath, kind: "merged", before, after });
|
|
482
|
-
}
|
|
483
|
-
entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
484
|
-
return { source, target: targetName, entries };
|
|
485
|
-
}
|
|
486
|
-
finally {
|
|
487
|
-
await repo.shutdown();
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
async function mergeBranch(cwd, source) {
|
|
491
|
-
if (!source)
|
|
492
|
-
throw new Error("source branch name is required");
|
|
493
|
-
const root = path.resolve(cwd);
|
|
494
|
-
const config = await (0, config_js_1.readConfig)(root);
|
|
495
|
-
if (!config.branches)
|
|
496
|
-
throw new Error("pushwork repo has no branches");
|
|
497
|
-
const targetName = await (0, branches_js_1.readBranchFile)(root);
|
|
498
|
-
if (!targetName)
|
|
499
|
-
throw new Error("no current branch is set");
|
|
500
|
-
if (source === targetName) {
|
|
501
|
-
throw new Error(`cannot merge "${source}" into itself`);
|
|
502
|
-
}
|
|
503
|
-
dlog("merge source=%s target=%s", source, targetName);
|
|
504
|
-
const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
|
|
505
|
-
try {
|
|
506
|
-
const shape = await (0, index_js_1.resolveShape)(config.shape);
|
|
507
|
-
const rootHandle = await repo.find(config.rootUrl);
|
|
508
|
-
const branchesDoc = rootHandle.doc();
|
|
509
|
-
if (!(0, branches_js_1.isBranchesDoc)(branchesDoc)) {
|
|
510
|
-
throw new Error(`root doc at ${config.rootUrl} is not a branches doc`);
|
|
511
|
-
}
|
|
512
|
-
if (!branchesDoc.branches[source]) {
|
|
513
|
-
throw new Error(`source branch "${source}" does not exist`);
|
|
514
|
-
}
|
|
515
|
-
const targetUrl = branchesDoc.branches[targetName];
|
|
516
|
-
const sourceUrl = branchesDoc.branches[source];
|
|
517
|
-
const targetFolder = await repo.find(targetUrl);
|
|
518
|
-
const sourceFolder = await repo.find(sourceUrl);
|
|
519
|
-
// Refuse on dirty working tree (mirror switchBranch policy).
|
|
520
|
-
const tFiles = await readFileBytes(repo, await shape.decode({ repo, root: targetFolder }));
|
|
521
|
-
const ig = await (0, ignore_js_1.loadIgnore)(root);
|
|
522
|
-
const fsFiles = await (0, fs_tree_js_1.walkDir)(root, ig);
|
|
523
|
-
const dirty = computeDiff(tFiles, fsFiles);
|
|
524
|
-
if (dirty.added.length || dirty.modified.length || dirty.deleted.length) {
|
|
525
|
-
throw new Error(`refusing to merge: working tree has uncommitted changes on branch "${targetName}". run \`pushwork save\` first.`);
|
|
526
|
-
}
|
|
527
|
-
const tTree = await shape.decode({ repo, root: targetFolder });
|
|
528
|
-
const sTree = await shape.decode({ repo, root: sourceFolder });
|
|
529
|
-
const tLeaves = (0, index_js_1.flattenLeaves)(tTree);
|
|
530
|
-
const sLeaves = (0, index_js_1.flattenLeaves)(sTree);
|
|
531
|
-
const merged = [];
|
|
532
|
-
const added = [];
|
|
533
|
-
// For paths in both: merge file docs in place.
|
|
534
|
-
for (const [posixPath, sUrl] of sLeaves) {
|
|
535
|
-
const tUrl = tLeaves.get(posixPath);
|
|
536
|
-
if (!tUrl)
|
|
537
|
-
continue;
|
|
538
|
-
const tBare = (0, index_js_1.stripHeads)(tUrl);
|
|
539
|
-
const sBare = (0, index_js_1.stripHeads)(sUrl);
|
|
540
|
-
if (tBare === sBare) {
|
|
541
|
-
// Same file doc identity (shared) — already in sync, nothing to do.
|
|
542
|
-
continue;
|
|
543
|
-
}
|
|
544
|
-
const tHandle = await repo.find(tBare);
|
|
545
|
-
const sHandle = await repo.find(sBare);
|
|
546
|
-
tHandle.update((d) => Automerge.merge(d, Automerge.clone(sHandle.doc())));
|
|
547
|
-
merged.push(posixPath);
|
|
548
|
-
dlog("merge merged file at %s (%s ← %s)", posixPath, tBare, sBare);
|
|
549
|
-
}
|
|
550
|
-
// For paths only in source: deep-clone source's file doc, add to target's folder.
|
|
551
|
-
const newLeaves = new Map();
|
|
552
|
-
for (const [posixPath, sUrl] of sLeaves) {
|
|
553
|
-
if (tLeaves.has(posixPath))
|
|
554
|
-
continue;
|
|
555
|
-
const sBare = (0, index_js_1.stripHeads)(sUrl);
|
|
556
|
-
const sHandle = await repo.find(sBare);
|
|
557
|
-
const cloned = repo.clone(sHandle);
|
|
558
|
-
let finalUrl = cloned.url;
|
|
559
|
-
const parsed = (0, automerge_repo_1.parseAutomergeUrl)(sUrl);
|
|
560
|
-
if (parsed.heads) {
|
|
561
|
-
finalUrl = (0, index_js_1.pinUrl)(cloned);
|
|
562
|
-
}
|
|
563
|
-
newLeaves.set(posixPath, finalUrl);
|
|
564
|
-
added.push(posixPath);
|
|
565
|
-
dlog("merge added %s url=%s", posixPath, finalUrl);
|
|
566
|
-
}
|
|
567
|
-
if (newLeaves.size > 0) {
|
|
568
|
-
// Build a tree for the encode call: existing target leaves + new ones.
|
|
569
|
-
const nextTree = (0, index_js_1.newDir)();
|
|
570
|
-
for (const [p, url] of tLeaves) {
|
|
571
|
-
(0, index_js_1.setFileAt)(nextTree, p.split("/").filter(Boolean), url);
|
|
572
|
-
}
|
|
573
|
-
for (const [p, url] of newLeaves) {
|
|
574
|
-
(0, index_js_1.setFileAt)(nextTree, p.split("/").filter(Boolean), url);
|
|
575
|
-
}
|
|
576
|
-
await shape.encode({ repo, tree: nextTree, previousRoot: targetFolder });
|
|
577
|
-
}
|
|
578
|
-
// Materialize current branch (target) onto disk to reflect the merge.
|
|
579
|
-
const finalTree = await shape.decode({ repo, root: targetFolder });
|
|
580
|
-
await materializeTree(repo, root, finalTree);
|
|
581
|
-
merged.sort();
|
|
582
|
-
added.sort();
|
|
583
|
-
return { source, target: targetName, merged, added };
|
|
584
|
-
}
|
|
585
|
-
finally {
|
|
586
|
-
await repo.shutdown();
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
322
|
/**
|
|
590
|
-
* Capture the working tree's changes against the
|
|
591
|
-
*
|
|
592
|
-
*
|
|
323
|
+
* Capture the working tree's changes against the saved state into a local
|
|
324
|
+
* snarf, then reset the working tree to the saved state. Snarfs live in
|
|
325
|
+
* `.pushwork/snarf/` and are never synced.
|
|
593
326
|
*/
|
|
594
327
|
async function cutWorkdir(cwd, opts = {}) {
|
|
595
328
|
const root = path.resolve(cwd);
|
|
596
329
|
const config = await (0, config_js_1.readConfig)(root);
|
|
597
|
-
|
|
598
|
-
dlog("cut root=%s branch=%s name=%s", root, branchName, opts.name ?? "(unnamed)");
|
|
330
|
+
dlog("cut root=%s name=%s", root, opts.name ?? "(unnamed)");
|
|
599
331
|
const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
|
|
600
332
|
try {
|
|
601
333
|
const shape = await (0, index_js_1.resolveShape)(config.shape);
|
|
602
|
-
const
|
|
603
|
-
const folderHandle = await (0, branches_js_1.resolveEffectiveRoot)(repo, rootHandle, branchName);
|
|
334
|
+
const folderHandle = await repo.find(config.rootUrl);
|
|
604
335
|
const previousTree = await shape.decode({ repo, root: folderHandle });
|
|
605
336
|
const previousFiles = await readFileBytes(repo, previousTree);
|
|
606
337
|
const ig = await (0, ignore_js_1.loadIgnore)(root);
|
|
@@ -609,13 +340,13 @@ async function cutWorkdir(cwd, opts = {}) {
|
|
|
609
340
|
for (const [p, bytes] of fsFiles) {
|
|
610
341
|
const prev = previousFiles.get(p);
|
|
611
342
|
if (!prev) {
|
|
612
|
-
entries.push({ path: p, kind: "added", contentBase64: (0,
|
|
343
|
+
entries.push({ path: p, kind: "added", contentBase64: (0, snarf_js_1.encodeBytes)(bytes) });
|
|
613
344
|
}
|
|
614
345
|
else if (!(0, fs_tree_js_1.byteEq)(prev.bytes, bytes)) {
|
|
615
346
|
entries.push({
|
|
616
347
|
path: p,
|
|
617
348
|
kind: "modified",
|
|
618
|
-
contentBase64: (0,
|
|
349
|
+
contentBase64: (0, snarf_js_1.encodeBytes)(bytes),
|
|
619
350
|
});
|
|
620
351
|
}
|
|
621
352
|
}
|
|
@@ -627,35 +358,32 @@ async function cutWorkdir(cwd, opts = {}) {
|
|
|
627
358
|
throw new Error("nothing to cut: working tree clean");
|
|
628
359
|
}
|
|
629
360
|
entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
630
|
-
const
|
|
361
|
+
const snarf = await (0, snarf_js_1.appendSnarf)(root, {
|
|
631
362
|
name: opts.name,
|
|
632
|
-
branch: branchName,
|
|
633
363
|
entries,
|
|
634
364
|
});
|
|
635
|
-
// Reset working tree to the
|
|
365
|
+
// Reset working tree to the saved state.
|
|
636
366
|
await materializeTree(repo, root, previousTree);
|
|
637
|
-
dlog("cut complete id=%d entries=%d",
|
|
638
|
-
return { id:
|
|
367
|
+
dlog("cut complete id=%d entries=%d", snarf.id, entries.length);
|
|
368
|
+
return { id: snarf.id, entries: entries.length };
|
|
639
369
|
}
|
|
640
370
|
finally {
|
|
641
371
|
await repo.shutdown();
|
|
642
372
|
}
|
|
643
373
|
}
|
|
644
374
|
/**
|
|
645
|
-
* Apply a
|
|
375
|
+
* Apply a snarf on top of the current working tree, then remove the snarf
|
|
646
376
|
* entry. Refuses if the working tree has uncommitted changes (caller can
|
|
647
377
|
* `pushwork save` or `pushwork cut` first).
|
|
648
378
|
*/
|
|
649
|
-
async function
|
|
379
|
+
async function pasteSnarf(cwd, selector) {
|
|
650
380
|
const root = path.resolve(cwd);
|
|
651
381
|
const config = await (0, config_js_1.readConfig)(root);
|
|
652
|
-
|
|
653
|
-
// Check the working tree is clean against the current branch state.
|
|
382
|
+
// Check the working tree is clean against the saved state.
|
|
654
383
|
const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
|
|
655
384
|
try {
|
|
656
385
|
const shape = await (0, index_js_1.resolveShape)(config.shape);
|
|
657
|
-
const
|
|
658
|
-
const folderHandle = await (0, branches_js_1.resolveEffectiveRoot)(repo, rootHandle, branchName);
|
|
386
|
+
const folderHandle = await repo.find(config.rootUrl);
|
|
659
387
|
const previousTree = await shape.decode({ repo, root: folderHandle });
|
|
660
388
|
const previousFiles = await readFileBytes(repo, previousTree);
|
|
661
389
|
const ig = await (0, ignore_js_1.loadIgnore)(root);
|
|
@@ -668,13 +396,13 @@ async function pasteStash(cwd, selector) {
|
|
|
668
396
|
finally {
|
|
669
397
|
await repo.shutdown();
|
|
670
398
|
}
|
|
671
|
-
const
|
|
672
|
-
if (!
|
|
399
|
+
const snarf = await (0, snarf_js_1.takeSnarf)(root, selector);
|
|
400
|
+
if (!snarf) {
|
|
673
401
|
throw new Error(selector
|
|
674
|
-
? `no
|
|
675
|
-
: "nothing to paste: no
|
|
402
|
+
? `no snarf matches "${selector}"`
|
|
403
|
+
: "nothing to paste: no snarfs");
|
|
676
404
|
}
|
|
677
|
-
for (const entry of
|
|
405
|
+
for (const entry of snarf.entries) {
|
|
678
406
|
const target = path.join(root, fromPosix(entry.path));
|
|
679
407
|
if (entry.kind === "deleted") {
|
|
680
408
|
try {
|
|
@@ -686,57 +414,15 @@ async function pasteStash(cwd, selector) {
|
|
|
686
414
|
await pruneEmptyDirs(root, path.dirname(fromPosix(entry.path)));
|
|
687
415
|
}
|
|
688
416
|
else if (entry.contentBase64 != null) {
|
|
689
|
-
const bytes = (0,
|
|
417
|
+
const bytes = (0, snarf_js_1.decodeBytes)(entry.contentBase64);
|
|
690
418
|
await (0, fs_tree_js_1.writeFileAtomic)(target, bytes);
|
|
691
419
|
}
|
|
692
420
|
}
|
|
693
|
-
dlog("paste complete id=%d entries=%d",
|
|
694
|
-
return { id:
|
|
695
|
-
}
|
|
696
|
-
async function showStashes(cwd) {
|
|
697
|
-
return (0, stash_js_1.listStashes)(path.resolve(cwd));
|
|
421
|
+
dlog("paste complete id=%d entries=%d", snarf.id, snarf.entries.length);
|
|
422
|
+
return { id: snarf.id, name: snarf.name, entries: snarf.entries.length };
|
|
698
423
|
}
|
|
699
|
-
async function
|
|
700
|
-
|
|
701
|
-
throw new Error("branch name is required");
|
|
702
|
-
const root = path.resolve(cwd);
|
|
703
|
-
const config = await (0, config_js_1.readConfig)(root);
|
|
704
|
-
if (!config.branches)
|
|
705
|
-
throw new Error("pushwork repo has no branches");
|
|
706
|
-
const currentName = await (0, branches_js_1.readBranchFile)(root);
|
|
707
|
-
const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
|
|
708
|
-
try {
|
|
709
|
-
const shape = await (0, index_js_1.resolveShape)(config.shape);
|
|
710
|
-
const rootHandle = await repo.find(config.rootUrl);
|
|
711
|
-
const doc = rootHandle.doc();
|
|
712
|
-
if (!(0, branches_js_1.isBranchesDoc)(doc)) {
|
|
713
|
-
throw new Error(`root doc at ${config.rootUrl} is not a branches doc`);
|
|
714
|
-
}
|
|
715
|
-
if (!doc.branches[name]) {
|
|
716
|
-
throw new Error(`branch "${name}" does not exist`);
|
|
717
|
-
}
|
|
718
|
-
// Refuse if the working dir has uncommitted changes against the current branch.
|
|
719
|
-
if (currentName) {
|
|
720
|
-
const folderHandle = await (0, branches_js_1.resolveEffectiveRoot)(repo, rootHandle, currentName);
|
|
721
|
-
const previousTree = await shape.decode({ repo, root: folderHandle });
|
|
722
|
-
const previousFiles = await readFileBytes(repo, previousTree);
|
|
723
|
-
const ig = await (0, ignore_js_1.loadIgnore)(root);
|
|
724
|
-
const fsFiles = await (0, fs_tree_js_1.walkDir)(root, ig);
|
|
725
|
-
const d = computeDiff(previousFiles, fsFiles);
|
|
726
|
-
if (d.added.length || d.modified.length || d.deleted.length) {
|
|
727
|
-
throw new Error(`refusing to switch: working tree has uncommitted changes on branch "${currentName}". run \`pushwork save\` first.`);
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
// Materialize from the new branch.
|
|
731
|
-
const newFolder = await repo.find(doc.branches[name]);
|
|
732
|
-
const tree = await shape.decode({ repo, root: newFolder });
|
|
733
|
-
await materializeTree(repo, root, tree);
|
|
734
|
-
await (0, branches_js_1.writeBranchFile)(root, name);
|
|
735
|
-
dlog("switch → %s", name);
|
|
736
|
-
}
|
|
737
|
-
finally {
|
|
738
|
-
await repo.shutdown();
|
|
739
|
-
}
|
|
424
|
+
async function showSnarfs(cwd) {
|
|
425
|
+
return (0, snarf_js_1.listSnarfs)(path.resolve(cwd));
|
|
740
426
|
}
|
|
741
427
|
function stampLastSyncAt(handle) {
|
|
742
428
|
handle.change((d) => {
|
|
@@ -794,17 +480,13 @@ async function pushFiles(repo, fsFiles, previous, artifactDirs) {
|
|
|
794
480
|
}
|
|
795
481
|
else if (prev) {
|
|
796
482
|
// Changed path: mutate the existing file doc in place. This keeps
|
|
797
|
-
// the file URL stable
|
|
483
|
+
// the file URL stable across edits and avoids the propagation
|
|
798
484
|
// race where a brand-new file doc URL is referenced by the folder
|
|
799
485
|
// before its bytes have reached the sync server.
|
|
800
486
|
//
|
|
801
487
|
// For string content (text files) we use Automerge.updateText so
|
|
802
488
|
// concurrent character-level edits merge correctly. Bytes and
|
|
803
489
|
// ImmutableString are atomic — last writer wins on the field.
|
|
804
|
-
//
|
|
805
|
-
// Branch isolation is enforced separately: `createBranch` deep
|
|
806
|
-
// clones every file doc the source branch references, so two
|
|
807
|
-
// branches never share a UnixFileEntry doc identity.
|
|
808
490
|
const refreshUrl = (0, index_js_1.stripHeads)(prev.url);
|
|
809
491
|
const handle = await repo.find(refreshUrl);
|
|
810
492
|
handle.change((d) => {
|