isomorphic-jj 1.0.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.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +971 -0
  3. package/dist/index.cjs +12609 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.mjs +12600 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/package.json +90 -0
  8. package/src/api/repository.js +6885 -0
  9. package/src/backend/isomorphic-git-backend.js +687 -0
  10. package/src/backend/lazy-git-backend.js +180 -0
  11. package/src/browser/helpers.js +189 -0
  12. package/src/browser/index.d.ts +174 -0
  13. package/src/browser/index.js +12 -0
  14. package/src/core/background-ops.js +272 -0
  15. package/src/core/bookmark-store.js +232 -0
  16. package/src/core/change-graph.js +285 -0
  17. package/src/core/conflict-model.js +505 -0
  18. package/src/core/jj-checkout.js +78 -0
  19. package/src/core/jj-operation-store.js +120 -0
  20. package/src/core/jj-tree-state.js +94 -0
  21. package/src/core/jj-view-store.js +105 -0
  22. package/src/core/merge-driver-registry.js +432 -0
  23. package/src/core/operation-log.js +272 -0
  24. package/src/core/revset-engine.js +1081 -0
  25. package/src/core/storage-manager.js +207 -0
  26. package/src/core/tag-store.js +167 -0
  27. package/src/core/user-config.js +142 -0
  28. package/src/core/working-copy.js +314 -0
  29. package/src/core/workspace-manager.js +283 -0
  30. package/src/drivers/built-in-drivers.js +296 -0
  31. package/src/index.js +15 -0
  32. package/src/protos/local_working_copy.proto +76 -0
  33. package/src/protos/simple_op_store.proto +147 -0
  34. package/src/types.d.ts +1129 -0
  35. package/src/utils/errors.js +21 -0
  36. package/src/utils/id-generation.js +54 -0
  37. package/src/utils/validation.js +124 -0
