isomorphic-jj 1.8.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 +371 -0
- package/LICENSE +1 -1
- package/README.md +117 -30
- package/dist/index.cjs +2225 -419
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2225 -418
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -7
- package/src/api/repository.js +862 -161
- package/src/backend/isomorphic-git-backend.js +13 -12
- package/src/browser/helpers.js +38 -16
- package/src/browser/index.d.ts +9 -2
- package/src/core/change-graph.js +125 -4
- package/src/core/conflict-model.js +72 -7
- package/src/core/jj-checkout.js +5 -16
- package/src/core/jj-operation-store.js +6 -17
- package/src/core/jj-tree-state.js +5 -16
- package/src/core/jj-view-store.js +6 -17
- package/src/core/merge-driver-registry.js +11 -10
- package/src/core/revset-engine.js +127 -7
- package/src/core/storage-manager.js +9 -8
- package/src/core/tag-store.js +2 -5
- package/src/core/working-copy.js +51 -4
- package/src/core/workspace-manager.js +4 -3
- package/src/protos/local_working_copy.json.js +146 -0
- package/src/protos/schema.js +49 -0
- package/src/protos/simple_op_store.json.js +374 -0
- package/src/types.d.ts +33 -0
- package/src/utils/atomic-write.js +3 -1
- package/src/utils/bytes.js +67 -0
- package/src/utils/id-generation.js +18 -4
- package/src/utils/mkdirp.js +41 -0
- package/src/utils/posix-path.js +117 -0
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { JJError } from '../utils/errors.js';
|
|
9
|
+
import { isBytes, utf8Encode, utf8Decode } from '../utils/bytes.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Check if a file path matches a glob pattern
|
|
@@ -38,11 +39,11 @@ function matchesPattern(path, pattern) {
|
|
|
38
39
|
/**
|
|
39
40
|
* Check if a file is likely binary based on content
|
|
40
41
|
*
|
|
41
|
-
* @param {string|
|
|
42
|
+
* @param {string|Uint8Array} content - File content
|
|
42
43
|
* @returns {boolean} Whether content appears binary
|
|
43
44
|
*/
|
|
44
45
|
function isBinaryContent(content) {
|
|
45
|
-
if (
|
|
46
|
+
if (isBytes(content)) return true;
|
|
46
47
|
if (typeof content !== 'string') return false;
|
|
47
48
|
|
|
48
49
|
// Check for null bytes (common in binary files)
|
|
@@ -357,9 +358,9 @@ export class MergeDriverRegistry {
|
|
|
357
358
|
/**
|
|
358
359
|
* Prepare content for driver based on binary/text
|
|
359
360
|
*
|
|
360
|
-
* @param {string|
|
|
361
|
-
* @param {boolean} isBinary - Whether to return as
|
|
362
|
-
* @returns {string|
|
|
361
|
+
* @param {string|Uint8Array|null} content - Raw content
|
|
362
|
+
* @param {boolean} isBinary - Whether to return as bytes
|
|
363
|
+
* @returns {string|Uint8Array|null} Prepared content
|
|
363
364
|
*/
|
|
364
365
|
prepareContent(content, isBinary) {
|
|
365
366
|
if (content === null || content === undefined) {
|
|
@@ -367,14 +368,14 @@ export class MergeDriverRegistry {
|
|
|
367
368
|
}
|
|
368
369
|
|
|
369
370
|
if (isBinary) {
|
|
370
|
-
// Convert to
|
|
371
|
-
if (
|
|
372
|
-
if (typeof content === 'string') return
|
|
373
|
-
return
|
|
371
|
+
// Convert to bytes if needed
|
|
372
|
+
if (isBytes(content)) return content;
|
|
373
|
+
if (typeof content === 'string') return utf8Encode(content);
|
|
374
|
+
return utf8Encode(String(content));
|
|
374
375
|
} else {
|
|
375
376
|
// Convert to string if needed
|
|
376
377
|
if (typeof content === 'string') return content;
|
|
377
|
-
if (
|
|
378
|
+
if (isBytes(content)) return utf8Decode(content);
|
|
378
379
|
return String(content);
|
|
379
380
|
}
|
|
380
381
|
}
|
|
@@ -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 [];
|
|
@@ -666,15 +672,27 @@ export class RevsetEngine {
|
|
|
666
672
|
}
|
|
667
673
|
|
|
668
674
|
case 'divergent': {
|
|
675
|
+
// See issue #32 / converge(): a divergent change has more than one
|
|
676
|
+
// visible commit sharing its changeId (added via
|
|
677
|
+
// ChangeGraph.addDivergentCopy(), which is why counting `changeId`
|
|
678
|
+
// occurrences across getAll() actually finds something now —
|
|
679
|
+
// every other change has exactly one entry here). `c.divergent ===
|
|
680
|
+
// true` also matches directly, since addDivergentCopy() sets that
|
|
681
|
+
// flag on the copy it adds; the count check catches the *primary*
|
|
682
|
+
// copy too (it's never itself flagged, since nothing about IT
|
|
683
|
+
// changed) so both/all copies of a divergent changeId are
|
|
684
|
+
// reported, not just the one(s) added after the first.
|
|
669
685
|
await this.graph.load();
|
|
670
686
|
const all = this.graph.getAll();
|
|
671
687
|
const counts = new Map();
|
|
672
688
|
for (const c of all) {
|
|
673
689
|
counts.set(c.changeId, (counts.get(c.changeId) || 0) + 1);
|
|
674
690
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
691
|
+
const matched = all.filter((c) => c.divergent === true || counts.get(c.changeId) > 1);
|
|
692
|
+
// De-duplicate: multiple copies of the same divergent changeId
|
|
693
|
+
// would otherwise each contribute their own (identical) changeId
|
|
694
|
+
// to the result.
|
|
695
|
+
return Array.from(new Set(matched.map((c) => c.changeId)));
|
|
678
696
|
}
|
|
679
697
|
|
|
680
698
|
case 'merges':
|
|
@@ -1000,6 +1018,108 @@ export class RevsetEngine {
|
|
|
1000
1018
|
return ancestors;
|
|
1001
1019
|
}
|
|
1002
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
|
+
|
|
1003
1123
|
/**
|
|
1004
1124
|
* Resolve a revset argument to a set of change IDs (v1.5).
|
|
1005
1125
|
*
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { JJError } from '../utils/errors.js';
|
|
8
8
|
import { KeyedMutex } from '../utils/mutex.js';
|
|
9
|
+
import { mkdirp } from '../utils/mkdirp.js';
|
|
9
10
|
|
|
10
11
|
export class Storage {
|
|
11
12
|
/**
|
|
@@ -40,17 +41,17 @@ export class Storage {
|
|
|
40
41
|
async init() {
|
|
41
42
|
try {
|
|
42
43
|
// Create main directories
|
|
43
|
-
await this.fs
|
|
44
|
-
await this.fs
|
|
45
|
-
await this.fs
|
|
44
|
+
await mkdirp(this.fs, this.jjDir);
|
|
45
|
+
await mkdirp(this.fs, this.repoDir);
|
|
46
|
+
await mkdirp(this.fs, this.workingCopyDir);
|
|
46
47
|
|
|
47
48
|
// Create repo subdirectories
|
|
48
|
-
await this.fs
|
|
49
|
-
await this.fs
|
|
50
|
-
await this.fs
|
|
49
|
+
await mkdirp(this.fs, `${this.repoDir}/store`);
|
|
50
|
+
await mkdirp(this.fs, `${this.repoDir}/op_log`);
|
|
51
|
+
await mkdirp(this.fs, `${this.repoDir}/conflicts`);
|
|
51
52
|
|
|
52
53
|
// Create default workspace directory
|
|
53
|
-
await this.fs
|
|
54
|
+
await mkdirp(this.fs, `${this.workingCopyDir}/default`);
|
|
54
55
|
} catch (error) {
|
|
55
56
|
throw new JJError(
|
|
56
57
|
'STORAGE_INIT_FAILED',
|
|
@@ -127,7 +128,7 @@ export class Storage {
|
|
|
127
128
|
try {
|
|
128
129
|
// Ensure parent directory exists
|
|
129
130
|
const dirPath = fullPath.substring(0, fullPath.lastIndexOf('/'));
|
|
130
|
-
await this.fs
|
|
131
|
+
await mkdirp(this.fs, dirPath);
|
|
131
132
|
|
|
132
133
|
// Write to temp file
|
|
133
134
|
await this.fs.promises.writeFile(tmpPath, jsonData, 'utf8');
|
package/src/core/tag-store.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { JJError } from '../utils/errors.js';
|
|
9
|
+
import { mkdirp } from '../utils/mkdirp.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Validates tag name format
|
|
@@ -100,11 +101,7 @@ export class TagStore {
|
|
|
100
101
|
async save(tags) {
|
|
101
102
|
// Ensure the store directory exists
|
|
102
103
|
const storeDir = `${this.jjDir}/store`;
|
|
103
|
-
|
|
104
|
-
await this.fs.promises.mkdir(storeDir, { recursive: true });
|
|
105
|
-
} catch (err) {
|
|
106
|
-
// Directory might already exist, ignore
|
|
107
|
-
}
|
|
104
|
+
await mkdirp(this.fs, storeDir);
|
|
108
105
|
await this.fs.promises.writeFile(
|
|
109
106
|
this.tagsFile,
|
|
110
107
|
JSON.stringify({ tags, tracked: this.tracking || {} }, null, 2)
|
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
|
}
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
import { JJError } from '../utils/errors.js';
|
|
11
11
|
import { validateChangeId } from '../utils/validation.js';
|
|
12
12
|
import { generateId } from '../utils/id-generation.js';
|
|
13
|
-
import path from 'path';
|
|
13
|
+
import * as path from '../utils/posix-path.js';
|
|
14
|
+
import { mkdirp } from '../utils/mkdirp.js';
|
|
14
15
|
|
|
15
16
|
export class WorkspaceManager {
|
|
16
17
|
/**
|
|
@@ -108,11 +109,11 @@ export class WorkspaceManager {
|
|
|
108
109
|
};
|
|
109
110
|
|
|
110
111
|
// Create workspace directory
|
|
111
|
-
await this.fs
|
|
112
|
+
await mkdirp(this.fs, args.path);
|
|
112
113
|
|
|
113
114
|
// Create workspace-specific working_copy directory in .jj
|
|
114
115
|
const workspaceCopyDir = path.join(this.repoDir, '.jj', 'working_copy', workspaceId);
|
|
115
|
-
await this.fs
|
|
116
|
+
await mkdirp(this.fs, workspaceCopyDir);
|
|
116
117
|
|
|
117
118
|
// Create .git file pointing to main repo's .git directory
|
|
118
119
|
// This allows Git tools to work in the workspace
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Precompiled protobuf reflection JSON for local_working_copy.proto
|
|
3
|
+
*
|
|
4
|
+
* Generated from src/protos/local_working_copy.proto by scripts/build-protos.mjs. This
|
|
5
|
+
* is bundled directly (instead of loaded from a filesystem path at runtime)
|
|
6
|
+
* so that protobuf schema loading works identically in Node and in browser
|
|
7
|
+
* bundles — protobuf.load() requires filesystem/network access that does
|
|
8
|
+
* not exist in a browser bundle (see issue #28). Regenerate with:
|
|
9
|
+
* node scripts/build-protos.mjs
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export default {
|
|
13
|
+
nested: {
|
|
14
|
+
local_working_copy: {
|
|
15
|
+
nested: {
|
|
16
|
+
FileType: {
|
|
17
|
+
valuesOptions: {
|
|
18
|
+
Conflict: {
|
|
19
|
+
deprecated: true,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
values: {
|
|
23
|
+
Normal: 0,
|
|
24
|
+
Symlink: 1,
|
|
25
|
+
Executable: 2,
|
|
26
|
+
Conflict: 3,
|
|
27
|
+
GitSubmodule: 4,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
MaterializedConflictData: {
|
|
31
|
+
fields: {
|
|
32
|
+
conflictMarkerLen: {
|
|
33
|
+
type: 'uint32',
|
|
34
|
+
id: 1,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
FileState: {
|
|
39
|
+
fields: {
|
|
40
|
+
mtimeMillisSinceEpoch: {
|
|
41
|
+
type: 'int64',
|
|
42
|
+
id: 1,
|
|
43
|
+
},
|
|
44
|
+
size: {
|
|
45
|
+
type: 'uint64',
|
|
46
|
+
id: 2,
|
|
47
|
+
},
|
|
48
|
+
fileType: {
|
|
49
|
+
type: 'FileType',
|
|
50
|
+
id: 3,
|
|
51
|
+
},
|
|
52
|
+
materializedConflictData: {
|
|
53
|
+
type: 'MaterializedConflictData',
|
|
54
|
+
id: 5,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
reserved: [[4, 4]],
|
|
58
|
+
},
|
|
59
|
+
FileStateEntry: {
|
|
60
|
+
fields: {
|
|
61
|
+
path: {
|
|
62
|
+
type: 'string',
|
|
63
|
+
id: 1,
|
|
64
|
+
},
|
|
65
|
+
state: {
|
|
66
|
+
type: 'FileState',
|
|
67
|
+
id: 2,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
SparsePatterns: {
|
|
72
|
+
fields: {
|
|
73
|
+
prefixes: {
|
|
74
|
+
rule: 'repeated',
|
|
75
|
+
type: 'string',
|
|
76
|
+
id: 1,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
TreeState: {
|
|
81
|
+
fields: {
|
|
82
|
+
legacyTreeId: {
|
|
83
|
+
type: 'bytes',
|
|
84
|
+
id: 1,
|
|
85
|
+
options: {
|
|
86
|
+
deprecated: true,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
treeIds: {
|
|
90
|
+
rule: 'repeated',
|
|
91
|
+
type: 'bytes',
|
|
92
|
+
id: 5,
|
|
93
|
+
},
|
|
94
|
+
fileStates: {
|
|
95
|
+
rule: 'repeated',
|
|
96
|
+
type: 'FileStateEntry',
|
|
97
|
+
id: 2,
|
|
98
|
+
},
|
|
99
|
+
isFileStatesSorted: {
|
|
100
|
+
type: 'bool',
|
|
101
|
+
id: 6,
|
|
102
|
+
},
|
|
103
|
+
sparsePatterns: {
|
|
104
|
+
type: 'SparsePatterns',
|
|
105
|
+
id: 3,
|
|
106
|
+
},
|
|
107
|
+
watchmanClock: {
|
|
108
|
+
type: 'WatchmanClock',
|
|
109
|
+
id: 4,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
WatchmanClock: {
|
|
114
|
+
oneofs: {
|
|
115
|
+
watchmanClock: {
|
|
116
|
+
oneof: ['stringClock', 'unixTimestamp'],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
fields: {
|
|
120
|
+
stringClock: {
|
|
121
|
+
type: 'string',
|
|
122
|
+
id: 1,
|
|
123
|
+
},
|
|
124
|
+
unixTimestamp: {
|
|
125
|
+
type: 'int64',
|
|
126
|
+
id: 2,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
Checkout: {
|
|
131
|
+
fields: {
|
|
132
|
+
operationId: {
|
|
133
|
+
type: 'bytes',
|
|
134
|
+
id: 2,
|
|
135
|
+
},
|
|
136
|
+
workspaceName: {
|
|
137
|
+
type: 'string',
|
|
138
|
+
id: 3,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
reserved: [[1, 1]],
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protobuf schema loader
|
|
3
|
+
*
|
|
4
|
+
* Builds protobuf.js `Root` instances from precompiled reflection JSON
|
|
5
|
+
* (see local_working_copy.json.js / simple_op_store.json.js, generated from
|
|
6
|
+
* the .proto files in this directory) instead of calling `protobuf.load()`
|
|
7
|
+
* against a filesystem path.
|
|
8
|
+
*
|
|
9
|
+
* Why: `protobuf.load()` resolves its argument as a file path (Node `fs`) or
|
|
10
|
+
* URL (XHR/fetch), which requires either a real filesystem or a network
|
|
11
|
+
* request. In a bundled browser build there is no filesystem path to read
|
|
12
|
+
* (see issue #28), and shipping the raw .proto text and fetching it at
|
|
13
|
+
* runtime would require bundler-specific asset handling. Reflection JSON is
|
|
14
|
+
* plain data, so it can be imported like any other JS module and works
|
|
15
|
+
* identically under Node, Jest, and any browser bundler (Vite, esbuild,
|
|
16
|
+
* webpack, Rollup, ...).
|
|
17
|
+
*
|
|
18
|
+
* Regenerate the JSON files after editing a .proto with:
|
|
19
|
+
* npm run build:protos
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import protobuf from 'protobufjs/light.js';
|
|
23
|
+
import localWorkingCopyJson from './local_working_copy.json.js';
|
|
24
|
+
import simpleOpStoreJson from './simple_op_store.json.js';
|
|
25
|
+
|
|
26
|
+
/** @type {protobuf.Root|undefined} */
|
|
27
|
+
let localWorkingCopyRoot;
|
|
28
|
+
/** @type {protobuf.Root|undefined} */
|
|
29
|
+
let simpleOpStoreRoot;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @returns {protobuf.Root} Root for local_working_copy.proto (Checkout, TreeState, ...)
|
|
33
|
+
*/
|
|
34
|
+
export function getLocalWorkingCopyRoot() {
|
|
35
|
+
if (!localWorkingCopyRoot) {
|
|
36
|
+
localWorkingCopyRoot = protobuf.Root.fromJSON(localWorkingCopyJson);
|
|
37
|
+
}
|
|
38
|
+
return localWorkingCopyRoot;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @returns {protobuf.Root} Root for simple_op_store.proto (Operation, View, ...)
|
|
43
|
+
*/
|
|
44
|
+
export function getSimpleOpStoreRoot() {
|
|
45
|
+
if (!simpleOpStoreRoot) {
|
|
46
|
+
simpleOpStoreRoot = protobuf.Root.fromJSON(simpleOpStoreJson);
|
|
47
|
+
}
|
|
48
|
+
return simpleOpStoreRoot;
|
|
49
|
+
}
|