isomorphic-jj 1.8.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +222 -0
- package/LICENSE +1 -1
- package/README.md +117 -30
- package/dist/index.cjs +1911 -344
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1911 -343
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -7
- package/src/api/repository.js +775 -158
- package/src/backend/isomorphic-git-backend.js +13 -12
- package/src/browser/helpers.js +38 -16
- package/src/browser/index.d.ts +9 -2
- package/src/core/change-graph.js +125 -4
- package/src/core/conflict-model.js +72 -7
- package/src/core/jj-checkout.js +5 -16
- package/src/core/jj-operation-store.js +6 -17
- package/src/core/jj-tree-state.js +5 -16
- package/src/core/jj-view-store.js +6 -17
- package/src/core/merge-driver-registry.js +11 -10
- package/src/core/revset-engine.js +15 -3
- package/src/core/storage-manager.js +9 -8
- package/src/core/tag-store.js +2 -5
- package/src/core/workspace-manager.js +4 -3
- package/src/protos/local_working_copy.json.js +146 -0
- package/src/protos/schema.js +49 -0
- package/src/protos/simple_op_store.json.js +374 -0
- package/src/types.d.ts +33 -0
- package/src/utils/atomic-write.js +3 -1
- package/src/utils/bytes.js +67 -0
- package/src/utils/id-generation.js +18 -4
- package/src/utils/mkdirp.js +41 -0
- package/src/utils/posix-path.js +117 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,227 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.0 — merge the scoped and unscoped version lineages
|
|
4
|
+
|
|
5
|
+
No code change — a deliberate, one-time exception to this family's usual
|
|
6
|
+
"version restarts at 0.0.0 on a scope move" convention, forced by making
|
|
7
|
+
the unscoped `isomorphic-jj` mirror permanent (see the unscoped-mirror
|
|
8
|
+
workflow's own history) rather than a one-off "final release" bridge:
|
|
9
|
+
`isomorphic-jj`'s unscoped name already had real published history through
|
|
10
|
+
**1.8.0** from before the `@johnhenry/*` rename, while the scoped package
|
|
11
|
+
had separately restarted at 0.0.0 and reached 0.4.0. Both names publish
|
|
12
|
+
the same version on every release from here on, so the shared version has
|
|
13
|
+
to sit above BOTH names' currently-published maximum, or the unscoped
|
|
14
|
+
mirror's `npm install isomorphic-jj` would move npm's `latest` tag
|
|
15
|
+
*backward* in version number the moment it published. `1.9.0` is one
|
|
16
|
+
minor above the unscoped side's existing 1.8.0 ceiling — chosen over a
|
|
17
|
+
patch bump because this release also carries real new functionality
|
|
18
|
+
(0.3.0/0.4.0's `jj converge`, N-way divergence resolution, and the four
|
|
19
|
+
other jj-v0.45 fixes), not just the version-numbering fix itself.
|
|
20
|
+
|
|
21
|
+
Going forward, `publish.yml`/`publish-unscoped.yml` both preflight-check
|
|
22
|
+
that the tag being published is strictly greater than whatever's
|
|
23
|
+
currently live under EITHER name before publishing either one, so this
|
|
24
|
+
can't silently regress again.
|
|
25
|
+
|
|
26
|
+
## 0.4.0 — `converge()` resolves any number of divergent copies
|
|
27
|
+
|
|
28
|
+
Follow-up to 0.3.0's `converge()` (#32): it originally capped automatic
|
|
29
|
+
resolution at exactly two divergent copies, throwing `CONVERGE_AMBIGUOUS`
|
|
30
|
+
for a third. That cap is gone — `converge()` now resolves any number of
|
|
31
|
+
copies of one change id in a single call. Real jj's own CLI still only
|
|
32
|
+
converges one *change id* per invocation as of v0.45.1 (an explicit TODO
|
|
33
|
+
in its own source: "consider adding logic to deal with more than one
|
|
34
|
+
divergent change-id in one invocation"), which is what this package's
|
|
35
|
+
changeId-at-a-time API already matched — this follow-up instead closes
|
|
36
|
+
the gap in what happens *within* that one change id, where real jj's
|
|
37
|
+
underlying engine (`TruncatedEvolutionGraph`/`find_divergent_changes`)
|
|
38
|
+
already resolves every divergent revision together, not just a pair.
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- **`converge()` is N-way, not pairwise.** The per-path resolution rule
|
|
43
|
+
generalizes directly: for each file, collect every DISTINCT value across
|
|
44
|
+
all copies that differs from the shared base. Zero distinct changed
|
|
45
|
+
values means nobody touched it; exactly one means only one side (or
|
|
46
|
+
several copies that happened to make the identical change) touched it,
|
|
47
|
+
and that value wins; two or more is a genuine conflict between that many
|
|
48
|
+
copies. `CONVERGE_AMBIGUOUS` no longer exists — there is nothing left for
|
|
49
|
+
it to guard against.
|
|
50
|
+
- **A conflict with more than two disagreeing copies carries a new `sides`
|
|
51
|
+
shape.** Every other conflict in this codebase (from `merge()`/
|
|
52
|
+
`rebase()`) is inherently two-sided and keeps `sides: { base, left,
|
|
53
|
+
right }`. An N-way `converge()` conflict instead gets `sides: { base,
|
|
54
|
+
versions }`, where `versions` is `Array<{ commitId, content }>` — one
|
|
55
|
+
entry per distinct disagreeing value, so a caller can tell which
|
|
56
|
+
divergent copy contributed which content without decoding a fixed
|
|
57
|
+
left/right pair. Added `ConflictModel.createNWayConflict()` as the one
|
|
58
|
+
place that builds this shape.
|
|
59
|
+
|
|
60
|
+
## 0.3.0 — Track jj through v0.45.1
|
|
61
|
+
|
|
62
|
+
Five fixes/additions found via real usage of isomorphic-jj in a browser
|
|
63
|
+
showcase app (ORRERY), spanning the browser entry, op-log/undo, and
|
|
64
|
+
graph-rewrite (squash/rebase/abandon) machinery — grouped into one release
|
|
65
|
+
because several of them touch the same shared machinery. Also closes the
|
|
66
|
+
version gap to jj v0.45.1 (v0.45.0 added `jj converge`; v0.45.1 was
|
|
67
|
+
patch-only, no JS-relevant changes).
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
- **The `/browser` entry can now actually run in a bundle (#28).**
|
|
72
|
+
`createBrowserFS()` called `require('@isomorphic-git/lightning-fs')`,
|
|
73
|
+
which doesn't exist in an ESM browser bundle; it's now an async
|
|
74
|
+
`import()`, and also accepts an injected `fs` (an instance, or a
|
|
75
|
+
LightningFS-shaped constructor) so callers can bring `memfs` or their
|
|
76
|
+
own. The four protobuf-backed stores (`JJCheckout`, `JJTreeState`,
|
|
77
|
+
`JJOperationStore`, `JJViewStore`) loaded their `.proto` schemas via
|
|
78
|
+
`protobuf.load()` against a `__dirname`-derived filesystem path, which
|
|
79
|
+
has no meaning in a bundle; they now build a `protobufjs` `Root` from
|
|
80
|
+
precompiled reflection JSON (`src/protos/*.json.js`, regenerated by
|
|
81
|
+
`npm run build:protos`, wired into `npm run build`) via the
|
|
82
|
+
dependency-free `protobufjs/light.js` entry point. Bundling a real
|
|
83
|
+
scratch app (`esbuild --platform=browser`) against the fix surfaced
|
|
84
|
+
several more Node-only assumptions that block a real browser run, fixed
|
|
85
|
+
the same way (isomorphic, not bundler-specific hacks): a bare
|
|
86
|
+
`import path from 'path'` (→ `src/utils/posix-path.js`, a tiny
|
|
87
|
+
POSIX-only join/dirname/resolve — this also fixes a latent Windows bug,
|
|
88
|
+
since Node's `path.join` uses backslashes there and git/jj paths must
|
|
89
|
+
never); `import crypto from 'crypto'` / dynamic `import('crypto')` (→
|
|
90
|
+
`randomHex()` in `id-generation.js`, built on the `crypto.getRandomValues`
|
|
91
|
+
global that file already used elsewhere); direct `Buffer.from`/
|
|
92
|
+
`Buffer.isBuffer` calls (→ `src/utils/bytes.js`, which only ever needs
|
|
93
|
+
`Uint8Array` and prefers a real `Buffer` when present so existing Node
|
|
94
|
+
behavior is unchanged); `fs.promises.mkdir(p, { recursive: true })`
|
|
95
|
+
(LightningFS silently ignores `recursive` and throws `ENOENT` on a
|
|
96
|
+
missing parent — `src/utils/mkdirp.js` walks and creates each segment
|
|
97
|
+
instead); and a few `readFile`/`writeFile` calls using the hyphenated
|
|
98
|
+
`'utf-8'` spelling (LightningFS only accepts the exact string `'utf8'`).
|
|
99
|
+
Verified end-to-end by bundling a scratch app and running it in real
|
|
100
|
+
headless Chromium: createBrowserFS → createJJ → git.init → write →
|
|
101
|
+
describe → log all completed successfully against IndexedDB-backed
|
|
102
|
+
LightningFS.
|
|
103
|
+
- **`undo()`/`operations.restore()` now revert graph rewrites, and `undo()`
|
|
104
|
+
walks progressively further back instead of undoing itself (#29).**
|
|
105
|
+
`squash()`/`moveChange()` (rebase)/`abandon()` never recorded a
|
|
106
|
+
changeSnapshot, so undo() had nothing to restore their in-place
|
|
107
|
+
ChangeGraph mutations — the DAG stayed rewritten after "undoing" them.
|
|
108
|
+
They now do, the same mechanism issue #12 introduced for `describe()`.
|
|
109
|
+
Calling `undo()` twice also only undid the undo: since `undo()` is
|
|
110
|
+
itself recorded as a new operation, a second `undo()` naively targeting
|
|
111
|
+
the log's raw parent chain landed back on the just-undone operation
|
|
112
|
+
instead of stepping further into the past — contradicting real jj's
|
|
113
|
+
documented behavior (`jj-undo(1)`: "If jj undo is used repeatedly, it
|
|
114
|
+
will restore increasingly older operations"). A new `resolveUndoTarget()`
|
|
115
|
+
recognizes 'undo'/'redo' bookkeeping operations in the log and steps
|
|
116
|
+
past them. `operations.restore()` previously only touched bookmarks and
|
|
117
|
+
the working-copy pointer; it now shares the same
|
|
118
|
+
`computeGraphReversal()`/`applyGraphReversal()` primitive as `undo()`/
|
|
119
|
+
`redo()` to fully revert the graph, files, and conflict state to any
|
|
120
|
+
earlier operation. Also fixed in passing: `undo()` used to
|
|
121
|
+
unconditionally clear ALL conflicts whenever the operation being undone
|
|
122
|
+
hadn't recorded its own conflict snapshot (true for every op except
|
|
123
|
+
`merge()`) — undoing an unrelated `describe()` silently wiped conflicts
|
|
124
|
+
from an earlier, unrelated `merge()`.
|
|
125
|
+
- **`squash()` merges file content, not just descriptions (#30).** It
|
|
126
|
+
combined the two descriptions into the destination but never moved the
|
|
127
|
+
source change's file contents into it — the squashed change's tree was
|
|
128
|
+
left completely unchanged. It now computes what source actually changed
|
|
129
|
+
relative to its own parent (added/modified/deleted paths) and folds just
|
|
130
|
+
that diff onto dest, rather than overwriting dest's whole snapshot with
|
|
131
|
+
source's.
|
|
132
|
+
- **`abandon(changeId)` re-parents children (#30).** It set the abandoned
|
|
133
|
+
flag but left children pointing at the abandoned commit; it now
|
|
134
|
+
re-parents every child onto the abandoned change's own parent(s)
|
|
135
|
+
(matching real `jj abandon`), including making a child a root when the
|
|
136
|
+
abandoned change had no parent.
|
|
137
|
+
- **`edit()`/`new()` fully sync the working copy (#30).** They wrote the
|
|
138
|
+
target change's files but only ever overlaid them on top of whatever was
|
|
139
|
+
already on disk, so a file present in the previously-checked-out change
|
|
140
|
+
but absent from the target leaked into the next snapshot. Both now go
|
|
141
|
+
through a shared `syncWorkingCopyFiles()` helper that deletes every
|
|
142
|
+
currently-tracked file NOT in the target snapshot in addition to writing
|
|
143
|
+
everything the target has. `new()` previously didn't touch the working
|
|
144
|
+
directory at all when targeting a change other than the current head's
|
|
145
|
+
default parent; it now checks the target out too.
|
|
146
|
+
- **`rebase()` performs a real three-way merge and detects conflicts
|
|
147
|
+
(#31).** Rebasing a change onto a sibling that edited the same lines
|
|
148
|
+
kept the rebased change's file contents verbatim and recorded no
|
|
149
|
+
conflict. `moveChange()` (which `rebase()` delegates to) now runs the
|
|
150
|
+
same `detectConflicts()` three-way merge `merge()` already used, against
|
|
151
|
+
(old base = the change's own parent before reparenting, old commit = the
|
|
152
|
+
change's own content, new base = the new parent's content), recording
|
|
153
|
+
any detected conflict the same way `merge()` does.
|
|
154
|
+
|
|
155
|
+
### Added
|
|
156
|
+
|
|
157
|
+
- **`converge(changeId)` — auto-resolve divergent commits (#32, jj
|
|
158
|
+
v0.45.0).** A change is divergent when more than one visible commit
|
|
159
|
+
shares its change id; this package's `divergent()` revset already
|
|
160
|
+
modeled that (`c.divergent === true`, plus a changeId-occurrence count
|
|
161
|
+
across the graph), but nothing ever actually produced a divergent
|
|
162
|
+
change — the flag was set nowhere, and `ChangeGraph.nodes` being a
|
|
163
|
+
`Map<changeId, change>` made two entries genuinely sharing a changeId
|
|
164
|
+
structurally impossible via `addChange()`. `ChangeGraph.addDivergentCopy()`
|
|
165
|
+
is the actual producer: it stores a second commit under a synthetic
|
|
166
|
+
internal key while keeping its own `changeId` field equal to the
|
|
167
|
+
original's, which is what activates `divergent()` for real (previously
|
|
168
|
+
dead code) and gives `converge()` something to resolve.
|
|
169
|
+
`converge({ changeId })` (a bare changeId string also works) attempts
|
|
170
|
+
automatic resolution using the same three-way-merge/conflict-detection
|
|
171
|
+
machinery `rebase()` uses (#31) — reused, not duplicated a third time in
|
|
172
|
+
this codebase. On success, it creates one replacement commit (a fresh
|
|
173
|
+
commit id, the merged file content, combined description) under the same
|
|
174
|
+
change id and drops the superseded copy. On a genuine per-path conflict,
|
|
175
|
+
it's reported as data (`{ resolved: false, conflicts }`), matching
|
|
176
|
+
`merge()`'s own non-throwing convention for conflicts, and the conflict
|
|
177
|
+
is recorded the normal way. Throws `NOT_DIVERGENT` when there's nothing
|
|
178
|
+
to converge, and `CONVERGE_AMBIGUOUS` when more than two visible copies
|
|
179
|
+
exist — automatic resolution only attempts a pairwise merge, matching
|
|
180
|
+
real jj's non-interactive mode aborting resolution it can't disambiguate
|
|
181
|
+
rather than guessing.
|
|
182
|
+
- **`ChangeGraph.deleteChange()`** — outright removal of a change record,
|
|
183
|
+
as distinct from `abandon()`'s soft-delete (`abandoned: true`) flag.
|
|
184
|
+
Added for undo()/redo()/operations.restore() to reverse an `addChange()`
|
|
185
|
+
when winding the graph back (#29), though in practice undo() never calls
|
|
186
|
+
it — see the doc comment on `computeGraphReversal()` for why a newly
|
|
187
|
+
created change is deliberately left in the graph (unreferenced, not
|
|
188
|
+
deleted) rather than torn down, matching real jj's "hidden, not gone"
|
|
189
|
+
model for unreachable commits.
|
|
190
|
+
|
|
191
|
+
### Judgment calls worth flagging
|
|
192
|
+
|
|
193
|
+
- **`write()`'s commit id/fileSnapshot staying stale until the next
|
|
194
|
+
snapshot is intentional, not a bug (#30).** jj has no staging area, but
|
|
195
|
+
real jj also doesn't recompute a tree/commit hash on every file write —
|
|
196
|
+
only when an actual `jj` command runs and snapshots the working copy.
|
|
197
|
+
`write()` is the equivalent of editing a file with your OS's own tools
|
|
198
|
+
outside of jj; `describe()`/`new()`/`edit()`/`status()`/etc. are the
|
|
199
|
+
equivalent of running a jj command. Documented rather than changed.
|
|
200
|
+
- **Divergent copies are modeled as "another commit sharing a changeId",
|
|
201
|
+
not "two independent changeIds grouped together" (#32)** — matching what
|
|
202
|
+
the pre-existing (if previously non-functional) `divergent()` revset
|
|
203
|
+
already implied, rather than inventing a parallel representation.
|
|
204
|
+
Converge only handles the common case where the two copies share the
|
|
205
|
+
same parent(s) (a real divergence — two rewrites of one prior position
|
|
206
|
+
in history) and are leaves (no children of their own); this package's
|
|
207
|
+
graph model references parents by changeId, not by a more granular
|
|
208
|
+
commit-level pointer the way real jj's does, so a divergent copy's own
|
|
209
|
+
independent descendants aren't distinguishable from the primary copy's —
|
|
210
|
+
documented as a known limitation of the simplified graph model rather
|
|
211
|
+
than a full rewrite of how parent/child edges are stored.
|
|
212
|
+
|
|
213
|
+
### Testing
|
|
214
|
+
|
|
215
|
+
1823 tests passing (was 1732 at the start of this pass); lint (0 errors),
|
|
216
|
+
format:check, typecheck, and build all still green. New test files:
|
|
217
|
+
`tests/integration/undo-redo-graph-rewrites.test.js`,
|
|
218
|
+
`tests/integration/squash-merge-abandon-reparent-file-sync.test.js`,
|
|
219
|
+
`tests/integration/rebase-conflict-detection.test.js`,
|
|
220
|
+
`tests/integration/converge.test.js`, plus new unit coverage for
|
|
221
|
+
`src/utils/posix-path.js`, `src/utils/bytes.js`, `src/utils/mkdirp.js`,
|
|
222
|
+
and `ChangeGraph`'s `deleteChange()`/`addDivergentCopy()`/
|
|
223
|
+
`getDivergentSiblings()`/`deleteDivergentCopy()`.
|
|
224
|
+
|
|
3
225
|
## 0.2.0 — 2026-09-02
|
|
4
226
|
|
|
5
227
|
The first `@johnhenry/isomorphic-jj` release since 0.1.0, and the first
|
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# @johnhenry/isomorphic-jj
|
|
2
2
|
|
|
3
|
-
> **This package has moved to `@johnhenry/isomorphic-jj`.** This is the final release under the old name.
|
|
4
|
-
|
|
5
3
|
> **Jujutsu version control for JavaScript**—stable change IDs, fearless undo, and no staging area. Works in Node.js and browsers.
|
|
6
4
|
|
|
7
5
|
Full documentation: [opensource.johnhenry.me/isomorphic-jj](https://opensource.johnhenry.me/isomorphic-jj/)
|
|
@@ -39,6 +37,29 @@ await jj.undo();
|
|
|
39
37
|
|
|
40
38
|
---
|
|
41
39
|
|
|
40
|
+
## Contents
|
|
41
|
+
|
|
42
|
+
- [Try it in 60 seconds](#try-it-in-60-seconds)
|
|
43
|
+
- [Quick Start](#quick-start)
|
|
44
|
+
- [Examples](#examples)
|
|
45
|
+
- [What's New in v1.0](#whats-new-in-v10)
|
|
46
|
+
- [How is this Different?](#how-is-this-different)
|
|
47
|
+
- [Features](#features)
|
|
48
|
+
- [Use Cases](#use-cases)
|
|
49
|
+
- [API Reference](#api-reference)
|
|
50
|
+
- [Why isomorphic-jj?](#why-isomorphic-jj)
|
|
51
|
+
- [Architecture](#architecture)
|
|
52
|
+
- [Project Status](#project-status)
|
|
53
|
+
- [Installation & Environment](#installation--environment)
|
|
54
|
+
- [Security model](#security-model)
|
|
55
|
+
- [Contributing](#contributing)
|
|
56
|
+
- [FAQ](#faq)
|
|
57
|
+
- [Family](#family)
|
|
58
|
+
- [License](#license)
|
|
59
|
+
- [Acknowledgments](#acknowledgments)
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
42
63
|
## Try it in 60 seconds
|
|
43
64
|
|
|
44
65
|
### CLI
|
|
@@ -86,7 +107,7 @@ console.log(await jj.log({ limit: 5 }));
|
|
|
86
107
|
import { createJJ } from '@johnhenry/isomorphic-jj';
|
|
87
108
|
import { createBrowserFS } from '@johnhenry/isomorphic-jj/browser';
|
|
88
109
|
|
|
89
|
-
const fs = createBrowserFS({ name: 'my-repo' });
|
|
110
|
+
const fs = await createBrowserFS({ name: 'my-repo' });
|
|
90
111
|
const jj = await createJJ({ fs, dir: '/repo' });
|
|
91
112
|
// Same API as Node from here on — see examples/12-browser.mjs.
|
|
92
113
|
```
|
|
@@ -189,7 +210,7 @@ isomorphic-jj v1.0 now implements virtually all commonly-used JJ CLI commands:
|
|
|
189
210
|
See [ROADMAP.md](./ROADMAP.md) for the release-by-release feature comparison and what's still planned.
|
|
190
211
|
|
|
191
212
|
### Testing & Quality
|
|
192
|
-
- **
|
|
213
|
+
- **1823 tests** passing — ~97% statement / 91% branch coverage
|
|
193
214
|
- **100% backward compatible** - All existing code continues to work
|
|
194
215
|
- **Zero breaking changes** - Deprecated features show warnings but still function
|
|
195
216
|
|
|
@@ -254,7 +275,7 @@ import git from 'isomorphic-git';
|
|
|
254
275
|
import http from 'isomorphic-git/http/web';
|
|
255
276
|
|
|
256
277
|
// Create browser filesystem (uses IndexedDB)
|
|
257
|
-
const fs = createBrowserFS({ name: 'my-repo' });
|
|
278
|
+
const fs = await createBrowserFS({ name: 'my-repo' });
|
|
258
279
|
|
|
259
280
|
const jj = await createJJ({ fs, dir: '/repo', git, http });
|
|
260
281
|
await jj.git.init({ userName: 'User', userEmail: 'user@example.com' });
|
|
@@ -732,7 +753,7 @@ import { createBrowserFS, requestPersistentStorage } from '@johnhenry/isomorphic
|
|
|
732
753
|
const persistent = await requestPersistentStorage();
|
|
733
754
|
|
|
734
755
|
// Create filesystem with IndexedDB backend
|
|
735
|
-
const fs = createBrowserFS({ backend: 'idb', name: 'my-repo' });
|
|
756
|
+
const fs = await createBrowserFS({ backend: 'idb', name: 'my-repo' });
|
|
736
757
|
|
|
737
758
|
// Check browser capabilities
|
|
738
759
|
import { detectCapabilities } from '@johnhenry/isomorphic-jj/browser';
|
|
@@ -905,8 +926,8 @@ const jj: JJ = await createJJ(options: CreateJJOptions);
|
|
|
905
926
|
- **Repository**: `init()`, `status()`, `stats()`
|
|
906
927
|
- **Files**: `write()`, `read()`, `cat()`, `move()` (deprecated for history), `remove()`, `listFiles()` | **Namespace**: `file.write()`, `file.show()`, `file.list()`, `file.move()`, `file.remove()`
|
|
907
928
|
- **Changes**: `describe()`, `new()`, `amend()`, `commit()`, `edit()`, `show()`
|
|
908
|
-
- **History**: `log()`, `obslog()`, `squash()`, `split()`, `rebase()`, `abandon()`, `unabandon()`
|
|
909
|
-
- **Operations**: `undo()`, `operations.list()`, `operations.at()`
|
|
929
|
+
- **History**: `log()`, `obslog()`, `squash()`, `split()`, `rebase()`, `abandon()`, `unabandon()`, `converge()`
|
|
930
|
+
- **Operations**: `undo()`, `redo()`, `operations.list()`, `operations.at()`, `operations.restore()`
|
|
910
931
|
- **Bookmarks**: `bookmark.list()`, `bookmark.set()`, `bookmark.move()`, `bookmark.delete()`
|
|
911
932
|
- **Git**: `git.init()`, `git.fetch()`, `git.push()`, `git.import()`, `git.export()`
|
|
912
933
|
- **Remotes**: `remote.add()`, `remote.fetch()`, `remote.push()`
|
|
@@ -999,10 +1020,10 @@ repo/
|
|
|
999
1020
|
|
|
1000
1021
|
## Project Status
|
|
1001
1022
|
|
|
1002
|
-
**Current Version**: 0.
|
|
1003
|
-
**Test Coverage**:
|
|
1004
|
-
**JJ CLI parity**: tracks Jujutsu through v0.
|
|
1005
|
-
**Status**: Actively developed; broad JJ CLI semantic parity, used in the examples/apps in this repo and in [JJHub](#
|
|
1023
|
+
**Current Version**: 0.3.0 (`@johnhenry/isomorphic-jj`; same library/API lineage as the old `isomorphic-jj`, which reached v1.7.0 before the rename — see the provenance note at the top of this file)
|
|
1024
|
+
**Test Coverage**: 1823 tests passing, ~97% statements / 91% branches
|
|
1025
|
+
**JJ CLI parity**: tracks Jujutsu through v0.45.1
|
|
1026
|
+
**Status**: Actively developed; broad JJ CLI semantic parity, used in the examples/apps in this repo and in [JJHub](#family). Not yet 1.0 — API surface can still shift; pin a version and read the CHANGELOG before upgrading.
|
|
1006
1027
|
|
|
1007
1028
|
**Completed:**
|
|
1008
1029
|
- ✅ v0.1: Core JJ experience (stable IDs, undo, bookmarks, revsets)
|
|
@@ -1014,6 +1035,7 @@ repo/
|
|
|
1014
1035
|
- ✅ v1.5: Parity refresh tracking jj through **v0.43** — new revsets (`change_id`, `commit_id`, `subject`, `author_name`/`email`, `committer*`, `signed`, `divergent`, `merges`, `forks`, `first_parent`, `first_ancestors`, `fork_point`, `merge_point`, `exactly`, `present`, `coalesce`, `remote_tags`, `ancestors(x, depth)`), working `tags()`/`remote_tags()` revsets, and new commands: `revert()`, `redo()`, `sign()`/`unsign()`, `file.search()`, `bookmark.advance()`, `tag.set()`. See [CHANGELOG.md](./CHANGELOG.md).
|
|
1015
1036
|
- ✅ 0.1.0 (post-rename): Parity refresh tracking jj through **v0.44** — `tag.track()`/`tag.untrack()`, the `builtin_log()` revset alias, and `file.search({ nameOnly: true })`; `git_refs()`/`git_head()` documented as deprecated (jj removed both in v0.43). See [CHANGELOG.md](./CHANGELOG.md) for what was deliberately left for a maintainer call (`jj run`, fetch-time remote tag/bookmark import).
|
|
1016
1037
|
- ✅ 0.2.0: Storage-layer audit fixes — same-process storage locking, `undo()` reverting in-place graph/tag mutations, ambiguous change-id detection, categorized `push()` error codes, a conflict-marker newline fix, atomic protobuf writes, and open-ended revset ranges (`..b`, `a..`, `::`). See [CHANGELOG.md](./CHANGELOG.md).
|
|
1038
|
+
- ✅ 0.3.0: Parity refresh tracking jj through **v0.45.1** — a working `/browser` entry (async `createBrowserFS()`, bundle-safe protobuf schema loading, no stray Node-only imports), `undo()`/`operations.restore()` reverting graph rewrites with real progressive undo/redo, `squash()`/`abandon()`/`edit()`/`new()` fully syncing content instead of only metadata, `rebase()` performing a real three-way merge and recording conflicts, and the new `converge()` command (jj v0.45.0) to auto-resolve divergent commits. See [CHANGELOG.md](./CHANGELOG.md).
|
|
1017
1039
|
|
|
1018
1040
|
See [ROADMAP.md](./ROADMAP.md) for detailed plans, and [CHANGELOG.md](./CHANGELOG.md) for the release history.
|
|
1019
1041
|
|
|
@@ -1023,7 +1045,7 @@ See [ROADMAP.md](./ROADMAP.md) for detailed plans, and [CHANGELOG.md](./CHANGELO
|
|
|
1023
1045
|
|
|
1024
1046
|
### Requirements
|
|
1025
1047
|
|
|
1026
|
-
- **Node.js**:
|
|
1048
|
+
- **Node.js**: 26.0.0 or higher, matching `engines.node` and CI (uses the Web Crypto global, stabilized in Node 20 — the family floor is higher than the functional minimum)
|
|
1027
1049
|
- **Browsers**: Chrome 90+, Firefox 88+, Safari 14+, Edge 90+
|
|
1028
1050
|
- **Optional**: Git CLI (for JJ CLI interop testing)
|
|
1029
1051
|
|
|
@@ -1044,6 +1066,58 @@ See [ROADMAP.md](./ROADMAP.md) for detailed plans, and [CHANGELOG.md](./CHANGELO
|
|
|
1044
1066
|
|
|
1045
1067
|
---
|
|
1046
1068
|
|
|
1069
|
+
## Security model
|
|
1070
|
+
|
|
1071
|
+
isomorphic-jj is a headless reimplementation of JJ's model, not a wrapper
|
|
1072
|
+
around the `jj` or `git` binaries — that architectural choice is also its
|
|
1073
|
+
main security property. It does not run a sandbox and does not vet the
|
|
1074
|
+
repositories you point it at.
|
|
1075
|
+
|
|
1076
|
+
**What isomorphic-jj guarantees:**
|
|
1077
|
+
|
|
1078
|
+
- **No shell-out, ever.** The library never spawns a child process to run
|
|
1079
|
+
`git`, `jj`, or any other binary — repository paths, branch/bookmark
|
|
1080
|
+
names, and commit messages are handled as plain JS values through
|
|
1081
|
+
`isomorphic-git`'s and `protobufjs`'s pure-JS encoders, never interpolated
|
|
1082
|
+
into a shell command string. This removes the whole class of
|
|
1083
|
+
shell-injection risk that a CLI-wrapping library (like `simple-git`) has
|
|
1084
|
+
to defend against by construction, not by escaping.
|
|
1085
|
+
- **Storage writes are atomic.** `jj-operation-store.js`, `jj-view-store.js`,
|
|
1086
|
+
and `jj-tree-state.js` route every write through a shared
|
|
1087
|
+
`atomicWriteFile()` temp+rename helper, and same-process concurrent writes
|
|
1088
|
+
to the same path are serialized through a path-keyed async mutex — a
|
|
1089
|
+
crash or a racing autosnapshot mid-write cannot leave a truncated or
|
|
1090
|
+
undecodable store file. Fixed in [PR #19](https://github.com/johnhenry/isomorphic-jj/pull/19) (0.2.0); scoped to
|
|
1091
|
+
same-process concurrency — cross-process locking is not yet implemented
|
|
1092
|
+
(see "still yours" below).
|
|
1093
|
+
|
|
1094
|
+
**What is still yours:**
|
|
1095
|
+
|
|
1096
|
+
- **Remote authentication and transport trust.** `git.push()`/`git.fetch()`
|
|
1097
|
+
delegate to `isomorphic-git`'s HTTP client (and, in the browser, whatever
|
|
1098
|
+
CORS proxy you configure) — isomorphic-jj neither stores nor validates
|
|
1099
|
+
credentials, and does not verify a remote server's identity beyond what
|
|
1100
|
+
that HTTP layer does. A misconfigured or malicious CORS proxy sees
|
|
1101
|
+
everything a normal Git remote would.
|
|
1102
|
+
- **Custom merge drivers run arbitrary code with full process access.**
|
|
1103
|
+
`mergeDrivers.register()` lets you register a function per file
|
|
1104
|
+
pattern (`'*.json'`, `'package.json'`, …); a registered driver — yours or
|
|
1105
|
+
a dependency's — executes with the same privileges as the rest of your
|
|
1106
|
+
process during every merge that touches a matching path. isomorphic-jj
|
|
1107
|
+
does not sandbox driver execution; treat a third-party merge driver the
|
|
1108
|
+
same as any other third-party code you'd `require()`.
|
|
1109
|
+
- **Cross-process locking is not implemented.** The same-process mutex
|
|
1110
|
+
above does not protect a repository opened by two separate OS processes
|
|
1111
|
+
at once (e.g. two CLI invocations, or a CLI run concurrently with a
|
|
1112
|
+
long-running server process) — don't rely on isomorphic-jj to serialize
|
|
1113
|
+
those for you.
|
|
1114
|
+
- **No allowlisting of untrusted repository content.** Cloning or importing
|
|
1115
|
+
a Git history runs its objects through the same parsing code regardless
|
|
1116
|
+
of whether you trust the source, matching `isomorphic-git`'s own model —
|
|
1117
|
+
this library adds no additional vetting layer.
|
|
1118
|
+
|
|
1119
|
+
---
|
|
1120
|
+
|
|
1047
1121
|
## Contributing
|
|
1048
1122
|
|
|
1049
1123
|
We welcome contributions! Here's how to help:
|
|
@@ -1085,7 +1159,7 @@ A: Yes! Colocated repos expose normal Git commits. Git users never see JJ metada
|
|
|
1085
1159
|
A: Comparable to isomorphic-git for Git operations. JJ metadata (JSON) is fast in Node, acceptable in browsers. Large histories need pagination.
|
|
1086
1160
|
|
|
1087
1161
|
**Q: Does this support all JJ features?**
|
|
1088
|
-
A: Broad semantic parity (tracks Jujutsu through v0.
|
|
1162
|
+
A: Broad semantic parity (tracks Jujutsu through v0.45.1), not every CLI/UX detail — it's a library, not a terminal UI. See [ROADMAP.md](./ROADMAP.md) for what's planned and what's deliberately out of scope.
|
|
1089
1163
|
|
|
1090
1164
|
**Q: Can I migrate my Git repo?**
|
|
1091
1165
|
A: Yes! `jj.git.init()` works on existing Git repositories.
|
|
@@ -1095,22 +1169,35 @@ A: JJ's model genuinely improves common workflows—stable change IDs, fearless
|
|
|
1095
1169
|
|
|
1096
1170
|
---
|
|
1097
1171
|
|
|
1098
|
-
##
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
- [
|
|
1113
|
-
-
|
|
1172
|
+
## Family
|
|
1173
|
+
|
|
1174
|
+
isomorphic-jj has no other package in the `@johnhenry` npm scope yet — it
|
|
1175
|
+
consumes and is consumed by projects outside that scope. Those relationships
|
|
1176
|
+
are still concrete enough to document precisely, so they're recorded here
|
|
1177
|
+
rather than under a vaguer "see also".
|
|
1178
|
+
|
|
1179
|
+
- **[JJHub](https://jjhub.erisera.com)** — a Jujutsu-native overlay on GitHub,
|
|
1180
|
+
and the closest thing this library has to a downstream consumer: it calls
|
|
1181
|
+
`createJJ()` directly (not a wrapped or forked copy) to get stable change
|
|
1182
|
+
IDs and stacked PRs on top of existing GitHub repos, platform-wide undo,
|
|
1183
|
+
and an MCP server exposing the same fearless-undo, no-staging-area
|
|
1184
|
+
workflow to coding agents. JJHub is a private Erisera product, not a
|
|
1185
|
+
`@johnhenry/*` package — install its CLI with `npm i -g jjhub`.
|
|
1186
|
+
- **[`isomorphic-git`](https://github.com/isomorphic-git/isomorphic-git)** —
|
|
1187
|
+
a real, non-optional dependency: this library's Git backend support
|
|
1188
|
+
(`jj.git.init/clone/push/fetch`) is built directly on isomorphic-git's
|
|
1189
|
+
pure-JS Git object model, not a fork or reimplementation of it. Peer
|
|
1190
|
+
dependency, not bundled — see [Installation & Environment](#installation--environment).
|
|
1191
|
+
|
|
1192
|
+
### Prior art
|
|
1193
|
+
|
|
1194
|
+
- [Jujutsu](https://jj-vcs.github.io/jj/) — the original JJ version control
|
|
1195
|
+
system this library reimplements the semantics of (headlessly, in JS; not
|
|
1196
|
+
a wrapper around the `jj` binary).
|
|
1197
|
+
- [simple-git](https://github.com/steveukx/git-js) — a Git CLI wrapper for
|
|
1198
|
+
Node.js; unlike isomorphic-jj, it shells out to the `git` binary rather
|
|
1199
|
+
than reimplementing the object model in JS.
|
|
1200
|
+
- [Git plumbing vs porcelain](https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain) — the architecture pattern this library's own layering follows.
|
|
1114
1201
|
|
|
1115
1202
|
---
|
|
1116
1203
|
|