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
package/src/core/jj-checkout.js
CHANGED
|
@@ -19,6 +19,10 @@ const protoDir = inDist
|
|
|
19
19
|
: path.join(__dirname, '..', 'protos');
|
|
20
20
|
|
|
21
21
|
export class JJCheckout {
|
|
22
|
+
/**
|
|
23
|
+
* @param {any} fs - File system module (isomorphic-git compatible)
|
|
24
|
+
* @param {any} dir - Repository directory path
|
|
25
|
+
*/
|
|
22
26
|
constructor(fs, dir) {
|
|
23
27
|
this.fs = fs;
|
|
24
28
|
this.dir = dir;
|
|
@@ -42,7 +46,7 @@ export class JJCheckout {
|
|
|
42
46
|
// Create message (use camelCase for protobufjs)
|
|
43
47
|
const message = Checkout.create({
|
|
44
48
|
operationId: opIdBuffer,
|
|
45
|
-
workspaceName: workspaceName
|
|
49
|
+
workspaceName: workspaceName,
|
|
46
50
|
});
|
|
47
51
|
|
|
48
52
|
// Verify the message
|
|
@@ -62,7 +66,7 @@ export class JJCheckout {
|
|
|
62
66
|
/**
|
|
63
67
|
* Read checkout file
|
|
64
68
|
*
|
|
65
|
-
* @returns {
|
|
69
|
+
* @returns {Promise<Record<string, any>>} Decoded checkout data
|
|
66
70
|
*/
|
|
67
71
|
async readCheckout() {
|
|
68
72
|
const checkoutPath = `${this.dir}/.jj/working_copy/checkout`;
|
|
@@ -73,12 +77,12 @@ export class JJCheckout {
|
|
|
73
77
|
const Checkout = root.lookupType('local_working_copy.Checkout');
|
|
74
78
|
|
|
75
79
|
// Decode
|
|
76
|
-
const message = Checkout.decode(buffer);
|
|
80
|
+
const message = /** @type {any} */ (Checkout.decode(buffer));
|
|
77
81
|
|
|
78
82
|
// Return the decoded message with proper field access
|
|
79
83
|
return {
|
|
80
84
|
operation_id: message.operationId,
|
|
81
|
-
workspace_name: message.workspaceName
|
|
85
|
+
workspace_name: message.workspaceName,
|
|
82
86
|
};
|
|
83
87
|
}
|
|
84
88
|
}
|
|
@@ -19,6 +19,10 @@ const protoDir = inDist
|
|
|
19
19
|
: path.join(__dirname, '..', 'protos');
|
|
20
20
|
|
|
21
21
|
export class JJOperationStore {
|
|
22
|
+
/**
|
|
23
|
+
* @param {any} fs - File system module (isomorphic-git compatible)
|
|
24
|
+
* @param {any} dir - Repository directory path
|
|
25
|
+
*/
|
|
22
26
|
constructor(fs, dir) {
|
|
23
27
|
this.fs = fs;
|
|
24
28
|
this.dir = dir;
|
|
@@ -31,7 +35,7 @@ export class JJOperationStore {
|
|
|
31
35
|
* @param {string} operationId - Operation ID as hex string (128 characters)
|
|
32
36
|
* @param {string} viewId - View ID as hex string (128 characters)
|
|
33
37
|
* @param {Array<string>} parentIds - Parent operation IDs as hex strings
|
|
34
|
-
* @param {
|
|
38
|
+
* @param {Record<string, any>} metadata - Operation metadata
|
|
35
39
|
*/
|
|
36
40
|
async writeOperation(operationId, viewId, parentIds, metadata) {
|
|
37
41
|
// Load protobuf schema
|
|
@@ -40,23 +44,23 @@ export class JJOperationStore {
|
|
|
40
44
|
|
|
41
45
|
// Convert hex IDs to bytes
|
|
42
46
|
const viewIdBuffer = Buffer.from(viewId, 'hex');
|
|
43
|
-
const parentBuffers = parentIds.map(id => Buffer.from(id, 'hex'));
|
|
47
|
+
const parentBuffers = parentIds.map((id) => Buffer.from(id, 'hex'));
|
|
44
48
|
|
|
45
49
|
// Create metadata message (use camelCase for protobufjs)
|
|
46
50
|
const metadataMsg = {
|
|
47
51
|
startTime: {
|
|
48
52
|
millisSinceEpoch: metadata.start_time.millis_since_epoch,
|
|
49
|
-
tzOffset: metadata.start_time.tz_offset
|
|
53
|
+
tzOffset: metadata.start_time.tz_offset,
|
|
50
54
|
},
|
|
51
55
|
endTime: {
|
|
52
56
|
millisSinceEpoch: metadata.end_time.millis_since_epoch,
|
|
53
|
-
tzOffset: metadata.end_time.tz_offset
|
|
57
|
+
tzOffset: metadata.end_time.tz_offset,
|
|
54
58
|
},
|
|
55
59
|
description: metadata.description,
|
|
56
60
|
hostname: metadata.hostname,
|
|
57
61
|
username: metadata.username,
|
|
58
62
|
isSnapshot: metadata.is_snapshot,
|
|
59
|
-
tags: metadata.tags
|
|
63
|
+
tags: metadata.tags,
|
|
60
64
|
};
|
|
61
65
|
|
|
62
66
|
// Create operation message (use camelCase for protobufjs)
|
|
@@ -65,7 +69,7 @@ export class JJOperationStore {
|
|
|
65
69
|
parents: parentBuffers,
|
|
66
70
|
metadata: metadataMsg,
|
|
67
71
|
commitPredecessors: [], // Empty for now
|
|
68
|
-
storesCommitPredecessors: false
|
|
72
|
+
storesCommitPredecessors: false,
|
|
69
73
|
});
|
|
70
74
|
|
|
71
75
|
// Verify the message
|
|
@@ -86,7 +90,7 @@ export class JJOperationStore {
|
|
|
86
90
|
* Read operation file
|
|
87
91
|
*
|
|
88
92
|
* @param {string} operationId - Operation ID as hex string (128 characters)
|
|
89
|
-
* @returns {
|
|
93
|
+
* @returns {Promise<Record<string, any>>} Decoded operation data
|
|
90
94
|
*/
|
|
91
95
|
async readOperation(operationId) {
|
|
92
96
|
// Read from .jj/repo/op_store/operations/OPERATIONID
|
|
@@ -98,7 +102,7 @@ export class JJOperationStore {
|
|
|
98
102
|
const Operation = root.lookupType('simple_op_store.Operation');
|
|
99
103
|
|
|
100
104
|
// Decode
|
|
101
|
-
const message = Operation.decode(buffer);
|
|
105
|
+
const message = /** @type {any} */ (Operation.decode(buffer));
|
|
102
106
|
|
|
103
107
|
// Return the decoded message with proper field access
|
|
104
108
|
return {
|
|
@@ -107,20 +111,20 @@ export class JJOperationStore {
|
|
|
107
111
|
metadata: {
|
|
108
112
|
start_time: {
|
|
109
113
|
millis_since_epoch: message.metadata.startTime.millisSinceEpoch,
|
|
110
|
-
tz_offset: message.metadata.startTime.tzOffset
|
|
114
|
+
tz_offset: message.metadata.startTime.tzOffset,
|
|
111
115
|
},
|
|
112
116
|
end_time: {
|
|
113
117
|
millis_since_epoch: message.metadata.endTime.millisSinceEpoch,
|
|
114
|
-
tz_offset: message.metadata.endTime.tzOffset
|
|
118
|
+
tz_offset: message.metadata.endTime.tzOffset,
|
|
115
119
|
},
|
|
116
120
|
description: message.metadata.description,
|
|
117
121
|
hostname: message.metadata.hostname,
|
|
118
122
|
username: message.metadata.username,
|
|
119
123
|
is_snapshot: message.metadata.isSnapshot,
|
|
120
|
-
tags: message.metadata.tags
|
|
124
|
+
tags: message.metadata.tags,
|
|
121
125
|
},
|
|
122
126
|
commit_predecessors: message.commitPredecessors,
|
|
123
|
-
stores_commit_predecessors: message.storesCommitPredecessors
|
|
127
|
+
stores_commit_predecessors: message.storesCommitPredecessors,
|
|
124
128
|
};
|
|
125
129
|
}
|
|
126
130
|
}
|
|
@@ -19,6 +19,10 @@ const protoDir = inDist
|
|
|
19
19
|
: path.join(__dirname, '..', 'protos');
|
|
20
20
|
|
|
21
21
|
export class JJTreeState {
|
|
22
|
+
/**
|
|
23
|
+
* @param {any} fs - File system module (isomorphic-git compatible)
|
|
24
|
+
* @param {any} dir - Repository directory path
|
|
25
|
+
*/
|
|
22
26
|
constructor(fs, dir) {
|
|
23
27
|
this.fs = fs;
|
|
24
28
|
this.dir = dir;
|
|
@@ -29,7 +33,7 @@ export class JJTreeState {
|
|
|
29
33
|
* Write tree_state file
|
|
30
34
|
*
|
|
31
35
|
* @param {string} treeId - Tree ID as hex string (40 characters for SHA-1)
|
|
32
|
-
* @param {Array} fileStates - Array of file state objects
|
|
36
|
+
* @param {Array<any>} fileStates - Array of file state objects
|
|
33
37
|
*/
|
|
34
38
|
async writeTreeState(treeId, fileStates = []) {
|
|
35
39
|
// Load protobuf schema
|
|
@@ -40,13 +44,13 @@ export class JJTreeState {
|
|
|
40
44
|
const treeIdBuffer = Buffer.from(treeId, 'hex');
|
|
41
45
|
|
|
42
46
|
// Convert file states to protobuf format
|
|
43
|
-
const fileStateEntries = fileStates.map(fs => ({
|
|
47
|
+
const fileStateEntries = fileStates.map((fs) => ({
|
|
44
48
|
path: fs.path,
|
|
45
49
|
state: {
|
|
46
50
|
mtimeMillisSinceEpoch: fs.mtime, // Use camelCase for protobufjs
|
|
47
51
|
size: fs.size,
|
|
48
|
-
fileType: fs.fileType || 0 // 0 = Normal
|
|
49
|
-
}
|
|
52
|
+
fileType: fs.fileType || 0, // 0 = Normal
|
|
53
|
+
},
|
|
50
54
|
}));
|
|
51
55
|
|
|
52
56
|
// Create message (use camelCase for protobufjs)
|
|
@@ -55,8 +59,8 @@ export class JJTreeState {
|
|
|
55
59
|
fileStates: fileStateEntries,
|
|
56
60
|
isFileStatesSorted: false, // We don't sort them
|
|
57
61
|
sparsePatterns: {
|
|
58
|
-
prefixes: [] // No sparse patterns
|
|
59
|
-
}
|
|
62
|
+
prefixes: [], // No sparse patterns
|
|
63
|
+
},
|
|
60
64
|
});
|
|
61
65
|
|
|
62
66
|
// Verify the message
|
|
@@ -76,7 +80,7 @@ export class JJTreeState {
|
|
|
76
80
|
/**
|
|
77
81
|
* Read tree_state file
|
|
78
82
|
*
|
|
79
|
-
* @returns {
|
|
83
|
+
* @returns {Promise<Record<string, any>>} Decoded tree state data
|
|
80
84
|
*/
|
|
81
85
|
async readTreeState() {
|
|
82
86
|
const treeStatePath = `${this.dir}/.jj/working_copy/tree_state`;
|
|
@@ -87,14 +91,14 @@ export class JJTreeState {
|
|
|
87
91
|
const TreeState = root.lookupType('local_working_copy.TreeState');
|
|
88
92
|
|
|
89
93
|
// Decode
|
|
90
|
-
const message = TreeState.decode(buffer);
|
|
94
|
+
const message = /** @type {any} */ (TreeState.decode(buffer));
|
|
91
95
|
|
|
92
96
|
// Return the decoded message with proper field access
|
|
93
97
|
return {
|
|
94
98
|
tree_ids: message.treeIds,
|
|
95
99
|
file_states: message.fileStates,
|
|
96
100
|
is_file_states_sorted: message.isFileStatesSorted,
|
|
97
|
-
sparse_patterns: message.sparsePatterns
|
|
101
|
+
sparse_patterns: message.sparsePatterns,
|
|
98
102
|
};
|
|
99
103
|
}
|
|
100
104
|
}
|
|
@@ -19,6 +19,10 @@ const protoDir = inDist
|
|
|
19
19
|
: path.join(__dirname, '..', 'protos');
|
|
20
20
|
|
|
21
21
|
export class JJViewStore {
|
|
22
|
+
/**
|
|
23
|
+
* @param {any} fs - File system module (isomorphic-git compatible)
|
|
24
|
+
* @param {any} dir - Repository directory path
|
|
25
|
+
*/
|
|
22
26
|
constructor(fs, dir) {
|
|
23
27
|
this.fs = fs;
|
|
24
28
|
this.dir = dir;
|
|
@@ -38,9 +42,10 @@ export class JJViewStore {
|
|
|
38
42
|
const View = root.lookupType('simple_op_store.View');
|
|
39
43
|
|
|
40
44
|
// Convert hex IDs to bytes
|
|
41
|
-
const headBuffers = headIds.map(id => Buffer.from(id, 'hex'));
|
|
45
|
+
const headBuffers = headIds.map((id) => Buffer.from(id, 'hex'));
|
|
42
46
|
|
|
43
47
|
// Convert wcCommitIds object to map
|
|
48
|
+
/** @type {Record<string, any>} */
|
|
44
49
|
const wcCommitIdsMap = {};
|
|
45
50
|
for (const [workspace, commitId] of Object.entries(wcCommitIds)) {
|
|
46
51
|
wcCommitIdsMap[workspace] = Buffer.from(commitId, 'hex');
|
|
@@ -55,7 +60,7 @@ export class JJViewStore {
|
|
|
55
60
|
remoteViews: [],
|
|
56
61
|
gitRefs: [],
|
|
57
62
|
gitHead: null,
|
|
58
|
-
hasGitRefsMigratedToRemoteTags: false
|
|
63
|
+
hasGitRefsMigratedToRemoteTags: false,
|
|
59
64
|
});
|
|
60
65
|
|
|
61
66
|
// Verify the message
|
|
@@ -76,7 +81,7 @@ export class JJViewStore {
|
|
|
76
81
|
* Read view file
|
|
77
82
|
*
|
|
78
83
|
* @param {string} viewId - View ID as hex string (128 characters)
|
|
79
|
-
* @returns {
|
|
84
|
+
* @returns {Promise<Record<string, any>>} Decoded view data
|
|
80
85
|
*/
|
|
81
86
|
async readView(viewId) {
|
|
82
87
|
// Read from .jj/repo/op_store/views/VIEWID
|
|
@@ -88,9 +93,10 @@ export class JJViewStore {
|
|
|
88
93
|
const View = root.lookupType('simple_op_store.View');
|
|
89
94
|
|
|
90
95
|
// Decode
|
|
91
|
-
const message = View.decode(buffer);
|
|
96
|
+
const message = /** @type {any} */ (View.decode(buffer));
|
|
92
97
|
|
|
93
98
|
// Convert wcCommitIds map back to object
|
|
99
|
+
/** @type {Record<string, any>} */
|
|
94
100
|
const wcCommitIds = {};
|
|
95
101
|
for (const [workspace, commitIdBuffer] of Object.entries(message.wcCommitIds || {})) {
|
|
96
102
|
wcCommitIds[workspace] = commitIdBuffer;
|
|
@@ -105,7 +111,7 @@ export class JJViewStore {
|
|
|
105
111
|
remote_views: message.remoteViews,
|
|
106
112
|
git_refs: message.gitRefs,
|
|
107
113
|
git_head: message.gitHead,
|
|
108
|
-
has_git_refs_migrated_to_remote_tags: message.hasGitRefsMigratedToRemoteTags
|
|
114
|
+
has_git_refs_migrated_to_remote_tags: message.hasGitRefsMigratedToRemoteTags,
|
|
109
115
|
};
|
|
110
116
|
}
|
|
111
117
|
}
|
|
@@ -21,12 +21,15 @@ function matchesPattern(path, pattern) {
|
|
|
21
21
|
|
|
22
22
|
// Convert glob pattern to regex
|
|
23
23
|
// Support: *, **, ?, [abc]
|
|
24
|
+
// Literal dots must be escaped BEFORE the ** -> '.*' substitution, otherwise
|
|
25
|
+
// the dot-escaping pass corrupts the inserted '.*' into the literal '\.*'
|
|
26
|
+
// (zero-or-more dots) instead of "any characters".
|
|
24
27
|
const regexPattern = pattern
|
|
28
|
+
.replace(/\./g, '\\.')
|
|
25
29
|
.replace(/\*\*/g, '__DOUBLESTAR__')
|
|
26
30
|
.replace(/\*/g, '[^/]*')
|
|
27
31
|
.replace(/__DOUBLESTAR__/g, '.*')
|
|
28
|
-
.replace(/\?/g, '[^/]')
|
|
29
|
-
.replace(/\./g, '\\.');
|
|
32
|
+
.replace(/\?/g, '[^/]');
|
|
30
33
|
|
|
31
34
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
32
35
|
return regex.test(path);
|
|
@@ -46,12 +49,10 @@ function isBinaryContent(content) {
|
|
|
46
49
|
if (content.indexOf('\0') !== -1) return true;
|
|
47
50
|
|
|
48
51
|
// Check for high ratio of non-printable characters
|
|
49
|
-
const nonPrintable = content.split('').filter(
|
|
50
|
-
(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
).length;
|
|
52
|
+
const nonPrintable = content.split('').filter((char) => {
|
|
53
|
+
const code = char.charCodeAt(0);
|
|
54
|
+
return code < 32 && code !== 9 && code !== 10 && code !== 13;
|
|
55
|
+
}).length;
|
|
55
56
|
|
|
56
57
|
const ratio = nonPrintable / content.length;
|
|
57
58
|
return ratio > 0.3;
|
|
@@ -60,8 +61,8 @@ function isBinaryContent(content) {
|
|
|
60
61
|
/**
|
|
61
62
|
* Default merge driver - standard three-way text merge
|
|
62
63
|
*
|
|
63
|
-
* @param {
|
|
64
|
-
* @returns {Promise<
|
|
64
|
+
* @param {{ content: { base?: string, ours?: string, theirs?: string } }} context
|
|
65
|
+
* @returns {Promise<Record<string, any>>}
|
|
65
66
|
*/
|
|
66
67
|
async function defaultMergeDriver(context) {
|
|
67
68
|
const { content } = context;
|
|
@@ -88,10 +89,12 @@ async function defaultMergeDriver(context) {
|
|
|
88
89
|
return {
|
|
89
90
|
content: conflictMarkers,
|
|
90
91
|
hasConflict: true,
|
|
91
|
-
conflicts: [
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
conflicts: [
|
|
93
|
+
{
|
|
94
|
+
type: 'content',
|
|
95
|
+
sides: ['ours', 'theirs'],
|
|
96
|
+
},
|
|
97
|
+
],
|
|
95
98
|
};
|
|
96
99
|
}
|
|
97
100
|
|
|
@@ -102,14 +105,14 @@ async function defaultMergeDriver(context) {
|
|
|
102
105
|
* @param {Object} options - Wrapper options
|
|
103
106
|
* @param {number} [options.timeout=5000] - Timeout in ms
|
|
104
107
|
* @param {boolean} [options.strict=false] - Throw on error instead of falling back
|
|
105
|
-
* @param {
|
|
108
|
+
* @param {any} [options.jj] - JJ instance for event emission
|
|
106
109
|
* @param {string} [options.pattern] - Pattern that matched this driver
|
|
107
110
|
* @returns {Function} Wrapped driver
|
|
108
111
|
*/
|
|
109
112
|
function wrapDriver(driver, options = {}) {
|
|
110
113
|
const { timeout = 5000, strict = false, jj = null, pattern = null } = options;
|
|
111
114
|
|
|
112
|
-
return async (context) => {
|
|
115
|
+
return async (/** @type {any} */ context) => {
|
|
113
116
|
let timeoutId;
|
|
114
117
|
const timeoutPromise = new Promise((_, reject) => {
|
|
115
118
|
timeoutId = setTimeout(() => reject(new Error('Driver timeout')), timeout);
|
|
@@ -120,10 +123,7 @@ function wrapDriver(driver, options = {}) {
|
|
|
120
123
|
});
|
|
121
124
|
|
|
122
125
|
try {
|
|
123
|
-
const result = await Promise.race([
|
|
124
|
-
driver(context),
|
|
125
|
-
timeoutPromise,
|
|
126
|
-
]);
|
|
126
|
+
const result = await Promise.race([driver(context), timeoutPromise]);
|
|
127
127
|
|
|
128
128
|
// Clear timeout if driver completed successfully
|
|
129
129
|
clearTimeout(timeoutId);
|
|
@@ -140,14 +140,16 @@ function wrapDriver(driver, options = {}) {
|
|
|
140
140
|
|
|
141
141
|
// Emit driver:failed event if jj instance available
|
|
142
142
|
if (jj && jj.dispatchEvent) {
|
|
143
|
-
jj.dispatchEvent(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
143
|
+
jj.dispatchEvent(
|
|
144
|
+
new CustomEvent('driver:failed', {
|
|
145
|
+
detail: {
|
|
146
|
+
path: context.path,
|
|
147
|
+
pattern: pattern,
|
|
148
|
+
error: error.message,
|
|
149
|
+
timestamp: new Date().toISOString(),
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
);
|
|
151
153
|
}
|
|
152
154
|
|
|
153
155
|
// In strict mode, throw the error
|
|
@@ -170,16 +172,24 @@ function wrapDriver(driver, options = {}) {
|
|
|
170
172
|
* MergeDriverRegistry manages custom merge drivers
|
|
171
173
|
*/
|
|
172
174
|
export class MergeDriverRegistry {
|
|
175
|
+
/**
|
|
176
|
+
* @param {any} [jj]
|
|
177
|
+
*/
|
|
173
178
|
constructor(jj = null) {
|
|
174
|
-
/** @type {Array<{pattern: string, driver: Function, accepts:
|
|
179
|
+
/** @type {Array<{pattern: string, driver: Function, accepts: any, strict: boolean}>} */
|
|
175
180
|
this.drivers = [];
|
|
181
|
+
/** @type {any} */
|
|
176
182
|
this.jj = jj; // JJ instance for event emission
|
|
183
|
+
// Expose the built-in fallback driver so callers (e.g. ConflictModel) can
|
|
184
|
+
// detect when findDriver() fell through to it rather than resolving a
|
|
185
|
+
// real custom driver, and skip driver execution in that case.
|
|
186
|
+
this.defaultDriver = defaultMergeDriver;
|
|
177
187
|
}
|
|
178
188
|
|
|
179
189
|
/**
|
|
180
190
|
* Register one or more merge drivers
|
|
181
191
|
*
|
|
182
|
-
* @param {
|
|
192
|
+
* @param {Record<string, any>} drivers - Pattern -> driver map
|
|
183
193
|
*
|
|
184
194
|
* @example
|
|
185
195
|
* registry.register({
|
|
@@ -206,11 +216,9 @@ export class MergeDriverRegistry {
|
|
|
206
216
|
}
|
|
207
217
|
|
|
208
218
|
if (typeof driver !== 'function') {
|
|
209
|
-
throw new JJError(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
{ pattern }
|
|
213
|
-
);
|
|
219
|
+
throw new JJError('INVALID_DRIVER', `Driver for pattern "${pattern}" must be a function`, {
|
|
220
|
+
pattern,
|
|
221
|
+
});
|
|
214
222
|
}
|
|
215
223
|
|
|
216
224
|
// Wrap driver with error handling
|
|
@@ -325,7 +333,7 @@ export class MergeDriverRegistry {
|
|
|
325
333
|
* @param {string|Buffer|null} context.theirs - Their content
|
|
326
334
|
* @param {Object} context.metadata - Metadata
|
|
327
335
|
* @param {boolean} isBinary - Whether content is binary
|
|
328
|
-
* @returns {Promise<
|
|
336
|
+
* @returns {Promise<Record<string, any>>}
|
|
329
337
|
*/
|
|
330
338
|
async executeDriver(driver, context, isBinary) {
|
|
331
339
|
const { base, ours, theirs, ...rest } = context;
|
|
@@ -381,11 +389,30 @@ export class MergeDriverRegistry {
|
|
|
381
389
|
isBinaryFile(path, content) {
|
|
382
390
|
// Check by extension first
|
|
383
391
|
const binaryExtensions = [
|
|
384
|
-
'.png',
|
|
385
|
-
'.
|
|
386
|
-
'.
|
|
387
|
-
'.
|
|
388
|
-
'.
|
|
392
|
+
'.png',
|
|
393
|
+
'.jpg',
|
|
394
|
+
'.jpeg',
|
|
395
|
+
'.gif',
|
|
396
|
+
'.bmp',
|
|
397
|
+
'.ico',
|
|
398
|
+
'.zip',
|
|
399
|
+
'.tar',
|
|
400
|
+
'.gz',
|
|
401
|
+
'.bz2',
|
|
402
|
+
'.7z',
|
|
403
|
+
'.pdf',
|
|
404
|
+
'.doc',
|
|
405
|
+
'.docx',
|
|
406
|
+
'.xls',
|
|
407
|
+
'.xlsx',
|
|
408
|
+
'.exe',
|
|
409
|
+
'.dll',
|
|
410
|
+
'.so',
|
|
411
|
+
'.dylib',
|
|
412
|
+
'.mp3',
|
|
413
|
+
'.mp4',
|
|
414
|
+
'.avi',
|
|
415
|
+
'.mov',
|
|
389
416
|
];
|
|
390
417
|
|
|
391
418
|
const ext = path.substring(path.lastIndexOf('.')).toLowerCase();
|
|
@@ -412,11 +439,30 @@ export class MergeDriverRegistry {
|
|
|
412
439
|
*/
|
|
413
440
|
export function isBinaryFile(path, content) {
|
|
414
441
|
const binaryExtensions = [
|
|
415
|
-
'.png',
|
|
416
|
-
'.
|
|
417
|
-
'.
|
|
418
|
-
'.
|
|
419
|
-
'.
|
|
442
|
+
'.png',
|
|
443
|
+
'.jpg',
|
|
444
|
+
'.jpeg',
|
|
445
|
+
'.gif',
|
|
446
|
+
'.bmp',
|
|
447
|
+
'.ico',
|
|
448
|
+
'.zip',
|
|
449
|
+
'.tar',
|
|
450
|
+
'.gz',
|
|
451
|
+
'.bz2',
|
|
452
|
+
'.7z',
|
|
453
|
+
'.pdf',
|
|
454
|
+
'.doc',
|
|
455
|
+
'.docx',
|
|
456
|
+
'.xls',
|
|
457
|
+
'.xlsx',
|
|
458
|
+
'.exe',
|
|
459
|
+
'.dll',
|
|
460
|
+
'.so',
|
|
461
|
+
'.dylib',
|
|
462
|
+
'.mp3',
|
|
463
|
+
'.mp4',
|
|
464
|
+
'.avi',
|
|
465
|
+
'.mov',
|
|
420
466
|
];
|
|
421
467
|
|
|
422
468
|
const ext = path.substring(path.lastIndexOf('.')).toLowerCase();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OperationLog - Manages the append-only operation log
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Records all repository mutations for complete undo/redo and time-travel.
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -13,6 +13,7 @@ export class OperationLog {
|
|
|
13
13
|
*/
|
|
14
14
|
constructor(storage) {
|
|
15
15
|
this.storage = storage;
|
|
16
|
+
/** @type {any[]} */
|
|
16
17
|
this.operations = [];
|
|
17
18
|
this.headOperationId = null;
|
|
18
19
|
}
|
|
@@ -39,9 +40,9 @@ export class OperationLog {
|
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
42
|
* Record a new operation
|
|
42
|
-
*
|
|
43
|
-
* @param {
|
|
44
|
-
* @returns {Promise<
|
|
43
|
+
*
|
|
44
|
+
* @param {Record<string, any>} operation - Operation to record (without id)
|
|
45
|
+
* @returns {Promise<Record<string, any>>} Recorded operation with id
|
|
45
46
|
*/
|
|
46
47
|
async recordOperation(operation) {
|
|
47
48
|
// Ensure operations are loaded
|
|
@@ -76,20 +77,20 @@ export class OperationLog {
|
|
|
76
77
|
|
|
77
78
|
/**
|
|
78
79
|
* Get all operations
|
|
79
|
-
*
|
|
80
|
-
* @returns {Promise<Array
|
|
80
|
+
*
|
|
81
|
+
* @returns {Promise<Array<any>>} Array of all operations (chronological order)
|
|
81
82
|
*/
|
|
82
83
|
async list() {
|
|
83
84
|
if (this.operations.length === 0 && this.headOperationId === null) {
|
|
84
85
|
await this.load();
|
|
85
86
|
}
|
|
86
|
-
|
|
87
|
+
|
|
87
88
|
return this.operations;
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
/**
|
|
91
92
|
* Get the head (latest) operation
|
|
92
|
-
*
|
|
93
|
+
*
|
|
93
94
|
* @returns {Promise<Object|null>} Latest operation or null if log is empty
|
|
94
95
|
*/
|
|
95
96
|
async getHeadOperation() {
|
|
@@ -102,10 +103,10 @@ export class OperationLog {
|
|
|
102
103
|
|
|
103
104
|
/**
|
|
104
105
|
* Undo the last operation
|
|
105
|
-
*
|
|
106
|
+
*
|
|
106
107
|
* Returns the view from the previous operation, allowing the caller
|
|
107
108
|
* to restore repository state.
|
|
108
|
-
*
|
|
109
|
+
*
|
|
109
110
|
* @returns {Promise<Object>} View from previous operation
|
|
110
111
|
*/
|
|
111
112
|
async undo() {
|
|
@@ -128,10 +129,10 @@ export class OperationLog {
|
|
|
128
129
|
// Return view from parent operation
|
|
129
130
|
const currentHead = this.operations[this.operations.length - 1];
|
|
130
131
|
const parentId = currentHead.parents[0];
|
|
131
|
-
|
|
132
|
+
|
|
132
133
|
// Find parent operation
|
|
133
|
-
const parentOp = this.operations.find(op => op.id === parentId);
|
|
134
|
-
|
|
134
|
+
const parentOp = this.operations.find((op) => op.id === parentId);
|
|
135
|
+
|
|
135
136
|
if (!parentOp) {
|
|
136
137
|
// If parent not found, return view from operation before current
|
|
137
138
|
return this.operations[this.operations.length - 2].view;
|
|
@@ -142,7 +143,7 @@ export class OperationLog {
|
|
|
142
143
|
|
|
143
144
|
/**
|
|
144
145
|
* Get snapshot (view) at a specific operation
|
|
145
|
-
*
|
|
146
|
+
*
|
|
146
147
|
* @param {string} operationId - Operation ID
|
|
147
148
|
* @returns {Promise<Object>} View from that operation
|
|
148
149
|
*/
|
|
@@ -151,8 +152,8 @@ export class OperationLog {
|
|
|
151
152
|
await this.load();
|
|
152
153
|
}
|
|
153
154
|
|
|
154
|
-
const operation = this.operations.find(op => op.id === operationId);
|
|
155
|
-
|
|
155
|
+
const operation = this.operations.find((op) => op.id === operationId);
|
|
156
|
+
|
|
156
157
|
if (!operation) {
|
|
157
158
|
throw new JJError('OPERATION_NOT_FOUND', `Operation ${operationId} not found`, {
|
|
158
159
|
operationId,
|
|
@@ -174,7 +175,7 @@ export class OperationLog {
|
|
|
174
175
|
await this.load();
|
|
175
176
|
}
|
|
176
177
|
|
|
177
|
-
return this.operations.find(op => op.id === operationId) || null;
|
|
178
|
+
return this.operations.find((op) => op.id === operationId) || null;
|
|
178
179
|
}
|
|
179
180
|
|
|
180
181
|
/**
|
|
@@ -199,7 +200,7 @@ export class OperationLog {
|
|
|
199
200
|
}
|
|
200
201
|
|
|
201
202
|
// Find the operation
|
|
202
|
-
const opIndex = this.operations.findIndex(op => op.id === operationId);
|
|
203
|
+
const opIndex = this.operations.findIndex((op) => op.id === operationId);
|
|
203
204
|
if (opIndex === -1) {
|
|
204
205
|
throw new JJError('OPERATION_NOT_FOUND', `Operation ${operationId} not found`, {
|
|
205
206
|
operationId,
|
|
@@ -217,23 +218,20 @@ export class OperationLog {
|
|
|
217
218
|
}
|
|
218
219
|
|
|
219
220
|
// Find children (operations that reference this as a parent)
|
|
220
|
-
const children = this.operations.filter(op =>
|
|
221
|
-
op.parents && op.parents.includes(operationId)
|
|
222
|
-
);
|
|
221
|
+
const children = this.operations.filter((op) => op.parents && op.parents.includes(operationId));
|
|
223
222
|
|
|
224
223
|
// Get the parent of the operation being abandoned (may be undefined for root)
|
|
225
|
-
const grandparentId =
|
|
226
|
-
? operation.parents[0]
|
|
227
|
-
: undefined;
|
|
224
|
+
const grandparentId =
|
|
225
|
+
operation.parents && operation.parents.length > 0 ? operation.parents[0] : undefined;
|
|
228
226
|
|
|
229
227
|
// Relink children to grandparent
|
|
230
228
|
const relinkedChildren = [];
|
|
231
229
|
for (const child of children) {
|
|
232
230
|
// Replace references to abandoned operation with grandparent
|
|
233
231
|
const oldParents = [...child.parents];
|
|
234
|
-
child.parents = child.parents
|
|
235
|
-
p === operationId ? grandparentId : p
|
|
236
|
-
|
|
232
|
+
child.parents = child.parents
|
|
233
|
+
.map((/** @type {any} */ p) => (p === operationId ? grandparentId : p))
|
|
234
|
+
.filter(Boolean); // Remove undefined if there was no grandparent
|
|
237
235
|
|
|
238
236
|
// If there was no grandparent, the child becomes a root operation
|
|
239
237
|
if (!grandparentId && child.parents.length === 0) {
|
|
@@ -252,15 +250,14 @@ export class OperationLog {
|
|
|
252
250
|
|
|
253
251
|
// Update head if we abandoned the head operation
|
|
254
252
|
if (this.headOperationId === operationId) {
|
|
255
|
-
this.headOperationId =
|
|
256
|
-
? this.operations[this.operations.length - 1].id
|
|
257
|
-
: null;
|
|
253
|
+
this.headOperationId =
|
|
254
|
+
this.operations.length > 0 ? this.operations[this.operations.length - 1].id : null;
|
|
258
255
|
}
|
|
259
256
|
|
|
260
257
|
// Rewrite JSONL file
|
|
261
|
-
const content =
|
|
262
|
-
.map(op => JSON.stringify(op))
|
|
263
|
-
|
|
258
|
+
const content =
|
|
259
|
+
this.operations.map((op) => JSON.stringify(op)).join('\n') +
|
|
260
|
+
(this.operations.length > 0 ? '\n' : '');
|
|
264
261
|
await this.storage.write('repo/op_log/oplog.jsonl', content);
|
|
265
262
|
|
|
266
263
|
return {
|