isomorphic-jj 1.4.3 → 1.7.0
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/CHANGELOG.md +226 -0
- package/README.md +9 -11
- package/bin/isojj.js +270 -102
- package/dist/index.cjs +2910 -1666
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2910 -1666
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -4
- package/src/api/repository.js +1099 -739
- package/src/backend/isomorphic-git-backend.js +183 -136
- package/src/backend/lazy-git-backend.js +12 -19
- package/src/browser/helpers.js +6 -2
- package/src/core/background-ops.js +58 -41
- package/src/core/bookmark-store.js +12 -8
- package/src/core/change-graph.js +26 -29
- package/src/core/conflict-model.js +66 -26
- package/src/core/jj-checkout.js +8 -4
- package/src/core/jj-operation-store.js +16 -12
- package/src/core/jj-tree-state.js +13 -9
- package/src/core/jj-view-store.js +11 -5
- package/src/core/merge-driver-registry.js +92 -46
- package/src/core/operation-log.js +30 -33
- package/src/core/revset-engine.js +1069 -485
- package/src/core/storage-manager.js +25 -19
- package/src/core/tag-store.js +2 -2
- package/src/core/user-config.js +3 -1
- package/src/core/working-copy.js +125 -9
- package/src/core/workspace-manager.js +2 -2
- package/src/drivers/built-in-drivers.js +66 -33
- package/src/types.d.ts +67 -1
- package/src/utils/errors.js +3 -3
- package/src/utils/id-generation.js +4 -4
- package/src/utils/validation.js +6 -10
|
@@ -15,16 +15,16 @@ import crypto from 'crypto';
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Backend adapter for isomorphic-git
|
|
18
|
-
*
|
|
18
|
+
*
|
|
19
19
|
* @class IsomorphicGitBackend
|
|
20
20
|
*/
|
|
21
21
|
export class IsomorphicGitBackend {
|
|
22
22
|
/**
|
|
23
23
|
* Create isomorphic-git backend
|
|
24
|
-
*
|
|
24
|
+
*
|
|
25
25
|
* @param {Object} options - Backend options
|
|
26
|
-
* @param {
|
|
27
|
-
* @param {
|
|
26
|
+
* @param {any} options.fs - Filesystem implementation (Node fs or LightningFS)
|
|
27
|
+
* @param {any} [options.http] - HTTP client for network operations
|
|
28
28
|
* @param {string} options.dir - Repository directory path
|
|
29
29
|
*/
|
|
30
30
|
constructor({ fs, http, dir }) {
|
|
@@ -36,11 +36,9 @@ export class IsomorphicGitBackend {
|
|
|
36
36
|
);
|
|
37
37
|
}
|
|
38
38
|
if (!dir) {
|
|
39
|
-
throw new JJError(
|
|
40
|
-
'
|
|
41
|
-
|
|
42
|
-
{ suggestion: 'Provide dir option with repository path' }
|
|
43
|
-
);
|
|
39
|
+
throw new JJError('INVALID_BACKEND_CONFIG', 'Repository directory (dir) is required', {
|
|
40
|
+
suggestion: 'Provide dir option with repository path',
|
|
41
|
+
});
|
|
44
42
|
}
|
|
45
43
|
|
|
46
44
|
this.fs = fs;
|
|
@@ -65,21 +63,19 @@ export class IsomorphicGitBackend {
|
|
|
65
63
|
|
|
66
64
|
// Create .jj/repo structure for jj CLI compatibility
|
|
67
65
|
await this._createJJRepoStructure();
|
|
68
|
-
|
|
69
66
|
} catch (error) {
|
|
70
|
-
throw new JJError(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
);
|
|
67
|
+
throw new JJError('INIT_FAILED', `Failed to initialize Git repository: ${error.message}`, {
|
|
68
|
+
dir: this.dir,
|
|
69
|
+
originalError: error,
|
|
70
|
+
});
|
|
75
71
|
}
|
|
76
72
|
}
|
|
77
73
|
|
|
78
74
|
/**
|
|
79
75
|
* Read Git object from storage
|
|
80
|
-
*
|
|
76
|
+
*
|
|
81
77
|
* @param {string} oid - Git object SHA-1 hash (40-char hex)
|
|
82
|
-
* @returns {Promise<{type: string, data:
|
|
78
|
+
* @returns {Promise<{type: string, data: any}>} Git object
|
|
83
79
|
*/
|
|
84
80
|
async getObject(oid) {
|
|
85
81
|
try {
|
|
@@ -91,23 +87,21 @@ export class IsomorphicGitBackend {
|
|
|
91
87
|
return { type, data: object };
|
|
92
88
|
} catch (error) {
|
|
93
89
|
if (error.code === 'NotFoundError' || error.code === 'ReadObjectFail') {
|
|
94
|
-
throw new JJError(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
);
|
|
90
|
+
throw new JJError('NOT_FOUND', `Git object ${oid} not found`, {
|
|
91
|
+
oid,
|
|
92
|
+
suggestion: 'Ensure object exists in repository',
|
|
93
|
+
});
|
|
99
94
|
}
|
|
100
|
-
throw new JJError(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
);
|
|
95
|
+
throw new JJError('STORAGE_READ_FAILED', `Failed to read Git object: ${error.message}`, {
|
|
96
|
+
oid,
|
|
97
|
+
originalError: error,
|
|
98
|
+
});
|
|
105
99
|
}
|
|
106
100
|
}
|
|
107
101
|
|
|
108
102
|
/**
|
|
109
103
|
* Write Git object to storage
|
|
110
|
-
*
|
|
104
|
+
*
|
|
111
105
|
* @param {string} type - Object type ('blob', 'tree', 'commit', 'tag')
|
|
112
106
|
* @param {Uint8Array} data - Object data
|
|
113
107
|
* @returns {Promise<string>} Object SHA-1 hash
|
|
@@ -117,22 +111,22 @@ export class IsomorphicGitBackend {
|
|
|
117
111
|
const oid = await git.writeObject({
|
|
118
112
|
fs: this.fs,
|
|
119
113
|
dir: this.dir,
|
|
120
|
-
type,
|
|
114
|
+
type: /** @type {any} */ (type),
|
|
121
115
|
object: data,
|
|
122
116
|
});
|
|
123
117
|
return oid;
|
|
124
118
|
} catch (error) {
|
|
125
|
-
throw new JJError(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
);
|
|
119
|
+
throw new JJError('STORAGE_WRITE_FAILED', `Failed to write Git object: ${error.message}`, {
|
|
120
|
+
type,
|
|
121
|
+
dataSize: data.length,
|
|
122
|
+
originalError: error,
|
|
123
|
+
});
|
|
130
124
|
}
|
|
131
125
|
}
|
|
132
126
|
|
|
133
127
|
/**
|
|
134
128
|
* Read Git reference
|
|
135
|
-
*
|
|
129
|
+
*
|
|
136
130
|
* @param {string} name - Full ref name (e.g., 'refs/heads/main')
|
|
137
131
|
* @returns {Promise<string|null>} Commit SHA-1 or null if not found
|
|
138
132
|
*/
|
|
@@ -148,17 +142,16 @@ export class IsomorphicGitBackend {
|
|
|
148
142
|
if (error.code === 'NotFoundError' || error.code === 'ResolveRefError') {
|
|
149
143
|
return null;
|
|
150
144
|
}
|
|
151
|
-
throw new JJError(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
);
|
|
145
|
+
throw new JJError('STORAGE_READ_FAILED', `Failed to read Git ref: ${error.message}`, {
|
|
146
|
+
ref: name,
|
|
147
|
+
originalError: error,
|
|
148
|
+
});
|
|
156
149
|
}
|
|
157
150
|
}
|
|
158
151
|
|
|
159
152
|
/**
|
|
160
153
|
* Create, update, or delete Git reference
|
|
161
|
-
*
|
|
154
|
+
*
|
|
162
155
|
* @param {string} name - Full ref name
|
|
163
156
|
* @param {string|null} oid - Commit SHA-1 or null to delete
|
|
164
157
|
* @returns {Promise<void>}
|
|
@@ -187,17 +180,17 @@ export class IsomorphicGitBackend {
|
|
|
187
180
|
});
|
|
188
181
|
}
|
|
189
182
|
} catch (error) {
|
|
190
|
-
throw new JJError(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
);
|
|
183
|
+
throw new JJError('STORAGE_WRITE_FAILED', `Failed to update Git ref: ${error.message}`, {
|
|
184
|
+
ref: name,
|
|
185
|
+
oid,
|
|
186
|
+
originalError: error,
|
|
187
|
+
});
|
|
195
188
|
}
|
|
196
189
|
}
|
|
197
190
|
|
|
198
191
|
/**
|
|
199
192
|
* List Git references
|
|
200
|
-
*
|
|
193
|
+
*
|
|
201
194
|
* @param {string} [prefix=''] - Ref prefix filter
|
|
202
195
|
* @returns {Promise<Array<{name: string, oid: string}>>} List of refs
|
|
203
196
|
*/
|
|
@@ -206,22 +199,22 @@ export class IsomorphicGitBackend {
|
|
|
206
199
|
// Use expandRef to get all possible refs
|
|
207
200
|
const result = [];
|
|
208
201
|
const refDirs = ['refs/heads', 'refs/tags', 'refs/remotes'];
|
|
209
|
-
|
|
202
|
+
|
|
210
203
|
for (const refDir of refDirs) {
|
|
211
204
|
if (prefix && !refDir.startsWith(prefix) && !prefix.startsWith(refDir)) {
|
|
212
205
|
continue;
|
|
213
206
|
}
|
|
214
|
-
|
|
207
|
+
|
|
215
208
|
try {
|
|
216
209
|
const dirPath = `${this.dir}/.git/${refDir}`;
|
|
217
210
|
const entries = await this.fs.promises.readdir(dirPath, { recursive: true });
|
|
218
|
-
|
|
211
|
+
|
|
219
212
|
for (const entry of entries) {
|
|
220
213
|
const refName = `${refDir}/${entry}`;
|
|
221
214
|
if (prefix && !refName.startsWith(prefix)) {
|
|
222
215
|
continue;
|
|
223
216
|
}
|
|
224
|
-
|
|
217
|
+
|
|
225
218
|
try {
|
|
226
219
|
const oid = await this.readRef(refName);
|
|
227
220
|
if (oid) {
|
|
@@ -235,14 +228,13 @@ export class IsomorphicGitBackend {
|
|
|
235
228
|
// Directory doesn't exist, skip
|
|
236
229
|
}
|
|
237
230
|
}
|
|
238
|
-
|
|
231
|
+
|
|
239
232
|
return result.sort((a, b) => a.name.localeCompare(b.name));
|
|
240
233
|
} catch (error) {
|
|
241
|
-
throw new JJError(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
);
|
|
234
|
+
throw new JJError('STORAGE_READ_FAILED', `Failed to list Git refs: ${error.message}`, {
|
|
235
|
+
prefix,
|
|
236
|
+
originalError: error,
|
|
237
|
+
});
|
|
246
238
|
}
|
|
247
239
|
}
|
|
248
240
|
|
|
@@ -255,7 +247,7 @@ export class IsomorphicGitBackend {
|
|
|
255
247
|
* @param {string} opts.author.name - Author name
|
|
256
248
|
* @param {string} opts.author.email - Author email
|
|
257
249
|
* @param {number} [opts.author.timestamp] - Author timestamp (milliseconds since epoch)
|
|
258
|
-
* @param {
|
|
250
|
+
* @param {{name: string, email: string, timestamp?: number}} [opts.committer] - Committer info (defaults to author)
|
|
259
251
|
* @param {string[]} [opts.parents=[]] - Parent commit SHAs
|
|
260
252
|
* @returns {Promise<string>} Commit SHA
|
|
261
253
|
*/
|
|
@@ -264,16 +256,22 @@ export class IsomorphicGitBackend {
|
|
|
264
256
|
const author = {
|
|
265
257
|
name: opts.author.name,
|
|
266
258
|
email: opts.author.email,
|
|
267
|
-
timestamp: opts.author.timestamp
|
|
259
|
+
timestamp: opts.author.timestamp
|
|
260
|
+
? Math.floor(opts.author.timestamp / 1000)
|
|
261
|
+
: Math.floor(Date.now() / 1000),
|
|
268
262
|
timezoneOffset: 0,
|
|
269
263
|
};
|
|
270
264
|
|
|
271
|
-
const committer = opts.committer
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
265
|
+
const committer = opts.committer
|
|
266
|
+
? {
|
|
267
|
+
name: opts.committer.name,
|
|
268
|
+
email: opts.committer.email,
|
|
269
|
+
timestamp: opts.committer.timestamp
|
|
270
|
+
? Math.floor(opts.committer.timestamp / 1000)
|
|
271
|
+
: author.timestamp,
|
|
272
|
+
timezoneOffset: 0,
|
|
273
|
+
}
|
|
274
|
+
: author;
|
|
277
275
|
|
|
278
276
|
const commitSha = await git.commit({
|
|
279
277
|
fs: this.fs,
|
|
@@ -286,34 +284,84 @@ export class IsomorphicGitBackend {
|
|
|
286
284
|
|
|
287
285
|
return commitSha;
|
|
288
286
|
} catch (error) {
|
|
289
|
-
throw new JJError(
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
);
|
|
287
|
+
throw new JJError('COMMIT_FAILED', `Failed to create Git commit: ${error.message}`, {
|
|
288
|
+
message: opts.message,
|
|
289
|
+
originalError: error,
|
|
290
|
+
});
|
|
294
291
|
}
|
|
295
292
|
}
|
|
296
293
|
|
|
297
294
|
/**
|
|
298
|
-
* Get the current tree SHA for the
|
|
295
|
+
* Get the current tree SHA for the staging area (git index).
|
|
296
|
+
*
|
|
297
|
+
* `git.writeTree()` writes exactly the flat `TreeObject` array it's given —
|
|
298
|
+
* it does not build one from the working directory or index. To get "the
|
|
299
|
+
* tree that would be committed right now", walk the index (STAGE) to
|
|
300
|
+
* collect every staged blob's path/oid/mode, group them into a nested
|
|
301
|
+
* directory structure, and write each subtree bottom-up.
|
|
299
302
|
*
|
|
300
303
|
* @returns {Promise<string>} Tree SHA
|
|
301
304
|
*/
|
|
302
305
|
async getCurrentTree() {
|
|
303
306
|
try {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
307
|
+
/** @type {Array<{path: string, oid: string, mode: number}>} */
|
|
308
|
+
const flatEntries = (
|
|
309
|
+
await git.walk({
|
|
310
|
+
fs: this.fs,
|
|
311
|
+
dir: this.dir,
|
|
312
|
+
trees: [git.STAGE()],
|
|
313
|
+
map: async (filepath, [stage]) => {
|
|
314
|
+
if (!stage || filepath === '.') return undefined;
|
|
315
|
+
const type = await stage.type();
|
|
316
|
+
if (type !== 'blob') return undefined; // directory marker, not a leaf
|
|
317
|
+
return { path: filepath, oid: await stage.oid(), mode: await stage.mode() };
|
|
318
|
+
},
|
|
319
|
+
})
|
|
320
|
+
).filter(Boolean);
|
|
321
|
+
|
|
322
|
+
// Group the flat, index-relative paths into a nested tree of
|
|
323
|
+
// directories, each holding its immediate file/subdirectory children.
|
|
324
|
+
/** @type {{dirs: Map<string, any>, files: Array<{name: string, oid: string, mode: string}>}} */
|
|
325
|
+
const root = { dirs: new Map(), files: [] };
|
|
326
|
+
for (const entry of flatEntries) {
|
|
327
|
+
const parts = entry.path.split('/');
|
|
328
|
+
let node = root;
|
|
329
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
330
|
+
const segment = parts[i];
|
|
331
|
+
if (!node.dirs.has(segment)) {
|
|
332
|
+
node.dirs.set(segment, { dirs: new Map(), files: [] });
|
|
333
|
+
}
|
|
334
|
+
node = node.dirs.get(segment);
|
|
335
|
+
}
|
|
336
|
+
node.files.push({
|
|
337
|
+
name: parts[parts.length - 1],
|
|
338
|
+
oid: entry.oid,
|
|
339
|
+
// isomorphic-git expects an octal mode STRING (e.g. "100644"), but
|
|
340
|
+
// the walker returns the mode as a decimal number.
|
|
341
|
+
mode: entry.mode.toString(8),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const writeDir = async (/** @type {any} */ node) => {
|
|
346
|
+
/** @type {any[]} */
|
|
347
|
+
const treeEntries = [];
|
|
348
|
+
for (const [name, child] of node.dirs.entries()) {
|
|
349
|
+
const oid = await writeDir(child);
|
|
350
|
+
treeEntries.push({ mode: '040000', path: name, oid, type: 'tree' });
|
|
351
|
+
}
|
|
352
|
+
for (const file of node.files) {
|
|
353
|
+
treeEntries.push({ mode: file.mode, path: file.name, oid: file.oid, type: 'blob' });
|
|
354
|
+
}
|
|
355
|
+
return await git.writeTree(
|
|
356
|
+
/** @type {any} */ ({ fs: this.fs, dir: this.dir, tree: treeEntries })
|
|
357
|
+
);
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
return await writeDir(root);
|
|
311
361
|
} catch (error) {
|
|
312
|
-
throw new JJError(
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
{ originalError: error }
|
|
316
|
-
);
|
|
362
|
+
throw new JJError('TREE_READ_FAILED', `Failed to get current tree: ${error.message}`, {
|
|
363
|
+
originalError: error,
|
|
364
|
+
});
|
|
317
365
|
}
|
|
318
366
|
}
|
|
319
367
|
|
|
@@ -347,11 +395,16 @@ export class IsomorphicGitBackend {
|
|
|
347
395
|
}
|
|
348
396
|
}
|
|
349
397
|
} catch (error) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
)
|
|
398
|
+
// A per-file STAGE_FILE_FAILED (thrown above) is already categorized —
|
|
399
|
+
// re-throw it as-is instead of losing its specific code by re-wrapping
|
|
400
|
+
// it in the more generic STAGE_FAILED (which is reserved for failures
|
|
401
|
+
// in _getAllFiles() itself, e.g. an unreadable directory).
|
|
402
|
+
if (error instanceof JJError) {
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
throw new JJError('STAGE_FAILED', `Failed to stage files: ${error.message}`, {
|
|
406
|
+
originalError: error,
|
|
407
|
+
});
|
|
355
408
|
}
|
|
356
409
|
}
|
|
357
410
|
|
|
@@ -414,9 +467,9 @@ export class IsomorphicGitBackend {
|
|
|
414
467
|
* @param {boolean} [opts.relative] - Depth measured from current shallow depth (v0.4)
|
|
415
468
|
* @param {boolean} [opts.singleBranch] - Only fetch single branch (v0.4)
|
|
416
469
|
* @param {boolean} [opts.noTags] - Don't fetch tags (v0.4)
|
|
417
|
-
* @param {
|
|
418
|
-
* @param {
|
|
419
|
-
* @returns {Promise<{fetchedRefs: Array
|
|
470
|
+
* @param {any} [opts.onProgress] - Progress callback
|
|
471
|
+
* @param {any} [opts.onAuth] - Authentication callback
|
|
472
|
+
* @returns {Promise<{fetchedRefs: Array<any>, updatedRefs: Array<any>}>} Fetch result
|
|
420
473
|
*/
|
|
421
474
|
async fetch(opts) {
|
|
422
475
|
if (!this.http) {
|
|
@@ -440,8 +493,8 @@ export class IsomorphicGitBackend {
|
|
|
440
493
|
dir: this.dir,
|
|
441
494
|
remote: opts.remote,
|
|
442
495
|
ref,
|
|
443
|
-
depth: opts.depth,
|
|
444
|
-
relative: opts.relative,
|
|
496
|
+
depth: opts.depth, // v0.4: Shallow clone support
|
|
497
|
+
relative: opts.relative, // v0.4: Relative depth
|
|
445
498
|
singleBranch: opts.singleBranch, // v0.4: Single branch only
|
|
446
499
|
tags: opts.noTags === true ? false : undefined, // v0.4: Skip tags
|
|
447
500
|
onProgress: opts.onProgress,
|
|
@@ -460,37 +513,34 @@ export class IsomorphicGitBackend {
|
|
|
460
513
|
return { fetchedRefs, updatedRefs };
|
|
461
514
|
} catch (error) {
|
|
462
515
|
if (error.code === 'HttpError' || error.code === 'NetworkError') {
|
|
463
|
-
throw new JJError(
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
);
|
|
516
|
+
throw new JJError('NETWORK_ERROR', `Network error during fetch: ${error.message}`, {
|
|
517
|
+
remote: opts.remote,
|
|
518
|
+
originalError: error,
|
|
519
|
+
});
|
|
468
520
|
}
|
|
469
521
|
if (error.code === 'AuthError') {
|
|
470
|
-
throw new JJError(
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
);
|
|
522
|
+
throw new JJError('AUTH_FAILED', `Authentication failed: ${error.message}`, {
|
|
523
|
+
remote: opts.remote,
|
|
524
|
+
originalError: error,
|
|
525
|
+
});
|
|
475
526
|
}
|
|
476
|
-
throw new JJError(
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
);
|
|
527
|
+
throw new JJError('FETCH_FAILED', `Fetch failed: ${error.message}`, {
|
|
528
|
+
remote: opts.remote,
|
|
529
|
+
originalError: error,
|
|
530
|
+
});
|
|
481
531
|
}
|
|
482
532
|
}
|
|
483
533
|
|
|
484
534
|
/**
|
|
485
535
|
* Push objects and refs to remote repository
|
|
486
|
-
*
|
|
536
|
+
*
|
|
487
537
|
* @param {Object} opts - Push options
|
|
488
538
|
* @param {string} opts.remote - Remote name or URL
|
|
489
539
|
* @param {string[]} [opts.refs] - Ref specs to push
|
|
490
540
|
* @param {boolean} [opts.force] - Allow non-fast-forward
|
|
491
|
-
* @param {
|
|
492
|
-
* @param {
|
|
493
|
-
* @returns {Promise<{pushedRefs: Array
|
|
541
|
+
* @param {any} [opts.onProgress] - Progress callback
|
|
542
|
+
* @param {any} [opts.onAuth] - Authentication callback
|
|
543
|
+
* @returns {Promise<{pushedRefs: Array<any>, rejectedRefs: Array<any>}>} Push result
|
|
494
544
|
*/
|
|
495
545
|
async push(opts) {
|
|
496
546
|
if (!this.http) {
|
|
@@ -534,31 +584,28 @@ export class IsomorphicGitBackend {
|
|
|
534
584
|
return { pushedRefs, rejectedRefs };
|
|
535
585
|
} catch (error) {
|
|
536
586
|
if (error.code === 'HttpError' || error.code === 'NetworkError') {
|
|
537
|
-
throw new JJError(
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
);
|
|
587
|
+
throw new JJError('NETWORK_ERROR', `Network error during push: ${error.message}`, {
|
|
588
|
+
remote: opts.remote,
|
|
589
|
+
originalError: error,
|
|
590
|
+
});
|
|
542
591
|
}
|
|
543
592
|
if (error.code === 'AuthError') {
|
|
544
|
-
throw new JJError(
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
);
|
|
593
|
+
throw new JJError('AUTH_FAILED', `Authentication failed: ${error.message}`, {
|
|
594
|
+
remote: opts.remote,
|
|
595
|
+
originalError: error,
|
|
596
|
+
});
|
|
549
597
|
}
|
|
550
598
|
if (error.code === 'PushRejectedError') {
|
|
551
|
-
throw new JJError(
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
);
|
|
599
|
+
throw new JJError('PUSH_REJECTED', `Push rejected (non-fast-forward): ${error.message}`, {
|
|
600
|
+
remote: opts.remote,
|
|
601
|
+
suggestion: 'Use force option or pull first',
|
|
602
|
+
originalError: error,
|
|
603
|
+
});
|
|
556
604
|
}
|
|
557
|
-
throw new JJError(
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
);
|
|
605
|
+
throw new JJError('PUSH_FAILED', `Push failed: ${error.message}`, {
|
|
606
|
+
remote: opts.remote,
|
|
607
|
+
originalError: error,
|
|
608
|
+
});
|
|
562
609
|
}
|
|
563
610
|
}
|
|
564
611
|
|
|
@@ -637,17 +684,17 @@ export class IsomorphicGitBackend {
|
|
|
637
684
|
const metadata = {
|
|
638
685
|
start_time: {
|
|
639
686
|
millis_since_epoch: Date.now(),
|
|
640
|
-
tz_offset: new Date().getTimezoneOffset()
|
|
687
|
+
tz_offset: new Date().getTimezoneOffset(),
|
|
641
688
|
},
|
|
642
689
|
end_time: {
|
|
643
690
|
millis_since_epoch: Date.now(),
|
|
644
|
-
tz_offset: new Date().getTimezoneOffset()
|
|
691
|
+
tz_offset: new Date().getTimezoneOffset(),
|
|
645
692
|
},
|
|
646
693
|
description: 'initialize repo',
|
|
647
694
|
hostname: 'localhost',
|
|
648
695
|
username: 'user',
|
|
649
696
|
is_snapshot: false,
|
|
650
|
-
tags: {}
|
|
697
|
+
tags: {},
|
|
651
698
|
};
|
|
652
699
|
await opStore.writeOperation(operationId, viewId, [], metadata);
|
|
653
700
|
|
|
@@ -676,7 +723,7 @@ export class IsomorphicGitBackend {
|
|
|
676
723
|
|
|
677
724
|
// Check if .jj is already in .gitignore
|
|
678
725
|
const lines = content.split('\n');
|
|
679
|
-
const hasJJ = lines.some(line => line.trim() === '.jj' || line.trim() === '.jj/');
|
|
726
|
+
const hasJJ = lines.some((line) => line.trim() === '.jj' || line.trim() === '.jj/');
|
|
680
727
|
|
|
681
728
|
if (!hasJJ) {
|
|
682
729
|
// Add .jj to .gitignore
|
|
@@ -44,11 +44,7 @@ export class LazyGitBackend extends IsomorphicGitBackend {
|
|
|
44
44
|
async readBlob(oid) {
|
|
45
45
|
// Validate OID format
|
|
46
46
|
if (!/^[0-9a-f]{40}$/.test(oid)) {
|
|
47
|
-
throw new JJError(
|
|
48
|
-
'INVALID_OID',
|
|
49
|
-
`Invalid object ID format: ${oid}`,
|
|
50
|
-
{ oid }
|
|
51
|
-
);
|
|
47
|
+
throw new JJError('INVALID_OID', `Invalid object ID format: ${oid}`, { oid });
|
|
52
48
|
}
|
|
53
49
|
|
|
54
50
|
try {
|
|
@@ -60,19 +56,15 @@ export class LazyGitBackend extends IsomorphicGitBackend {
|
|
|
60
56
|
// Track as missing BEFORE attempting fetch
|
|
61
57
|
this.missingObjects.add(oid);
|
|
62
58
|
|
|
63
|
-
// Only fetch if we haven't already tried
|
|
59
|
+
// Only fetch if we haven't already tried. If the fetch (or the retry
|
|
60
|
+
// read) fails, the error propagates directly — the object is already
|
|
61
|
+
// tracked as missing above.
|
|
64
62
|
if (!this.fetchedObjects.has(oid)) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
this.fetchedObjects.add(oid);
|
|
63
|
+
await this._fetchObject(oid);
|
|
64
|
+
this.fetchedObjects.add(oid);
|
|
68
65
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
} catch (fetchError) {
|
|
72
|
-
// Fetch failed, but we still track as missing
|
|
73
|
-
// Re-throw the fetch error
|
|
74
|
-
throw fetchError;
|
|
75
|
-
}
|
|
66
|
+
// Try reading again after fetch
|
|
67
|
+
return await this._gitReadBlob(oid);
|
|
76
68
|
}
|
|
77
69
|
}
|
|
78
70
|
|
|
@@ -164,9 +156,10 @@ export class LazyGitBackend extends IsomorphicGitBackend {
|
|
|
164
156
|
lazyLoad: this.lazyLoad,
|
|
165
157
|
missingObjects: this.missingObjects.size,
|
|
166
158
|
fetchedObjects: this.fetchedObjects.size,
|
|
167
|
-
hitRate:
|
|
168
|
-
|
|
169
|
-
|
|
159
|
+
hitRate:
|
|
160
|
+
this.fetchedObjects.size > 0
|
|
161
|
+
? (this.fetchedObjects.size - this.missingObjects.size) / this.fetchedObjects.size
|
|
162
|
+
: 0,
|
|
170
163
|
};
|
|
171
164
|
}
|
|
172
165
|
|
package/src/browser/helpers.js
CHANGED
|
@@ -51,7 +51,8 @@ export function createBrowserFS(opts = {}) {
|
|
|
51
51
|
} catch (error) {
|
|
52
52
|
throw new Error(
|
|
53
53
|
'LightningFS not found. Install it with: npm install @isomorphic-git/lightning-fs\n' +
|
|
54
|
-
|
|
54
|
+
'Original error: ' +
|
|
55
|
+
error.message
|
|
55
56
|
);
|
|
56
57
|
}
|
|
57
58
|
}
|
|
@@ -182,7 +183,10 @@ export function detectCapabilities() {
|
|
|
182
183
|
environment: 'browser',
|
|
183
184
|
indexedDB: typeof indexedDB !== 'undefined',
|
|
184
185
|
serviceWorker: typeof navigator !== 'undefined' && 'serviceWorker' in navigator,
|
|
185
|
-
persistentStorage:
|
|
186
|
+
persistentStorage:
|
|
187
|
+
typeof navigator !== 'undefined' &&
|
|
188
|
+
navigator.storage &&
|
|
189
|
+
typeof navigator.storage.persist === 'function',
|
|
186
190
|
sharedArrayBuffer: typeof SharedArrayBuffer !== 'undefined',
|
|
187
191
|
webWorker: typeof Worker !== 'undefined',
|
|
188
192
|
};
|