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,207 @@
1
+ /**
2
+ * Storage Manager for isomorphic-jj
3
+ *
4
+ * Manages JSON storage in .jj directory with atomic writes and caching.
5
+ */
6
+
7
+ import { JJError } from '../utils/errors.js';
8
+
9
+ export class Storage {
10
+ /**
11
+ * @param {Object} fs - Filesystem implementation (Node fs, LightningFS, etc.)
12
+ * @param {string} dir - Repository directory path
13
+ */
14
+ constructor(fs, dir) {
15
+ this.fs = fs;
16
+ this.dir = dir;
17
+ this.jjDir = `${dir}/.jj`;
18
+ this.repoDir = `${dir}/.jj/repo`; // Core repo data
19
+ this.workingCopyDir = `${dir}/.jj/working_copy`; // Default workspace working copy
20
+ this.cache = new Map();
21
+ }
22
+
23
+ /**
24
+ * Initialize .jj directory structure (JJ workspace model)
25
+ *
26
+ * Structure:
27
+ * .jj/
28
+ * repo/ - Core repository data (shared by all workspaces)
29
+ * store/ - Change graph, operations, bookmarks
30
+ * working_copy/ - Default workspace working copy state
31
+ */
32
+ async init() {
33
+ try {
34
+ // Create main directories
35
+ await this.fs.promises.mkdir(this.jjDir, { recursive: true });
36
+ await this.fs.promises.mkdir(this.repoDir, { recursive: true });
37
+ await this.fs.promises.mkdir(this.workingCopyDir, { recursive: true });
38
+
39
+ // Create repo subdirectories
40
+ await this.fs.promises.mkdir(`${this.repoDir}/store`, { recursive: true });
41
+ await this.fs.promises.mkdir(`${this.repoDir}/op_log`, { recursive: true });
42
+ await this.fs.promises.mkdir(`${this.repoDir}/conflicts`, { recursive: true });
43
+
44
+ // Create default workspace directory
45
+ await this.fs.promises.mkdir(`${this.workingCopyDir}/default`, { recursive: true });
46
+ } catch (error) {
47
+ throw new JJError('STORAGE_INIT_FAILED', `Failed to initialize .jj directory: ${error.message}`, {
48
+ dir: this.jjDir,
49
+ originalError: error,
50
+ });
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Read and parse JSON file
56
+ *
57
+ * @param {string} path - Relative path from .jj directory
58
+ * @returns {Promise<Object|null>} Parsed JSON or null if file doesn't exist
59
+ */
60
+ async read(path) {
61
+ const fullPath = `${this.jjDir}/${path}`;
62
+
63
+ // Check cache
64
+ if (this.cache.has(path)) {
65
+ return this.cache.get(path);
66
+ }
67
+
68
+ try {
69
+ const content = await this.fs.promises.readFile(fullPath, 'utf8');
70
+ const data = JSON.parse(content);
71
+ this.cache.set(path, data);
72
+ return data;
73
+ } catch (error) {
74
+ if (error.code === 'ENOENT') {
75
+ return null;
76
+ }
77
+ throw new JJError('STORAGE_READ_FAILED', `Failed to read ${path}: ${error.message}`, {
78
+ path: fullPath,
79
+ originalError: error,
80
+ });
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Write JSON file atomically
86
+ *
87
+ * @param {string} path - Relative path from .jj directory
88
+ * @param {Object|string} data - Data to write (object will be stringified)
89
+ */
90
+ async write(path, data) {
91
+ const fullPath = `${this.jjDir}/${path}`;
92
+ const tmpPath = `${fullPath}.tmp.${Date.now()}`;
93
+ const jsonData = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
94
+
95
+ try {
96
+ // Ensure parent directory exists
97
+ const dirPath = fullPath.substring(0, fullPath.lastIndexOf('/'));
98
+ await this.fs.promises.mkdir(dirPath, { recursive: true });
99
+
100
+ // Write to temp file
101
+ await this.fs.promises.writeFile(tmpPath, jsonData, 'utf8');
102
+
103
+ // Atomic rename
104
+ await this.fs.promises.rename(tmpPath, fullPath);
105
+
106
+ // Update cache
107
+ if (typeof data !== 'string') {
108
+ this.cache.set(path, data);
109
+ }
110
+ } catch (error) {
111
+ // Clean up temp file
112
+ try {
113
+ await this.fs.promises.unlink(tmpPath);
114
+ } catch {}
115
+
116
+ throw new JJError('STORAGE_WRITE_FAILED', `Failed to write ${path}: ${error.message}`, {
117
+ path: fullPath,
118
+ originalError: error,
119
+ });
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Read JSONL file (newline-delimited JSON)
125
+ *
126
+ * @param {string} path - Relative path from .jj directory
127
+ * @returns {Promise<Array>} Array of parsed JSON objects
128
+ */
129
+ async readLines(path) {
130
+ const fullPath = `${this.jjDir}/${path}`;
131
+
132
+ try {
133
+ const content = await this.fs.promises.readFile(fullPath, 'utf8');
134
+ return content
135
+ .trim()
136
+ .split('\n')
137
+ .filter((line) => line.length > 0)
138
+ .map((line) => JSON.parse(line));
139
+ } catch (error) {
140
+ if (error.code === 'ENOENT') {
141
+ return [];
142
+ }
143
+ throw new JJError('STORAGE_READ_FAILED', `Failed to read ${path}: ${error.message}`, {
144
+ path: fullPath,
145
+ originalError: error,
146
+ });
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Append line to JSONL file
152
+ *
153
+ * @param {string} path - Relative path from .jj directory
154
+ * @param {string} line - JSON string to append
155
+ */
156
+ async appendLine(path, line) {
157
+ const fullPath = `${this.jjDir}/${path}`;
158
+
159
+ try {
160
+ // Read existing content
161
+ let existing = '';
162
+ try {
163
+ existing = await this.fs.promises.readFile(fullPath, 'utf8');
164
+ } catch (error) {
165
+ if (error.code !== 'ENOENT') throw error;
166
+ }
167
+
168
+ // Write atomically
169
+ await this.write(path, existing + line + '\n');
170
+ } catch (error) {
171
+ throw new JJError('STORAGE_WRITE_FAILED', `Failed to append to ${path}: ${error.message}`, {
172
+ path: fullPath,
173
+ originalError: error,
174
+ });
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Check if file exists
180
+ *
181
+ * @param {string} path - Relative path from .jj directory
182
+ * @returns {Promise<boolean>}
183
+ */
184
+ async exists(path) {
185
+ const fullPath = `${this.jjDir}/${path}`;
186
+
187
+ try {
188
+ await this.fs.promises.stat(fullPath);
189
+ return true;
190
+ } catch {
191
+ return false;
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Invalidate cache
197
+ *
198
+ * @param {string} [path] - Specific path to invalidate, or all if not provided
199
+ */
200
+ invalidateCache(path) {
201
+ if (path) {
202
+ this.cache.delete(path);
203
+ } else {
204
+ this.cache.clear();
205
+ }
206
+ }
207
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Tag Store - Manages immutable references to changes
3
+ *
4
+ * Tags are immutable references that cannot be moved once created.
5
+ * They are typically used for release points and important milestones.
6
+ */
7
+
8
+ import { JJError } from '../utils/errors.js';
9
+
10
+ /**
11
+ * Validates tag name format
12
+ * @param {string} name - Tag name to validate
13
+ * @throws {JJError} If name is invalid
14
+ */
15
+ function validateTagName(name) {
16
+ if (!name || typeof name !== 'string') {
17
+ throw new JJError('INVALID_TAG_NAME', 'Tag name must be a non-empty string');
18
+ }
19
+
20
+ if (name.trim() !== name || name.includes(' ')) {
21
+ throw new JJError(
22
+ 'INVALID_TAG_NAME',
23
+ 'Tag name cannot contain spaces or leading/trailing whitespace',
24
+ { name }
25
+ );
26
+ }
27
+
28
+ if (name.length === 0) {
29
+ throw new JJError('INVALID_TAG_NAME', 'Tag name cannot be empty');
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Simple tag pattern matching (supports glob-like patterns)
35
+ * @param {string} name - Tag name
36
+ * @param {string} pattern - Pattern (e.g., "v1*", "*-rc")
37
+ * @returns {boolean} True if name matches pattern
38
+ */
39
+ function matchesPattern(name, pattern) {
40
+ // Convert glob pattern to regex
41
+ const regexPattern = pattern
42
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars
43
+ .replace(/\*/g, '.*') // Convert * to .*
44
+ .replace(/\?/g, '.'); // Convert ? to .
45
+
46
+ const regex = new RegExp(`^${regexPattern}$`);
47
+ return regex.test(name);
48
+ }
49
+
50
+ export class TagStore {
51
+ /**
52
+ * @param {Object} fs - Filesystem interface
53
+ * @param {string} jjDir - Path to .jj directory
54
+ */
55
+ constructor(fs, jjDir) {
56
+ this.fs = fs;
57
+ this.jjDir = jjDir;
58
+ this.tagsFile = `${jjDir}/store/tags.json`;
59
+ }
60
+
61
+ /**
62
+ * Loads tags from storage
63
+ * @returns {Promise<Object>} Map of tag name to changeId
64
+ */
65
+ async load() {
66
+ try {
67
+ const data = await this.fs.readFile(this.tagsFile, 'utf8');
68
+ return JSON.parse(data);
69
+ } catch (error) {
70
+ if (error.code === 'ENOENT') {
71
+ return {};
72
+ }
73
+ throw error;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Saves tags to storage
79
+ * @param {Object} tags - Map of tag name to changeId
80
+ * @returns {Promise<void>}
81
+ */
82
+ async save(tags) {
83
+ await this.fs.writeFile(this.tagsFile, JSON.stringify(tags, null, 2));
84
+ }
85
+
86
+ /**
87
+ * Creates a new tag
88
+ * @param {string} name - Tag name
89
+ * @param {string} changeId - Change ID to tag
90
+ * @returns {Promise<{name: string, changeId: string}>}
91
+ * @throws {JJError} If tag already exists or name is invalid
92
+ */
93
+ async create(name, changeId) {
94
+ validateTagName(name);
95
+
96
+ const tags = await this.load();
97
+
98
+ if (tags[name]) {
99
+ throw new JJError('TAG_EXISTS', `Tag "${name}" already exists`, {
100
+ name,
101
+ existingChangeId: tags[name],
102
+ });
103
+ }
104
+
105
+ tags[name] = changeId;
106
+ await this.save(tags);
107
+
108
+ return { name, changeId };
109
+ }
110
+
111
+ /**
112
+ * Lists all tags, optionally filtered by pattern
113
+ * @param {string} [pattern] - Optional glob pattern
114
+ * @returns {Promise<Array<{name: string, changeId: string}>>}
115
+ */
116
+ async list(pattern) {
117
+ const tags = await this.load();
118
+
119
+ let tagNames = Object.keys(tags);
120
+
121
+ if (pattern) {
122
+ tagNames = tagNames.filter((name) => matchesPattern(name, pattern));
123
+ }
124
+
125
+ return tagNames.map((name) => ({
126
+ name,
127
+ changeId: tags[name],
128
+ }));
129
+ }
130
+
131
+ /**
132
+ * Deletes a tag
133
+ * @param {string} name - Tag name to delete
134
+ * @returns {Promise<void>}
135
+ * @throws {JJError} If tag doesn't exist
136
+ */
137
+ async delete(name) {
138
+ const tags = await this.load();
139
+
140
+ if (!tags[name]) {
141
+ throw new JJError('TAG_NOT_FOUND', `Tag "${name}" not found`, { name });
142
+ }
143
+
144
+ delete tags[name];
145
+ await this.save(tags);
146
+ }
147
+
148
+ /**
149
+ * Checks if a tag exists
150
+ * @param {string} name - Tag name
151
+ * @returns {Promise<boolean>}
152
+ */
153
+ async exists(name) {
154
+ const tags = await this.load();
155
+ return !!tags[name];
156
+ }
157
+
158
+ /**
159
+ * Gets the changeId for a tag
160
+ * @param {string} name - Tag name
161
+ * @returns {Promise<string|null>} Change ID or null if not found
162
+ */
163
+ async get(name) {
164
+ const tags = await this.load();
165
+ return tags[name] || null;
166
+ }
167
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * UserConfig - Manages user configuration (name, email, etc.)
3
+ *
4
+ * In real JJ, this would come from:
5
+ * - ~/.jjconfig.toml (global config)
6
+ * - .jj/repo/config.toml (repo config)
7
+ * - Environment variables
8
+ *
9
+ * For now, we store it in the repository.
10
+ */
11
+
12
+ export class UserConfig {
13
+ /**
14
+ * @param {Storage} storage - Storage manager instance
15
+ */
16
+ constructor(storage) {
17
+ this.storage = storage;
18
+ this.config = null;
19
+ }
20
+
21
+ /**
22
+ * Initialize with default config
23
+ *
24
+ * @param {Object} opts - Initial config
25
+ * @param {string} [opts.userName='User'] - User name
26
+ * @param {string} [opts.userEmail='user@example.com'] - User email
27
+ */
28
+ async init(opts = {}) {
29
+ this.config = {
30
+ user: {
31
+ name: opts.userName || 'User',
32
+ email: opts.userEmail || 'user@example.com',
33
+ },
34
+ };
35
+
36
+ await this.save();
37
+ }
38
+
39
+ /**
40
+ * Load config from storage
41
+ */
42
+ async load() {
43
+ try {
44
+ const data = await this.storage.read('config.json');
45
+ if (data) {
46
+ this.config = data;
47
+ }
48
+ } catch (error) {
49
+ // No config file yet, use defaults
50
+ await this.init();
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Save config to storage
56
+ */
57
+ async save() {
58
+ await this.storage.write('config.json', this.config);
59
+ }
60
+
61
+ /**
62
+ * Get user info
63
+ *
64
+ * @returns {Object} User info { name, email }
65
+ */
66
+ getUser() {
67
+ if (!this.config) {
68
+ return { name: 'User', email: 'user@example.com' };
69
+ }
70
+ return this.config.user;
71
+ }
72
+
73
+ /**
74
+ * Set user info
75
+ *
76
+ * @param {Object} user - User info
77
+ * @param {string} user.name - User name
78
+ * @param {string} user.email - User email
79
+ */
80
+ async setUser(user) {
81
+ if (!this.config) {
82
+ await this.load();
83
+ }
84
+
85
+ this.config.user = {
86
+ name: user.name,
87
+ email: user.email,
88
+ };
89
+
90
+ await this.save();
91
+ }
92
+
93
+ /**
94
+ * Get config value
95
+ *
96
+ * @param {string} key - Config key (dot notation supported)
97
+ * @returns {*} Config value
98
+ */
99
+ get(key) {
100
+ if (!this.config) {
101
+ return undefined;
102
+ }
103
+
104
+ const parts = key.split('.');
105
+ let value = this.config;
106
+
107
+ for (const part of parts) {
108
+ value = value?.[part];
109
+ if (value === undefined) {
110
+ return undefined;
111
+ }
112
+ }
113
+
114
+ return value;
115
+ }
116
+
117
+ /**
118
+ * Set config value
119
+ *
120
+ * @param {string} key - Config key (dot notation supported)
121
+ * @param {*} value - Config value
122
+ */
123
+ async set(key, value) {
124
+ if (!this.config) {
125
+ await this.load();
126
+ }
127
+
128
+ const parts = key.split('.');
129
+ const lastPart = parts.pop();
130
+ let target = this.config;
131
+
132
+ for (const part of parts) {
133
+ if (!target[part]) {
134
+ target[part] = {};
135
+ }
136
+ target = target[part];
137
+ }
138
+
139
+ target[lastPart] = value;
140
+ await this.save();
141
+ }
142
+ }