isomorphic-jj 1.0.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/LICENSE +21 -0
- package/README.md +971 -0
- package/dist/index.cjs +12609 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +12600 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +90 -0
- package/src/api/repository.js +6885 -0
- package/src/backend/isomorphic-git-backend.js +687 -0
- package/src/backend/lazy-git-backend.js +180 -0
- package/src/browser/helpers.js +189 -0
- package/src/browser/index.d.ts +174 -0
- package/src/browser/index.js +12 -0
- package/src/core/background-ops.js +272 -0
- package/src/core/bookmark-store.js +232 -0
- package/src/core/change-graph.js +285 -0
- package/src/core/conflict-model.js +505 -0
- package/src/core/jj-checkout.js +78 -0
- package/src/core/jj-operation-store.js +120 -0
- package/src/core/jj-tree-state.js +94 -0
- package/src/core/jj-view-store.js +105 -0
- package/src/core/merge-driver-registry.js +432 -0
- package/src/core/operation-log.js +272 -0
- package/src/core/revset-engine.js +1081 -0
- package/src/core/storage-manager.js +207 -0
- package/src/core/tag-store.js +167 -0
- package/src/core/user-config.js +142 -0
- package/src/core/working-copy.js +314 -0
- package/src/core/workspace-manager.js +283 -0
- package/src/drivers/built-in-drivers.js +296 -0
- package/src/index.js +15 -0
- package/src/protos/local_working_copy.proto +76 -0
- package/src/protos/simple_op_store.proto +147 -0
- package/src/types.d.ts +1129 -0
- package/src/utils/errors.js +21 -0
- package/src/utils/id-generation.js +54 -0
- package/src/utils/validation.js +124 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChangeGraph - Manages the change graph with stable change IDs
|
|
3
|
+
*
|
|
4
|
+
* Tracks changes, their relationships (parents/children), and evolution (amends/rewrites).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { JJError } from '../utils/errors.js';
|
|
8
|
+
import { validateChangeId } from '../utils/validation.js';
|
|
9
|
+
|
|
10
|
+
export class ChangeGraph {
|
|
11
|
+
/**
|
|
12
|
+
* @param {Storage} storage - Storage manager instance
|
|
13
|
+
*/
|
|
14
|
+
constructor(storage) {
|
|
15
|
+
this.storage = storage;
|
|
16
|
+
this.nodes = new Map(); // changeId → Change
|
|
17
|
+
this.commitIndex = new Map(); // commitId → changeId
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Initialize empty graph
|
|
22
|
+
*/
|
|
23
|
+
async init() {
|
|
24
|
+
this.nodes.clear();
|
|
25
|
+
this.commitIndex.clear();
|
|
26
|
+
|
|
27
|
+
await this.storage.write('repo/store/graph.json', {
|
|
28
|
+
version: 1,
|
|
29
|
+
changes: {},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Load graph from storage
|
|
35
|
+
*/
|
|
36
|
+
async load() {
|
|
37
|
+
const data = await this.storage.read('repo/store/graph.json');
|
|
38
|
+
|
|
39
|
+
if (!data) {
|
|
40
|
+
throw new JJError('STORAGE_CORRUPT', 'repo/store/graph.json not found', {
|
|
41
|
+
suggestion: 'Initialize repository with init() or check .jj directory',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (data.version !== 1) {
|
|
46
|
+
throw new JJError('STORAGE_VERSION_MISMATCH', `Unsupported graph version: ${data.version}`, {
|
|
47
|
+
version: data.version,
|
|
48
|
+
suggestion: 'Upgrade isomorphic-jj or run migration tool',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.nodes.clear();
|
|
53
|
+
this.commitIndex.clear();
|
|
54
|
+
|
|
55
|
+
for (const [changeId, change] of Object.entries(data.changes)) {
|
|
56
|
+
this.nodes.set(changeId, change);
|
|
57
|
+
this.commitIndex.set(change.commitId, changeId);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Save graph to storage
|
|
63
|
+
*/
|
|
64
|
+
async save() {
|
|
65
|
+
const changes = {};
|
|
66
|
+
for (const [changeId, change] of this.nodes.entries()) {
|
|
67
|
+
changes[changeId] = change;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await this.storage.write('repo/store/graph.json', {
|
|
71
|
+
version: 1,
|
|
72
|
+
changes,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Add a change to the graph
|
|
78
|
+
*
|
|
79
|
+
* @param {Object} change - Change object
|
|
80
|
+
*/
|
|
81
|
+
async addChange(change) {
|
|
82
|
+
validateChangeId(change.changeId);
|
|
83
|
+
|
|
84
|
+
if (this.nodes.has(change.changeId)) {
|
|
85
|
+
throw new JJError('CHANGE_EXISTS', `Change ${change.changeId} already exists`, {
|
|
86
|
+
changeId: change.changeId,
|
|
87
|
+
suggestion: 'Use evolveChange() to update an existing change',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.nodes.set(change.changeId, change);
|
|
92
|
+
this.commitIndex.set(change.commitId, change.changeId);
|
|
93
|
+
|
|
94
|
+
await this.save();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get a change by ID
|
|
99
|
+
*
|
|
100
|
+
* @param {string} changeId - Change ID
|
|
101
|
+
* @returns {Object|null} Change object or null if not found
|
|
102
|
+
*/
|
|
103
|
+
async getChange(changeId) {
|
|
104
|
+
validateChangeId(changeId);
|
|
105
|
+
return this.nodes.get(changeId) || null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get all changes
|
|
110
|
+
*
|
|
111
|
+
* @returns {Array} Array of all changes
|
|
112
|
+
*/
|
|
113
|
+
getAll() {
|
|
114
|
+
return Array.from(this.nodes.values());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Find change by commit ID
|
|
119
|
+
*
|
|
120
|
+
* @param {string} commitId - Git commit SHA-1
|
|
121
|
+
* @returns {string|null} Change ID or null if not found
|
|
122
|
+
*/
|
|
123
|
+
findByCommitId(commitId) {
|
|
124
|
+
return this.commitIndex.get(commitId) || null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get parent change IDs
|
|
129
|
+
*
|
|
130
|
+
* @param {string} changeId - Change ID
|
|
131
|
+
* @returns {Array<string>} Array of parent change IDs
|
|
132
|
+
*/
|
|
133
|
+
getParents(changeId) {
|
|
134
|
+
const change = this.nodes.get(changeId);
|
|
135
|
+
return change ? change.parents : [];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get child change IDs
|
|
140
|
+
*
|
|
141
|
+
* @param {string} changeId - Change ID
|
|
142
|
+
* @returns {Array<string>} Array of child change IDs
|
|
143
|
+
*/
|
|
144
|
+
getChildren(changeId) {
|
|
145
|
+
const children = [];
|
|
146
|
+
|
|
147
|
+
for (const change of this.nodes.values()) {
|
|
148
|
+
if (change.parents.includes(changeId)) {
|
|
149
|
+
children.push(change.changeId);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return children;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Evolve a change (update commitId, track predecessor)
|
|
158
|
+
*
|
|
159
|
+
* Used when amending or rewriting a change.
|
|
160
|
+
*
|
|
161
|
+
* @param {string} changeId - Change ID
|
|
162
|
+
* @param {string} newCommitId - New commit SHA-1
|
|
163
|
+
*/
|
|
164
|
+
async evolveChange(changeId, newCommitId) {
|
|
165
|
+
validateChangeId(changeId);
|
|
166
|
+
|
|
167
|
+
const change = this.nodes.get(changeId);
|
|
168
|
+
if (!change) {
|
|
169
|
+
throw new JJError('CHANGE_NOT_FOUND', `Change ${changeId} not found`, {
|
|
170
|
+
changeId,
|
|
171
|
+
suggestion: 'Check change ID or use log() to see available changes',
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Track predecessor
|
|
176
|
+
if (!change.predecessors) {
|
|
177
|
+
change.predecessors = [];
|
|
178
|
+
}
|
|
179
|
+
change.predecessors.push(change.commitId);
|
|
180
|
+
|
|
181
|
+
// Update commit index
|
|
182
|
+
this.commitIndex.delete(change.commitId);
|
|
183
|
+
change.commitId = newCommitId;
|
|
184
|
+
this.commitIndex.set(newCommitId, changeId);
|
|
185
|
+
|
|
186
|
+
await this.save();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Update a change (modify in place)
|
|
191
|
+
*
|
|
192
|
+
* @param {Object} change - Updated change object
|
|
193
|
+
*/
|
|
194
|
+
async updateChange(change) {
|
|
195
|
+
validateChangeId(change.changeId);
|
|
196
|
+
|
|
197
|
+
if (!this.nodes.has(change.changeId)) {
|
|
198
|
+
throw new JJError('CHANGE_NOT_FOUND', `Change ${change.changeId} not found`, {
|
|
199
|
+
changeId: change.changeId,
|
|
200
|
+
suggestion: 'Use addChange() to create a new change',
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
this.nodes.set(change.changeId, change);
|
|
205
|
+
await this.save();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get all ancestors of a change (recursive parent traversal)
|
|
210
|
+
*
|
|
211
|
+
* @param {string} changeId - Starting change ID
|
|
212
|
+
* @returns {Array<string>} Array of ancestor change IDs (breadth-first order)
|
|
213
|
+
*/
|
|
214
|
+
getAncestors(changeId) {
|
|
215
|
+
const ancestors = [];
|
|
216
|
+
const visited = new Set();
|
|
217
|
+
const queue = [changeId];
|
|
218
|
+
|
|
219
|
+
while (queue.length > 0) {
|
|
220
|
+
const current = queue.shift();
|
|
221
|
+
|
|
222
|
+
if (visited.has(current)) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
visited.add(current);
|
|
226
|
+
|
|
227
|
+
const parents = this.getParents(current);
|
|
228
|
+
for (const parent of parents) {
|
|
229
|
+
if (!visited.has(parent)) {
|
|
230
|
+
ancestors.push(parent);
|
|
231
|
+
queue.push(parent);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return ancestors;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Create a change with default values (helper for tests)
|
|
241
|
+
*
|
|
242
|
+
* @param {Object} params - Change parameters
|
|
243
|
+
* @param {string[]} [params.parents] - Parent change IDs
|
|
244
|
+
* @param {string} [params.description] - Change description
|
|
245
|
+
* @param {Object} [params.fileSnapshot] - File snapshot
|
|
246
|
+
* @param {Object} [params.conflicts] - Conflict markers
|
|
247
|
+
* @returns {Promise<Object>} Created change object
|
|
248
|
+
*/
|
|
249
|
+
async createChange(params = {}) {
|
|
250
|
+
const crypto = await import('crypto');
|
|
251
|
+
const changeId = params.changeId || crypto.randomBytes(16).toString('hex');
|
|
252
|
+
const commitId = params.commitId || crypto.randomBytes(20).toString('hex');
|
|
253
|
+
const tree = params.tree || crypto.randomBytes(20).toString('hex');
|
|
254
|
+
const timestamp = params.timestamp || new Date().toISOString();
|
|
255
|
+
|
|
256
|
+
const change = {
|
|
257
|
+
changeId,
|
|
258
|
+
parents: params.parents || [],
|
|
259
|
+
description: params.description || '',
|
|
260
|
+
fileSnapshot: params.fileSnapshot || {},
|
|
261
|
+
commitId,
|
|
262
|
+
tree,
|
|
263
|
+
author: params.author || {
|
|
264
|
+
name: 'Test User',
|
|
265
|
+
email: 'test@example.com',
|
|
266
|
+
timestamp,
|
|
267
|
+
},
|
|
268
|
+
committer: params.committer || {
|
|
269
|
+
name: 'Test User',
|
|
270
|
+
email: 'test@example.com',
|
|
271
|
+
timestamp,
|
|
272
|
+
},
|
|
273
|
+
timestamp,
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// Add conflicts if provided
|
|
277
|
+
if (params.conflicts) {
|
|
278
|
+
change.conflicts = params.conflicts;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
await this.addChange(change);
|
|
282
|
+
|
|
283
|
+
return change;
|
|
284
|
+
}
|
|
285
|
+
}
|