isomorphic-jj 1.8.0 → 1.9.2
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 +371 -0
- package/LICENSE +1 -1
- package/README.md +117 -30
- package/dist/index.cjs +2225 -419
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2225 -418
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -7
- package/src/api/repository.js +862 -161
- package/src/backend/isomorphic-git-backend.js +13 -12
- package/src/browser/helpers.js +38 -16
- package/src/browser/index.d.ts +9 -2
- package/src/core/change-graph.js +125 -4
- package/src/core/conflict-model.js +72 -7
- package/src/core/jj-checkout.js +5 -16
- package/src/core/jj-operation-store.js +6 -17
- package/src/core/jj-tree-state.js +5 -16
- package/src/core/jj-view-store.js +6 -17
- package/src/core/merge-driver-registry.js +11 -10
- package/src/core/revset-engine.js +127 -7
- package/src/core/storage-manager.js +9 -8
- package/src/core/tag-store.js +2 -5
- package/src/core/working-copy.js +51 -4
- package/src/core/workspace-manager.js +4 -3
- package/src/protos/local_working_copy.json.js +146 -0
- package/src/protos/schema.js +49 -0
- package/src/protos/simple_op_store.json.js +374 -0
- package/src/types.d.ts +33 -0
- package/src/utils/atomic-write.js +3 -1
- package/src/utils/bytes.js +67 -0
- package/src/utils/id-generation.js +18 -4
- package/src/utils/mkdirp.js +41 -0
- package/src/utils/posix-path.js +117 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Precompiled protobuf reflection JSON for simple_op_store.proto
|
|
3
|
+
*
|
|
4
|
+
* Generated from src/protos/simple_op_store.proto by scripts/build-protos.mjs. This
|
|
5
|
+
* is bundled directly (instead of loaded from a filesystem path at runtime)
|
|
6
|
+
* so that protobuf schema loading works identically in Node and in browser
|
|
7
|
+
* bundles — protobuf.load() requires filesystem/network access that does
|
|
8
|
+
* not exist in a browser bundle (see issue #28). Regenerate with:
|
|
9
|
+
* node scripts/build-protos.mjs
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export default {
|
|
13
|
+
nested: {
|
|
14
|
+
simple_op_store: {
|
|
15
|
+
nested: {
|
|
16
|
+
RefConflictLegacy: {
|
|
17
|
+
fields: {
|
|
18
|
+
removes: {
|
|
19
|
+
rule: 'repeated',
|
|
20
|
+
type: 'bytes',
|
|
21
|
+
id: 1,
|
|
22
|
+
options: {
|
|
23
|
+
deprecated: true,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
adds: {
|
|
27
|
+
rule: 'repeated',
|
|
28
|
+
type: 'bytes',
|
|
29
|
+
id: 2,
|
|
30
|
+
options: {
|
|
31
|
+
deprecated: true,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
RefConflict: {
|
|
37
|
+
fields: {
|
|
38
|
+
removes: {
|
|
39
|
+
rule: 'repeated',
|
|
40
|
+
type: 'Term',
|
|
41
|
+
id: 1,
|
|
42
|
+
},
|
|
43
|
+
adds: {
|
|
44
|
+
rule: 'repeated',
|
|
45
|
+
type: 'Term',
|
|
46
|
+
id: 2,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
nested: {
|
|
50
|
+
Term: {
|
|
51
|
+
oneofs: {
|
|
52
|
+
_value: {
|
|
53
|
+
oneof: ['value'],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
fields: {
|
|
57
|
+
value: {
|
|
58
|
+
type: 'bytes',
|
|
59
|
+
id: 1,
|
|
60
|
+
options: {
|
|
61
|
+
proto3_optional: true,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
RefTarget: {
|
|
69
|
+
oneofs: {
|
|
70
|
+
value: {
|
|
71
|
+
oneof: ['commitId', 'conflictLegacy', 'conflict'],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
fields: {
|
|
75
|
+
commitId: {
|
|
76
|
+
type: 'bytes',
|
|
77
|
+
id: 1,
|
|
78
|
+
options: {
|
|
79
|
+
deprecated: true,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
conflictLegacy: {
|
|
83
|
+
type: 'RefConflictLegacy',
|
|
84
|
+
id: 2,
|
|
85
|
+
options: {
|
|
86
|
+
deprecated: true,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
conflict: {
|
|
90
|
+
type: 'RefConflict',
|
|
91
|
+
id: 3,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
RefTargetTerm: {
|
|
96
|
+
oneofs: {
|
|
97
|
+
_value: {
|
|
98
|
+
oneof: ['value'],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
fields: {
|
|
102
|
+
value: {
|
|
103
|
+
type: 'bytes',
|
|
104
|
+
id: 1,
|
|
105
|
+
options: {
|
|
106
|
+
proto3_optional: true,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
RemoteRefState: {
|
|
112
|
+
values: {
|
|
113
|
+
New: 0,
|
|
114
|
+
Tracked: 1,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
RemoteBookmark: {
|
|
118
|
+
oneofs: {
|
|
119
|
+
_state: {
|
|
120
|
+
oneof: ['state'],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
fields: {
|
|
124
|
+
remoteName: {
|
|
125
|
+
type: 'string',
|
|
126
|
+
id: 1,
|
|
127
|
+
},
|
|
128
|
+
target: {
|
|
129
|
+
type: 'RefTarget',
|
|
130
|
+
id: 2,
|
|
131
|
+
},
|
|
132
|
+
state: {
|
|
133
|
+
type: 'RemoteRefState',
|
|
134
|
+
id: 3,
|
|
135
|
+
options: {
|
|
136
|
+
proto3_optional: true,
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
Bookmark: {
|
|
142
|
+
fields: {
|
|
143
|
+
name: {
|
|
144
|
+
type: 'string',
|
|
145
|
+
id: 1,
|
|
146
|
+
},
|
|
147
|
+
localTarget: {
|
|
148
|
+
type: 'RefTarget',
|
|
149
|
+
id: 2,
|
|
150
|
+
},
|
|
151
|
+
remoteBookmarks: {
|
|
152
|
+
rule: 'repeated',
|
|
153
|
+
type: 'RemoteBookmark',
|
|
154
|
+
id: 3,
|
|
155
|
+
options: {
|
|
156
|
+
deprecated: true,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
GitRef: {
|
|
162
|
+
fields: {
|
|
163
|
+
name: {
|
|
164
|
+
type: 'string',
|
|
165
|
+
id: 1,
|
|
166
|
+
},
|
|
167
|
+
commitId: {
|
|
168
|
+
type: 'bytes',
|
|
169
|
+
id: 2,
|
|
170
|
+
options: {
|
|
171
|
+
deprecated: true,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
target: {
|
|
175
|
+
type: 'RefTarget',
|
|
176
|
+
id: 3,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
RemoteRef: {
|
|
181
|
+
fields: {
|
|
182
|
+
name: {
|
|
183
|
+
type: 'string',
|
|
184
|
+
id: 1,
|
|
185
|
+
},
|
|
186
|
+
targetTerms: {
|
|
187
|
+
rule: 'repeated',
|
|
188
|
+
type: 'RefTargetTerm',
|
|
189
|
+
id: 2,
|
|
190
|
+
},
|
|
191
|
+
state: {
|
|
192
|
+
type: 'RemoteRefState',
|
|
193
|
+
id: 3,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
Tag: {
|
|
198
|
+
fields: {
|
|
199
|
+
name: {
|
|
200
|
+
type: 'string',
|
|
201
|
+
id: 1,
|
|
202
|
+
},
|
|
203
|
+
target: {
|
|
204
|
+
type: 'RefTarget',
|
|
205
|
+
id: 2,
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
View: {
|
|
210
|
+
fields: {
|
|
211
|
+
headIds: {
|
|
212
|
+
rule: 'repeated',
|
|
213
|
+
type: 'bytes',
|
|
214
|
+
id: 1,
|
|
215
|
+
},
|
|
216
|
+
wcCommitId: {
|
|
217
|
+
type: 'bytes',
|
|
218
|
+
id: 2,
|
|
219
|
+
options: {
|
|
220
|
+
deprecated: true,
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
wcCommitIds: {
|
|
224
|
+
keyType: 'string',
|
|
225
|
+
type: 'bytes',
|
|
226
|
+
id: 8,
|
|
227
|
+
},
|
|
228
|
+
bookmarks: {
|
|
229
|
+
rule: 'repeated',
|
|
230
|
+
type: 'Bookmark',
|
|
231
|
+
id: 5,
|
|
232
|
+
},
|
|
233
|
+
localTags: {
|
|
234
|
+
rule: 'repeated',
|
|
235
|
+
type: 'Tag',
|
|
236
|
+
id: 6,
|
|
237
|
+
},
|
|
238
|
+
remoteViews: {
|
|
239
|
+
rule: 'repeated',
|
|
240
|
+
type: 'RemoteView',
|
|
241
|
+
id: 11,
|
|
242
|
+
},
|
|
243
|
+
gitRefs: {
|
|
244
|
+
rule: 'repeated',
|
|
245
|
+
type: 'GitRef',
|
|
246
|
+
id: 3,
|
|
247
|
+
},
|
|
248
|
+
gitHeadLegacy: {
|
|
249
|
+
type: 'bytes',
|
|
250
|
+
id: 7,
|
|
251
|
+
options: {
|
|
252
|
+
deprecated: true,
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
gitHead: {
|
|
256
|
+
type: 'RefTarget',
|
|
257
|
+
id: 9,
|
|
258
|
+
},
|
|
259
|
+
hasGitRefsMigratedToRemoteTags: {
|
|
260
|
+
type: 'bool',
|
|
261
|
+
id: 12,
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
reserved: [
|
|
265
|
+
[4, 4],
|
|
266
|
+
[10, 10],
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
RemoteView: {
|
|
270
|
+
fields: {
|
|
271
|
+
name: {
|
|
272
|
+
type: 'string',
|
|
273
|
+
id: 1,
|
|
274
|
+
},
|
|
275
|
+
bookmarks: {
|
|
276
|
+
rule: 'repeated',
|
|
277
|
+
type: 'RemoteRef',
|
|
278
|
+
id: 2,
|
|
279
|
+
},
|
|
280
|
+
tags: {
|
|
281
|
+
rule: 'repeated',
|
|
282
|
+
type: 'RemoteRef',
|
|
283
|
+
id: 3,
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
Operation: {
|
|
288
|
+
fields: {
|
|
289
|
+
viewId: {
|
|
290
|
+
type: 'bytes',
|
|
291
|
+
id: 1,
|
|
292
|
+
},
|
|
293
|
+
parents: {
|
|
294
|
+
rule: 'repeated',
|
|
295
|
+
type: 'bytes',
|
|
296
|
+
id: 2,
|
|
297
|
+
},
|
|
298
|
+
metadata: {
|
|
299
|
+
type: 'OperationMetadata',
|
|
300
|
+
id: 3,
|
|
301
|
+
},
|
|
302
|
+
commitPredecessors: {
|
|
303
|
+
rule: 'repeated',
|
|
304
|
+
type: 'CommitPredecessors',
|
|
305
|
+
id: 4,
|
|
306
|
+
},
|
|
307
|
+
storesCommitPredecessors: {
|
|
308
|
+
type: 'bool',
|
|
309
|
+
id: 5,
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
Timestamp: {
|
|
314
|
+
fields: {
|
|
315
|
+
millisSinceEpoch: {
|
|
316
|
+
type: 'int64',
|
|
317
|
+
id: 1,
|
|
318
|
+
},
|
|
319
|
+
tzOffset: {
|
|
320
|
+
type: 'int32',
|
|
321
|
+
id: 2,
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
OperationMetadata: {
|
|
326
|
+
fields: {
|
|
327
|
+
startTime: {
|
|
328
|
+
type: 'Timestamp',
|
|
329
|
+
id: 1,
|
|
330
|
+
},
|
|
331
|
+
endTime: {
|
|
332
|
+
type: 'Timestamp',
|
|
333
|
+
id: 2,
|
|
334
|
+
},
|
|
335
|
+
description: {
|
|
336
|
+
type: 'string',
|
|
337
|
+
id: 3,
|
|
338
|
+
},
|
|
339
|
+
hostname: {
|
|
340
|
+
type: 'string',
|
|
341
|
+
id: 4,
|
|
342
|
+
},
|
|
343
|
+
username: {
|
|
344
|
+
type: 'string',
|
|
345
|
+
id: 5,
|
|
346
|
+
},
|
|
347
|
+
isSnapshot: {
|
|
348
|
+
type: 'bool',
|
|
349
|
+
id: 7,
|
|
350
|
+
},
|
|
351
|
+
tags: {
|
|
352
|
+
keyType: 'string',
|
|
353
|
+
type: 'string',
|
|
354
|
+
id: 6,
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
CommitPredecessors: {
|
|
359
|
+
fields: {
|
|
360
|
+
commitId: {
|
|
361
|
+
type: 'bytes',
|
|
362
|
+
id: 1,
|
|
363
|
+
},
|
|
364
|
+
predecessorIds: {
|
|
365
|
+
rule: 'repeated',
|
|
366
|
+
type: 'bytes',
|
|
367
|
+
id: 2,
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
};
|
package/src/types.d.ts
CHANGED
|
@@ -481,6 +481,26 @@ export interface UnabandonArgs {
|
|
|
481
481
|
changeId: ChangeID;
|
|
482
482
|
}
|
|
483
483
|
|
|
484
|
+
/**
|
|
485
|
+
* Converge (auto-resolve divergent copies) arguments
|
|
486
|
+
*/
|
|
487
|
+
export interface ConvergeArgs {
|
|
488
|
+
changeId: ChangeID;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Converge result
|
|
493
|
+
*/
|
|
494
|
+
export interface ConvergeResult {
|
|
495
|
+
changeId: ChangeID;
|
|
496
|
+
/** Whether the divergent copies were successfully merged into one. */
|
|
497
|
+
resolved: boolean;
|
|
498
|
+
/** Per-path conflicts found — non-empty only when `resolved` is false. */
|
|
499
|
+
conflicts: Conflict[];
|
|
500
|
+
/** The resulting commit id, present only when `resolved` is true. */
|
|
501
|
+
commitId?: string;
|
|
502
|
+
}
|
|
503
|
+
|
|
484
504
|
/**
|
|
485
505
|
* Log query options
|
|
486
506
|
*/
|
|
@@ -1064,6 +1084,19 @@ export interface JJ {
|
|
|
1064
1084
|
unabandon(args: UnabandonArgs): Promise<void>;
|
|
1065
1085
|
absorb(args?: AbsorbArgs): Promise<AbsorbResult>;
|
|
1066
1086
|
|
|
1087
|
+
/**
|
|
1088
|
+
* Auto-resolve divergent copies of a change — multiple visible commits
|
|
1089
|
+
* sharing one change id (see the `divergent()` revset) — by three-way
|
|
1090
|
+
* merging them (matches `jj converge`, jj v0.45.0).
|
|
1091
|
+
*
|
|
1092
|
+
* A genuine per-path conflict is reported as data (`resolved: false`),
|
|
1093
|
+
* matching merge()'s own convention, rather than thrown. Throws when
|
|
1094
|
+
* there's nothing to converge, or when more than two visible copies
|
|
1095
|
+
* exist (automatic resolution only attempts a pairwise merge — matches
|
|
1096
|
+
* real jj aborting non-interactive resolution it can't disambiguate).
|
|
1097
|
+
*/
|
|
1098
|
+
converge(args: ConvergeArgs | ChangeID): Promise<ConvergeResult>;
|
|
1099
|
+
|
|
1067
1100
|
/**
|
|
1068
1101
|
* Create a change that reverses another change (matches `jj revert`, v1.5).
|
|
1069
1102
|
* Canonical replacement for the deprecated `backout()`.
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* calling `writeFile()` directly on the canonical path — see issue #16.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { mkdirp } from './mkdirp.js';
|
|
16
|
+
|
|
15
17
|
/**
|
|
16
18
|
* @param {any} fs - Filesystem implementation (Node fs, LightningFS, etc.)
|
|
17
19
|
* @param {string} targetPath - Canonical path to write to
|
|
@@ -27,7 +29,7 @@ export async function atomicWriteFile(fs, targetPath, content) {
|
|
|
27
29
|
try {
|
|
28
30
|
const dirPath = targetPath.substring(0, targetPath.lastIndexOf('/'));
|
|
29
31
|
if (dirPath) {
|
|
30
|
-
await fs
|
|
32
|
+
await mkdirp(fs, dirPath);
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
await fs.promises.writeFile(tmpPath, content);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-environment byte/string conversion helpers.
|
|
3
|
+
*
|
|
4
|
+
* Why not just Node's `Buffer`: it's a Node global that most browser
|
|
5
|
+
* bundlers (Vite, esbuild, webpack 5+) do not polyfill by default, so code
|
|
6
|
+
* that calls `Buffer.from(...)` compiles fine but throws `ReferenceError:
|
|
7
|
+
* Buffer is not defined` at runtime in a browser bundle (see issue #28).
|
|
8
|
+
*
|
|
9
|
+
* `utf8Encode`/`utf8Decode` still prefer the real `Buffer` when it's present
|
|
10
|
+
* (i.e. under Node) so existing behavior — including `.toString()` producing
|
|
11
|
+
* the original text back, which a plain `Uint8Array` cannot do — is
|
|
12
|
+
* unchanged there; `typeof Buffer` is safe to evaluate even where `Buffer`
|
|
13
|
+
* was never declared (unlike referencing it directly), so this doesn't
|
|
14
|
+
* reintroduce the crash outside Node. `hexToBytes`/`bytesToHex`/`isBytes`
|
|
15
|
+
* only ever need `Uint8Array`, so they don't need the same branch.
|
|
16
|
+
*/
|
|
17
|
+
const hasBuffer = typeof Buffer !== 'undefined';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} hex - Even-length hex string
|
|
21
|
+
* @returns {Uint8Array}
|
|
22
|
+
*/
|
|
23
|
+
export function hexToBytes(hex) {
|
|
24
|
+
const length = hex.length / 2;
|
|
25
|
+
const bytes = new Uint8Array(length);
|
|
26
|
+
for (let i = 0; i < length; i++) {
|
|
27
|
+
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
|
|
28
|
+
}
|
|
29
|
+
return bytes;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {Uint8Array} bytes
|
|
34
|
+
* @returns {string} Lowercase hex string
|
|
35
|
+
*/
|
|
36
|
+
export function bytesToHex(bytes) {
|
|
37
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} str
|
|
42
|
+
* @returns {Uint8Array} UTF-8 encoded bytes (a real `Buffer` under Node)
|
|
43
|
+
*/
|
|
44
|
+
export function utf8Encode(str) {
|
|
45
|
+
return hasBuffer ? Buffer.from(str, 'utf-8') : new TextEncoder().encode(str);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {Uint8Array} bytes
|
|
50
|
+
* @returns {string} UTF-8 decoded string
|
|
51
|
+
*/
|
|
52
|
+
export function utf8Decode(bytes) {
|
|
53
|
+
return hasBuffer ? Buffer.from(bytes).toString('utf-8') : new TextDecoder('utf-8').decode(bytes);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* True for anything that walks/quacks like binary content — a real Node
|
|
58
|
+
* `Buffer` (which is itself a `Uint8Array` subclass) or a plain
|
|
59
|
+
* `Uint8Array`/typed array, as produced by protobufjs in environments
|
|
60
|
+
* without `Buffer` (e.g. a browser bundle).
|
|
61
|
+
*
|
|
62
|
+
* @param {any} value
|
|
63
|
+
* @returns {boolean}
|
|
64
|
+
*/
|
|
65
|
+
export function isBytes(value) {
|
|
66
|
+
return value instanceof Uint8Array;
|
|
67
|
+
}
|
|
@@ -4,6 +4,22 @@
|
|
|
4
4
|
* Generates stable change IDs and operation IDs using cryptographically secure random values.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Generate `byteLength` cryptographically random bytes as a lowercase hex string.
|
|
9
|
+
*
|
|
10
|
+
* Uses the standard Web Crypto API (`crypto.getRandomValues`), available as
|
|
11
|
+
* a global in both Node.js (18.14+) and every browser — unlike Node's
|
|
12
|
+
* `crypto.randomBytes`, which requires `import 'crypto'` and does not exist
|
|
13
|
+
* in a browser bundle (see issue #28).
|
|
14
|
+
*
|
|
15
|
+
* @param {number} byteLength - Number of random bytes to generate
|
|
16
|
+
* @returns {string} `byteLength * 2`-character lowercase hex string
|
|
17
|
+
*/
|
|
18
|
+
export function randomHex(byteLength) {
|
|
19
|
+
const bytes = crypto.getRandomValues(new Uint8Array(byteLength));
|
|
20
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
21
|
+
}
|
|
22
|
+
|
|
7
23
|
/**
|
|
8
24
|
* Generate a stable change ID
|
|
9
25
|
*
|
|
@@ -13,8 +29,7 @@
|
|
|
13
29
|
* @returns {string} 32-character lowercase hex string
|
|
14
30
|
*/
|
|
15
31
|
export function generateChangeId() {
|
|
16
|
-
|
|
17
|
-
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
32
|
+
return randomHex(16);
|
|
18
33
|
}
|
|
19
34
|
|
|
20
35
|
/**
|
|
@@ -48,7 +63,6 @@ export async function generateOperationId(operation) {
|
|
|
48
63
|
* @returns {string} ID string
|
|
49
64
|
*/
|
|
50
65
|
export function generateId(prefix = '') {
|
|
51
|
-
const
|
|
52
|
-
const id = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
66
|
+
const id = randomHex(16);
|
|
53
67
|
return prefix ? `${prefix}-${id}` : id;
|
|
54
68
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursive directory creation that works across filesystem backends.
|
|
3
|
+
*
|
|
4
|
+
* Why not `fs.promises.mkdir(path, { recursive: true })`: that's a Node
|
|
5
|
+
* `fs` feature. `@isomorphic-git/lightning-fs` (the filesystem this library
|
|
6
|
+
* documents for browser use — see issue #28) implements `mkdir()` but
|
|
7
|
+
* silently ignores the `recursive` option and throws `ENOENT` if any
|
|
8
|
+
* intermediate directory is missing, so code that assumed Node's recursive
|
|
9
|
+
* mkdir semantics would work identically against LightningFS actually
|
|
10
|
+
* broke the very first real repo init in a browser. This walks the path
|
|
11
|
+
* one segment at a time and creates each directory that doesn't exist yet,
|
|
12
|
+
* which is correct against both Node's `fs` and LightningFS (and is a
|
|
13
|
+
* harmless no-op-per-segment against Node's `fs` too).
|
|
14
|
+
*
|
|
15
|
+
* @param {any} fs - Filesystem implementation (isomorphic-git compatible;
|
|
16
|
+
* needs `fs.promises.mkdir(path)`)
|
|
17
|
+
* @param {string} dirPath - Absolute, POSIX-style directory path to create
|
|
18
|
+
* @returns {Promise<void>}
|
|
19
|
+
*/
|
|
20
|
+
export async function mkdirp(fs, dirPath) {
|
|
21
|
+
const isAbsolute = dirPath.charCodeAt(0) === 47; // '/'
|
|
22
|
+
const prefix = isAbsolute ? '/' : '';
|
|
23
|
+
const segments = dirPath.split('/').filter(Boolean);
|
|
24
|
+
|
|
25
|
+
// Compare against `current === ''` (not a truthiness check) below: the
|
|
26
|
+
// first accumulated segment of an absolute path is itself just `/x`,
|
|
27
|
+
// which parses fine, but a naive `current ? ... : ...` truthiness check
|
|
28
|
+
// would treat the *string* `''` the same as "unset" forever and never
|
|
29
|
+
// prepend the leading `/`.
|
|
30
|
+
let current = '';
|
|
31
|
+
for (const segment of segments) {
|
|
32
|
+
current = current === '' ? prefix + segment : `${current}/${segment}`;
|
|
33
|
+
try {
|
|
34
|
+
await fs.promises.mkdir(current);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
// Already exists — fine, that's exactly what "recursive" means.
|
|
37
|
+
if (err && (err.code === 'EEXIST' || err.code === 'EISDIR')) continue;
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal POSIX-only path utilities (join / dirname / resolve).
|
|
3
|
+
*
|
|
4
|
+
* Why not Node's `path` module: this library is isomorphic — it also needs
|
|
5
|
+
* to run in browser bundles (see issue #28), where bare `import path from
|
|
6
|
+
* 'path'` fails unless a bundler-specific polyfill is configured. Git/jj
|
|
7
|
+
* repository paths are always POSIX-style (forward slashes), regardless of
|
|
8
|
+
* host OS, so depending on Node's `path` module was also subtly wrong on
|
|
9
|
+
* Windows: `path.join()` there uses `path.win32` semantics and would
|
|
10
|
+
* produce backslash-joined paths for repository-relative paths, which is
|
|
11
|
+
* not what git/jj object storage or working-copy paths want.
|
|
12
|
+
*
|
|
13
|
+
* These implement the same normalization rules as Node's `path.posix` for
|
|
14
|
+
* the operations this codebase actually uses.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} path
|
|
19
|
+
* @returns {string} `path` with any run of `/` collapsed to a single `/`,
|
|
20
|
+
* and `.`/`..` segments resolved. Preserves a leading `/` (absolute) and
|
|
21
|
+
* a trailing `/` when present in the input.
|
|
22
|
+
*/
|
|
23
|
+
function normalize(path) {
|
|
24
|
+
if (path === '') return '.';
|
|
25
|
+
|
|
26
|
+
const isAbsolute = path.charCodeAt(0) === 47; // '/'
|
|
27
|
+
const trailingSlash = path.charCodeAt(path.length - 1) === 47;
|
|
28
|
+
|
|
29
|
+
const segments = path.split('/');
|
|
30
|
+
/** @type {string[]} */
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const segment of segments) {
|
|
33
|
+
if (segment === '' || segment === '.') continue;
|
|
34
|
+
if (segment === '..') {
|
|
35
|
+
if (out.length && out[out.length - 1] !== '..') {
|
|
36
|
+
out.pop();
|
|
37
|
+
} else if (!isAbsolute) {
|
|
38
|
+
out.push('..');
|
|
39
|
+
}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
out.push(segment);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let result = out.join('/');
|
|
46
|
+
if (!result && !isAbsolute) result = '.';
|
|
47
|
+
if (isAbsolute) result = '/' + result;
|
|
48
|
+
if (trailingSlash && result.charCodeAt(result.length - 1) !== 47) result += '/';
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {...string} parts
|
|
54
|
+
* @returns {string} All `parts` joined with `/` and normalized.
|
|
55
|
+
*/
|
|
56
|
+
export function join(...parts) {
|
|
57
|
+
if (parts.length === 0) return '.';
|
|
58
|
+
const joined = parts.filter((p) => p !== '').join('/');
|
|
59
|
+
return joined === '' ? '.' : normalize(joined);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {string} path
|
|
64
|
+
* @returns {string} The directory portion of `path` (Node `path.dirname` semantics).
|
|
65
|
+
*/
|
|
66
|
+
export function dirname(path) {
|
|
67
|
+
if (path.length === 0) return '.';
|
|
68
|
+
const isAbsolute = path.charCodeAt(0) === 47;
|
|
69
|
+
let end = -1;
|
|
70
|
+
let matchedSlash = true;
|
|
71
|
+
for (let i = path.length - 1; i >= 1; i--) {
|
|
72
|
+
if (path.charCodeAt(i) === 47) {
|
|
73
|
+
if (!matchedSlash) {
|
|
74
|
+
end = i;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
matchedSlash = false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (end === -1) return isAbsolute ? '/' : '.';
|
|
83
|
+
if (isAbsolute && end === 0) return '/';
|
|
84
|
+
return path.slice(0, end);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolves a sequence of path segments into an absolute path.
|
|
89
|
+
*
|
|
90
|
+
* Unlike Node's `path.resolve`, this never consults the real process cwd —
|
|
91
|
+
* segments are resolved right-to-left, and if no absolute segment is found
|
|
92
|
+
* the result is left relative to a virtual `/` root (this library always
|
|
93
|
+
* deals in absolute repository-relative paths, so callers are expected to
|
|
94
|
+
* supply an absolute base).
|
|
95
|
+
*
|
|
96
|
+
* @param {...string} parts
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
export function resolve(...parts) {
|
|
100
|
+
let resolved = '';
|
|
101
|
+
let isAbsolute = false;
|
|
102
|
+
|
|
103
|
+
for (let i = parts.length - 1; i >= -1 && !isAbsolute; i--) {
|
|
104
|
+
const path = i >= 0 ? parts[i] : '/';
|
|
105
|
+
if (path.length === 0) continue;
|
|
106
|
+
resolved = path + '/' + resolved;
|
|
107
|
+
isAbsolute = path.charCodeAt(0) === 47;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
resolved = normalize(resolved);
|
|
111
|
+
// normalize() preserves a trailing slash; path.resolve() never returns one
|
|
112
|
+
// (except for the root itself).
|
|
113
|
+
if (resolved.length > 1 && resolved.endsWith('/')) {
|
|
114
|
+
resolved = resolved.slice(0, -1);
|
|
115
|
+
}
|
|
116
|
+
return isAbsolute ? resolved : '/' + resolved;
|
|
117
|
+
}
|