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.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +971 -0
  3. package/dist/index.cjs +12609 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.mjs +12600 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/package.json +90 -0
  8. package/src/api/repository.js +6885 -0
  9. package/src/backend/isomorphic-git-backend.js +687 -0
  10. package/src/backend/lazy-git-backend.js +180 -0
  11. package/src/browser/helpers.js +189 -0
  12. package/src/browser/index.d.ts +174 -0
  13. package/src/browser/index.js +12 -0
  14. package/src/core/background-ops.js +272 -0
  15. package/src/core/bookmark-store.js +232 -0
  16. package/src/core/change-graph.js +285 -0
  17. package/src/core/conflict-model.js +505 -0
  18. package/src/core/jj-checkout.js +78 -0
  19. package/src/core/jj-operation-store.js +120 -0
  20. package/src/core/jj-tree-state.js +94 -0
  21. package/src/core/jj-view-store.js +105 -0
  22. package/src/core/merge-driver-registry.js +432 -0
  23. package/src/core/operation-log.js +272 -0
  24. package/src/core/revset-engine.js +1081 -0
  25. package/src/core/storage-manager.js +207 -0
  26. package/src/core/tag-store.js +167 -0
  27. package/src/core/user-config.js +142 -0
  28. package/src/core/working-copy.js +314 -0
  29. package/src/core/workspace-manager.js +283 -0
  30. package/src/drivers/built-in-drivers.js +296 -0
  31. package/src/index.js +15 -0
  32. package/src/protos/local_working_copy.proto +76 -0
  33. package/src/protos/simple_op_store.proto +147 -0
  34. package/src/types.d.ts +1129 -0
  35. package/src/utils/errors.js +21 -0
  36. package/src/utils/id-generation.js +54 -0
  37. package/src/utils/validation.js +124 -0
