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.
Files changed (169) hide show
  1. package/dist/branches.d.ts +1 -0
  2. package/dist/branches.d.ts.map +1 -1
  3. package/dist/cli/commands.d.ts +71 -0
  4. package/dist/cli/commands.d.ts.map +1 -0
  5. package/dist/cli/commands.js +794 -0
  6. package/dist/cli/commands.js.map +1 -0
  7. package/dist/cli/index.d.ts +2 -0
  8. package/dist/cli/index.d.ts.map +1 -0
  9. package/dist/cli/index.js +19 -0
  10. package/dist/cli/index.js.map +1 -0
  11. package/dist/cli.js +67 -112
  12. package/dist/cli.js.map +1 -1
  13. package/dist/commands.d.ts +58 -0
  14. package/dist/commands.d.ts.map +1 -0
  15. package/dist/commands.js +975 -0
  16. package/dist/commands.js.map +1 -0
  17. package/dist/config/index.d.ts +71 -0
  18. package/dist/config/index.d.ts.map +1 -0
  19. package/dist/config/index.js +314 -0
  20. package/dist/config/index.js.map +1 -0
  21. package/dist/config.d.ts +1 -2
  22. package/dist/config.d.ts.map +1 -1
  23. package/dist/config.js +1 -2
  24. package/dist/config.js.map +1 -1
  25. package/dist/core/change-detection.d.ts +80 -0
  26. package/dist/core/change-detection.d.ts.map +1 -0
  27. package/dist/core/change-detection.js +560 -0
  28. package/dist/core/change-detection.js.map +1 -0
  29. package/dist/core/config.d.ts +81 -0
  30. package/dist/core/config.d.ts.map +1 -0
  31. package/dist/core/config.js +304 -0
  32. package/dist/core/config.js.map +1 -0
  33. package/dist/core/index.d.ts +6 -0
  34. package/dist/core/index.d.ts.map +1 -0
  35. package/dist/core/index.js +22 -0
  36. package/dist/core/index.js.map +1 -0
  37. package/dist/core/move-detection.d.ts +34 -0
  38. package/dist/core/move-detection.d.ts.map +1 -0
  39. package/dist/core/move-detection.js +128 -0
  40. package/dist/core/move-detection.js.map +1 -0
  41. package/dist/core/snapshot.d.ts +105 -0
  42. package/dist/core/snapshot.d.ts.map +1 -0
  43. package/dist/core/snapshot.js +254 -0
  44. package/dist/core/snapshot.js.map +1 -0
  45. package/dist/core/sync-engine.d.ts +177 -0
  46. package/dist/core/sync-engine.d.ts.map +1 -0
  47. package/dist/core/sync-engine.js +1471 -0
  48. package/dist/core/sync-engine.js.map +1 -0
  49. package/dist/index.d.ts +2 -4
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +4 -14
  52. package/dist/index.js.map +1 -1
  53. package/dist/pushwork.d.ts +28 -61
  54. package/dist/pushwork.d.ts.map +1 -1
  55. package/dist/pushwork.js +127 -445
  56. package/dist/pushwork.js.map +1 -1
  57. package/dist/shapes/types.d.ts +1 -0
  58. package/dist/shapes/types.d.ts.map +1 -1
  59. package/dist/shapes/types.js.map +1 -1
  60. package/dist/shapes/vfs.d.ts.map +1 -1
  61. package/dist/shapes/vfs.js +6 -2
  62. package/dist/shapes/vfs.js.map +1 -1
  63. package/dist/snarf.d.ts +21 -0
  64. package/dist/snarf.d.ts.map +1 -0
  65. package/dist/snarf.js +117 -0
  66. package/dist/snarf.js.map +1 -0
  67. package/dist/stash.d.ts +0 -2
  68. package/dist/stash.d.ts.map +1 -1
  69. package/dist/stash.js +0 -1
  70. package/dist/stash.js.map +1 -1
  71. package/dist/types/config.d.ts +102 -0
  72. package/dist/types/config.d.ts.map +1 -0
  73. package/dist/types/config.js +10 -0
  74. package/dist/types/config.js.map +1 -0
  75. package/dist/types/documents.d.ts +88 -0
  76. package/dist/types/documents.d.ts.map +1 -0
  77. package/dist/types/documents.js +23 -0
  78. package/dist/types/documents.js.map +1 -0
  79. package/dist/types/index.d.ts +4 -0
  80. package/dist/types/index.d.ts.map +1 -0
  81. package/dist/types/index.js +20 -0
  82. package/dist/types/index.js.map +1 -0
  83. package/dist/types/snapshot.d.ts +64 -0
  84. package/dist/types/snapshot.d.ts.map +1 -0
  85. package/dist/types/snapshot.js +3 -0
  86. package/dist/types/snapshot.js.map +1 -0
  87. package/dist/utils/content-similarity.d.ts +53 -0
  88. package/dist/utils/content-similarity.d.ts.map +1 -0
  89. package/dist/utils/content-similarity.js +155 -0
  90. package/dist/utils/content-similarity.js.map +1 -0
  91. package/dist/utils/content.d.ts +10 -0
  92. package/dist/utils/content.d.ts.map +1 -0
  93. package/dist/utils/content.js +35 -0
  94. package/dist/utils/content.js.map +1 -0
  95. package/dist/utils/directory.d.ts +24 -0
  96. package/dist/utils/directory.d.ts.map +1 -0
  97. package/dist/utils/directory.js +56 -0
  98. package/dist/utils/directory.js.map +1 -0
  99. package/dist/utils/fs.d.ts +74 -0
  100. package/dist/utils/fs.d.ts.map +1 -0
  101. package/dist/utils/fs.js +298 -0
  102. package/dist/utils/fs.js.map +1 -0
  103. package/dist/utils/index.d.ts +5 -0
  104. package/dist/utils/index.d.ts.map +1 -0
  105. package/dist/utils/index.js +21 -0
  106. package/dist/utils/index.js.map +1 -0
  107. package/dist/utils/mime-types.d.ts +13 -0
  108. package/dist/utils/mime-types.d.ts.map +1 -0
  109. package/dist/utils/mime-types.js +247 -0
  110. package/dist/utils/mime-types.js.map +1 -0
  111. package/dist/utils/network-sync.d.ts +30 -0
  112. package/dist/utils/network-sync.d.ts.map +1 -0
  113. package/dist/utils/network-sync.js +391 -0
  114. package/dist/utils/network-sync.js.map +1 -0
  115. package/dist/utils/node-polyfills.d.ts +9 -0
  116. package/dist/utils/node-polyfills.d.ts.map +1 -0
  117. package/dist/utils/node-polyfills.js +9 -0
  118. package/dist/utils/node-polyfills.js.map +1 -0
  119. package/dist/utils/output.d.ts +129 -0
  120. package/dist/utils/output.d.ts.map +1 -0
  121. package/dist/utils/output.js +375 -0
  122. package/dist/utils/output.js.map +1 -0
  123. package/dist/utils/repo-factory.d.ts +15 -0
  124. package/dist/utils/repo-factory.d.ts.map +1 -0
  125. package/dist/utils/repo-factory.js +156 -0
  126. package/dist/utils/repo-factory.js.map +1 -0
  127. package/dist/utils/string-similarity.d.ts +14 -0
  128. package/dist/utils/string-similarity.d.ts.map +1 -0
  129. package/dist/utils/string-similarity.js +43 -0
  130. package/dist/utils/string-similarity.js.map +1 -0
  131. package/dist/utils/text-diff.d.ts +37 -0
  132. package/dist/utils/text-diff.d.ts.map +1 -0
  133. package/dist/utils/text-diff.js +131 -0
  134. package/dist/utils/text-diff.js.map +1 -0
  135. package/dist/utils/trace.d.ts +19 -0
  136. package/dist/utils/trace.d.ts.map +1 -0
  137. package/dist/utils/trace.js +68 -0
  138. package/dist/utils/trace.js.map +1 -0
  139. package/dist/version.d.ts +11 -0
  140. package/dist/version.d.ts.map +1 -0
  141. package/dist/version.js +93 -0
  142. package/dist/version.js.map +1 -0
  143. package/package.json +5 -1
  144. package/.prettierrc +0 -9
  145. package/flake.lock +0 -128
  146. package/flake.nix +0 -66
  147. package/pnpm-workspace.yaml +0 -5
  148. package/src/branches.ts +0 -93
  149. package/src/cli.ts +0 -292
  150. package/src/config.ts +0 -64
  151. package/src/fs-tree.ts +0 -70
  152. package/src/ignore.ts +0 -33
  153. package/src/index.ts +0 -38
  154. package/src/log.ts +0 -8
  155. package/src/pushwork.ts +0 -1055
  156. package/src/repo.ts +0 -76
  157. package/src/shapes/custom.ts +0 -29
  158. package/src/shapes/file.ts +0 -115
  159. package/src/shapes/index.ts +0 -19
  160. package/src/shapes/patchwork-folder.ts +0 -156
  161. package/src/shapes/types.ts +0 -79
  162. package/src/shapes/vfs.ts +0 -93
  163. package/src/stash.ts +0 -106
  164. package/test/integration/branches.test.ts +0 -389
  165. package/test/integration/pushwork.test.ts +0 -547
  166. package/test/setup.ts +0 -29
  167. package/test/unit/doc-shape.test.ts +0 -612
  168. package/tsconfig.json +0 -22
  169. 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.pasteStash = pasteStash;
