isomorphic-jj 1.4.3 → 1.7.0
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 +226 -0
- package/README.md +9 -11
- package/bin/isojj.js +270 -102
- package/dist/index.cjs +2910 -1666
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2910 -1666
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -4
- package/src/api/repository.js +1099 -739
- package/src/backend/isomorphic-git-backend.js +183 -136
- package/src/backend/lazy-git-backend.js +12 -19
- package/src/browser/helpers.js +6 -2
- package/src/core/background-ops.js +58 -41
- package/src/core/bookmark-store.js +12 -8
- package/src/core/change-graph.js +26 -29
- package/src/core/conflict-model.js +66 -26
- package/src/core/jj-checkout.js +8 -4
- package/src/core/jj-operation-store.js +16 -12
- package/src/core/jj-tree-state.js +13 -9
- package/src/core/jj-view-store.js +11 -5
- package/src/core/merge-driver-registry.js +92 -46
- package/src/core/operation-log.js +30 -33
- package/src/core/revset-engine.js +1069 -485
- package/src/core/storage-manager.js +25 -19
- package/src/core/tag-store.js +2 -2
- package/src/core/user-config.js +3 -1
- package/src/core/working-copy.js +125 -9
- package/src/core/workspace-manager.js +2 -2
- package/src/drivers/built-in-drivers.js +66 -33
- package/src/types.d.ts +67 -1
- package/src/utils/errors.js +3 -3
- package/src/utils/id-generation.js +4 -4
- package/src/utils/validation.js +6 -10
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Storage Manager for isomorphic-jj
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Manages JSON storage in .jj directory with atomic writes and caching.
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -8,15 +8,15 @@ import { JJError } from '../utils/errors.js';
|
|
|
8
8
|
|
|
9
9
|
export class Storage {
|
|
10
10
|
/**
|
|
11
|
-
* @param {
|
|
11
|
+
* @param {any} fs - Filesystem implementation (Node fs, LightningFS, etc.)
|
|
12
12
|
* @param {string} dir - Repository directory path
|
|
13
13
|
*/
|
|
14
14
|
constructor(fs, dir) {
|
|
15
15
|
this.fs = fs;
|
|
16
16
|
this.dir = dir;
|
|
17
17
|
this.jjDir = `${dir}/.jj`;
|
|
18
|
-
this.repoDir = `${dir}/.jj/repo`;
|
|
19
|
-
this.workingCopyDir = `${dir}/.jj/working_copy`;
|
|
18
|
+
this.repoDir = `${dir}/.jj/repo`; // Core repo data
|
|
19
|
+
this.workingCopyDir = `${dir}/.jj/working_copy`; // Default workspace working copy
|
|
20
20
|
this.cache = new Map();
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -44,16 +44,20 @@ export class Storage {
|
|
|
44
44
|
// Create default workspace directory
|
|
45
45
|
await this.fs.promises.mkdir(`${this.workingCopyDir}/default`, { recursive: true });
|
|
46
46
|
} catch (error) {
|
|
47
|
-
throw new JJError(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
throw new JJError(
|
|
48
|
+
'STORAGE_INIT_FAILED',
|
|
49
|
+
`Failed to initialize .jj directory: ${error.message}`,
|
|
50
|
+
{
|
|
51
|
+
dir: this.jjDir,
|
|
52
|
+
originalError: error,
|
|
53
|
+
}
|
|
54
|
+
);
|
|
51
55
|
}
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
/**
|
|
55
59
|
* Read and parse JSON file
|
|
56
|
-
*
|
|
60
|
+
*
|
|
57
61
|
* @param {string} path - Relative path from .jj directory
|
|
58
62
|
* @returns {Promise<Object|null>} Parsed JSON or null if file doesn't exist
|
|
59
63
|
*/
|
|
@@ -83,7 +87,7 @@ export class Storage {
|
|
|
83
87
|
|
|
84
88
|
/**
|
|
85
89
|
* Write JSON file atomically
|
|
86
|
-
*
|
|
90
|
+
*
|
|
87
91
|
* @param {string} path - Relative path from .jj directory
|
|
88
92
|
* @param {Object|string} data - Data to write (object will be stringified)
|
|
89
93
|
*/
|
|
@@ -111,7 +115,9 @@ export class Storage {
|
|
|
111
115
|
// Clean up temp file
|
|
112
116
|
try {
|
|
113
117
|
await this.fs.promises.unlink(tmpPath);
|
|
114
|
-
} catch {
|
|
118
|
+
} catch {
|
|
119
|
+
// Best-effort cleanup; ignore if the temp file is already gone.
|
|
120
|
+
}
|
|
115
121
|
|
|
116
122
|
throw new JJError('STORAGE_WRITE_FAILED', `Failed to write ${path}: ${error.message}`, {
|
|
117
123
|
path: fullPath,
|
|
@@ -122,9 +128,9 @@ export class Storage {
|
|
|
122
128
|
|
|
123
129
|
/**
|
|
124
130
|
* Read JSONL file (newline-delimited JSON)
|
|
125
|
-
*
|
|
131
|
+
*
|
|
126
132
|
* @param {string} path - Relative path from .jj directory
|
|
127
|
-
* @returns {Promise<Array
|
|
133
|
+
* @returns {Promise<Array<any>>} Array of parsed JSON objects
|
|
128
134
|
*/
|
|
129
135
|
async readLines(path) {
|
|
130
136
|
const fullPath = `${this.jjDir}/${path}`;
|
|
@@ -134,8 +140,8 @@ export class Storage {
|
|
|
134
140
|
return content
|
|
135
141
|
.trim()
|
|
136
142
|
.split('\n')
|
|
137
|
-
.filter((line) => line.length > 0)
|
|
138
|
-
.map((line) => JSON.parse(line));
|
|
143
|
+
.filter((/** @type {any} */ line) => line.length > 0)
|
|
144
|
+
.map((/** @type {any} */ line) => JSON.parse(line));
|
|
139
145
|
} catch (error) {
|
|
140
146
|
if (error.code === 'ENOENT') {
|
|
141
147
|
return [];
|
|
@@ -149,7 +155,7 @@ export class Storage {
|
|
|
149
155
|
|
|
150
156
|
/**
|
|
151
157
|
* Append line to JSONL file
|
|
152
|
-
*
|
|
158
|
+
*
|
|
153
159
|
* @param {string} path - Relative path from .jj directory
|
|
154
160
|
* @param {string} line - JSON string to append
|
|
155
161
|
*/
|
|
@@ -177,13 +183,13 @@ export class Storage {
|
|
|
177
183
|
|
|
178
184
|
/**
|
|
179
185
|
* Check if file exists
|
|
180
|
-
*
|
|
186
|
+
*
|
|
181
187
|
* @param {string} path - Relative path from .jj directory
|
|
182
188
|
* @returns {Promise<boolean>}
|
|
183
189
|
*/
|
|
184
190
|
async exists(path) {
|
|
185
191
|
const fullPath = `${this.jjDir}/${path}`;
|
|
186
|
-
|
|
192
|
+
|
|
187
193
|
try {
|
|
188
194
|
await this.fs.promises.stat(fullPath);
|
|
189
195
|
return true;
|
|
@@ -194,7 +200,7 @@ export class Storage {
|
|
|
194
200
|
|
|
195
201
|
/**
|
|
196
202
|
* Invalidate cache
|
|
197
|
-
*
|
|
203
|
+
*
|
|
198
204
|
* @param {string} [path] - Specific path to invalidate, or all if not provided
|
|
199
205
|
*/
|
|
200
206
|
invalidateCache(path) {
|
package/src/core/tag-store.js
CHANGED
|
@@ -49,7 +49,7 @@ function matchesPattern(name, pattern) {
|
|
|
49
49
|
|
|
50
50
|
export class TagStore {
|
|
51
51
|
/**
|
|
52
|
-
* @param {
|
|
52
|
+
* @param {any} fs - Filesystem interface
|
|
53
53
|
* @param {string} jjDir - Path to .jj directory
|
|
54
54
|
*/
|
|
55
55
|
constructor(fs, jjDir) {
|
|
@@ -60,7 +60,7 @@ export class TagStore {
|
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
62
|
* Loads tags from storage
|
|
63
|
-
* @returns {Promise<
|
|
63
|
+
* @returns {Promise<Record<string, any>>} Map of tag name to changeId
|
|
64
64
|
*/
|
|
65
65
|
async load() {
|
|
66
66
|
try {
|
package/src/core/user-config.js
CHANGED
|
@@ -80,6 +80,8 @@ export class UserConfig {
|
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
82
|
* Deep merge two config objects (workspace overrides global)
|
|
83
|
+
* @param {any} target - Base config object
|
|
84
|
+
* @param {any} source - Overriding config object
|
|
83
85
|
* @private
|
|
84
86
|
*/
|
|
85
87
|
_deepMerge(target, source) {
|
|
@@ -171,7 +173,7 @@ export class UserConfig {
|
|
|
171
173
|
}
|
|
172
174
|
|
|
173
175
|
const parts = key.split('.');
|
|
174
|
-
const lastPart = parts.pop();
|
|
176
|
+
const lastPart = /** @type {string} */ (parts.pop());
|
|
175
177
|
let target = this.config;
|
|
176
178
|
|
|
177
179
|
for (const part of parts) {
|
package/src/core/working-copy.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WorkingCopy - Manages working copy state and file tracking
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Tracks file states (mtime, size, mode) for efficient modification detection.
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ import { validateChangeId, validatePath } from '../utils/validation.js';
|
|
|
10
10
|
export class WorkingCopy {
|
|
11
11
|
/**
|
|
12
12
|
* @param {Storage} storage - Storage manager instance
|
|
13
|
-
* @param {
|
|
13
|
+
* @param {any} fs - Filesystem implementation
|
|
14
14
|
* @param {string} dir - Repository directory
|
|
15
15
|
* @param {string} [workspaceId='default'] - Workspace identifier
|
|
16
16
|
*/
|
|
@@ -85,8 +85,8 @@ export class WorkingCopy {
|
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
87
|
* Get current working copy state
|
|
88
|
-
*
|
|
89
|
-
* @returns {
|
|
88
|
+
*
|
|
89
|
+
* @returns {Promise<Record<string, any>>} Working copy state
|
|
90
90
|
*/
|
|
91
91
|
async getState() {
|
|
92
92
|
if (!this.state) {
|
|
@@ -97,7 +97,7 @@ export class WorkingCopy {
|
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
99
|
* Get current change ID
|
|
100
|
-
*
|
|
100
|
+
*
|
|
101
101
|
* @returns {string} Current change ID
|
|
102
102
|
*/
|
|
103
103
|
getCurrentChangeId() {
|
|
@@ -111,7 +111,7 @@ export class WorkingCopy {
|
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
113
|
* Set current change ID
|
|
114
|
-
*
|
|
114
|
+
*
|
|
115
115
|
* @param {string} changeId - New change ID
|
|
116
116
|
*/
|
|
117
117
|
async setCurrentChange(changeId) {
|
|
@@ -127,7 +127,7 @@ export class WorkingCopy {
|
|
|
127
127
|
|
|
128
128
|
/**
|
|
129
129
|
* Track a file in the working copy
|
|
130
|
-
*
|
|
130
|
+
*
|
|
131
131
|
* @param {string} path - File path relative to repo root
|
|
132
132
|
* @param {Object} fileState - File state (mtime, size, mode, hash)
|
|
133
133
|
*/
|
|
@@ -160,9 +160,9 @@ export class WorkingCopy {
|
|
|
160
160
|
|
|
161
161
|
/**
|
|
162
162
|
* Get list of modified files
|
|
163
|
-
*
|
|
163
|
+
*
|
|
164
164
|
* Uses mtime + size comparison for fast detection.
|
|
165
|
-
*
|
|
165
|
+
*
|
|
166
166
|
* @returns {Promise<Array<string>>} Array of modified file paths
|
|
167
167
|
*/
|
|
168
168
|
async getModifiedFiles() {
|
|
@@ -198,6 +198,122 @@ export class WorkingCopy {
|
|
|
198
198
|
return modified;
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Recursively walk the working directory on disk and return every file path
|
|
203
|
+
* relative to the repo root (v1.6).
|
|
204
|
+
*
|
|
205
|
+
* This is the foundation of automatic snapshotting: unlike listFiles() (which
|
|
206
|
+
* only returns files already tracked in state), this discovers files created
|
|
207
|
+
* out-of-band (editors, shell, git checkout). The .git and .jj metadata dirs
|
|
208
|
+
* and node_modules are always excluded.
|
|
209
|
+
*
|
|
210
|
+
* @param {object} [opts]
|
|
211
|
+
* @param {string[]} [opts.exclude] - Additional top-level directory/file names to skip
|
|
212
|
+
* @returns {Promise<Array<string>>} Repo-relative file paths (forward-slash separated)
|
|
213
|
+
*/
|
|
214
|
+
async walk(opts = {}) {
|
|
215
|
+
const exclude = new Set(['.git', '.jj', 'node_modules', ...(opts.exclude || [])]);
|
|
216
|
+
/** @type {string[]} */
|
|
217
|
+
const results = [];
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* @param {string} current - Absolute directory path
|
|
221
|
+
* @param {string} prefix - Repo-relative prefix
|
|
222
|
+
*/
|
|
223
|
+
const recurse = async (current, prefix) => {
|
|
224
|
+
/** @type {string[]} */
|
|
225
|
+
let entries;
|
|
226
|
+
try {
|
|
227
|
+
entries = await this.fs.promises.readdir(current);
|
|
228
|
+
} catch {
|
|
229
|
+
return; // not a readable directory
|
|
230
|
+
}
|
|
231
|
+
for (const name of entries) {
|
|
232
|
+
if (exclude.has(name)) continue;
|
|
233
|
+
const full = `${current}/${name}`;
|
|
234
|
+
const rel = prefix ? `${prefix}/${name}` : name;
|
|
235
|
+
let stats;
|
|
236
|
+
try {
|
|
237
|
+
stats = await this.fs.promises.stat(full);
|
|
238
|
+
} catch {
|
|
239
|
+
// Some in-memory filesystems don't materialize directory entries;
|
|
240
|
+
// treat an unstattable name as a possible directory and recurse.
|
|
241
|
+
await recurse(full, rel);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (stats.isDirectory()) {
|
|
245
|
+
await recurse(full, rel);
|
|
246
|
+
} else if (stats.isFile()) {
|
|
247
|
+
results.push(rel);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
await recurse(this.dir, '');
|
|
253
|
+
return results;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Snapshot the working directory: scan the disk, then reconcile tracked file
|
|
258
|
+
* state with what actually exists (v1.6, mirrors jj's snapshot-before-command).
|
|
259
|
+
*
|
|
260
|
+
* Newly-seen files are tracked, changed files (by mtime/size) are updated, and
|
|
261
|
+
* tracked-but-missing files are untracked. Sparse patterns are honored.
|
|
262
|
+
*
|
|
263
|
+
* @param {object} [opts]
|
|
264
|
+
* @param {string[]} [opts.exclude] - Additional names to skip while walking
|
|
265
|
+
* @returns {Promise<{added: string[], modified: string[], deleted: string[]}>}
|
|
266
|
+
*/
|
|
267
|
+
async snapshot(opts = {}) {
|
|
268
|
+
if (!this.state) {
|
|
269
|
+
await this.load();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const onDisk = await this.walk(opts);
|
|
273
|
+
const trackedBefore = new Set(Object.keys(this.state.fileStates));
|
|
274
|
+
const seen = new Set();
|
|
275
|
+
/** @type {string[]} */
|
|
276
|
+
const added = [];
|
|
277
|
+
/** @type {string[]} */
|
|
278
|
+
const modified = [];
|
|
279
|
+
|
|
280
|
+
for (const rel of onDisk) {
|
|
281
|
+
// Respect sparse checkout: don't snapshot files outside the sparse set.
|
|
282
|
+
if (!this.matchesSparsePatterns(rel)) continue;
|
|
283
|
+
seen.add(rel);
|
|
284
|
+
|
|
285
|
+
let stats;
|
|
286
|
+
try {
|
|
287
|
+
stats = await this.fs.promises.stat(`${this.dir}/${rel}`);
|
|
288
|
+
} catch {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const prev = this.state.fileStates[rel];
|
|
293
|
+
const next = { mtime: stats.mtime, size: stats.size, mode: stats.mode };
|
|
294
|
+
|
|
295
|
+
if (!prev) {
|
|
296
|
+
this.state.fileStates[rel] = next;
|
|
297
|
+
added.push(rel);
|
|
298
|
+
} else if (prev.mtime !== stats.mtime || prev.size !== stats.size) {
|
|
299
|
+
this.state.fileStates[rel] = next;
|
|
300
|
+
modified.push(rel);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** @type {string[]} */
|
|
305
|
+
const deleted = [];
|
|
306
|
+
for (const rel of trackedBefore) {
|
|
307
|
+
if (!seen.has(rel)) {
|
|
308
|
+
delete this.state.fileStates[rel];
|
|
309
|
+
deleted.push(rel);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
await this.save();
|
|
314
|
+
return { added, modified, deleted };
|
|
315
|
+
}
|
|
316
|
+
|
|
201
317
|
/**
|
|
202
318
|
* List all tracked files in working copy
|
|
203
319
|
*
|
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { JJError } from '../utils/errors.js';
|
|
11
|
-
import { validateChangeId
|
|
11
|
+
import { validateChangeId } from '../utils/validation.js';
|
|
12
12
|
import { generateId } from '../utils/id-generation.js';
|
|
13
13
|
import path from 'path';
|
|
14
14
|
|
|
15
15
|
export class WorkspaceManager {
|
|
16
16
|
/**
|
|
17
17
|
* @param {Storage} storage - Storage manager instance
|
|
18
|
-
* @param {
|
|
18
|
+
* @param {any} fs - Filesystem implementation
|
|
19
19
|
* @param {string} repoDir - Repository directory
|
|
20
20
|
*/
|
|
21
21
|
constructor(storage, fs, repoDir) {
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
* Deep merge for JSON objects
|
|
9
9
|
* Recursively merges objects, with theirs taking precedence on conflicts
|
|
10
10
|
*
|
|
11
|
-
* @param {
|
|
12
|
-
* @param {
|
|
13
|
-
* @param {
|
|
14
|
-
* @returns {
|
|
11
|
+
* @param {any} base - Base object
|
|
12
|
+
* @param {any} ours - Our changes
|
|
13
|
+
* @param {any} theirs - Their changes
|
|
14
|
+
* @returns {Record<string, any>|null} Merged object
|
|
15
15
|
*/
|
|
16
16
|
function deepMergeJSON(base, ours, theirs) {
|
|
17
17
|
// If all are identical, return any
|
|
@@ -30,7 +30,12 @@ function deepMergeJSON(base, ours, theirs) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
// Both sides changed - need smart merge
|
|
33
|
-
if (
|
|
33
|
+
if (
|
|
34
|
+
typeof ours !== 'object' ||
|
|
35
|
+
typeof theirs !== 'object' ||
|
|
36
|
+
Array.isArray(ours) ||
|
|
37
|
+
Array.isArray(theirs)
|
|
38
|
+
) {
|
|
34
39
|
// Primitives or arrays - conflict
|
|
35
40
|
return null;
|
|
36
41
|
}
|
|
@@ -61,7 +66,12 @@ function deepMergeJSON(base, ours, theirs) {
|
|
|
61
66
|
result[key] = oursVal;
|
|
62
67
|
} else {
|
|
63
68
|
// Both changed differently
|
|
64
|
-
if (
|
|
69
|
+
if (
|
|
70
|
+
typeof oursVal === 'object' &&
|
|
71
|
+
typeof theirsVal === 'object' &&
|
|
72
|
+
!Array.isArray(oursVal) &&
|
|
73
|
+
!Array.isArray(theirsVal)
|
|
74
|
+
) {
|
|
65
75
|
// Recurse for nested objects
|
|
66
76
|
const merged = deepMergeJSON(baseVal || {}, oursVal, theirsVal);
|
|
67
77
|
if (merged === null) {
|
|
@@ -83,8 +93,10 @@ function deepMergeJSON(base, ours, theirs) {
|
|
|
83
93
|
*
|
|
84
94
|
* Attempts to merge JSON files intelligently by merging objects.
|
|
85
95
|
* Falls back to conflict markers if merge isn't possible.
|
|
96
|
+
*
|
|
97
|
+
* @param {{ path?: string, content: { base?: string, ours?: string, theirs?: string } }} args
|
|
86
98
|
*/
|
|
87
|
-
export async function jsonDriver({
|
|
99
|
+
export async function jsonDriver({ content }) {
|
|
88
100
|
const { base, ours, theirs } = content;
|
|
89
101
|
|
|
90
102
|
try {
|
|
@@ -106,10 +118,12 @@ export async function jsonDriver({ path, content }) {
|
|
|
106
118
|
return {
|
|
107
119
|
content: conflictContent,
|
|
108
120
|
hasConflict: true,
|
|
109
|
-
conflicts: [
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
121
|
+
conflicts: [
|
|
122
|
+
{
|
|
123
|
+
type: 'json-merge-conflict',
|
|
124
|
+
message: 'Both sides modified the same field with different values',
|
|
125
|
+
},
|
|
126
|
+
],
|
|
113
127
|
};
|
|
114
128
|
}
|
|
115
129
|
} catch (error) {
|
|
@@ -118,10 +132,12 @@ export async function jsonDriver({ path, content }) {
|
|
|
118
132
|
return {
|
|
119
133
|
content: conflictContent,
|
|
120
134
|
hasConflict: true,
|
|
121
|
-
conflicts: [
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
135
|
+
conflicts: [
|
|
136
|
+
{
|
|
137
|
+
type: 'json-parse-error',
|
|
138
|
+
message: `JSON parsing failed: ${error.message}`,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
125
141
|
};
|
|
126
142
|
}
|
|
127
143
|
}
|
|
@@ -133,8 +149,10 @@ export async function jsonDriver({ path, content }) {
|
|
|
133
149
|
* - Union merge for dependencies/devDependencies
|
|
134
150
|
* - Take theirs for version (semver conflicts are hard)
|
|
135
151
|
* - Merge other fields intelligently
|
|
152
|
+
*
|
|
153
|
+
* @param {{ path?: string, content: { base?: string, ours?: string, theirs?: string } }} args
|
|
136
154
|
*/
|
|
137
|
-
export async function packageJsonDriver({
|
|
155
|
+
export async function packageJsonDriver({ content }) {
|
|
138
156
|
const { base, ours, theirs } = content;
|
|
139
157
|
|
|
140
158
|
try {
|
|
@@ -147,12 +165,17 @@ export async function packageJsonDriver({ path, content }) {
|
|
|
147
165
|
|
|
148
166
|
// Merge top-level fields
|
|
149
167
|
for (const key of Object.keys({ ...oursObj, ...theirsObj })) {
|
|
150
|
-
if (
|
|
168
|
+
if (
|
|
169
|
+
key === 'dependencies' ||
|
|
170
|
+
key === 'devDependencies' ||
|
|
171
|
+
key === 'peerDependencies' ||
|
|
172
|
+
key === 'optionalDependencies'
|
|
173
|
+
) {
|
|
151
174
|
// Union merge dependencies
|
|
152
175
|
merged[key] = {
|
|
153
176
|
...baseObj[key],
|
|
154
177
|
...oursObj[key],
|
|
155
|
-
...theirsObj[key],
|
|
178
|
+
...theirsObj[key], // theirs wins on conflicts
|
|
156
179
|
};
|
|
157
180
|
} else if (key === 'version') {
|
|
158
181
|
// Version conflicts are tricky - take theirs
|
|
@@ -162,7 +185,7 @@ export async function packageJsonDriver({ path, content }) {
|
|
|
162
185
|
merged[key] = {
|
|
163
186
|
...baseObj[key],
|
|
164
187
|
...oursObj[key],
|
|
165
|
-
...theirsObj[key],
|
|
188
|
+
...theirsObj[key], // theirs wins
|
|
166
189
|
};
|
|
167
190
|
} else {
|
|
168
191
|
// Other fields - use standard logic
|
|
@@ -198,10 +221,12 @@ export async function packageJsonDriver({ path, content }) {
|
|
|
198
221
|
return {
|
|
199
222
|
content: conflictContent,
|
|
200
223
|
hasConflict: true,
|
|
201
|
-
conflicts: [
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
224
|
+
conflicts: [
|
|
225
|
+
{
|
|
226
|
+
type: 'parse-error',
|
|
227
|
+
message: `package.json parsing failed: ${error.message}`,
|
|
228
|
+
},
|
|
229
|
+
],
|
|
205
230
|
};
|
|
206
231
|
}
|
|
207
232
|
}
|
|
@@ -211,8 +236,10 @@ export async function packageJsonDriver({ path, content }) {
|
|
|
211
236
|
*
|
|
212
237
|
* Note: This is a simple implementation. For production use, consider
|
|
213
238
|
* using a proper YAML library like 'js-yaml'.
|
|
239
|
+
*
|
|
240
|
+
* @param {{ path?: string, content: { base?: string, ours?: string, theirs?: string } }} args
|
|
214
241
|
*/
|
|
215
|
-
export async function yamlDriver({
|
|
242
|
+
export async function yamlDriver({ content }) {
|
|
216
243
|
const { base, ours, theirs } = content;
|
|
217
244
|
|
|
218
245
|
// Simple YAML merge - just do text-based for now
|
|
@@ -235,10 +262,12 @@ export async function yamlDriver({ path, content }) {
|
|
|
235
262
|
return {
|
|
236
263
|
content: conflictContent,
|
|
237
264
|
hasConflict: true,
|
|
238
|
-
conflicts: [
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
265
|
+
conflicts: [
|
|
266
|
+
{
|
|
267
|
+
type: 'yaml-conflict',
|
|
268
|
+
message: 'YAML files modified on both sides',
|
|
269
|
+
},
|
|
270
|
+
],
|
|
242
271
|
};
|
|
243
272
|
}
|
|
244
273
|
|
|
@@ -247,8 +276,10 @@ export async function yamlDriver({ path, content }) {
|
|
|
247
276
|
*
|
|
248
277
|
* Attempts to merge markdown files by sections (headers).
|
|
249
278
|
* Falls back to conflict markers for complex cases.
|
|
279
|
+
*
|
|
280
|
+
* @param {{ path?: string, content: { base?: string, ours?: string, theirs?: string } }} args
|
|
250
281
|
*/
|
|
251
|
-
export async function markdownDriver({
|
|
282
|
+
export async function markdownDriver({ content }) {
|
|
252
283
|
const { base, ours, theirs } = content;
|
|
253
284
|
|
|
254
285
|
// Simple markdown merge - just text-based for now
|
|
@@ -272,10 +303,12 @@ export async function markdownDriver({ path, content }) {
|
|
|
272
303
|
return {
|
|
273
304
|
content: conflictContent,
|
|
274
305
|
hasConflict: true,
|
|
275
|
-
conflicts: [
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
306
|
+
conflicts: [
|
|
307
|
+
{
|
|
308
|
+
type: 'markdown-conflict',
|
|
309
|
+
message: 'Markdown files modified on both sides',
|
|
310
|
+
},
|
|
311
|
+
],
|
|
279
312
|
};
|
|
280
313
|
}
|
|
281
314
|
|
package/src/types.d.ts
CHANGED
|
@@ -294,6 +294,12 @@ export interface CreateJJOptions {
|
|
|
294
294
|
backend?: string | any;
|
|
295
295
|
/** Event hooks */
|
|
296
296
|
hooks?: Hooks;
|
|
297
|
+
/**
|
|
298
|
+
* Automatically snapshot the working directory (disk-walk) before
|
|
299
|
+
* read/commit operations so out-of-band file changes are picked up.
|
|
300
|
+
* Defaults to `true`; set `false` to only track files written via jj.write().
|
|
301
|
+
*/
|
|
302
|
+
autoSnapshot?: boolean;
|
|
297
303
|
}
|
|
298
304
|
|
|
299
305
|
/**
|
|
@@ -1042,6 +1048,13 @@ export interface JJ {
|
|
|
1042
1048
|
edit(args: EditArgs): Promise<void>;
|
|
1043
1049
|
status(): Promise<Status>;
|
|
1044
1050
|
stats(): Promise<RepositoryStats>;
|
|
1051
|
+
/**
|
|
1052
|
+
* Snapshot the working directory (disk-walk), reconciling tracked file state
|
|
1053
|
+
* and the working-copy change's content with what exists on disk (v1.6).
|
|
1054
|
+
* Runs automatically before status/describe/diff/read/file.* unless the repo
|
|
1055
|
+
* was created with `{ autoSnapshot: false }`.
|
|
1056
|
+
*/
|
|
1057
|
+
snapshot(): Promise<{ added: string[]; modified: string[]; deleted: string[] }>;
|
|
1045
1058
|
|
|
1046
1059
|
// History editing
|
|
1047
1060
|
squash(args: SquashArgs): Promise<void>;
|
|
@@ -1051,6 +1064,39 @@ export interface JJ {
|
|
|
1051
1064
|
unabandon(args: UnabandonArgs): Promise<void>;
|
|
1052
1065
|
absorb(args?: AbsorbArgs): Promise<AbsorbResult>;
|
|
1053
1066
|
|
|
1067
|
+
/**
|
|
1068
|
+
* Create a change that reverses another change (matches `jj revert`, v1.5).
|
|
1069
|
+
* Canonical replacement for the deprecated `backout()`.
|
|
1070
|
+
*/
|
|
1071
|
+
revert(args: { revision?: ChangeID; change?: ChangeID; changeId?: ChangeID; target?: ChangeID; message?: string }): Promise<{
|
|
1072
|
+
changeId: ChangeID;
|
|
1073
|
+
description: string;
|
|
1074
|
+
revertedFrom: ChangeID;
|
|
1075
|
+
backedOut: ChangeID;
|
|
1076
|
+
fileSnapshot: Record<string, string>;
|
|
1077
|
+
}>;
|
|
1078
|
+
|
|
1079
|
+
/** @deprecated Use {@link JJ.revert}. Retained for backward compatibility. */
|
|
1080
|
+
backout(args: { revision?: ChangeID; change?: ChangeID; changeId?: ChangeID; target?: ChangeID; message?: string }): Promise<{
|
|
1081
|
+
changeId: ChangeID;
|
|
1082
|
+
description: string;
|
|
1083
|
+
backedOut: ChangeID;
|
|
1084
|
+
fileSnapshot: Record<string, string>;
|
|
1085
|
+
}>;
|
|
1086
|
+
|
|
1087
|
+
/** Sign a change, recording signature metadata (matches `jj sign`, v1.5). */
|
|
1088
|
+
sign(args?: { revision?: ChangeID; change?: ChangeID; changeId?: ChangeID; backend?: string; key?: string }): Promise<{
|
|
1089
|
+
changeId: ChangeID;
|
|
1090
|
+
signed: true;
|
|
1091
|
+
signature: { status: string; backend: string; key: string | null; timestamp: string };
|
|
1092
|
+
}>;
|
|
1093
|
+
|
|
1094
|
+
/** Remove signature metadata from a change (matches `jj unsign`, v1.5). */
|
|
1095
|
+
unsign(args?: { revision?: ChangeID; change?: ChangeID; changeId?: ChangeID }): Promise<{
|
|
1096
|
+
changeId: ChangeID;
|
|
1097
|
+
signed: false;
|
|
1098
|
+
}>;
|
|
1099
|
+
|
|
1054
1100
|
// Queries
|
|
1055
1101
|
log(opts?: LogOptions & { count?: false }): Promise<LogEntry[]>;
|
|
1056
1102
|
log(opts: LogOptions & { count: true }): Promise<number>;
|
|
@@ -1059,6 +1105,11 @@ export interface JJ {
|
|
|
1059
1105
|
|
|
1060
1106
|
// Operations
|
|
1061
1107
|
undo(opts?: UndoOptions): Promise<UndoResult>;
|
|
1108
|
+
/** Re-apply an operation previously reverted by {@link JJ.undo} (matches `jj redo`, v1.5). */
|
|
1109
|
+
redo(): Promise<{
|
|
1110
|
+
redoneOperation: { description: string; undoneOpId: OperationID };
|
|
1111
|
+
restoredState: { workingCopy: ChangeID; heads: ChangeID[]; fileCount: number };
|
|
1112
|
+
}>;
|
|
1062
1113
|
operations: {
|
|
1063
1114
|
list(opts?: { limit?: number }): Promise<Operation[]>;
|
|
1064
1115
|
at(args: { operation: OperationID }): Promise<JJ>;
|
|
@@ -1116,6 +1167,8 @@ export interface JJ {
|
|
|
1116
1167
|
set(args: BookmarkSetArgs): Promise<{ name: string; changeId: ChangeID }>;
|
|
1117
1168
|
create(args: BookmarkCreateArgs): Promise<{ name: string; changeId: ChangeID }>;
|
|
1118
1169
|
move(args: BookmarkMoveArgs): Promise<{ name: string; from: ChangeID; to: ChangeID }>;
|
|
1170
|
+
/** Move a bookmark forward only; rejects non-descendant targets (matches `jj bookmark advance`, v1.5). */
|
|
1171
|
+
advance(args: { name: string; to?: ChangeID; target?: ChangeID; changeId?: ChangeID; change?: ChangeID; revision?: ChangeID }): Promise<{ name: string; from: ChangeID; to: ChangeID }>;
|
|
1119
1172
|
delete(args: BookmarkDeleteArgs): Promise<{ deleted: string }>;
|
|
1120
1173
|
rename(args: BookmarkRenameArgs): Promise<{ oldName: string; newName: string; changeId: ChangeID }>;
|
|
1121
1174
|
track(args: BookmarkTrackArgs): Promise<{ name: string; remote: string; tracking: boolean }>;
|
|
@@ -1152,6 +1205,8 @@ export interface JJ {
|
|
|
1152
1205
|
file: {
|
|
1153
1206
|
show(args: ReadArgs): Promise<string | Uint8Array>;
|
|
1154
1207
|
list(args?: ListFilesArgs): Promise<string[]>;
|
|
1208
|
+
/** Search tracked file contents (matches `jj file search`, v1.5). Regex by default. */
|
|
1209
|
+
search(args: { pattern: string; changeId?: ChangeID; change?: ChangeID; kind?: 'regex' | 'substring'; path?: string }): Promise<Array<{ path: string; lineNumber: number; line: string }>>;
|
|
1155
1210
|
write(args: WriteArgs): Promise<WriteResult>;
|
|
1156
1211
|
move(args: MoveFileArgs): Promise<MoveResult>;
|
|
1157
1212
|
remove(args: RemoveArgs): Promise<RemoveResult>;
|
|
@@ -1270,6 +1325,15 @@ export interface JJ {
|
|
|
1270
1325
|
reset(): Promise<void>;
|
|
1271
1326
|
clear(): Promise<void>;
|
|
1272
1327
|
};
|
|
1328
|
+
|
|
1329
|
+
// Tags (immutable references; `set` is the v1.5 upsert)
|
|
1330
|
+
tag: {
|
|
1331
|
+
create(args: { name: string; changeId?: ChangeID }): Promise<{ name: string; changeId: ChangeID }>;
|
|
1332
|
+
/** Create or move a tag (matches `jj tag set`, v1.5). */
|
|
1333
|
+
set(args: { name: string; changeId?: ChangeID; change?: ChangeID; revision?: ChangeID }): Promise<{ name: string; changeId: ChangeID; updated: boolean }>;
|
|
1334
|
+
list(args?: { pattern?: string }): Promise<Array<{ name: string; changeId: ChangeID }>>;
|
|
1335
|
+
delete(args: { name: string }): Promise<{ deleted: string }>;
|
|
1336
|
+
};
|
|
1273
1337
|
}
|
|
1274
1338
|
|
|
1275
1339
|
// ============================================================================
|
|
@@ -1290,6 +1354,8 @@ export function createJJ(options: CreateJJOptions): Promise<JJ>;
|
|
|
1290
1354
|
export class JJError extends Error {
|
|
1291
1355
|
code: string;
|
|
1292
1356
|
suggestion?: string;
|
|
1357
|
+
context: Record<string, any>;
|
|
1358
|
+
details: Record<string, any>;
|
|
1293
1359
|
|
|
1294
|
-
constructor(code: string, message: string,
|
|
1360
|
+
constructor(code: string, message: string, context?: { suggestion?: string } & Record<string, any>);
|
|
1295
1361
|
}
|