@codesherlock/codesherlock-beta-mcp-server 0.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 (87) hide show
  1. package/.env +9 -0
  2. package/README.md +185 -0
  3. package/build/handlers/analyzeCommitHandler.d.ts +36 -0
  4. package/build/handlers/analyzeCommitHandler.d.ts.map +1 -0
  5. package/build/handlers/analyzeCommitHandler.js +397 -0
  6. package/build/handlers/analyzeCommitHandler.js.map +1 -0
  7. package/build/handlers/events.d.ts +7 -0
  8. package/build/handlers/events.d.ts.map +1 -0
  9. package/build/handlers/events.js +15 -0
  10. package/build/handlers/events.js.map +1 -0
  11. package/build/handlers/resources.d.ts +10 -0
  12. package/build/handlers/resources.d.ts.map +1 -0
  13. package/build/handlers/resources.js +14 -0
  14. package/build/handlers/resources.js.map +1 -0
  15. package/build/handlers/tools.d.ts +6 -0
  16. package/build/handlers/tools.d.ts.map +1 -0
  17. package/build/handlers/tools.js +29 -0
  18. package/build/handlers/tools.js.map +1 -0
  19. package/build/index.d.ts +3 -0
  20. package/build/index.d.ts.map +1 -0
  21. package/build/index.js +82 -0
  22. package/build/index.js.map +1 -0
  23. package/build/schemas/toolSchemas.d.ts +50 -0
  24. package/build/schemas/toolSchemas.d.ts.map +1 -0
  25. package/build/schemas/toolSchemas.js +48 -0
  26. package/build/schemas/toolSchemas.js.map +1 -0
  27. package/build/services/backendApiService.d.ts +81 -0
  28. package/build/services/backendApiService.d.ts.map +1 -0
  29. package/build/services/backendApiService.js +265 -0
  30. package/build/services/backendApiService.js.map +1 -0
  31. package/build/services/commitReviewService.d.ts +71 -0
  32. package/build/services/commitReviewService.d.ts.map +1 -0
  33. package/build/services/commitReviewService.js +506 -0
  34. package/build/services/commitReviewService.js.map +1 -0
  35. package/build/services/gitService.d.ts +159 -0
  36. package/build/services/gitService.d.ts.map +1 -0
  37. package/build/services/gitService.js +778 -0
  38. package/build/services/gitService.js.map +1 -0
  39. package/build/services/loggingService.d.ts +64 -0
  40. package/build/services/loggingService.d.ts.map +1 -0
  41. package/build/services/loggingService.js +185 -0
  42. package/build/services/loggingService.js.map +1 -0
  43. package/build/services/zipService.d.ts +9 -0
  44. package/build/services/zipService.d.ts.map +1 -0
  45. package/build/services/zipService.js +47 -0
  46. package/build/services/zipService.js.map +1 -0
  47. package/build/tests/analysisFormatter.test.d.ts +2 -0
  48. package/build/tests/analysisFormatter.test.d.ts.map +1 -0
  49. package/build/tests/analysisFormatter.test.js +92 -0
  50. package/build/tests/analysisFormatter.test.js.map +1 -0
  51. package/build/tests/analyzeCommitHandler.test.d.ts +2 -0
  52. package/build/tests/analyzeCommitHandler.test.d.ts.map +1 -0
  53. package/build/tests/analyzeCommitHandler.test.js +111 -0
  54. package/build/tests/analyzeCommitHandler.test.js.map +1 -0
  55. package/build/tests/backendApiService.test.d.ts +2 -0
  56. package/build/tests/backendApiService.test.d.ts.map +1 -0
  57. package/build/tests/backendApiService.test.js +109 -0
  58. package/build/tests/backendApiService.test.js.map +1 -0
  59. package/build/tests/commitReviewService.test.d.ts +2 -0
  60. package/build/tests/commitReviewService.test.d.ts.map +1 -0
  61. package/build/tests/commitReviewService.test.js +120 -0
  62. package/build/tests/commitReviewService.test.js.map +1 -0
  63. package/build/tests/errorExtractor.test.d.ts +2 -0
  64. package/build/tests/errorExtractor.test.d.ts.map +1 -0
  65. package/build/tests/errorExtractor.test.js +61 -0
  66. package/build/tests/errorExtractor.test.js.map +1 -0
  67. package/build/tests/loggingService.test.d.ts +2 -0
  68. package/build/tests/loggingService.test.d.ts.map +1 -0
  69. package/build/tests/loggingService.test.js +153 -0
  70. package/build/tests/loggingService.test.js.map +1 -0
  71. package/build/tests/setup.test.d.ts +2 -0
  72. package/build/tests/setup.test.d.ts.map +1 -0
  73. package/build/tests/setup.test.js +7 -0
  74. package/build/tests/setup.test.js.map +1 -0
  75. package/build/tests/tools.test.d.ts +2 -0
  76. package/build/tests/tools.test.d.ts.map +1 -0
  77. package/build/tests/tools.test.js +58 -0
  78. package/build/tests/tools.test.js.map +1 -0
  79. package/build/utils/analysisFormatter.d.ts +40 -0
  80. package/build/utils/analysisFormatter.d.ts.map +1 -0
  81. package/build/utils/analysisFormatter.js +97 -0
  82. package/build/utils/analysisFormatter.js.map +1 -0
  83. package/build/utils/errorExtractor.d.ts +36 -0
  84. package/build/utils/errorExtractor.d.ts.map +1 -0
  85. package/build/utils/errorExtractor.js +178 -0
  86. package/build/utils/errorExtractor.js.map +1 -0
  87. package/package.json +55 -0
