@andrebuzeli/git-mcp 7.6.1 → 8.0.1

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 (89) hide show
  1. package/README.md +309 -0
  2. package/dist/index.js +3 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/providers/giteaProvider.d.ts +17 -1
  5. package/dist/providers/giteaProvider.d.ts.map +1 -1
  6. package/dist/providers/giteaProvider.js +86 -1
  7. package/dist/providers/giteaProvider.js.map +1 -1
  8. package/dist/providers/providerManager.d.ts +2 -0
  9. package/dist/providers/providerManager.d.ts.map +1 -1
  10. package/dist/providers/providerManager.js +3 -0
  11. package/dist/providers/providerManager.js.map +1 -1
  12. package/dist/server.d.ts +2 -0
  13. package/dist/server.d.ts.map +1 -1
  14. package/dist/server.js +1 -1
  15. package/dist/server.js.map +1 -1
  16. package/dist/tools/gitAnalytics.d.ts.map +1 -1
  17. package/dist/tools/gitAnalytics.js +5 -0
  18. package/dist/tools/gitAnalytics.js.map +1 -1
  19. package/dist/tools/gitBackup.d.ts +2 -2
  20. package/dist/tools/gitBackup.d.ts.map +1 -1
  21. package/dist/tools/gitBackup.js +5 -6
  22. package/dist/tools/gitBackup.js.map +1 -1
  23. package/dist/tools/gitBranches.d.ts.map +1 -1
  24. package/dist/tools/gitBranches.js +35 -21
  25. package/dist/tools/gitBranches.js.map +1 -1
  26. package/dist/tools/gitConfig.d.ts +2 -2
  27. package/dist/tools/gitConfig.d.ts.map +1 -1
  28. package/dist/tools/gitConfig.js +7 -8
  29. package/dist/tools/gitConfig.js.map +1 -1
  30. package/dist/tools/gitFix.d.ts +2 -1
  31. package/dist/tools/gitFix.d.ts.map +1 -1
  32. package/dist/tools/gitFix.js +24 -17
  33. package/dist/tools/gitFix.js.map +1 -1
  34. package/dist/tools/gitFix.tool.d.ts +2 -2
  35. package/dist/tools/gitFix.tool.d.ts.map +1 -1
  36. package/dist/tools/gitFix.tool.js +2 -2
  37. package/dist/tools/gitFix.tool.js.map +1 -1
  38. package/dist/tools/gitHistory.d.ts.map +1 -1
  39. package/dist/tools/gitHistory.js +9 -10
  40. package/dist/tools/gitHistory.js.map +1 -1
  41. package/dist/tools/gitIssues.d.ts.map +1 -1
  42. package/dist/tools/gitIssues.js +43 -12
  43. package/dist/tools/gitIssues.js.map +1 -1
  44. package/dist/tools/gitMonitor.d.ts.map +1 -1
  45. package/dist/tools/gitMonitor.js +9 -10
  46. package/dist/tools/gitMonitor.js.map +1 -1
  47. package/dist/tools/gitPulls.d.ts.map +1 -1
  48. package/dist/tools/gitPulls.js +23 -4
  49. package/dist/tools/gitPulls.js.map +1 -1
  50. package/dist/tools/gitRelease.d.ts.map +1 -1
  51. package/dist/tools/gitRelease.js +34 -3
  52. package/dist/tools/gitRelease.js.map +1 -1
  53. package/dist/tools/gitRemote.d.ts +6 -1
  54. package/dist/tools/gitRemote.d.ts.map +1 -1
  55. package/dist/tools/gitRemote.js +17 -9
  56. package/dist/tools/gitRemote.js.map +1 -1
  57. package/dist/tools/gitReset.d.ts.map +1 -1
  58. package/dist/tools/gitReset.js +6 -7
  59. package/dist/tools/gitReset.js.map +1 -1
  60. package/dist/tools/gitStash.d.ts +2 -2
  61. package/dist/tools/gitStash.d.ts.map +1 -1
  62. package/dist/tools/gitStash.js +21 -25
  63. package/dist/tools/gitStash.js.map +1 -1
  64. package/dist/tools/gitSync.d.ts.map +1 -1
  65. package/dist/tools/gitSync.js +16 -19
  66. package/dist/tools/gitSync.js.map +1 -1
  67. package/dist/tools/gitTags.d.ts.map +1 -1
  68. package/dist/tools/gitTags.js +12 -23
  69. package/dist/tools/gitTags.js.map +1 -1
  70. package/dist/tools/gitUpdate.d.ts.map +1 -1
  71. package/dist/tools/gitUpdate.js +28 -41
  72. package/dist/tools/gitUpdate.js.map +1 -1
  73. package/dist/tools/gitUpload.d.ts.map +1 -1
  74. package/dist/tools/gitUpload.js +19 -21
  75. package/dist/tools/gitUpload.js.map +1 -1
  76. package/dist/tools/gitWorkflow.d.ts.map +1 -1
  77. package/dist/tools/gitWorkflow.js +21 -39
  78. package/dist/tools/gitWorkflow.js.map +1 -1
  79. package/dist/types.d.ts +2 -0
  80. package/dist/types.d.ts.map +1 -1
  81. package/dist/utils/apiHelpers.d.ts +30 -0
  82. package/dist/utils/apiHelpers.d.ts.map +1 -0
  83. package/dist/utils/apiHelpers.js +126 -0
  84. package/dist/utils/apiHelpers.js.map +1 -0
  85. package/dist/utils/gitAdapter.d.ts +222 -0
  86. package/dist/utils/gitAdapter.d.ts.map +1 -0
  87. package/dist/utils/gitAdapter.js +1071 -0
  88. package/dist/utils/gitAdapter.js.map +1 -0
  89. package/package.json +12 -4
