isomorphic-jj 1.7.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 +425 -1
- package/LICENSE +1 -1
- package/README.md +233 -35
- package/bin/isojj.js +4 -1
- package/dist/index.cjs +2419 -378
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2419 -377
- package/dist/index.mjs.map +1 -1
- package/package.json +24 -10
- package/src/api/repository.js +966 -153
- package/src/backend/isomorphic-git-backend.js +72 -15
- package/src/browser/helpers.js +40 -18
- 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 +11 -19
- package/src/core/jj-tree-state.js +8 -18
- package/src/core/jj-view-store.js +9 -19
- package/src/core/merge-driver-registry.js +11 -10
- package/src/core/revset-engine.js +63 -6
- package/src/core/storage-manager.js +56 -18
- package/src/core/tag-store.js +42 -15
- 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 +46 -0
- 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/mutex.js +63 -0
- package/src/utils/posix-path.js +117 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,427 @@
|
|
|
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
|
+
|
|
225
|
+
## 0.2.0 — 2026-09-02
|
|
226
|
+
|
|
227
|
+
The first `@johnhenry/isomorphic-jj` release since 0.1.0, and the first
|
|
228
|
+
scoped release to carry the audit fixes from PR #19 below — **the published
|
|
229
|
+
0.1.0 tarball on npm predates #19** and does not include them.
|
|
230
|
+
|
|
231
|
+
### Fixed (PR #19 — audit findings)
|
|
232
|
+
|
|
233
|
+
Fixes all 8 findings from a bug-hunt audit of the storage/operation-log,
|
|
234
|
+
undo/redo, revset, and Git-push layers:
|
|
235
|
+
|
|
236
|
+
- **Same-process storage locking.** `Storage.appendLine()`/`write()` did
|
|
237
|
+
read-then-write with no lock, so concurrent same-process calls (e.g. a
|
|
238
|
+
user command racing `BackgroundOps`'s autosnapshot) could silently drop an
|
|
239
|
+
entry. Added a path-keyed in-process async mutex (`src/utils/mutex.js`)
|
|
240
|
+
that serializes the full critical section for both methods. Scoped to
|
|
241
|
+
same-process concurrency per the audit's guidance; cross-process locking
|
|
242
|
+
is a follow-up.
|
|
243
|
+
- **`undo()` now reverts in-place content mutations.** Previously it never
|
|
244
|
+
restored `ChangeGraph`/tag mutations made in place. `describe()` now
|
|
245
|
+
snapshots the affected `ChangeGraph` node before mutating it (mirroring
|
|
246
|
+
the existing `fileSnapshot` pattern), and `undo()` restores it.
|
|
247
|
+
`operations.revert()` now also diffs/restores `tags`, not just
|
|
248
|
+
`bookmarks`.
|
|
249
|
+
- **Ambiguous change-id prefixes now throw `AMBIGUOUS_ID`.** `show()`
|
|
250
|
+
previously resolved an ambiguous prefix silently to the first match; it
|
|
251
|
+
now throws `AMBIGUOUS_ID` listing all matches.
|
|
252
|
+
- **`push()` surfaces categorized error codes.** Previously all per-ref
|
|
253
|
+
errors were swallowed into a bare ref name. Rejected refs now carry a
|
|
254
|
+
categorized `code` (`AUTH_FAILED`/`NETWORK_ERROR`/`NON_FAST_FORWARD`/
|
|
255
|
+
`OTHER`) and `reason` so callers can tell an auth failure apart from a
|
|
256
|
+
non-fast-forward rejection instead of guessing.
|
|
257
|
+
- **Conflict-marker newline fix.** Markers could fuse content onto boundary
|
|
258
|
+
markers when content lacked a trailing newline; now forces a newline
|
|
259
|
+
separator for non-empty content.
|
|
260
|
+
- **Atomic protobuf writes.** `jj-operation-store.js`, `jj-view-store.js`,
|
|
261
|
+
and `jj-tree-state.js` risked a truncated/undecodable file on a mid-write
|
|
262
|
+
crash. Now route through a shared `atomicWriteFile()` temp+rename helper
|
|
263
|
+
(`src/utils/atomic-write.js`).
|
|
264
|
+
- **Weak temp-file naming fixed alongside storage locking** by adding a
|
|
265
|
+
random suffix, so two concurrent writes can't collide on the same temp
|
|
266
|
+
filename.
|
|
267
|
+
- **Open-ended revset ranges** (`..b`, `a..`, `::`) failed with a
|
|
268
|
+
misleading "invalid IDs" error; they now delegate to the existing
|
|
269
|
+
`ancestors()`/`descendants()`/`all()` implementations.
|
|
270
|
+
|
|
271
|
+
Closes #11–#18. See [PR #19](https://github.com/johnhenry/isomorphic-jj/pull/19).
|
|
272
|
+
|
|
273
|
+
### CI (PR #20)
|
|
274
|
+
|
|
275
|
+
- Dropped macOS from the test matrix — hosted macOS runners are scarce/slow
|
|
276
|
+
to schedule and this is a pure-JS library with no native code path; Linux
|
|
277
|
+
+ Windows already cover realistic cross-platform risk. See
|
|
278
|
+
[PR #20](https://github.com/johnhenry/isomorphic-jj/pull/20).
|
|
279
|
+
|
|
280
|
+
### Documentation overhaul: examples suite
|
|
281
|
+
|
|
282
|
+
- **New `examples/` directory: 12 numbered, runnable examples** covering the
|
|
283
|
+
core loop (init/describe/new/log), stable-ID stacked changes, history
|
|
284
|
+
editing (split/squash/abandon/duplicate), branching and merging (including
|
|
285
|
+
`new({ parents })` for true merge changes), first-class conflicts, revsets,
|
|
286
|
+
bookmarks + tags (with the v0.44 `tag.track()` surface), Git interop,
|
|
287
|
+
configuration layers, undo/oplog/time travel, the `file.*` namespace, and
|
|
288
|
+
browser usage. Each example runs in a temp directory, cleans up after
|
|
289
|
+
itself, and *asserts* what it demonstrates, so the set doubles as a smoke
|
|
290
|
+
test. `npm run examples` runs the loop; `npm run example:NN` runs one.
|
|
291
|
+
- **Example 08 exercises real `git.clone()`/`git.push()`/`git.fetch()` with
|
|
292
|
+
zero network**: it builds a bare fixture repo with the git CLI and serves
|
|
293
|
+
it on 127.0.0.1 through `git http-backend` (buffering request bodies so
|
|
294
|
+
CGI gets a `CONTENT_LENGTH` — isomorphic-git sends chunked bodies). Skips
|
|
295
|
+
cleanly where the git CLI is unavailable.
|
|
296
|
+
- **CI**: the test workflow now runs the examples loop as a smoke step on
|
|
297
|
+
one matrix leg (ubuntu-latest / Node 22), deliberately a different leg
|
|
298
|
+
than the coverage gate.
|
|
299
|
+
- Companion docs for the site (opensource.johnhenry.me) were written from
|
|
300
|
+
this repo's README/API/MIGRATION/CONFIGURATION material in the same pass;
|
|
301
|
+
behavioral quirks the examples surfaced (e.g. `file()` matching snapshots
|
|
302
|
+
rather than modifications, duration revsets needing committer timestamps,
|
|
303
|
+
`init()` after `git.clone()` re-rooting `refs/heads/main`) are documented
|
|
304
|
+
there rather than papered over.
|
|
305
|
+
|
|
306
|
+
### Publishing
|
|
307
|
+
|
|
308
|
+
- `@johnhenry/isomorphic-jj@0.2.0` is published from `main`. A final
|
|
309
|
+
unscoped bridge release, `isomorphic-jj@1.8.0` (same code as this
|
|
310
|
+
release), will follow so existing unscoped installs get one more version
|
|
311
|
+
pointing at the new package before the unscoped name is deprecated.
|
|
312
|
+
|
|
313
|
+
## 0.1.0 — Track jj through v0.44
|
|
314
|
+
|
|
315
|
+
Jujutsu shipped v0.44.0 (2026-08-05) since isomorphic-jj's last parity pass
|
|
316
|
+
(v1.5.0/README, which tracked jj through v0.43). This release closes the gap
|
|
317
|
+
for the parts of v0.43–v0.44 that are in scope for a headless, browser-safe
|
|
318
|
+
reimplementation of jj's model — a library, not the `jj` CLI's terminal UX or
|
|
319
|
+
its Rust process/Git-plumbing internals.
|
|
320
|
+
|
|
321
|
+
### Added
|
|
322
|
+
|
|
323
|
+
- **`tag.track({ name, remote })` / `tag.untrack({ name })`** — jj v0.44 made
|
|
324
|
+
tags remote-trackable the same way bookmarks already were, and added `jj
|
|
325
|
+
tag track`/`untrack` to manage that per-tag. `TagStore` now persists
|
|
326
|
+
tracking state (`{ remote, remoteName }` per local tag name) the same way
|
|
327
|
+
`BookmarkStore` already did for bookmarks, and `tag.list()` includes a
|
|
328
|
+
`tracking: { remote, ref }` field once a tag is tracked. The on-disk
|
|
329
|
+
`tags.json` format gained a `{ tags, tracked }` envelope; the old flat
|
|
330
|
+
`{ name: changeId }` format is still read for backward compatibility.
|
|
331
|
+
- **`builtin_log()` revset function** — jj v0.44 added `builtin_log()` as an
|
|
332
|
+
alias for the revset the built-in `jj log` uses by default, so a custom
|
|
333
|
+
`revsets.log` config can extend it instead of duplicating it. isomorphic-jj's
|
|
334
|
+
`log()` has always defaulted to `all()` when no revset is given, so
|
|
335
|
+
`builtin_log()` is wired to the same set and can be composed like any other
|
|
336
|
+
revset function (`builtin_log() & mine()`).
|
|
337
|
+
- **`file.search({ ..., nameOnly: true })`** — jj v0.44 changed `jj file
|
|
338
|
+
search`'s default CLI output to print every matched *line* (prefixed with
|
|
339
|
+
its file path), moving the old "just the file paths" behavior behind a new
|
|
340
|
+
`--name-only` flag. isomorphic-jj's `file.search()` already returned
|
|
341
|
+
structured per-line matches (`{ path, lineNumber, line }`) — that was
|
|
342
|
+
already the new default — so the only gap was the old, path-only shape;
|
|
343
|
+
`{ nameOnly: true }` now returns a deduplicated `string[]` of matching
|
|
344
|
+
paths, matching `--name-only`.
|
|
345
|
+
|
|
346
|
+
### Changed
|
|
347
|
+
|
|
348
|
+
- **`git_refs()` / `git_head()` revsets are now documented as deprecated.**
|
|
349
|
+
Upstream jj deprecated both in favor of `bookmarks()`/`tags()`/`@`, then
|
|
350
|
+
removed them outright in jj v0.43 (calling them in real jj now errors).
|
|
351
|
+
isomorphic-jj keeps them working — removing a function outright is a
|
|
352
|
+
breaking API change this pass didn't want to force on existing callers
|
|
353
|
+
unilaterally — but they're no longer listed in the revset engine's
|
|
354
|
+
"did you mean" suggestion text, and their JSDoc now calls out the upstream
|
|
355
|
+
removal and recommends `bookmarks()`/`tags()`/`@` instead. See "Not
|
|
356
|
+
changed" below for the case for a harder break in a future major version.
|
|
357
|
+
|
|
358
|
+
### Testing
|
|
359
|
+
|
|
360
|
+
1714 tests passing (was 1701); lint (0 errors), format:check, typecheck, and
|
|
361
|
+
build all still green.
|
|
362
|
+
|
|
363
|
+
### Not changed (needs a human design call)
|
|
364
|
+
|
|
365
|
+
jj v0.43–v0.44 included several changes this pass deliberately left alone:
|
|
366
|
+
|
|
367
|
+
- **`jj run`** (v0.43 new command, gained `--passthrough`/`--ignore-changes`/
|
|
368
|
+
`--ignore-errors`/oldest-first-ordering in v0.44) — runs an arbitrary
|
|
369
|
+
subprocess against each revision's own private working copy. isomorphic-jj
|
|
370
|
+
doesn't shell out to anything and has to keep working in browsers with no
|
|
371
|
+
process/filesystem access; "run a command" doesn't have an isomorphic
|
|
372
|
+
equivalent. Adding it would mean either a Node-only stub (inconsistent with
|
|
373
|
+
the rest of the API surface) or scope-creeping into a job-runner. Flagging
|
|
374
|
+
for a maintainer decision rather than guessing.
|
|
375
|
+
- **Auto-importing remote tags/bookmarks during `git.fetch()`.** jj v0.44's
|
|
376
|
+
headline change is that `jj git fetch` now fetches tags the same way it
|
|
377
|
+
fetches bookmarks — as `<name>@<remote>`, auto-tracked by default. Wiring
|
|
378
|
+
that up properly for tags would need `git.fetch()` to actually populate
|
|
379
|
+
`TagStore`'s (and, by the same logic, `BookmarkStore`'s) remote-tracking
|
|
380
|
+
state from the fetched refs. Investigating this surfaced a **pre-existing**
|
|
381
|
+
gap: `git.fetch()` already doesn't call `bookmarks.setRemote()` for fetched
|
|
382
|
+
bookmarks today, even though `BookmarkStore.setRemote()`/`getRemote()` have
|
|
383
|
+
existed since v0.4 — remote ref sync was never wired all the way through at
|
|
384
|
+
the fetch call site. Building tag-fetch-tracking on top of that gap would
|
|
385
|
+
mean either (a) fixing the older bookmark gap too as a drive-by, our
|
|
386
|
+
papering over an inconsistency where tags "worked" and bookmarks didn't, or
|
|
387
|
+
(b) building a second, tag-only special case. Both are judgment calls
|
|
388
|
+
about a real architectural gap, not a mechanical port of one version's
|
|
389
|
+
changelog — left for a maintainer to decide. `tag.track()`/`untrack()`
|
|
390
|
+
(added this pass) at least let callers record and query tracking intent by
|
|
391
|
+
hand in the meantime.
|
|
392
|
+
- **`jj git clone --tag=PATTERN` / `--fetch-tags` removal.** Real jj replaced
|
|
393
|
+
`--fetch-tags=all|none|included` with `--tag=PATTERN` in v0.44. isojj's
|
|
394
|
+
`git.clone()`/`git.fetch()` never exposed CLI-shaped `--fetch-tags` flags in
|
|
395
|
+
the first place (just a boolean `noTags`), so there's no removed flag to
|
|
396
|
+
mirror; a `PATTERN`-based tag filter for clone/fetch would be new surface
|
|
397
|
+
area tied to the fetch-tracking gap above.
|
|
398
|
+
- **`jj git push --allow-conflicts`.** Real jj normally refuses to push
|
|
399
|
+
commits containing conflicts unless this flag is passed. isojj's
|
|
400
|
+
`git.push()` doesn't currently check for conflicts before pushing at all —
|
|
401
|
+
there's no existing guard to add a bypass flag for. Adding the guard itself
|
|
402
|
+
(and then the bypass) is more surface area than a one-version parity pass.
|
|
403
|
+
- **CLI argument parsing "last occurrence wins" (v0.44).** Checked
|
|
404
|
+
`bin/isojj.js`'s `parseArgs()`: it already stores each `--flag` into a
|
|
405
|
+
plain object keyed by flag name, so repeating a flag already naturally
|
|
406
|
+
overwrites the earlier value with no error — this was already jj v0.44's
|
|
407
|
+
new behavior by construction. No change needed.
|
|
408
|
+
- **`try(expr, fallback...)` template function, `jj workspace list` root
|
|
409
|
+
display, `diff.stat.max-bar-width`, `colors.crossed-out`, Gerrit/config-gc/
|
|
410
|
+
`/etc/jj` config-discovery changes.** These are jj's template engine and
|
|
411
|
+
terminal/config-file layers, which isomorphic-jj's CLI (a thin, generic
|
|
412
|
+
API-argument passthrough — see `bin/isojj.js`) doesn't attempt to replicate
|
|
413
|
+
1:1 for any command. Out of scope for this pass; noted here for
|
|
414
|
+
completeness of the v0.43/v0.44 diff.
|
|
415
|
+
|
|
416
|
+
## 0.0.0
|
|
417
|
+
|
|
418
|
+
- **Renamed: `isomorphic-jj` is now `@johnhenry/isomorphic-jj`, restarted at 0.0.0.** Same library, same API — a new address and era, not a maturity signal (1.7.0 lineage).
|
|
419
|
+
|
|
420
|
+
```sh
|
|
421
|
+
npm install @johnhenry/isomorphic-jj
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
|
|
3
425
|
All notable changes to isomorphic-jj are documented here. The project tracks the
|
|
4
426
|
[Jujutsu (jj)](https://github.com/jj-vcs/jj) CLI; each release notes the upstream
|
|
5
427
|
jj version whose semantics it targets.
|
|
@@ -171,7 +593,9 @@ adds a batch of revset functions and commands, and cleans up the toolchain.
|
|
|
171
593
|
- `forks()` — changes with more than one child.
|
|
172
594
|
- `first_parent(x)` / `first_ancestors(x)` — first-parent navigation (jj v0.32).
|
|
173
595
|
- `fork_point(x)` — youngest common ancestor of a set (jj v0.32).
|
|
174
|
-
- `merge_point(x)` — youngest common descendant of a set
|
|
596
|
+
- `merge_point(x)` — youngest common descendant of a set (implemented ahead of
|
|
597
|
+
upstream; jj stabilized its own `merge_point()` in v0.44 with matching
|
|
598
|
+
semantics — "the point where multiple branches merge").
|
|
175
599
|
- `exactly(x, n)` — the set, but errors unless it has exactly `n` elements (jj v0.34).
|
|
176
600
|
- `present(x)` — evaluate without erroring on unknown symbols.
|
|
177
601
|
- `coalesce(a, b, …)` — the first argument that resolves to a non-empty set.
|