isomorphic-jj 1.9.0 → 1.9.2
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 +149 -0
- package/dist/index.cjs +250 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +250 -11
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/api/repository.js +87 -3
- package/src/core/revset-engine.js +112 -4
- package/src/core/working-copy.js +51 -4
package/dist/index.mjs
CHANGED
|
@@ -989,6 +989,44 @@ class ChangeGraph {
|
|
|
989
989
|
*/
|
|
990
990
|
|
|
991
991
|
|
|
992
|
+
/**
|
|
993
|
+
* Normalize an mtime value to a millisecond epoch number so two stat()
|
|
994
|
+
* results (or a stat() result and a stored value) can be compared by VALUE
|
|
995
|
+
* instead of by object identity (see issue #41).
|
|
996
|
+
*
|
|
997
|
+
* `fs.promises.stat()` returns a fresh `Date` instance on every call, so two
|
|
998
|
+
* separate stats of the same unchanged file are never `===`/`!==`-equal even
|
|
999
|
+
* when the underlying timestamp is identical. Additionally, once a tracked
|
|
1000
|
+
* mtime has been through a `JSON.stringify`/`JSON.parse` round-trip (Storage
|
|
1001
|
+
* persists working-copy state as JSON), a `Date` becomes an ISO string, not a
|
|
1002
|
+
* `Date` — so the stored side of the comparison may be a `Date`, a plain
|
|
1003
|
+
* epoch-ms number (this module now stores numbers going forward), or an ISO
|
|
1004
|
+
* string (older/reloaded-from-disk state). This normalizes all three to a
|
|
1005
|
+
* comparable number.
|
|
1006
|
+
*
|
|
1007
|
+
* Deliberately duck-types the Date case (`typeof value.getTime ===
|
|
1008
|
+
* 'function'`) instead of `value instanceof Date`: Node's built-in `fs`
|
|
1009
|
+
* module constructs `Stats.mtime` using its own realm's `Date`, which is not
|
|
1010
|
+
* always the same `Date` constructor as this module's under test runners
|
|
1011
|
+
* that sandbox each test file in its own VM context (observed under Jest
|
|
1012
|
+
* with `--experimental-vm-modules`) — there, a real `Date` instance from
|
|
1013
|
+
* `fs.promises.stat()` fails `instanceof Date` here even though it's a
|
|
1014
|
+
* genuine Date. Duck-typing survives that realm mismatch.
|
|
1015
|
+
*
|
|
1016
|
+
* @param {Date|number|string|undefined} value
|
|
1017
|
+
* @returns {number}
|
|
1018
|
+
*/
|
|
1019
|
+
function mtimeMs(value) {
|
|
1020
|
+
if (value === null || value === undefined) return NaN;
|
|
1021
|
+
if (typeof value === 'number') return value;
|
|
1022
|
+
if (typeof value === 'string') return new Date(value).getTime();
|
|
1023
|
+
// Per the type contract above, anything left is Date-like -- duck-typed
|
|
1024
|
+
// (see the comment above) rather than `instanceof Date`, so no runtime
|
|
1025
|
+
// check is needed here either; a value outside this contract is a caller
|
|
1026
|
+
// bug, not something to silently coerce.
|
|
1027
|
+
return /** @type {any} */ (value).getTime();
|
|
1028
|
+
}
|
|
1029
|
+
|
|
992
1030
|
class WorkingCopy {
|
|
993
1031
|
/**
|
|
994
1032
|
* @param {Storage} storage - Storage manager instance
|
|
@@ -1160,8 +1198,10 @@ class WorkingCopy {
|
|
|
1160
1198
|
try {
|
|
1161
1199
|
const stats = await this.fs.promises.stat(fullPath);
|
|
1162
1200
|
|
|
1163
|
-
// Fast path: check mtime and size
|
|
1164
|
-
|
|
1201
|
+
// Fast path: check mtime and size. Compare by VALUE (see mtimeMs
|
|
1202
|
+
// above) — comparing the raw stat()/stored values with !== is
|
|
1203
|
+
// always "different" even when nothing changed (issue #41).
|
|
1204
|
+
if (mtimeMs(stats.mtime) !== mtimeMs(tracked.mtime) || stats.size !== tracked.size) {
|
|
1165
1205
|
modified.push(path);
|
|
1166
1206
|
}
|
|
1167
1207
|
} catch (error) {
|
|
@@ -1272,12 +1312,19 @@ class WorkingCopy {
|
|
|
1272
1312
|
}
|
|
1273
1313
|
|
|
1274
1314
|
const prev = this.state.fileStates[rel];
|
|
1275
|
-
|
|
1315
|
+
// Store mtime as a plain epoch-ms number (not the raw stat() value):
|
|
1316
|
+
// numbers round-trip through JSON exactly, so state reloaded from disk
|
|
1317
|
+
// later compares identically to state still cached in-memory. Goes
|
|
1318
|
+
// through mtimeMs() rather than assuming `.getTime()` exists directly —
|
|
1319
|
+
// some fs implementations (e.g. the in-memory mock used in tests)
|
|
1320
|
+
// return mtime as a plain epoch-ms number already, not a Date (see
|
|
1321
|
+
// mtimeMs above and issue #41).
|
|
1322
|
+
const next = { mtime: mtimeMs(stats.mtime), size: stats.size, mode: stats.mode };
|
|
1276
1323
|
|
|
1277
1324
|
if (!prev) {
|
|
1278
1325
|
this.state.fileStates[rel] = next;
|
|
1279
1326
|
added.push(rel);
|
|
1280
|
-
} else if (
|
|
1327
|
+
} else if (mtimeMs(stats.mtime) !== mtimeMs(prev.mtime) || prev.size !== stats.size) {
|
|
1281
1328
|
this.state.fileStates[rel] = next;
|
|
1282
1329
|
modified.push(rel);
|
|
1283
1330
|
}
|
|
@@ -2666,9 +2713,12 @@ class RevsetEngine {
|
|
|
2666
2713
|
const unquote = (/** @type {string} */ s) => s.trim().replace(/^['"]|['"]$/g, '');
|
|
2667
2714
|
|
|
2668
2715
|
switch (name) {
|
|
2669
|
-
case 'all':
|
|
2716
|
+
case 'all': {
|
|
2670
2717
|
await this.graph.load();
|
|
2671
|
-
|
|
2718
|
+
const allChanges = this.graph.getAll();
|
|
2719
|
+
const hidden = await this._computeHiddenOrphans(allChanges);
|
|
2720
|
+
return allChanges.filter((c) => !hidden.has(c.changeId)).map((c) => c.changeId);
|
|
2721
|
+
}
|
|
2672
2722
|
|
|
2673
2723
|
// `builtin_log()` (jj v0.44) is the revset the built-in `jj log` uses
|
|
2674
2724
|
// when no user override is configured. isomorphic-jj's `log()` already
|
|
@@ -2676,9 +2726,12 @@ class RevsetEngine {
|
|
|
2676
2726
|
// `builtin_log()` is a pure alias for that same default — this lets
|
|
2677
2727
|
// callers compose it (e.g. `builtin_log() & mine()`) instead of
|
|
2678
2728
|
// duplicating `all()`.
|
|
2679
|
-
case 'builtin_log':
|
|
2729
|
+
case 'builtin_log': {
|
|
2680
2730
|
await this.graph.load();
|
|
2681
|
-
|
|
2731
|
+
const allChanges = this.graph.getAll();
|
|
2732
|
+
const hidden = await this._computeHiddenOrphans(allChanges);
|
|
2733
|
+
return allChanges.filter((c) => !hidden.has(c.changeId)).map((c) => c.changeId);
|
|
2734
|
+
}
|
|
2682
2735
|
|
|
2683
2736
|
case 'none':
|
|
2684
2737
|
return [];
|
|
@@ -3139,6 +3192,108 @@ class RevsetEngine {
|
|
|
3139
3192
|
return ancestors;
|
|
3140
3193
|
}
|
|
3141
3194
|
|
|
3195
|
+
/**
|
|
3196
|
+
* Change IDs that `all()`/`builtin_log()` (and therefore the default
|
|
3197
|
+
* `jj.log()`) should exclude — issue #37(b).
|
|
3198
|
+
*
|
|
3199
|
+
* undo()/redo()/operations.restore() deliberately don't delete a change
|
|
3200
|
+
* record for a `new()`/`squash()` that gets reverted — they only un-ref
|
|
3201
|
+
* it (restore the working-copy pointer/heads that made it reachable),
|
|
3202
|
+
* matching real jj where an unreachable change stays resolvable by its
|
|
3203
|
+
* change id (hidden, not gone) until explicitly gc'd — see
|
|
3204
|
+
* computeGraphReversal's doc comment in repository.js. But that means
|
|
3205
|
+
* the orphaned change record lingers in the graph, and before this fix
|
|
3206
|
+
* `all()` (a flat `graph.getAll()`) surfaced it in `log()` right
|
|
3207
|
+
* alongside everything still actually reachable.
|
|
3208
|
+
*
|
|
3209
|
+
* Real jj hides a commit from the default log when it is BOTH:
|
|
3210
|
+
* - unreachable from any visible head (not itself a head, and not an
|
|
3211
|
+
* ancestor of one), AND
|
|
3212
|
+
* - "discardable": empty (no file diff versus its own parent) and
|
|
3213
|
+
* undescribed.
|
|
3214
|
+
* Merge commits (2+ parents) are conservatively never treated as
|
|
3215
|
+
* "empty" here — real jj's diff-emptiness rule for merges is more
|
|
3216
|
+
* involved and isn't needed by anything this package creates today.
|
|
3217
|
+
*
|
|
3218
|
+
* "Visible head" here deliberately does NOT mean "any change with no
|
|
3219
|
+
* children" (that's what the `visible_heads()` revset function computes,
|
|
3220
|
+
* structurally, from the raw graph) — an orphan `new()`/`squash()`
|
|
3221
|
+
* change that undo() un-referenced has no children EITHER, so it would
|
|
3222
|
+
* trivially count as its own head under that definition and never be
|
|
3223
|
+
* excludable. Real jj instead tracks an explicit, maintained set of
|
|
3224
|
+
* heads in its View (updated as operations run — abandoning/squashing a
|
|
3225
|
+
* change removes it, rebasing children onto a new parent removes the
|
|
3226
|
+
* old parent, etc.). This package doesn't thread the operation log's
|
|
3227
|
+
* `view.heads` through to the revset engine, but the working-copy
|
|
3228
|
+
* pointer plus every bookmark/tag target is the closest available
|
|
3229
|
+
* proxy for "what's actually still in play" — anything only reachable
|
|
3230
|
+
* from those, not from every dangling graph leaf.
|
|
3231
|
+
*
|
|
3232
|
+
* @param {any[]} allChanges - `graph.getAll()`
|
|
3233
|
+
* @returns {Promise<Set<string>>} change IDs to exclude
|
|
3234
|
+
*/
|
|
3235
|
+
async _computeHiddenOrphans(allChanges) {
|
|
3236
|
+
const trackedHeads = new Set();
|
|
3237
|
+
try {
|
|
3238
|
+
const wcId = this.workingCopy && this.workingCopy.getCurrentChangeId();
|
|
3239
|
+
if (wcId) trackedHeads.add(wcId);
|
|
3240
|
+
} catch {
|
|
3241
|
+
// working copy not loaded/available — fine, just no wc-based root.
|
|
3242
|
+
}
|
|
3243
|
+
if (this.bookmarkStore) {
|
|
3244
|
+
await this.bookmarkStore.load();
|
|
3245
|
+
for (const bookmark of await this.bookmarkStore.list()) {
|
|
3246
|
+
if (bookmark && bookmark.changeId) trackedHeads.add(bookmark.changeId);
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
if (this.tagStore) {
|
|
3250
|
+
for (const tag of await this.tagStore.list()) {
|
|
3251
|
+
if (tag && tag.changeId) trackedHeads.add(tag.changeId);
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3255
|
+
const reachable = new Set();
|
|
3256
|
+
for (const headId of trackedHeads) {
|
|
3257
|
+
for (const id of await this.getAncestors(headId)) {
|
|
3258
|
+
reachable.add(id);
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
// Sentinel "no message given" descriptions this package itself writes
|
|
3263
|
+
// (new()'s '(no description)', squash()'s post-squash working-copy
|
|
3264
|
+
// change '(no description set)') are, for visibility purposes, just
|
|
3265
|
+
// as undescribed as a genuinely empty/missing description field.
|
|
3266
|
+
const UNDESCRIBED = new Set([undefined, null, '', '(no description)', '(no description set)']);
|
|
3267
|
+
|
|
3268
|
+
const byId = new Map(allChanges.map((c) => [c.changeId, c]));
|
|
3269
|
+
const isEmpty = (/** @type {any} */ change) => {
|
|
3270
|
+
const parents = change.parents || [];
|
|
3271
|
+
if (parents.length === 0) {
|
|
3272
|
+
return !change.fileSnapshot || Object.keys(change.fileSnapshot).length === 0;
|
|
3273
|
+
}
|
|
3274
|
+
if (parents.length > 1) return false; // merges: not handled by this rule
|
|
3275
|
+
const parent = byId.get(parents[0]);
|
|
3276
|
+
const parentSnapshot = (parent && parent.fileSnapshot) || {};
|
|
3277
|
+
const ownSnapshot = change.fileSnapshot || {};
|
|
3278
|
+
const parentKeys = Object.keys(parentSnapshot);
|
|
3279
|
+
const ownKeys = Object.keys(ownSnapshot);
|
|
3280
|
+
if (parentKeys.length !== ownKeys.length) return false;
|
|
3281
|
+
return parentKeys.every((k) => parentSnapshot[k] === ownSnapshot[k]);
|
|
3282
|
+
};
|
|
3283
|
+
|
|
3284
|
+
const hidden = new Set();
|
|
3285
|
+
for (const change of allChanges) {
|
|
3286
|
+
if (
|
|
3287
|
+
!reachable.has(change.changeId) &&
|
|
3288
|
+
isEmpty(change) &&
|
|
3289
|
+
UNDESCRIBED.has(change.description)
|
|
3290
|
+
) {
|
|
3291
|
+
hidden.add(change.changeId);
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
return hidden;
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3142
3297
|
/**
|
|
3143
3298
|
* Resolve a revset argument to a set of change IDs (v1.5).
|
|
3144
3299
|
*
|
|
@@ -7897,7 +8052,16 @@ async function createJJ(options) {
|
|
|
7897
8052
|
await graph.load();
|
|
7898
8053
|
for (const [changeId, snapshot] of Object.entries(reversal.changeSnapshotToApply)) {
|
|
7899
8054
|
if (await graph.getChange(changeId)) {
|
|
7900
|
-
|
|
8055
|
+
// Bypass the git-sync middleware (baseGraph, not the wrapped
|
|
8056
|
+
// `graph`): undo/restore are pure reverts, and `snapshot` already
|
|
8057
|
+
// carries the change's ORIGINAL commitId from before the reverted
|
|
8058
|
+
// operation(s) ran. Going through graph.updateChange() would fire
|
|
8059
|
+
// onUpdateChange -> syncChangeToGit(), which mints a brand-new
|
|
8060
|
+
// commit (and thus a new commitId) from the CURRENT tree instead
|
|
8061
|
+
// of restoring the one that was actually there — see issue #37(c):
|
|
8062
|
+
// "undo/restore give reverted commits new commit IDs". Writing the
|
|
8063
|
+
// snapshot verbatim via baseGraph puts the original commitId back.
|
|
8064
|
+
await baseGraph.updateChange(snapshot);
|
|
7901
8065
|
}
|
|
7902
8066
|
}
|
|
7903
8067
|
|
|
@@ -10035,6 +10199,16 @@ async function createJJ(options) {
|
|
|
10035
10199
|
// which operation it targeted so a following undo() can walk one
|
|
10036
10200
|
// step further back (resolveUndoTarget) instead of toggling, and
|
|
10037
10201
|
// carrying exactly what redo() needs to re-apply (captured above).
|
|
10202
|
+
//
|
|
10203
|
+
// Also record `changeSnapshot: redoChangeSnapshot` — the SAME
|
|
10204
|
+
// ground-truth-before-this-op values, but under the field name
|
|
10205
|
+
// computeGraphReversal() actually scans for. Without this, undo()
|
|
10206
|
+
// itself was the one mutating operation in the whole API that never
|
|
10207
|
+
// recorded its own changeSnapshot, so a LATER undo() targeting THIS
|
|
10208
|
+
// undo (to step back past it) had nothing to restore: it would
|
|
10209
|
+
// repoint the working copy and files, but leave whatever this
|
|
10210
|
+
// undo() overwrote (e.g. a change's description) exactly as this
|
|
10211
|
+
// undo() left it. See issue #37(a).
|
|
10038
10212
|
await oplog.recordOperation({
|
|
10039
10213
|
timestamp: new Date().toISOString(),
|
|
10040
10214
|
user: await getUserOplogInfo(),
|
|
@@ -10042,6 +10216,7 @@ async function createJJ(options) {
|
|
|
10042
10216
|
eventType: 'undo',
|
|
10043
10217
|
targetOpId: targetOp.id,
|
|
10044
10218
|
undoneOpId: targetOp.id, // kept for API/type compatibility
|
|
10219
|
+
changeSnapshot: redoChangeSnapshot,
|
|
10045
10220
|
redoChangeSnapshot,
|
|
10046
10221
|
redoFileSnapshot,
|
|
10047
10222
|
redoConflictsSnapshot,
|
|
@@ -10119,9 +10294,25 @@ async function createJJ(options) {
|
|
|
10119
10294
|
// by scanning forward. undo() captured it directly for exactly this
|
|
10120
10295
|
// reason (see its own comment).
|
|
10121
10296
|
await graph.load();
|
|
10297
|
+
|
|
10298
|
+
// Capture ground truth right before overwriting, same as undo()
|
|
10299
|
+
// does for its own changeSnapshot (issue #37a) — lets a later
|
|
10300
|
+
// undo() targeting THIS redo op restore what redo() is about to
|
|
10301
|
+
// clobber, instead of finding no changeSnapshot at all.
|
|
10302
|
+
/** @type {Record<string, any>} */
|
|
10303
|
+
const undoChangeSnapshot = {};
|
|
10304
|
+
for (const changeId of Object.keys(undoOp.redoChangeSnapshot || {})) {
|
|
10305
|
+
const current = await graph.getChange(changeId);
|
|
10306
|
+
if (current) undoChangeSnapshot[changeId] = current;
|
|
10307
|
+
}
|
|
10308
|
+
|
|
10122
10309
|
for (const [changeId, snapshot] of Object.entries(undoOp.redoChangeSnapshot || {})) {
|
|
10123
10310
|
if (await graph.getChange(changeId)) {
|
|
10124
|
-
|
|
10311
|
+
// Bypass the git-sync middleware — see the matching comment in
|
|
10312
|
+
// applyGraphReversal(). `snapshot` already carries the change's
|
|
10313
|
+
// correct commitId from before the undo it's reversing ran
|
|
10314
|
+
// (issue #37c applies to redo() too).
|
|
10315
|
+
await baseGraph.updateChange(snapshot);
|
|
10125
10316
|
}
|
|
10126
10317
|
}
|
|
10127
10318
|
if (undoOp.redoFileSnapshot !== undefined) {
|
|
@@ -10142,6 +10333,7 @@ async function createJJ(options) {
|
|
|
10142
10333
|
description: 'redo operation',
|
|
10143
10334
|
eventType: 'redo',
|
|
10144
10335
|
redoOf: undoOp.id,
|
|
10336
|
+
changeSnapshot: undoChangeSnapshot,
|
|
10145
10337
|
parents: [],
|
|
10146
10338
|
view: redoneView,
|
|
10147
10339
|
});
|
|
@@ -10432,6 +10624,23 @@ async function createJJ(options) {
|
|
|
10432
10624
|
// Revert the change graph, working-copy files, and conflict state
|
|
10433
10625
|
// to exactly what they were right after targetOp ran.
|
|
10434
10626
|
const reversal = computeGraphReversal(ops, targetOp);
|
|
10627
|
+
|
|
10628
|
+
// Capture ground truth for every changeId this restore is about to
|
|
10629
|
+
// overwrite, BEFORE applying the reversal — exactly like undo()
|
|
10630
|
+
// does for its own changeSnapshot. Without this, restore()'s own
|
|
10631
|
+
// oplog entry carried no changeSnapshot at all, so undoing the
|
|
10632
|
+
// restore itself had nothing to put back: a later undo() would
|
|
10633
|
+
// repoint the working copy and files, but leave whatever the
|
|
10634
|
+
// restore had just overwritten (e.g. a change's description)
|
|
10635
|
+
// exactly as the restore left it. See issue #37(a).
|
|
10636
|
+
await graph.load();
|
|
10637
|
+
/** @type {Record<string, any>} */
|
|
10638
|
+
const changeSnapshot = {};
|
|
10639
|
+
for (const changeId of Object.keys(reversal.changeSnapshotToApply)) {
|
|
10640
|
+
const current = await graph.getChange(changeId);
|
|
10641
|
+
if (current) changeSnapshot[changeId] = current;
|
|
10642
|
+
}
|
|
10643
|
+
|
|
10435
10644
|
await applyGraphReversal(reversal);
|
|
10436
10645
|
|
|
10437
10646
|
// Record this restoration as a new operation
|
|
@@ -10440,6 +10649,7 @@ async function createJJ(options) {
|
|
|
10440
10649
|
user: await getUserOplogInfo(),
|
|
10441
10650
|
description: `restore to operation ${args.operation}`,
|
|
10442
10651
|
parents: [],
|
|
10652
|
+
changeSnapshot,
|
|
10443
10653
|
view: targetOp.view,
|
|
10444
10654
|
});
|
|
10445
10655
|
|
|
@@ -10799,10 +11009,22 @@ async function createJJ(options) {
|
|
|
10799
11009
|
},
|
|
10800
11010
|
description: '(no description set)',
|
|
10801
11011
|
timestamp: new Date().toISOString(),
|
|
10802
|
-
|
|
11012
|
+
// Start from the DESTINATION's tree (which now includes source's
|
|
11013
|
+
// folded-in changes, set above), not an empty object — matches
|
|
11014
|
+
// real `jj squash`, where the new working-copy change looks
|
|
11015
|
+
// identical to its new parent until you touch something. Issue
|
|
11016
|
+
// #39: this used to hard-code `{}`, so status()/snapshot() saw
|
|
11017
|
+
// every on-disk file as untracked/new until something re-wrote
|
|
11018
|
+
// it and triggered a fresh auto-snapshot.
|
|
11019
|
+
fileSnapshot: { ...destChange.fileSnapshot },
|
|
10803
11020
|
};
|
|
10804
11021
|
|
|
10805
11022
|
await graph.addChange(newChange); // Middleware will sync to Git
|
|
11023
|
+
// Sync the working directory to match (issue #30's fix for
|
|
11024
|
+
// new()/edit(); squash()'s own synthetic post-squash change never
|
|
11025
|
+
// did this) — a no-op when source's parent was already dest (the
|
|
11026
|
+
// common case), but necessary whenever source/dest aren't adjacent.
|
|
11027
|
+
await syncWorkingCopyFiles(newChange.fileSnapshot);
|
|
10806
11028
|
await workingCopy.setCurrentChange(newChangeId);
|
|
10807
11029
|
newWorkingCopyId = newChangeId;
|
|
10808
11030
|
}
|
|
@@ -10884,6 +11106,12 @@ async function createJJ(options) {
|
|
|
10884
11106
|
throw new JJError('CHANGE_NOT_FOUND', `Change ${changeId} not found`);
|
|
10885
11107
|
}
|
|
10886
11108
|
|
|
11109
|
+
// Captured BEFORE anything mutates — see issue #38: abandoning the
|
|
11110
|
+
// change @ currently points at must move @ off it (real jj checks
|
|
11111
|
+
// out a fresh empty change on the abandoned change's own parent(s));
|
|
11112
|
+
// abandoning any OTHER change must leave @ exactly where it was.
|
|
11113
|
+
const wasWorkingCopy = changeId === workingCopy.getCurrentChangeId();
|
|
11114
|
+
|
|
10887
11115
|
// Dispatch change:abandoning event (preventable)
|
|
10888
11116
|
await dispatchEventAsync(jj, 'change:abandoning', {
|
|
10889
11117
|
operation: 'abandon',
|
|
@@ -10936,6 +11164,17 @@ async function createJJ(options) {
|
|
|
10936
11164
|
{ cancelable: false }
|
|
10937
11165
|
);
|
|
10938
11166
|
|
|
11167
|
+
// If @ was pointing at the change we just abandoned, it can't stay
|
|
11168
|
+
// there — real jj moves @ to a new empty change on the abandoned
|
|
11169
|
+
// change's own parent(s) (issue #38). Reuse new()'s own "create an
|
|
11170
|
+
// empty change with given parents" logic rather than duplicating it
|
|
11171
|
+
// — this is exactly the workaround callers had to reach for
|
|
11172
|
+
// themselves (`new({ parents: [parentOfAt] })` before abandoning).
|
|
11173
|
+
if (wasWorkingCopy) {
|
|
11174
|
+
const newWorkingCopyChange = await jj.new({ parents: change.parents || [] });
|
|
11175
|
+
return { ...change, newWorkingCopyChangeId: newWorkingCopyChange.changeId };
|
|
11176
|
+
}
|
|
11177
|
+
|
|
10939
11178
|
return change;
|
|
10940
11179
|
},
|
|
10941
11180
|
|