@@ -0,0 +1,272 @@
1
+ /**
2
+ * BackgroundOps - Background operations manager
3
+ *
4
+ * Handles file watching, automatic snapshots, and async operations.
5
+ */
6
+
7
+ import { JJError } from '../utils/errors.js';
8
+
9
+ export class BackgroundOps {
10
+ /**
11
+ * @param {Object} jj - JJ instance
12
+ * @param {Object} fs - Filesystem implementation
13
+ * @param {string} dir - Repository directory
14
+ */
15
+ constructor(jj, fs, dir) {
16
+ this.jj = jj;
17
+ this.fs = fs;
18
+ this.dir = dir;
19
+ this.watchers = new Map();
20
+ this.operations = new Map(); // operationId -> operation
21
+ this.timers = new Map(); // watcherId -> timer
22
+ this.running = false;
23
+ }
24
+
25
+ /**
26
+ * Start background operations
27
+ */
28
+ async start() {
29
+ if (this.running) {
30
+ return;
31
+ }
32
+
33
+ this.running = true;
34
+ }
35
+
36
+ /**
37
+ * Stop background operations
38
+ */
39
+ async stop() {
40
+ if (!this.running) {
41
+ return;
42
+ }
43
+
44
+ // Clear all timers
45
+ for (const timer of this.timers.values()) {
46
+ clearTimeout(timer);
47
+ }
48
+ this.timers.clear();
49
+
50
+ // Stop all watchers
51
+ for (const watcher of this.watchers.values()) {
52
+ if (watcher.close) {
53
+ watcher.close();
54
+ }
55
+ }
56
+
57
+ this.watchers.clear();
58
+ this.running = false;
59
+ }
60
+
61
+ /**
62
+ * Watch a path for changes
63
+ *
64
+ * @param {string} path - Path to watch
65
+ * @param {Function} callback - Callback function(event, filename)
66
+ * @returns {string} Watcher ID
67
+ */
68
+ async watch(path, callback) {
69
+ if (!this.running) {
70
+ throw new JJError('BACKGROUND_OPS_NOT_RUNNING', 'Background operations not started', {
71
+ suggestion: 'Call start() first',
72
+ });
73
+ }
74
+
75
+ // Check if fs.watch is available (Node.js)
76
+ if (this.fs.watch) {
77
+ const watcherId = `watcher-${this.watchers.size}`;
78
+ const watcher = this.fs.watch(path, { recursive: true }, callback);
79
+ this.watchers.set(watcherId, watcher);
80
+ return watcherId;
81
+ }
82
+
83
+ // Browser: use FileSystem Access API observeDirectory if available
84
+ if (typeof FileSystemObserver !== 'undefined') {
85
+ const watcherId = `watcher-${this.watchers.size}`;
86
+ const observer = new FileSystemObserver(async (records) => {
87
+ for (const record of records) {
88
+ callback(record.type, record.relativePathComponents.join('/'));
89
+ }
90
+ });
91
+
92
+ // Note: This API is experimental and may not be available
93
+ this.watchers.set(watcherId, observer);
94
+ return watcherId;
95
+ }
96
+
97
+ throw new JJError('WATCH_NOT_SUPPORTED', 'File watching not supported in this environment');
98
+ }
99
+
100
+ /**
101
+ * Unwatch a path
102
+ *
103
+ * @param {string} watcherId - Watcher ID
104
+ */
105
+ async unwatch(watcherId) {
106
+ const watcher = this.watchers.get(watcherId);
107
+ if (!watcher) {
108
+ return;
109
+ }
110
+
111
+ // Clear any pending timer for this watcher
112
+ const timer = this.timers.get(watcherId);
113
+ if (timer) {
114
+ clearTimeout(timer);
115
+ this.timers.delete(watcherId);
116
+ }
117
+
118
+ if (watcher.close) {
119
+ watcher.close();
120
+ } else if (watcher.disconnect) {
121
+ watcher.disconnect();
122
+ }
123
+
124
+ this.watchers.delete(watcherId);
125
+ }
126
+
127
+ /**
128
+ * Queue a background operation
129
+ *
130
+ * @param {Function} operation - Async operation to run
131
+ * @param {Object} opts - Options
132
+ * @param {string} [opts.description] - Operation description
133
+ * @returns {Promise<Object>} Operation result
134
+ */
135
+ async queue(operation, opts = {}) {
136
+ const operationId = `op-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
137
+
138
+ const op = {
139
+ id: operationId,
140
+ description: opts.description || 'background operation',
141
+ status: 'pending',
142
+ created: new Date().toISOString(),
143
+ promise: null,
144
+ result: null,
145
+ error: null,
146
+ };
147
+
148
+ this.operations.set(operationId, op);
149
+
150
+ // Run operation asynchronously
151
+ op.promise = (async () => {
152
+ try {
153
+ op.status = 'running';
154
+ op.result = await operation();
155
+ op.status = 'completed';
156
+ op.completed = new Date().toISOString();
157
+ return op.result;
158
+ } catch (error) {
159
+ op.status = 'failed';
160
+ op.error = error;
161
+ op.completed = new Date().toISOString();
162
+ throw error;
163
+ }
164
+ })();
165
+
166
+ return {
167
+ id: operationId,
168
+ promise: op.promise,
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Get operation status
174
+ *
175
+ * @param {string} operationId - Operation ID
176
+ * @returns {Object|null} Operation status
177
+ */
178
+ getOperation(operationId) {
179
+ return this.operations.get(operationId) || null;
180
+ }
181
+
182
+ /**
183
+ * List all operations
184
+ *
185
+ * @param {Object} opts - List options
186
+ * @param {string} [opts.status] - Filter by status
187
+ * @returns {Array<Object>} Operations
188
+ */
189
+ listOperations(opts = {}) {
190
+ let ops = Array.from(this.operations.values());
191
+
192
+ if (opts.status) {
193
+ ops = ops.filter(op => op.status === opts.status);
194
+ }
195
+
196
+ return ops.map(op => ({
197
+ id: op.id,
198
+ description: op.description,
199
+ status: op.status,
200
+ created: op.created,
201
+ completed: op.completed,
202
+ error: op.error ? op.error.message : null,
203
+ }));
204
+ }
205
+
206
+ /**
207
+ * Enable auto-snapshot on file changes
208
+ *
209
+ * @param {Object} opts - Options
210
+ * @param {number} [opts.debounceMs=1000] - Debounce delay in ms
211
+ */
212
+ async enableAutoSnapshot(opts = {}) {
213
+ const debounceMs = opts.debounceMs || 1000;
214
+
215
+ const watcherId = await this.watch(this.dir, (event, filename) => {
216
+ if (filename && filename.startsWith('.jj/')) {
217
+ return; // Ignore .jj directory changes
218
+ }
219
+
220
+ if (filename && filename.startsWith('.git/')) {
221
+ return; // Ignore .git directory changes
222
+ }
223
+
224
+ // Debounce: wait for changes to settle
225
+ const existingTimer = this.timers.get(watcherId);
226
+ if (existingTimer) {
227
+ clearTimeout(existingTimer);
228
+ }
229
+
230
+ const timer = setTimeout(async () => {
231
+ try {
232
+ // Create automatic snapshot
233
+ await this.queue(async () => {
234
+ await this.jj.describe({ message: `Auto-snapshot: ${filename || 'multiple files'} changed` });
235
+ }, { description: 'auto-snapshot' });
236
+ } catch (error) {
237
+ console.error('Auto-snapshot failed:', error);
238
+ } finally {
239
+ // Clean up timer reference
240
+ this.timers.delete(watcherId);
241
+ }
242
+ }, debounceMs);
243
+
244
+ // Use unref() to allow process to exit if this is the only pending timer
245
+ if (timer.unref) {
246
+ timer.unref();
247
+ }
248
+
249
+ this.timers.set(watcherId, timer);
250
+ });
251
+
252
+ return watcherId;
253
+ }
254
+
255
+ /**
256
+ * Clean up completed operations
257
+ *
258
+ * @param {number} [maxAge] - Max age in ms (default: 1 hour)
259
+ */
260
+ cleanupOperations(maxAge = 3600000) {
261
+ const cutoff = Date.now() - maxAge;
262
+
263
+ for (const [id, op] of this.operations.entries()) {
264
+ if (op.status === 'completed' || op.status === 'failed') {
265
+ const completedTime = new Date(op.completed).getTime();
266
+ if (completedTime < cutoff) {
267
+ this.operations.delete(id);
268
+ }
269
+ }
270
+ }
271
+ }
272
+ }
@@ -0,0 +1,232 @@
1
+ /**
2
+ * BookmarkStore - Manages local and remote bookmarks
3
+ *
4
+ * Bookmarks are named pointers to changes, similar to Git branches.
5
+ */
6
+
7
+ import { JJError } from '../utils/errors.js';
8
+ import { validateChangeId, validateBookmarkName } from '../utils/validation.js';
9
+
10
+ export class BookmarkStore {
11
+ /**
12
+ * @param {Storage} storage - Storage manager instance
13
+ */
14
+ constructor(storage) {
15
+ this.storage = storage;
16
+ this.local = new Map();
17
+ this.remote = new Map(); // remote → (bookmark → changeId)
18
+ }
19
+
20
+ /**
21
+ * Initialize empty bookmark store
22
+ */
23
+ async init() {
24
+ this.local.clear();
25
+ this.remote.clear();
26
+
27
+ await this.storage.write('repo/store/bookmarks.json', {
28
+ version: 1,
29
+ local: {},
30
+ remote: {},
31
+ tracked: {},
32
+ });
33
+ }
34
+
35
+ /**
36
+ * Load bookmarks from storage
37
+ */
38
+ async load() {
39
+ const data = await this.storage.read('repo/store/bookmarks.json');
40
+
41
+ if (!data) {
42
+ throw new JJError('STORAGE_CORRUPT', 'repo/store/bookmarks.json not found', {
43
+ suggestion: 'Initialize repository with init()',
44
+ });
45
+ }
46
+
47
+ if (data.version !== 1) {
48
+ throw new JJError(
49
+ 'STORAGE_VERSION_MISMATCH',
50
+ `Unsupported bookmarks version: ${data.version}`,
51
+ {
52
+ version: data.version,
53
+ suggestion: 'Upgrade isomorphic-jj or run migration tool',
54
+ }
55
+ );
56
+ }
57
+
58
+ this.local.clear();
59
+ this.remote.clear();
60
+
61
+ // Load local bookmarks
62
+ for (const [name, target] of Object.entries(data.local)) {
63
+ this.local.set(name, target);
64
+ }
65
+
66
+ // Load remote bookmarks
67
+ for (const [remoteName, bookmarks] of Object.entries(data.remote)) {
68
+ const remoteBookmarks = new Map();
69
+ for (const [name, target] of Object.entries(bookmarks)) {
70
+ remoteBookmarks.set(name, target);
71
+ }
72
+ this.remote.set(remoteName, remoteBookmarks);
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Save bookmarks to storage
78
+ */
79
+ async save() {
80
+ const local = {};
81
+ for (const [name, target] of this.local.entries()) {
82
+ local[name] = target;
83
+ }
84
+
85
+ const remote = {};
86
+ for (const [remoteName, bookmarks] of this.remote.entries()) {
87
+ remote[remoteName] = {};
88
+ for (const [name, target] of bookmarks.entries()) {
89
+ remote[remoteName][name] = target;
90
+ }
91
+ }
92
+
93
+ await this.storage.write('repo/store/bookmarks.json', {
94
+ version: 1,
95
+ local,
96
+ remote,
97
+ tracked: {},
98
+ });
99
+ }
100
+
101
+ /**
102
+ * Set a local bookmark
103
+ *
104
+ * @param {string} name - Bookmark name
105
+ * @param {string} target - Change ID to point to
106
+ */
107
+ async set(name, target) {
108
+ validateBookmarkName(name);
109
+ validateChangeId(target);
110
+
111
+ if (this.local.has(name)) {
112
+ throw new JJError('BOOKMARK_EXISTS', `Bookmark ${name} already exists`, {
113
+ name,
114
+ suggestion: 'Use move() to update existing bookmark',
115
+ });
116
+ }
117
+
118
+ this.local.set(name, target);
119
+ await this.save();
120
+ }
121
+
122
+ /**
123
+ * Move an existing local bookmark
124
+ *
125
+ * @param {string} name - Bookmark name
126
+ * @param {string} target - New change ID to point to
127
+ */
128
+ async move(name, target) {
129
+ validateBookmarkName(name);
130
+ validateChangeId(target);
131
+
132
+ if (!this.local.has(name)) {
133
+ throw new JJError('BOOKMARK_NOT_FOUND', `Bookmark ${name} not found`, {
134
+ name,
135
+ suggestion: 'Use set() to create a new bookmark',
136
+ });
137
+ }
138
+
139
+ this.local.set(name, target);
140
+ await this.save();
141
+ }
142
+
143
+ /**
144
+ * Delete a local bookmark
145
+ *
146
+ * @param {string} name - Bookmark name
147
+ */
148
+ async delete(name) {
149
+ validateBookmarkName(name);
150
+
151
+ if (!this.local.has(name)) {
152
+ throw new JJError('BOOKMARK_NOT_FOUND', `Bookmark ${name} not found`, {
153
+ name,
154
+ });
155
+ }
156
+
157
+ this.local.delete(name);
158
+ await this.save();
159
+ }
160
+
161
+ /**
162
+ * Get bookmark target
163
+ *
164
+ * @param {string} name - Bookmark name
165
+ * @returns {Promise<string|null>} Change ID or null if not found
166
+ */
167
+ async get(name) {
168
+ return this.local.get(name) || null;
169
+ }
170
+
171
+ /**
172
+ * Set a remote bookmark
173
+ *
174
+ * @param {string} remoteName - Remote name
175
+ * @param {string} name - Bookmark name
176
+ * @param {string} target - Change ID to point to
177
+ */
178
+ async setRemote(remoteName, name, target) {
179
+ validateBookmarkName(name);
180
+ validateChangeId(target);
181
+
182
+ if (!this.remote.has(remoteName)) {
183
+ this.remote.set(remoteName, new Map());
184
+ }
185
+
186
+ this.remote.get(remoteName).set(name, target);
187
+ await this.save();
188
+ }
189
+
190
+ /**
191
+ * Get remote bookmark target
192
+ *
193
+ * @param {string} remoteName - Remote name
194
+ * @param {string} name - Bookmark name
195
+ * @returns {Promise<string|null>} Change ID or null if not found
196
+ */
197
+ async getRemote(remoteName, name) {
198
+ const remoteBookmarks = this.remote.get(remoteName);
199
+ return remoteBookmarks ? remoteBookmarks.get(name) || null : null;
200
+ }
201
+
202
+ /**
203
+ * List all bookmarks (local and remote)
204
+ *
205
+ * @returns {Promise<Array>} Array of bookmark objects
206
+ */
207
+ async list() {
208
+ const result = [];
209
+
210
+ // Add local bookmarks
211
+ for (const [name, target] of this.local.entries()) {
212
+ result.push({
213
+ name,
214
+ changeId: target,
215
+ remote: null,
216
+ });
217
+ }
218
+
219
+ // Add remote bookmarks
220
+ for (const [remoteName, bookmarks] of this.remote.entries()) {
221
+ for (const [name, target] of bookmarks.entries()) {
222
+ result.push({
223
+ name: `${remoteName}/${name}`,
224
+ changeId: target,
225
+ remote: remoteName,
226
+ });
227
+ }
228
+ }
229
+
230
+ return result;
231
+ }
232
+ }