@@ -0,0 +1,778 @@
1
+ import { simpleGit } from 'simple-git';
2
+ import { logger } from './loggingService.js';
3
+ /**
4
+ * Git Service
5
+ * Handles all Git-related operations like getting diffs, file content, etc.
6
+ *
7
+ * NOTE: simple-git StatusResult property mappings:
8
+ * - not_added: Untracked files (new files never added to git, shown as ?? in git status)
9
+ * - created: Newly staged files (added to index with `git add`, shown as A in git status)
10
+ * - staged: Modified files that are staged (shown as M in index)
11
+ * - modified: Modified files with unstaged changes (shown as M in working tree)
12
+ * - deleted: Deleted files (shown as D)
13
+ * - renamed: Renamed files (shown as R)
14
+ */
15
+ export class GitService {
16
+ getGitInstance(repoPath) {
17
+ return simpleGit(repoPath);
18
+ }
19
+ /**
20
+ * Get diff for a specific commit or PR
21
+ * @param repoPath - Path to the git repository
22
+ * @param commitHash - Commit hash or PR reference
23
+ * @returns Git diff content
24
+ */
25
+ async getDiff(repoPath, commitHash) {
26
+ try {
27
+ const git = this.getGitInstance(repoPath);
28
+ // If commitHash is empty or "HEAD", get unstaged changes
29
+ if (!commitHash || commitHash === 'HEAD') {
30
+ logger.logInfo('Fetching diff for HEAD (unstaged changes)', { repoPath });
31
+ return await git.diff();
32
+ }
33
+ // If commitHash contains '..' it's a range comparison
34
+ if (commitHash.includes('..')) {
35
+ logger.logInfo(`Fetching diff for range: ${commitHash}`, { repoPath });
36
+ return await git.diff([commitHash]);
37
+ }
38
+ // Otherwise, get diff for specific commit (comparing with parent)
39
+ logger.logInfo(`Fetching diff for commit: ${commitHash}`, { repoPath });
40
+ return await git.show([commitHash]);
41
+ }
42
+ catch (error) {
43
+ logger.logError('Error fetching diff', error, { repoPath, commitHash });
44
+ throw error;
45
+ }
46
+ }
47
+ /**
48
+ * Get diff for uncommitted changes (both staged and unstaged)
49
+ * @param repoPath - Path to the git repository
50
+ * @returns Git diff content for uncommitted changes
51
+ */
52
+ async getUncommittedDiff(repoPath) {
53
+ try {
54
+ const git = this.getGitInstance(repoPath);
55
+ // Get both staged and unstaged diffs
56
+ const stagedDiff = await git.diff(['--cached']);
57
+ const unstagedDiff = await git.diff();
58
+ // Combine both diffs
59
+ const combinedDiff = [stagedDiff, unstagedDiff]
60
+ .filter(diff => diff.trim())
61
+ .join('\n');
62
+ return combinedDiff;
63
+ }
64
+ catch (error) {
65
+ logger.logError('Error fetching uncommitted diff', error, { repoPath });
66
+ throw error;
67
+ }
68
+ }
69
+ /**
70
+ * Get file content from repository
71
+ * @param repoPath - Path to the git repository
72
+ * @param filePath - Path to the file within the repo
73
+ * @param ref - Git reference (branch, tag, commit hash)
74
+ * @returns File content as string
75
+ */
76
+ async getFileContent(repoPath, filePath, ref) {
77
+ try {
78
+ const git = this.getGitInstance(repoPath);
79
+ // If no ref is provided, get current working tree version
80
+ if (!ref) {
81
+ const { readFileSync, existsSync } = await import('fs');
82
+ const { join } = await import('path');
83
+ const fullPath = join(repoPath, filePath);
84
+ logger.logInfo('Reading file from working tree', {
85
+ filePath,
86
+ fullPath,
87
+ exists: String(existsSync(fullPath))
88
+ });
89
+ if (!existsSync(fullPath)) {
90
+ logger.logWarning('File does not exist in working tree', {
91
+ filePath,
92
+ fullPath
93
+ });
94
+ throw new Error(`File does not exist: ${fullPath}`);
95
+ }
96
+ const content = readFileSync(fullPath, 'utf-8');
97
+ logger.logInfo('File read from working tree', {
98
+ filePath,
99
+ contentLength: String(content.length),
100
+ lineCount: String(content.split('\n').length)
101
+ });
102
+ return content;
103
+ }
104
+ // Get file content from specific commit/branch/tag
105
+ logger.logInfo('Reading file from git commit', {
106
+ filePath,
107
+ ref,
108
+ gitCommand: `${ref}:${filePath}`
109
+ });
110
+ const content = await git.show([`${ref}:${filePath}`]);
111
+ logger.logInfo('File read from git commit', {
112
+ filePath,
113
+ ref,
114
+ contentLength: String(content.length),
115
+ lineCount: String(content.split('\n').length)
116
+ });
117
+ return content;
118
+ }
119
+ catch (error) {
120
+ logger.logError('Error fetching file content', error, {
121
+ repoPath,
122
+ filePath,
123
+ ref: ref || 'working tree',
124
+ errorMessage: String(error)
125
+ });
126
+ throw error;
127
+ }
128
+ }
129
+ /**
130
+ * Get list of changed files in a commit or PR
131
+ * @param repoPath - Path to the git repository
132
+ * @param commitHash - Commit hash or PR reference
133
+ * @returns Array of changed file paths
134
+ */
135
+ async getChangedFiles(repoPath, commitHash) {
136
+ try {
137
+ const git = this.getGitInstance(repoPath);
138
+ // If commitHash is empty, get unstaged and staged files
139
+ if (!commitHash) {
140
+ const status = await git.status();
141
+ // Collect all changed files from different categories:
142
+ // - modified: files with unstaged modifications
143
+ // - created: newly staged files (added to index)
144
+ // - not_added: untracked files (simple-git's name for untracked)
145
+ // - deleted: deleted files
146
+ // - renamed: renamed files (use the new path)
147
+ return [
148
+ ...status.modified,
149
+ ...status.created,
150
+ ...status.not_added,
151
+ ...status.deleted,
152
+ ...status.renamed.map(r => r.to)
153
+ ];
154
+ }
155
+ // Get files changed in specific commit
156
+ const diffSummary = await git.diffSummary([`${commitHash}^`, commitHash]);
157
+ return diffSummary.files.map(file => file.file);
158
+ }
159
+ catch (error) {
160
+ logger.logError('Error fetching changed files', error, { repoPath, commitHash });
161
+ throw error;
162
+ }
163
+ }
164
+ /**
165
+ * Get commit information
166
+ * @param repoPath - Path to the git repository
167
+ * @param commitHash - Commit hash
168
+ * @returns Commit metadata
169
+ */
170
+ async getCommitInfo(repoPath, commitHash) {
171
+ try {
172
+ const git = this.getGitInstance(repoPath);
173
+ // Get commit log
174
+ const log = await git.log([commitHash, '-1']);
175
+ if (!log.latest) {
176
+ throw new Error(`Commit ${commitHash} not found`);
177
+ }
178
+ const commit = log.latest;
179
+ // Get changed files for this commit
180
+ const changedFiles = await this.getChangedFiles(repoPath, commitHash);
181
+ return {
182
+ hash: commit.hash,
183
+ author: commit.author_name,
184
+ email: commit.author_email,
185
+ date: commit.date,
186
+ message: commit.message,
187
+ files: changedFiles
188
+ };
189
+ }
190
+ catch (error) {
191
+ logger.logError('Error fetching commit info', error, { repoPath, commitHash });
192
+ throw error;
193
+ }
194
+ }
195
+ /**
196
+ * Get repository status
197
+ * @param repoPath - Path to the git repository
198
+ * @returns Repository status information
199
+ *
200
+ * NOTE: simple-git uses these property names:
201
+ * - not_added = untracked files (files not in git)
202
+ * - created = newly staged files (git add on new file)
203
+ * - staged = modified files that are staged
204
+ */
205
+ async getRepoStatus(repoPath) {
206
+ try {
207
+ const git = this.getGitInstance(repoPath);
208
+ const status = await git.status();
209
+ return {
210
+ branch: status.current || 'HEAD',
211
+ modified: status.modified,
212
+ // Staged files include both newly created (added) and modified-then-staged
213
+ staged: [
214
+ ...status.created,
215
+ ...status.staged
216
+ ],
217
+ // Untracked files - simple-git calls these "not_added"
218
+ untracked: status.not_added
219
+ };
220
+ }
221
+ catch (error) {
222
+ logger.logError('Error fetching repo status', error, { repoPath });
223
+ throw error;
224
+ }
225
+ }
226
+ /**
227
+ * Get the hash of the current HEAD commit.
228
+ * Falls back to a placeholder when repository has no commits.
229
+ */
230
+ async getCurrentCommitHash(repoPath) {
231
+ try {
232
+ const git = this.getGitInstance(repoPath);
233
+ const hash = await git.revparse(["HEAD"]);
234
+ return hash.trim();
235
+ }
236
+ catch (error) {
237
+ // For repositories with no commits or when HEAD is unavailable
238
+ logger.logWarning("Unable to resolve current commit hash; using placeholder", {
239
+ repoPath,
240
+ error: String(error),
241
+ });
242
+ return "UNCOMMITTED";
243
+ }
244
+ }
245
+ /**
246
+ * Get diff for a specific file
247
+ * @param repoPath - Path to the git repository
248
+ * @param filePath - Path to the file
249
+ * @param commitHash - Commit hash (optional, if null/empty uses HEAD)
250
+ * @param fileStatus - Optional file status (untracked/added/modified/deleted) to optimize diff retrieval
251
+ * @returns Diff content
252
+ */
253
+ async getFileDiff(repoPath, filePath, commitHash, fileStatus) {
254
+ try {
255
+ const git = this.getGitInstance(repoPath);
256
+ // UNCOMMITTED changes
257
+ if (!commitHash) {
258
+ // Optimize: If file is untracked or added, skip git diff attempts and generate synthetic patch directly
259
+ if (fileStatus === 'untracked' || fileStatus === 'added') {
260
+ logger.logInfo(`Generating synthetic patch for ${fileStatus} file`, {
261
+ filePath,
262
+ repoPath,
263
+ status: fileStatus
264
+ });
265
+ const syntheticPatch = await this.buildUntrackedPatch(repoPath, filePath);
266
+ logger.logInfo('Synthetic patch generated', {
267
+ filePath,
268
+ patchLength: String(syntheticPatch.length)
269
+ });
270
+ return syntheticPatch;
271
+ }
272
+ logger.logInfo('Getting diff for uncommitted file', {
273
+ filePath,
274
+ repoPath,
275
+ step: 'checking staged diff first'
276
+ });
277
+ // 1. Try staged diff first (--cached)
278
+ try {
279
+ const staged = await git.diff(['--cached', '--', filePath]);
280
+ if (staged.trim()) {
281
+ logger.logInfo('Found staged diff', {
282
+ filePath,
283
+ diffLength: String(staged.length)
284
+ });
285
+ return staged;
286
+ }
287
+ else {
288
+ logger.logInfo('Staged diff is empty', { filePath });
289
+ }
290
+ }
291
+ catch (e) {
292
+ // Staged diff failed, continue to unstaged
293
+ logger.logInfo(`Staged diff failed for ${filePath}, trying unstaged`, {
294
+ error: String(e),
295
+ filePath
296
+ });
297
+ }
298
+ // 2. Try unstaged diff
299
+ logger.logInfo('Trying unstaged diff', { filePath });
300
+ try {
301
+ const unstaged = await git.diff(['--', filePath]);
302
+ if (unstaged.trim()) {
303
+ logger.logInfo('Found unstaged diff', {
304
+ filePath,
305
+ diffLength: String(unstaged.length)
306
+ });
307
+ return unstaged;
308
+ }
309
+ else {
310
+ logger.logInfo('Unstaged diff is empty', { filePath });
311
+ }
312
+ }
313
+ catch (e) {
314
+ // Unstaged diff failed, continue to check if untracked
315
+ logger.logInfo(`Unstaged diff failed for ${filePath}, checking if untracked`, {
316
+ error: String(e),
317
+ filePath
318
+ });
319
+ }
320
+ // 3. UNTRACKED FILE (NO DIFF EXISTS)
321
+ // Git diff does NOT show untracked files by default
322
+ // We must generate a synthetic patch
323
+ logger.logInfo(`No diff found for ${filePath}, generating synthetic patch for untracked/added file`, {
324
+ filePath,
325
+ repoPath
326
+ });
327
+ const syntheticPatch = await this.buildUntrackedPatch(repoPath, filePath);
328
+ logger.logInfo('Synthetic patch generated', {
329
+ filePath,
330
+ patchLength: String(syntheticPatch.length)
331
+ });
332
+ return syntheticPatch;
333
+ }
334
+ // Diff for specific commit
335
+ logger.logInfo('Getting diff for committed file', {
336
+ filePath,
337
+ commitHash,
338
+ gitCommand: `${commitHash}^..${commitHash}`
339
+ });
340
+ const diff = await git.diff([`${commitHash}^..${commitHash}`, '--', filePath]);
341
+ logger.logInfo('Committed file diff retrieved', {
342
+ filePath,
343
+ commitHash,
344
+ diffLength: String(diff.length)
345
+ });
346
+ return diff;
347
+ }
348
+ catch (error) {
349
+ logger.logError('Error fetching file diff', error, {
350
+ repoPath,
351
+ filePath,
352
+ commitHash: commitHash || 'working tree',
353
+ errorMessage: String(error)
354
+ });
355
+ throw error;
356
+ }
357
+ }
358
+ /**
359
+ * Build a synthetic git patch for untracked files
360
+ * Git does NOT diff untracked files, so we must create the patch ourselves
361
+ * @param repoPath - Path to the git repository
362
+ * @param filePath - Path to the untracked file
363
+ * @returns Synthetic git diff patch
364
+ */
365
+ async buildUntrackedPatch(repoPath, filePath) {
366
+ try {
367
+ logger.logInfo('Building synthetic patch for untracked/added file', {
368
+ filePath,
369
+ repoPath
370
+ });
371
+ const { readFileSync, existsSync } = await import('fs');
372
+ const { join } = await import('path');
373
+ const fullPath = join(repoPath, filePath);
374
+ logger.logInfo('Checking file existence for patch', {
375
+ filePath,
376
+ fullPath,
377
+ exists: String(existsSync(fullPath))
378
+ });
379
+ // Check if file exists
380
+ if (!existsSync(fullPath)) {
381
+ logger.logWarning(`File does not exist for untracked patch: ${filePath}`, {
382
+ filePath,
383
+ fullPath
384
+ });
385
+ return '';
386
+ }
387
+ logger.logInfo('Reading file content for synthetic patch', {
388
+ filePath,
389
+ fullPath
390
+ });
391
+ const content = readFileSync(fullPath, 'utf-8');
392
+ const lines = content.split('\n');
393
+ const lineCount = lines.length;
394
+ logger.logInfo('File content read for patch generation', {
395
+ filePath,
396
+ contentLength: String(content.length),
397
+ lineCount: String(lineCount)
398
+ });
399
+ // Generate proper git diff format for new file
400
+ const patchLines = [
401
+ `diff --git a/${filePath} b/${filePath}`,
402
+ `new file mode 100644`,
403
+ `index 0000000..e69de29`,
404
+ `--- /dev/null`,
405
+ `+++ b/${filePath}`,
406
+ ];
407
+ if (lineCount > 0) {
408
+ // Add hunk header
409
+ patchLines.push(`@@ -0,0 +1,${lineCount} @@`);
410
+ // Add lines with + prefix
411
+ for (const line of lines) {
412
+ patchLines.push(`+${line}`);
413
+ }
414
+ }
415
+ else {
416
+ // Empty file
417
+ patchLines.push(`@@ -0,0 +1,0 @@`);
418
+ }
419
+ return patchLines.join('\n');
420
+ }
421
+ catch (error) {
422
+ logger.logError('Error building untracked patch', error, { repoPath, filePath });
423
+ // Return empty string on error rather than throwing
424
+ // This allows the pipeline to continue with other files
425
+ return '';
426
+ }
427
+ }
428
+ /**
429
+ * Get changed files with status
430
+ * @param repoPath - Path to the git repository
431
+ * @param commitHash - Commit hash (optional)
432
+ * @returns Array of objects with path and status
433
+ *
434
+ * NOTE: simple-git file status codes:
435
+ * - file.index: status in the staging area (index)
436
+ * - file.working_dir: status in the working directory
437
+ * - '?' = untracked
438
+ * - 'A' = added (staged new file)
439
+ * - 'M' = modified
440
+ * - 'D' = deleted
441
+ * - 'R' = renamed
442
+ * - ' ' = unmodified
443
+ */
444
+ async getChangedFilesWithStatus(repoPath, commitHash) {
445
+ try {
446
+ const git = this.getGitInstance(repoPath);
447
+ if (!commitHash) {
448
+ // Get status including untracked files
449
+ logger.logInfo('Getting git status for uncommitted changes', { repoPath });
450
+ const status = await git.status();
451
+ // Log raw status information
452
+ logger.logInfo('Git status retrieved', {
453
+ repoPath,
454
+ totalFiles: String(status.files?.length || 0),
455
+ modifiedCount: String(status.modified?.length || 0),
456
+ createdCount: String(status.created?.length || 0),
457
+ notAddedCount: String(status.not_added?.length || 0),
458
+ deletedCount: String(status.deleted?.length || 0),
459
+ renamedCount: String(status.renamed?.length || 0),
460
+ stagedCount: String(status.staged?.length || 0)
461
+ });
462
+ // Ensure we have files array
463
+ if (!status.files || status.files.length === 0) {
464
+ logger.logInfo('No changed files found in repository status', { repoPath });
465
+ return [];
466
+ }
467
+ // Log all files with their raw status codes
468
+ logger.logInfo('Raw git status files', {
469
+ files: JSON.stringify(status.files.map(f => ({
470
+ path: f.path,
471
+ index: f.index,
472
+ working_dir: f.working_dir
473
+ })))
474
+ });
475
+ const filesWithStatus = status.files.map(file => {
476
+ if (!file.path) {
477
+ logger.logWarning('File entry missing path in status', { file: JSON.stringify(file) });
478
+ return null;
479
+ }
480
+ // Determine file status based on simple-git's index and working_dir codes
481
+ const fileStatus = this.determineFileStatus(file.index, file.working_dir);
482
+ logger.logInfo('File status determined', {
483
+ path: file.path,
484
+ indexCode: file.index,
485
+ workingDirCode: file.working_dir,
486
+ determinedStatus: fileStatus
487
+ });
488
+ return {
489
+ path: file.path,
490
+ status: fileStatus
491
+ };
492
+ }).filter((file) => file !== null);
493
+ // Log summary of files by status
494
+ const filesByStatus = filesWithStatus.reduce((acc, file) => {
495
+ acc[file.status] = (acc[file.status] || 0) + 1;
496
+ return acc;
497
+ }, {});
498
+ logger.logInfo('Files grouped by status', {
499
+ repoPath,
500
+ filesByStatus: JSON.stringify(filesByStatus),
501
+ totalFiles: String(filesWithStatus.length),
502
+ fileList: JSON.stringify(filesWithStatus.map(f => `${f.path} (${f.status})`))
503
+ });
504
+ return filesWithStatus;
505
+ }
506
+ // Get files changed in specific commit with status
507
+ const result = await git.raw(['show', '--name-status', '--format=', commitHash]);
508
+ return result.trim().split('\n').filter(Boolean).map(line => {
509
+ const parts = line.split('\t');
510
+ const code = parts[0][0];
511
+ let path = parts[1];
512
+ let statusStr = 'modified';
513
+ if (code === 'A')
514
+ statusStr = 'added';
515
+ else if (code === 'D')
516
+ statusStr = 'deleted';
517
+ else if (code === 'R') {
518
+ statusStr = 'renamed';
519
+ path = parts[2] || parts[1];
520
+ }
521
+ return { status: statusStr, path };
522
+ });
523
+ }
524
+ catch (error) {
525
+ logger.logError('Error fetching changed files with status', error, { repoPath, commitHash: commitHash || 'HEAD' });
526
+ throw error;
527
+ }
528
+ }
529
+ /**
530
+ * Determine file status from simple-git's index and working_dir codes
531
+ * @param indexCode - Status code in the staging area
532
+ * @param workingDirCode - Status code in the working directory
533
+ * @returns Human-readable status string
534
+ */
535
+ determineFileStatus(indexCode, workingDirCode) {
536
+ // Untracked files have '?' in both index and working_dir
537
+ if (indexCode === '?' && workingDirCode === '?') {
538
+ return 'untracked';
539
+ }
540
+ // Added files (staged new files) have 'A' in index
541
+ if (indexCode === 'A') {
542
+ return 'added';
543
+ }
544
+ // Deleted files have 'D' in either index or working_dir
545
+ if (indexCode === 'D' || workingDirCode === 'D') {
546
+ return 'deleted';
547
+ }
548
+ // Renamed files have 'R' in either index or working_dir
549
+ if (indexCode === 'R' || workingDirCode === 'R') {
550
+ return 'renamed';
551
+ }
552
+ // Default to 'modified' for other cases (M = Modified)
553
+ return 'modified';
554
+ }
555
+ /**
556
+ * Analyze git changes for a commit or uncommitted changes
557
+ * @param uncommitted - If true, analyze uncommitted changes. If false, analyze HEAD.
558
+ * @param directory - Path to git repository
559
+ */
560
+ async analyzeGitChanges(uncommitted, directory) {
561
+ try {
562
+ logger.logInfo(`Analyzing git changes`, {
563
+ directory,
564
+ uncommitted: String(uncommitted),
565
+ targetHash: uncommitted ? 'working tree' : 'HEAD'
566
+ });
567
+ const targetHash = uncommitted ? undefined : 'HEAD';
568
+ const changes = await this.getChangedFilesWithStatus(directory, targetHash);
569
+ logger.logInfo(`Found ${changes.length} changed files`, {
570
+ directory,
571
+ uncommitted: String(uncommitted),
572
+ files: JSON.stringify(changes.map(c => `${c.path} (${c.status})`))
573
+ });
574
+ const results = [];
575
+ for (const change of changes) {
576
+ logger.logInfo(`Processing file in analyzeGitChanges`, {
577
+ filename: change.path,
578
+ status: change.status,
579
+ isAdded: String(change.status === 'added'),
580
+ targetHash: targetHash || 'working tree'
581
+ });
582
+ let new_content = '';
583
+ if (change.status !== 'deleted') {
584
+ try {
585
+ logger.logInfo(`Getting content for file`, {
586
+ filename: change.path,
587
+ status: change.status,
588
+ source: targetHash ? `commit ${targetHash}` : 'working tree'
589
+ });
590
+ new_content = await this.getFileContent(directory, change.path, targetHash);
591
+ logger.logInfo(`Content retrieved successfully`, {
592
+ filename: change.path,
593
+ status: change.status,
594
+ contentLength: String(new_content.length)
595
+ });
596
+ }
597
+ catch (error) {
598
+ logger.logWarning(`Failed to read content for ${change.path}`, {
599
+ error: String(error),
600
+ status: change.status,
601
+ filename: change.path
602
+ });
603
+ }
604
+ }
605
+ else {
606
+ logger.logInfo(`Skipping content for deleted file`, {
607
+ filename: change.path
608
+ });
609
+ }
610
+ let patch = '';
611
+ try {
612
+ logger.logInfo(`Getting diff for file`, {
613
+ filename: change.path,
614
+ status: change.status,
615
+ targetHash: targetHash || 'working tree'
616
+ });
617
+ patch = await this.getFileDiff(directory, change.path, targetHash, change.status);
618
+ logger.logInfo(`Diff retrieved successfully`, {
619
+ filename: change.path,
620
+ status: change.status,
621
+ patchLength: String(patch.length)
622
+ });
623
+ }
624
+ catch (error) {
625
+ logger.logWarning(`Failed to get diff for ${change.path}`, {
626
+ error: String(error),
627
+ status: change.status,
628
+ filename: change.path
629
+ });
630
+ }
631
+ const result = {
632
+ filename: change.path,
633
+ status: change.status,
634
+ new_content,
635
+ patch
636
+ };
637
+ logger.logInfo(`File processing complete`, {
638
+ filename: change.path,
639
+ status: change.status,
640
+ hasContent: String(new_content.length > 0),
641
+ hasPatch: String(patch.length > 0),
642
+ contentLength: String(new_content.length),
643
+ patchLength: String(patch.length)
644
+ });
645
+ results.push(result);
646
+ }
647
+ logger.logInfo(`Git changes analysis complete`, {
648
+ directory,
649
+ totalFiles: String(results.length),
650
+ filesProcessed: JSON.stringify(results.map(r => ({
651
+ filename: r.filename,
652
+ status: r.status,
653
+ hasContent: r.new_content.length > 0,
654
+ hasPatch: r.patch.length > 0
655
+ })))
656
+ });
657
+ return results;
658
+ }
659
+ catch (error) {
660
+ logger.logError('Error analyzing git changes', error, { directory });
661
+ throw error;
662
+ }
663
+ }
664
+ /**
665
+ * Get file changes for a commit or uncommitted changes
666
+ * @param uncommitted - true for unstaged changes, false for specific commit
667
+ * @param commitHash - Git commit hash (required when uncommitted=false)
668
+ * @param directory - Path to the Git repository root
669
+ * @returns Array of file changes with filename, status, new_content, and patch
670
+ */
671
+ async getFileChanges(uncommitted, commitHash, directory) {
672
+ try {
673
+ // Validate parameters
674
+ if (!uncommitted && !commitHash) {
675
+ throw new Error("commitHash is required when uncommitted is false");
676
+ }
677
+ logger.logInfo(`Getting file changes`, {
678
+ directory,
679
+ uncommitted: String(uncommitted),
680
+ commitHash: commitHash || 'none'
681
+ });
682
+ // Determine the target commit/ref
683
+ const targetHash = uncommitted ? undefined : commitHash;
684
+ // Get changed files with status
685
+ const changes = await this.getChangedFilesWithStatus(directory, targetHash);
686
+ logger.logInfo(`Found ${changes.length} changed files`);
687
+ const results = [];
688
+ for (const change of changes) {
689
+ logger.logInfo('Processing file change', {
690
+ filename: change.path,
691
+ status: change.status,
692
+ targetHash: targetHash || 'working tree',
693
+ isAdded: String(change.status === 'added')
694
+ });
695
+ let new_content = '';
696
+ // Get new content for non-deleted files
697
+ if (change.status !== 'deleted') {
698
+ try {
699
+ logger.logInfo('Getting file content', {
700
+ filename: change.path,
701
+ status: change.status,
702
+ source: targetHash ? `commit ${targetHash}` : 'working tree',
703
+ isAdded: String(change.status === 'added')
704
+ });
705
+ new_content = await this.getFileContent(directory, change.path, targetHash);
706
+ logger.logInfo('File content retrieved', {
707
+ filename: change.path,
708
+ status: change.status,
709
+ contentLength: String(new_content.length),
710
+ lineCount: String(new_content.split('\n').length)
711
+ });
712
+ }
713
+ catch (error) {
714
+ logger.logWarning(`Failed to read content for ${change.path}`, {
715
+ error: String(error),
716
+ status: change.status,
717
+ targetHash: targetHash || 'working tree'
718
+ });
719
+ }
720
+ }
721
+ else {
722
+ logger.logInfo('Skipping content retrieval for deleted file', {
723
+ filename: change.path
724
+ });
725
+ }
726
+ // Get diff/patch for the file
727
+ let patch = '';
728
+ try {
729
+ logger.logInfo('Getting file diff', {
730
+ filename: change.path,
731
+ status: change.status,
732
+ targetHash: targetHash || 'working tree',
733
+ isAdded: String(change.status === 'added')
734
+ });
735
+ patch = await this.getFileDiff(directory, change.path, targetHash, change.status);
736
+ logger.logInfo('File diff retrieved', {
737
+ filename: change.path,
738
+ status: change.status,
739
+ patchLength: String(patch.length),
740
+ hasPatch: String(patch.length > 0)
741
+ });
742
+ }
743
+ catch (error) {
744
+ logger.logWarning(`Failed to get diff for ${change.path}`, {
745
+ error: String(error),
746
+ status: change.status,
747
+ targetHash: targetHash || 'working tree'
748
+ });
749
+ }
750
+ results.push({
751
+ filename: change.path,
752
+ status: change.status,
753
+ new_content,
754
+ patch
755
+ });
756
+ logger.logInfo('File change processed', {
757
+ filename: change.path,
758
+ status: change.status,
759
+ hasContent: String(new_content.length > 0),
760
+ hasPatch: String(patch.length > 0),
761
+ contentLength: String(new_content.length),
762
+ patchLength: String(patch.length)
763
+ });
764
+ }
765
+ logger.logInfo(`Successfully processed ${results.length} file changes`);
766
+ return results;
767
+ }
768
+ catch (error) {
769
+ logger.logError('Error in getFileChanges', error, {
770
+ directory,
771
+ uncommitted: String(uncommitted),
772
+ commitHash: commitHash || 'none'
773
+ });
774
+ throw error;
775
+ }
776
+ }
777
+ }
778
+ //# sourceMappingURL=gitService.js.map