isomorphic-jj 1.10.0 → 1.12.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 +207 -0
- package/dist/index.cjs +384 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +384 -23
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/api/repository.js +343 -17
- package/src/core/revset-engine.js +41 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,212 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.12.0 — 2026-09-27 — op-recording gaps left by #43/#45's fixes (#48)
|
|
4
|
+
|
|
5
|
+
Follow-up to #43/#45 (below). Walking every op backward then forward now
|
|
6
|
+
lands correctly in 49 of 60 restores (up from before); the 11 misses were
|
|
7
|
+
five distinct gaps, all in the operation-recording machinery those fixes
|
|
8
|
+
introduced or depend on, plus one pre-existing bug in `operations.list()`
|
|
9
|
+
discovered while writing regression tests for the others.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **The descendant rebase performed by `edit()`/`snapshot()`/
|
|
14
|
+
`autoSnapshotWorkingCopy()` was never itself recorded in the operation
|
|
15
|
+
(R1).** `_rebuildDescendants()` mutates every rebased descendant via
|
|
16
|
+
`graph.updateChange()`, but had no way to hand its callers each
|
|
17
|
+
descendant's pre-rebase state — so `undo()`/`operations.restore()` had
|
|
18
|
+
nothing to reverse the rebase with, even though the ANCESTOR's own
|
|
19
|
+
content change was (already) correctly captured. Added an optional 4th
|
|
20
|
+
parameter, `outChangeSnapshot`, populated with `{[descendantId]:
|
|
21
|
+
fullRecordBeforeRebuild}` for every descendant actually mutated; all
|
|
22
|
+
three call sites now pass their own `changeSnapshot` object through so
|
|
23
|
+
it lands in the same operation record.
|
|
24
|
+
- **Restoring to a `conflicts.resolve()` (or `merge()`/`converge()`/
|
|
25
|
+
`moveChange()`) op could restore the resolved file but leave the
|
|
26
|
+
change's conflict state unresolved (R2).** `computeGraphReversal()`
|
|
27
|
+
recovers "conflict state right after `landingOp`" by scanning for a
|
|
28
|
+
LATER op's own "before" snapshot — which only exists if some later op
|
|
29
|
+
also touches conflicts. Conflict resolution is usually a one-off event
|
|
30
|
+
with nothing later re-touching the `ConflictModel` at all, so landing
|
|
31
|
+
exactly on it found no candidate and silently left whatever conflict
|
|
32
|
+
state already happened to be live untouched (file content usually
|
|
33
|
+
recovered correctly anyway, by coincidence — some unrelated later op
|
|
34
|
+
touching the same file's `changeSnapshot`). `conflicts.resolve()`,
|
|
35
|
+
`merge()`, `converge()` (unresolved case), and `moveChange()` now also
|
|
36
|
+
record `conflictsSnapshotAfter` — the result of the mutation THEY
|
|
37
|
+
performed — and `computeGraphReversal()` falls back to it when nothing
|
|
38
|
+
later reveals the answer.
|
|
39
|
+
- **A file changed directly on disk and picked up by
|
|
40
|
+
`autoSnapshotWorkingCopy()` (not the explicit `snapshot()` API) had no
|
|
41
|
+
"before" copy recorded anywhere (R3) — described as "silent
|
|
42
|
+
re-snapshot" when it also rebases descendants.** `autoSnapshotWorkingCopy()`
|
|
43
|
+
— the implicit reconciliation `status()`/`read()`/`describe()`/etc.
|
|
44
|
+
trigger on every call — mutated the graph (and rebased descendants) with
|
|
45
|
+
no operation recorded at all. A later `undo()`/`operations.restore()`
|
|
46
|
+
had nothing to reverse it with, and since the same function runs again
|
|
47
|
+
on every subsequent call with no drift left to detect, a real,
|
|
48
|
+
one-time content change looked exactly like it had simply never
|
|
49
|
+
happened. Now records a real operation (mirroring the explicit
|
|
50
|
+
`snapshot()` API, `description: 'auto-snapshot working copy'`) whenever
|
|
51
|
+
it actually refreshes content — gated on the same `added`/`modified`/
|
|
52
|
+
`deleted` check it already uses, so a no-op call still records nothing.
|
|
53
|
+
- **After `edit()` moves `@` to an older change, `log()` could hide a real,
|
|
54
|
+
undescribed tip with real work in it, and `edit()` recorded only its own
|
|
55
|
+
target in `view.heads`.** `_computeHiddenOrphans`'s `autoCreated ||`
|
|
56
|
+
branch (needed for two undo-artifact cases — see its doc comment) has no
|
|
57
|
+
way to distinguish "a synthetic scaffolding change that's still
|
|
58
|
+
discardable" from "a real, live tip the user simply isn't checked out on
|
|
59
|
+
right now" once the FLAG is set once at creation and never cleared.
|
|
60
|
+
`edit()` recording only its own target in `view.heads` (correct back
|
|
61
|
+
when `edit()` was a pure checkout, wrong now that it also commits the
|
|
62
|
+
change being left) meant this package's own "explicit, maintained set of
|
|
63
|
+
live heads" proxy never saw the other, real tip at all. Fixed two ways:
|
|
64
|
+
`edit()`'s recorded `view.heads` now uses `computeCurrentHeads()` (every
|
|
65
|
+
non-abandoned change with no non-abandoned children) instead of
|
|
66
|
+
`[args.changeId]`; and `RevsetEngine` now takes the operation log as a
|
|
67
|
+
constructor param and `_computeHiddenOrphans` adds the latest op's
|
|
68
|
+
`view.heads` to its `trackedHeads` signal, alongside the working-copy
|
|
69
|
+
pointer and every bookmark/tag target — the "explicit, maintained set of
|
|
70
|
+
heads" this package's own doc comment already said real jj's View keeps,
|
|
71
|
+
now actually wired through.
|
|
72
|
+
- **`operations.list()` reversed its own internal array in place instead
|
|
73
|
+
of a copy, silently scrambling operation order on the SECOND call within
|
|
74
|
+
a session.** `oplog.list()` returns its live internal array by
|
|
75
|
+
reference (by design — `computeGraphReversal()` and others need the
|
|
76
|
+
real, oldest-first list); `operations.list()`'s own `.reverse()` (to
|
|
77
|
+
present newest-first) mutated that same array as a side effect. Calling
|
|
78
|
+
it twice — a completely reasonable thing to do (list history, do
|
|
79
|
+
something, list history again) — un-reversed it back to oldest-first,
|
|
80
|
+
and anything recorded via `oplog.recordOperation()` in between landed
|
|
81
|
+
appended to what was, at that moment, the WRONG end. Found while writing
|
|
82
|
+
a regression test for the fixes above (two `operations.list()` calls
|
|
83
|
+
around one `autoSnapshotWorkingCopy()`), not from the issue's own repro
|
|
84
|
+
— but real and, in principle, capable of corrupting `undo()`/`redo()`/
|
|
85
|
+
`operations.restore()` for any caller that also calls
|
|
86
|
+
`jj.operations.list()` earlier in the same session. Fixed by copying
|
|
87
|
+
before reversing.
|
|
88
|
+
|
|
89
|
+
See `tests/integration/issue-43-restore-reliability.test.js`'s new
|
|
90
|
+
"issue #48" suites (8 new tests across all five gaps above) and
|
|
91
|
+
`tests/unit/core/revset-branches-coverage.test.js`'s new oplog-signal
|
|
92
|
+
suite (2 tests) for the hidden-tips fix specifically. All verified with
|
|
93
|
+
a negative control: reverting just the source changes fails exactly
|
|
94
|
+
these new tests while every pre-existing test still passes, confirming
|
|
95
|
+
they exercise real behavior rather than being tautological.
|
|
96
|
+
|
|
97
|
+
## 1.11.0 — 2026-09-26 — edit() of an older change rebases its descendants (#45)
|
|
98
|
+
|
|
99
|
+
Part 5 of #43 (parts 1-4 shipped in 1.10.0 / #44), previously punted because
|
|
100
|
+
an initial attempt broke `tests/integration/absorb.test.js`'s "should work
|
|
101
|
+
after edit()" test.
|
|
102
|
+
|
|
103
|
+
### Fixed
|
|
104
|
+
|
|
105
|
+
- **`snapshot()` and `autoSnapshotWorkingCopy()` now propagate an edited
|
|
106
|
+
non-leaf change's new content onto its descendants (#45).** `edit()`
|
|
107
|
+
itself never touches content — it's a pure checkout (moves `@`, syncs disk
|
|
108
|
+
to the target's existing `fileSnapshot`). The actual moment a change's
|
|
109
|
+
*committed* content changes is inside the public `snapshot()` and the
|
|
110
|
+
internal `autoSnapshotWorkingCopy()`, which reconcile on-disk state into
|
|
111
|
+
the CURRENT working-copy change's `fileSnapshot`. If you `edit()`'d into a
|
|
112
|
+
change that already had descendants and then modified a file, both
|
|
113
|
+
functions updated only that change's own `fileSnapshot` and never
|
|
114
|
+
propagated the update, so the descendants stayed permanently stale unless
|
|
115
|
+
you happened to `edit()` directly into one of them (which just re-synced
|
|
116
|
+
disk FROM its own still-stale snapshot — no help). Both now check for
|
|
117
|
+
descendants (`_findDescendants`) after committing the new content and, if
|
|
118
|
+
any exist, rebase them onto it (`_rebuildDescendants`) — matching real
|
|
119
|
+
jj's automatic rebase-on-edit. `edit()` was correctly left untouched: it
|
|
120
|
+
never mutates content on the way IN, so it was never the right hook point.
|
|
121
|
+
- **Un-deprecated `_rebuildDescendants()` and fixed a real-content-vs-
|
|
122
|
+
synthetic-edit ordering bug in it (#45).** This line-level, three-way-
|
|
123
|
+
merge-style rebase helper had been marked `@deprecated` with zero callers
|
|
124
|
+
(`absorb()` uses a different function, `_rebuildDescendantsWithStates`) —
|
|
125
|
+
reused here instead of writing a second rebase implementation. Its first
|
|
126
|
+
pass builds "the original parent state" for every change by reading the
|
|
127
|
+
parent's CURRENT `fileSnapshot` live from the graph. Called (as it must
|
|
128
|
+
be) *after* the ancestor's own `graph.updateChange()` has already written
|
|
129
|
+
its new content, that live read captures the NEW content as if it were
|
|
130
|
+
the pre-edit baseline for the ancestor's DIRECT children — every line of
|
|
131
|
+
a stale descendant then looks like the descendant's own deliberate edit
|
|
132
|
+
(stale != "original"), so the old content gets preserved verbatim: a
|
|
133
|
+
silent no-op that reproduces the exact bug rather than fixing it. Added an
|
|
134
|
+
optional third parameter, `originalAncestorSnapshot`, that callers supply
|
|
135
|
+
with the ancestor's pre-mutation snapshot; the first pass uses it in place
|
|
136
|
+
of the live read specifically for the ancestor's direct children. Deeper
|
|
137
|
+
descendants are unaffected (their own immediate parent hasn't been mutated
|
|
138
|
+
yet when the function reads it, so the live read is correct for them).
|
|
139
|
+
- **`edit()`'s OWN inline content-commit — a third site the first pass
|
|
140
|
+
missed — now propagates too (#45).** There are three places a checked-out
|
|
141
|
+
change's content actually gets committed, not two: alongside the public
|
|
142
|
+
`snapshot()` and the internal `autoSnapshotWorkingCopy()` above, `edit()`
|
|
143
|
+
has its own separate inline step that snapshots disk for the change being
|
|
144
|
+
switched AWAY FROM and commits it via `graph.updateChange()` directly —
|
|
145
|
+
entirely bypassing both other functions. The first pass explicitly left
|
|
146
|
+
`edit()` untouched, on the theory (recorded in this same entry, in an
|
|
147
|
+
earlier draft) that this site was pure checkout bookkeeping unrelated to
|
|
148
|
+
content commits, and that `tests/integration/absorb.test.js`'s "should
|
|
149
|
+
work after edit()" would therefore keep passing unmodified. Both halves of
|
|
150
|
+
that theory were wrong: this inline step *is* a real content commit (it's
|
|
151
|
+
literally how `write()`-then-`edit()`-away ever reaches the graph at all,
|
|
152
|
+
since `write()` itself only touches disk), and it needed the exact same
|
|
153
|
+
descendant-propagation treatment as the other two sites. Once added, the
|
|
154
|
+
`absorb.test.js` collision the very first attempt at #45 hit *did*
|
|
155
|
+
materialize here — confirming this was the actual site that original
|
|
156
|
+
attempt (and this issue's own motivating repro: "edit an ancestor, modify
|
|
157
|
+
it, edit back") was about all along.
|
|
158
|
+
- **Fixed the `absorb.test.js` fixture, not the feature.** Traced exactly
|
|
159
|
+
why: "should work after edit()" wrote `file.txt` on `change1`, edited away
|
|
160
|
+
to `change2`, then called `absorb()` expecting it to fold `change2`'s
|
|
161
|
+
(stale) content back into `change1`. That "diff" only existed because
|
|
162
|
+
`change2` hadn't picked up `change1`'s edit — precisely the bug being
|
|
163
|
+
fixed. Once fixed, `change2` correctly matches `change1` the moment you
|
|
164
|
+
edit away, the manufactured diff disappears, and `absorb()` correctly
|
|
165
|
+
finds nothing to do. Updated the fixture to make a genuine *new* edit at
|
|
166
|
+
`change2` after returning to it, so `absorb()` has a real, bug-independent
|
|
167
|
+
diff to fold — preserving the test's actual intent ("absorb still works
|
|
168
|
+
normally in a session where an ancestor was edited earlier") without
|
|
169
|
+
depending on the bug.
|
|
170
|
+
- **Fixed a second, pre-existing bug in `_rebuildDescendants()` that this
|
|
171
|
+
same follow-up surfaced: file deletions were resurrected as empty
|
|
172
|
+
strings.** The function had no way to represent "this file doesn't exist"
|
|
173
|
+
separately from "empty content" — every file was coerced with `|| ''`
|
|
174
|
+
before diffing. A descendant that deliberately deleted a file the parent
|
|
175
|
+
still has (e.g. `backout()`'s reversal change, which legitimately has no
|
|
176
|
+
`fileSnapshot` entry at all for a removed file) got that file rebuilt as
|
|
177
|
+
`''` instead of staying deleted, breaking
|
|
178
|
+
`tests/integration/backout.test.js`'s two file-addition-reversal tests.
|
|
179
|
+
This was always latent — `_rebuildDescendants()` had zero callers at all
|
|
180
|
+
before this issue gave it a real one, so the bug was never exercised.
|
|
181
|
+
Fixed by tracking each file's existence (`hasOwnProperty`, not `||`) on
|
|
182
|
+
all three sides (descendant, original parent, updated parent) and
|
|
183
|
+
handling "descendant deleted it," "descendant added it fresh," and "both
|
|
184
|
+
sides have real content" as distinct cases, rather than collapsing all of
|
|
185
|
+
them into one string-diff.
|
|
186
|
+
|
|
187
|
+
### Testing
|
|
188
|
+
|
|
189
|
+
New file `tests/integration/issue-43-restore-reliability.test.js`, describe
|
|
190
|
+
block "issue #45": edit into a non-leaf change, modify a file, and confirm
|
|
191
|
+
the descendant picks up the edit (a) via an explicit `jj.snapshot()` call,
|
|
192
|
+
(b) via `autoSnapshotWorkingCopy()` triggered by `jj.status()` after an
|
|
193
|
+
out-of-band disk write (a same-size `jj.write()` wouldn't reach this path —
|
|
194
|
+
`write()` immediately updates the tracked mtime/size itself, so the
|
|
195
|
+
disk-walk would see nothing "modified"), and (c) via `edit()`-away alone
|
|
196
|
+
with no explicit `snapshot()`/`status()` call in between — all three checked
|
|
197
|
+
via `jj.show()`. A fourth test confirms a leaf change (no descendants) still
|
|
198
|
+
works, exercising the other side of the `descendants.length > 0` branch. A
|
|
199
|
+
fifth test constructs a descendant that deliberately deleted a file and
|
|
200
|
+
confirms rebuilding it preserves that deletion instead of resurrecting an
|
|
201
|
+
empty string. Negative controls: with the `src/api/repository.js` changes
|
|
202
|
+
reverted, the propagation tests fail with stale pre-edit content, the
|
|
203
|
+
deletion test fails with a resurrected `''`, and the leaf-change test still
|
|
204
|
+
passes; reapplying the fix makes all five pass, and
|
|
205
|
+
`tests/integration/backout.test.js`'s two previously-broken tests pass
|
|
206
|
+
again too. 1864 tests passing; lint (0 errors), format:check, typecheck,
|
|
207
|
+
and build all green; all 12 `examples/*.mjs` run clean; branch coverage
|
|
208
|
+
90.06% (gate: 90%).
|
|
209
|
+
|
|
3
210
|
## 1.10.0 — 2026-09-26 — operations.restore() reliability (#43)
|
|
4
211
|
|
|
5
212
|
A substantial follow-up to 1.9.1/1.9.2's undo/restore work (#37/#38/#39/#41):
|