isomorphic-jj 1.9.2 → 1.10.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 +126 -0
- package/dist/index.cjs +320 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +320 -11
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/api/repository.js +291 -10
- package/src/core/revset-engine.js +29 -1
package/dist/index.mjs
CHANGED
|
@@ -3281,11 +3281,39 @@ class RevsetEngine {
|
|
|
3281
3281
|
return parentKeys.every((k) => parentSnapshot[k] === ownSnapshot[k]);
|
|
3282
3282
|
};
|
|
3283
3283
|
|
|
3284
|
+
// issue #43(2): the original #37(b) rule above (unreachable + isEmpty +
|
|
3285
|
+
// undescribed) only hides an orphan while it still happens to have
|
|
3286
|
+
// zero file diff versus its CURRENT parent. Two real shapes slip past
|
|
3287
|
+
// that once undo()/operations.restore() are involved, because both are
|
|
3288
|
+
// purely internal rewrite/undo artifacts that were never "empty" in
|
|
3289
|
+
// any way a user would recognize — they simply never held real user
|
|
3290
|
+
// content of their own, and a later revert of something else can make
|
|
3291
|
+
// isEmpty() disagree even though nothing about THIS change changed:
|
|
3292
|
+
//
|
|
3293
|
+
// 1. squash() creates a fresh empty `@` on top of `dest` when
|
|
3294
|
+
// squashing the working copy. If that squash is later undone,
|
|
3295
|
+
// the fresh `@` becomes unreferenced — but if `dest` itself gets
|
|
3296
|
+
// reverted back to its pre-squash content by the SAME undo,
|
|
3297
|
+
// isEmpty() now compares the orphan's (frozen, post-squash)
|
|
3298
|
+
// fileSnapshot against dest's (reverted, pre-squash) one and
|
|
3299
|
+
// finds a mismatch, even though the orphan is exactly as
|
|
3300
|
+
// "discardable" as it was the instant it was created.
|
|
3301
|
+
// 2. new() creates a fresh empty change on top of the previous `@`.
|
|
3302
|
+
// If something writes into it (write()/snapshot()) before an
|
|
3303
|
+
// undo() unwinds back past the new(), the orphan is left with
|
|
3304
|
+
// real file content that no longer matches its parent, even
|
|
3305
|
+
// though the user never gave it a real description.
|
|
3306
|
+
//
|
|
3307
|
+
// Changes created by squash()/new() are tagged `autoCreated: true` at
|
|
3308
|
+
// creation (see squash()/new() in repository.js) specifically so this
|
|
3309
|
+
// filter can hide them on that basis alone — still gated on being
|
|
3310
|
+
// unreachable and undescribed, so a change the user actually described
|
|
3311
|
+
// (or that's still live) is never hidden by this rule.
|
|
3284
3312
|
const hidden = new Set();
|
|
3285
3313
|
for (const change of allChanges) {
|
|
3286
3314
|
if (
|
|
3287
3315
|
!reachable.has(change.changeId) &&
|
|
3288
|
-
isEmpty(change) &&
|
|
3316
|
+
(isEmpty(change) || change.autoCreated) &&
|
|
3289
3317
|
UNDESCRIBED.has(change.description)
|
|
3290
3318
|
) {
|
|
3291
3319
|
hidden.add(change.changeId);
|
|
@@ -8085,6 +8113,38 @@ async function createJJ(options) {
|
|
|
8085
8113
|
// an earlier, unrelated merge(). Fixed as part of issue #29.)
|
|
8086
8114
|
};
|
|
8087
8115
|
|
|
8116
|
+
/**
|
|
8117
|
+
* Compute the full current set of "head" changeIds — every non-abandoned
|
|
8118
|
+
* change with no non-abandoned child. Used to record an accurate
|
|
8119
|
+
* `view.heads` when an operation runs (issue #43).
|
|
8120
|
+
*
|
|
8121
|
+
* Before this helper existed, several oplog-recording call sites
|
|
8122
|
+
* (squash(), rebase()/moveChange(), abandon()) hand-wrote a single-
|
|
8123
|
+
* element or empty `heads` array covering only the changeId(s) that
|
|
8124
|
+
* specific call happened to touch. That silently dropped any OTHER
|
|
8125
|
+
* live head the operation didn't itself touch (e.g. a sibling branch
|
|
8126
|
+
* left untouched by an abandon()/squash() of a different branch — "two
|
|
8127
|
+
* sibling changes report only one head between them"), and squash() in
|
|
8128
|
+
* particular recorded the OLD destination changeId instead of the fresh
|
|
8129
|
+
* working-copy change it creates on top of it when squashing @ ("after
|
|
8130
|
+
* a squash [heads] points at the parent instead of the new tip").
|
|
8131
|
+
*
|
|
8132
|
+
* @returns {Promise<string[]>}
|
|
8133
|
+
*/
|
|
8134
|
+
const computeCurrentHeads = async () => {
|
|
8135
|
+
await graph.load();
|
|
8136
|
+
const allChanges = await graph.getAllChanges();
|
|
8137
|
+
const nonAbandoned = allChanges.filter((/** @type {any} */ c) => !c.abandoned);
|
|
8138
|
+
const nonAbandonedIds = new Set(nonAbandoned.map((/** @type {any} */ c) => c.changeId));
|
|
8139
|
+
const heads = [];
|
|
8140
|
+
for (const change of nonAbandoned) {
|
|
8141
|
+
const children = graph.getChildren(change.changeId);
|
|
8142
|
+
const hasLiveChild = children.some((/** @type {string} */ id) => nonAbandonedIds.has(id));
|
|
8143
|
+
if (!hasLiveChild) heads.push(change.changeId);
|
|
8144
|
+
}
|
|
8145
|
+
return heads;
|
|
8146
|
+
};
|
|
8147
|
+
|
|
8088
8148
|
/**
|
|
8089
8149
|
* Re-parent every child of `changeId` onto `newParents` instead — used
|
|
8090
8150
|
* when a change is being elided from the graph (abandon(), issue #30;
|
|
@@ -8116,6 +8176,18 @@ async function createJJ(options) {
|
|
|
8116
8176
|
}
|
|
8117
8177
|
};
|
|
8118
8178
|
|
|
8179
|
+
// NOTE (issue #43(5), punted — see PR description): an earlier version of
|
|
8180
|
+
// this file added a `rebaseDescendantsOntoUpdatedParent()` helper, called
|
|
8181
|
+
// from edit(), to propagate an edited ancestor's content onto descendants
|
|
8182
|
+
// automatically. It was reverted: it directly conflicts with absorb()'s
|
|
8183
|
+
// job (folding a working-copy edit back into an ancestor it actually
|
|
8184
|
+
// belongs to) — with the auto-rebase in place, edit()+write()+edit()-away
|
|
8185
|
+
// already changes the descendant before absorb() ever runs, so absorb()
|
|
8186
|
+
// finds nothing left to do (see tests/integration/absorb.test.js
|
|
8187
|
+
// "should work after edit()", which regressed against this). Fixing
|
|
8188
|
+
// #43(5) properly needs to account for that interaction rather than
|
|
8189
|
+
// duplicate it; left for a follow-up.
|
|
8190
|
+
|
|
8119
8191
|
/**
|
|
8120
8192
|
* Determine which operation a fresh undo() call should revert, matching
|
|
8121
8193
|
* real jj's progressive-undo semantics (`jj-undo(1)`: "If used once after
|
|
@@ -8551,6 +8623,17 @@ async function createJJ(options) {
|
|
|
8551
8623
|
});
|
|
8552
8624
|
}
|
|
8553
8625
|
|
|
8626
|
+
await workingCopy.load();
|
|
8627
|
+
|
|
8628
|
+
// Capture disk state right before this write mutates it, and record
|
|
8629
|
+
// an operation for it (issue #43(1)): write() previously recorded NO
|
|
8630
|
+
// operation at all, so an edit typed into a change's working copy
|
|
8631
|
+
// could never be rewound by undo()/operations.restore() regardless
|
|
8632
|
+
// of which op you targeted — the change kept its post-edit content
|
|
8633
|
+
// forever. Mirrors the "snapshot before, record after" pattern every
|
|
8634
|
+
// other mutator in this file uses (see describe(), moveChange()).
|
|
8635
|
+
const fileSnapshotBefore = await snapshotFilesystem();
|
|
8636
|
+
|
|
8554
8637
|
const fullPath = `${dir}/${args.path}`;
|
|
8555
8638
|
|
|
8556
8639
|
// Ensure directory exists
|
|
@@ -8566,7 +8649,6 @@ async function createJJ(options) {
|
|
|
8566
8649
|
await fs.promises.writeFile(fullPath, data, 'utf8');
|
|
8567
8650
|
|
|
8568
8651
|
// Track the file in working copy
|
|
8569
|
-
await workingCopy.load();
|
|
8570
8652
|
const stats = await fs.promises.stat(fullPath);
|
|
8571
8653
|
await workingCopy.trackFile(args.path, {
|
|
8572
8654
|
mtime: stats.mtime,
|
|
@@ -8574,6 +8656,20 @@ async function createJJ(options) {
|
|
|
8574
8656
|
mode: stats.mode,
|
|
8575
8657
|
});
|
|
8576
8658
|
|
|
8659
|
+
await oplog.recordOperation({
|
|
8660
|
+
timestamp: new Date().toISOString(),
|
|
8661
|
+
user: await getUserOplogInfo(),
|
|
8662
|
+
description: `write ${args.path}`,
|
|
8663
|
+
parents: [],
|
|
8664
|
+
view: {
|
|
8665
|
+
bookmarks: {},
|
|
8666
|
+
remoteBookmarks: {},
|
|
8667
|
+
heads: await computeCurrentHeads(),
|
|
8668
|
+
workingCopy: workingCopy.getCurrentChangeId(),
|
|
8669
|
+
fileSnapshot: fileSnapshotBefore, // Store filesystem state from before this write
|
|
8670
|
+
},
|
|
8671
|
+
});
|
|
8672
|
+
|
|
8577
8673
|
// Return useful information about the written file
|
|
8578
8674
|
return {
|
|
8579
8675
|
path: args.path,
|
|
@@ -9002,6 +9098,15 @@ async function createJJ(options) {
|
|
|
9002
9098
|
async moveChange(args) {
|
|
9003
9099
|
await graph.load();
|
|
9004
9100
|
|
|
9101
|
+
// Capture disk state right before this rebase mutates anything, so
|
|
9102
|
+
// undo()/redo()/operations.restore() can rewind past it — see issue
|
|
9103
|
+
// #43(1): rebase()/moveChange() previously recorded no "files
|
|
9104
|
+
// before" snapshot at all, so restoring to a point right before a
|
|
9105
|
+
// rebase ran had no on-disk state to fall back to and would instead
|
|
9106
|
+
// pick up whatever a LATER operation happened to record (the wrong
|
|
9107
|
+
// point in time).
|
|
9108
|
+
const fileSnapshotBefore = await snapshotFilesystem();
|
|
9109
|
+
|
|
9005
9110
|
// Support multiple parameter names for flexibility
|
|
9006
9111
|
const changeId = args.changeId || args.from || args.source;
|
|
9007
9112
|
const newParent = args.newParent || args.to || args.destination;
|
|
@@ -9114,6 +9219,32 @@ async function createJJ(options) {
|
|
|
9114
9219
|
await conflicts.addConflict(conflict);
|
|
9115
9220
|
}
|
|
9116
9221
|
|
|
9222
|
+
// Materialize real conflict-marker text into the moved change's file
|
|
9223
|
+
// content — issue #43(3): rebase()/moveChange() used to record the
|
|
9224
|
+
// conflict correctly as DATA (via conflicts.addConflict() above) but
|
|
9225
|
+
// left the file's content exactly as it was pre-rebase ("ours"),
|
|
9226
|
+
// never writing marker text into change.fileSnapshot or, when this
|
|
9227
|
+
// change is the checked-out working copy, onto disk. Reuses
|
|
9228
|
+
// conflicts.markers()'s own marker-formatting logic (the same format
|
|
9229
|
+
// jj.conflicts.markers() already returns) instead of re-deriving
|
|
9230
|
+
// marker text a second way.
|
|
9231
|
+
if (detectedConflicts.length > 0) {
|
|
9232
|
+
change.fileSnapshot = { ...(change.fileSnapshot || {}) };
|
|
9233
|
+
const isCheckedOut = changeId === workingCopy.getCurrentChangeId();
|
|
9234
|
+
for (const conflict of detectedConflicts) {
|
|
9235
|
+
const markerText = await jj.conflicts.markers({ conflictId: conflict.conflictId });
|
|
9236
|
+
change.fileSnapshot[conflict.path] = markerText;
|
|
9237
|
+
if (isCheckedOut) {
|
|
9238
|
+
const fullPath = join(dir, conflict.path);
|
|
9239
|
+
const pathParts = conflict.path.split('/');
|
|
9240
|
+
if (pathParts.length > 1) {
|
|
9241
|
+
await mkdirp(fs, join(dir, pathParts.slice(0, -1).join('/')));
|
|
9242
|
+
}
|
|
9243
|
+
await fs.promises.writeFile(fullPath, markerText, 'utf8');
|
|
9244
|
+
}
|
|
9245
|
+
}
|
|
9246
|
+
}
|
|
9247
|
+
|
|
9117
9248
|
// Update parent
|
|
9118
9249
|
change.parents = [newParent];
|
|
9119
9250
|
await graph.updateChange(change);
|
|
@@ -9129,8 +9260,9 @@ async function createJJ(options) {
|
|
|
9129
9260
|
view: {
|
|
9130
9261
|
bookmarks: {},
|
|
9131
9262
|
remoteBookmarks: {},
|
|
9132
|
-
heads:
|
|
9263
|
+
heads: await computeCurrentHeads(),
|
|
9133
9264
|
workingCopy: workingCopy.getCurrentChangeId(),
|
|
9265
|
+
fileSnapshot: fileSnapshotBefore,
|
|
9134
9266
|
},
|
|
9135
9267
|
});
|
|
9136
9268
|
|
|
@@ -9585,6 +9717,16 @@ async function createJJ(options) {
|
|
|
9585
9717
|
description: args.message || '(no description)',
|
|
9586
9718
|
timestamp: new Date().toISOString(),
|
|
9587
9719
|
fileSnapshot: initialSnapshot, // Initialize with parent's snapshot
|
|
9720
|
+
// Issue #43(2): marks this as an internal, synthetic artifact
|
|
9721
|
+
// rather than something the user actually created, so the revset
|
|
9722
|
+
// engine's orphan-visibility filter (_computeHiddenOrphans) can
|
|
9723
|
+
// still hide it after it's un-referenced by a later undo()/
|
|
9724
|
+
// operations.restore(), even if some out-of-band mutation (e.g.
|
|
9725
|
+
// write()+snapshot() before the undo unwinds this far) has since
|
|
9726
|
+
// made it look "non-empty" relative to its parent. A real
|
|
9727
|
+
// description (args.message, or a later describe()) still makes
|
|
9728
|
+
// it visible regardless, via the UNDESCRIBED check.
|
|
9729
|
+
autoCreated: true,
|
|
9588
9730
|
};
|
|
9589
9731
|
|
|
9590
9732
|
// Dispatch change:creating event (preventable)
|
|
@@ -9705,17 +9847,51 @@ async function createJJ(options) {
|
|
|
9705
9847
|
await graph.load();
|
|
9706
9848
|
await workingCopy.load();
|
|
9707
9849
|
const result = await workingCopy.snapshot();
|
|
9850
|
+
|
|
9851
|
+
// Disk content itself is not mutated by this call (it's a read,
|
|
9852
|
+
// not a write) — the same value is therefore valid both as the
|
|
9853
|
+
// refreshed change.fileSnapshot AND as the recorded operation's
|
|
9854
|
+
// `view.fileSnapshot` "before" value (nothing on disk changes
|
|
9855
|
+
// between the two).
|
|
9856
|
+
const wcId = workingCopy.getCurrentChangeId();
|
|
9857
|
+
const currentDiskSnapshot = await snapshotFilesystem();
|
|
9858
|
+
|
|
9708
9859
|
// Refresh the working-copy change's content snapshot from disk.
|
|
9860
|
+
/** @type {Record<string, any>} */
|
|
9861
|
+
const changeSnapshot = {};
|
|
9709
9862
|
try {
|
|
9710
|
-
const wcId = workingCopy.getCurrentChangeId();
|
|
9711
9863
|
const change = await graph.getChange(wcId);
|
|
9712
9864
|
if (change) {
|
|
9713
|
-
|
|
9865
|
+
// Snapshot BEFORE mutating so undo()/redo()/operations.restore()
|
|
9866
|
+
// can reverse this content refresh — see issue #43(1):
|
|
9867
|
+
// snapshot() previously mutated `change.fileSnapshot` in place
|
|
9868
|
+
// with no "before" capture and recorded no operation at all, so
|
|
9869
|
+
// computeGraphReversal() had nothing to find for this changeId
|
|
9870
|
+
// if a restore()/undo() landed between an out-of-band edit and
|
|
9871
|
+
// the next real command.
|
|
9872
|
+
changeSnapshot[wcId] = structuredClone(change);
|
|
9873
|
+
change.fileSnapshot = currentDiskSnapshot;
|
|
9714
9874
|
await graph.updateChange(change);
|
|
9715
9875
|
}
|
|
9716
9876
|
} catch {
|
|
9717
9877
|
// Best-effort content refresh.
|
|
9718
9878
|
}
|
|
9879
|
+
|
|
9880
|
+
await oplog.recordOperation({
|
|
9881
|
+
timestamp: new Date().toISOString(),
|
|
9882
|
+
user: await getUserOplogInfo(),
|
|
9883
|
+
description: 'snapshot working copy',
|
|
9884
|
+
parents: [],
|
|
9885
|
+
changeSnapshot,
|
|
9886
|
+
view: {
|
|
9887
|
+
bookmarks: {},
|
|
9888
|
+
remoteBookmarks: {},
|
|
9889
|
+
heads: await computeCurrentHeads(),
|
|
9890
|
+
workingCopy: wcId,
|
|
9891
|
+
fileSnapshot: currentDiskSnapshot,
|
|
9892
|
+
},
|
|
9893
|
+
});
|
|
9894
|
+
|
|
9719
9895
|
return result;
|
|
9720
9896
|
},
|
|
9721
9897
|
|
|
@@ -10179,8 +10355,20 @@ async function createJJ(options) {
|
|
|
10179
10355
|
const current = await graph.getChange(changeId);
|
|
10180
10356
|
if (current) redoChangeSnapshot[changeId] = current;
|
|
10181
10357
|
}
|
|
10358
|
+
// Capture disk state right before this undo() runs. Used both as
|
|
10359
|
+
// `redoFileSnapshot` (for a later redo() — only meaningful when this
|
|
10360
|
+
// reversal actually touches files) AND, unconditionally, as THIS
|
|
10361
|
+
// undo operation's own `view.fileSnapshot` "before" value below —
|
|
10362
|
+
// see issue #43(1): undo()/redo()/operations.restore() used to
|
|
10363
|
+
// record `view: landingView` verbatim, i.e. reusing the LANDING
|
|
10364
|
+
// operation's own recorded fileSnapshot (disk state before THAT op
|
|
10365
|
+
// ran) instead of disk state before THIS undo ran. A later
|
|
10366
|
+
// restore()/undo() scanning forward past this one would then pick
|
|
10367
|
+
// up that stale, wrong snapshot instead of what was actually on disk
|
|
10368
|
+
// right before this undo executed.
|
|
10369
|
+
const diskSnapshotBeforeThisOp = await snapshotFilesystem();
|
|
10182
10370
|
const redoFileSnapshot =
|
|
10183
|
-
reversal.fileSnapshot !== undefined ?
|
|
10371
|
+
reversal.fileSnapshot !== undefined ? diskSnapshotBeforeThisOp : undefined;
|
|
10184
10372
|
let redoConflictsSnapshot;
|
|
10185
10373
|
if (reversal.conflictsSnapshot !== undefined) {
|
|
10186
10374
|
await conflicts.load();
|
|
@@ -10221,7 +10409,7 @@ async function createJJ(options) {
|
|
|
10221
10409
|
redoFileSnapshot,
|
|
10222
10410
|
redoConflictsSnapshot,
|
|
10223
10411
|
parents: [],
|
|
10224
|
-
view: landingView,
|
|
10412
|
+
view: { ...landingView, fileSnapshot: diskSnapshotBeforeThisOp },
|
|
10225
10413
|
});
|
|
10226
10414
|
|
|
10227
10415
|
const restoredFileCount =
|
|
@@ -10315,6 +10503,15 @@ async function createJJ(options) {
|
|
|
10315
10503
|
await baseGraph.updateChange(snapshot);
|
|
10316
10504
|
}
|
|
10317
10505
|
}
|
|
10506
|
+
|
|
10507
|
+
// Capture disk state right before THIS redo() mutates it — see issue
|
|
10508
|
+
// #43(1)/the matching fix in undo(): `view: redoneView` below reuses
|
|
10509
|
+
// the ORIGINAL (re-applied) operation's own recorded pointers, but
|
|
10510
|
+
// its `fileSnapshot` sub-field must reflect disk state before THIS
|
|
10511
|
+
// redo ran, not before that original operation ran, or a later
|
|
10512
|
+
// restore()/undo() scanning forward past this redo would pick up a
|
|
10513
|
+
// stale, wrong snapshot.
|
|
10514
|
+
const diskSnapshotBeforeThisOp = await snapshotFilesystem();
|
|
10318
10515
|
if (undoOp.redoFileSnapshot !== undefined) {
|
|
10319
10516
|
await syncWorkingCopyFiles(undoOp.redoFileSnapshot);
|
|
10320
10517
|
}
|
|
@@ -10335,7 +10532,7 @@ async function createJJ(options) {
|
|
|
10335
10532
|
redoOf: undoOp.id,
|
|
10336
10533
|
changeSnapshot: undoChangeSnapshot,
|
|
10337
10534
|
parents: [],
|
|
10338
|
-
view: redoneView,
|
|
10535
|
+
view: { ...redoneView, fileSnapshot: diskSnapshotBeforeThisOp },
|
|
10339
10536
|
});
|
|
10340
10537
|
|
|
10341
10538
|
const restoredFileCount =
|
|
@@ -10641,6 +10838,17 @@ async function createJJ(options) {
|
|
|
10641
10838
|
if (current) changeSnapshot[changeId] = current;
|
|
10642
10839
|
}
|
|
10643
10840
|
|
|
10841
|
+
// Capture disk state right before THIS restore() mutates it — see
|
|
10842
|
+
// issue #43(1)/the matching fix in undo()/redo(): recording
|
|
10843
|
+
// `view: targetOp.view` verbatim reused targetOp's OWN recorded
|
|
10844
|
+
// `fileSnapshot` (disk state before targetOp ran), not disk state
|
|
10845
|
+
// before THIS restore ran. A later restore()/undo() scanning
|
|
10846
|
+
// forward past this one would then pick up that stale, wrong
|
|
10847
|
+
// snapshot instead of what was actually on disk right before this
|
|
10848
|
+
// restore executed — this is exactly the "forward restore to
|
|
10849
|
+
// 'edit tip' lost style.css" repro.
|
|
10850
|
+
const diskSnapshotBeforeThisOp = await snapshotFilesystem();
|
|
10851
|
+
|
|
10644
10852
|
await applyGraphReversal(reversal);
|
|
10645
10853
|
|
|
10646
10854
|
// Record this restoration as a new operation
|
|
@@ -10650,7 +10858,7 @@ async function createJJ(options) {
|
|
|
10650
10858
|
description: `restore to operation ${args.operation}`,
|
|
10651
10859
|
parents: [],
|
|
10652
10860
|
changeSnapshot,
|
|
10653
|
-
view: targetOp.view,
|
|
10861
|
+
view: { ...targetOp.view, fileSnapshot: diskSnapshotBeforeThisOp },
|
|
10654
10862
|
});
|
|
10655
10863
|
|
|
10656
10864
|
return {
|
|
@@ -10886,6 +11094,15 @@ async function createJJ(options) {
|
|
|
10886
11094
|
await workingCopy.load();
|
|
10887
11095
|
await userConfig.load();
|
|
10888
11096
|
|
|
11097
|
+
// Capture disk state right before this squash mutates anything, so
|
|
11098
|
+
// undo()/redo()/operations.restore() can rewind past it — see issue
|
|
11099
|
+
// #43(1): squash() previously recorded no "files before" snapshot at
|
|
11100
|
+
// all, so restoring to a point right before a squash ran had no
|
|
11101
|
+
// recorded on-disk state and would fall through to whatever a LATER
|
|
11102
|
+
// operation happened to record instead (the repro: a forward
|
|
11103
|
+
// restore to "edit tip" lost `style.css`).
|
|
11104
|
+
const fileSnapshotBefore = await snapshotFilesystem();
|
|
11105
|
+
|
|
10889
11106
|
// Support 'into' as alias for 'dest' (matches JJ CLI)
|
|
10890
11107
|
const dest = args.into || args.dest;
|
|
10891
11108
|
|
|
@@ -11017,6 +11234,18 @@ async function createJJ(options) {
|
|
|
11017
11234
|
// every on-disk file as untracked/new until something re-wrote
|
|
11018
11235
|
// it and triggered a fresh auto-snapshot.
|
|
11019
11236
|
fileSnapshot: { ...destChange.fileSnapshot },
|
|
11237
|
+
// Marks this as an internal, synthetic artifact rather than
|
|
11238
|
+
// something the user actually created — issue #43(2): if this
|
|
11239
|
+
// squash is later undone, this change becomes unreferenced, and
|
|
11240
|
+
// by then `destChange` may have been reverted to pre-squash
|
|
11241
|
+
// content, making this change look "non-empty" relative to its
|
|
11242
|
+
// (now-reverted) parent even though it never held any real user
|
|
11243
|
+
// content of its own. The revset engine's orphan-visibility
|
|
11244
|
+
// filter (_computeHiddenOrphans) treats `autoCreated` as
|
|
11245
|
+
// sufficient on its own (given it's also unreferenced and
|
|
11246
|
+
// undescribed) rather than re-diffing against a parent that may
|
|
11247
|
+
// no longer resemble what this change was actually created from.
|
|
11248
|
+
autoCreated: true,
|
|
11020
11249
|
};
|
|
11021
11250
|
|
|
11022
11251
|
await graph.addChange(newChange); // Middleware will sync to Git
|
|
@@ -11039,8 +11268,18 @@ async function createJJ(options) {
|
|
|
11039
11268
|
view: {
|
|
11040
11269
|
bookmarks: {},
|
|
11041
11270
|
remoteBookmarks: {},
|
|
11042
|
-
|
|
11271
|
+
// issue #43(1): this used to hard-code `[destChangeId]` — wrong
|
|
11272
|
+
// whenever squash() created a fresh working-copy change on top
|
|
11273
|
+
// of dest (the real new tip was `newWorkingCopyId`, not
|
|
11274
|
+
// `destChangeId` itself), and incomplete whenever a sibling head
|
|
11275
|
+
// elsewhere in the graph existed and wasn't touched by this
|
|
11276
|
+
// squash at all ("two sibling changes report only one head
|
|
11277
|
+
// between them"). computeCurrentHeads() reflects the WHOLE
|
|
11278
|
+
// repo's actual head set after this squash, not just the
|
|
11279
|
+
// changeId(s) this call happened to touch.
|
|
11280
|
+
heads: await computeCurrentHeads(),
|
|
11043
11281
|
workingCopy: newWorkingCopyId,
|
|
11282
|
+
fileSnapshot: fileSnapshotBefore,
|
|
11044
11283
|
},
|
|
11045
11284
|
});
|
|
11046
11285
|
|
|
@@ -11112,6 +11351,12 @@ async function createJJ(options) {
|
|
|
11112
11351
|
// abandoning any OTHER change must leave @ exactly where it was.
|
|
11113
11352
|
const wasWorkingCopy = changeId === workingCopy.getCurrentChangeId();
|
|
11114
11353
|
|
|
11354
|
+
// Capture disk state right before this abandon mutates anything, so
|
|
11355
|
+
// undo()/redo()/operations.restore() can rewind past it — see issue
|
|
11356
|
+
// #43(1): abandon() previously recorded no "files before" snapshot
|
|
11357
|
+
// at all.
|
|
11358
|
+
const fileSnapshotBefore = await snapshotFilesystem();
|
|
11359
|
+
|
|
11115
11360
|
// Dispatch change:abandoning event (preventable)
|
|
11116
11361
|
await dispatchEventAsync(jj, 'change:abandoning', {
|
|
11117
11362
|
operation: 'abandon',
|
|
@@ -11146,8 +11391,16 @@ async function createJJ(options) {
|
|
|
11146
11391
|
view: {
|
|
11147
11392
|
bookmarks: {},
|
|
11148
11393
|
remoteBookmarks: {},
|
|
11149
|
-
|
|
11394
|
+
// issue #43(1): this used to hard-code `[]`, dropping every live
|
|
11395
|
+
// head from the recorded operation (including any untouched
|
|
11396
|
+
// sibling branch — "two sibling changes report only one head
|
|
11397
|
+
// between them"). computeCurrentHeads() reflects the actual
|
|
11398
|
+
// repo-wide head set right after this abandon (and its
|
|
11399
|
+
// reparenting) ran, before the compensating jj.new() below (if
|
|
11400
|
+
// any) records its own separate operation.
|
|
11401
|
+
heads: await computeCurrentHeads(),
|
|
11150
11402
|
workingCopy: workingCopy.getCurrentChangeId(),
|
|
11403
|
+
fileSnapshot: fileSnapshotBefore,
|
|
11151
11404
|
},
|
|
11152
11405
|
});
|
|
11153
11406
|
|
|
@@ -13409,6 +13662,32 @@ async function createJJ(options) {
|
|
|
13409
13662
|
throw new JJError('CONFLICT_NOT_FOUND', `Conflict ${args.conflictId} not found`);
|
|
13410
13663
|
}
|
|
13411
13664
|
|
|
13665
|
+
// Snapshot BEFORE mutating so undo()/redo()/operations.restore()
|
|
13666
|
+
// can reverse this resolution — issue #43(4): conflicts.resolve()
|
|
13667
|
+
// previously recorded no operation at all, so resolving a conflict
|
|
13668
|
+
// could never be undone: no changeSnapshot of the working-copy
|
|
13669
|
+
// change's content, no conflictsSnapshot of the pre-resolve
|
|
13670
|
+
// conflict state, and no fileSnapshot of the on-disk content
|
|
13671
|
+
// before the resolved content was written.
|
|
13672
|
+
await graph.load();
|
|
13673
|
+
await workingCopy.load();
|
|
13674
|
+
// structuredClone matters here (unlike the read-only conflictsSnapshot
|
|
13675
|
+
// capture in moveChange()/merge()): resolveConflict() below mutates
|
|
13676
|
+
// the SAME conflict object in place (`conflict.resolved = true`),
|
|
13677
|
+
// and Object.fromEntries() on the live Map would otherwise leave
|
|
13678
|
+
// this "before" snapshot holding a reference to that very object —
|
|
13679
|
+
// corrupting it into already showing `resolved: true` by the time
|
|
13680
|
+
// it's recorded.
|
|
13681
|
+
const conflictsSnapshotBefore = structuredClone({
|
|
13682
|
+
conflicts: Object.fromEntries(conflicts.conflicts),
|
|
13683
|
+
fileConflicts: Object.fromEntries(conflicts.fileConflicts),
|
|
13684
|
+
});
|
|
13685
|
+
const fileSnapshotBefore = await snapshotFilesystem();
|
|
13686
|
+
const wcId = workingCopy.getCurrentChangeId();
|
|
13687
|
+
const wcChange = await graph.getChange(wcId);
|
|
13688
|
+
/** @type {Record<string, any>} */
|
|
13689
|
+
const changeSnapshot = wcChange ? { [wcId]: structuredClone(wcChange) } : {};
|
|
13690
|
+
|
|
13412
13691
|
let resolvedContent;
|
|
13413
13692
|
|
|
13414
13693
|
// v0.5: Support different resolution methods
|
|
@@ -13452,6 +13731,36 @@ async function createJJ(options) {
|
|
|
13452
13731
|
await conflicts.resolveConflict(args.conflictId, 'manual');
|
|
13453
13732
|
await conflicts.save();
|
|
13454
13733
|
|
|
13734
|
+
// Refresh the working-copy change's own fileSnapshot to match the
|
|
13735
|
+
// resolved on-disk content (conflicted files always live under
|
|
13736
|
+
// the checked-out working copy — see the writeFile above), so
|
|
13737
|
+
// undo()/restore() has an accurate "after" state to compare
|
|
13738
|
+
// against, and log()/read() see the resolution immediately rather
|
|
13739
|
+
// than waiting for the next auto-snapshot.
|
|
13740
|
+
if (wcChange) {
|
|
13741
|
+
wcChange.fileSnapshot = {
|
|
13742
|
+
...(wcChange.fileSnapshot || {}),
|
|
13743
|
+
[conflict.path]: resolvedContent,
|
|
13744
|
+
};
|
|
13745
|
+
await graph.updateChange(wcChange);
|
|
13746
|
+
}
|
|
13747
|
+
|
|
13748
|
+
await oplog.recordOperation({
|
|
13749
|
+
timestamp: new Date().toISOString(),
|
|
13750
|
+
user: await getUserOplogInfo(),
|
|
13751
|
+
description: `resolve conflict ${args.conflictId} (${conflict.path})`,
|
|
13752
|
+
parents: [],
|
|
13753
|
+
changeSnapshot,
|
|
13754
|
+
conflictsSnapshot: conflictsSnapshotBefore,
|
|
13755
|
+
view: {
|
|
13756
|
+
bookmarks: {},
|
|
13757
|
+
remoteBookmarks: {},
|
|
13758
|
+
heads: await computeCurrentHeads(),
|
|
13759
|
+
workingCopy: wcId,
|
|
13760
|
+
fileSnapshot: fileSnapshotBefore,
|
|
13761
|
+
},
|
|
13762
|
+
});
|
|
13763
|
+
|
|
13455
13764
|
return { resolved: true };
|
|
13456
13765
|
},
|
|
13457
13766
|
|