isomorphic-jj 1.12.1 → 1.14.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 +95 -0
- package/README.md +4 -2
- package/dist/index.cjs +188 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +188 -27
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/api/repository.js +122 -24
- package/src/core/operation-log.js +18 -0
- package/src/core/revset-engine.js +48 -3
- package/src/types.d.ts +77 -33
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,100 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.14.0 — 2026-10-09 — describe()/metaedit() honor changeId; operations.restore() restores the view
|
|
4
|
+
|
|
5
|
+
Fixes [#55](https://github.com/johnhenry/isomorphic-jj/issues/55) and
|
|
6
|
+
[#56](https://github.com/johnhenry/isomorphic-jj/issues/56). Both change
|
|
7
|
+
observable behavior, hence a minor bump.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **`describe()` and `metaedit()` ignored `changeId`.** Both only read the
|
|
12
|
+
older `revision` option (`metaedit` also `change`), so
|
|
13
|
+
`describe({ changeId, message })` silently rewrote the working-copy change
|
|
14
|
+
instead of the named one -- the documented option (API.md) did nothing and
|
|
15
|
+
`DescribeArgs` in `types.d.ts` didn't list it. Both now accept `changeId`
|
|
16
|
+
(alongside `revision`), rewrite exactly that change, leave the working
|
|
17
|
+
copy's description alone, and keep change IDs stable. `DescribeArgs` gains
|
|
18
|
+
`changeId` and `revision`. Regression tests:
|
|
19
|
+
`tests/integration/describe-change-id.test.js`.
|
|
20
|
+
- **`operations.restore()` left changes created after the target operation
|
|
21
|
+
visible.** The restore operation recorded the right view (heads, working
|
|
22
|
+
copy), but the revset layer only hid orphaned changes that were empty and
|
|
23
|
+
undescribed, so a described change stayed in `log()`, `heads(all())` and
|
|
24
|
+
`visible_heads()`. As in jj, restoring now restores the *view*: every
|
|
25
|
+
change created after the target operation and unreachable from the
|
|
26
|
+
restored working copy, bookmarks and tags is hidden (not deleted -- it
|
|
27
|
+
stays in the graph and resolvable by change ID). The hidden set is stored
|
|
28
|
+
in the operation's `view.hiddenChanges`, so it persists across later
|
|
29
|
+
operations, `undo()`/`redo()` of the restore bring the changes back, and
|
|
30
|
+
they reappear if the working copy/bookmarks make them reachable again
|
|
31
|
+
(e.g. `edit({ changeId })`). **Behavior change:** `log()` after a restore
|
|
32
|
+
no longer lists such changes. Regression tests:
|
|
33
|
+
`tests/integration/op-restore-view.test.js`.
|
|
34
|
+
|
|
35
|
+
## 1.13.0 — 2026-10-05 — Write-path fixes: converge on a fresh instance, new files lost on edit(), rebase content
|
|
36
|
+
|
|
37
|
+
Fixed in [PR #52](https://github.com/johnhenry/isomorphic-jj/pull/52).
|
|
38
|
+
|
|
39
|
+
Three bugs found while wiring a consumer (JJHub's isomorphic jj engine)
|
|
40
|
+
to write through this library, constructing one fresh `createJJ()`
|
|
41
|
+
instance per call so no read is ever served from a stale cache. Two of
|
|
42
|
+
them change observable behavior, hence a minor bump.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- **`converge()` threw `WORKING_COPY_NOT_LOADED` on a fresh instance.**
|
|
47
|
+
Both outcomes record the working copy in their operation, but
|
|
48
|
+
`converge()` loaded only the graph and user config — so it only worked
|
|
49
|
+
after some earlier call on the same instance had loaded the working
|
|
50
|
+
copy. It now loads it itself. Regression tests: "on a fresh instance"
|
|
51
|
+
in `tests/integration/converge.test.js`.
|
|
52
|
+
- **A new file `write()` created was deleted by the next `edit()` away.**
|
|
53
|
+
`write()` tracks a file without recording it in the working-copy
|
|
54
|
+
change (the next command commits it, as in jj), but `edit()` first
|
|
55
|
+
deleted every tracked file missing from the change being left, as
|
|
56
|
+
cross-branch "pollution" from an earlier checkout. Since issue #30 every
|
|
57
|
+
switch fully syncs the working directory to its target, so nothing
|
|
58
|
+
leaks that way any more; what the cleanup actually removed was every new
|
|
59
|
+
file — top-level or nested — written into that change. Edits to files
|
|
60
|
+
already recorded survived, which made it look path-dependent. `edit()`
|
|
61
|
+
now commits every tracked file into the change it leaves, like
|
|
62
|
+
`new()`/`describe()`. **Behavior change:** a tracked file on disk is
|
|
63
|
+
now always the checked-out change's content when `edit()` leaves it;
|
|
64
|
+
code that removed a file from a change by editing the graph alone (not
|
|
65
|
+
`remove()`) must now remove it from disk too —
|
|
66
|
+
`tests/integration/issue-43-restore-reliability.test.js`'s
|
|
67
|
+
descendant-deletion test did exactly that and now uses `remove()`.
|
|
68
|
+
Regression tests: "write() then edit() away" in
|
|
69
|
+
`tests/integration/file-operations.test.js`.
|
|
70
|
+
- **`moveChange()`/`rebase()` kept the change's own tree verbatim.** The
|
|
71
|
+
three-way conflict check from issue #31 ran, but the rebased content
|
|
72
|
+
was never derived from it: the change kept every file only its OLD
|
|
73
|
+
parent had and never picked up what only the NEW parent changed. Its
|
|
74
|
+
content is now jj's rebase, path by path — the new parent's value
|
|
75
|
+
wherever the change didn't touch a path relative to its old parent,
|
|
76
|
+
the change's own value wherever it did (a path both sides changed
|
|
77
|
+
differently is the conflict #31 already reports, with markers). When
|
|
78
|
+
the moved change is checked out, its files on disk are synced to the
|
|
79
|
+
result. **Behavior change:** a rebased change's `fileSnapshot` now
|
|
80
|
+
differs from before for any path its old and new parents disagree on.
|
|
81
|
+
Regression tests: "content of the rebased change" in
|
|
82
|
+
`tests/integration/rebase-conflict-detection.test.js`.
|
|
83
|
+
|
|
84
|
+
### Not changed
|
|
85
|
+
|
|
86
|
+
- `moveChange()` still re-derives only the moved change's own content,
|
|
87
|
+
not that of its descendants (they follow it structurally, by parent
|
|
88
|
+
change id, but keep their trees) — see the README's status section.
|
|
89
|
+
- Conflict detection still compares whole files, not lines: two sides
|
|
90
|
+
changing different lines of one file is a conflict.
|
|
91
|
+
|
|
92
|
+
### Testing
|
|
93
|
+
|
|
94
|
+
1882 tests passing, 0 skipped; lint, format:check, typecheck, and all 12
|
|
95
|
+
`examples/*.mjs` green; coverage 97.39% statements / 90.13% branches
|
|
96
|
+
(gate 90%). Each new regression test fails against 1.12.1's source.
|
|
97
|
+
|
|
3
98
|
## 1.12.1 — 2026-09-27 — Conflict type shape + README status refresh
|
|
4
99
|
|
|
5
100
|
A cross-library documentation/typing audit flagged four things to verify
|
package/README.md
CHANGED
|
@@ -1020,8 +1020,8 @@ repo/
|
|
|
1020
1020
|
|
|
1021
1021
|
## Project Status
|
|
1022
1022
|
|
|
1023
|
-
**Current Version**: 1.
|
|
1024
|
-
**Test Coverage**:
|
|
1023
|
+
**Current Version**: 1.14.0 (`@johnhenry/isomorphic-jj`; same library/API lineage as the old `isomorphic-jj`, which reached v1.8.0 before the rename — see the provenance note at the top of this file, and the CHANGELOG's `1.9.0` entry for how the scoped/unscoped version lineages were merged back together)
|
|
1024
|
+
**Test Coverage**: 1882 tests passing, ~97% statements / 90% branches
|
|
1025
1025
|
**JJ CLI parity**: tracks Jujutsu through v0.45.1
|
|
1026
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 in the API-stability sense (the version number is past 1.0 only because merging the scoped/unscoped lineages forced it above the unscoped side's prior ceiling, not because the API is frozen) — API surface can still shift; pin a version and read the CHANGELOG before upgrading.
|
|
1027
1027
|
|
|
@@ -1038,6 +1038,8 @@ repo/
|
|
|
1038
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).
|
|
1039
1039
|
- ✅ 0.4.0: `converge()` generalized from pairwise to N-way — any number of divergent copies of one change id resolve in a single call, matching how jj's own underlying engine already treats divergence internally (issue #32). See [CHANGELOG.md](./CHANGELOG.md).
|
|
1040
1040
|
- ✅ 1.9.0: Scoped (`@johnhenry/isomorphic-jj`) and unscoped (`isomorphic-jj`) version lineages merged into one shared version going forward, with both names dual-published on every release.
|
|
1041
|
+
- ✅ 1.14.0: `describe()`/`metaedit()` honor `changeId`; `operations.restore()` restores the view (changes created after the target op are hidden from `log()`). See [CHANGELOG.md](./CHANGELOG.md).
|
|
1042
|
+
- ✅ 1.13.0: Write-path fixes — `converge()` works on a fresh instance, `edit()` no longer deletes new files `write()` created in the change it leaves, and `moveChange()`/`rebase()` derives the rebased change's content (new parent + the change's own edits) instead of keeping its tree verbatim. Not done: a rebase re-derives only the moved change's content, not its descendants', and conflicts are detected per whole file, not per line. See [CHANGELOG.md](./CHANGELOG.md).
|
|
1041
1043
|
- ✅ 1.9.1 – 1.12.0: Operation-log reliability pass — `undo()`/`operations.restore()` correctness across write/snapshot/squash/rebase/abandon/edit, `log()` visibility fixes, and recording gaps in descendant rebases and conflict-resolution ops (issues #37–#39, #41, #43, #45, #48). See [CHANGELOG.md](./CHANGELOG.md) for the full per-fix breakdown.
|
|
1042
1044
|
|
|
1043
1045
|
See [ROADMAP.md](./ROADMAP.md) for detailed plans, and [CHANGELOG.md](./CHANGELOG.md) for the release history.
|
package/dist/index.cjs
CHANGED
|
@@ -1517,6 +1517,24 @@ class OperationLog {
|
|
|
1517
1517
|
operation.parents = operation.parents || [];
|
|
1518
1518
|
}
|
|
1519
1519
|
|
|
1520
|
+
// `view.hiddenChanges` (written by operations.restore()) is part of the
|
|
1521
|
+
// view, so it persists across later operations. undo()/redo()/restore()
|
|
1522
|
+
// set it explicitly (their view is a snapshot of another operation);
|
|
1523
|
+
// every other operation carries the current head's value forward.
|
|
1524
|
+
if (
|
|
1525
|
+
this.headOperationId &&
|
|
1526
|
+
operation.view &&
|
|
1527
|
+
!('hiddenChanges' in operation.view) &&
|
|
1528
|
+
operation.eventType !== 'undo' &&
|
|
1529
|
+
operation.eventType !== 'redo'
|
|
1530
|
+
) {
|
|
1531
|
+
const headOp = this.operations.find((op) => op.id === this.headOperationId);
|
|
1532
|
+
const inherited = headOp.view && headOp.view.hiddenChanges;
|
|
1533
|
+
if (inherited && inherited.length > 0) {
|
|
1534
|
+
operation.view = { ...operation.view, hiddenChanges: inherited };
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1520
1538
|
// Generate operation ID
|
|
1521
1539
|
const id = await generateOperationId(operation);
|
|
1522
1540
|
const fullOperation = {
|
|
@@ -2763,16 +2781,18 @@ class RevsetEngine {
|
|
|
2763
2781
|
case 'visible_heads': {
|
|
2764
2782
|
await this.graph.load();
|
|
2765
2783
|
const allChanges = this.graph.getAll();
|
|
2766
|
-
const
|
|
2784
|
+
const restoreHidden = await this._computeRestoreHidden();
|
|
2785
|
+
const visibleChanges = allChanges.filter((c) => !restoreHidden.has(c.changeId));
|
|
2786
|
+
const changeIdSet = new Set(visibleChanges.map((c) => c.changeId));
|
|
2767
2787
|
const hasChildren = new Set();
|
|
2768
|
-
for (const change of
|
|
2788
|
+
for (const change of visibleChanges) {
|
|
2769
2789
|
if (change.parents) {
|
|
2770
2790
|
for (const parent of change.parents) {
|
|
2771
2791
|
if (changeIdSet.has(parent)) hasChildren.add(parent);
|
|
2772
2792
|
}
|
|
2773
2793
|
}
|
|
2774
2794
|
}
|
|
2775
|
-
return
|
|
2795
|
+
return visibleChanges.filter((c) => !hasChildren.has(c.changeId)).map((c) => c.changeId);
|
|
2776
2796
|
}
|
|
2777
2797
|
|
|
2778
2798
|
// `git_refs()` and `git_head()` are deprecated compatibility shims.
|
|
@@ -3258,6 +3278,49 @@ class RevsetEngine {
|
|
|
3258
3278
|
* @returns {Promise<Set<string>>} change IDs to exclude
|
|
3259
3279
|
*/
|
|
3260
3280
|
async _computeHiddenOrphans(allChanges) {
|
|
3281
|
+
const restoreHidden = await this._computeRestoreHidden();
|
|
3282
|
+
const hidden = await this._computeUnreferencedOrphans(allChanges);
|
|
3283
|
+
for (const id of restoreHidden) hidden.add(id);
|
|
3284
|
+
return hidden;
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3287
|
+
/**
|
|
3288
|
+
* Changes hidden by `operations.restore()` (issue #56): the head
|
|
3289
|
+
* operation's `view.hiddenChanges`, minus any that the current working
|
|
3290
|
+
* copy / bookmarks / tags make reachable again. Deliberately does NOT use
|
|
3291
|
+
* the operation log's recorded heads for reachability -- later operations
|
|
3292
|
+
* record structural heads, which would include the hidden leaf itself.
|
|
3293
|
+
*
|
|
3294
|
+
* @returns {Promise<Set<string>>}
|
|
3295
|
+
*/
|
|
3296
|
+
async _computeRestoreHidden() {
|
|
3297
|
+
const hidden = new Set();
|
|
3298
|
+
if (!this.oplog) return hidden;
|
|
3299
|
+
const headOp = await this.oplog.getHeadOperation();
|
|
3300
|
+
const listed = (headOp && headOp.view && headOp.view.hiddenChanges) || [];
|
|
3301
|
+
if (listed.length === 0) return hidden;
|
|
3302
|
+
|
|
3303
|
+
const roots = new Set([this.workingCopy.getCurrentChangeId()]);
|
|
3304
|
+
await this.bookmarkStore.load();
|
|
3305
|
+
for (const bookmark of await this.bookmarkStore.list()) roots.add(bookmark.changeId);
|
|
3306
|
+
for (const tag of await this.tagStore.list()) roots.add(tag.changeId);
|
|
3307
|
+
const reachable = new Set();
|
|
3308
|
+
for (const rootId of roots) {
|
|
3309
|
+
for (const id of await this.getAncestors(rootId)) reachable.add(id);
|
|
3310
|
+
}
|
|
3311
|
+
for (const id of listed) {
|
|
3312
|
+
if (!reachable.has(id)) hidden.add(id);
|
|
3313
|
+
}
|
|
3314
|
+
return hidden;
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
/**
|
|
3318
|
+
* The #37(b)/#43(2)/#48 rule: unreachable, undescribed orphans.
|
|
3319
|
+
*
|
|
3320
|
+
* @param {any[]} allChanges
|
|
3321
|
+
* @returns {Promise<Set<string>>}
|
|
3322
|
+
*/
|
|
3323
|
+
async _computeUnreferencedOrphans(allChanges) {
|
|
3261
3324
|
const trackedHeads = new Set();
|
|
3262
3325
|
try {
|
|
3263
3326
|
const wcId = this.workingCopy && this.workingCopy.getCurrentChangeId();
|
|
@@ -8203,6 +8266,61 @@ async function createJJ(options) {
|
|
|
8203
8266
|
return heads;
|
|
8204
8267
|
};
|
|
8205
8268
|
|
|
8269
|
+
/**
|
|
8270
|
+
* Changes that `operations.restore(targetOp)` must hide (issue #56): every
|
|
8271
|
+
* change created after `targetOp` (i.e. not among the heads / working copy /
|
|
8272
|
+
* touched changes recorded up to targetOp, nor their ancestors) that is not
|
|
8273
|
+
* reachable from the restored working copy, bookmarks or tags. Hidden, not
|
|
8274
|
+
* deleted -- they reappear if a later operation makes them reachable again.
|
|
8275
|
+
* Previously hidden changes that targetOp's own view already hid stay hidden.
|
|
8276
|
+
*
|
|
8277
|
+
* @param {any[]} ops - Full operation list (chronological)
|
|
8278
|
+
* @param {any} targetOp
|
|
8279
|
+
* @returns {Promise<string[]>}
|
|
8280
|
+
*/
|
|
8281
|
+
const computeRestoreHiddenChanges = async (ops, targetOp) => {
|
|
8282
|
+
await graph.load();
|
|
8283
|
+
const ancestorsOf = (/** @type {Iterable<string>} */ seeds) => {
|
|
8284
|
+
const seen = new Set();
|
|
8285
|
+
const stack = [...seeds];
|
|
8286
|
+
while (stack.length > 0) {
|
|
8287
|
+
const id = /** @type {string} */ (stack.pop());
|
|
8288
|
+
if (seen.has(id)) continue;
|
|
8289
|
+
seen.add(id);
|
|
8290
|
+
for (const parent of graph.getParents(id)) stack.push(parent);
|
|
8291
|
+
}
|
|
8292
|
+
return seen;
|
|
8293
|
+
};
|
|
8294
|
+
|
|
8295
|
+
const targetIndex = ops.findIndex((o) => o.id === targetOp.id);
|
|
8296
|
+
const knownSeeds = new Set();
|
|
8297
|
+
for (const op of ops.slice(0, targetIndex + 1)) {
|
|
8298
|
+
const view = op.view || {};
|
|
8299
|
+
for (const id of view.heads || []) knownSeeds.add(id);
|
|
8300
|
+
if (view.workingCopy) knownSeeds.add(view.workingCopy);
|
|
8301
|
+
for (const id of Object.keys(op.changeSnapshot || {})) knownSeeds.add(id);
|
|
8302
|
+
}
|
|
8303
|
+
const known = ancestorsOf(knownSeeds);
|
|
8304
|
+
|
|
8305
|
+
const roots = new Set([workingCopy.getCurrentChangeId()]);
|
|
8306
|
+
await bookmarks.load();
|
|
8307
|
+
for (const bookmark of await bookmarks.list()) roots.add(bookmark.changeId);
|
|
8308
|
+
for (const tag of await tags.list()) roots.add(tag.changeId);
|
|
8309
|
+
const reachable = ancestorsOf(roots);
|
|
8310
|
+
|
|
8311
|
+
const hidden = new Set(
|
|
8312
|
+
((targetOp.view && targetOp.view.hiddenChanges) || []).filter(
|
|
8313
|
+
(/** @type {string} */ id) => !reachable.has(id)
|
|
8314
|
+
)
|
|
8315
|
+
);
|
|
8316
|
+
for (const change of graph.getAll()) {
|
|
8317
|
+
if (!known.has(change.changeId) && !reachable.has(change.changeId)) {
|
|
8318
|
+
hidden.add(change.changeId);
|
|
8319
|
+
}
|
|
8320
|
+
}
|
|
8321
|
+
return [...hidden];
|
|
8322
|
+
};
|
|
8323
|
+
|
|
8206
8324
|
/**
|
|
8207
8325
|
* Re-parent every child of `changeId` onto `newParents` instead — used
|
|
8208
8326
|
* when a change is being elided from the graph (abandon(), issue #30;
|
|
@@ -9330,6 +9448,27 @@ async function createJJ(options) {
|
|
|
9330
9448
|
await conflicts.addConflict(conflict);
|
|
9331
9449
|
}
|
|
9332
9450
|
|
|
9451
|
+
// The rebased change's content, path by path: (new base) + (what the
|
|
9452
|
+
// change itself did relative to its old base), as jj's rebase. It
|
|
9453
|
+
// used to keep its own full tree verbatim — keeping every file only
|
|
9454
|
+
// the OLD base had and never picking up what only the NEW base
|
|
9455
|
+
// changed. A path both sides changed differently keeps the change's
|
|
9456
|
+
// own value here; if that's a real conflict, detectConflicts() above
|
|
9457
|
+
// reported it and the marker text below replaces it.
|
|
9458
|
+
const contentChanged = Boolean(oldParentChange);
|
|
9459
|
+
if (oldParentChange) {
|
|
9460
|
+
/** @type {Record<string, string>} */
|
|
9461
|
+
const rebased = {};
|
|
9462
|
+
const allPaths = new Set([...baseFiles.keys(), ...leftFiles.keys(), ...rightFiles.keys()]);
|
|
9463
|
+
for (const filePath of allPaths) {
|
|
9464
|
+
const baseValue = baseFiles.get(filePath);
|
|
9465
|
+
const ownValue = leftFiles.get(filePath);
|
|
9466
|
+
const value = ownValue === baseValue ? rightFiles.get(filePath) : ownValue;
|
|
9467
|
+
if (value !== undefined) rebased[filePath] = value;
|
|
9468
|
+
}
|
|
9469
|
+
change.fileSnapshot = rebased;
|
|
9470
|
+
}
|
|
9471
|
+
|
|
9333
9472
|
// Materialize real conflict-marker text into the moved change's file
|
|
9334
9473
|
// content — issue #43(3): rebase()/moveChange() used to record the
|
|
9335
9474
|
// conflict correctly as DATA (via conflicts.addConflict() above) but
|
|
@@ -9371,6 +9510,13 @@ async function createJJ(options) {
|
|
|
9371
9510
|
}
|
|
9372
9511
|
}
|
|
9373
9512
|
|
|
9513
|
+
// A checked-out change's files on disk are its content: bring them in
|
|
9514
|
+
// line with the rebased snapshot (markers included), tracked, so the
|
|
9515
|
+
// next command's auto-snapshot finds nothing to re-record.
|
|
9516
|
+
if (contentChanged && changeId === workingCopy.getCurrentChangeId()) {
|
|
9517
|
+
await syncWorkingCopyFiles(change.fileSnapshot || {});
|
|
9518
|
+
}
|
|
9519
|
+
|
|
9374
9520
|
// Update parent
|
|
9375
9521
|
change.parents = [newParent];
|
|
9376
9522
|
await graph.updateChange(change);
|
|
@@ -9509,6 +9655,11 @@ async function createJJ(options) {
|
|
|
9509
9655
|
* @param {Record<string, any>} [args]
|
|
9510
9656
|
*/
|
|
9511
9657
|
async describe(args = {}) {
|
|
9658
|
+
// Documented option is `changeId`; `revision` is the older spelling.
|
|
9659
|
+
if (args.changeId && !args.revision) {
|
|
9660
|
+
args = { ...args, revision: args.changeId };
|
|
9661
|
+
}
|
|
9662
|
+
|
|
9512
9663
|
await graph.load();
|
|
9513
9664
|
await workingCopy.load();
|
|
9514
9665
|
await userConfig.load();
|
|
@@ -9658,10 +9809,13 @@ async function createJJ(options) {
|
|
|
9658
9809
|
* @returns {Promise<Object>} Updated change object
|
|
9659
9810
|
*/
|
|
9660
9811
|
async metaedit(args = {}) {
|
|
9661
|
-
// Normalize: accept 'change' as
|
|
9812
|
+
// Normalize: accept 'change' / 'changeId' as aliases for 'revision'
|
|
9662
9813
|
if (args.change && !args.revision) {
|
|
9663
9814
|
args = { ...args, revision: args.change };
|
|
9664
9815
|
}
|
|
9816
|
+
if (args.changeId && !args.revision) {
|
|
9817
|
+
args = { ...args, revision: args.changeId };
|
|
9818
|
+
}
|
|
9665
9819
|
|
|
9666
9820
|
// Normalize: accept 'message' as alias for 'description'
|
|
9667
9821
|
if (args.message && !args.description) {
|
|
@@ -10354,29 +10508,18 @@ async function createJJ(options) {
|
|
|
10354
10508
|
// see issue #29.
|
|
10355
10509
|
const fileSnapshotBeforeSwitch = await snapshotFilesystem();
|
|
10356
10510
|
|
|
10357
|
-
//
|
|
10358
|
-
//
|
|
10511
|
+
// Commit the change being left: every tracked file on disk is its
|
|
10512
|
+
// content, as in new()/describe(). This used to first DELETE every
|
|
10513
|
+
// tracked file missing from the change's recorded fileSnapshot, as
|
|
10514
|
+
// "pollution" from an earlier checkout — but a switch now fully syncs
|
|
10515
|
+
// the working directory to its target (issue #30, below), so nothing
|
|
10516
|
+
// leaks that way any more, and what that cleanup actually deleted was
|
|
10517
|
+
// every new file write() had created in this change: write() tracks a
|
|
10518
|
+
// file without recording it in the change, so it is only committed
|
|
10519
|
+
// here, by the next command.
|
|
10359
10520
|
if (previousChangeId !== args.changeId) {
|
|
10360
10521
|
const previousChange = await graph.getChange(previousChangeId);
|
|
10361
10522
|
if (previousChange) {
|
|
10362
|
-
const previousSnapshot = previousChange.fileSnapshot || {};
|
|
10363
|
-
const workingDirFiles = await workingCopy.listFiles();
|
|
10364
|
-
|
|
10365
|
-
// Delete files that exist in working dir but NOT in previous changeId's snapshot
|
|
10366
|
-
// This removes pollution from other branches that were previously checked out
|
|
10367
|
-
for (const filePath of workingDirFiles) {
|
|
10368
|
-
if (!previousSnapshot[filePath]) {
|
|
10369
|
-
try {
|
|
10370
|
-
const fullPath = join(dir, filePath);
|
|
10371
|
-
await fs.promises.unlink(fullPath);
|
|
10372
|
-
await workingCopy.untrackFile(filePath);
|
|
10373
|
-
} catch (error) {
|
|
10374
|
-
// Ignore errors - file might already be deleted or inaccessible
|
|
10375
|
-
}
|
|
10376
|
-
}
|
|
10377
|
-
}
|
|
10378
|
-
|
|
10379
|
-
// Now snapshot the CLEAN working directory (no pollution!)
|
|
10380
10523
|
const currentSnapshot = await snapshotFilesystem();
|
|
10381
10524
|
if (currentSnapshot && Object.keys(currentSnapshot).length > 0) {
|
|
10382
10525
|
changeSnapshot[previousChangeId] = structuredClone(previousChange);
|
|
@@ -10600,7 +10743,11 @@ async function createJJ(options) {
|
|
|
10600
10743
|
redoFileSnapshot,
|
|
10601
10744
|
redoConflictsSnapshot,
|
|
10602
10745
|
parents: [],
|
|
10603
|
-
view: {
|
|
10746
|
+
view: {
|
|
10747
|
+
...landingView,
|
|
10748
|
+
fileSnapshot: diskSnapshotBeforeThisOp,
|
|
10749
|
+
hiddenChanges: landingView.hiddenChanges || [],
|
|
10750
|
+
},
|
|
10604
10751
|
});
|
|
10605
10752
|
|
|
10606
10753
|
const restoredFileCount =
|
|
@@ -10723,7 +10870,11 @@ async function createJJ(options) {
|
|
|
10723
10870
|
redoOf: undoOp.id,
|
|
10724
10871
|
changeSnapshot: undoChangeSnapshot,
|
|
10725
10872
|
parents: [],
|
|
10726
|
-
view: {
|
|
10873
|
+
view: {
|
|
10874
|
+
...redoneView,
|
|
10875
|
+
fileSnapshot: diskSnapshotBeforeThisOp,
|
|
10876
|
+
hiddenChanges: redoneView.hiddenChanges || [],
|
|
10877
|
+
},
|
|
10727
10878
|
});
|
|
10728
10879
|
|
|
10729
10880
|
const restoredFileCount =
|
|
@@ -11055,6 +11206,13 @@ async function createJJ(options) {
|
|
|
11055
11206
|
|
|
11056
11207
|
await applyGraphReversal(reversal);
|
|
11057
11208
|
|
|
11209
|
+
// Restoring the view hides (not deletes) every change that was
|
|
11210
|
+
// created after targetOp and is not reachable from the restored
|
|
11211
|
+
// working copy / bookmarks / tags -- see issue #56. The list is
|
|
11212
|
+
// recorded in the view so it survives later operations, and so
|
|
11213
|
+
// undo()/redo() of the restore bring the changes back.
|
|
11214
|
+
const hiddenChanges = await computeRestoreHiddenChanges(ops, targetOp);
|
|
11215
|
+
|
|
11058
11216
|
// Record this restoration as a new operation
|
|
11059
11217
|
await oplog.recordOperation({
|
|
11060
11218
|
timestamp: new Date().toISOString(),
|
|
@@ -11062,7 +11220,7 @@ async function createJJ(options) {
|
|
|
11062
11220
|
description: `restore to operation ${args.operation}`,
|
|
11063
11221
|
parents: [],
|
|
11064
11222
|
changeSnapshot,
|
|
11065
|
-
view: { ...targetOp.view, fileSnapshot: diskSnapshotBeforeThisOp },
|
|
11223
|
+
view: { ...targetOp.view, fileSnapshot: diskSnapshotBeforeThisOp, hiddenChanges },
|
|
11066
11224
|
});
|
|
11067
11225
|
|
|
11068
11226
|
return {
|
|
@@ -11681,6 +11839,9 @@ async function createJJ(options) {
|
|
|
11681
11839
|
}
|
|
11682
11840
|
|
|
11683
11841
|
await graph.load();
|
|
11842
|
+
// Both outcomes below record the working copy in their operation —
|
|
11843
|
+
// a fresh instance hasn't loaded it yet.
|
|
11844
|
+
await workingCopy.load();
|
|
11684
11845
|
await userConfig.load();
|
|
11685
11846
|
const siblings = graph.getDivergentSiblings(changeId);
|
|
11686
11847
|
|