isomorphic-jj 1.9.2 → 1.11.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 +239 -0
- package/dist/index.cjs +448 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +448 -22
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/api/repository.js +419 -21
- package/src/core/revset-engine.js +29 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,244 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.11.0 — 2026-09-26 — edit() of an older change rebases its descendants (#45)
|
|
4
|
+
|
|
5
|
+
Part 5 of #43 (parts 1-4 shipped in 1.10.0 / #44), previously punted because
|
|
6
|
+
an initial attempt broke `tests/integration/absorb.test.js`'s "should work
|
|
7
|
+
after edit()" test.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **`snapshot()` and `autoSnapshotWorkingCopy()` now propagate an edited
|
|
12
|
+
non-leaf change's new content onto its descendants (#45).** `edit()`
|
|
13
|
+
itself never touches content — it's a pure checkout (moves `@`, syncs disk
|
|
14
|
+
to the target's existing `fileSnapshot`). The actual moment a change's
|
|
15
|
+
*committed* content changes is inside the public `snapshot()` and the
|
|
16
|
+
internal `autoSnapshotWorkingCopy()`, which reconcile on-disk state into
|
|
17
|
+
the CURRENT working-copy change's `fileSnapshot`. If you `edit()`'d into a
|
|
18
|
+
change that already had descendants and then modified a file, both
|
|
19
|
+
functions updated only that change's own `fileSnapshot` and never
|
|
20
|
+
propagated the update, so the descendants stayed permanently stale unless
|
|
21
|
+
you happened to `edit()` directly into one of them (which just re-synced
|
|
22
|
+
disk FROM its own still-stale snapshot — no help). Both now check for
|
|
23
|
+
descendants (`_findDescendants`) after committing the new content and, if
|
|
24
|
+
any exist, rebase them onto it (`_rebuildDescendants`) — matching real
|
|
25
|
+
jj's automatic rebase-on-edit. `edit()` was correctly left untouched: it
|
|
26
|
+
never mutates content on the way IN, so it was never the right hook point.
|
|
27
|
+
- **Un-deprecated `_rebuildDescendants()` and fixed a real-content-vs-
|
|
28
|
+
synthetic-edit ordering bug in it (#45).** This line-level, three-way-
|
|
29
|
+
merge-style rebase helper had been marked `@deprecated` with zero callers
|
|
30
|
+
(`absorb()` uses a different function, `_rebuildDescendantsWithStates`) —
|
|
31
|
+
reused here instead of writing a second rebase implementation. Its first
|
|
32
|
+
pass builds "the original parent state" for every change by reading the
|
|
33
|
+
parent's CURRENT `fileSnapshot` live from the graph. Called (as it must
|
|
34
|
+
be) *after* the ancestor's own `graph.updateChange()` has already written
|
|
35
|
+
its new content, that live read captures the NEW content as if it were
|
|
36
|
+
the pre-edit baseline for the ancestor's DIRECT children — every line of
|
|
37
|
+
a stale descendant then looks like the descendant's own deliberate edit
|
|
38
|
+
(stale != "original"), so the old content gets preserved verbatim: a
|
|
39
|
+
silent no-op that reproduces the exact bug rather than fixing it. Added an
|
|
40
|
+
optional third parameter, `originalAncestorSnapshot`, that callers supply
|
|
41
|
+
with the ancestor's pre-mutation snapshot; the first pass uses it in place
|
|
42
|
+
of the live read specifically for the ancestor's direct children. Deeper
|
|
43
|
+
descendants are unaffected (their own immediate parent hasn't been mutated
|
|
44
|
+
yet when the function reads it, so the live read is correct for them).
|
|
45
|
+
- **`edit()`'s OWN inline content-commit — a third site the first pass
|
|
46
|
+
missed — now propagates too (#45).** There are three places a checked-out
|
|
47
|
+
change's content actually gets committed, not two: alongside the public
|
|
48
|
+
`snapshot()` and the internal `autoSnapshotWorkingCopy()` above, `edit()`
|
|
49
|
+
has its own separate inline step that snapshots disk for the change being
|
|
50
|
+
switched AWAY FROM and commits it via `graph.updateChange()` directly —
|
|
51
|
+
entirely bypassing both other functions. The first pass explicitly left
|
|
52
|
+
`edit()` untouched, on the theory (recorded in this same entry, in an
|
|
53
|
+
earlier draft) that this site was pure checkout bookkeeping unrelated to
|
|
54
|
+
content commits, and that `tests/integration/absorb.test.js`'s "should
|
|
55
|
+
work after edit()" would therefore keep passing unmodified. Both halves of
|
|
56
|
+
that theory were wrong: this inline step *is* a real content commit (it's
|
|
57
|
+
literally how `write()`-then-`edit()`-away ever reaches the graph at all,
|
|
58
|
+
since `write()` itself only touches disk), and it needed the exact same
|
|
59
|
+
descendant-propagation treatment as the other two sites. Once added, the
|
|
60
|
+
`absorb.test.js` collision the very first attempt at #45 hit *did*
|
|
61
|
+
materialize here — confirming this was the actual site that original
|
|
62
|
+
attempt (and this issue's own motivating repro: "edit an ancestor, modify
|
|
63
|
+
it, edit back") was about all along.
|
|
64
|
+
- **Fixed the `absorb.test.js` fixture, not the feature.** Traced exactly
|
|
65
|
+
why: "should work after edit()" wrote `file.txt` on `change1`, edited away
|
|
66
|
+
to `change2`, then called `absorb()` expecting it to fold `change2`'s
|
|
67
|
+
(stale) content back into `change1`. That "diff" only existed because
|
|
68
|
+
`change2` hadn't picked up `change1`'s edit — precisely the bug being
|
|
69
|
+
fixed. Once fixed, `change2` correctly matches `change1` the moment you
|
|
70
|
+
edit away, the manufactured diff disappears, and `absorb()` correctly
|
|
71
|
+
finds nothing to do. Updated the fixture to make a genuine *new* edit at
|
|
72
|
+
`change2` after returning to it, so `absorb()` has a real, bug-independent
|
|
73
|
+
diff to fold — preserving the test's actual intent ("absorb still works
|
|
74
|
+
normally in a session where an ancestor was edited earlier") without
|
|
75
|
+
depending on the bug.
|
|
76
|
+
- **Fixed a second, pre-existing bug in `_rebuildDescendants()` that this
|
|
77
|
+
same follow-up surfaced: file deletions were resurrected as empty
|
|
78
|
+
strings.** The function had no way to represent "this file doesn't exist"
|
|
79
|
+
separately from "empty content" — every file was coerced with `|| ''`
|
|
80
|
+
before diffing. A descendant that deliberately deleted a file the parent
|
|
81
|
+
still has (e.g. `backout()`'s reversal change, which legitimately has no
|
|
82
|
+
`fileSnapshot` entry at all for a removed file) got that file rebuilt as
|
|
83
|
+
`''` instead of staying deleted, breaking
|
|
84
|
+
`tests/integration/backout.test.js`'s two file-addition-reversal tests.
|
|
85
|
+
This was always latent — `_rebuildDescendants()` had zero callers at all
|
|
86
|
+
before this issue gave it a real one, so the bug was never exercised.
|
|
87
|
+
Fixed by tracking each file's existence (`hasOwnProperty`, not `||`) on
|
|
88
|
+
all three sides (descendant, original parent, updated parent) and
|
|
89
|
+
handling "descendant deleted it," "descendant added it fresh," and "both
|
|
90
|
+
sides have real content" as distinct cases, rather than collapsing all of
|
|
91
|
+
them into one string-diff.
|
|
92
|
+
|
|
93
|
+
### Testing
|
|
94
|
+
|
|
95
|
+
New file `tests/integration/issue-43-restore-reliability.test.js`, describe
|
|
96
|
+
block "issue #45": edit into a non-leaf change, modify a file, and confirm
|
|
97
|
+
the descendant picks up the edit (a) via an explicit `jj.snapshot()` call,
|
|
98
|
+
(b) via `autoSnapshotWorkingCopy()` triggered by `jj.status()` after an
|
|
99
|
+
out-of-band disk write (a same-size `jj.write()` wouldn't reach this path —
|
|
100
|
+
`write()` immediately updates the tracked mtime/size itself, so the
|
|
101
|
+
disk-walk would see nothing "modified"), and (c) via `edit()`-away alone
|
|
102
|
+
with no explicit `snapshot()`/`status()` call in between — all three checked
|
|
103
|
+
via `jj.show()`. A fourth test confirms a leaf change (no descendants) still
|
|
104
|
+
works, exercising the other side of the `descendants.length > 0` branch. A
|
|
105
|
+
fifth test constructs a descendant that deliberately deleted a file and
|
|
106
|
+
confirms rebuilding it preserves that deletion instead of resurrecting an
|
|
107
|
+
empty string. Negative controls: with the `src/api/repository.js` changes
|
|
108
|
+
reverted, the propagation tests fail with stale pre-edit content, the
|
|
109
|
+
deletion test fails with a resurrected `''`, and the leaf-change test still
|
|
110
|
+
passes; reapplying the fix makes all five pass, and
|
|
111
|
+
`tests/integration/backout.test.js`'s two previously-broken tests pass
|
|
112
|
+
again too. 1864 tests passing; lint (0 errors), format:check, typecheck,
|
|
113
|
+
and build all green; all 12 `examples/*.mjs` run clean; branch coverage
|
|
114
|
+
90.06% (gate: 90%).
|
|
115
|
+
|
|
116
|
+
## 1.10.0 — 2026-09-26 — operations.restore() reliability (#43)
|
|
117
|
+
|
|
118
|
+
A substantial follow-up to 1.9.1/1.9.2's undo/restore work (#37/#38/#39/#41):
|
|
119
|
+
five distinct gaps that all fed the same root symptom — `operations.restore()`
|
|
120
|
+
(and `undo()`/`redo()`) not reliably reproducing the repository state at an
|
|
121
|
+
arbitrary past operation. Grouped into one minor release because all five
|
|
122
|
+
touch the same reversal machinery (`computeGraphReversal()`/
|
|
123
|
+
`applyGraphReversal()`, the `all()` revset's orphan filter, and the conflict
|
|
124
|
+
model), and because two of the fixes are real behavioral changes (`log()`'s
|
|
125
|
+
default visibility, and what `rebase()` actually writes into a conflicted
|
|
126
|
+
file) rather than pure bugfixes.
|
|
127
|
+
|
|
128
|
+
### Fixed
|
|
129
|
+
|
|
130
|
+
- **`write()` and `snapshot()` now record their own operation (#43 part 1).**
|
|
131
|
+
Both mutated the working copy (disk content, and for `snapshot()` the
|
|
132
|
+
working-copy change's `fileSnapshot` too) without ever calling
|
|
133
|
+
`oplog.recordOperation()`. An edit typed into a change's working copy via
|
|
134
|
+
either was therefore completely invisible to `undo()`/`operations.restore()`
|
|
135
|
+
— the change kept its post-edit content forever regardless of which
|
|
136
|
+
operation you restored to, because there was no oplog entry to target and
|
|
137
|
+
no "before" snapshot for `computeGraphReversal()` to find. Both now follow
|
|
138
|
+
the same "snapshot the before-state, then record" pattern every other
|
|
139
|
+
mutator in this file already uses (see `describe()`), including a
|
|
140
|
+
`changeSnapshot` for `snapshot()` (which mutates a `ChangeGraph` node in
|
|
141
|
+
place) and a `view.fileSnapshot` for both.
|
|
142
|
+
- **`squash()`, `rebase()`/`moveChange()`, and `abandon()` now record a
|
|
143
|
+
"files before" snapshot as part of their own operation (#43 part 1).**
|
|
144
|
+
`computeGraphReversal()` reconstructs "the state right after operation X"
|
|
145
|
+
by scanning forward from X for the first LATER operation that recorded a
|
|
146
|
+
`view.fileSnapshot` "before" value. These three operations never recorded
|
|
147
|
+
one, so restoring to a point right before one of them ran had nothing
|
|
148
|
+
accurate to fall back on: the scan would skip straight over the gap and
|
|
149
|
+
either find nothing (leaving disk untouched) or land on a much later,
|
|
150
|
+
unrelated operation's "before" value — the wrong point in time entirely.
|
|
151
|
+
Reproduced via a stack-walk (build a chain of describe/new/write/squash/
|
|
152
|
+
describe operations, `operations.restore()` to every operation in it, and
|
|
153
|
+
compare on-disk files against an independently-tracked expectation) — this
|
|
154
|
+
is what surfaces as "a forward restore to an earlier op loses a file that
|
|
155
|
+
was added between a squash and the target". All three now capture disk
|
|
156
|
+
state before mutating anything and include it in their own `view`.
|
|
157
|
+
- **`undo()`/`redo()`/`operations.restore()` now record disk state right
|
|
158
|
+
before THEMSELVES ran, not the landing/target operation's own "before"
|
|
159
|
+
value (#43 part 1).** All three used to record `view: landingView` (or
|
|
160
|
+
`redoneView`/`targetOp.view`) verbatim — reusing that OTHER operation's
|
|
161
|
+
own recorded `fileSnapshot`, which represents disk state before THAT
|
|
162
|
+
operation ran, not before this undo/redo/restore call mutated anything.
|
|
163
|
+
A later `restore()`/`undo()` scanning forward past one of these would then
|
|
164
|
+
pick up that stale, wrong snapshot. Each now captures its own disk state
|
|
165
|
+
immediately before applying its reversal and merges it into the pointer
|
|
166
|
+
fields (`workingCopy`/`heads`/`bookmarks`) it still legitimately inherits
|
|
167
|
+
from the landing operation.
|
|
168
|
+
- **`view.heads` no longer silently drops live heads (#43 part 1).** Every
|
|
169
|
+
operation that recorded `view.heads` hand-wrote it as either `[]` or a
|
|
170
|
+
single changeId — whatever that specific call happened to touch —
|
|
171
|
+
instead of the repository's actual current head set. `squash()` in
|
|
172
|
+
particular recorded the destination changeId even when squashing the
|
|
173
|
+
working copy creates a fresh new tip on top of it (so `heads` pointed at
|
|
174
|
+
the parent, not the tip), and any operation that didn't itself touch an
|
|
175
|
+
untouched sibling branch silently reported only one head where two
|
|
176
|
+
existed. Added `computeCurrentHeads()` (every non-abandoned change with no
|
|
177
|
+
non-abandoned child) and switched `squash()`/`rebase()`/`moveChange()`/
|
|
178
|
+
`abandon()`/`conflicts.resolve()` to use it.
|
|
179
|
+
- **`log()` now hides orphans that are purely internal rewrite/undo
|
|
180
|
+
artifacts, even once they differ from their reverted parent (#43 part 2).**
|
|
181
|
+
The #37(b) fix hid an orphan only while it stayed exactly empty relative
|
|
182
|
+
to its CURRENT parent. Two shapes slip past that once undo() is involved:
|
|
183
|
+
(a) `squash()`'s synthetic post-squash `@`, if that squash is later undone
|
|
184
|
+
and the same undo reverts the destination's content out from under it —
|
|
185
|
+
the orphan's frozen (post-squash) `fileSnapshot` no longer matches the
|
|
186
|
+
destination's (reverted, pre-squash) one, even though nothing about the
|
|
187
|
+
orphan itself changed; (b) `new()`'s freshly created change, if something
|
|
188
|
+
writes into it before an `undo()` unwinds back past the `new()`. Both
|
|
189
|
+
`squash()`'s synthetic working-copy change and `new()`'s created change
|
|
190
|
+
are now tagged `autoCreated: true` at creation, and the revset engine's
|
|
191
|
+
`_computeHiddenOrphans()` hides an unreachable, undescribed change if it's
|
|
192
|
+
EITHER still empty relative to its parent OR `autoCreated` — the latter
|
|
193
|
+
doesn't need to re-diff against a parent that may no longer resemble what
|
|
194
|
+
the change was actually created from.
|
|
195
|
+
- **`rebase()`/`moveChange()` now writes real conflict-marker text into the
|
|
196
|
+
working copy instead of leaving the pre-rebase ("ours") content verbatim
|
|
197
|
+
(#43 part 3) — behavioral change.** Rebase already recorded a detected
|
|
198
|
+
conflict correctly as data (via `conflicts.addConflict()`), but never
|
|
199
|
+
touched the file's actual content: `change.fileSnapshot` (and, when the
|
|
200
|
+
rebased change was checked out, the on-disk file) kept showing whatever
|
|
201
|
+
content it had before the rebase, with no indication a conflict existed
|
|
202
|
+
short of calling `conflicts.list()` separately. It now calls
|
|
203
|
+
`jj.conflicts.markers()` for each detected conflict (reusing that
|
|
204
|
+
existing marker-formatting logic rather than re-deriving it a second way)
|
|
205
|
+
and writes the result into both `change.fileSnapshot` and, when
|
|
206
|
+
applicable, the working copy on disk.
|
|
207
|
+
- **`conflicts.resolve()` now records an operation, so a resolution can be
|
|
208
|
+
undone (#43 part 4).** It wrote the resolved content straight to disk and
|
|
209
|
+
mutated the `ConflictModel`'s persisted state directly, with no
|
|
210
|
+
`oplog.recordOperation()` call at all — resolving a conflict was
|
|
211
|
+
completely invisible to `undo()`/`operations.restore()`. It now captures
|
|
212
|
+
a `changeSnapshot` of the working-copy change, a `conflictsSnapshot` of
|
|
213
|
+
the pre-resolve conflict state (deep-cloned — `resolveConflict()` mutates
|
|
214
|
+
the same conflict object in place, so a naive `Object.fromEntries()` over
|
|
215
|
+
the live `Map` would otherwise leave the "before" snapshot aliased to the
|
|
216
|
+
post-mutation object), and a `view.fileSnapshot`, then records them.
|
|
217
|
+
|
|
218
|
+
### Not fixed (punted)
|
|
219
|
+
|
|
220
|
+
- **#43 part 5 (minor): `edit()` of a non-leaf change doesn't rebase its
|
|
221
|
+
descendants' file content.** An initial fix (propagate the edited
|
|
222
|
+
ancestor's content onto descendants when moving away from it) was
|
|
223
|
+
implemented and then reverted: it directly conflicts with `absorb()`'s own
|
|
224
|
+
job of folding a working-copy edit back into the ancestor it actually
|
|
225
|
+
belongs to — with the auto-rebase in place, `edit()` + `write()` +
|
|
226
|
+
`edit()`-away already changes the descendant before `absorb()` ever runs,
|
|
227
|
+
so `absorb()` finds nothing left to do (caught by the existing
|
|
228
|
+
`tests/integration/absorb.test.js` "should work after edit()" test).
|
|
229
|
+
Fixing this properly needs to account for that interaction; left for a
|
|
230
|
+
follow-up.
|
|
231
|
+
|
|
232
|
+
### Testing
|
|
233
|
+
|
|
234
|
+
1859 tests passing (12 new regression tests, matched by a new negative
|
|
235
|
+
control confirming that 12 of them fail against the pre-fix code — the
|
|
236
|
+
other 3 are supplementary correctness checks that already held, e.g.
|
|
237
|
+
"a descendant's own edit is never clobbered"); lint (0 errors),
|
|
238
|
+
format:check, typecheck, and build all still green; all 12 `examples/*.mjs`
|
|
239
|
+
run clean; branch coverage 90.07% (gate: 90%). New test file:
|
|
240
|
+
`tests/integration/issue-43-restore-reliability.test.js`.
|
|
241
|
+
|
|
3
242
|
## 1.9.2 — 2026-09-26 — auto-snapshot mtime comparison (#41)
|
|
4
243
|
|
|
5
244
|
### Fixed
|