memory-git 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/dist/index.js ADDED
@@ -0,0 +1,1024 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MemoryGit = void 0;
7
+ const isomorphic_git_1 = __importDefault(require("isomorphic-git"));
8
+ const node_1 = __importDefault(require("isomorphic-git/http/node"));
9
+ const memfs_1 = require("memfs");
10
+ const fs_1 = require("fs");
11
+ const path_1 = __importDefault(require("path"));
12
+ /**
13
+ * MemoryGit - In-memory Git implementation
14
+ *
15
+ * Loads the project into memory, executes all git operations in memory,
16
+ * and syncs to disk only when flush() is called.
17
+ *
18
+ * All real disk operations use async versions to not block the Node.js event loop.
19
+ */
20
+ class MemoryGit {
21
+ /** Instance name */
22
+ name;
23
+ /** Memory filesystem */
24
+ fs;
25
+ /** Volume instance */
26
+ vol;
27
+ /** In-memory repository directory */
28
+ dir = '/repo';
29
+ /** Real disk directory (if loaded from disk) */
30
+ realDir = null;
31
+ /** Whether the repository is initialized */
32
+ isInitialized = false;
33
+ /** Author information for commits */
34
+ author = { name: 'Memory Git', email: 'memory@git.local' };
35
+ operations = [];
36
+ _stash = [];
37
+ /**
38
+ * Creates a new MemoryGit instance
39
+ * @param name - Unique name to identify the instance
40
+ */
41
+ constructor(name = 'memory-git') {
42
+ this.name = name;
43
+ this.fs = memfs_1.fs;
44
+ this.vol = memfs_1.vol;
45
+ // Clear volume to ensure clean state
46
+ this.vol.reset();
47
+ }
48
+ /**
49
+ * Logs an operation
50
+ * @private
51
+ */
52
+ _logOperation(operation, params, result = null, error = null) {
53
+ const entry = {
54
+ timestamp: new Date().toISOString(),
55
+ operation,
56
+ params: this._sanitizeParams(params),
57
+ success: error === null,
58
+ result: result,
59
+ error: error ? error.message : null
60
+ };
61
+ this.operations.push(entry);
62
+ return entry;
63
+ }
64
+ /**
65
+ * Removes large data from params for logging
66
+ * @private
67
+ */
68
+ _sanitizeParams(params) {
69
+ const sanitized = { ...params };
70
+ if (sanitized.content && typeof sanitized.content === 'string' && sanitized.content.length > 100) {
71
+ sanitized.content = `[${sanitized.content.length} bytes]`;
72
+ }
73
+ if (Buffer.isBuffer(sanitized.content)) {
74
+ sanitized.content = `[Buffer: ${sanitized.content.length} bytes]`;
75
+ }
76
+ return sanitized;
77
+ }
78
+ /**
79
+ * Checks if a path exists on real disk (async)
80
+ * @private
81
+ */
82
+ async _realPathExists(filepath) {
83
+ try {
84
+ await fs_1.promises.access(filepath);
85
+ return true;
86
+ }
87
+ catch {
88
+ return false;
89
+ }
90
+ }
91
+ /**
92
+ * Sets the author for commits
93
+ * @param name - Author name
94
+ * @param email - Author email
95
+ */
96
+ setAuthor(name, email) {
97
+ this.author = { name, email };
98
+ this._logOperation('setAuthor', { name, email });
99
+ }
100
+ /**
101
+ * Initializes a new repository in memory
102
+ */
103
+ async init() {
104
+ try {
105
+ this.fs.mkdirSync(this.dir, { recursive: true });
106
+ await isomorphic_git_1.default.init({ fs: this.fs, dir: this.dir, defaultBranch: 'main' });
107
+ this.isInitialized = true;
108
+ this._logOperation('init', { dir: this.dir }, { success: true });
109
+ return true;
110
+ }
111
+ catch (error) {
112
+ this._logOperation('init', { dir: this.dir }, null, error);
113
+ throw error;
114
+ }
115
+ }
116
+ /**
117
+ * Loads an existing repository from disk to memory
118
+ * @param sourcePath - Path to the repository on disk
119
+ * @param options - Loading options
120
+ * @returns Number of files loaded
121
+ */
122
+ async loadFromDisk(sourcePath, options = {}) {
123
+ try {
124
+ this.realDir = path_1.default.resolve(sourcePath);
125
+ const ignore = options.ignore || ['node_modules', '.pnpm-store'];
126
+ // Create base directory in memory
127
+ this.fs.mkdirSync(this.dir, { recursive: true });
128
+ // Copy recursively from disk to memory (async)
129
+ const fileCount = await this._copyToMemoryAsync(this.realDir, this.dir, ignore);
130
+ this.isInitialized = true;
131
+ this._logOperation('loadFromDisk', { sourcePath: this.realDir, ignore }, {
132
+ success: true,
133
+ filesLoaded: fileCount
134
+ });
135
+ return fileCount;
136
+ }
137
+ catch (error) {
138
+ this._logOperation('loadFromDisk', { sourcePath }, null, error);
139
+ throw error;
140
+ }
141
+ }
142
+ /**
143
+ * Copies files from real disk to memory filesystem (async)
144
+ * @private
145
+ */
146
+ async _copyToMemoryAsync(realPath, memoryPath, ignore = []) {
147
+ const entries = await fs_1.promises.readdir(realPath, { withFileTypes: true });
148
+ // Process entries in parallel for better performance
149
+ const promises = entries.map(async (entry) => {
150
+ // Check if should ignore
151
+ if (ignore.includes(entry.name))
152
+ return 0;
153
+ const realEntryPath = path_1.default.join(realPath, entry.name);
154
+ const memoryEntryPath = path_1.default.posix.join(memoryPath, entry.name);
155
+ if (entry.isDirectory()) {
156
+ this.fs.mkdirSync(memoryEntryPath, { recursive: true });
157
+ return await this._copyToMemoryAsync(realEntryPath, memoryEntryPath, ignore);
158
+ }
159
+ else if (entry.isFile()) {
160
+ const content = await fs_1.promises.readFile(realEntryPath);
161
+ this.fs.writeFileSync(memoryEntryPath, content);
162
+ return 1;
163
+ }
164
+ return 0;
165
+ });
166
+ const results = await Promise.all(promises);
167
+ return results.reduce((acc, val) => acc + val, 0);
168
+ }
169
+ /**
170
+ * Counts files in a directory in memory
171
+ * @private
172
+ */
173
+ _countFiles(dir) {
174
+ let count = 0;
175
+ const entries = this.fs.readdirSync(dir);
176
+ for (const entry of entries) {
177
+ const fullPath = path_1.default.posix.join(dir, entry);
178
+ const stat = this.fs.statSync(fullPath);
179
+ if (stat.isDirectory()) {
180
+ count += this._countFiles(fullPath);
181
+ }
182
+ else {
183
+ count++;
184
+ }
185
+ }
186
+ return count;
187
+ }
188
+ /**
189
+ * Writes a file to the in-memory repository
190
+ * @param filepath - Relative file path
191
+ * @param content - File content
192
+ */
193
+ async writeFile(filepath, content) {
194
+ try {
195
+ const fullPath = path_1.default.posix.join(this.dir, filepath);
196
+ const dir = path_1.default.posix.dirname(fullPath);
197
+ // Create directories if needed
198
+ this.fs.mkdirSync(dir, { recursive: true });
199
+ this.fs.writeFileSync(fullPath, content);
200
+ this._logOperation('writeFile', { filepath, content }, { success: true });
201
+ return true;
202
+ }
203
+ catch (error) {
204
+ this._logOperation('writeFile', { filepath }, null, error);
205
+ throw error;
206
+ }
207
+ }
208
+ /**
209
+ * Reads a file from the in-memory repository
210
+ * @param filepath - Relative file path
211
+ * @returns File content
212
+ */
213
+ async readFile(filepath) {
214
+ try {
215
+ const fullPath = path_1.default.posix.join(this.dir, filepath);
216
+ const content = this.fs.readFileSync(fullPath, 'utf8');
217
+ this._logOperation('readFile', { filepath }, { success: true, size: content.length });
218
+ return content;
219
+ }
220
+ catch (error) {
221
+ this._logOperation('readFile', { filepath }, null, error);
222
+ throw error;
223
+ }
224
+ }
225
+ /**
226
+ * Checks if a file exists
227
+ * @param filepath - Relative file path
228
+ */
229
+ async fileExists(filepath) {
230
+ try {
231
+ const fullPath = path_1.default.posix.join(this.dir, filepath);
232
+ return this.fs.existsSync(fullPath);
233
+ }
234
+ catch {
235
+ return false;
236
+ }
237
+ }
238
+ /**
239
+ * Deletes a file from the in-memory repository
240
+ * @param filepath - Relative file path
241
+ */
242
+ async deleteFile(filepath) {
243
+ try {
244
+ const fullPath = path_1.default.posix.join(this.dir, filepath);
245
+ this.fs.unlinkSync(fullPath);
246
+ this._logOperation('deleteFile', { filepath }, { success: true });
247
+ return true;
248
+ }
249
+ catch (error) {
250
+ this._logOperation('deleteFile', { filepath }, null, error);
251
+ throw error;
252
+ }
253
+ }
254
+ /**
255
+ * Adds file(s) to the staging area
256
+ * @param filepath - Relative file path(s)
257
+ */
258
+ async add(filepath) {
259
+ try {
260
+ const files = Array.isArray(filepath) ? filepath : [filepath];
261
+ for (const file of files) {
262
+ await isomorphic_git_1.default.add({ fs: this.fs, dir: this.dir, filepath: file });
263
+ }
264
+ this._logOperation('add', { filepath: files }, { success: true });
265
+ return true;
266
+ }
267
+ catch (error) {
268
+ this._logOperation('add', { filepath }, null, error);
269
+ throw error;
270
+ }
271
+ }
272
+ /**
273
+ * Removes file(s) from the staging area and working tree
274
+ * @param filepath - Relative file path
275
+ */
276
+ async remove(filepath) {
277
+ try {
278
+ await isomorphic_git_1.default.remove({ fs: this.fs, dir: this.dir, filepath });
279
+ this._logOperation('remove', { filepath }, { success: true });
280
+ return true;
281
+ }
282
+ catch (error) {
283
+ this._logOperation('remove', { filepath }, null, error);
284
+ throw error;
285
+ }
286
+ }
287
+ /**
288
+ * Creates a commit with staged changes
289
+ * @param message - Commit message
290
+ * @returns SHA of the created commit
291
+ */
292
+ async commit(message) {
293
+ try {
294
+ const sha = await isomorphic_git_1.default.commit({
295
+ fs: this.fs,
296
+ dir: this.dir,
297
+ message,
298
+ author: this.author
299
+ });
300
+ this._logOperation('commit', { message }, { success: true, sha });
301
+ return sha;
302
+ }
303
+ catch (error) {
304
+ this._logOperation('commit', { message }, null, error);
305
+ throw error;
306
+ }
307
+ }
308
+ /**
309
+ * Gets repository status
310
+ * @returns List of files with their status
311
+ */
312
+ async status() {
313
+ try {
314
+ const statusMatrix = await isomorphic_git_1.default.statusMatrix({ fs: this.fs, dir: this.dir });
315
+ const result = statusMatrix.map(([filepath, head, workdir, stage]) => ({
316
+ filepath: filepath,
317
+ head: head,
318
+ workdir: workdir,
319
+ stage: stage,
320
+ status: this._getStatusText(head, workdir, stage)
321
+ }));
322
+ this._logOperation('status', {}, { success: true, files: result.length });
323
+ return result;
324
+ }
325
+ catch (error) {
326
+ this._logOperation('status', {}, null, error);
327
+ throw error;
328
+ }
329
+ }
330
+ /**
331
+ * Converts numeric status to readable text
332
+ * @private
333
+ */
334
+ _getStatusText(head, workdir, stage) {
335
+ if (head === 0 && workdir === 2 && stage === 0)
336
+ return 'new, untracked';
337
+ if (head === 0 && workdir === 2 && stage === 2)
338
+ return 'added, staged';
339
+ if (head === 0 && workdir === 2 && stage === 3)
340
+ return 'added, staged, with unstaged changes';
341
+ if (head === 1 && workdir === 1 && stage === 1)
342
+ return 'unmodified';
343
+ if (head === 1 && workdir === 2 && stage === 1)
344
+ return 'modified, unstaged';
345
+ if (head === 1 && workdir === 2 && stage === 2)
346
+ return 'modified, staged';
347
+ if (head === 1 && workdir === 2 && stage === 3)
348
+ return 'modified, staged, with unstaged changes';
349
+ if (head === 1 && workdir === 0 && stage === 0)
350
+ return 'deleted, unstaged';
351
+ if (head === 1 && workdir === 0 && stage === 1)
352
+ return 'deleted, staged';
353
+ if (head === 1 && workdir === 1 && stage === 0)
354
+ return 'deleted, staged';
355
+ return `unknown (${head}, ${workdir}, ${stage})`;
356
+ }
357
+ /**
358
+ * Gets commit log
359
+ * @param depth - Number of commits to return
360
+ * @returns List of commits
361
+ */
362
+ async log(depth = 10) {
363
+ try {
364
+ const commits = await isomorphic_git_1.default.log({ fs: this.fs, dir: this.dir, depth });
365
+ const result = commits.map(commit => ({
366
+ sha: commit.oid,
367
+ message: commit.commit.message,
368
+ author: commit.commit.author.name,
369
+ email: commit.commit.author.email,
370
+ timestamp: new Date(commit.commit.author.timestamp * 1000).toISOString()
371
+ }));
372
+ this._logOperation('log', { depth }, { success: true, commits: result.length });
373
+ return result;
374
+ }
375
+ catch (error) {
376
+ this._logOperation('log', { depth }, null, error);
377
+ throw error;
378
+ }
379
+ }
380
+ /**
381
+ * Creates a new branch
382
+ * @param branchName - Branch name
383
+ */
384
+ async createBranch(branchName) {
385
+ try {
386
+ await isomorphic_git_1.default.branch({ fs: this.fs, dir: this.dir, ref: branchName });
387
+ this._logOperation('createBranch', { branchName }, { success: true });
388
+ return true;
389
+ }
390
+ catch (error) {
391
+ this._logOperation('createBranch', { branchName }, null, error);
392
+ throw error;
393
+ }
394
+ }
395
+ /**
396
+ * Deletes a branch
397
+ * @param branchName - Branch name
398
+ */
399
+ async deleteBranch(branchName) {
400
+ try {
401
+ await isomorphic_git_1.default.deleteBranch({ fs: this.fs, dir: this.dir, ref: branchName });
402
+ this._logOperation('deleteBranch', { branchName }, { success: true });
403
+ return true;
404
+ }
405
+ catch (error) {
406
+ this._logOperation('deleteBranch', { branchName }, null, error);
407
+ throw error;
408
+ }
409
+ }
410
+ /**
411
+ * Switches to a branch
412
+ * @param branchName - Branch name
413
+ */
414
+ async checkout(branchName) {
415
+ try {
416
+ await isomorphic_git_1.default.checkout({ fs: this.fs, dir: this.dir, ref: branchName });
417
+ this._logOperation('checkout', { branchName }, { success: true });
418
+ return true;
419
+ }
420
+ catch (error) {
421
+ this._logOperation('checkout', { branchName }, null, error);
422
+ throw error;
423
+ }
424
+ }
425
+ /**
426
+ * Lists all branches
427
+ * @returns List of branches
428
+ */
429
+ async listBranches() {
430
+ try {
431
+ const branches = await isomorphic_git_1.default.listBranches({ fs: this.fs, dir: this.dir });
432
+ const current = await isomorphic_git_1.default.currentBranch({ fs: this.fs, dir: this.dir });
433
+ const result = branches.map(branch => ({
434
+ name: branch,
435
+ current: branch === current
436
+ }));
437
+ this._logOperation('listBranches', {}, { success: true, branches: result });
438
+ return result;
439
+ }
440
+ catch (error) {
441
+ this._logOperation('listBranches', {}, null, error);
442
+ throw error;
443
+ }
444
+ }
445
+ /**
446
+ * Gets the current branch
447
+ * @returns Current branch name
448
+ */
449
+ async currentBranch() {
450
+ try {
451
+ const branch = await isomorphic_git_1.default.currentBranch({ fs: this.fs, dir: this.dir });
452
+ this._logOperation('currentBranch', {}, { success: true, branch });
453
+ return branch || undefined;
454
+ }
455
+ catch (error) {
456
+ this._logOperation('currentBranch', {}, null, error);
457
+ throw error;
458
+ }
459
+ }
460
+ /**
461
+ * Merges a branch into the current branch
462
+ * @param theirBranch - Branch name to merge
463
+ */
464
+ async merge(theirBranch) {
465
+ try {
466
+ const result = await isomorphic_git_1.default.merge({
467
+ fs: this.fs,
468
+ dir: this.dir,
469
+ theirs: theirBranch,
470
+ author: this.author
471
+ });
472
+ this._logOperation('merge', { theirBranch }, { success: true, ...result });
473
+ return result;
474
+ }
475
+ catch (error) {
476
+ this._logOperation('merge', { theirBranch }, null, error);
477
+ throw error;
478
+ }
479
+ }
480
+ /**
481
+ * Adds a remote
482
+ * @param remoteName - Remote name
483
+ * @param url - Remote URL
484
+ */
485
+ async addRemote(remoteName, url) {
486
+ try {
487
+ await isomorphic_git_1.default.addRemote({ fs: this.fs, dir: this.dir, remote: remoteName, url });
488
+ this._logOperation('addRemote', { remoteName, url }, { success: true });
489
+ return true;
490
+ }
491
+ catch (error) {
492
+ this._logOperation('addRemote', { remoteName, url }, null, error);
493
+ throw error;
494
+ }
495
+ }
496
+ /**
497
+ * Removes a remote
498
+ * @param remoteName - Remote name
499
+ */
500
+ async deleteRemote(remoteName) {
501
+ try {
502
+ await isomorphic_git_1.default.deleteRemote({ fs: this.fs, dir: this.dir, remote: remoteName });
503
+ this._logOperation('deleteRemote', { remoteName }, { success: true });
504
+ return true;
505
+ }
506
+ catch (error) {
507
+ this._logOperation('deleteRemote', { remoteName }, null, error);
508
+ throw error;
509
+ }
510
+ }
511
+ /**
512
+ * Lists configured remotes
513
+ * @returns List of remotes
514
+ */
515
+ async listRemotes() {
516
+ try {
517
+ const remotes = await isomorphic_git_1.default.listRemotes({ fs: this.fs, dir: this.dir });
518
+ this._logOperation('listRemotes', {}, { success: true, remotes });
519
+ return remotes;
520
+ }
521
+ catch (error) {
522
+ this._logOperation('listRemotes', {}, null, error);
523
+ throw error;
524
+ }
525
+ }
526
+ /**
527
+ * Creates a tag
528
+ * @param tagName - Tag name
529
+ * @param ref - Reference (commit SHA or branch)
530
+ */
531
+ async createTag(tagName, ref = 'HEAD') {
532
+ try {
533
+ await isomorphic_git_1.default.tag({ fs: this.fs, dir: this.dir, ref: tagName, object: ref });
534
+ this._logOperation('createTag', { tagName, ref }, { success: true });
535
+ return true;
536
+ }
537
+ catch (error) {
538
+ this._logOperation('createTag', { tagName, ref }, null, error);
539
+ throw error;
540
+ }
541
+ }
542
+ /**
543
+ * Lists all tags
544
+ * @returns List of tags
545
+ */
546
+ async listTags() {
547
+ try {
548
+ const tags = await isomorphic_git_1.default.listTags({ fs: this.fs, dir: this.dir });
549
+ this._logOperation('listTags', {}, { success: true, tags });
550
+ return tags;
551
+ }
552
+ catch (error) {
553
+ this._logOperation('listTags', {}, null, error);
554
+ throw error;
555
+ }
556
+ }
557
+ /**
558
+ * Returns the history of all operations performed
559
+ * @returns List of operations
560
+ */
561
+ getOperationsLog() {
562
+ return [...this.operations];
563
+ }
564
+ /**
565
+ * Clears the operation log
566
+ */
567
+ clearOperationsLog() {
568
+ this.operations = [];
569
+ this._logOperation('clearOperationsLog', {}, { success: true });
570
+ }
571
+ /**
572
+ * Gets operation statistics
573
+ * @returns Statistics
574
+ */
575
+ getOperationsStats() {
576
+ const stats = {
577
+ total: this.operations.length,
578
+ successful: this.operations.filter(op => op.success).length,
579
+ failed: this.operations.filter(op => !op.success).length,
580
+ byOperation: {}
581
+ };
582
+ for (const op of this.operations) {
583
+ if (!stats.byOperation[op.operation]) {
584
+ stats.byOperation[op.operation] = { total: 0, successful: 0, failed: 0 };
585
+ }
586
+ stats.byOperation[op.operation].total++;
587
+ if (op.success) {
588
+ stats.byOperation[op.operation].successful++;
589
+ }
590
+ else {
591
+ stats.byOperation[op.operation].failed++;
592
+ }
593
+ }
594
+ return stats;
595
+ }
596
+ /**
597
+ * Exports the operation log in JSON format
598
+ * @returns JSON string of operations
599
+ */
600
+ exportOperationsLog() {
601
+ return JSON.stringify({
602
+ name: this.name,
603
+ exportedAt: new Date().toISOString(),
604
+ stats: this.getOperationsStats(),
605
+ operations: this.operations
606
+ }, null, 2);
607
+ }
608
+ /**
609
+ * Syncs all changes from memory to disk
610
+ * @param targetPath - Destination path (optional, uses original path if not specified)
611
+ * @param options - Flush options
612
+ * @returns Number of files flushed
613
+ */
614
+ async flush(targetPath = null, options = {}) {
615
+ try {
616
+ const destination = targetPath ? path_1.default.resolve(targetPath) : this.realDir;
617
+ if (!destination) {
618
+ throw new Error('No destination path specified and repository was not loaded from disk');
619
+ }
620
+ // Create destination directory if it doesn't exist (async)
621
+ const destinationExists = await this._realPathExists(destination);
622
+ if (!destinationExists) {
623
+ await fs_1.promises.mkdir(destination, { recursive: true });
624
+ }
625
+ // Copy recursively from memory to disk (async)
626
+ const fileCount = await this._copyToDiskAsync(this.dir, destination);
627
+ this._logOperation('flush', { targetPath: destination, options }, {
628
+ success: true,
629
+ filesFlushed: fileCount
630
+ });
631
+ return fileCount;
632
+ }
633
+ catch (error) {
634
+ this._logOperation('flush', { targetPath }, null, error);
635
+ throw error;
636
+ }
637
+ }
638
+ /**
639
+ * Copies files from memory to disk (async)
640
+ * @private
641
+ */
642
+ async _copyToDiskAsync(memoryPath, realPath) {
643
+ const entries = this.fs.readdirSync(memoryPath);
644
+ // Process entries in parallel for better performance
645
+ const promises = entries.map(async (entry) => {
646
+ const memoryEntryPath = path_1.default.posix.join(memoryPath, entry);
647
+ const realEntryPath = path_1.default.join(realPath, entry);
648
+ const stat = this.fs.statSync(memoryEntryPath);
649
+ if (stat.isDirectory()) {
650
+ const dirExists = await this._realPathExists(realEntryPath);
651
+ if (!dirExists) {
652
+ await fs_1.promises.mkdir(realEntryPath, { recursive: true });
653
+ }
654
+ return await this._copyToDiskAsync(memoryEntryPath, realEntryPath);
655
+ }
656
+ else {
657
+ const content = this.fs.readFileSync(memoryEntryPath);
658
+ await fs_1.promises.writeFile(realEntryPath, content);
659
+ return 1;
660
+ }
661
+ });
662
+ const results = await Promise.all(promises);
663
+ return results.reduce((acc, val) => acc + val, 0);
664
+ }
665
+ /**
666
+ * Lists files in the in-memory repository
667
+ * @param dir - Relative directory (optional)
668
+ * @param includeGit - Include .git folder in listing
669
+ * @returns List of files
670
+ */
671
+ async listFiles(dir = '', includeGit = false) {
672
+ try {
673
+ const fullPath = path_1.default.posix.join(this.dir, dir);
674
+ const files = this._listFilesRecursive(fullPath, '', includeGit);
675
+ this._logOperation('listFiles', { dir }, { success: true, files: files.length });
676
+ return files;
677
+ }
678
+ catch (error) {
679
+ this._logOperation('listFiles', { dir }, null, error);
680
+ throw error;
681
+ }
682
+ }
683
+ /**
684
+ * Lists files recursively
685
+ * @private
686
+ */
687
+ _listFilesRecursive(dir, base = '', includeGit = false) {
688
+ const files = [];
689
+ const entries = this.fs.readdirSync(dir);
690
+ for (const entry of entries) {
691
+ const fullPath = path_1.default.posix.join(dir, entry);
692
+ const relativePath = base ? path_1.default.posix.join(base, entry) : entry;
693
+ const stat = this.fs.statSync(fullPath);
694
+ if (stat.isDirectory()) {
695
+ if (entry === '.git' && !includeGit)
696
+ continue;
697
+ files.push(...this._listFilesRecursive(fullPath, relativePath, includeGit));
698
+ }
699
+ else {
700
+ files.push(relativePath);
701
+ }
702
+ }
703
+ return files;
704
+ }
705
+ /**
706
+ * Gets the diff between working tree and HEAD
707
+ * @returns List of modified files
708
+ */
709
+ async diff() {
710
+ try {
711
+ const changes = [];
712
+ const statusMatrix = await isomorphic_git_1.default.statusMatrix({ fs: this.fs, dir: this.dir });
713
+ for (const [filepath, head, workdir, stage] of statusMatrix) {
714
+ if (head !== workdir || head !== stage) {
715
+ changes.push({
716
+ filepath: filepath,
717
+ status: this._getStatusText(head, workdir, stage)
718
+ });
719
+ }
720
+ }
721
+ this._logOperation('diff', {}, {
722
+ success: true,
723
+ changes: changes.length
724
+ });
725
+ return changes;
726
+ }
727
+ catch (error) {
728
+ this._logOperation('diff', {}, null, error);
729
+ throw error;
730
+ }
731
+ }
732
+ /**
733
+ * Gets file content at a specific commit
734
+ * @param filepath - File path
735
+ * @param ref - Reference (commit SHA, branch, tag)
736
+ * @returns File content
737
+ */
738
+ async readFileAtRef(filepath, ref = 'HEAD') {
739
+ try {
740
+ const { blob } = await isomorphic_git_1.default.readBlob({
741
+ fs: this.fs,
742
+ dir: this.dir,
743
+ oid: await isomorphic_git_1.default.resolveRef({ fs: this.fs, dir: this.dir, ref }),
744
+ filepath
745
+ });
746
+ const content = Buffer.from(blob).toString('utf8');
747
+ this._logOperation('readFileAtRef', { filepath, ref }, { success: true });
748
+ return content;
749
+ }
750
+ catch (error) {
751
+ this._logOperation('readFileAtRef', { filepath, ref }, null, error);
752
+ throw error;
753
+ }
754
+ }
755
+ /**
756
+ * Resets file changes
757
+ * @param filepath - File path
758
+ */
759
+ async resetFile(filepath) {
760
+ try {
761
+ await isomorphic_git_1.default.checkout({
762
+ fs: this.fs,
763
+ dir: this.dir,
764
+ filepaths: [filepath],
765
+ force: true
766
+ });
767
+ this._logOperation('resetFile', { filepath }, { success: true });
768
+ return true;
769
+ }
770
+ catch (error) {
771
+ this._logOperation('resetFile', { filepath }, null, error);
772
+ throw error;
773
+ }
774
+ }
775
+ /**
776
+ * Stashes current changes (simulates by saving in memory)
777
+ * @returns Number of files saved to stash
778
+ */
779
+ async stash() {
780
+ try {
781
+ const statusMatrix = await isomorphic_git_1.default.statusMatrix({ fs: this.fs, dir: this.dir });
782
+ const stashedFiles = [];
783
+ for (const [filepath, head, workdir] of statusMatrix) {
784
+ if (workdir === 2 || workdir === 0) {
785
+ const fullPath = path_1.default.posix.join(this.dir, filepath);
786
+ try {
787
+ const content = this.fs.readFileSync(fullPath);
788
+ stashedFiles.push({
789
+ filepath: filepath,
790
+ content: content,
791
+ wasNew: head === 0
792
+ });
793
+ }
794
+ catch {
795
+ // Deleted file
796
+ stashedFiles.push({ filepath: filepath, deleted: true });
797
+ }
798
+ }
799
+ }
800
+ this._stash.push(stashedFiles);
801
+ // Reset to HEAD
802
+ for (const file of stashedFiles) {
803
+ const fullPath = path_1.default.posix.join(this.dir, file.filepath);
804
+ if (file.deleted) {
805
+ // Restore deleted file
806
+ try {
807
+ await isomorphic_git_1.default.checkout({
808
+ fs: this.fs,
809
+ dir: this.dir,
810
+ filepaths: [file.filepath],
811
+ force: true
812
+ });
813
+ }
814
+ catch {
815
+ // Ignore if didn't exist
816
+ }
817
+ }
818
+ else if (file.wasNew) {
819
+ // Remove new file
820
+ try {
821
+ this.fs.unlinkSync(fullPath);
822
+ }
823
+ catch {
824
+ // Ignore
825
+ }
826
+ }
827
+ else {
828
+ // Restore modified file
829
+ await isomorphic_git_1.default.checkout({
830
+ fs: this.fs,
831
+ dir: this.dir,
832
+ filepaths: [file.filepath],
833
+ force: true
834
+ });
835
+ }
836
+ }
837
+ this._logOperation('stash', {}, { success: true, files: stashedFiles.length });
838
+ return stashedFiles.length;
839
+ }
840
+ catch (error) {
841
+ this._logOperation('stash', {}, null, error);
842
+ throw error;
843
+ }
844
+ }
845
+ /**
846
+ * Restores from stash
847
+ * @returns Number of files restored
848
+ */
849
+ async stashPop() {
850
+ try {
851
+ if (this._stash.length === 0) {
852
+ throw new Error('No stash available');
853
+ }
854
+ const stashedFiles = this._stash.pop();
855
+ for (const file of stashedFiles) {
856
+ const fullPath = path_1.default.posix.join(this.dir, file.filepath);
857
+ if (file.deleted) {
858
+ try {
859
+ this.fs.unlinkSync(fullPath);
860
+ }
861
+ catch {
862
+ // Ignore
863
+ }
864
+ }
865
+ else {
866
+ // Create directory if needed
867
+ const dir = path_1.default.posix.dirname(fullPath);
868
+ this.fs.mkdirSync(dir, { recursive: true });
869
+ this.fs.writeFileSync(fullPath, file.content);
870
+ }
871
+ }
872
+ this._logOperation('stashPop', {}, { success: true, files: stashedFiles.length });
873
+ return stashedFiles.length;
874
+ }
875
+ catch (error) {
876
+ this._logOperation('stashPop', {}, null, error);
877
+ throw error;
878
+ }
879
+ }
880
+ /**
881
+ * Lists available stashes
882
+ * @returns Number of stashes
883
+ */
884
+ stashList() {
885
+ return this._stash.length;
886
+ }
887
+ /**
888
+ * Clones a remote repository to memory
889
+ * @param url - Repository URL
890
+ * @param options - Clone options
891
+ */
892
+ async clone(url, options = {}) {
893
+ try {
894
+ this.fs.mkdirSync(this.dir, { recursive: true });
895
+ await isomorphic_git_1.default.clone({
896
+ fs: this.fs,
897
+ http: node_1.default,
898
+ dir: this.dir,
899
+ url,
900
+ depth: options.depth || undefined,
901
+ singleBranch: options.singleBranch || false,
902
+ ...options
903
+ });
904
+ this.isInitialized = true;
905
+ this._logOperation('clone', { url, options }, { success: true });
906
+ return true;
907
+ }
908
+ catch (error) {
909
+ this._logOperation('clone', { url, options }, null, error);
910
+ throw error;
911
+ }
912
+ }
913
+ /**
914
+ * Fetches from a remote
915
+ * @param remote - Remote name (default: 'origin')
916
+ */
917
+ async fetch(remote = 'origin') {
918
+ try {
919
+ await isomorphic_git_1.default.fetch({
920
+ fs: this.fs,
921
+ http: node_1.default,
922
+ dir: this.dir,
923
+ remote
924
+ });
925
+ this._logOperation('fetch', { remote }, { success: true });
926
+ return true;
927
+ }
928
+ catch (error) {
929
+ this._logOperation('fetch', { remote }, null, error);
930
+ throw error;
931
+ }
932
+ }
933
+ /**
934
+ * Pulls from a remote
935
+ * @param remote - Remote name (default: 'origin')
936
+ * @param branch - Branch name
937
+ */
938
+ async pull(remote = 'origin', branch = null) {
939
+ try {
940
+ const currentBranchName = branch || await this.currentBranch();
941
+ await isomorphic_git_1.default.pull({
942
+ fs: this.fs,
943
+ http: node_1.default,
944
+ dir: this.dir,
945
+ remote,
946
+ ref: currentBranchName,
947
+ author: this.author
948
+ });
949
+ this._logOperation('pull', { remote, branch: currentBranchName }, { success: true });
950
+ return true;
951
+ }
952
+ catch (error) {
953
+ this._logOperation('pull', { remote, branch }, null, error);
954
+ throw error;
955
+ }
956
+ }
957
+ /**
958
+ * Clears the in-memory filesystem and reinitializes
959
+ */
960
+ async clear() {
961
+ try {
962
+ this.vol.reset();
963
+ this.isInitialized = false;
964
+ this._stash = [];
965
+ this._logOperation('clear', {}, { success: true });
966
+ return true;
967
+ }
968
+ catch (error) {
969
+ this._logOperation('clear', {}, null, error);
970
+ throw error;
971
+ }
972
+ }
973
+ /**
974
+ * Gets repository information
975
+ * @returns Repository information
976
+ */
977
+ async getRepoInfo() {
978
+ const info = {
979
+ initialized: this.isInitialized,
980
+ memoryDir: this.dir,
981
+ realDir: this.realDir,
982
+ currentBranch: null,
983
+ branches: [],
984
+ remotes: [],
985
+ fileCount: 0,
986
+ commits: 0
987
+ };
988
+ if (this.isInitialized) {
989
+ info.currentBranch = (await this.currentBranch()) || null;
990
+ info.branches = await this.listBranches();
991
+ info.remotes = await this.listRemotes();
992
+ info.fileCount = this._countFiles(this.dir);
993
+ try {
994
+ const logEntries = await isomorphic_git_1.default.log({ fs: this.fs, dir: this.dir });
995
+ info.commits = logEntries.length;
996
+ }
997
+ catch {
998
+ // Repo without commits
999
+ }
1000
+ }
1001
+ return info;
1002
+ }
1003
+ /**
1004
+ * Gets estimated memory usage
1005
+ * @returns Memory usage information
1006
+ */
1007
+ getMemoryUsage() {
1008
+ const json = this.vol.toJSON();
1009
+ const totalSize = Object.values(json).reduce((acc, content) => {
1010
+ if (typeof content === 'string') {
1011
+ return acc + content.length;
1012
+ }
1013
+ return acc;
1014
+ }, 0);
1015
+ return {
1016
+ files: Object.keys(json).length,
1017
+ estimatedSizeBytes: totalSize,
1018
+ estimatedSizeMB: (totalSize / 1024 / 1024).toFixed(2),
1019
+ operationsLogged: this.operations.length
1020
+ };
1021
+ }
1022
+ }
1023
+ exports.MemoryGit = MemoryGit;
1024
+ //# sourceMappingURL=index.js.map