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.
@@ -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.promises.mkdir(dirPath, { recursive: true });
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
- const bytes = crypto.getRandomValues(new Uint8Array(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 bytes = crypto.getRandomValues(new Uint8Array(16));
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
+ }