pushwork 2.0.0-preview.2 → 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/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 +54 -108
- 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 +3 -14
- package/dist/index.js.map +1 -1
- package/dist/pushwork.d.ts +18 -65
- package/dist/pushwork.d.ts.map +1 -1
- package/dist/pushwork.js +97 -559
- package/dist/pushwork.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/package.json +1 -1
package/dist/pushwork.js
CHANGED
|
@@ -33,7 +33,6 @@ 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;
|
|
@@ -42,35 +41,26 @@ exports.nuclearizeRepo = nuclearizeRepo;
|
|
|
42
41
|
exports.save = save;
|
|
43
42
|
exports.status = status;
|
|
44
43
|
exports.diff = diff;
|
|
45
|
-
exports.listBranches = listBranches;
|
|
46
|
-
exports.currentBranch = currentBranch;
|
|
47
|
-
exports.createBranch = createBranch;
|
|
48
|
-
exports.previewMerge = previewMerge;
|
|
49
|
-
exports.mergeBranch = mergeBranch;
|
|
50
44
|
exports.cutWorkdir = cutWorkdir;
|
|
51
|
-
exports.
|
|
52
|
-
exports.
|
|
53
|
-
exports.switchBranch = switchBranch;
|
|
45
|
+
exports.pasteSnarf = pasteSnarf;
|
|
46
|
+
exports.showSnarfs = showSnarfs;
|
|
54
47
|
const fs = __importStar(require("fs/promises"));
|
|
55
48
|
const path = __importStar(require("path"));
|
|
56
49
|
const Automerge = __importStar(require("@automerge/automerge"));
|
|
57
50
|
const automerge_repo_1 = require("@automerge/automerge-repo");
|
|
58
51
|
const config_js_1 = require("./config.js");
|
|
59
|
-
const branches_js_1 = require("./branches.js");
|
|
60
|
-
Object.defineProperty(exports, "deleteBranchFile", { enumerable: true, get: function () { return branches_js_1.deleteBranchFile; } });
|
|
61
52
|
const ignore_js_1 = require("./ignore.js");
|
|
62
53
|
const fs_tree_js_1 = require("./fs-tree.js");
|
|
63
54
|
const log_js_1 = require("./log.js");
|
|
64
55
|
const repo_js_1 = require("./repo.js");
|
|
65
|
-
const
|
|
56
|
+
const snarf_js_1 = require("./snarf.js");
|
|
66
57
|
const index_js_1 = require("./shapes/index.js");
|
|
67
58
|
const dlog = (0, log_js_1.log)("pushwork");
|
|
68
59
|
const DEFAULT_ARTIFACT_DIRECTORIES = ["dist"];
|
|
69
60
|
async function init(opts) {
|
|
70
61
|
const root = path.resolve(opts.dir);
|
|
71
|
-
const useBranches = opts.branches ?? true;
|
|
72
62
|
const online = opts.online ?? true;
|
|
73
|
-
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);
|
|
74
64
|
if (await (0, config_js_1.configExists)(root)) {
|
|
75
65
|
throw new Error(`pushwork already initialized at ${root}`);
|
|
76
66
|
}
|
|
@@ -93,29 +83,15 @@ async function init(opts) {
|
|
|
93
83
|
stampLastSyncAt(folderHandle);
|
|
94
84
|
await (0, repo_js_1.waitForSync)(folderHandle, { idleMs: 1500, maxMs: 10000 });
|
|
95
85
|
}
|
|
96
|
-
let rootUrl = folderUrl;
|
|
97
|
-
if (useBranches) {
|
|
98
|
-
const branchesHandle = repo.create({
|
|
99
|
-
"@patchwork": { type: "branches", ...(title ? { title } : {}) },
|
|
100
|
-
branches: { [branches_js_1.DEFAULT_BRANCH]: folderUrl },
|
|
101
|
-
});
|
|
102
|
-
if (online) {
|
|
103
|
-
await (0, repo_js_1.waitForSync)(branchesHandle, { minMs: 1500, idleMs: 1500, maxMs: 10000 });
|
|
104
|
-
}
|
|
105
|
-
rootUrl = branchesHandle.url;
|
|
106
|
-
dlog("init wrapped in BranchesDoc=%s", rootUrl);
|
|
107
|
-
await (0, branches_js_1.writeBranchFile)(root, branches_js_1.DEFAULT_BRANCH);
|
|
108
|
-
}
|
|
109
86
|
await (0, config_js_1.writeConfig)(root, {
|
|
110
87
|
version: config_js_1.CONFIG_VERSION,
|
|
111
|
-
rootUrl,
|
|
88
|
+
rootUrl: folderUrl,
|
|
112
89
|
backend: opts.backend,
|
|
113
90
|
shape: opts.shape,
|
|
114
91
|
artifactDirectories: artifactDirs,
|
|
115
|
-
branches: useBranches,
|
|
116
92
|
});
|
|
117
|
-
dlog("init complete: rootUrl=%s",
|
|
118
|
-
return
|
|
93
|
+
dlog("init complete: rootUrl=%s", folderUrl);
|
|
94
|
+
return folderUrl;
|
|
119
95
|
}
|
|
120
96
|
finally {
|
|
121
97
|
await repo.shutdown();
|
|
@@ -137,36 +113,36 @@ async function clone(opts) {
|
|
|
137
113
|
const repo = await (0, repo_js_1.openRepo)(opts.backend, (0, config_js_1.storageDir)(root), { offline: !online });
|
|
138
114
|
try {
|
|
139
115
|
const shape = await (0, index_js_1.resolveShape)(opts.shape);
|
|
140
|
-
|
|
116
|
+
let folderHandle = await repo.find(opts.url);
|
|
141
117
|
if (online) {
|
|
142
|
-
await (0, repo_js_1.waitForSync)(
|
|
118
|
+
await (0, repo_js_1.waitForSync)(folderHandle, { idleMs: 1500, maxMs: 15000 });
|
|
143
119
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
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);
|
|
152
133
|
if (online) {
|
|
153
134
|
await (0, repo_js_1.waitForSync)(folderHandle, { idleMs: 1500, maxMs: 15000 });
|
|
154
135
|
}
|
|
155
|
-
|
|
156
|
-
dlog("clone branch=%s folder=%s", branchName, folderHandle.url);
|
|
157
|
-
}
|
|
158
|
-
else if (opts.branch) {
|
|
159
|
-
throw new Error(`--branch passed but root doc is not a branches doc (type=${docType})`);
|
|
136
|
+
storedUrl = chosenUrl;
|
|
160
137
|
}
|
|
161
138
|
const tree = await shape.decode({ repo, root: folderHandle });
|
|
162
139
|
await materializeTree(repo, root, tree);
|
|
163
140
|
await (0, config_js_1.writeConfig)(root, {
|
|
164
141
|
version: config_js_1.CONFIG_VERSION,
|
|
165
|
-
rootUrl:
|
|
142
|
+
rootUrl: storedUrl,
|
|
166
143
|
backend: opts.backend,
|
|
167
144
|
shape: opts.shape,
|
|
168
145
|
artifactDirectories: artifactDirs,
|
|
169
|
-
branches: useBranches,
|
|
170
146
|
});
|
|
171
147
|
dlog("clone complete");
|
|
172
148
|
}
|
|
@@ -174,6 +150,22 @@ async function clone(opts) {
|
|
|
174
150
|
await repo.shutdown();
|
|
175
151
|
}
|
|
176
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
|
+
}
|
|
177
169
|
async function url(cwd) {
|
|
178
170
|
const config = await (0, config_js_1.readConfig)(path.resolve(cwd));
|
|
179
171
|
return config.rootUrl;
|
|
@@ -185,11 +177,10 @@ async function sync(cwd, opts = {}) {
|
|
|
185
177
|
await commitWorkdir(cwd, { online: true });
|
|
186
178
|
}
|
|
187
179
|
/**
|
|
188
|
-
* Re-create every Automerge doc this repo references — every UnixFileEntry
|
|
189
|
-
*
|
|
190
|
-
* with
|
|
191
|
-
*
|
|
192
|
-
* the new docs to the server.
|
|
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.
|
|
193
184
|
*
|
|
194
185
|
* Use sparingly: this orphans the previous URLs from this repo's perspective.
|
|
195
186
|
* Anyone who had cloned the old URL keeps working from those docs; this
|
|
@@ -198,58 +189,34 @@ async function sync(cwd, opts = {}) {
|
|
|
198
189
|
async function nuclearizeRepo(cwd) {
|
|
199
190
|
const root = path.resolve(cwd);
|
|
200
191
|
const config = await (0, config_js_1.readConfig)(root);
|
|
201
|
-
|
|
202
|
-
dlog("nuclear root=%s branches=%s current=%s", root, config.branches, branchName);
|
|
192
|
+
dlog("nuclear root=%s", root);
|
|
203
193
|
const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
|
|
204
194
|
try {
|
|
205
195
|
const shape = await (0, index_js_1.resolveShape)(config.shape);
|
|
206
|
-
const
|
|
196
|
+
const oldFolder = await repo.find(config.rootUrl);
|
|
207
197
|
const title = path.basename(root) || undefined;
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
mimeType: oldDoc.mimeType,
|
|
222
|
-
content: oldDoc.content,
|
|
223
|
-
});
|
|
224
|
-
let finalUrl = newFileHandle.url;
|
|
225
|
-
if ((0, automerge_repo_1.parseAutomergeUrl)(fileUrl).heads) {
|
|
226
|
-
finalUrl = (0, index_js_1.pinUrl)(newFileHandle);
|
|
227
|
-
}
|
|
228
|
-
(0, index_js_1.setFileAt)(newTree, posixPath.split("/").filter(Boolean), finalUrl);
|
|
229
|
-
}
|
|
230
|
-
// Encode without previousRoot → fresh folder/directory doc URL.
|
|
231
|
-
return shape.encode({ repo, tree: newTree, title: folderTitle });
|
|
232
|
-
};
|
|
233
|
-
let newRootUrl;
|
|
234
|
-
if (config.branches && (0, branches_js_1.isBranchesDoc)(oldRootHandle.doc())) {
|
|
235
|
-
const oldDoc = oldRootHandle.doc();
|
|
236
|
-
const newBranches = {};
|
|
237
|
-
for (const [name, oldFolderUrl] of Object.entries(oldDoc.branches)) {
|
|
238
|
-
newBranches[name] = await rebuildFolder(oldFolderUrl, title);
|
|
239
|
-
dlog("nuclear rebuilt branch %s → %s", name, newBranches[name]);
|
|
240
|
-
}
|
|
241
|
-
const newRoot = repo.create({
|
|
242
|
-
"@patchwork": {
|
|
243
|
-
type: "branches",
|
|
244
|
-
...(title ? { title } : {}),
|
|
245
|
-
},
|
|
246
|
-
branches: newBranches,
|
|
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,
|
|
247
211
|
});
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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);
|
|
252
217
|
}
|
|
218
|
+
// Encode without previousRoot → fresh folder/directory doc URL.
|
|
219
|
+
const newRootUrl = await shape.encode({ repo, tree: newTree, title });
|
|
253
220
|
dlog("nuclear new rootUrl=%s", newRootUrl);
|
|
254
221
|
await (0, config_js_1.writeConfig)(root, { ...config, rootUrl: newRootUrl });
|
|
255
222
|
}
|
|
@@ -263,28 +230,13 @@ async function save(cwd) {
|
|
|
263
230
|
async function commitWorkdir(cwd, { online }) {
|
|
264
231
|
const root = path.resolve(cwd);
|
|
265
232
|
const config = await (0, config_js_1.readConfig)(root);
|
|
266
|
-
|
|
267
|
-
dlog("commit online=%s root=%s branch=%s", online, root, branchName);
|
|
233
|
+
dlog("commit online=%s root=%s", online, root);
|
|
268
234
|
const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), {
|
|
269
235
|
offline: !online,
|
|
270
236
|
});
|
|
271
237
|
try {
|
|
272
238
|
const shape = await (0, index_js_1.resolveShape)(config.shape);
|
|
273
|
-
const
|
|
274
|
-
// In branches mode + online, touch every branch's folder doc so the
|
|
275
|
-
// network adapter announces them. Without this, a branch created
|
|
276
|
-
// offline (`pushwork branch X`) is never pushed to the server, even
|
|
277
|
-
// though its entry is in the BranchesDoc.
|
|
278
|
-
const otherBranchHandles = [];
|
|
279
|
-
if (online && config.branches && (0, branches_js_1.isBranchesDoc)(rootHandle.doc())) {
|
|
280
|
-
const doc = rootHandle.doc();
|
|
281
|
-
for (const [name, url] of Object.entries(doc.branches)) {
|
|
282
|
-
if (name === branchName)
|
|
283
|
-
continue;
|
|
284
|
-
otherBranchHandles.push(await repo.find(url));
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
const folderHandle = await (0, branches_js_1.resolveEffectiveRoot)(repo, rootHandle, branchName);
|
|
239
|
+
const folderHandle = await repo.find(config.rootUrl);
|
|
288
240
|
const previousTree = await shape.decode({ repo, root: folderHandle });
|
|
289
241
|
const previousFiles = await readFileBytes(repo, previousTree);
|
|
290
242
|
const ig = await (0, ignore_js_1.loadIgnore)(root);
|
|
@@ -300,21 +252,11 @@ async function commitWorkdir(cwd, { online }) {
|
|
|
300
252
|
// working tree changed — a sync is also a checkpoint that "we
|
|
301
253
|
// reconciled with the server at this time."
|
|
302
254
|
stampLastSyncAt(folderHandle);
|
|
303
|
-
// Wait for the current branch's folder, the BranchesDoc itself
|
|
304
|
-
// (when in branches mode), and any other branch folder docs to
|
|
305
|
-
// flush. The maxMs is generous so a brand-new offline-created
|
|
306
|
-
// branch reliably propagates.
|
|
307
255
|
await (0, repo_js_1.waitForSync)(folderHandle, {
|
|
308
256
|
minMs: 3000,
|
|
309
257
|
idleMs: 1500,
|
|
310
258
|
maxMs: 15000,
|
|
311
259
|
});
|
|
312
|
-
if (config.branches) {
|
|
313
|
-
await (0, repo_js_1.waitForSync)(rootHandle, { idleMs: 1500, maxMs: 10000 });
|
|
314
|
-
}
|
|
315
|
-
for (const h of otherBranchHandles) {
|
|
316
|
-
await (0, repo_js_1.waitForSync)(h, { idleMs: 1500, maxMs: 10000 });
|
|
317
|
-
}
|
|
318
260
|
}
|
|
319
261
|
const finalTree = await shape.decode({ repo, root: folderHandle });
|
|
320
262
|
await materializeTree(repo, root, finalTree);
|
|
@@ -327,18 +269,16 @@ async function commitWorkdir(cwd, { online }) {
|
|
|
327
269
|
async function status(cwd) {
|
|
328
270
|
const root = path.resolve(cwd);
|
|
329
271
|
const config = await (0, config_js_1.readConfig)(root);
|
|
330
|
-
const branchName = config.branches ? await (0, branches_js_1.readBranchFile)(root) : null;
|
|
331
272
|
const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
|
|
332
273
|
try {
|
|
333
274
|
const shape = await (0, index_js_1.resolveShape)(config.shape);
|
|
334
|
-
const
|
|
335
|
-
const folderHandle = await (0, branches_js_1.resolveEffectiveRoot)(repo, rootHandle, branchName);
|
|
275
|
+
const folderHandle = await repo.find(config.rootUrl);
|
|
336
276
|
const previousTree = await shape.decode({ repo, root: folderHandle });
|
|
337
277
|
const previousFiles = await readFileBytes(repo, previousTree);
|
|
338
278
|
const ig = await (0, ignore_js_1.loadIgnore)(root);
|
|
339
279
|
const fsFiles = await (0, fs_tree_js_1.walkDir)(root, ig);
|
|
340
280
|
const diff = computeDiff(previousFiles, fsFiles);
|
|
341
|
-
return {
|
|
281
|
+
return { diff };
|
|
342
282
|
}
|
|
343
283
|
finally {
|
|
344
284
|
await repo.shutdown();
|
|
@@ -347,12 +287,10 @@ async function status(cwd) {
|
|
|
347
287
|
async function diff(cwd, limitToPath) {
|
|
348
288
|
const root = path.resolve(cwd);
|
|
349
289
|
const config = await (0, config_js_1.readConfig)(root);
|
|
350
|
-
const branchName = config.branches ? await (0, branches_js_1.readBranchFile)(root) : null;
|
|
351
290
|
const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
|
|
352
291
|
try {
|
|
353
292
|
const shape = await (0, index_js_1.resolveShape)(config.shape);
|
|
354
|
-
const
|
|
355
|
-
const folderHandle = await (0, branches_js_1.resolveEffectiveRoot)(repo, rootHandle, branchName);
|
|
293
|
+
const folderHandle = await repo.find(config.rootUrl);
|
|
356
294
|
const previousTree = await shape.decode({ repo, root: folderHandle });
|
|
357
295
|
const previousFiles = await readFileBytes(repo, previousTree);
|
|
358
296
|
const ig = await (0, ignore_js_1.loadIgnore)(root);
|
|
@@ -381,309 +319,19 @@ async function diff(cwd, limitToPath) {
|
|
|
381
319
|
await repo.shutdown();
|
|
382
320
|
}
|
|
383
321
|
}
|
|
384
|
-
async function listBranches(cwd) {
|
|
385
|
-
const root = path.resolve(cwd);
|
|
386
|
-
const config = await (0, config_js_1.readConfig)(root);
|
|
387
|
-
if (!config.branches) {
|
|
388
|
-
throw new Error("pushwork repo has no branches");
|
|
389
|
-
}
|
|
390
|
-
const current = await (0, branches_js_1.readBranchFile)(root);
|
|
391
|
-
const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
|
|
392
|
-
try {
|
|
393
|
-
const rootHandle = await repo.find(config.rootUrl);
|
|
394
|
-
const doc = rootHandle.doc();
|
|
395
|
-
if (!(0, branches_js_1.isBranchesDoc)(doc)) {
|
|
396
|
-
throw new Error(`root doc at ${config.rootUrl} is not a branches doc`);
|
|
397
|
-
}
|
|
398
|
-
return { current, names: (0, branches_js_1.listBranchNames)(doc) };
|
|
399
|
-
}
|
|
400
|
-
finally {
|
|
401
|
-
await repo.shutdown();
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
async function currentBranch(cwd) {
|
|
405
|
-
const root = path.resolve(cwd);
|
|
406
|
-
const config = await (0, config_js_1.readConfig)(root);
|
|
407
|
-
if (!config.branches)
|
|
408
|
-
return null;
|
|
409
|
-
return (0, branches_js_1.readBranchFile)(root);
|
|
410
|
-
}
|
|
411
|
-
async function createBranch(cwd, name) {
|
|
412
|
-
if (!name)
|
|
413
|
-
throw new Error("branch name is required");
|
|
414
|
-
if (name.includes("/") || name.includes("\\")) {
|
|
415
|
-
throw new Error("branch name may not contain slashes");
|
|
416
|
-
}
|
|
417
|
-
const root = path.resolve(cwd);
|
|
418
|
-
const config = await (0, config_js_1.readConfig)(root);
|
|
419
|
-
if (!config.branches)
|
|
420
|
-
throw new Error("pushwork repo has no branches");
|
|
421
|
-
const currentName = await (0, branches_js_1.readBranchFile)(root);
|
|
422
|
-
if (!currentName)
|
|
423
|
-
throw new Error("no current branch is set");
|
|
424
|
-
const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
|
|
425
|
-
try {
|
|
426
|
-
const shape = await (0, index_js_1.resolveShape)(config.shape);
|
|
427
|
-
const rootHandle = await repo.find(config.rootUrl);
|
|
428
|
-
const doc = rootHandle.doc();
|
|
429
|
-
if (!(0, branches_js_1.isBranchesDoc)(doc)) {
|
|
430
|
-
throw new Error(`root doc at ${config.rootUrl} is not a branches doc`);
|
|
431
|
-
}
|
|
432
|
-
if (doc.branches[name]) {
|
|
433
|
-
throw new Error(`branch "${name}" already exists`);
|
|
434
|
-
}
|
|
435
|
-
const sourceUrl = doc.branches[currentName];
|
|
436
|
-
if (!sourceUrl) {
|
|
437
|
-
throw new Error(`current branch "${currentName}" not found in branches doc`);
|
|
438
|
-
}
|
|
439
|
-
const sourceHandle = await repo.find(sourceUrl);
|
|
440
|
-
// Clone the folder doc.
|
|
441
|
-
const clonedFolder = repo.clone(sourceHandle);
|
|
442
|
-
dlog("createBranch %s cloned folder %s → %s", name, sourceUrl, clonedFolder.url);
|
|
443
|
-
// Deep-clone every file doc the source folder references, then rewrite
|
|
444
|
-
// the cloned folder's leaves to point at the new file URLs. Without
|
|
445
|
-
// this step both branches would alias the same UnixFileEntry docs and
|
|
446
|
-
// editing one branch would silently mutate the other.
|
|
447
|
-
const sourceTree = await shape.decode({ repo, root: sourceHandle });
|
|
448
|
-
const fileUrlRemap = new Map();
|
|
449
|
-
for (const [, fileUrl] of (0, index_js_1.flattenLeaves)(sourceTree)) {
|
|
450
|
-
const bare = (0, index_js_1.stripHeads)(fileUrl);
|
|
451
|
-
if (fileUrlRemap.has(bare))
|
|
452
|
-
continue;
|
|
453
|
-
const orig = await repo.find(bare);
|
|
454
|
-
const cloned = repo.clone(orig);
|
|
455
|
-
fileUrlRemap.set(bare, cloned.url);
|
|
456
|
-
dlog("createBranch cloned file %s → %s", bare, cloned.url);
|
|
457
|
-
}
|
|
458
|
-
const newTree = (0, index_js_1.newDir)();
|
|
459
|
-
for (const [posixPath, fileUrl] of (0, index_js_1.flattenLeaves)(sourceTree)) {
|
|
460
|
-
const bare = (0, index_js_1.stripHeads)(fileUrl);
|
|
461
|
-
const remappedBare = fileUrlRemap.get(bare);
|
|
462
|
-
if (!remappedBare)
|
|
463
|
-
continue;
|
|
464
|
-
const parsed = (0, automerge_repo_1.parseAutomergeUrl)(fileUrl);
|
|
465
|
-
// Preserve heads-pinning if the source URL was pinned.
|
|
466
|
-
let finalUrl = remappedBare;
|
|
467
|
-
if (parsed.heads) {
|
|
468
|
-
const newHandle = await repo.find(remappedBare);
|
|
469
|
-
finalUrl = (0, index_js_1.pinUrl)(newHandle);
|
|
470
|
-
}
|
|
471
|
-
const segments = posixPath.split("/").filter(Boolean);
|
|
472
|
-
(0, index_js_1.setFileAt)(newTree, segments, finalUrl);
|
|
473
|
-
}
|
|
474
|
-
await shape.encode({ repo, tree: newTree, previousRoot: clonedFolder });
|
|
475
|
-
rootHandle.change((d) => {
|
|
476
|
-
d.branches[name] = clonedFolder.url;
|
|
477
|
-
});
|
|
478
|
-
// Switch to the new branch. The deep clone has identical content to the
|
|
479
|
-
// source, so the working tree on disk is already correct — we just
|
|
480
|
-
// update .pushwork/branch.
|
|
481
|
-
await (0, branches_js_1.writeBranchFile)(root, name);
|
|
482
|
-
dlog("createBranch switched to %s", name);
|
|
483
|
-
return clonedFolder.url;
|
|
484
|
-
}
|
|
485
|
-
finally {
|
|
486
|
-
await repo.shutdown();
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
/**
|
|
490
|
-
* Apply changes from `source` branch onto the current branch.
|
|
491
|
-
*
|
|
492
|
-
* For each path:
|
|
493
|
-
* - In both branches: their UnixFileEntry docs share Automerge history (deep
|
|
494
|
-
* cloned at branch creation), so we Automerge-merge source's content into
|
|
495
|
-
* target's. Concurrent edits are CRDT-merged inside each file doc.
|
|
496
|
-
* - Only in source: deep-clone the source's file doc into a new doc and add
|
|
497
|
-
* it to target's folder. Editing on either branch afterward stays isolated.
|
|
498
|
-
* - Only in target: untouched. We don't propagate deletions from source — the
|
|
499
|
-
* user can do that explicitly.
|
|
500
|
-
*
|
|
501
|
-
* Refuses if the working tree has uncommitted changes against the current
|
|
502
|
-
* branch (run `pushwork save` first). Offline only — propagation happens on
|
|
503
|
-
* the next `pushwork sync`.
|
|
504
|
-
*/
|
|
505
|
-
/**
|
|
506
|
-
* Compute what `merge <source>` would do without mutating any docs or the
|
|
507
|
-
* working tree. For paths in both branches we apply the merge to a *clone*
|
|
508
|
-
* of the target's file doc to learn the merged bytes; for paths only in
|
|
509
|
-
* source we just read source's bytes.
|
|
510
|
-
*/
|
|
511
|
-
async function previewMerge(cwd, source) {
|
|
512
|
-
if (!source)
|
|
513
|
-
throw new Error("source branch name is required");
|
|
514
|
-
const root = path.resolve(cwd);
|
|
515
|
-
const config = await (0, config_js_1.readConfig)(root);
|
|
516
|
-
if (!config.branches)
|
|
517
|
-
throw new Error("pushwork repo has no branches");
|
|
518
|
-
const targetName = await (0, branches_js_1.readBranchFile)(root);
|
|
519
|
-
if (!targetName)
|
|
520
|
-
throw new Error("no current branch is set");
|
|
521
|
-
if (source === targetName) {
|
|
522
|
-
throw new Error(`cannot merge "${source}" into itself`);
|
|
523
|
-
}
|
|
524
|
-
const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
|
|
525
|
-
try {
|
|
526
|
-
const shape = await (0, index_js_1.resolveShape)(config.shape);
|
|
527
|
-
const rootHandle = await repo.find(config.rootUrl);
|
|
528
|
-
const branchesDoc = rootHandle.doc();
|
|
529
|
-
if (!(0, branches_js_1.isBranchesDoc)(branchesDoc)) {
|
|
530
|
-
throw new Error(`root doc at ${config.rootUrl} is not a branches doc`);
|
|
531
|
-
}
|
|
532
|
-
if (!branchesDoc.branches[source]) {
|
|
533
|
-
throw new Error(`source branch "${source}" does not exist`);
|
|
534
|
-
}
|
|
535
|
-
const targetFolder = await repo.find(branchesDoc.branches[targetName]);
|
|
536
|
-
const sourceFolder = await repo.find(branchesDoc.branches[source]);
|
|
537
|
-
const tTree = await shape.decode({ repo, root: targetFolder });
|
|
538
|
-
const sTree = await shape.decode({ repo, root: sourceFolder });
|
|
539
|
-
const tLeaves = (0, index_js_1.flattenLeaves)(tTree);
|
|
540
|
-
const sLeaves = (0, index_js_1.flattenLeaves)(sTree);
|
|
541
|
-
const entries = [];
|
|
542
|
-
for (const [posixPath, sUrl] of sLeaves) {
|
|
543
|
-
const tUrl = tLeaves.get(posixPath);
|
|
544
|
-
const sBare = (0, index_js_1.stripHeads)(sUrl);
|
|
545
|
-
const sHandle = await repo.find(sBare);
|
|
546
|
-
if (!tUrl) {
|
|
547
|
-
entries.push({
|
|
548
|
-
path: posixPath,
|
|
549
|
-
kind: "added",
|
|
550
|
-
after: (0, index_js_1.contentToBytes)(sHandle.doc().content),
|
|
551
|
-
});
|
|
552
|
-
continue;
|
|
553
|
-
}
|
|
554
|
-
const tBare = (0, index_js_1.stripHeads)(tUrl);
|
|
555
|
-
if (tBare === sBare)
|
|
556
|
-
continue;
|
|
557
|
-
const tHandle = await repo.find(tBare);
|
|
558
|
-
const before = (0, index_js_1.contentToBytes)(tHandle.doc().content);
|
|
559
|
-
// Compute merge result without touching the target doc.
|
|
560
|
-
const merged = Automerge.merge(Automerge.clone(tHandle.doc()), Automerge.clone(sHandle.doc()));
|
|
561
|
-
const after = (0, index_js_1.contentToBytes)(merged.content);
|
|
562
|
-
if ((0, fs_tree_js_1.byteEq)(before, after))
|
|
563
|
-
continue;
|
|
564
|
-
entries.push({ path: posixPath, kind: "merged", before, after });
|
|
565
|
-
}
|
|
566
|
-
entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
567
|
-
return { source, target: targetName, entries };
|
|
568
|
-
}
|
|
569
|
-
finally {
|
|
570
|
-
await repo.shutdown();
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
async function mergeBranch(cwd, source) {
|
|
574
|
-
if (!source)
|
|
575
|
-
throw new Error("source branch name is required");
|
|
576
|
-
const root = path.resolve(cwd);
|
|
577
|
-
const config = await (0, config_js_1.readConfig)(root);
|
|
578
|
-
if (!config.branches)
|
|
579
|
-
throw new Error("pushwork repo has no branches");
|
|
580
|
-
const targetName = await (0, branches_js_1.readBranchFile)(root);
|
|
581
|
-
if (!targetName)
|
|
582
|
-
throw new Error("no current branch is set");
|
|
583
|
-
if (source === targetName) {
|
|
584
|
-
throw new Error(`cannot merge "${source}" into itself`);
|
|
585
|
-
}
|
|
586
|
-
dlog("merge source=%s target=%s", source, targetName);
|
|
587
|
-
const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
|
|
588
|
-
try {
|
|
589
|
-
const shape = await (0, index_js_1.resolveShape)(config.shape);
|
|
590
|
-
const rootHandle = await repo.find(config.rootUrl);
|
|
591
|
-
const branchesDoc = rootHandle.doc();
|
|
592
|
-
if (!(0, branches_js_1.isBranchesDoc)(branchesDoc)) {
|
|
593
|
-
throw new Error(`root doc at ${config.rootUrl} is not a branches doc`);
|
|
594
|
-
}
|
|
595
|
-
if (!branchesDoc.branches[source]) {
|
|
596
|
-
throw new Error(`source branch "${source}" does not exist`);
|
|
597
|
-
}
|
|
598
|
-
const targetUrl = branchesDoc.branches[targetName];
|
|
599
|
-
const sourceUrl = branchesDoc.branches[source];
|
|
600
|
-
const targetFolder = await repo.find(targetUrl);
|
|
601
|
-
const sourceFolder = await repo.find(sourceUrl);
|
|
602
|
-
// Refuse on dirty working tree (mirror switchBranch policy).
|
|
603
|
-
const tFiles = await readFileBytes(repo, await shape.decode({ repo, root: targetFolder }));
|
|
604
|
-
const ig = await (0, ignore_js_1.loadIgnore)(root);
|
|
605
|
-
const fsFiles = await (0, fs_tree_js_1.walkDir)(root, ig);
|
|
606
|
-
const dirty = computeDiff(tFiles, fsFiles);
|
|
607
|
-
if (dirty.added.length || dirty.modified.length || dirty.deleted.length) {
|
|
608
|
-
throw new Error(`refusing to merge: working tree has uncommitted changes on branch "${targetName}". run \`pushwork save\` first.`);
|
|
609
|
-
}
|
|
610
|
-
const tTree = await shape.decode({ repo, root: targetFolder });
|
|
611
|
-
const sTree = await shape.decode({ repo, root: sourceFolder });
|
|
612
|
-
const tLeaves = (0, index_js_1.flattenLeaves)(tTree);
|
|
613
|
-
const sLeaves = (0, index_js_1.flattenLeaves)(sTree);
|
|
614
|
-
const merged = [];
|
|
615
|
-
const added = [];
|
|
616
|
-
// For paths in both: merge file docs in place.
|
|
617
|
-
for (const [posixPath, sUrl] of sLeaves) {
|
|
618
|
-
const tUrl = tLeaves.get(posixPath);
|
|
619
|
-
if (!tUrl)
|
|
620
|
-
continue;
|
|
621
|
-
const tBare = (0, index_js_1.stripHeads)(tUrl);
|
|
622
|
-
const sBare = (0, index_js_1.stripHeads)(sUrl);
|
|
623
|
-
if (tBare === sBare) {
|
|
624
|
-
// Same file doc identity (shared) — already in sync, nothing to do.
|
|
625
|
-
continue;
|
|
626
|
-
}
|
|
627
|
-
const tHandle = await repo.find(tBare);
|
|
628
|
-
const sHandle = await repo.find(sBare);
|
|
629
|
-
tHandle.update((d) => Automerge.merge(d, Automerge.clone(sHandle.doc())));
|
|
630
|
-
merged.push(posixPath);
|
|
631
|
-
dlog("merge merged file at %s (%s ← %s)", posixPath, tBare, sBare);
|
|
632
|
-
}
|
|
633
|
-
// For paths only in source: deep-clone source's file doc, add to target's folder.
|
|
634
|
-
const newLeaves = new Map();
|
|
635
|
-
for (const [posixPath, sUrl] of sLeaves) {
|
|
636
|
-
if (tLeaves.has(posixPath))
|
|
637
|
-
continue;
|
|
638
|
-
const sBare = (0, index_js_1.stripHeads)(sUrl);
|
|
639
|
-
const sHandle = await repo.find(sBare);
|
|
640
|
-
const cloned = repo.clone(sHandle);
|
|
641
|
-
let finalUrl = cloned.url;
|
|
642
|
-
const parsed = (0, automerge_repo_1.parseAutomergeUrl)(sUrl);
|
|
643
|
-
if (parsed.heads) {
|
|
644
|
-
finalUrl = (0, index_js_1.pinUrl)(cloned);
|
|
645
|
-
}
|
|
646
|
-
newLeaves.set(posixPath, finalUrl);
|
|
647
|
-
added.push(posixPath);
|
|
648
|
-
dlog("merge added %s url=%s", posixPath, finalUrl);
|
|
649
|
-
}
|
|
650
|
-
if (newLeaves.size > 0) {
|
|
651
|
-
// Build a tree for the encode call: existing target leaves + new ones.
|
|
652
|
-
const nextTree = (0, index_js_1.newDir)();
|
|
653
|
-
for (const [p, url] of tLeaves) {
|
|
654
|
-
(0, index_js_1.setFileAt)(nextTree, p.split("/").filter(Boolean), url);
|
|
655
|
-
}
|
|
656
|
-
for (const [p, url] of newLeaves) {
|
|
657
|
-
(0, index_js_1.setFileAt)(nextTree, p.split("/").filter(Boolean), url);
|
|
658
|
-
}
|
|
659
|
-
await shape.encode({ repo, tree: nextTree, previousRoot: targetFolder });
|
|
660
|
-
}
|
|
661
|
-
// Materialize current branch (target) onto disk to reflect the merge.
|
|
662
|
-
const finalTree = await shape.decode({ repo, root: targetFolder });
|
|
663
|
-
await materializeTree(repo, root, finalTree);
|
|
664
|
-
merged.sort();
|
|
665
|
-
added.sort();
|
|
666
|
-
return { source, target: targetName, merged, added };
|
|
667
|
-
}
|
|
668
|
-
finally {
|
|
669
|
-
await repo.shutdown();
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
322
|
/**
|
|
673
|
-
* Capture the working tree's changes against the
|
|
674
|
-
*
|
|
675
|
-
*
|
|
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.
|
|
676
326
|
*/
|
|
677
327
|
async function cutWorkdir(cwd, opts = {}) {
|
|
678
328
|
const root = path.resolve(cwd);
|
|
679
329
|
const config = await (0, config_js_1.readConfig)(root);
|
|
680
|
-
|
|
681
|
-
dlog("cut root=%s branch=%s name=%s", root, branchName, opts.name ?? "(unnamed)");
|
|
330
|
+
dlog("cut root=%s name=%s", root, opts.name ?? "(unnamed)");
|
|
682
331
|
const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
|
|
683
332
|
try {
|
|
684
333
|
const shape = await (0, index_js_1.resolveShape)(config.shape);
|
|
685
|
-
const
|
|
686
|
-
const folderHandle = await (0, branches_js_1.resolveEffectiveRoot)(repo, rootHandle, branchName);
|
|
334
|
+
const folderHandle = await repo.find(config.rootUrl);
|
|
687
335
|
const previousTree = await shape.decode({ repo, root: folderHandle });
|
|
688
336
|
const previousFiles = await readFileBytes(repo, previousTree);
|
|
689
337
|
const ig = await (0, ignore_js_1.loadIgnore)(root);
|
|
@@ -692,13 +340,13 @@ async function cutWorkdir(cwd, opts = {}) {
|
|
|
692
340
|
for (const [p, bytes] of fsFiles) {
|
|
693
341
|
const prev = previousFiles.get(p);
|
|
694
342
|
if (!prev) {
|
|
695
|
-
entries.push({ path: p, kind: "added", contentBase64: (0,
|
|
343
|
+
entries.push({ path: p, kind: "added", contentBase64: (0, snarf_js_1.encodeBytes)(bytes) });
|
|
696
344
|
}
|
|
697
345
|
else if (!(0, fs_tree_js_1.byteEq)(prev.bytes, bytes)) {
|
|
698
346
|
entries.push({
|
|
699
347
|
path: p,
|
|
700
348
|
kind: "modified",
|
|
701
|
-
contentBase64: (0,
|
|
349
|
+
contentBase64: (0, snarf_js_1.encodeBytes)(bytes),
|
|
702
350
|
});
|
|
703
351
|
}
|
|
704
352
|
}
|
|
@@ -710,35 +358,32 @@ async function cutWorkdir(cwd, opts = {}) {
|
|
|
710
358
|
throw new Error("nothing to cut: working tree clean");
|
|
711
359
|
}
|
|
712
360
|
entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
713
|
-
const
|
|
361
|
+
const snarf = await (0, snarf_js_1.appendSnarf)(root, {
|
|
714
362
|
name: opts.name,
|
|
715
|
-
branch: branchName,
|
|
716
363
|
entries,
|
|
717
364
|
});
|
|
718
|
-
// Reset working tree to the
|
|
365
|
+
// Reset working tree to the saved state.
|
|
719
366
|
await materializeTree(repo, root, previousTree);
|
|
720
|
-
dlog("cut complete id=%d entries=%d",
|
|
721
|
-
return { id:
|
|
367
|
+
dlog("cut complete id=%d entries=%d", snarf.id, entries.length);
|
|
368
|
+
return { id: snarf.id, entries: entries.length };
|
|
722
369
|
}
|
|
723
370
|
finally {
|
|
724
371
|
await repo.shutdown();
|
|
725
372
|
}
|
|
726
373
|
}
|
|
727
374
|
/**
|
|
728
|
-
* Apply a
|
|
375
|
+
* Apply a snarf on top of the current working tree, then remove the snarf
|
|
729
376
|
* entry. Refuses if the working tree has uncommitted changes (caller can
|
|
730
377
|
* `pushwork save` or `pushwork cut` first).
|
|
731
378
|
*/
|
|
732
|
-
async function
|
|
379
|
+
async function pasteSnarf(cwd, selector) {
|
|
733
380
|
const root = path.resolve(cwd);
|
|
734
381
|
const config = await (0, config_js_1.readConfig)(root);
|
|
735
|
-
|
|
736
|
-
// Check the working tree is clean against the current branch state.
|
|
382
|
+
// Check the working tree is clean against the saved state.
|
|
737
383
|
const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
|
|
738
384
|
try {
|
|
739
385
|
const shape = await (0, index_js_1.resolveShape)(config.shape);
|
|
740
|
-
const
|
|
741
|
-
const folderHandle = await (0, branches_js_1.resolveEffectiveRoot)(repo, rootHandle, branchName);
|
|
386
|
+
const folderHandle = await repo.find(config.rootUrl);
|
|
742
387
|
const previousTree = await shape.decode({ repo, root: folderHandle });
|
|
743
388
|
const previousFiles = await readFileBytes(repo, previousTree);
|
|
744
389
|
const ig = await (0, ignore_js_1.loadIgnore)(root);
|
|
@@ -751,13 +396,13 @@ async function pasteStash(cwd, selector) {
|
|
|
751
396
|
finally {
|
|
752
397
|
await repo.shutdown();
|
|
753
398
|
}
|
|
754
|
-
const
|
|
755
|
-
if (!
|
|
399
|
+
const snarf = await (0, snarf_js_1.takeSnarf)(root, selector);
|
|
400
|
+
if (!snarf) {
|
|
756
401
|
throw new Error(selector
|
|
757
|
-
? `no
|
|
758
|
-
: "nothing to paste: no
|
|
402
|
+
? `no snarf matches "${selector}"`
|
|
403
|
+
: "nothing to paste: no snarfs");
|
|
759
404
|
}
|
|
760
|
-
for (const entry of
|
|
405
|
+
for (const entry of snarf.entries) {
|
|
761
406
|
const target = path.join(root, fromPosix(entry.path));
|
|
762
407
|
if (entry.kind === "deleted") {
|
|
763
408
|
try {
|
|
@@ -769,118 +414,15 @@ async function pasteStash(cwd, selector) {
|
|
|
769
414
|
await pruneEmptyDirs(root, path.dirname(fromPosix(entry.path)));
|
|
770
415
|
}
|
|
771
416
|
else if (entry.contentBase64 != null) {
|
|
772
|
-
const bytes = (0,
|
|
417
|
+
const bytes = (0, snarf_js_1.decodeBytes)(entry.contentBase64);
|
|
773
418
|
await (0, fs_tree_js_1.writeFileAtomic)(target, bytes);
|
|
774
419
|
}
|
|
775
420
|
}
|
|
776
|
-
dlog("paste complete id=%d entries=%d",
|
|
777
|
-
return { id:
|
|
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 };
|
|
778
423
|
}
|
|
779
|
-
async function
|
|
780
|
-
return (0,
|
|
781
|
-
}
|
|
782
|
-
async function switchBranch(cwd, name) {
|
|
783
|
-
if (!name)
|
|
784
|
-
throw new Error("branch name is required");
|
|
785
|
-
const root = path.resolve(cwd);
|
|
786
|
-
const config = await (0, config_js_1.readConfig)(root);
|
|
787
|
-
if (!config.branches)
|
|
788
|
-
throw new Error("pushwork repo has no branches");
|
|
789
|
-
const currentName = await (0, branches_js_1.readBranchFile)(root);
|
|
790
|
-
const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
|
|
791
|
-
try {
|
|
792
|
-
const shape = await (0, index_js_1.resolveShape)(config.shape);
|
|
793
|
-
const rootHandle = await repo.find(config.rootUrl);
|
|
794
|
-
const doc = rootHandle.doc();
|
|
795
|
-
if (!(0, branches_js_1.isBranchesDoc)(doc)) {
|
|
796
|
-
throw new Error(`root doc at ${config.rootUrl} is not a branches doc`);
|
|
797
|
-
}
|
|
798
|
-
if (!doc.branches[name]) {
|
|
799
|
-
throw new Error(`branch "${name}" does not exist`);
|
|
800
|
-
}
|
|
801
|
-
const stranded = !!currentName && !doc.branches[currentName];
|
|
802
|
-
// Refuse if the working dir has uncommitted changes against the current
|
|
803
|
-
// branch. The user can `pushwork save` to commit, or `pushwork cut` +
|
|
804
|
-
// `pushwork paste` to carry the changes across the switch.
|
|
805
|
-
if (currentName && !stranded) {
|
|
806
|
-
const folderHandle = await (0, branches_js_1.resolveEffectiveRoot)(repo, rootHandle, currentName);
|
|
807
|
-
const previousTree = await shape.decode({ repo, root: folderHandle });
|
|
808
|
-
const previousFiles = await readFileBytes(repo, previousTree);
|
|
809
|
-
const ig = await (0, ignore_js_1.loadIgnore)(root);
|
|
810
|
-
const fsFiles = await (0, fs_tree_js_1.walkDir)(root, ig);
|
|
811
|
-
const d = computeDiff(previousFiles, fsFiles);
|
|
812
|
-
if (d.added.length || d.modified.length || d.deleted.length) {
|
|
813
|
-
throw new Error(`refusing to switch: working tree has uncommitted changes on branch "${currentName}". run \`pushwork save\` to commit, or \`pushwork cut\` + \`pushwork switch ${name}\` + \`pushwork paste\` to carry them across.`);
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
// Materialize from the new branch.
|
|
817
|
-
const newFolder = await repo.find(doc.branches[name]);
|
|
818
|
-
const tree = await shape.decode({ repo, root: newFolder });
|
|
819
|
-
// Stranded: the current branch is gone, so we have no reference for a
|
|
820
|
-
// dirty check. Auto-cut working changes against the destination branch,
|
|
821
|
-
// materialize, then auto-paste so the user's work survives the switch.
|
|
822
|
-
let strandedStashId = null;
|
|
823
|
-
if (stranded) {
|
|
824
|
-
const newFiles = await readFileBytes(repo, tree);
|
|
825
|
-
const ig = await (0, ignore_js_1.loadIgnore)(root);
|
|
826
|
-
const fsFiles = await (0, fs_tree_js_1.walkDir)(root, ig);
|
|
827
|
-
const entries = [];
|
|
828
|
-
for (const [p, bytes] of fsFiles) {
|
|
829
|
-
const prev = newFiles.get(p);
|
|
830
|
-
if (!prev) {
|
|
831
|
-
entries.push({ path: p, kind: "added", contentBase64: (0, stash_js_1.encodeBytes)(bytes) });
|
|
832
|
-
}
|
|
833
|
-
else if (!(0, fs_tree_js_1.byteEq)(prev.bytes, bytes)) {
|
|
834
|
-
entries.push({ path: p, kind: "modified", contentBase64: (0, stash_js_1.encodeBytes)(bytes) });
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
for (const [p] of newFiles) {
|
|
838
|
-
if (!fsFiles.has(p))
|
|
839
|
-
entries.push({ path: p, kind: "deleted" });
|
|
840
|
-
}
|
|
841
|
-
if (entries.length > 0) {
|
|
842
|
-
entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
843
|
-
const stash = await (0, stash_js_1.appendStash)(root, {
|
|
844
|
-
name: `stranded-from-${currentName}`,
|
|
845
|
-
branch: currentName,
|
|
846
|
-
entries,
|
|
847
|
-
});
|
|
848
|
-
strandedStashId = stash.id;
|
|
849
|
-
process.stderr.write(`warning: branch "${currentName}" no longer exists; auto-cut ${entries.length} entr${entries.length === 1 ? "y" : "ies"} as stash #${stash.id} and will auto-paste after switch\n`);
|
|
850
|
-
}
|
|
851
|
-
else {
|
|
852
|
-
process.stderr.write(`warning: branch "${currentName}" no longer exists; switching (working tree already matches "${name}")\n`);
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
await materializeTree(repo, root, tree);
|
|
856
|
-
await (0, branches_js_1.writeBranchFile)(root, name);
|
|
857
|
-
dlog("switch → %s", name);
|
|
858
|
-
if (strandedStashId != null) {
|
|
859
|
-
const stash = await (0, stash_js_1.takeStash)(root, String(strandedStashId));
|
|
860
|
-
if (stash) {
|
|
861
|
-
for (const entry of stash.entries) {
|
|
862
|
-
const target = path.join(root, fromPosix(entry.path));
|
|
863
|
-
if (entry.kind === "deleted") {
|
|
864
|
-
try {
|
|
865
|
-
await fs.unlink(target);
|
|
866
|
-
}
|
|
867
|
-
catch {
|
|
868
|
-
// already gone
|
|
869
|
-
}
|
|
870
|
-
await pruneEmptyDirs(root, path.dirname(fromPosix(entry.path)));
|
|
871
|
-
}
|
|
872
|
-
else if (entry.contentBase64 != null) {
|
|
873
|
-
const bytes = (0, stash_js_1.decodeBytes)(entry.contentBase64);
|
|
874
|
-
await (0, fs_tree_js_1.writeFileAtomic)(target, bytes);
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
dlog("stranded auto-paste applied stash #%d (%d entries)", stash.id, stash.entries.length);
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
finally {
|
|
882
|
-
await repo.shutdown();
|
|
883
|
-
}
|
|
424
|
+
async function showSnarfs(cwd) {
|
|
425
|
+
return (0, snarf_js_1.listSnarfs)(path.resolve(cwd));
|
|
884
426
|
}
|
|
885
427
|
function stampLastSyncAt(handle) {
|
|
886
428
|
handle.change((d) => {
|
|
@@ -938,17 +480,13 @@ async function pushFiles(repo, fsFiles, previous, artifactDirs) {
|
|
|
938
480
|
}
|
|
939
481
|
else if (prev) {
|
|
940
482
|
// Changed path: mutate the existing file doc in place. This keeps
|
|
941
|
-
// the file URL stable
|
|
483
|
+
// the file URL stable across edits and avoids the propagation
|
|
942
484
|
// race where a brand-new file doc URL is referenced by the folder
|
|
943
485
|
// before its bytes have reached the sync server.
|
|
944
486
|
//
|
|
945
487
|
// For string content (text files) we use Automerge.updateText so
|
|
946
488
|
// concurrent character-level edits merge correctly. Bytes and
|
|
947
489
|
// ImmutableString are atomic — last writer wins on the field.
|
|
948
|
-
//
|
|
949
|
-
// Branch isolation is enforced separately: `createBranch` deep
|
|
950
|
-
// clones every file doc the source branch references, so two
|
|
951
|
-
// branches never share a UnixFileEntry doc identity.
|
|
952
490
|
const refreshUrl = (0, index_js_1.stripHeads)(prev.url);
|
|
953
491
|
const handle = await repo.find(refreshUrl);
|
|
954
492
|
handle.change((d) => {
|