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.
- package/LICENSE +21 -0
- package/README.md +971 -0
- package/dist/index.cjs +12609 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +12600 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +90 -0
- package/src/api/repository.js +6885 -0
- package/src/backend/isomorphic-git-backend.js +687 -0
- package/src/backend/lazy-git-backend.js +180 -0
- package/src/browser/helpers.js +189 -0
- package/src/browser/index.d.ts +174 -0
- package/src/browser/index.js +12 -0
- package/src/core/background-ops.js +272 -0
- package/src/core/bookmark-store.js +232 -0
- package/src/core/change-graph.js +285 -0
- package/src/core/conflict-model.js +505 -0
- package/src/core/jj-checkout.js +78 -0
- package/src/core/jj-operation-store.js +120 -0
- package/src/core/jj-tree-state.js +94 -0
- package/src/core/jj-view-store.js +105 -0
- package/src/core/merge-driver-registry.js +432 -0
- package/src/core/operation-log.js +272 -0
- package/src/core/revset-engine.js +1081 -0
- package/src/core/storage-manager.js +207 -0
- package/src/core/tag-store.js +167 -0
- package/src/core/user-config.js +142 -0
- package/src/core/working-copy.js +314 -0
- package/src/core/workspace-manager.js +283 -0
- package/src/drivers/built-in-drivers.js +296 -0
- package/src/index.js +15 -0
- package/src/protos/local_working_copy.proto +76 -0
- package/src/protos/simple_op_store.proto +147 -0
- package/src/types.d.ts +1129 -0
- package/src/utils/errors.js +21 -0
- package/src/utils/id-generation.js +54 -0
- package/src/utils/validation.js +124 -0
package/README.md
ADDED
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
# isomorphic-jj
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/isomorphic-jj)
|
|
4
|
+
[](https://github.com/johnhenry/isomorphic-jj)
|
|
5
|
+
[](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
|