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.
@@ -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|Buffer} content - File content
42
+ * @param {string|Uint8Array} content - File content
42
43
  * @returns {boolean} Whether content appears binary
43
44
  */
44
45
  function isBinaryContent(content) {
45
- if (Buffer.isBuffer(content)) return true;
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|Buffer|null} content - Raw content
361
- * @param {boolean} isBinary - Whether to return as Buffer
362
- * @returns {string|Buffer|null} Prepared content
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 Buffer if needed
371
- if (Buffer.isBuffer(content)) return content;
372
- if (typeof content === 'string') return Buffer.from(content, 'utf-8');
373
- return Buffer.from(String(content));
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 (Buffer.isBuffer(content)) return content.toString('utf-8');
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
- return this.graph.getAll().map((c) => c.changeId);
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
- return this.graph.getAll().map((c) => c.changeId);
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
- return all
676
- .filter((c) => c.divergent === true || counts.get(c.changeId) > 1)
677
- .map((c) => c.changeId);
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.promises.mkdir(this.jjDir, { recursive: true });
44
- await this.fs.promises.mkdir(this.repoDir, { recursive: true });
45
- await this.fs.promises.mkdir(this.workingCopyDir, { recursive: true });
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.promises.mkdir(`${this.repoDir}/store`, { recursive: true });
49
- await this.fs.promises.mkdir(`${this.repoDir}/op_log`, { recursive: true });
50
- await this.fs.promises.mkdir(`${this.repoDir}/conflicts`, { recursive: true });
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.promises.mkdir(`${this.workingCopyDir}/default`, { recursive: true });
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.promises.mkdir(dirPath, { recursive: true });
131
+ await mkdirp(this.fs, dirPath);
131
132
 
132
133
  // Write to temp file
133
134
  await this.fs.promises.writeFile(tmpPath, jsonData, 'utf8');
@@ -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
- try {
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)
@@ -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
- if (stats.mtime !== tracked.mtime || stats.size !== tracked.size) {
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
- const next = { mtime: stats.mtime, size: stats.size, mode: stats.mode };
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 (prev.mtime !== stats.mtime || prev.size !== stats.size) {
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.promises.mkdir(args.path, { recursive: true });
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.promises.mkdir(workspaceCopyDir, { recursive: true });
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
+ }