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/package.json
CHANGED
package/src/api/repository.js
CHANGED
|
@@ -282,7 +282,16 @@ export async function createJJ(options) {
|
|
|
282
282
|
await graph.load();
|
|
283
283
|
for (const [changeId, snapshot] of Object.entries(reversal.changeSnapshotToApply)) {
|
|
284
284
|
if (await graph.getChange(changeId)) {
|
|
285
|
-
|
|
285
|
+
// Bypass the git-sync middleware (baseGraph, not the wrapped
|
|
286
|
+
// `graph`): undo/restore are pure reverts, and `snapshot` already
|
|
287
|
+
// carries the change's ORIGINAL commitId from before the reverted
|
|
288
|
+
// operation(s) ran. Going through graph.updateChange() would fire
|
|
289
|
+
// onUpdateChange -> syncChangeToGit(), which mints a brand-new
|
|
290
|
+
// commit (and thus a new commitId) from the CURRENT tree instead
|
|
291
|
+
// of restoring the one that was actually there — see issue #37(c):
|
|
292
|
+
// "undo/restore give reverted commits new commit IDs". Writing the
|
|
293
|
+
// snapshot verbatim via baseGraph puts the original commitId back.
|
|
294
|
+
await baseGraph.updateChange(snapshot);
|
|
286
295
|
}
|
|
287
296
|
}
|
|
288
297
|
|
|
@@ -2420,6 +2429,16 @@ export async function createJJ(options) {
|
|
|
2420
2429
|
// which operation it targeted so a following undo() can walk one
|
|
2421
2430
|
// step further back (resolveUndoTarget) instead of toggling, and
|
|
2422
2431
|
// carrying exactly what redo() needs to re-apply (captured above).
|
|
2432
|
+
//
|
|
2433
|
+
// Also record `changeSnapshot: redoChangeSnapshot` — the SAME
|
|
2434
|
+
// ground-truth-before-this-op values, but under the field name
|
|
2435
|
+
// computeGraphReversal() actually scans for. Without this, undo()
|
|
2436
|
+
// itself was the one mutating operation in the whole API that never
|
|
2437
|
+
// recorded its own changeSnapshot, so a LATER undo() targeting THIS
|
|
2438
|
+
// undo (to step back past it) had nothing to restore: it would
|
|
2439
|
+
// repoint the working copy and files, but leave whatever this
|
|
2440
|
+
// undo() overwrote (e.g. a change's description) exactly as this
|
|
2441
|
+
// undo() left it. See issue #37(a).
|
|
2423
2442
|
await oplog.recordOperation({
|
|
2424
2443
|
timestamp: new Date().toISOString(),
|
|
2425
2444
|
user: await getUserOplogInfo(),
|
|
@@ -2427,6 +2446,7 @@ export async function createJJ(options) {
|
|
|
2427
2446
|
eventType: 'undo',
|
|
2428
2447
|
targetOpId: targetOp.id,
|
|
2429
2448
|
undoneOpId: targetOp.id, // kept for API/type compatibility
|
|
2449
|
+
changeSnapshot: redoChangeSnapshot,
|
|
2430
2450
|
redoChangeSnapshot,
|
|
2431
2451
|
redoFileSnapshot,
|
|
2432
2452
|
redoConflictsSnapshot,
|
|
@@ -2504,9 +2524,25 @@ export async function createJJ(options) {
|
|
|
2504
2524
|
// by scanning forward. undo() captured it directly for exactly this
|
|
2505
2525
|
// reason (see its own comment).
|
|
2506
2526
|
await graph.load();
|
|
2527
|
+
|
|
2528
|
+
// Capture ground truth right before overwriting, same as undo()
|
|
2529
|
+
// does for its own changeSnapshot (issue #37a) — lets a later
|
|
2530
|
+
// undo() targeting THIS redo op restore what redo() is about to
|
|
2531
|
+
// clobber, instead of finding no changeSnapshot at all.
|
|
2532
|
+
/** @type {Record<string, any>} */
|
|
2533
|
+
const undoChangeSnapshot = {};
|
|
2534
|
+
for (const changeId of Object.keys(undoOp.redoChangeSnapshot || {})) {
|
|
2535
|
+
const current = await graph.getChange(changeId);
|
|
2536
|
+
if (current) undoChangeSnapshot[changeId] = current;
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2507
2539
|
for (const [changeId, snapshot] of Object.entries(undoOp.redoChangeSnapshot || {})) {
|
|
2508
2540
|
if (await graph.getChange(changeId)) {
|
|
2509
|
-
|
|
2541
|
+
// Bypass the git-sync middleware — see the matching comment in
|
|
2542
|
+
// applyGraphReversal(). `snapshot` already carries the change's
|
|
2543
|
+
// correct commitId from before the undo it's reversing ran
|
|
2544
|
+
// (issue #37c applies to redo() too).
|
|
2545
|
+
await baseGraph.updateChange(snapshot);
|
|
2510
2546
|
}
|
|
2511
2547
|
}
|
|
2512
2548
|
if (undoOp.redoFileSnapshot !== undefined) {
|
|
@@ -2527,6 +2563,7 @@ export async function createJJ(options) {
|
|
|
2527
2563
|
description: 'redo operation',
|
|
2528
2564
|
eventType: 'redo',
|
|
2529
2565
|
redoOf: undoOp.id,
|
|
2566
|
+
changeSnapshot: undoChangeSnapshot,
|
|
2530
2567
|
parents: [],
|
|
2531
2568
|
view: redoneView,
|
|
2532
2569
|
});
|
|
@@ -2817,6 +2854,23 @@ export async function createJJ(options) {
|
|
|
2817
2854
|
// Revert the change graph, working-copy files, and conflict state
|
|
2818
2855
|
// to exactly what they were right after targetOp ran.
|
|
2819
2856
|
const reversal = computeGraphReversal(ops, targetOp);
|
|
2857
|
+
|
|
2858
|
+
// Capture ground truth for every changeId this restore is about to
|
|
2859
|
+
// overwrite, BEFORE applying the reversal — exactly like undo()
|
|
2860
|
+
// does for its own changeSnapshot. Without this, restore()'s own
|
|
2861
|
+
// oplog entry carried no changeSnapshot at all, so undoing the
|
|
2862
|
+
// restore itself had nothing to put back: a later undo() would
|
|
2863
|
+
// repoint the working copy and files, but leave whatever the
|
|
2864
|
+
// restore had just overwritten (e.g. a change's description)
|
|
2865
|
+
// exactly as the restore left it. See issue #37(a).
|
|
2866
|
+
await graph.load();
|
|
2867
|
+
/** @type {Record<string, any>} */
|
|
2868
|
+
const changeSnapshot = {};
|
|
2869
|
+
for (const changeId of Object.keys(reversal.changeSnapshotToApply)) {
|
|
2870
|
+
const current = await graph.getChange(changeId);
|
|
2871
|
+
if (current) changeSnapshot[changeId] = current;
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2820
2874
|
await applyGraphReversal(reversal);
|
|
2821
2875
|
|
|
2822
2876
|
// Record this restoration as a new operation
|
|
@@ -2825,6 +2879,7 @@ export async function createJJ(options) {
|
|
|
2825
2879
|
user: await getUserOplogInfo(),
|
|
2826
2880
|
description: `restore to operation ${args.operation}`,
|
|
2827
2881
|
parents: [],
|
|
2882
|
+
changeSnapshot,
|
|
2828
2883
|
view: targetOp.view,
|
|
2829
2884
|
});
|
|
2830
2885
|
|
|
@@ -3184,10 +3239,22 @@ export async function createJJ(options) {
|
|
|
3184
3239
|
},
|
|
3185
3240
|
description: '(no description set)',
|
|
3186
3241
|
timestamp: new Date().toISOString(),
|
|
3187
|
-
|
|
3242
|
+
// Start from the DESTINATION's tree (which now includes source's
|
|
3243
|
+
// folded-in changes, set above), not an empty object — matches
|
|
3244
|
+
// real `jj squash`, where the new working-copy change looks
|
|
3245
|
+
// identical to its new parent until you touch something. Issue
|
|
3246
|
+
// #39: this used to hard-code `{}`, so status()/snapshot() saw
|
|
3247
|
+
// every on-disk file as untracked/new until something re-wrote
|
|
3248
|
+
// it and triggered a fresh auto-snapshot.
|
|
3249
|
+
fileSnapshot: { ...destChange.fileSnapshot },
|
|
3188
3250
|
};
|
|
3189
3251
|
|
|
3190
3252
|
await graph.addChange(newChange); // Middleware will sync to Git
|
|
3253
|
+
// Sync the working directory to match (issue #30's fix for
|
|
3254
|
+
// new()/edit(); squash()'s own synthetic post-squash change never
|
|
3255
|
+
// did this) — a no-op when source's parent was already dest (the
|
|
3256
|
+
// common case), but necessary whenever source/dest aren't adjacent.
|
|
3257
|
+
await syncWorkingCopyFiles(newChange.fileSnapshot);
|
|
3191
3258
|
await workingCopy.setCurrentChange(newChangeId);
|
|
3192
3259
|
newWorkingCopyId = newChangeId;
|
|
3193
3260
|
}
|
|
@@ -3269,6 +3336,12 @@ export async function createJJ(options) {
|
|
|
3269
3336
|
throw new JJError('CHANGE_NOT_FOUND', `Change ${changeId} not found`);
|
|
3270
3337
|
}
|
|
3271
3338
|
|
|
3339
|
+
// Captured BEFORE anything mutates — see issue #38: abandoning the
|
|
3340
|
+
// change @ currently points at must move @ off it (real jj checks
|
|
3341
|
+
// out a fresh empty change on the abandoned change's own parent(s));
|
|
3342
|
+
// abandoning any OTHER change must leave @ exactly where it was.
|
|
3343
|
+
const wasWorkingCopy = changeId === workingCopy.getCurrentChangeId();
|
|
3344
|
+
|
|
3272
3345
|
// Dispatch change:abandoning event (preventable)
|
|
3273
3346
|
await dispatchEventAsync(jj, 'change:abandoning', {
|
|
3274
3347
|
operation: 'abandon',
|
|
@@ -3321,6 +3394,17 @@ export async function createJJ(options) {
|
|
|
3321
3394
|
{ cancelable: false }
|
|
3322
3395
|
);
|
|
3323
3396
|
|
|
3397
|
+
// If @ was pointing at the change we just abandoned, it can't stay
|
|
3398
|
+
// there — real jj moves @ to a new empty change on the abandoned
|
|
3399
|
+
// change's own parent(s) (issue #38). Reuse new()'s own "create an
|
|
3400
|
+
// empty change with given parents" logic rather than duplicating it
|
|
3401
|
+
// — this is exactly the workaround callers had to reach for
|
|
3402
|
+
// themselves (`new({ parents: [parentOfAt] })` before abandoning).
|
|
3403
|
+
if (wasWorkingCopy) {
|
|
3404
|
+
const newWorkingCopyChange = await jj.new({ parents: change.parents || [] });
|
|
3405
|
+
return { ...change, newWorkingCopyChangeId: newWorkingCopyChange.changeId };
|
|
3406
|
+
}
|
|
3407
|
+
|
|
3324
3408
|
return change;
|
|
3325
3409
|
},
|
|
3326
3410
|
|
|
@@ -539,9 +539,12 @@ export class RevsetEngine {
|
|
|
539
539
|
const unquote = (/** @type {string} */ s) => s.trim().replace(/^['"]|['"]$/g, '');
|
|
540
540
|
|
|
541
541
|
switch (name) {
|
|
542
|
-
case 'all':
|
|
542
|
+
case 'all': {
|
|
543
543
|
await this.graph.load();
|
|
544
|
-
|
|
544
|
+
const allChanges = this.graph.getAll();
|
|
545
|
+
const hidden = await this._computeHiddenOrphans(allChanges);
|
|
546
|
+
return allChanges.filter((c) => !hidden.has(c.changeId)).map((c) => c.changeId);
|
|
547
|
+
}
|
|
545
548
|
|
|
546
549
|
// `builtin_log()` (jj v0.44) is the revset the built-in `jj log` uses
|
|
547
550
|
// when no user override is configured. isomorphic-jj's `log()` already
|
|
@@ -549,9 +552,12 @@ export class RevsetEngine {
|
|
|
549
552
|
// `builtin_log()` is a pure alias for that same default — this lets
|
|
550
553
|
// callers compose it (e.g. `builtin_log() & mine()`) instead of
|
|
551
554
|
// duplicating `all()`.
|
|
552
|
-
case 'builtin_log':
|
|
555
|
+
case 'builtin_log': {
|
|
553
556
|
await this.graph.load();
|
|
554
|
-
|
|
557
|
+
const allChanges = this.graph.getAll();
|
|
558
|
+
const hidden = await this._computeHiddenOrphans(allChanges);
|
|
559
|
+
return allChanges.filter((c) => !hidden.has(c.changeId)).map((c) => c.changeId);
|
|
560
|
+
}
|
|
555
561
|
|
|
556
562
|
case 'none':
|
|
557
563
|
return [];
|
|
@@ -1012,6 +1018,108 @@ export class RevsetEngine {
|
|
|
1012
1018
|
return ancestors;
|
|
1013
1019
|
}
|
|
1014
1020
|
|
|
1021
|
+
/**
|
|
1022
|
+
* Change IDs that `all()`/`builtin_log()` (and therefore the default
|
|
1023
|
+
* `jj.log()`) should exclude — issue #37(b).
|
|
1024
|
+
*
|
|
1025
|
+
* undo()/redo()/operations.restore() deliberately don't delete a change
|
|
1026
|
+
* record for a `new()`/`squash()` that gets reverted — they only un-ref
|
|
1027
|
+
* it (restore the working-copy pointer/heads that made it reachable),
|
|
1028
|
+
* matching real jj where an unreachable change stays resolvable by its
|
|
1029
|
+
* change id (hidden, not gone) until explicitly gc'd — see
|
|
1030
|
+
* computeGraphReversal's doc comment in repository.js. But that means
|
|
1031
|
+
* the orphaned change record lingers in the graph, and before this fix
|
|
1032
|
+
* `all()` (a flat `graph.getAll()`) surfaced it in `log()` right
|
|
1033
|
+
* alongside everything still actually reachable.
|
|
1034
|
+
*
|
|
1035
|
+
* Real jj hides a commit from the default log when it is BOTH:
|
|
1036
|
+
* - unreachable from any visible head (not itself a head, and not an
|
|
1037
|
+
* ancestor of one), AND
|
|
1038
|
+
* - "discardable": empty (no file diff versus its own parent) and
|
|
1039
|
+
* undescribed.
|
|
1040
|
+
* Merge commits (2+ parents) are conservatively never treated as
|
|
1041
|
+
* "empty" here — real jj's diff-emptiness rule for merges is more
|
|
1042
|
+
* involved and isn't needed by anything this package creates today.
|
|
1043
|
+
*
|
|
1044
|
+
* "Visible head" here deliberately does NOT mean "any change with no
|
|
1045
|
+
* children" (that's what the `visible_heads()` revset function computes,
|
|
1046
|
+
* structurally, from the raw graph) — an orphan `new()`/`squash()`
|
|
1047
|
+
* change that undo() un-referenced has no children EITHER, so it would
|
|
1048
|
+
* trivially count as its own head under that definition and never be
|
|
1049
|
+
* excludable. Real jj instead tracks an explicit, maintained set of
|
|
1050
|
+
* heads in its View (updated as operations run — abandoning/squashing a
|
|
1051
|
+
* change removes it, rebasing children onto a new parent removes the
|
|
1052
|
+
* old parent, etc.). This package doesn't thread the operation log's
|
|
1053
|
+
* `view.heads` through to the revset engine, but the working-copy
|
|
1054
|
+
* pointer plus every bookmark/tag target is the closest available
|
|
1055
|
+
* proxy for "what's actually still in play" — anything only reachable
|
|
1056
|
+
* from those, not from every dangling graph leaf.
|
|
1057
|
+
*
|
|
1058
|
+
* @param {any[]} allChanges - `graph.getAll()`
|
|
1059
|
+
* @returns {Promise<Set<string>>} change IDs to exclude
|
|
1060
|
+
*/
|
|
1061
|
+
async _computeHiddenOrphans(allChanges) {
|
|
1062
|
+
const trackedHeads = new Set();
|
|
1063
|
+
try {
|
|
1064
|
+
const wcId = this.workingCopy && this.workingCopy.getCurrentChangeId();
|
|
1065
|
+
if (wcId) trackedHeads.add(wcId);
|
|
1066
|
+
} catch {
|
|
1067
|
+
// working copy not loaded/available — fine, just no wc-based root.
|
|
1068
|
+
}
|
|
1069
|
+
if (this.bookmarkStore) {
|
|
1070
|
+
await this.bookmarkStore.load();
|
|
1071
|
+
for (const bookmark of await this.bookmarkStore.list()) {
|
|
1072
|
+
if (bookmark && bookmark.changeId) trackedHeads.add(bookmark.changeId);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
if (this.tagStore) {
|
|
1076
|
+
for (const tag of await this.tagStore.list()) {
|
|
1077
|
+
if (tag && tag.changeId) trackedHeads.add(tag.changeId);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
const reachable = new Set();
|
|
1082
|
+
for (const headId of trackedHeads) {
|
|
1083
|
+
for (const id of await this.getAncestors(headId)) {
|
|
1084
|
+
reachable.add(id);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// Sentinel "no message given" descriptions this package itself writes
|
|
1089
|
+
// (new()'s '(no description)', squash()'s post-squash working-copy
|
|
1090
|
+
// change '(no description set)') are, for visibility purposes, just
|
|
1091
|
+
// as undescribed as a genuinely empty/missing description field.
|
|
1092
|
+
const UNDESCRIBED = new Set([undefined, null, '', '(no description)', '(no description set)']);
|
|
1093
|
+
|
|
1094
|
+
const byId = new Map(allChanges.map((c) => [c.changeId, c]));
|
|
1095
|
+
const isEmpty = (/** @type {any} */ change) => {
|
|
1096
|
+
const parents = change.parents || [];
|
|
1097
|
+
if (parents.length === 0) {
|
|
1098
|
+
return !change.fileSnapshot || Object.keys(change.fileSnapshot).length === 0;
|
|
1099
|
+
}
|
|
1100
|
+
if (parents.length > 1) return false; // merges: not handled by this rule
|
|
1101
|
+
const parent = byId.get(parents[0]);
|
|
1102
|
+
const parentSnapshot = (parent && parent.fileSnapshot) || {};
|
|
1103
|
+
const ownSnapshot = change.fileSnapshot || {};
|
|
1104
|
+
const parentKeys = Object.keys(parentSnapshot);
|
|
1105
|
+
const ownKeys = Object.keys(ownSnapshot);
|
|
1106
|
+
if (parentKeys.length !== ownKeys.length) return false;
|
|
1107
|
+
return parentKeys.every((k) => parentSnapshot[k] === ownSnapshot[k]);
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1110
|
+
const hidden = new Set();
|
|
1111
|
+
for (const change of allChanges) {
|
|
1112
|
+
if (
|
|
1113
|
+
!reachable.has(change.changeId) &&
|
|
1114
|
+
isEmpty(change) &&
|
|
1115
|
+
UNDESCRIBED.has(change.description)
|
|
1116
|
+
) {
|
|
1117
|
+
hidden.add(change.changeId);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
return hidden;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1015
1123
|
/**
|
|
1016
1124
|
* Resolve a revset argument to a set of change IDs (v1.5).
|
|
1017
1125
|
*
|
package/src/core/working-copy.js
CHANGED
|
@@ -7,6 +7,44 @@
|
|
|
7
7
|
import { JJError } from '../utils/errors.js';
|
|
8
8
|
import { validateChangeId, validatePath } from '../utils/validation.js';
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Normalize an mtime value to a millisecond epoch number so two stat()
|
|
12
|
+
* results (or a stat() result and a stored value) can be compared by VALUE
|
|
13
|
+
* instead of by object identity (see issue #41).
|
|
14
|
+
*
|
|
15
|
+
* `fs.promises.stat()` returns a fresh `Date` instance on every call, so two
|
|
16
|
+
* separate stats of the same unchanged file are never `===`/`!==`-equal even
|
|
17
|
+
* when the underlying timestamp is identical. Additionally, once a tracked
|
|
18
|
+
* mtime has been through a `JSON.stringify`/`JSON.parse` round-trip (Storage
|
|
19
|
+
* persists working-copy state as JSON), a `Date` becomes an ISO string, not a
|
|
20
|
+
* `Date` — so the stored side of the comparison may be a `Date`, a plain
|
|
21
|
+
* epoch-ms number (this module now stores numbers going forward), or an ISO
|
|
22
|
+
* string (older/reloaded-from-disk state). This normalizes all three to a
|
|
23
|
+
* comparable number.
|
|
24
|
+
*
|
|
25
|
+
* Deliberately duck-types the Date case (`typeof value.getTime ===
|
|
26
|
+
* 'function'`) instead of `value instanceof Date`: Node's built-in `fs`
|
|
27
|
+
* module constructs `Stats.mtime` using its own realm's `Date`, which is not
|
|
28
|
+
* always the same `Date` constructor as this module's under test runners
|
|
29
|
+
* that sandbox each test file in its own VM context (observed under Jest
|
|
30
|
+
* with `--experimental-vm-modules`) — there, a real `Date` instance from
|
|
31
|
+
* `fs.promises.stat()` fails `instanceof Date` here even though it's a
|
|
32
|
+
* genuine Date. Duck-typing survives that realm mismatch.
|
|
33
|
+
*
|
|
34
|
+
* @param {Date|number|string|undefined} value
|
|
35
|
+
* @returns {number}
|
|
36
|
+
*/
|
|
37
|
+
function mtimeMs(value) {
|
|
38
|
+
if (value === null || value === undefined) return NaN;
|
|
39
|
+
if (typeof value === 'number') return value;
|
|
40
|
+
if (typeof value === 'string') return new Date(value).getTime();
|
|
41
|
+
// Per the type contract above, anything left is Date-like -- duck-typed
|
|
42
|
+
// (see the comment above) rather than `instanceof Date`, so no runtime
|
|
43
|
+
// check is needed here either; a value outside this contract is a caller
|
|
44
|
+
// bug, not something to silently coerce.
|
|
45
|
+
return /** @type {any} */ (value).getTime();
|
|
46
|
+
}
|
|
47
|
+
|
|
10
48
|
export class WorkingCopy {
|
|
11
49
|
/**
|
|
12
50
|
* @param {Storage} storage - Storage manager instance
|
|
@@ -178,8 +216,10 @@ export class WorkingCopy {
|
|
|
178
216
|
try {
|
|
179
217
|
const stats = await this.fs.promises.stat(fullPath);
|
|
180
218
|
|
|
181
|
-
// Fast path: check mtime and size
|
|
182
|
-
|
|
219
|
+
// Fast path: check mtime and size. Compare by VALUE (see mtimeMs
|
|
220
|
+
// above) — comparing the raw stat()/stored values with !== is
|
|
221
|
+
// always "different" even when nothing changed (issue #41).
|
|
222
|
+
if (mtimeMs(stats.mtime) !== mtimeMs(tracked.mtime) || stats.size !== tracked.size) {
|
|
183
223
|
modified.push(path);
|
|
184
224
|
}
|
|
185
225
|
} catch (error) {
|
|
@@ -290,12 +330,19 @@ export class WorkingCopy {
|
|
|
290
330
|
}
|
|
291
331
|
|
|
292
332
|
const prev = this.state.fileStates[rel];
|
|
293
|
-
|
|
333
|
+
// Store mtime as a plain epoch-ms number (not the raw stat() value):
|
|
334
|
+
// numbers round-trip through JSON exactly, so state reloaded from disk
|
|
335
|
+
// later compares identically to state still cached in-memory. Goes
|
|
336
|
+
// through mtimeMs() rather than assuming `.getTime()` exists directly —
|
|
337
|
+
// some fs implementations (e.g. the in-memory mock used in tests)
|
|
338
|
+
// return mtime as a plain epoch-ms number already, not a Date (see
|
|
339
|
+
// mtimeMs above and issue #41).
|
|
340
|
+
const next = { mtime: mtimeMs(stats.mtime), size: stats.size, mode: stats.mode };
|
|
294
341
|
|
|
295
342
|
if (!prev) {
|
|
296
343
|
this.state.fileStates[rel] = next;
|
|
297
344
|
added.push(rel);
|
|
298
|
-
} else if (
|
|
345
|
+
} else if (mtimeMs(stats.mtime) !== mtimeMs(prev.mtime) || prev.size !== stats.size) {
|
|
299
346
|
this.state.fileStates[rel] = next;
|
|
300
347
|
modified.push(rel);
|
|
301
348
|
}
|