@@ -0,0 +1,1071 @@
1
+ import * as git from 'isomorphic-git';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import http from 'isomorphic-git/http/node';
5
+ // ============================================================================
6
+ // ISOMORPHIC GIT ADAPTER IMPLEMENTATION
7
+ // ============================================================================
8
+ export class IsomorphicGitAdapter {
9
+ constructor(providerManager) {
10
+ this.providerManager = providerManager;
11
+ }
12
+ // ========================================================================
13
+ // HELPER METHODS
14
+ // ========================================================================
15
+ /**
16
+ * Get authentication callback for HTTP operations
17
+ */
18
+ getAuthCallback(remote) {
19
+ return () => {
20
+ // Determine if this is GitHub or Gitea based on remote URL
21
+ const isGitHub = remote.includes('github.com');
22
+ if (isGitHub) {
23
+ const token = process.env.GITHUB_TOKEN;
24
+ if (!token) {
25
+ throw new Error('GITHUB_TOKEN not found in environment variables');
26
+ }
27
+ return {
28
+ username: token,
29
+ password: 'x-oauth-basic',
30
+ };
31
+ }
32
+ else {
33
+ const token = process.env.GITEA_TOKEN;
34
+ if (!token) {
35
+ throw new Error('GITEA_TOKEN not found in environment variables');
36
+ }
37
+ return {
38
+ username: token,
39
+ password: 'x-oauth-basic',
40
+ };
41
+ }
42
+ };
43
+ }
44
+ /**
45
+ * Get author information from provider or fallback to config/default
46
+ */
47
+ async getAuthor(dir, providedAuthor) {
48
+ if (providedAuthor) {
49
+ return {
50
+ ...providedAuthor,
51
+ timestamp: providedAuthor.timestamp || Math.floor(Date.now() / 1000),
52
+ timezoneOffset: providedAuthor.timezoneOffset || new Date().getTimezoneOffset(),
53
+ };
54
+ }
55
+ // Try to get from provider manager (GitHub/Gitea username)
56
+ try {
57
+ const githubToken = process.env.GITHUB_TOKEN;
58
+ const giteaToken = process.env.GITEA_TOKEN;
59
+ if (githubToken && this.providerManager.github) {
60
+ const user = await this.providerManager.github.rest.users.getAuthenticated();
61
+ return {
62
+ name: user.data.name || user.data.login,
63
+ email: user.data.email || `${user.data.login}@users.noreply.github.com`,
64
+ timestamp: Math.floor(Date.now() / 1000),
65
+ timezoneOffset: new Date().getTimezoneOffset(),
66
+ };
67
+ }
68
+ if (giteaToken && this.providerManager.giteaBaseUrl) {
69
+ const axios = (await import('axios')).default;
70
+ const response = await axios.get(`${this.providerManager.giteaBaseUrl}/api/v1/user`, {
71
+ headers: { Authorization: `token ${giteaToken}` },
72
+ });
73
+ const user = response.data;
74
+ return {
75
+ name: user.full_name || user.login,
76
+ email: user.email || `${user.login}@gitea.local`,
77
+ timestamp: Math.floor(Date.now() / 1000),
78
+ timezoneOffset: new Date().getTimezoneOffset(),
79
+ };
80
+ }
81
+ }
82
+ catch (err) {
83
+ // Continue to fallback
84
+ }
85
+ // Try to get from local Git config
86
+ try {
87
+ const name = await git.getConfig({ fs, dir, path: 'user.name' });
88
+ const email = await git.getConfig({ fs, dir, path: 'user.email' });
89
+ if (name && email) {
90
+ return {
91
+ name,
92
+ email,
93
+ timestamp: Math.floor(Date.now() / 1000),
94
+ timezoneOffset: new Date().getTimezoneOffset(),
95
+ };
96
+ }
97
+ }
98
+ catch (err) {
99
+ // Continue to fallback
100
+ }
101
+ // Default fallback
102
+ return {
103
+ name: 'MCP User',
104
+ email: 'mcp@localhost',
105
+ timestamp: Math.floor(Date.now() / 1000),
106
+ timezoneOffset: new Date().getTimezoneOffset(),
107
+ };
108
+ }
109
+ /**
110
+ * Convert isomorphic-git status matrix to StatusResult format
111
+ */
112
+ async convertStatusMatrix(dir, matrix) {
113
+ const FILE = 0, HEAD = 1, WORKDIR = 2, STAGE = 3;
114
+ const modified = [];
115
+ const created = [];
116
+ const deleted = [];
117
+ const not_added = [];
118
+ const files = [];
119
+ for (const row of matrix) {
120
+ const filepath = row[FILE];
121
+ const headStatus = row[HEAD];
122
+ const workdirStatus = row[WORKDIR];
123
+ const stageStatus = row[STAGE];
124
+ // Determine status
125
+ if (headStatus === 0 && workdirStatus === 2 && stageStatus === 0) {
126
+ // New file, not staged
127
+ not_added.push(filepath);
128
+ files.push({ path: filepath, working_dir: 'new' });
129
+ }
130
+ else if (headStatus === 0 && workdirStatus === 2 && stageStatus === 2) {
131
+ // New file, staged
132
+ created.push(filepath);
133
+ files.push({ path: filepath, index: 'new', working_dir: 'new' });
134
+ }
135
+ else if (headStatus === 1 && workdirStatus === 2 && stageStatus === 1) {
136
+ // Modified file, not staged
137
+ not_added.push(filepath);
138
+ files.push({ path: filepath, working_dir: 'modified' });
139
+ }
140
+ else if (headStatus === 1 && workdirStatus === 2 && stageStatus === 2) {
141
+ // Modified file, staged
142
+ modified.push(filepath);
143
+ files.push({ path: filepath, index: 'modified', working_dir: 'modified' });
144
+ }
145
+ else if (headStatus === 1 && workdirStatus === 0 && stageStatus === 1) {
146
+ // Deleted file, not staged
147
+ not_added.push(filepath);
148
+ files.push({ path: filepath, working_dir: 'deleted' });
149
+ }
150
+ else if (headStatus === 1 && workdirStatus === 0 && stageStatus === 0) {
151
+ // Deleted file, staged
152
+ deleted.push(filepath);
153
+ files.push({ path: filepath, index: 'deleted', working_dir: 'deleted' });
154
+ }
155
+ }
156
+ const current = await this.getCurrentBranch(dir);
157
+ const staged = [...modified, ...created, ...deleted]; // All staged files
158
+ const isClean = modified.length === 0 && created.length === 0 &&
159
+ deleted.length === 0 && not_added.length === 0;
160
+ return {
161
+ modified,
162
+ created,
163
+ deleted,
164
+ renamed: [], // isomorphic-git doesn't detect renames automatically
165
+ not_added,
166
+ staged,
167
+ conflicted: [],
168
+ current,
169
+ tracking: null, // Will be implemented in remote operations
170
+ ahead: 0,
171
+ behind: 0,
172
+ isClean,
173
+ files,
174
+ };
175
+ }
176
+ // ========================================================================
177
+ // REPOSITORY OPERATIONS
178
+ // ========================================================================
179
+ async init(dir, defaultBranch = 'master') {
180
+ await git.init({
181
+ fs,
182
+ dir,
183
+ defaultBranch,
184
+ });
185
+ }
186
+ async status(dir) {
187
+ const matrix = await git.statusMatrix({
188
+ fs,
189
+ dir,
190
+ });
191
+ return this.convertStatusMatrix(dir, matrix);
192
+ }
193
+ // ========================================================================
194
+ // STAGING OPERATIONS
195
+ // ========================================================================
196
+ async add(dir, files) {
197
+ for (const filepath of files) {
198
+ if (filepath === '.') {
199
+ // Add all files
200
+ const matrix = await git.statusMatrix({ fs, dir });
201
+ for (const row of matrix) {
202
+ const file = row[0];
203
+ const workdirStatus = row[2];
204
+ if (workdirStatus === 2) { // File exists in workdir
205
+ await git.add({ fs, dir, filepath: file });
206
+ }
207
+ else if (workdirStatus === 0 && row[1] === 1) { // File deleted
208
+ await git.remove({ fs, dir, filepath: file });
209
+ }
210
+ }
211
+ }
212
+ else {
213
+ await git.add({ fs, dir, filepath });
214
+ }
215
+ }
216
+ }
217
+ async remove(dir, files) {
218
+ for (const filepath of files) {
219
+ await git.remove({ fs, dir, filepath });
220
+ }
221
+ }
222
+ // ========================================================================
223
+ // COMMIT OPERATIONS
224
+ // ========================================================================
225
+ async commit(dir, message, providedAuthor) {
226
+ const author = await this.getAuthor(dir, providedAuthor);
227
+ const sha = await git.commit({
228
+ fs,
229
+ dir,
230
+ message,
231
+ author: {
232
+ name: author.name,
233
+ email: author.email,
234
+ timestamp: author.timestamp,
235
+ timezoneOffset: author.timezoneOffset,
236
+ },
237
+ });
238
+ return sha;
239
+ }
240
+ // ========================================================================
241
+ // BRANCH OPERATIONS
242
+ // ========================================================================
243
+ async listBranches(dir, remote = false) {
244
+ const branches = remote
245
+ ? await git.listBranches({ fs, dir, remote: 'origin' })
246
+ : await git.listBranches({ fs, dir });
247
+ return branches;
248
+ }
249
+ async createBranch(dir, branchName, startPoint) {
250
+ // Simply create branch - isomorphic-git creates it at current HEAD by default
251
+ await git.branch({ fs, dir, ref: branchName });
252
+ }
253
+ async deleteBranch(dir, branchName, force) {
254
+ // Note: isomorphic-git doesn't have a force option for branch deletion
255
+ // We'll implement it by checking if branch is merged
256
+ await git.deleteBranch({ fs, dir, ref: branchName });
257
+ }
258
+ async renameBranch(dir, oldName, newName) {
259
+ // isomorphic-git doesn't have renameBranch, so we rename the ref file directly
260
+ const currentBranch = await this.getCurrentBranch(dir);
261
+ const wasOnOldBranch = currentBranch === oldName;
262
+ // Read the old branch ref
263
+ const oldRefPath = path.join(dir, '.git', 'refs', 'heads', oldName);
264
+ const newRefPath = path.join(dir, '.git', 'refs', 'heads', newName);
265
+ if (!fs.existsSync(oldRefPath)) {
266
+ throw new Error(`Branch ${oldName} does not exist`);
267
+ }
268
+ // Copy ref to new location
269
+ const refContent = fs.readFileSync(oldRefPath, 'utf8');
270
+ fs.writeFileSync(newRefPath, refContent);
271
+ // Delete old ref
272
+ fs.unlinkSync(oldRefPath);
273
+ // If we were on the old branch, update HEAD
274
+ if (wasOnOldBranch) {
275
+ const headPath = path.join(dir, '.git', 'HEAD');
276
+ fs.writeFileSync(headPath, `ref: refs/heads/${newName}\n`);
277
+ }
278
+ }
279
+ async checkout(dir, ref) {
280
+ await git.checkout({
281
+ fs,
282
+ dir,
283
+ ref,
284
+ force: false,
285
+ });
286
+ }
287
+ async getCurrentBranch(dir) {
288
+ try {
289
+ const branch = await git.currentBranch({ fs, dir, fullname: false });
290
+ return branch || 'HEAD';
291
+ }
292
+ catch (err) {
293
+ return 'HEAD';
294
+ }
295
+ }
296
+ // ========================================================================
297
+ // REMOTE OPERATIONS (Placeholder - will be implemented in next step)
298
+ // ========================================================================
299
+ async addRemote(dir, name, url) {
300
+ await git.addRemote({ fs, dir, remote: name, url });
301
+ }
302
+ async removeRemote(dir, name) {
303
+ await git.deleteRemote({ fs, dir, remote: name });
304
+ }
305
+ async listRemotes(dir) {
306
+ const remotes = await git.listRemotes({ fs, dir });
307
+ return remotes.map((r) => ({
308
+ name: r.remote,
309
+ remote: r.remote,
310
+ url: r.url,
311
+ fetch: r.url,
312
+ }));
313
+ }
314
+ async fetch(dir, remote, ref) {
315
+ const remotes = await git.listRemotes({ fs, dir });
316
+ const remoteUrl = remotes.find(r => r.remote === remote)?.url || '';
317
+ const onAuth = this.getAuthCallback(remoteUrl);
318
+ await git.fetch({
319
+ fs,
320
+ http,
321
+ dir,
322
+ remote,
323
+ ref,
324
+ onAuth,
325
+ singleBranch: !!ref,
326
+ });
327
+ }
328
+ async pull(dir, remote, branch) {
329
+ await git.pull({
330
+ fs,
331
+ http,
332
+ dir,
333
+ ref: branch,
334
+ remote,
335
+ onAuth: this.getAuthCallback(remote),
336
+ singleBranch: true,
337
+ });
338
+ }
339
+ async push(dir, remote, branch, force) {
340
+ // Get remote URL to detect provider
341
+ const remotes = await git.listRemotes({ fs, dir });
342
+ const remoteInfo = remotes.find(r => r.remote === remote);
343
+ if (!remoteInfo) {
344
+ throw new Error(`Remote '${remote}' not found`);
345
+ }
346
+ // Determine authentication based on provider
347
+ let onAuth;
348
+ if (remoteInfo.url.includes('github.com')) {
349
+ // GitHub: token as username, 'x-oauth-basic' as password
350
+ const token = process.env.GITHUB_TOKEN;
351
+ if (token) {
352
+ onAuth = () => ({
353
+ username: token,
354
+ password: 'x-oauth-basic'
355
+ });
356
+ }
357
+ }
358
+ else if (remoteInfo.url.includes('gitlab.com')) {
359
+ // GitLab: 'oauth2' as username, token as password
360
+ const token = process.env.GITLAB_TOKEN;
361
+ if (token) {
362
+ onAuth = () => ({
363
+ username: 'oauth2',
364
+ password: token
365
+ });
366
+ }
367
+ }
368
+ else {
369
+ // Gitea and others: username as username, token as password
370
+ const username = process.env.GITEA_USERNAME || 'git';
371
+ const token = process.env.GITEA_TOKEN;
372
+ if (token) {
373
+ onAuth = () => ({
374
+ username: username,
375
+ password: token
376
+ });
377
+ }
378
+ }
379
+ try {
380
+ // Set upstream tracking automatically
381
+ const currentBranch = await this.getCurrentBranch(dir);
382
+ const refToPush = branch.startsWith('refs/') ? branch : `refs/heads/${branch}`;
383
+ await git.push({
384
+ fs,
385
+ http,
386
+ dir,
387
+ remote,
388
+ ref: refToPush,
389
+ remoteRef: refToPush,
390
+ onAuth,
391
+ force,
392
+ onAuthFailure: () => {
393
+ throw new Error(`Authentication failed for remote '${remote}'`);
394
+ }
395
+ });
396
+ // Set upstream tracking branch configuration
397
+ if (!branch.startsWith('refs/') && currentBranch === branch) {
398
+ await git.setConfig({
399
+ fs,
400
+ dir,
401
+ path: `branch.${branch}.remote`,
402
+ value: remote
403
+ });
404
+ await git.setConfig({
405
+ fs,
406
+ dir,
407
+ path: `branch.${branch}.merge`,
408
+ value: `refs/heads/${branch}`
409
+ });
410
+ }
411
+ }
412
+ catch (error) {
413
+ if (error.code === 'HTTP401Unauthorized' || error.code === 'HTTP403Forbidden') {
414
+ throw new Error(`Push failed: Authentication error for ${remote}. ` +
415
+ `Check your token and permissions. Error: ${error.message}`);
416
+ }
417
+ throw error;
418
+ }
419
+ }
420
+ // ========================================================================
421
+ // MERGE OPERATIONS (Placeholder - will be implemented in next step)
422
+ // ========================================================================
423
+ async merge(dir, theirBranch, author) {
424
+ const commitAuthor = await this.getAuthor(dir, author);
425
+ const currentBranch = await this.getCurrentBranch(dir);
426
+ try {
427
+ await git.merge({
428
+ fs,
429
+ dir,
430
+ ours: currentBranch,
431
+ theirs: theirBranch,
432
+ author: {
433
+ name: commitAuthor.name,
434
+ email: commitAuthor.email,
435
+ },
436
+ });
437
+ // Atualizar working directory com os arquivos merged
438
+ await git.checkout({
439
+ fs,
440
+ dir,
441
+ ref: currentBranch,
442
+ force: true,
443
+ });
444
+ return {
445
+ success: true,
446
+ message: `Merged ${theirBranch} into ${currentBranch}`,
447
+ };
448
+ }
449
+ catch (err) {
450
+ if (err.code === 'MergeNotSupportedError') {
451
+ return {
452
+ success: false,
453
+ conflicts: err.data?.conflicts || [],
454
+ message: `Merge conflict detected. Files with conflicts: ${err.data?.conflicts?.join(', ') || 'unknown'}. Please resolve conflicts manually.`,
455
+ };
456
+ }
457
+ throw err;
458
+ }
459
+ }
460
+ // ========================================================================
461
+ // TAG OPERATIONS (Placeholder - will be implemented in next step)
462
+ // ========================================================================
463
+ async listTags(dir) {
464
+ const tags = await git.listTags({ fs, dir });
465
+ return tags;
466
+ }
467
+ async createTag(dir, tagName, ref = 'HEAD', message) {
468
+ const oid = await git.resolveRef({ fs, dir, ref });
469
+ if (message) {
470
+ const author = await this.getAuthor(dir);
471
+ await git.annotatedTag({
472
+ fs,
473
+ dir,
474
+ ref: tagName,
475
+ object: oid,
476
+ message,
477
+ tagger: {
478
+ name: author.name,
479
+ email: author.email,
480
+ timestamp: author.timestamp,
481
+ timezoneOffset: author.timezoneOffset,
482
+ },
483
+ });
484
+ }
485
+ else {
486
+ await git.tag({
487
+ fs,
488
+ dir,
489
+ ref: tagName,
490
+ object: oid,
491
+ });
492
+ }
493
+ }
494
+ async deleteTag(dir, tagName) {
495
+ await git.deleteTag({ fs, dir, ref: tagName });
496
+ }
497
+ // ========================================================================
498
+ // HISTORY OPERATIONS (Placeholder - will be implemented in next step)
499
+ // ========================================================================
500
+ async log(dir, options) {
501
+ const commits = await git.log({
502
+ fs,
503
+ dir,
504
+ ref: options?.ref || 'HEAD',
505
+ depth: options?.maxCount,
506
+ });
507
+ return commits.map(commit => ({
508
+ hash: commit.oid,
509
+ date: new Date(commit.commit.author.timestamp * 1000).toISOString(),
510
+ message: commit.commit.message,
511
+ author_name: commit.commit.author.name,
512
+ author_email: commit.commit.author.email,
513
+ refs: '', // Will be populated if needed
514
+ }));
515
+ }
516
+ async diff(dir, ref1, ref2) {
517
+ // Basic diff implementation - will be enhanced with walkers
518
+ return {
519
+ files: [],
520
+ summary: {
521
+ additions: 0,
522
+ deletions: 0,
523
+ changes: 0,
524
+ },
525
+ };
526
+ }
527
+ // ========================================================================
528
+ // RESET OPERATIONS (Placeholder - will be implemented in next step)
529
+ // ========================================================================
530
+ async reset(dir, ref, mode) {
531
+ // Resolve ref - handle HEAD~N syntax manually since isomorphic-git doesn't support it
532
+ let oid;
533
+ if (ref.match(/^HEAD~\d+$/)) {
534
+ // Extract number from HEAD~N
535
+ const steps = parseInt(ref.replace('HEAD~', ''));
536
+ const logs = await git.log({ fs, dir, depth: steps + 1 });
537
+ if (logs.length <= steps) {
538
+ throw new Error(`Not enough commits to resolve ${ref}`);
539
+ }
540
+ oid = logs[steps].oid;
541
+ }
542
+ else {
543
+ oid = await git.resolveRef({ fs, dir, ref });
544
+ }
545
+ const currentBranch = await this.getCurrentBranch(dir);
546
+ if (mode === 'hard') {
547
+ // Hard reset: move HEAD, update index, and update working directory
548
+ await git.writeRef({ fs, dir, ref: `refs/heads/${currentBranch}`, value: oid, force: true });
549
+ await git.checkout({ fs, dir, ref: oid, force: true });
550
+ }
551
+ else if (mode === 'mixed') {
552
+ // Mixed reset: move HEAD and update index, keep working directory
553
+ await git.writeRef({ fs, dir, ref: `refs/heads/${currentBranch}`, value: oid, force: true });
554
+ await git.checkout({ fs, dir, ref: oid });
555
+ }
556
+ else {
557
+ // Soft reset: only move HEAD
558
+ await git.writeRef({ fs, dir, ref: `refs/heads/${currentBranch}`, value: oid, force: true });
559
+ }
560
+ }
561
+ // ========================================================================
562
+ // STASH OPERATIONS - Hybrid implementation using refs/stash
563
+ // ========================================================================
564
+ /**
565
+ * Get stash reflog path
566
+ */
567
+ getStashReflogPath(dir) {
568
+ return path.join(dir, '.git', 'logs', 'refs', 'stash');
569
+ }
570
+ /**
571
+ * Get stash ref path
572
+ */
573
+ getStashRefPath(dir) {
574
+ return path.join(dir, '.git', 'refs', 'stash');
575
+ }
576
+ /**
577
+ * Read stash reflog entries
578
+ */
579
+ async readStashReflog(dir) {
580
+ const reflogPath = this.getStashReflogPath(dir);
581
+ if (!fs.existsSync(reflogPath)) {
582
+ return [];
583
+ }
584
+ const content = fs.readFileSync(reflogPath, 'utf8');
585
+ const lines = content.trim().split('\n').filter(l => l);
586
+ const entries = [];
587
+ for (let i = 0; i < lines.length; i++) {
588
+ const line = lines[i];
589
+ const match = line.match(/^(\w+) (\w+) .+ <.+> (\d+) [+-]\d+ (.+)$/);
590
+ if (match) {
591
+ const [, , newSha, timestamp, msg] = match;
592
+ entries.push({
593
+ index: i,
594
+ message: msg.replace(/^stash@\{\d+\}: /, ''),
595
+ date: new Date(parseInt(timestamp) * 1000),
596
+ ref: newSha,
597
+ });
598
+ }
599
+ }
600
+ return entries.reverse(); // Most recent first
601
+ }
602
+ /**
603
+ * Write stash reflog entry
604
+ */
605
+ async writeStashReflogEntry(dir, oldSha, newSha, message) {
606
+ const reflogPath = this.getStashReflogPath(dir);
607
+ const reflogDir = path.dirname(reflogPath);
608
+ // Create logs/refs directory if it doesn't exist
609
+ if (!fs.existsSync(reflogDir)) {
610
+ fs.mkdirSync(reflogDir, { recursive: true });
611
+ }
612
+ const author = await this.getAuthor(dir);
613
+ const timestamp = Math.floor(Date.now() / 1000);
614
+ const timezoneOffset = new Date().getTimezoneOffset();
615
+ const tzString = timezoneOffset <= 0
616
+ ? `+${String(Math.abs(timezoneOffset) / 60).padStart(2, '0')}00`
617
+ : `-${String(timezoneOffset / 60).padStart(2, '0')}00`;
618
+ const entry = `${oldSha} ${newSha} ${author.name} <${author.email}> ${timestamp} ${tzString} ${message}\n`;
619
+ fs.appendFileSync(reflogPath, entry);
620
+ }
621
+ async stashSave(dir, message, includeUntracked) {
622
+ const author = await this.getAuthor(dir);
623
+ const currentBranch = await this.getCurrentBranch(dir);
624
+ const stashMessage = message || `WIP on ${currentBranch}`;
625
+ // Get current HEAD
626
+ const headSha = await git.resolveRef({ fs, dir, ref: 'HEAD' });
627
+ // Create index commit (staged changes)
628
+ const matrix = await git.statusMatrix({ fs, dir });
629
+ const stagedFiles = [];
630
+ for (const row of matrix) {
631
+ const filepath = row[0];
632
+ const stageStatus = row[3];
633
+ if (stageStatus === 2) {
634
+ stagedFiles.push(filepath);
635
+ }
636
+ }
637
+ // Stage all changes for stash commit
638
+ for (const row of matrix) {
639
+ const filepath = row[0];
640
+ const workdirStatus = row[2];
641
+ if (workdirStatus === 2) {
642
+ await git.add({ fs, dir, filepath });
643
+ }
644
+ else if (workdirStatus === 0 && row[1] === 1) {
645
+ await git.remove({ fs, dir, filepath });
646
+ }
647
+ }
648
+ // Create stash commit
649
+ const stashSha = await git.commit({
650
+ fs,
651
+ dir,
652
+ message: `stash@{0}: ${stashMessage}`,
653
+ author: {
654
+ name: author.name,
655
+ email: author.email,
656
+ timestamp: author.timestamp,
657
+ timezoneOffset: author.timezoneOffset,
658
+ },
659
+ parent: [headSha],
660
+ });
661
+ // Update refs/stash
662
+ const stashRefPath = this.getStashRefPath(dir);
663
+ const stashRefDir = path.dirname(stashRefPath);
664
+ if (!fs.existsSync(stashRefDir)) {
665
+ fs.mkdirSync(stashRefDir, { recursive: true });
666
+ }
667
+ fs.writeFileSync(stashRefPath, stashSha + '\n');
668
+ // Write reflog entry
669
+ const oldStashSha = fs.existsSync(stashRefPath) ? fs.readFileSync(stashRefPath, 'utf8').trim() : '0000000000000000000000000000000000000000';
670
+ await this.writeStashReflogEntry(dir, oldStashSha, stashSha, `stash@{0}: ${stashMessage}`);
671
+ // Reset working directory to HEAD
672
+ await git.checkout({ fs, dir, ref: headSha, force: true });
673
+ // Restore originally staged files
674
+ for (const filepath of stagedFiles) {
675
+ await git.add({ fs, dir, filepath });
676
+ }
677
+ }
678
+ async stashList(dir) {
679
+ return await this.readStashReflog(dir);
680
+ }
681
+ async stashApply(dir, stashRef) {
682
+ const stashes = await this.readStashReflog(dir);
683
+ if (stashes.length === 0) {
684
+ throw new Error('No stash entries found');
685
+ }
686
+ let stashEntry;
687
+ if (stashRef) {
688
+ const match = stashRef.match(/stash@\{(\d+)\}/);
689
+ const index = match ? parseInt(match[1]) : 0;
690
+ stashEntry = stashes[index];
691
+ if (!stashEntry) {
692
+ throw new Error(`Stash entry not found: ${stashRef}`);
693
+ }
694
+ }
695
+ else {
696
+ stashEntry = stashes[0]; // Most recent
697
+ }
698
+ // Read commit to get files
699
+ const commit = await git.readCommit({ fs, dir, oid: stashEntry.ref });
700
+ // Checkout files from stash commit
701
+ await git.checkout({
702
+ fs,
703
+ dir,
704
+ ref: stashEntry.ref,
705
+ force: false,
706
+ filepaths: undefined,
707
+ });
708
+ }
709
+ async stashPop(dir, stashRef) {
710
+ await this.stashApply(dir, stashRef);
711
+ await this.stashDrop(dir, stashRef);
712
+ }
713
+ async stashDrop(dir, stashRef) {
714
+ const stashes = await this.readStashReflog(dir);
715
+ if (stashes.length === 0) {
716
+ throw new Error('No stash entries found');
717
+ }
718
+ let indexToDrop = 0;
719
+ if (stashRef) {
720
+ const match = stashRef.match(/stash@\{(\d+)\}/);
721
+ indexToDrop = match ? parseInt(match[1]) : 0;
722
+ }
723
+ if (indexToDrop >= stashes.length) {
724
+ throw new Error(`Stash entry not found: ${stashRef || 'stash@{0}'}`);
725
+ }
726
+ // Rewrite reflog without the dropped entry
727
+ const reflogPath = this.getStashReflogPath(dir);
728
+ const content = fs.readFileSync(reflogPath, 'utf8');
729
+ const lines = content.trim().split('\n').filter(l => l);
730
+ lines.splice(lines.length - 1 - indexToDrop, 1);
731
+ if (lines.length > 0) {
732
+ fs.writeFileSync(reflogPath, lines.join('\n') + '\n');
733
+ // Update refs/stash to point to the new top
734
+ const newTopLine = lines[lines.length - 1];
735
+ const match = newTopLine.match(/^(\w+) (\w+)/);
736
+ if (match) {
737
+ fs.writeFileSync(this.getStashRefPath(dir), match[2] + '\n');
738
+ }
739
+ }
740
+ else {
741
+ // No more stashes, remove ref and reflog
742
+ if (fs.existsSync(this.getStashRefPath(dir))) {
743
+ fs.unlinkSync(this.getStashRefPath(dir));
744
+ }
745
+ if (fs.existsSync(reflogPath)) {
746
+ fs.unlinkSync(reflogPath);
747
+ }
748
+ }
749
+ }
750
+ async stashClear(dir) {
751
+ const reflogPath = this.getStashReflogPath(dir);
752
+ const refPath = this.getStashRefPath(dir);
753
+ if (fs.existsSync(reflogPath)) {
754
+ fs.unlinkSync(reflogPath);
755
+ }
756
+ if (fs.existsSync(refPath)) {
757
+ fs.unlinkSync(refPath);
758
+ }
759
+ }
760
+ // ========================================================================
761
+ // CONFIG OPERATIONS - Hybrid implementation (local + global/system)
762
+ // ========================================================================
763
+ /**
764
+ * Get global Git config path (~/.gitconfig)
765
+ */
766
+ getGlobalConfigPath() {
767
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
768
+ return path.join(homeDir, '.gitconfig');
769
+ }
770
+ /**
771
+ * Get system Git config path
772
+ */
773
+ getSystemConfigPath() {
774
+ if (process.platform === 'win32') {
775
+ const programData = process.env.PROGRAMDATA || 'C:\\ProgramData';
776
+ return path.join(programData, 'Git', 'config');
777
+ }
778
+ else {
779
+ return '/etc/gitconfig';
780
+ }
781
+ }
782
+ /**
783
+ * Parse INI-style Git config file
784
+ */
785
+ parseGitConfig(content) {
786
+ const config = {};
787
+ const lines = content.split('\n');
788
+ let currentSection = '';
789
+ let currentSubsection = '';
790
+ for (const line of lines) {
791
+ const trimmed = line.trim();
792
+ // Skip empty lines and comments
793
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith(';')) {
794
+ continue;
795
+ }
796
+ // Match section headers: [section] or [section "subsection"]
797
+ const sectionMatch = trimmed.match(/^\[([^\]"]+)(?:\s+"([^"]+)")?\]$/);
798
+ if (sectionMatch) {
799
+ currentSection = sectionMatch[1];
800
+ currentSubsection = sectionMatch[2] || '';
801
+ continue;
802
+ }
803
+ // Match key-value pairs
804
+ const keyValueMatch = trimmed.match(/^([^=]+?)\s*=\s*(.*)$/);
805
+ if (keyValueMatch && currentSection) {
806
+ const key = keyValueMatch[1].trim();
807
+ const value = keyValueMatch[2].trim().replace(/^"|"$/g, ''); // Remove quotes
808
+ let fullKey = currentSection;
809
+ if (currentSubsection) {
810
+ fullKey += `.${currentSubsection}`;
811
+ }
812
+ fullKey += `.${key}`;
813
+ config[fullKey] = value;
814
+ }
815
+ }
816
+ return config;
817
+ }
818
+ /**
819
+ * Write INI-style Git config file
820
+ */
821
+ writeGitConfig(config) {
822
+ const sections = {};
823
+ // Group by section
824
+ for (const [fullKey, value] of Object.entries(config)) {
825
+ const parts = fullKey.split('.');
826
+ if (parts.length >= 2) {
827
+ const section = parts.slice(0, -1).join('.');
828
+ const key = parts[parts.length - 1];
829
+ if (!sections[section]) {
830
+ sections[section] = {};
831
+ }
832
+ sections[section][key] = value;
833
+ }
834
+ }
835
+ // Generate INI content
836
+ let content = '';
837
+ for (const [section, keys] of Object.entries(sections)) {
838
+ // Handle subsections: core.remote "origin" -> [core "origin"]
839
+ const sectionParts = section.split('.');
840
+ if (sectionParts.length > 1) {
841
+ content += `[${sectionParts[0]} "${sectionParts.slice(1).join('.')}"]\n`;
842
+ }
843
+ else {
844
+ content += `[${section}]\n`;
845
+ }
846
+ for (const [key, value] of Object.entries(keys)) {
847
+ if (value) { // Skip empty values
848
+ content += `\t${key} = ${value}\n`;
849
+ }
850
+ }
851
+ }
852
+ return content;
853
+ }
854
+ async getConfig(dir, key, scope) {
855
+ if (scope === 'global') {
856
+ const globalConfigPath = this.getGlobalConfigPath();
857
+ if (fs.existsSync(globalConfigPath)) {
858
+ const content = fs.readFileSync(globalConfigPath, 'utf8');
859
+ const config = this.parseGitConfig(content);
860
+ return config[key];
861
+ }
862
+ return undefined;
863
+ }
864
+ else if (scope === 'system') {
865
+ const systemConfigPath = this.getSystemConfigPath();
866
+ if (fs.existsSync(systemConfigPath)) {
867
+ const content = fs.readFileSync(systemConfigPath, 'utf8');
868
+ const config = this.parseGitConfig(content);
869
+ return config[key];
870
+ }
871
+ return undefined;
872
+ }
873
+ else {
874
+ // Local config via isomorphic-git
875
+ return await git.getConfig({ fs, dir, path: key });
876
+ }
877
+ }
878
+ async setConfig(dir, key, value, scope) {
879
+ if (scope === 'global') {
880
+ const globalConfigPath = this.getGlobalConfigPath();
881
+ let config = {};
882
+ if (fs.existsSync(globalConfigPath)) {
883
+ const content = fs.readFileSync(globalConfigPath, 'utf8');
884
+ config = this.parseGitConfig(content);
885
+ }
886
+ config[key] = value;
887
+ const newContent = this.writeGitConfig(config);
888
+ fs.writeFileSync(globalConfigPath, newContent);
889
+ }
890
+ else if (scope === 'system') {
891
+ const systemConfigPath = this.getSystemConfigPath();
892
+ let config = {};
893
+ if (fs.existsSync(systemConfigPath)) {
894
+ const content = fs.readFileSync(systemConfigPath, 'utf8');
895
+ config = this.parseGitConfig(content);
896
+ }
897
+ config[key] = value;
898
+ const newContent = this.writeGitConfig(config);
899
+ fs.writeFileSync(systemConfigPath, newContent);
900
+ }
901
+ else {
902
+ // Local config via isomorphic-git
903
+ await git.setConfig({ fs, dir, path: key, value });
904
+ }
905
+ }
906
+ async listConfig(dir, scope) {
907
+ if (scope === 'global') {
908
+ const globalConfigPath = this.getGlobalConfigPath();
909
+ if (fs.existsSync(globalConfigPath)) {
910
+ const content = fs.readFileSync(globalConfigPath, 'utf8');
911
+ return this.parseGitConfig(content);
912
+ }
913
+ return {};
914
+ }
915
+ else if (scope === 'system') {
916
+ const systemConfigPath = this.getSystemConfigPath();
917
+ if (fs.existsSync(systemConfigPath)) {
918
+ const content = fs.readFileSync(systemConfigPath, 'utf8');
919
+ return this.parseGitConfig(content);
920
+ }
921
+ return {};
922
+ }
923
+ else {
924
+ // Local config - read .git/config directly
925
+ const configPath = path.join(dir, '.git', 'config');
926
+ if (fs.existsSync(configPath)) {
927
+ const content = fs.readFileSync(configPath, 'utf8');
928
+ return this.parseGitConfig(content);
929
+ }
930
+ return {};
931
+ }
932
+ }
933
+ // Alias for backwards compatibility
934
+ async getAllConfig(dir, scope) {
935
+ return this.listConfig(dir, scope);
936
+ }
937
+ async unsetConfig(dir, key, scope) {
938
+ if (scope === 'global') {
939
+ const globalConfigPath = this.getGlobalConfigPath();
940
+ if (fs.existsSync(globalConfigPath)) {
941
+ const content = fs.readFileSync(globalConfigPath, 'utf8');
942
+ const config = this.parseGitConfig(content);
943
+ delete config[key];
944
+ const newContent = this.writeGitConfig(config);
945
+ fs.writeFileSync(globalConfigPath, newContent);
946
+ }
947
+ }
948
+ else if (scope === 'system') {
949
+ const systemConfigPath = this.getSystemConfigPath();
950
+ if (fs.existsSync(systemConfigPath)) {
951
+ const content = fs.readFileSync(systemConfigPath, 'utf8');
952
+ const config = this.parseGitConfig(content);
953
+ delete config[key];
954
+ const newContent = this.writeGitConfig(config);
955
+ fs.writeFileSync(systemConfigPath, newContent);
956
+ }
957
+ }
958
+ else {
959
+ // Local config - isomorphic-git doesn't have unsetConfig
960
+ // We'll read, modify, and write the .git/config file
961
+ const configPath = path.join(dir, '.git', 'config');
962
+ if (fs.existsSync(configPath)) {
963
+ const content = fs.readFileSync(configPath, 'utf8');
964
+ const config = this.parseGitConfig(content);
965
+ delete config[key];
966
+ const newContent = this.writeGitConfig(config);
967
+ fs.writeFileSync(configPath, newContent);
968
+ }
969
+ }
970
+ }
971
+ // ========================================================================
972
+ // RESET CONVENIENCE METHODS
973
+ // ========================================================================
974
+ async resetSoft(dir, ref) {
975
+ return this.reset(dir, ref, 'soft');
976
+ }
977
+ async resetMixed(dir, ref) {
978
+ return this.reset(dir, ref, 'mixed');
979
+ }
980
+ async resetHard(dir, ref) {
981
+ return this.reset(dir, ref, 'hard');
982
+ }
983
+ // ========================================================================
984
+ // GITIGNORE MANAGEMENT
985
+ // ========================================================================
986
+ async createGitignore(dir, patterns) {
987
+ const gitignorePath = path.join(dir, '.gitignore');
988
+ fs.writeFileSync(gitignorePath, patterns.join('\n') + '\n');
989
+ }
990
+ async addToGitignore(dir, patterns) {
991
+ const gitignorePath = path.join(dir, '.gitignore');
992
+ let existing = '';
993
+ if (fs.existsSync(gitignorePath)) {
994
+ existing = fs.readFileSync(gitignorePath, 'utf8');
995
+ }
996
+ const newPatterns = patterns.filter(p => !existing.includes(p));
997
+ if (newPatterns.length > 0) {
998
+ fs.appendFileSync(gitignorePath, '\n' + newPatterns.join('\n') + '\n');
999
+ }
1000
+ }
1001
+ async removeFromGitignore(dir, patterns) {
1002
+ const gitignorePath = path.join(dir, '.gitignore');
1003
+ if (!fs.existsSync(gitignorePath))
1004
+ return;
1005
+ let content = fs.readFileSync(gitignorePath, 'utf8');
1006
+ const lines = content.split('\n');
1007
+ const filtered = lines.filter(line => !patterns.includes(line.trim()));
1008
+ fs.writeFileSync(gitignorePath, filtered.join('\n'));
1009
+ }
1010
+ async listGitignore(dir) {
1011
+ const gitignorePath = path.join(dir, '.gitignore');
1012
+ if (!fs.existsSync(gitignorePath))
1013
+ return [];
1014
+ const content = fs.readFileSync(gitignorePath, 'utf8');
1015
+ return content.split('\n')
1016
+ .map(line => line.trim())
1017
+ .filter(line => line && !line.startsWith('#'));
1018
+ }
1019
+ // ========================================================================
1020
+ // FILE OPERATIONS
1021
+ // ========================================================================
1022
+ async readFile(dir, filepath, ref = 'HEAD') {
1023
+ try {
1024
+ const oid = await git.resolveRef({ fs, dir, ref });
1025
+ const { blob } = await git.readBlob({
1026
+ fs,
1027
+ dir,
1028
+ oid,
1029
+ filepath,
1030
+ });
1031
+ return Buffer.from(blob).toString('utf8');
1032
+ }
1033
+ catch (err) {
1034
+ // Fallback to reading from working directory
1035
+ const fullPath = path.join(dir, filepath);
1036
+ if (fs.existsSync(fullPath)) {
1037
+ return fs.readFileSync(fullPath, 'utf8');
1038
+ }
1039
+ throw new Error(`File not found: ${filepath}`);
1040
+ }
1041
+ }
1042
+ async listFiles(dir, ref = 'HEAD') {
1043
+ try {
1044
+ const oid = await git.resolveRef({ fs, dir, ref });
1045
+ const tree = await git.listFiles({ fs, dir, ref: oid });
1046
+ return tree;
1047
+ }
1048
+ catch (err) {
1049
+ // Fallback to reading working directory
1050
+ const files = [];
1051
+ const walk = (currentPath, relativePath = '') => {
1052
+ const entries = fs.readdirSync(currentPath, { withFileTypes: true });
1053
+ for (const entry of entries) {
1054
+ if (entry.name === '.git')
1055
+ continue;
1056
+ const fullPath = path.join(currentPath, entry.name);
1057
+ const relPath = path.join(relativePath, entry.name);
1058
+ if (entry.isDirectory()) {
1059
+ walk(fullPath, relPath);
1060
+ }
1061
+ else {
1062
+ files.push(relPath.replace(/\\/g, '/'));
1063
+ }
1064
+ }
1065
+ };
1066
+ walk(dir);
1067
+ return files;
1068
+ }
1069
+ }
1070
+ }
1071
+ //# sourceMappingURL=gitAdapter.js.map