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
|
@@ -11,7 +11,8 @@ import { JJCheckout } from '../core/jj-checkout.js';
|
|
|
11
11
|
import { JJTreeState } from '../core/jj-tree-state.js';
|
|
12
12
|
import { JJOperationStore } from '../core/jj-operation-store.js';
|
|
13
13
|
import { JJViewStore } from '../core/jj-view-store.js';
|
|
14
|
-
import
|
|
14
|
+
import { randomHex } from '../utils/id-generation.js';
|
|
15
|
+
import { mkdirp } from '../utils/mkdirp.js';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Backend adapter for isomorphic-git
|
|
@@ -674,14 +675,14 @@ export class IsomorphicGitBackend {
|
|
|
674
675
|
const jjRepo = `${jjDir}/repo`;
|
|
675
676
|
|
|
676
677
|
// Create directory structure
|
|
677
|
-
await this.fs
|
|
678
|
-
await this.fs
|
|
679
|
-
await this.fs
|
|
680
|
-
await this.fs
|
|
681
|
-
await this.fs
|
|
682
|
-
await this.fs
|
|
683
|
-
await this.fs
|
|
684
|
-
await this.fs
|
|
678
|
+
await mkdirp(this.fs, `${jjRepo}/store`);
|
|
679
|
+
await mkdirp(this.fs, `${jjRepo}/store/extra/heads`);
|
|
680
|
+
await mkdirp(this.fs, `${jjRepo}/op_store/operations`);
|
|
681
|
+
await mkdirp(this.fs, `${jjRepo}/op_store/views`);
|
|
682
|
+
await mkdirp(this.fs, `${jjRepo}/op_heads/heads`);
|
|
683
|
+
await mkdirp(this.fs, `${jjRepo}/index`);
|
|
684
|
+
await mkdirp(this.fs, `${jjRepo}/submodule_store`);
|
|
685
|
+
await mkdirp(this.fs, `${jjDir}/working_copy`);
|
|
685
686
|
|
|
686
687
|
// Write type files
|
|
687
688
|
await this.fs.promises.writeFile(`${jjRepo}/store/type`, 'git');
|
|
@@ -708,8 +709,8 @@ export class IsomorphicGitBackend {
|
|
|
708
709
|
*/
|
|
709
710
|
async _createInitialJJState() {
|
|
710
711
|
// Generate unique IDs for initial state
|
|
711
|
-
const operationId =
|
|
712
|
-
const viewId =
|
|
712
|
+
const operationId = randomHex(64); // 512-bit = 128 hex chars
|
|
713
|
+
const viewId = randomHex(64);
|
|
713
714
|
const emptyTreeId = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'; // Git's empty tree hash
|
|
714
715
|
|
|
715
716
|
// Get the initial commit from Git
|
|
@@ -721,7 +722,7 @@ export class IsomorphicGitBackend {
|
|
|
721
722
|
ref: 'HEAD',
|
|
722
723
|
});
|
|
723
724
|
if (headOid) {
|
|
724
|
-
initialCommitId =
|
|
725
|
+
initialCommitId = randomHex(64);
|
|
725
726
|
}
|
|
726
727
|
} catch (error) {
|
|
727
728
|
// No initial commit yet
|
package/src/browser/helpers.js
CHANGED
|
@@ -5,15 +5,21 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Create a browser-compatible filesystem
|
|
8
|
+
* Create a browser-compatible filesystem, defaulting to LightningFS.
|
|
9
9
|
*
|
|
10
|
-
* Requires: @isomorphic-git/lightning-fs
|
|
10
|
+
* Requires (unless `opts.fs` is supplied): @isomorphic-git/lightning-fs
|
|
11
11
|
*
|
|
12
|
-
* @param {Object} opts - Options
|
|
12
|
+
* @param {Object} [opts] - Options
|
|
13
13
|
* @param {string} [opts.backend='idb'] - Storage backend ('idb' or 'memory')
|
|
14
14
|
* @param {string} [opts.name='jj'] - Database name for IndexedDB
|
|
15
15
|
* @param {boolean} [opts.wipe=false] - Wipe existing data
|
|
16
|
-
* @
|
|
16
|
+
* @param {any} [opts.fs] - Bring your own filesystem instead of the default
|
|
17
|
+
* LightningFS auto-import. Pass either an already-constructed
|
|
18
|
+
* isomorphic-git-compatible fs (e.g. a `memfs` volume, or your own
|
|
19
|
+
* LightningFS instance) — returned as-is — or a constructor with the
|
|
20
|
+
* LightningFS signature (`new Ctor(name, { wipe })`), which is
|
|
21
|
+
* instantiated the same way the default LightningFS would be.
|
|
22
|
+
* @returns {Promise<Object>} Filesystem instance compatible with isomorphic-git
|
|
17
23
|
*
|
|
18
24
|
* @example
|
|
19
25
|
* ```javascript
|
|
@@ -22,7 +28,7 @@
|
|
|
22
28
|
* import git from 'isomorphic-git';
|
|
23
29
|
* import http from 'isomorphic-git/http/web';
|
|
24
30
|
*
|
|
25
|
-
* const fs = createBrowserFS({ backend: 'idb', name: 'my-repo' });
|
|
31
|
+
* const fs = await createBrowserFS({ backend: 'idb', name: 'my-repo' });
|
|
26
32
|
* const jj = await createJJ({
|
|
27
33
|
* fs,
|
|
28
34
|
* dir: '/repo',
|
|
@@ -30,24 +36,36 @@
|
|
|
30
36
|
* http
|
|
31
37
|
* });
|
|
32
38
|
* ```
|
|
39
|
+
*
|
|
40
|
+
* @example Bring your own fs (e.g. memfs)
|
|
41
|
+
* ```javascript
|
|
42
|
+
* import { fs as memfs } from 'memfs';
|
|
43
|
+
* const fs = await createBrowserFS({ fs: memfs });
|
|
44
|
+
* ```
|
|
33
45
|
*/
|
|
34
|
-
export function createBrowserFS(opts = {}) {
|
|
46
|
+
export async function createBrowserFS(opts = {}) {
|
|
35
47
|
// Check if we're in a browser environment
|
|
36
48
|
if (typeof window === 'undefined') {
|
|
37
49
|
throw new Error('createBrowserFS() should only be used in browser environments');
|
|
38
50
|
}
|
|
39
51
|
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
});
|
|
52
|
+
// Caller supplied their own filesystem — use it directly instead of
|
|
53
|
+
// pulling in LightningFS at all.
|
|
54
|
+
if (opts.fs) {
|
|
55
|
+
if (typeof opts.fs === 'function') {
|
|
56
|
+
return new opts.fs(opts.name || 'jj', { wipe: opts.wipe || false });
|
|
57
|
+
}
|
|
58
|
+
return opts.fs;
|
|
59
|
+
}
|
|
49
60
|
|
|
50
|
-
|
|
61
|
+
// Dynamic `import()` (not `require`) so this resolves in an ESM browser
|
|
62
|
+
// bundle — `require` does not exist there (see issue #28). Users must
|
|
63
|
+
// install @isomorphic-git/lightning-fs separately; it's an optional
|
|
64
|
+
// peer/dev dependency of this package.
|
|
65
|
+
let LightningFS;
|
|
66
|
+
try {
|
|
67
|
+
const mod = await import('@isomorphic-git/lightning-fs');
|
|
68
|
+
LightningFS = mod.default ?? mod;
|
|
51
69
|
} catch (error) {
|
|
52
70
|
throw new Error(
|
|
53
71
|
'LightningFS not found. Install it with: npm install @isomorphic-git/lightning-fs\n' +
|
|
@@ -55,6 +73,10 @@ export function createBrowserFS(opts = {}) {
|
|
|
55
73
|
error.message
|
|
56
74
|
);
|
|
57
75
|
}
|
|
76
|
+
|
|
77
|
+
return new LightningFS(opts.name || 'jj', {
|
|
78
|
+
wipe: opts.wipe || false,
|
|
79
|
+
});
|
|
58
80
|
}
|
|
59
81
|
|
|
60
82
|
/**
|
package/src/browser/index.d.ts
CHANGED
|
@@ -14,6 +14,13 @@ export interface BrowserFSOptions {
|
|
|
14
14
|
name?: string;
|
|
15
15
|
/** Wipe existing data on initialization */
|
|
16
16
|
wipe?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Bring your own filesystem instead of the default LightningFS
|
|
19
|
+
* auto-import. Pass either an already-constructed isomorphic-git
|
|
20
|
+
* compatible fs (returned as-is) or a LightningFS-shaped constructor
|
|
21
|
+
* (`new Ctor(name, { wipe })`).
|
|
22
|
+
*/
|
|
23
|
+
fs?: any;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
/**
|
|
@@ -99,11 +106,11 @@ export interface ServiceWorkerUtils {
|
|
|
99
106
|
* import git from 'isomorphic-git';
|
|
100
107
|
* import http from 'isomorphic-git/http/web';
|
|
101
108
|
*
|
|
102
|
-
* const fs = createBrowserFS({ backend: 'idb', name: 'my-repo' });
|
|
109
|
+
* const fs = await createBrowserFS({ backend: 'idb', name: 'my-repo' });
|
|
103
110
|
* const jj = await createJJ({ fs, dir: '/repo', git, http });
|
|
104
111
|
* ```
|
|
105
112
|
*/
|
|
106
|
-
export function createBrowserFS(opts?: BrowserFSOptions): any
|
|
113
|
+
export function createBrowserFS(opts?: BrowserFSOptions): Promise<any>;
|
|
107
114
|
|
|
108
115
|
/**
|
|
109
116
|
* Get storage quota information (browser only)
|
package/src/core/change-graph.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { JJError } from '../utils/errors.js';
|
|
8
8
|
import { validateChangeId } from '../utils/validation.js';
|
|
9
|
+
import { randomHex } from '../utils/id-generation.js';
|
|
9
10
|
|
|
10
11
|
export class ChangeGraph {
|
|
11
12
|
/**
|
|
@@ -95,6 +96,68 @@ export class ChangeGraph {
|
|
|
95
96
|
await this.save();
|
|
96
97
|
}
|
|
97
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Add a DIVERGENT copy of an existing change: another commit that also
|
|
101
|
+
* claims `change.changeId` (see issue #32 / the `divergent()` revset —
|
|
102
|
+
* "multiple visible commits sharing one change id"). `addChange()`
|
|
103
|
+
* can't represent this: `nodes` is a `Map<changeId, change>`, one entry
|
|
104
|
+
* per key, by design — every other API (getChange, getParents,
|
|
105
|
+
* getChildren, ...) keys on plain changeId and would be ambiguous if
|
|
106
|
+
* that stopped being unique.
|
|
107
|
+
*
|
|
108
|
+
* Divergent copies are therefore stored under a synthetic internal key
|
|
109
|
+
* (`${changeId}\0${commitId}`) so the `nodes` Map stays a true map, but
|
|
110
|
+
* each copy's *own* `changeId` field is unchanged — so `getAll()` (and
|
|
111
|
+
* therefore the existing `divergent()` revset filter, which counts
|
|
112
|
+
* occurrences of `c.changeId` across `getAll()`) sees both/all of them
|
|
113
|
+
* and correctly reports the change as divergent. `getChange(changeId)`
|
|
114
|
+
* deliberately keeps resolving to just the primary copy (the one
|
|
115
|
+
* actually stored under the plain `changeId` key) — for anything that
|
|
116
|
+
* needs every copy, see getDivergentSiblings().
|
|
117
|
+
*
|
|
118
|
+
* @param {Record<string, any>} change - The divergent copy (must have
|
|
119
|
+
* the same `changeId` as an existing change, and its own `commitId`)
|
|
120
|
+
*/
|
|
121
|
+
async addDivergentCopy(change) {
|
|
122
|
+
validateChangeId(change.changeId);
|
|
123
|
+
|
|
124
|
+
if (!this.nodes.has(change.changeId)) {
|
|
125
|
+
throw new JJError(
|
|
126
|
+
'CHANGE_NOT_FOUND',
|
|
127
|
+
`Cannot add a divergent copy of ${change.changeId}: no existing change with that id`,
|
|
128
|
+
{ changeId: change.changeId, suggestion: 'Use addChange() to create the first copy' }
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const key = `${change.changeId}\0${change.commitId}`;
|
|
133
|
+
if (this.nodes.has(key)) {
|
|
134
|
+
throw new JJError(
|
|
135
|
+
'CHANGE_EXISTS',
|
|
136
|
+
`Change ${change.changeId} already has a divergent copy with commit ${change.commitId}`,
|
|
137
|
+
{ changeId: change.changeId, commitId: change.commitId }
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
change.divergent = true;
|
|
142
|
+
this.nodes.set(key, change);
|
|
143
|
+
this.commitIndex.set(change.commitId, change.changeId);
|
|
144
|
+
|
|
145
|
+
await this.save();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get every visible copy of a (possibly divergent) change id — just the
|
|
150
|
+
* one copy in the common case, or the primary plus every divergent copy
|
|
151
|
+
* added via addDivergentCopy().
|
|
152
|
+
*
|
|
153
|
+
* @param {string} changeId - Change ID
|
|
154
|
+
* @returns {Array<any>} All copies sharing this changeId, primary first
|
|
155
|
+
*/
|
|
156
|
+
getDivergentSiblings(changeId) {
|
|
157
|
+
validateChangeId(changeId);
|
|
158
|
+
return this.getAll().filter((change) => change.changeId === changeId);
|
|
159
|
+
}
|
|
160
|
+
|
|
98
161
|
/**
|
|
99
162
|
* Get a change by ID
|
|
100
163
|
*
|
|
@@ -206,6 +269,65 @@ export class ChangeGraph {
|
|
|
206
269
|
await this.save();
|
|
207
270
|
}
|
|
208
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Remove one divergent copy added via addDivergentCopy() — e.g. after
|
|
274
|
+
* converge() (issue #32) resolves a divergence and only needs to drop
|
|
275
|
+
* the now-superseded copy. The *primary* copy (stored under the plain
|
|
276
|
+
* `changeId` key) is never touched by this — use deleteChange() (or
|
|
277
|
+
* updateChange() to overwrite it with a resolved result) for that.
|
|
278
|
+
*
|
|
279
|
+
* @param {string} changeId
|
|
280
|
+
* @param {string} commitId - The divergent copy's own commit id
|
|
281
|
+
* @returns {Promise<boolean>} Whether a copy was actually removed
|
|
282
|
+
*/
|
|
283
|
+
async deleteDivergentCopy(changeId, commitId) {
|
|
284
|
+
validateChangeId(changeId);
|
|
285
|
+
|
|
286
|
+
const key = `${changeId}\0${commitId}`;
|
|
287
|
+
const change = this.nodes.get(key);
|
|
288
|
+
if (!change) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
this.nodes.delete(key);
|
|
293
|
+
if (this.commitIndex.get(commitId) === changeId) {
|
|
294
|
+
this.commitIndex.delete(commitId);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
await this.save();
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Remove a change from the graph outright.
|
|
303
|
+
*
|
|
304
|
+
* Used to reverse an addChange() — e.g. undoing an operation that created
|
|
305
|
+
* a change (new(), or squash()'s synthetic empty working-copy change) —
|
|
306
|
+
* where restoring a "before" snapshot doesn't apply because there was no
|
|
307
|
+
* "before": the change simply didn't exist yet. A soft-delete (setting
|
|
308
|
+
* `abandoned: true` via updateChange()) is NOT the same thing and is used
|
|
309
|
+
* separately for abandon() itself.
|
|
310
|
+
*
|
|
311
|
+
* @param {string} changeId - Change ID to remove
|
|
312
|
+
* @returns {Promise<boolean>} Whether a change was actually removed
|
|
313
|
+
*/
|
|
314
|
+
async deleteChange(changeId) {
|
|
315
|
+
validateChangeId(changeId);
|
|
316
|
+
|
|
317
|
+
const change = this.nodes.get(changeId);
|
|
318
|
+
if (!change) {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
this.nodes.delete(changeId);
|
|
323
|
+
if (this.commitIndex.get(change.commitId) === changeId) {
|
|
324
|
+
this.commitIndex.delete(change.commitId);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
await this.save();
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
|
|
209
331
|
/**
|
|
210
332
|
* Get all ancestors of a change (recursive parent traversal)
|
|
211
333
|
*
|
|
@@ -243,10 +365,9 @@ export class ChangeGraph {
|
|
|
243
365
|
* @returns {Promise<Record<string, any>>} Created change object
|
|
244
366
|
*/
|
|
245
367
|
async createChange(params = {}) {
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
const tree = params.tree || crypto.randomBytes(20).toString('hex');
|
|
368
|
+
const changeId = params.changeId || randomHex(16);
|
|
369
|
+
const commitId = params.commitId || randomHex(20);
|
|
370
|
+
const tree = params.tree || randomHex(20);
|
|
250
371
|
const timestamp = params.timestamp || new Date().toISOString();
|
|
251
372
|
|
|
252
373
|
/** @type {Record<string, any>} */
|
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
* This matches JJ's philosophy: conflicts never block you.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import path from 'path';
|
|
13
|
+
import * as path from '../utils/posix-path.js';
|
|
14
14
|
import { JJError } from '../utils/errors.js';
|
|
15
|
+
import { isBytes } from '../utils/bytes.js';
|
|
15
16
|
import { generateId } from '../utils/id-generation.js';
|
|
17
|
+
import { mkdirp } from '../utils/mkdirp.js';
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* Conflict types supported by JJ
|
|
@@ -174,6 +176,15 @@ export class ConflictModel {
|
|
|
174
176
|
const conflict = this._detectPathConflict(path, baseContent, leftContent, rightContent);
|
|
175
177
|
if (conflict) {
|
|
176
178
|
conflicts.push(conflict);
|
|
179
|
+
} else if (workingCopyDir) {
|
|
180
|
+
// No conflict, but "no conflict" doesn't mean "nothing to do": of
|
|
181
|
+
// _detectPathConflict's null-returning cases, every one *except*
|
|
182
|
+
// "right side changed, left didn't" already matches what's on disk,
|
|
183
|
+
// because `leftContent` **is** what's on disk (read straight from
|
|
184
|
+
// the working copy above) -- so only that one case needs a write.
|
|
185
|
+
if (baseContent === leftContent && baseContent !== rightContent) {
|
|
186
|
+
await this._applyCleanResolution(workingCopyDir, path, rightContent);
|
|
187
|
+
}
|
|
177
188
|
}
|
|
178
189
|
}
|
|
179
190
|
|
|
@@ -261,6 +272,30 @@ export class ConflictModel {
|
|
|
261
272
|
return null;
|
|
262
273
|
}
|
|
263
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Create a content conflict with more than two disagreeing versions —
|
|
277
|
+
* every other conflict in this module has exactly two sides (`left`/
|
|
278
|
+
* `right`, or `left`/`right` on an add-add), because every OTHER caller
|
|
279
|
+
* (merge, rebase) only ever three-way-merges two changes at a time.
|
|
280
|
+
* converge() (issue #32) can face more than two divergent copies of one
|
|
281
|
+
* change in a single call, so `sides` here is `{ base, versions }`
|
|
282
|
+
* instead: `versions` is every distinct value found across all copies
|
|
283
|
+
* that differs from `base`, each paired with (one of) the commitId(s)
|
|
284
|
+
* that has it.
|
|
285
|
+
*
|
|
286
|
+
* @param {string} path
|
|
287
|
+
* @param {any} base
|
|
288
|
+
* @param {Array<{commitId: string, content: any}>} versions
|
|
289
|
+
*/
|
|
290
|
+
createNWayConflict(path, base, versions) {
|
|
291
|
+
return this._createConflict({
|
|
292
|
+
type: ConflictType.CONTENT,
|
|
293
|
+
path,
|
|
294
|
+
sides: { base, versions },
|
|
295
|
+
message: `${versions.length} divergent copies disagree on this file's content`,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
264
299
|
/**
|
|
265
300
|
* Create a conflict object
|
|
266
301
|
*
|
|
@@ -504,6 +539,36 @@ export class ConflictModel {
|
|
|
504
539
|
return result;
|
|
505
540
|
}
|
|
506
541
|
|
|
542
|
+
/**
|
|
543
|
+
* Apply a clean (non-conflicting) three-way resolution to the working
|
|
544
|
+
* copy, for the default (no merge driver) path in detectConflicts(): the
|
|
545
|
+
* "right side wins" case, where the resolved content differs from what's
|
|
546
|
+
* currently on disk (see the call site for why this is the only clean
|
|
547
|
+
* case that needs one).
|
|
548
|
+
*
|
|
549
|
+
* @param {string} workingCopyDir - Working copy directory
|
|
550
|
+
* @param {string} filePath - File path
|
|
551
|
+
* @param {any} content - The winning (right) side's content, or
|
|
552
|
+
* `undefined` to mean the file was deleted on that side
|
|
553
|
+
*/
|
|
554
|
+
async _applyCleanResolution(workingCopyDir, filePath, content) {
|
|
555
|
+
const fullPath = path.join(workingCopyDir, filePath);
|
|
556
|
+
|
|
557
|
+
if (content === undefined) {
|
|
558
|
+
await this.fs.promises.unlink(fullPath).catch(() => {});
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const dir = path.dirname(fullPath);
|
|
563
|
+
await mkdirp(this.fs, dir);
|
|
564
|
+
|
|
565
|
+
if (isBytes(content)) {
|
|
566
|
+
await this.fs.promises.writeFile(fullPath, content);
|
|
567
|
+
} else {
|
|
568
|
+
await this.fs.promises.writeFile(fullPath, content, 'utf8');
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
507
572
|
/**
|
|
508
573
|
* Write merge driver result to working copy (v0.5)
|
|
509
574
|
*
|
|
@@ -516,14 +581,14 @@ export class ConflictModel {
|
|
|
516
581
|
|
|
517
582
|
// Ensure directory exists
|
|
518
583
|
const dir = path.dirname(fullPath);
|
|
519
|
-
await this.fs
|
|
584
|
+
await mkdirp(this.fs, dir);
|
|
520
585
|
|
|
521
586
|
// Write main file
|
|
522
587
|
if (result.content !== null && result.content !== undefined) {
|
|
523
|
-
if (
|
|
588
|
+
if (isBytes(result.content)) {
|
|
524
589
|
await this.fs.promises.writeFile(fullPath, result.content);
|
|
525
590
|
} else {
|
|
526
|
-
await this.fs.promises.writeFile(fullPath, result.content, '
|
|
591
|
+
await this.fs.promises.writeFile(fullPath, result.content, 'utf8');
|
|
527
592
|
}
|
|
528
593
|
}
|
|
529
594
|
|
|
@@ -532,12 +597,12 @@ export class ConflictModel {
|
|
|
532
597
|
for (const [additionalPath, additionalContent] of Object.entries(result.additionalFiles)) {
|
|
533
598
|
const additionalFullPath = path.join(workingCopyDir, additionalPath);
|
|
534
599
|
const additionalDir = path.dirname(additionalFullPath);
|
|
535
|
-
await this.fs
|
|
600
|
+
await mkdirp(this.fs, additionalDir);
|
|
536
601
|
|
|
537
|
-
if (
|
|
602
|
+
if (isBytes(additionalContent)) {
|
|
538
603
|
await this.fs.promises.writeFile(additionalFullPath, additionalContent);
|
|
539
604
|
} else {
|
|
540
|
-
await this.fs.promises.writeFile(additionalFullPath, additionalContent, '
|
|
605
|
+
await this.fs.promises.writeFile(additionalFullPath, additionalContent, 'utf8');
|
|
541
606
|
}
|
|
542
607
|
}
|
|
543
608
|
}
|
package/src/core/jj-checkout.js
CHANGED
|
@@ -5,18 +5,8 @@
|
|
|
5
5
|
* This file tracks which operation the working copy is at.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import { fileURLToPath } from 'url';
|
|
11
|
-
|
|
12
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
-
const __dirname = path.dirname(__filename);
|
|
14
|
-
|
|
15
|
-
// Determine proto path based on whether we're running from dist or src
|
|
16
|
-
const inDist = __dirname.includes('/dist') || __dirname.includes('\\dist');
|
|
17
|
-
const protoDir = inDist
|
|
18
|
-
? path.join(__dirname, '..', 'src', 'protos')
|
|
19
|
-
: path.join(__dirname, '..', 'protos');
|
|
8
|
+
import { getLocalWorkingCopyRoot } from '../protos/schema.js';
|
|
9
|
+
import { hexToBytes } from '../utils/bytes.js';
|
|
20
10
|
|
|
21
11
|
export class JJCheckout {
|
|
22
12
|
/**
|
|
@@ -26,7 +16,6 @@ export class JJCheckout {
|
|
|
26
16
|
constructor(fs, dir) {
|
|
27
17
|
this.fs = fs;
|
|
28
18
|
this.dir = dir;
|
|
29
|
-
this.protoPath = path.join(protoDir, 'local_working_copy.proto');
|
|
30
19
|
}
|
|
31
20
|
|
|
32
21
|
/**
|
|
@@ -37,11 +26,11 @@ export class JJCheckout {
|
|
|
37
26
|
*/
|
|
38
27
|
async writeCheckout(operationId, workspaceName = 'default') {
|
|
39
28
|
// Load protobuf schema
|
|
40
|
-
const root =
|
|
29
|
+
const root = getLocalWorkingCopyRoot();
|
|
41
30
|
const Checkout = root.lookupType('local_working_copy.Checkout');
|
|
42
31
|
|
|
43
32
|
// Convert hex operation ID to bytes
|
|
44
|
-
const opIdBuffer =
|
|
33
|
+
const opIdBuffer = hexToBytes(operationId);
|
|
45
34
|
|
|
46
35
|
// Create message (use camelCase for protobufjs)
|
|
47
36
|
const message = Checkout.create({
|
|
@@ -73,7 +62,7 @@ export class JJCheckout {
|
|
|
73
62
|
const buffer = await this.fs.promises.readFile(checkoutPath);
|
|
74
63
|
|
|
75
64
|
// Load protobuf schema
|
|
76
|
-
const root =
|
|
65
|
+
const root = getLocalWorkingCopyRoot();
|
|
77
66
|
const Checkout = root.lookupType('local_working_copy.Checkout');
|
|
78
67
|
|
|
79
68
|
// Decode
|
|
@@ -5,19 +5,9 @@
|
|
|
5
5
|
* Operations track the history of changes to the repository.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import protobuf from 'protobufjs';
|
|
9
|
-
import path from 'path';
|
|
10
|
-
import { fileURLToPath } from 'url';
|
|
11
8
|
import { atomicWriteFile } from '../utils/atomic-write.js';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const __dirname = path.dirname(__filename);
|
|
15
|
-
|
|
16
|
-
// Determine proto path based on whether we're running from dist or src
|
|
17
|
-
const inDist = __dirname.includes('/dist') || __dirname.includes('\\dist');
|
|
18
|
-
const protoDir = inDist
|
|
19
|
-
? path.join(__dirname, '..', 'src', 'protos')
|
|
20
|
-
: path.join(__dirname, '..', 'protos');
|
|
9
|
+
import { getSimpleOpStoreRoot } from '../protos/schema.js';
|
|
10
|
+
import { hexToBytes } from '../utils/bytes.js';
|
|
21
11
|
|
|
22
12
|
export class JJOperationStore {
|
|
23
13
|
/**
|
|
@@ -27,7 +17,6 @@ export class JJOperationStore {
|
|
|
27
17
|
constructor(fs, dir) {
|
|
28
18
|
this.fs = fs;
|
|
29
19
|
this.dir = dir;
|
|
30
|
-
this.protoPath = path.join(protoDir, 'simple_op_store.proto');
|
|
31
20
|
}
|
|
32
21
|
|
|
33
22
|
/**
|
|
@@ -40,12 +29,12 @@ export class JJOperationStore {
|
|
|
40
29
|
*/
|
|
41
30
|
async writeOperation(operationId, viewId, parentIds, metadata) {
|
|
42
31
|
// Load protobuf schema
|
|
43
|
-
const root =
|
|
32
|
+
const root = getSimpleOpStoreRoot();
|
|
44
33
|
const Operation = root.lookupType('simple_op_store.Operation');
|
|
45
34
|
|
|
46
35
|
// Convert hex IDs to bytes
|
|
47
|
-
const viewIdBuffer =
|
|
48
|
-
const parentBuffers = parentIds.map((id) =>
|
|
36
|
+
const viewIdBuffer = hexToBytes(viewId);
|
|
37
|
+
const parentBuffers = parentIds.map((id) => hexToBytes(id));
|
|
49
38
|
|
|
50
39
|
// Create metadata message (use camelCase for protobufjs)
|
|
51
40
|
const metadataMsg = {
|
|
@@ -101,7 +90,7 @@ export class JJOperationStore {
|
|
|
101
90
|
const buffer = await this.fs.promises.readFile(opPath);
|
|
102
91
|
|
|
103
92
|
// Load protobuf schema
|
|
104
|
-
const root =
|
|
93
|
+
const root = getSimpleOpStoreRoot();
|
|
105
94
|
const Operation = root.lookupType('simple_op_store.Operation');
|
|
106
95
|
|
|
107
96
|
// Decode
|
|
@@ -5,19 +5,9 @@
|
|
|
5
5
|
* This file tracks the working copy tree and file states.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import protobuf from 'protobufjs';
|
|
9
|
-
import path from 'path';
|
|
10
|
-
import { fileURLToPath } from 'url';
|
|
11
8
|
import { atomicWriteFile } from '../utils/atomic-write.js';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const __dirname = path.dirname(__filename);
|
|
15
|
-
|
|
16
|
-
// Determine proto path based on whether we're running from dist or src
|
|
17
|
-
const inDist = __dirname.includes('/dist') || __dirname.includes('\\dist');
|
|
18
|
-
const protoDir = inDist
|
|
19
|
-
? path.join(__dirname, '..', 'src', 'protos')
|
|
20
|
-
: path.join(__dirname, '..', 'protos');
|
|
9
|
+
import { getLocalWorkingCopyRoot } from '../protos/schema.js';
|
|
10
|
+
import { hexToBytes } from '../utils/bytes.js';
|
|
21
11
|
|
|
22
12
|
export class JJTreeState {
|
|
23
13
|
/**
|
|
@@ -27,7 +17,6 @@ export class JJTreeState {
|
|
|
27
17
|
constructor(fs, dir) {
|
|
28
18
|
this.fs = fs;
|
|
29
19
|
this.dir = dir;
|
|
30
|
-
this.protoPath = path.join(protoDir, 'local_working_copy.proto');
|
|
31
20
|
}
|
|
32
21
|
|
|
33
22
|
/**
|
|
@@ -38,11 +27,11 @@ export class JJTreeState {
|
|
|
38
27
|
*/
|
|
39
28
|
async writeTreeState(treeId, fileStates = []) {
|
|
40
29
|
// Load protobuf schema
|
|
41
|
-
const root =
|
|
30
|
+
const root = getLocalWorkingCopyRoot();
|
|
42
31
|
const TreeState = root.lookupType('local_working_copy.TreeState');
|
|
43
32
|
|
|
44
33
|
// Convert hex tree ID to bytes
|
|
45
|
-
const treeIdBuffer =
|
|
34
|
+
const treeIdBuffer = hexToBytes(treeId);
|
|
46
35
|
|
|
47
36
|
// Convert file states to protobuf format
|
|
48
37
|
const fileStateEntries = fileStates.map((fs) => ({
|
|
@@ -88,7 +77,7 @@ export class JJTreeState {
|
|
|
88
77
|
const buffer = await this.fs.promises.readFile(treeStatePath);
|
|
89
78
|
|
|
90
79
|
// Load protobuf schema
|
|
91
|
-
const root =
|
|
80
|
+
const root = getLocalWorkingCopyRoot();
|
|
92
81
|
const TreeState = root.lookupType('local_working_copy.TreeState');
|
|
93
82
|
|
|
94
83
|
// Decode
|
|
@@ -5,19 +5,9 @@
|
|
|
5
5
|
* Views represent the state of the repository at a given operation.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import protobuf from 'protobufjs';
|
|
9
|
-
import path from 'path';
|
|
10
|
-
import { fileURLToPath } from 'url';
|
|
11
8
|
import { atomicWriteFile } from '../utils/atomic-write.js';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const __dirname = path.dirname(__filename);
|
|
15
|
-
|
|
16
|
-
// Determine proto path based on whether we're running from dist or src
|
|
17
|
-
const inDist = __dirname.includes('/dist') || __dirname.includes('\\dist');
|
|
18
|
-
const protoDir = inDist
|
|
19
|
-
? path.join(__dirname, '..', 'src', 'protos')
|
|
20
|
-
: path.join(__dirname, '..', 'protos');
|
|
9
|
+
import { getSimpleOpStoreRoot } from '../protos/schema.js';
|
|
10
|
+
import { hexToBytes } from '../utils/bytes.js';
|
|
21
11
|
|
|
22
12
|
export class JJViewStore {
|
|
23
13
|
/**
|
|
@@ -27,7 +17,6 @@ export class JJViewStore {
|
|
|
27
17
|
constructor(fs, dir) {
|
|
28
18
|
this.fs = fs;
|
|
29
19
|
this.dir = dir;
|
|
30
|
-
this.protoPath = path.join(protoDir, 'simple_op_store.proto');
|
|
31
20
|
}
|
|
32
21
|
|
|
33
22
|
/**
|
|
@@ -39,17 +28,17 @@ export class JJViewStore {
|
|
|
39
28
|
*/
|
|
40
29
|
async writeView(viewId, headIds, wcCommitIds = {}) {
|
|
41
30
|
// Load protobuf schema
|
|
42
|
-
const root =
|
|
31
|
+
const root = getSimpleOpStoreRoot();
|
|
43
32
|
const View = root.lookupType('simple_op_store.View');
|
|
44
33
|
|
|
45
34
|
// Convert hex IDs to bytes
|
|
46
|
-
const headBuffers = headIds.map((id) =>
|
|
35
|
+
const headBuffers = headIds.map((id) => hexToBytes(id));
|
|
47
36
|
|
|
48
37
|
// Convert wcCommitIds object to map
|
|
49
38
|
/** @type {Record<string, any>} */
|
|
50
39
|
const wcCommitIdsMap = {};
|
|
51
40
|
for (const [workspace, commitId] of Object.entries(wcCommitIds)) {
|
|
52
|
-
wcCommitIdsMap[workspace] =
|
|
41
|
+
wcCommitIdsMap[workspace] = hexToBytes(commitId);
|
|
53
42
|
}
|
|
54
43
|
|
|
55
44
|
// Create view message (use camelCase for protobufjs)
|
|
@@ -90,7 +79,7 @@ export class JJViewStore {
|
|
|
90
79
|
const buffer = await this.fs.promises.readFile(viewPath);
|
|
91
80
|
|
|
92
81
|
// Load protobuf schema
|
|
93
|
-
const root =
|
|
82
|
+
const root = getSimpleOpStoreRoot();
|
|
94
83
|
const View = root.lookupType('simple_op_store.View');
|
|
95
84
|
|
|
96
85
|
// Decode
|