package/README.md ADDED
@@ -0,0 +1,971 @@
1
+ # isomorphic-jj
2
+
3
+ [![npm version](https://img.shields.io/npm/v/isomorphic-jj.svg)](https://www.npmjs.com/package/isomorphic-jj)
4
+ [![test coverage](https://img.shields.io/badge/tests-460%20passing-brightgreen.svg)](https://github.com/johnhenry/isomorphic-jj)
5
+ [![license](https://img.shields.io/npm/l/isomorphic-jj.svg)](LICENSE)
6
+
7
+ > **Jujutsu version control for JavaScript**β€”stable change IDs, fearless undo, and no staging area. Works in Node.js and browsers.
8
+
9
+ **What makes it different:**
10
+ - 🎯 **Stable change IDs** that survive rebases/squashes
11
+ - ↩️ **Complete undo** for any operation (not just commits)
12
+ - 🚫 **No staging area**β€”your working copy IS a commit
13
+ - 🌳 **Conflicts as data**β€”merge now, resolve later
14
+ - 🌐 **True isomorphic**β€”same API in Node, browsers, and Web Workers
15
+
16
+ ```javascript
17
+ import { createJJ } from 'isomorphic-jj';
18
+ import git from 'isomorphic-git';
19
+ import fs from 'fs';
20
+
21
+ const jj = await createJJ({ fs, dir: './my-repo', git });
22
+ await jj.git.init({ userName: 'You', userEmail: 'you@example.com' });
23
+
24
+ // Edit files, then describe (no staging!)
25
+ await jj.describe({ message: 'Add feature' });
26
+
27
+ // Made a mistake? Undo it.
28
+ await jj.undo();
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Quick Start
34
+
35
+ **Coming from isomorphic-git?** Read the [Migration Guide](./MIGRATION_FROM_ISOMORPHIC_GIT.md) to see how isomorphic-jj simplifies your JavaScript version control workflow.
36
+
37
+ **Want to see what it can do?** Run the [comprehensive demo](./demo.mjs):
38
+ ```bash
39
+ node demo.mjs
40
+ ```
41
+ This showcases all 17 feature categories (including v1.0 enhancements) in ~3 seconds with beautiful output.
42
+
43
+ ---
44
+
45
+ ## What's New in v1.0
46
+
47
+ isomorphic-jj v1.0 achieves **complete JJ CLI semantic compatibility** while maintaining 100% backward compatibility:
48
+
49
+ ### New Convenience Functions
50
+ - **`commit()`** - Combines `describe()` + `new()` in one step for common workflow
51
+ - **`file.*` namespace** - Complete organized file operations: `file.write()`, `file.show()`, `file.list()`, `file.move()`, `file.remove()`
52
+ - **`rebase()`** - Proper JJ CLI semantics for rebasing changes (replaces `move()` for history ops)
53
+
54
+ ### Enhanced APIs
55
+ - **`new()`** - Added `insertAfter`/`insertBefore` for precise change placement
56
+ - **`squash()`** - Added `into` parameter, smart defaults (source=@, dest=parent)
57
+ - **`abandon()`** - Now defaults to `@` (working copy) when no changeId specified
58
+ - **`split()`** - Added `paths` parameter for selective file splitting
59
+
60
+ ### Renamed for Correctness
61
+ - **`unabandon()`** - Renamed from `restore()` to match JJ CLI semantics
62
+
63
+ ### Enhanced Revsets
64
+ - **`@-` / `@--`** - Navigate to parent/grandparent (like Git's HEAD~1, HEAD~2)
65
+ - **`@+` / `@++`** - Navigate to children/grandchildren
66
+ - **`bookmark(name)`** - Exact bookmark lookup (vs `bookmarks()` for patterns)
67
+
68
+ ### Complete Workspace Operations
69
+ - **`workspace.rename()`** - Rename workspaces by ID or name
70
+ - **`workspace.root()`** - Get workspace root directory
71
+ - **`workspace.updateStale()`** - Update workspaces pointing to abandoned changes
72
+
73
+ ### Complete JJ CLI Parity (~95% coverage!)
74
+ isomorphic-jj v1.0 now implements virtually all commonly-used JJ CLI commands:
75
+ - **`git.clone()`** - Clone Git repositories with JJ metadata
76
+ - **`git.remote.*`** - Complete remote management (list, add, remove, rename, setUrl)
77
+ - **`bookmark.rename()`** - Rename bookmarks
78
+ - **`bookmark.track/untrack/forget()`** - Remote bookmark management
79
+ - **`config.*`** - Configuration management (get, set, list)
80
+ - **`diff()`** - Show file differences between revisions
81
+ - **`next()` / `prev()`** - Navigate between parent/child revisions
82
+ - **`duplicate()`** - Create copies of changes
83
+ - **`restore()`** - Restore files from another revision
84
+ - **`file.annotate()`** - Git-blame equivalent (line-by-line history)
85
+ - **`operations.show/diff/restore()`** - Advanced operation log features
86
+ - **`remote.*`** - Convenience aliases for git operations (push, fetch, add)
87
+
88
+ See [JJ_CLI_PARITY.md](./JJ_CLI_PARITY.md) for complete feature comparison.
89
+
90
+ ### Testing & Quality
91
+ - **460 tests** passing - Complete test coverage for all v1.0 features
92
+ - **100% backward compatible** - All existing code continues to work
93
+ - **Zero breaking changes** - Deprecated features show warnings but still function
94
+
95
+ ---
96
+
97
+ ### Installation
98
+
99
+ ```bash
100
+ npm install isomorphic-jj isomorphic-git
101
+ ```
102
+
103
+ For browsers, also install:
104
+ ```bash
105
+ npm install @isomorphic-git/lightning-fs
106
+ ```
107
+
108
+ ### Basic Workflow
109
+
110
+ ```javascript
111
+ import { createJJ } from 'isomorphic-jj';
112
+ import git from 'isomorphic-git';
113
+ import fs from 'fs';
114
+ import http from 'isomorphic-git/http/node';
115
+
116
+ // Create repository
117
+ const jj = await createJJ({
118
+ fs,
119
+ dir: './repo',
120
+ git,
121
+ http
122
+ });
123
+
124
+ // Initialize (creates both .git and .jj directories)
125
+ await jj.git.init({
126
+ userName: 'Your Name',
127
+ userEmail: 'you@example.com'
128
+ });
129
+
130
+ // Write files and describe changes (no staging!)
131
+ await jj.write({ path: 'README.md', data: '# My Project' });
132
+ await jj.describe({ message: 'Initial commit' });
133
+
134
+ // Create a new change on top
135
+ await jj.new({ message: 'Add feature' });
136
+ await jj.write({ path: 'feature.js', data: 'export const feature = () => {}' });
137
+ await jj.amend({ message: 'Add feature implementation' });
138
+
139
+ // Oops! Undo the last operation
140
+ await jj.undo();
141
+
142
+ // View history
143
+ const log = await jj.log({ limit: 10 });
144
+ console.log(log);
145
+ ```
146
+
147
+ ### Browser Usage
148
+
149
+ ```javascript
150
+ import { createJJ } from 'isomorphic-jj';
151
+ import { createBrowserFS } from 'isomorphic-jj/browser';
152
+ import git from 'isomorphic-git';
153
+ import http from 'isomorphic-git/http/web';
154
+
155
+ // Create browser filesystem (uses IndexedDB)
156
+ const fs = createBrowserFS({ name: 'my-repo' });
157
+
158
+ const jj = await createJJ({ fs, dir: '/repo', git, http });
159
+ await jj.git.init({ userName: 'User', userEmail: 'user@example.com' });
160
+ // ... same API as Node.js!
161
+ ```
162
+
163
+ ---
164
+
165
+ ## How is this Different?
166
+
167
+ ### JJ vs Git Concepts
168
+
169
+ | Concept | Git | JJ / isomorphic-jj |
170
+ |---------|-----|-------------------|
171
+ | **Primary ID** | Commit SHA (changes on rewrite) | Change ID (stable) + Commit ID (mutable) |
172
+ | **Working state** | Working tree + staging index | Working copy IS a commit |
173
+ | **Branches** | Required for work | Optional bookmarks for sync only |
174
+ | **"Dirty" state** | Blocks many operations | Doesn't existβ€”always committed |
175
+ | **Undo** | `git reflog` (per-ref, manual) | `jj.undo()` (complete repo state) |
176
+ | **Conflicts** | Text markers that block workflow | Structured data you can commit |
177
+ | **Rewriting history** | Manual `rebase -i`, can lose work | Edit any change, descendants auto-update |
178
+
179
+ ### Mental Model Shift
180
+
181
+ **Git thinking:**
182
+ ```bash
183
+ # Edit files
184
+ git add . # Stage changes
185
+ git commit -m "message" # Create commit
186
+ git rebase -i HEAD~3 # Manually rewrite history
187
+ # Hope you didn't mess up!
188
+ ```
189
+
190
+ **JJ thinking:**
191
+ ```javascript
192
+ // Edit files (no staging!)
193
+ await jj.describe({ message: 'message' }); // Describe current change
194
+
195
+ // Edit any change in history
196
+ await jj.edit({ changeId: 'abc123' });
197
+ // Make changes...
198
+ await jj.amend();
199
+ // Descendants automatically rebased!
200
+
201
+ // Made a mistake? Just undo.
202
+ await jj.undo();
203
+ ```
204
+
205
+ ---
206
+
207
+ ## Features
208
+
209
+ ### Core Operations
210
+
211
+ #### File Operations
212
+ ```javascript
213
+ // Write files
214
+ await jj.write({ path: 'file.txt', data: 'content' });
215
+
216
+ // Read from working copy or any change
217
+ const content = await jj.read({ path: 'file.txt' });
218
+ const oldVersion = await jj.read({ path: 'file.txt', changeId: 'abc123' });
219
+
220
+ // ✨ NEW in v1.0: Complete file.* namespace (matches JJ CLI structure)
221
+ // Recommended: Use file.* for all file operations
222
+ await jj.file.write({ path: 'new.txt', data: 'content' });
223
+ const fileContent = await jj.file.show({ path: 'file.txt' });
224
+ const fileList = await jj.file.list();
225
+ await jj.file.move({ from: 'old.txt', to: 'new.txt' });
226
+ await jj.file.remove({ path: 'unwanted.txt' });
227
+
228
+ // Read from historical changes
229
+ const historicalFile = await jj.file.show({ path: 'file.txt', changeId: 'abc123' });
230
+
231
+ // 100% Backward compatible: top-level methods still work
232
+ await jj.write({ path: 'file.txt', data: 'content' });
233
+ const sameContent = await jj.read({ path: 'file.txt' });
234
+ await jj.move({ from: 'old.txt', to: 'new.txt' });
235
+ await jj.remove({ path: 'file.txt' });
236
+ const files = await jj.listFiles();
237
+ ```
238
+
239
+ #### Change Management
240
+ ```javascript
241
+ // Describe current change (creates Git commit automatically)
242
+ await jj.describe({ message: 'Implement feature X' });
243
+
244
+ // Create new change on top of working copy
245
+ await jj.new({ message: 'Start feature Y' });
246
+
247
+ // ✨ NEW in v1.0: commit() convenience function
248
+ // Combines describe() + new() in one operation
249
+ await jj.commit({
250
+ message: 'Complete feature X',
251
+ nextMessage: 'Start feature Y'
252
+ });
253
+
254
+ // Amend current change
255
+ await jj.amend({ message: 'Fix typo in feature X' });
256
+
257
+ // Edit historical changes
258
+ await jj.edit({ changeId: 'abc123' });
259
+ // Make changes...
260
+ await jj.amend();
261
+ // Descendants are automatically rebased!
262
+
263
+ // Check status
264
+ const status = await jj.status();
265
+ console.log(status.modified, status.added, status.removed);
266
+ ```
267
+
268
+ #### History Editing
269
+ ```javascript
270
+ // ✨ NEW in v1.0: Enhanced squash with 'into' parameter and smart defaults
271
+ await jj.squash({ into: 'change2' }); // Squashes @ into change2
272
+ await jj.squash(); // Smart default: squashes @ into parent(@)
273
+
274
+ // ✨ NEW in v1.0: Enhanced new() with precise placement
275
+ await jj.new({
276
+ message: 'Inserted change',
277
+ insertAfter: 'abc123', // Insert after specific change
278
+ insertBefore: 'def456' // Or insert before specific change
279
+ });
280
+
281
+ // ✨ NEW in v1.0: Split with paths parameter
282
+ await jj.split({
283
+ changeId: 'abc123',
284
+ description1: 'Part 1: Docs',
285
+ description2: 'Part 2: Tests',
286
+ paths: ['docs/*.md'] // Files for first split
287
+ });
288
+
289
+ // ✨ NEW in v1.0: abandon() defaults to working copy
290
+ await jj.abandon(); // Abandons @ (no changeId needed)
291
+ await jj.abandon({ changeId: 'experimental' }); // Or specify explicit change
292
+
293
+ // ✨ RENAMED in v1.0: unabandon() (was restore() - correct JJ semantics)
294
+ await jj.unabandon({ changeId: 'experimental' });
295
+
296
+ // ✨ NEW in v1.0: rebase() - Proper JJ CLI semantics for history operations
297
+ await jj.rebase({
298
+ changeId: 'feature',
299
+ newParent: 'updated-main',
300
+ paths: ['file.js'] // Optional: only rebase specific files
301
+ });
302
+
303
+ // DEPRECATED: move() for history operations (use rebase() instead)
304
+ // move() still works but shows deprecation warning for history operations
305
+ // move() will be file-only in v2.0
306
+ await jj.move({ changeId: 'feature', newParent: 'updated-main' }); // Works but deprecated
307
+ ```
308
+
309
+ #### Revsets - Powerful Queries
310
+ ```javascript
311
+ // Simple revsets
312
+ await jj.log({ revset: '@' }); // working copy
313
+ await jj.log({ revset: 'all()' }); // all commits
314
+ await jj.log({ revset: 'none()' }); // empty set (v1.0)
315
+ await jj.log({ revset: 'roots()' }); // root commits
316
+
317
+ // ✨ NEW in v1.0: bookmark(name) for exact bookmark lookup
318
+ await jj.log({ revset: 'bookmark(main)' }); // single bookmark by exact name
319
+ await jj.log({ revset: 'bookmarks(feat*)' }); // pattern matching (multiple)
320
+
321
+ // Filter by author or description
322
+ await jj.log({ revset: 'author(alice)' });
323
+ await jj.log({ revset: 'description(fix)' });
324
+
325
+ // File-based queries
326
+ await jj.log({ revset: 'file(*.js)' }); // commits touching JS files
327
+ await jj.log({ revset: 'mine()' }); // my commits
328
+
329
+ // ✨ NEW in v1.0: Navigation functions
330
+ await jj.log({ revset: 'parents(@)' }); // direct parents
331
+ await jj.log({ revset: 'children(@)' }); // direct children
332
+ await jj.log({ revset: 'parents(all())' }); // all commits with children
333
+
334
+ // ✨ NEW in v1.0: Shorthand operators
335
+ await jj.log({ revset: '@-' }); // parent (like HEAD~1)
336
+ await jj.log({ revset: '@--' }); // grandparent (like HEAD~2)
337
+ await jj.log({ revset: '@+' }); // children
338
+ await jj.log({ revset: '@++' }); // grandchildren
339
+
340
+ // ✨ NEW in v0.5: Time-based queries
341
+ await jj.log({ revset: 'last(5)' }); // last 5 commits
342
+ await jj.log({ revset: 'last(7d)' }); // last 7 days
343
+ await jj.log({ revset: 'last(24h)' }); // last 24 hours
344
+ await jj.log({ revset: 'since(2025-01-01)' }); // since date
345
+ await jj.log({ revset: 'between(2025-01-01, 2025-02-01)' }); // date range
346
+
347
+ // ✨ NEW in v0.5: Graph analytics
348
+ await jj.log({ revset: 'descendants(abc123)' }); // all descendants
349
+ await jj.log({ revset: 'descendants(abc123, 2)' }); // max 2 levels deep
350
+ await jj.log({ revset: 'common_ancestor(rev1, rev2)' }); // merge base
351
+ await jj.log({ revset: 'range(base..tip)' }); // commits in range
352
+ await jj.log({ revset: 'diverge_point(rev1, rev2)' }); // where branches split
353
+ await jj.log({ revset: 'connected(rev1, rev2)' }); // check if path exists
354
+
355
+ // ✨ NEW in v0.5: Set operations
356
+ await jj.log({ revset: 'last(7d) & file(*.js)' }); // recent JS changes
357
+ await jj.log({ revset: 'mine() | author(bob)' }); // mine or Bob's
358
+ await jj.log({ revset: 'all() ~ mine()' }); // everything except mine
359
+
360
+ // Traditional graph analysis
361
+ await jj.log({ revset: 'roots(all())' }); // commits with no parents
362
+ await jj.log({ revset: 'heads(all())' }); // commits with no children
363
+ await jj.log({ revset: 'latest(mine(), 5)' }); // my 5 latest commits
364
+
365
+ // Repository analytics
366
+ const stats = await jj.stats();
367
+ console.log(`Total: ${stats.changes.total}, Mine: ${stats.changes.mine}`);
368
+ ```
369
+
370
+ #### Complete Undo/Redo
371
+ ```javascript
372
+ // View operation history
373
+ const ops = await jj.obslog({ limit: 20 });
374
+
375
+ // Undo last operation (works for ANY operation)
376
+ await jj.undo();
377
+
378
+ // Undo multiple operations
379
+ await jj.undo({ count: 3 });
380
+
381
+ // Time travel to any past state
382
+ const historical = await jj.operations.at({ operation: ops[5].id });
383
+ const oldLog = await historical.log({ revset: 'all()' });
384
+ ```
385
+
386
+ ### Git Interoperability
387
+
388
+ ```javascript
389
+ // Initialize colocated repository (both .git and .jj)
390
+ await jj.git.init({
391
+ userName: 'Your Name',
392
+ userEmail: 'you@example.com'
393
+ });
394
+
395
+ // Fetch from Git remotes
396
+ await jj.git.fetch({ remote: 'origin' });
397
+
398
+ // Push to Git remotes
399
+ await jj.git.push({ remote: 'origin', refs: ['main'] });
400
+
401
+ // Import Git refs as bookmarks
402
+ await jj.git.import();
403
+
404
+ // Git users see normal commits
405
+ // JJ users get superior UX
406
+ // Full bidirectional compatibility!
407
+ ```
408
+
409
+ **Git interop works seamlessly:**
410
+ ```bash
411
+ cd my-repo
412
+ git log --oneline # See commits from JJ
413
+ git show HEAD # View latest change
414
+ git branch -a # See bookmarks as branches
415
+ git status # .jj directory is ignored
416
+ ```
417
+
418
+ ### First-Class Conflicts
419
+
420
+ ```javascript
421
+ // Merge creates conflicts but doesn't fail
422
+ const result = await jj.merge({
423
+ source: 'feature-branch',
424
+ dest: 'main'
425
+ });
426
+ console.log(`Detected ${result.conflicts.length} conflicts`);
427
+
428
+ // Continue working on something else
429
+ await jj.new({ message: 'Unrelated work' });
430
+
431
+ // Later, resolve conflicts
432
+ const conflicts = await jj.conflicts.list();
433
+ for (const conflict of conflicts) {
434
+ await jj.conflicts.resolve({
435
+ conflictId: conflict.conflictId,
436
+ resolution: { side: 'ours' } // or provide custom content
437
+ });
438
+ }
439
+
440
+ // Undo restores conflict state if needed
441
+ await jj.undo();
442
+ ```
443
+
444
+ #### Custom Merge Drivers (v0.5)
445
+
446
+ Merge drivers enable smart merging of structured files like JSON, package.json, and YAML:
447
+
448
+ ```javascript
449
+ import { jsonDriver, packageJsonDriver, yamlDriver, markdownDriver } from 'isomorphic-jj';
450
+
451
+ // Register merge drivers for different file types
452
+ jj.mergeDrivers.register({
453
+ 'package.json': packageJsonDriver, // Smart merge for package.json
454
+ '*.json': jsonDriver, // Generic JSON merge
455
+ '*.yaml': yamlDriver, // YAML merge
456
+ '*.md': markdownDriver, // Markdown merge
457
+ });
458
+
459
+ // Merge with automatic driver resolution
460
+ await jj.merge({ source: 'feature' });
461
+ // Drivers automatically merge files when possible
462
+
463
+ // Per-merge driver override
464
+ await jj.merge({
465
+ source: 'feature',
466
+ drivers: {
467
+ 'config.json': customDriver,
468
+ }
469
+ });
470
+
471
+ // Create custom merge driver
472
+ const customDriver = {
473
+ name: 'my-custom-driver',
474
+ canMerge: (base, ours, theirs) => {
475
+ // Return true if driver can handle this merge
476
+ return true;
477
+ },
478
+ merge: (base, ours, theirs) => {
479
+ // Return merged content or null if conflict
480
+ return mergedContent;
481
+ }
482
+ };
483
+
484
+ // Built-in drivers:
485
+ // - packageJsonDriver: Smart merge for package.json (union merge for dependencies)
486
+ // - jsonDriver: Generic JSON merge (object-level merging)
487
+ // - yamlDriver: YAML structure-aware merge
488
+ // - markdownDriver: Section-aware merge for Markdown
489
+ ```
490
+
491
+ #### Conflict Resolution Enhancements (v0.5)
492
+
493
+ New conflict resolution capabilities make handling conflicts easier:
494
+
495
+ ```javascript
496
+ // ✨ Dry-run merge preview
497
+ const preview = await jj.merge({
498
+ source: 'feature',
499
+ dryRun: true // Preview conflicts without applying
500
+ });
501
+ console.log(`Would create ${preview.conflicts.length} conflicts`);
502
+ preview.conflicts.forEach(c => {
503
+ console.log(` ${c.path}: ${c.type}`);
504
+ });
505
+
506
+ // ✨ Bulk resolution with strategies
507
+ await jj.conflicts.resolveAll({
508
+ strategy: 'ours', // Keep our version
509
+ });
510
+
511
+ await jj.conflicts.resolveAll({
512
+ strategy: 'theirs', // Take their version
513
+ });
514
+
515
+ await jj.conflicts.resolveAll({
516
+ strategy: 'union', // Combine both sides
517
+ });
518
+
519
+ // ✨ Filtered bulk resolution
520
+ await jj.conflicts.resolveAll({
521
+ strategy: 'ours',
522
+ filter: { path: '*.json' } // Only JSON files
523
+ });
524
+
525
+ await jj.conflicts.resolveAll({
526
+ strategy: 'theirs',
527
+ filter: { path: 'src/config/*' } // Specific directory
528
+ });
529
+
530
+ // ✨ Resolve with merge driver
531
+ await jj.conflicts.resolve({
532
+ conflictId: conflict.conflictId,
533
+ driver: 'package.json', // Use registered driver
534
+ });
535
+
536
+ // ✨ Get Git-style conflict markers
537
+ const markers = await jj.conflicts.markers({
538
+ conflictId: conflict.conflictId
539
+ });
540
+ console.log(markers);
541
+ // <<<<<<< ours
542
+ // our content
543
+ // =======
544
+ // their content
545
+ // >>>>>>> theirs
546
+
547
+ // Manual resolution
548
+ await jj.conflicts.resolve({
549
+ conflictId: conflict.conflictId,
550
+ resolution: 'manually merged content',
551
+ });
552
+
553
+ // Strategy-based resolution
554
+ await jj.conflicts.resolve({
555
+ conflictId: conflict.conflictId,
556
+ strategy: 'ours', // or 'theirs', 'union'
557
+ });
558
+ ```
559
+
560
+ ### Bookmarks (Not Branches)
561
+
562
+ In JJ, bookmarks are for remote sync, not local navigation:
563
+
564
+ ```javascript
565
+ // Most work doesn't need bookmarks
566
+ await jj.new();
567
+ await jj.describe({ message: 'Anonymous change' });
568
+
569
+ // Bookmarks when pushing to remotes
570
+ await jj.bookmark.set({ name: 'feature-x', target: '@' });
571
+ await jj.remote.push({ remote: 'origin', refs: ['feature-x'] });
572
+
573
+ // List bookmarks
574
+ const bookmarks = await jj.bookmark.list();
575
+ ```
576
+
577
+ ### Browser Support
578
+
579
+ ```javascript
580
+ import { createBrowserFS, requestPersistentStorage } from 'isomorphic-jj/browser';
581
+
582
+ // Request persistent storage (prevents eviction)
583
+ const persistent = await requestPersistentStorage();
584
+
585
+ // Create filesystem with IndexedDB backend
586
+ const fs = createBrowserFS({ backend: 'idb', name: 'my-repo' });
587
+
588
+ // Check browser capabilities
589
+ import { detectCapabilities } from 'isomorphic-jj/browser';
590
+ const caps = detectCapabilities();
591
+ if (caps.indexedDB && caps.serviceWorker) {
592
+ // Enable offline support
593
+ }
594
+
595
+ // Get storage quota
596
+ import { getStorageQuota } from 'isomorphic-jj/browser';
597
+ const quota = await getStorageQuota();
598
+ console.log(`Using ${quota.percentage}% of available storage`);
599
+ ```
600
+
601
+ ### Advanced Features
602
+
603
+ #### Multiple Working Copies (Workspaces)
604
+ ```javascript
605
+ // Work on multiple changes simultaneously
606
+ const workspace = await jj.workspace.add({
607
+ path: './feature-branch',
608
+ name: 'feature-work',
609
+ changeId: someChangeId
610
+ });
611
+
612
+ const all = await jj.workspace.list();
613
+
614
+ // ✨ NEW in v1.0: Complete workspace operations
615
+ // Rename workspace
616
+ await jj.workspace.rename({
617
+ workspace: workspace.id, // or workspace name
618
+ newName: 'renamed-feature'
619
+ });
620
+
621
+ // Get workspace root directory
622
+ const root = await jj.workspace.root({ workspace: 'renamed-feature' });
623
+ console.log(`Workspace path: ${root}`);
624
+
625
+ // Update stale workspaces (pointing to abandoned changes)
626
+ const staleResult = await jj.workspace.updateStale();
627
+ console.log(`Updated ${staleResult.updated} stale workspace(s)`);
628
+
629
+ // Update specific workspace only
630
+ await jj.workspace.updateStale({ workspace: 'feature-work' });
631
+
632
+ await jj.workspace.remove({ id: workspace.id });
633
+ ```
634
+
635
+ #### Background Operations (Node.js)
636
+ ```javascript
637
+ // Enable file watching and auto-snapshots
638
+ await jj.background.start();
639
+ await jj.background.enableAutoSnapshot({ debounceMs: 1000 });
640
+
641
+ // Queue async operations
642
+ await jj.background.queue(async () => {
643
+ await jj.git.fetch({ remote: 'origin' });
644
+ });
645
+ ```
646
+
647
+ #### Event System
648
+ ```javascript
649
+ // JJ extends EventTarget - listen to repository events
650
+ jj.addEventListener('change:creating', (event) => {
651
+ console.log('Creating change:', event.detail.description);
652
+ // event.preventDefault() to cancel operation
653
+ });
654
+
655
+ jj.addEventListener('change:created', (event) => {
656
+ console.log('Change created:', event.detail.changeId);
657
+ // Informational only, cannot cancel
658
+ });
659
+
660
+ jj.addEventListener('change:updating', (event) => {
661
+ console.log('Updating change:', event.detail.changeId);
662
+ // Can run validation and preventDefault() if needed
663
+ });
664
+
665
+ jj.addEventListener('change:updated', (event) => {
666
+ console.log('Change updated:', event.detail.changeId);
667
+ });
668
+
669
+ // Events fire automatically during describe(), new(), amend(), etc.
670
+ await jj.describe({ message: 'Fix bug' }); // Fires events!
671
+ ```
672
+
673
+ #### Shallow Clones
674
+ ```javascript
675
+ // Fetch with depth limit for faster clones
676
+ await jj.git.fetch({
677
+ remote: 'origin',
678
+ depth: 1, // Only latest commit
679
+ singleBranch: true,
680
+ noTags: true
681
+ });
682
+ ```
683
+
684
+ ---
685
+
686
+ ## Use Cases
687
+
688
+ ### Stacked Changes (Like Stacked PRs)
689
+
690
+ ```javascript
691
+ // Create dependent changes
692
+ await jj.write({ path: 'core.js', data: '...' });
693
+ const core = await jj.describe({ message: 'Refactor core' });
694
+
695
+ await jj.new({ message: 'Feature A using new core' });
696
+ await jj.write({ path: 'feature-a.js', data: '...' });
697
+ const featureA = await jj.amend();
698
+
699
+ await jj.new({ message: 'Feature B using feature A' });
700
+ await jj.write({ path: 'feature-b.js', data: '...' });
701
+
702
+ // Edit the middle change - descendants auto-rebase!
703
+ await jj.edit({ changeId: featureA.changeId });
704
+ await jj.amend({ message: 'Updated feature A' });
705
+ // Feature B is automatically updated!
706
+ ```
707
+
708
+ ### Experimentation Without Fear
709
+
710
+ ```javascript
711
+ // Try something risky
712
+ await jj.new({ message: 'Experimental refactor' });
713
+ // ... make major changes ...
714
+ await jj.describe({ message: 'Attempt 1' });
715
+
716
+ // Didn't work? Just undo
717
+ await jj.undo();
718
+
719
+ // Or try a different approach
720
+ await jj.new({ message: 'Better approach' });
721
+ // Operation log has complete history
722
+ ```
723
+
724
+ ### Code Review Workflow
725
+
726
+ ```javascript
727
+ // Changes have stable IDs across iterations
728
+ const changeId = await jj.describe({ message: 'Initial implementation' });
729
+
730
+ // Reviewer comments applied
731
+ await jj.edit({ changeId: changeId });
732
+ await jj.amend({ message: 'Address review comments' });
733
+ // Same changeId, different commitId
734
+
735
+ // Push for review
736
+ await jj.bookmark.set({ name: 'review/feature-x', target: changeId });
737
+ await jj.remote.push({ remote: 'origin', refs: ['review/feature-x'] });
738
+ ```
739
+
740
+ ---
741
+
742
+ ## API Reference
743
+
744
+ Full API documentation available in [TypeScript definitions](./src/types.d.ts).
745
+
746
+ ### Main Interface
747
+
748
+ ```typescript
749
+ import { createJJ, type JJ, type CreateJJOptions } from 'isomorphic-jj';
750
+
751
+ const jj: JJ = await createJJ(options: CreateJJOptions);
752
+ ```
753
+
754
+ ### Core Methods
755
+
756
+ - **Repository**: `init()`, `status()`, `stats()`
757
+ - **Files**: `write()`, `read()`, `cat()`, `move()` (deprecated for history), `remove()`, `listFiles()` | **Namespace**: `file.write()`, `file.show()`, `file.list()`, `file.move()`, `file.remove()`
758
+ - **Changes**: `describe()`, `new()`, `amend()`, `commit()`, `edit()`, `show()`
759
+ - **History**: `log()`, `obslog()`, `squash()`, `split()`, `rebase()`, `abandon()`, `unabandon()`
760
+ - **Operations**: `undo()`, `operations.list()`, `operations.at()`
761
+ - **Bookmarks**: `bookmark.list()`, `bookmark.set()`, `bookmark.move()`, `bookmark.delete()`
762
+ - **Git**: `git.init()`, `git.fetch()`, `git.push()`, `git.import()`, `git.export()`
763
+ - **Remotes**: `remote.add()`, `remote.fetch()`, `remote.push()`
764
+ - **Workspaces**: `workspace.add()`, `workspace.list()`, `workspace.remove()`, `workspace.rename()`, `workspace.root()`, `workspace.updateStale()`
765
+ - **Conflicts**: `merge()`, `conflicts.list()`, `conflicts.resolve()`, `conflicts.resolveAll()` (v0.5), `conflicts.markers()` (v0.5)
766
+ - **Merge Drivers** (v0.5): `mergeDrivers.register()`, `mergeDrivers.get()`, Built-in drivers: `jsonDriver`, `packageJsonDriver`, `yamlDriver`, `markdownDriver`
767
+ - **Background** (Node.js): `background.start()`, `background.stop()`, `background.enableAutoSnapshot()`
768
+
769
+ See [complete API documentation](./src/types.d.ts) for detailed signatures and options.
770
+
771
+ ---
772
+
773
+ ## Why isomorphic-jj?
774
+
775
+ ### JJ's Model is Better for Everyday Work
776
+
777
+ - **Stable change IDs** survive rebases/squashesβ€”like "review comments that follow the code"
778
+ - **Operation log** means you can undo anything, not just commits
779
+ - **No staging area** eliminates a major source of confusion
780
+ - **First-class conflicts** let you merge now, resolve later
781
+ - **Anonymous changes** simplify experimental work and stacked changes
782
+
783
+ ### We Want This in JavaScript, Everywhere
784
+
785
+ - **isomorphic-git** proved Git can run in Node and browsers
786
+ - **isomorphic-jj** extends this to JJ semantics while maintaining Git compatibility
787
+ - **True isomorphic**: Same API in Node, browsers, Web Workers, Service Workers
788
+ - **Git interop** is table stakesβ€”fetch/push to GitHub/GitLab just works
789
+
790
+ ### Git Compatibility Matters
791
+
792
+ - Colocated repositories work with both Git and JJ tools
793
+ - Git users see normal commits; JJ users get superior UX
794
+ - Fetch/push to Git remotes using proven isomorphic-git infrastructure
795
+ - Transparent collaboration between Git and JJ workflows
796
+
797
+ ---
798
+
799
+ ## Architecture
800
+
801
+ isomorphic-jj follows a three-layer architecture:
802
+
803
+ ```
804
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
805
+ β”‚ Your App / UI Layer β”‚
806
+ β”‚ (Web UI, CLI, VS Code extension) β”‚
807
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
808
+ β”‚
809
+ β–Ό
810
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
811
+ β”‚ isomorphic-jj (Porcelain Layer) β”‚
812
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
813
+ β”‚ β”‚ Core: ChangeGraph, OpLog, Revsets β”‚ β”‚
814
+ β”‚ β”‚ Operations: describe, new, squash β”‚ β”‚
815
+ β”‚ β”‚ Conflicts: First-class conflict model β”‚ β”‚
816
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
817
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
818
+ β”‚ Backend Interface (pluggable)
819
+ β–Ό
820
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
821
+ β”‚ Backend Adapter (Plumbing Layer) β”‚
822
+ β”‚ β€’ isomorphic-git (default) β”‚
823
+ β”‚ β€’ Mock backend (testing) β”‚
824
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
825
+ β”‚
826
+ β–Ό
827
+ Git objects + remotes
828
+ ```
829
+
830
+ **Design Principles:**
831
+ - Emulate JJ semantics, not implementation
832
+ - Backend agnostic with pluggable adapters
833
+ - Isomorphic by design (Node + browser)
834
+ - JSON storage for JJ metadata (`.jj/graph.json`, `.jj/oplog.jsonl`)
835
+ - Operation-first, not commit-first
836
+
837
+ **Storage:**
838
+ ```
839
+ repo/
840
+ β”œβ”€β”€ .git/ # Git objects (via backend)
841
+ └── .jj/
842
+ β”œβ”€β”€ graph.json # Change graph with stable IDs
843
+ β”œβ”€β”€ oplog.jsonl # Append-only operation log
844
+ β”œβ”€β”€ bookmarks.json # Bookmarks
845
+ β”œβ”€β”€ conflicts/ # Conflict descriptors
846
+ └── working-copy.json # Working copy state
847
+ ```
848
+
849
+ ---
850
+
851
+ ## Project Status
852
+
853
+ **Current Version**: v0.5.0
854
+ **Test Coverage**: 351 tests, 100% passing
855
+ **Status**: Ready for experimentation and prototyping
856
+
857
+ **Completed:**
858
+ - βœ… v0.1: Core JJ experience (stable IDs, undo, bookmarks, revsets)
859
+ - βœ… v0.2: History editing (squash, split, abandon, unabandon, move)
860
+ - βœ… v0.3: Git backend, conflicts, workspaces, browser support
861
+ - βœ… v0.4: Shallow clones, advanced revsets, event system
862
+ - βœ… v0.5: Custom merge drivers, enhanced revsets (time-based, graph analytics), conflict resolution enhancements
863
+
864
+ **Coming Next (v0.6):**
865
+ - Repository analytics and debugging tools
866
+ - Interactive workflows
867
+ - Performance optimizations
868
+
869
+ See [ROADMAP.md](./ROADMAP.md) for detailed plans through v1.0.
870
+
871
+ ---
872
+
873
+ ## Installation & Environment
874
+
875
+ ### Requirements
876
+
877
+ - **Node.js**: 18.0.0 or higher
878
+ - **Browsers**: Chrome 90+, Firefox 88+, Safari 14+, Edge 90+
879
+ - **Optional**: Git CLI (for JJ CLI interop testing)
880
+
881
+ ### Dependencies
882
+
883
+ **Required:**
884
+ - `protobufjs` - For JJ repository format encoding
885
+
886
+ **Peer Dependencies (optional):**
887
+ - `isomorphic-git` - For Git backend support
888
+ - `@isomorphic-git/lightning-fs` - For browser filesystem
889
+
890
+ ### Browser Considerations
891
+
892
+ - Uses IndexedDB or OPFS for storage
893
+ - Remote operations require CORS proxy for most Git hosts
894
+ - Memory limits apply (use `limit` options for large repos)
895
+
896
+ ---
897
+
898
+ ## Contributing
899
+
900
+ We welcome contributions! Here's how to help:
901
+
902
+ **Before opening a PR:**
903
+ 1. Open an issue to discuss your use case
904
+ 2. Follow "porcelain over plumbed backends" design
905
+ 3. Include tests and TypeScript types
906
+ 4. Update documentation
907
+
908
+ **Development setup:**
909
+ ```bash
910
+ git clone https://github.com/johnhenry/isomorphic-jj
911
+ cd isomorphic-jj
912
+ npm install
913
+ npm test # Run tests
914
+ npm run typecheck # Check types
915
+ npm run lint # Check code style
916
+ ```
917
+
918
+ **Areas needing help:**
919
+ - Revset parser/evaluator enhancements
920
+ - Conflict resolution algorithms
921
+ - Browser storage optimizations
922
+ - Documentation and examples
923
+ - VS Code extension
924
+
925
+ ---
926
+
927
+ ## FAQ
928
+
929
+ **Q: Do I need to learn JJ to use this?**
930
+ A: Basic familiarity helps. Key concepts: working copy IS a commit (no staging), changes have stable IDs, operation log enables fearless undo, bookmarks are for pushing not local navigation.
931
+
932
+ **Q: Can Git users collaborate with me?**
933
+ A: Yes! Colocated repos expose normal Git commits. Git users never see JJ metadata.
934
+
935
+ **Q: What's the performance like?**
936
+ A: Comparable to isomorphic-git for Git operations. JJ metadata (JSON) is fast in Node, acceptable in browsers. Large histories need pagination.
937
+
938
+ **Q: Does this support all JJ features?**
939
+ A: Not yet. We're at v0.4. See [ROADMAP.md](./ROADMAP.md) for planned features.
940
+
941
+ **Q: Can I migrate my Git repo?**
942
+ A: Yes! `jj.git.init()` works on existing Git repositories.
943
+
944
+ **Q: Why not just use Git?**
945
+ A: JJ's model genuinely improves common workflowsβ€”stable change IDs, fearless undo, no staging confusion, conflicts as data instead of blockers.
946
+
947
+ ---
948
+
949
+ ## Related Projects
950
+
951
+ - [isomorphic-git](https://isomorphic-git.org/) - Pure JS Git implementation (our foundation)
952
+ - [Jujutsu](https://jj-vcs.github.io/jj/) - The original JJ version control system
953
+ - [simple-git](https://github.com/steveukx/git-js) - Git wrapper for Node.js
954
+ - [Git plumbing vs porcelain](https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain) - Architecture pattern we follow
955
+
956
+ ---
957
+
958
+ ## License
959
+
960
+ MIT Β© John Henry
961
+
962
+ ---
963
+
964
+ ## Acknowledgments
965
+
966
+ Built on the shoulders of:
967
+ - [isomorphic-git](https://github.com/isomorphic-git/isomorphic-git) by William Hilton
968
+ - [Jujutsu](https://github.com/martinvonz/jj) by Martin von Zweigbergk
969
+ - The Git and JavaScript communities
970
+
971
+ **Status**: v1.0.0 | **Tests**: 371 passing | **Ready for**: Production use