51
- exports.showStashes = showStashes;
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 stash_js_1 = require("./stash.js");
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 branches=%s online=%s", root, opts.backend, opts.shape, useBranches, online);
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", rootUrl);
116
- return rootUrl;
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
- const rootHandle = await repo.find(opts.url);
116
+ let folderHandle = await repo.find(opts.url);
139
117
  if (online) {
140
- await (0, repo_js_1.waitForSync)(rootHandle, { idleMs: 1500, maxMs: 15000 });
118
+ await (0, repo_js_1.waitForSync)(folderHandle, { idleMs: 1500, maxMs: 15000 });
141
119
  }
142
- const docType = (0, branches_js_1.detectDocType)(rootHandle.doc());
143
- dlog("clone detected docType=%s", docType);
144
- let useBranches = false;
145
- let folderHandle = rootHandle;
146
- if (docType === "branches") {
147
- useBranches = true;
148
- const branchName = opts.branch ?? branches_js_1.DEFAULT_BRANCH;
149
- folderHandle = await (0, branches_js_1.resolveEffectiveRoot)(repo, rootHandle, branchName);
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
- await (0, branches_js_1.writeBranchFile)(root, branchName);
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: opts.url,
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
- const branchName = config.branches ? await (0, branches_js_1.readBranchFile)(root) : null;
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 rootHandle = await repo.find(config.rootUrl);
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 rootHandle = await repo.find(config.rootUrl);
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 { branch: branchName, diff };
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 rootHandle = await repo.find(config.rootUrl);
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 current branch's saved
591
- * state into a local stash, then reset the working tree to the saved state.
592
- * Stashes live in `.pushwork/stash.json` and are never synced.
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
- const branchName = config.branches ? await (0, branches_js_1.readBranchFile)(root) : null;
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 rootHandle = await repo.find(config.rootUrl);
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, stash_js_1.encodeBytes)(bytes) });
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, stash_js_1.encodeBytes)(bytes),
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 stash = await (0, stash_js_1.appendStash)(root, {
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 branch's saved state.
365
+ // Reset working tree to the saved state.
636
366
  await materializeTree(repo, root, previousTree);
637
- dlog("cut complete id=%d entries=%d", stash.id, entries.length);
638
- return { id: stash.id, entries: entries.length };
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 stash on top of the current working tree, then remove the stash
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 pasteStash(cwd, selector) {
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
- const branchName = config.branches ? await (0, branches_js_1.readBranchFile)(root) : null;
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 rootHandle = await repo.find(config.rootUrl);
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 stash = await (0, stash_js_1.takeStash)(root, selector);
672
- if (!stash) {
399
+ const snarf = await (0, snarf_js_1.takeSnarf)(root, selector);
400
+ if (!snarf) {
673
401
  throw new Error(selector
674
- ? `no stash matches "${selector}"`
675
- : "nothing to paste: no stashes");
402
+ ? `no snarf matches "${selector}"`
403
+ : "nothing to paste: no snarfs");
676
404
  }
677
- for (const entry of stash.entries) {
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, stash_js_1.decodeBytes)(entry.contentBase64);
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", stash.id, stash.entries.length);
694
- return { id: stash.id, name: stash.name, entries: stash.entries.length };
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 switchBranch(cwd, name) {
700
- if (!name)
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 within a branch and avoids the propagation
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) => {