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,687 @@
1
+ /**
2
+ * isomorphic-git Backend Adapter
3
+ *
4
+ * Provides real Git object storage using isomorphic-git library.
5
+ * Implements the JJBackend interface for Git interoperability.
6
+ */
7
+
8
+ import git from 'isomorphic-git';
9
+ import { JJError } from '../utils/errors.js';
10
+ import { JJCheckout } from '../core/jj-checkout.js';
11
+ import { JJTreeState } from '../core/jj-tree-state.js';
12
+ import { JJOperationStore } from '../core/jj-operation-store.js';
13
+ import { JJViewStore } from '../core/jj-view-store.js';
14
+ import crypto from 'crypto';
15
+
16
+ /**
17
+ * Backend adapter for isomorphic-git
18
+ *
19
+ * @class IsomorphicGitBackend
20
+ */
21
+ export class IsomorphicGitBackend {
22
+ /**
23
+ * Create isomorphic-git backend
24
+ *
25
+ * @param {Object} options - Backend options
26
+ * @param {Object} options.fs - Filesystem implementation (Node fs or LightningFS)
27
+ * @param {Object} [options.http] - HTTP client for network operations
28
+ * @param {string} options.dir - Repository directory path
29
+ */
30
+ constructor({ fs, http, dir }) {
31
+ if (!fs) {
32
+ throw new JJError(
33
+ 'INVALID_BACKEND_CONFIG',
34
+ 'Filesystem (fs) is required for isomorphic-git backend',
35
+ { suggestion: 'Provide fs option (Node fs or LightningFS)' }
36
+ );
37
+ }
38
+ if (!dir) {
39
+ throw new JJError(
40
+ 'INVALID_BACKEND_CONFIG',
41
+ 'Repository directory (dir) is required',
42
+ { suggestion: 'Provide dir option with repository path' }
43
+ );
44
+ }
45
+
46
+ this.fs = fs;
47
+ this.http = http;
48
+ this.dir = dir;
49
+ }
50
+
51
+ /**
52
+ * Initialize Git repository
53
+ *
54
+ * @param {Object} opts - Init options
55
+ * @param {string} [opts.defaultBranch='main'] - Default branch name
56
+ * @returns {Promise<void>}
57
+ */
58
+ async init(opts = {}) {
59
+ try {
60
+ await git.init({
61
+ fs: this.fs,
62
+ dir: this.dir,
63
+ defaultBranch: opts.defaultBranch || 'main',
64
+ });
65
+
66
+ // Create .jj/repo structure for jj CLI compatibility
67
+ await this._createJJRepoStructure();
68
+
69
+ } catch (error) {
70
+ throw new JJError(
71
+ 'INIT_FAILED',
72
+ `Failed to initialize Git repository: ${error.message}`,
73
+ { dir: this.dir, originalError: error }
74
+ );
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Read Git object from storage
80
+ *
81
+ * @param {string} oid - Git object SHA-1 hash (40-char hex)
82
+ * @returns {Promise<{type: string, data: Uint8Array}>} Git object
83
+ */
84
+ async getObject(oid) {
85
+ try {
86
+ const { type, object } = await git.readObject({
87
+ fs: this.fs,
88
+ dir: this.dir,
89
+ oid,
90
+ });
91
+ return { type, data: object };
92
+ } catch (error) {
93
+ if (error.code === 'NotFoundError' || error.code === 'ReadObjectFail') {
94
+ throw new JJError(
95
+ 'NOT_FOUND',
96
+ `Git object ${oid} not found`,
97
+ { oid, suggestion: 'Ensure object exists in repository' }
98
+ );
99
+ }
100
+ throw new JJError(
101
+ 'STORAGE_READ_FAILED',
102
+ `Failed to read Git object: ${error.message}`,
103
+ { oid, originalError: error }
104
+ );
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Write Git object to storage
110
+ *
111
+ * @param {string} type - Object type ('blob', 'tree', 'commit', 'tag')
112
+ * @param {Uint8Array} data - Object data
113
+ * @returns {Promise<string>} Object SHA-1 hash
114
+ */
115
+ async putObject(type, data) {
116
+ try {
117
+ const oid = await git.writeObject({
118
+ fs: this.fs,
119
+ dir: this.dir,
120
+ type,
121
+ object: data,
122
+ });
123
+ return oid;
124
+ } catch (error) {
125
+ throw new JJError(
126
+ 'STORAGE_WRITE_FAILED',
127
+ `Failed to write Git object: ${error.message}`,
128
+ { type, dataSize: data.length, originalError: error }
129
+ );
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Read Git reference
135
+ *
136
+ * @param {string} name - Full ref name (e.g., 'refs/heads/main')
137
+ * @returns {Promise<string|null>} Commit SHA-1 or null if not found
138
+ */
139
+ async readRef(name) {
140
+ try {
141
+ const oid = await git.resolveRef({
142
+ fs: this.fs,
143
+ dir: this.dir,
144
+ ref: name,
145
+ });
146
+ return oid;
147
+ } catch (error) {
148
+ if (error.code === 'NotFoundError' || error.code === 'ResolveRefError') {
149
+ return null;
150
+ }
151
+ throw new JJError(
152
+ 'STORAGE_READ_FAILED',
153
+ `Failed to read Git ref: ${error.message}`,
154
+ { ref: name, originalError: error }
155
+ );
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Create, update, or delete Git reference
161
+ *
162
+ * @param {string} name - Full ref name
163
+ * @param {string|null} oid - Commit SHA-1 or null to delete
164
+ * @returns {Promise<void>}
165
+ */
166
+ async updateRef(name, oid) {
167
+ try {
168
+ if (oid === null) {
169
+ // Delete ref
170
+ const refPath = `${this.dir}/.git/${name}`;
171
+ try {
172
+ await this.fs.promises.unlink(refPath);
173
+ } catch (error) {
174
+ // Ignore if ref doesn't exist
175
+ if (error.code !== 'ENOENT') {
176
+ throw error;
177
+ }
178
+ }
179
+ } else {
180
+ // Create or update ref
181
+ await git.writeRef({
182
+ fs: this.fs,
183
+ dir: this.dir,
184
+ ref: name,
185
+ value: oid,
186
+ force: true,
187
+ });
188
+ }
189
+ } catch (error) {
190
+ throw new JJError(
191
+ 'STORAGE_WRITE_FAILED',
192
+ `Failed to update Git ref: ${error.message}`,
193
+ { ref: name, oid, originalError: error }
194
+ );
195
+ }
196
+ }
197
+
198
+ /**
199
+ * List Git references
200
+ *
201
+ * @param {string} [prefix=''] - Ref prefix filter
202
+ * @returns {Promise<Array<{name: string, oid: string}>>} List of refs
203
+ */
204
+ async listRefs(prefix = '') {
205
+ try {
206
+ // Use expandRef to get all possible refs
207
+ const result = [];
208
+ const refDirs = ['refs/heads', 'refs/tags', 'refs/remotes'];
209
+
210
+ for (const refDir of refDirs) {
211
+ if (prefix && !refDir.startsWith(prefix) && !prefix.startsWith(refDir)) {
212
+ continue;
213
+ }
214
+
215
+ try {
216
+ const dirPath = `${this.dir}/.git/${refDir}`;
217
+ const entries = await this.fs.promises.readdir(dirPath, { recursive: true });
218
+
219
+ for (const entry of entries) {
220
+ const refName = `${refDir}/${entry}`;
221
+ if (prefix && !refName.startsWith(prefix)) {
222
+ continue;
223
+ }
224
+
225
+ try {
226
+ const oid = await this.readRef(refName);
227
+ if (oid) {
228
+ result.push({ name: refName, oid });
229
+ }
230
+ } catch (e) {
231
+ // Skip refs that can't be read
232
+ }
233
+ }
234
+ } catch (e) {
235
+ // Directory doesn't exist, skip
236
+ }
237
+ }
238
+
239
+ return result.sort((a, b) => a.name.localeCompare(b.name));
240
+ } catch (error) {
241
+ throw new JJError(
242
+ 'STORAGE_READ_FAILED',
243
+ `Failed to list Git refs: ${error.message}`,
244
+ { prefix, originalError: error }
245
+ );
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Create a Git commit from working directory state
251
+ *
252
+ * @param {Object} opts - Commit options
253
+ * @param {string} opts.message - Commit message
254
+ * @param {Object} opts.author - Author info
255
+ * @param {string} opts.author.name - Author name
256
+ * @param {string} opts.author.email - Author email
257
+ * @param {number} [opts.author.timestamp] - Author timestamp (milliseconds since epoch)
258
+ * @param {Object} [opts.committer] - Committer info (defaults to author)
259
+ * @param {string[]} [opts.parents=[]] - Parent commit SHAs
260
+ * @returns {Promise<string>} Commit SHA
261
+ */
262
+ async createCommit(opts) {
263
+ try {
264
+ const author = {
265
+ name: opts.author.name,
266
+ email: opts.author.email,
267
+ timestamp: opts.author.timestamp ? Math.floor(opts.author.timestamp / 1000) : Math.floor(Date.now() / 1000),
268
+ timezoneOffset: 0,
269
+ };
270
+
271
+ const committer = opts.committer ? {
272
+ name: opts.committer.name,
273
+ email: opts.committer.email,
274
+ timestamp: opts.committer.timestamp ? Math.floor(opts.committer.timestamp / 1000) : author.timestamp,
275
+ timezoneOffset: 0,
276
+ } : author;
277
+
278
+ const commitSha = await git.commit({
279
+ fs: this.fs,
280
+ dir: this.dir,
281
+ message: opts.message,
282
+ author,
283
+ committer,
284
+ parent: opts.parents || [],
285
+ });
286
+
287
+ return commitSha;
288
+ } catch (error) {
289
+ throw new JJError(
290
+ 'COMMIT_FAILED',
291
+ `Failed to create Git commit: ${error.message}`,
292
+ { message: opts.message, originalError: error }
293
+ );
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Get the current tree SHA for the working directory
299
+ *
300
+ * @returns {Promise<string>} Tree SHA
301
+ */
302
+ async getCurrentTree() {
303
+ try {
304
+ // Get the current staging area tree
305
+ // This will create tree objects from the current working directory
306
+ const tree = await git.writeTree({
307
+ fs: this.fs,
308
+ dir: this.dir,
309
+ });
310
+ return tree;
311
+ } catch (error) {
312
+ throw new JJError(
313
+ 'TREE_READ_FAILED',
314
+ `Failed to get current tree: ${error.message}`,
315
+ { originalError: error }
316
+ );
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Stage all files in the working directory
322
+ *
323
+ * @returns {Promise<void>}
324
+ */
325
+ async stageAll() {
326
+ try {
327
+ // Get all files in the directory
328
+ const files = await this._getAllFiles(this.dir);
329
+
330
+ // Add each file to the index
331
+ for (const filepath of files) {
332
+ try {
333
+ await git.add({
334
+ fs: this.fs,
335
+ dir: this.dir,
336
+ filepath,
337
+ });
338
+ } catch (error) {
339
+ // Skip files that can't be staged (like .git directory)
340
+ if (error.code !== 'ENOENT') {
341
+ throw new JJError(
342
+ 'STAGE_FILE_FAILED',
343
+ `Could not stage ${filepath}: ${error.message}`,
344
+ { filepath, originalError: error.message }
345
+ );
346
+ }
347
+ }
348
+ }
349
+ } catch (error) {
350
+ throw new JJError(
351
+ 'STAGE_FAILED',
352
+ `Failed to stage files: ${error.message}`,
353
+ { originalError: error }
354
+ );
355
+ }
356
+ }
357
+
358
+ /**
359
+ * Get all files in a directory recursively (helper method)
360
+ *
361
+ * @private
362
+ * @param {string} dir - Directory path
363
+ * @param {string} [baseDir=''] - Base directory for relative paths
364
+ * @returns {Promise<string[]>} Array of relative file paths
365
+ */
366
+ async _getAllFiles(dir, baseDir = '') {
367
+ const files = [];
368
+ const entries = await this.fs.promises.readdir(dir, { withFileTypes: true });
369
+
370
+ for (const entry of entries) {
371
+ const name = entry.name || entry;
372
+ const fullPath = `${dir}/${name}`;
373
+ const relativePath = baseDir ? `${baseDir}/${name}` : name;
374
+
375
+ // Skip .git and .jj directories
376
+ if (name === '.git' || name === '.jj') {
377
+ continue;
378
+ }
379
+
380
+ try {
381
+ const stats = await this.fs.promises.stat(fullPath);
382
+ if (stats.isDirectory && stats.isDirectory()) {
383
+ // Recurse into subdirectories
384
+ const subFiles = await this._getAllFiles(fullPath, relativePath);
385
+ files.push(...subFiles);
386
+ } else if (stats.isFile && stats.isFile()) {
387
+ files.push(relativePath);
388
+ } else {
389
+ // For simple objects without methods
390
+ const isDir = (await this.fs.promises.readdir(fullPath).catch(() => null)) !== null;
391
+ if (isDir) {
392
+ const subFiles = await this._getAllFiles(fullPath, relativePath);
393
+ files.push(...subFiles);
394
+ } else {
395
+ files.push(relativePath);
396
+ }
397
+ }
398
+ } catch (error) {
399
+ // Skip files we can't read
400
+ continue;
401
+ }
402
+ }
403
+
404
+ return files;
405
+ }
406
+
407
+ /**
408
+ * Fetch objects and refs from remote repository
409
+ *
410
+ * @param {Object} opts - Fetch options
411
+ * @param {string} opts.remote - Remote name or URL
412
+ * @param {string[]} [opts.refs] - Ref specs to fetch
413
+ * @param {number} [opts.depth] - Create a shallow clone with history truncated to depth (v0.4)
414
+ * @param {boolean} [opts.relative] - Depth measured from current shallow depth (v0.4)
415
+ * @param {boolean} [opts.singleBranch] - Only fetch single branch (v0.4)
416
+ * @param {boolean} [opts.noTags] - Don't fetch tags (v0.4)
417
+ * @param {Function} [opts.onProgress] - Progress callback
418
+ * @param {Function} [opts.onAuth] - Authentication callback
419
+ * @returns {Promise<{fetchedRefs: Array, updatedRefs: Array}>} Fetch result
420
+ */
421
+ async fetch(opts) {
422
+ if (!this.http) {
423
+ throw new JJError(
424
+ 'NETWORK_NOT_AVAILABLE',
425
+ 'HTTP client not provided - network operations unavailable',
426
+ { suggestion: 'Provide http option when creating backend' }
427
+ );
428
+ }
429
+
430
+ try {
431
+ const fetchedRefs = [];
432
+ const updatedRefs = [];
433
+
434
+ // Fetch each ref spec (isomorphic-git fetches one at a time)
435
+ const refsToFetch = opts.refs || ['HEAD'];
436
+ for (const ref of refsToFetch) {
437
+ await git.fetch({
438
+ fs: this.fs,
439
+ http: this.http,
440
+ dir: this.dir,
441
+ remote: opts.remote,
442
+ ref,
443
+ depth: opts.depth, // v0.4: Shallow clone support
444
+ relative: opts.relative, // v0.4: Relative depth
445
+ singleBranch: opts.singleBranch, // v0.4: Single branch only
446
+ tags: opts.noTags === true ? false : undefined, // v0.4: Skip tags
447
+ onProgress: opts.onProgress,
448
+ onAuth: opts.onAuth,
449
+ });
450
+
451
+ // Track fetched ref
452
+ const remoteRef = `refs/remotes/${opts.remote}/${ref.replace('refs/heads/', '')}`;
453
+ const oid = await this.readRef(remoteRef);
454
+ if (oid) {
455
+ fetchedRefs.push({ name: remoteRef, oid });
456
+ updatedRefs.push(remoteRef);
457
+ }
458
+ }
459
+
460
+ return { fetchedRefs, updatedRefs };
461
+ } catch (error) {
462
+ if (error.code === 'HttpError' || error.code === 'NetworkError') {
463
+ throw new JJError(
464
+ 'NETWORK_ERROR',
465
+ `Network error during fetch: ${error.message}`,
466
+ { remote: opts.remote, originalError: error }
467
+ );
468
+ }
469
+ if (error.code === 'AuthError') {
470
+ throw new JJError(
471
+ 'AUTH_FAILED',
472
+ `Authentication failed: ${error.message}`,
473
+ { remote: opts.remote, originalError: error }
474
+ );
475
+ }
476
+ throw new JJError(
477
+ 'FETCH_FAILED',
478
+ `Fetch failed: ${error.message}`,
479
+ { remote: opts.remote, originalError: error }
480
+ );
481
+ }
482
+ }
483
+
484
+ /**
485
+ * Push objects and refs to remote repository
486
+ *
487
+ * @param {Object} opts - Push options
488
+ * @param {string} opts.remote - Remote name or URL
489
+ * @param {string[]} [opts.refs] - Ref specs to push
490
+ * @param {boolean} [opts.force] - Allow non-fast-forward
491
+ * @param {Function} [opts.onProgress] - Progress callback
492
+ * @param {Function} [opts.onAuth] - Authentication callback
493
+ * @returns {Promise<{pushedRefs: Array, rejectedRefs: Array}>} Push result
494
+ */
495
+ async push(opts) {
496
+ if (!this.http) {
497
+ throw new JJError(
498
+ 'NETWORK_NOT_AVAILABLE',
499
+ 'HTTP client not provided - network operations unavailable',
500
+ { suggestion: 'Provide http option when creating backend' }
501
+ );
502
+ }
503
+
504
+ try {
505
+ const pushedRefs = [];
506
+ const rejectedRefs = [];
507
+
508
+ // Push each ref spec (isomorphic-git pushes one at a time)
509
+ const refsToPush = opts.refs || [];
510
+ for (const ref of refsToPush) {
511
+ try {
512
+ await git.push({
513
+ fs: this.fs,
514
+ http: this.http,
515
+ dir: this.dir,
516
+ remote: opts.remote,
517
+ ref,
518
+ force: opts.force || false,
519
+ onProgress: opts.onProgress,
520
+ onAuth: opts.onAuth,
521
+ });
522
+
523
+ // Track pushed ref
524
+ const oid = await this.readRef(ref);
525
+ if (oid) {
526
+ pushedRefs.push({ name: ref, oid });
527
+ }
528
+ } catch (pushError) {
529
+ // Track rejected refs
530
+ rejectedRefs.push(ref);
531
+ }
532
+ }
533
+
534
+ return { pushedRefs, rejectedRefs };
535
+ } catch (error) {
536
+ if (error.code === 'HttpError' || error.code === 'NetworkError') {
537
+ throw new JJError(
538
+ 'NETWORK_ERROR',
539
+ `Network error during push: ${error.message}`,
540
+ { remote: opts.remote, originalError: error }
541
+ );
542
+ }
543
+ if (error.code === 'AuthError') {
544
+ throw new JJError(
545
+ 'AUTH_FAILED',
546
+ `Authentication failed: ${error.message}`,
547
+ { remote: opts.remote, originalError: error }
548
+ );
549
+ }
550
+ if (error.code === 'PushRejectedError') {
551
+ throw new JJError(
552
+ 'PUSH_REJECTED',
553
+ `Push rejected (non-fast-forward): ${error.message}`,
554
+ { remote: opts.remote, suggestion: 'Use force option or pull first', originalError: error }
555
+ );
556
+ }
557
+ throw new JJError(
558
+ 'PUSH_FAILED',
559
+ `Push failed: ${error.message}`,
560
+ { remote: opts.remote, originalError: error }
561
+ );
562
+ }
563
+ }
564
+
565
+ /**
566
+ * Create minimal .jj/repo structure for jj CLI compatibility
567
+ * @private
568
+ */
569
+ async _createJJRepoStructure() {
570
+ const jjDir = `${this.dir}/.jj`;
571
+ const jjRepo = `${jjDir}/repo`;
572
+
573
+ // Create directory structure
574
+ await this.fs.promises.mkdir(`${jjRepo}/store`, { recursive: true });
575
+ await this.fs.promises.mkdir(`${jjRepo}/store/extra/heads`, { recursive: true });
576
+ await this.fs.promises.mkdir(`${jjRepo}/op_store/operations`, { recursive: true });
577
+ await this.fs.promises.mkdir(`${jjRepo}/op_store/views`, { recursive: true });
578
+ await this.fs.promises.mkdir(`${jjRepo}/op_heads/heads`, { recursive: true });
579
+ await this.fs.promises.mkdir(`${jjRepo}/index`, { recursive: true });
580
+ await this.fs.promises.mkdir(`${jjRepo}/submodule_store`, { recursive: true });
581
+ await this.fs.promises.mkdir(`${jjDir}/working_copy`, { recursive: true });
582
+
583
+ // Write type files
584
+ await this.fs.promises.writeFile(`${jjRepo}/store/type`, 'git');
585
+ await this.fs.promises.writeFile(`${jjRepo}/store/git_target`, '../../../.git');
586
+ await this.fs.promises.writeFile(`${jjRepo}/op_store/type`, 'simple_op_store');
587
+ await this.fs.promises.writeFile(`${jjRepo}/op_heads/type`, 'simple_op_heads_store');
588
+ await this.fs.promises.writeFile(`${jjRepo}/index/type`, 'default');
589
+ await this.fs.promises.writeFile(`${jjRepo}/submodule_store/type`, 'default');
590
+ await this.fs.promises.writeFile(`${jjDir}/working_copy/type`, 'local');
591
+
592
+ // Create .jj/.gitignore to prevent git from tracking jj metadata
593
+ await this.fs.promises.writeFile(`${jjDir}/.gitignore`, '/*\n');
594
+
595
+ // Add .jj to root .gitignore if it doesn't exist
596
+ await this._ensureGitignore();
597
+
598
+ // Create initial protobuf files for jj CLI compatibility
599
+ await this._createInitialJJState();
600
+ }
601
+
602
+ /**
603
+ * Create initial JJ state (protobuf files)
604
+ * @private
605
+ */
606
+ async _createInitialJJState() {
607
+ // Generate unique IDs for initial state
608
+ const operationId = crypto.randomBytes(64).toString('hex'); // 512-bit = 128 hex chars
609
+ const viewId = crypto.randomBytes(64).toString('hex');
610
+ const emptyTreeId = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'; // Git's empty tree hash
611
+
612
+ // Get the initial commit from Git
613
+ let initialCommitId = '0000000000000000000000000000000000000000';
614
+ try {
615
+ const headOid = await git.resolveRef({
616
+ fs: this.fs,
617
+ dir: this.dir,
618
+ ref: 'HEAD',
619
+ });
620
+ if (headOid) {
621
+ initialCommitId = crypto.randomBytes(64).toString('hex');
622
+ }
623
+ } catch (error) {
624
+ // No initial commit yet
625
+ }
626
+
627
+ // Create checkout file
628
+ const checkout = new JJCheckout(this.fs, this.dir);
629
+ await checkout.writeCheckout(operationId, 'default');
630
+
631
+ // Create tree_state file
632
+ const treeState = new JJTreeState(this.fs, this.dir);
633
+ await treeState.writeTreeState(emptyTreeId, []);
634
+
635
+ // Create initial operation
636
+ const opStore = new JJOperationStore(this.fs, this.dir);
637
+ const metadata = {
638
+ start_time: {
639
+ millis_since_epoch: Date.now(),
640
+ tz_offset: new Date().getTimezoneOffset()
641
+ },
642
+ end_time: {
643
+ millis_since_epoch: Date.now(),
644
+ tz_offset: new Date().getTimezoneOffset()
645
+ },
646
+ description: 'initialize repo',
647
+ hostname: 'localhost',
648
+ username: 'user',
649
+ is_snapshot: false,
650
+ tags: {}
651
+ };
652
+ await opStore.writeOperation(operationId, viewId, [], metadata);
653
+
654
+ // Create initial view
655
+ const viewStore = new JJViewStore(this.fs, this.dir);
656
+ await viewStore.writeView(viewId, [initialCommitId], { default: initialCommitId });
657
+
658
+ // Write operation head - the filename IS the operation ID
659
+ const opHeadPath = `${this.dir}/.jj/repo/op_heads/heads/${operationId}`;
660
+ await this.fs.promises.writeFile(opHeadPath, '');
661
+ }
662
+
663
+ /**
664
+ * Ensure .gitignore has .jj/ entry
665
+ * @private
666
+ */
667
+ async _ensureGitignore() {
668
+ const gitignorePath = `${this.dir}/.gitignore`;
669
+ let content = '';
670
+
671
+ try {
672
+ content = await this.fs.promises.readFile(gitignorePath, 'utf8');
673
+ } catch (error) {
674
+ if (error.code !== 'ENOENT') throw error;
675
+ }
676
+
677
+ // Check if .jj is already in .gitignore
678
+ const lines = content.split('\n');
679
+ const hasJJ = lines.some(line => line.trim() === '.jj' || line.trim() === '.jj/');
680
+
681
+ if (!hasJJ) {
682
+ // Add .jj to .gitignore
683
+ const newContent = content + (content && !content.endsWith('\n') ? '\n' : '') + '.jj/\n';
684
+ await this.fs.promises.writeFile(gitignorePath, newContent);
685
+ }
686
+ }
687
+ }