@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.
- package/.env +9 -0
- package/README.md +185 -0
- package/build/handlers/analyzeCommitHandler.d.ts +36 -0
- package/build/handlers/analyzeCommitHandler.d.ts.map +1 -0
- package/build/handlers/analyzeCommitHandler.js +397 -0
- package/build/handlers/analyzeCommitHandler.js.map +1 -0
- package/build/handlers/events.d.ts +7 -0
- package/build/handlers/events.d.ts.map +1 -0
- package/build/handlers/events.js +15 -0
- package/build/handlers/events.js.map +1 -0
- package/build/handlers/resources.d.ts +10 -0
- package/build/handlers/resources.d.ts.map +1 -0
- package/build/handlers/resources.js +14 -0
- package/build/handlers/resources.js.map +1 -0
- package/build/handlers/tools.d.ts +6 -0
- package/build/handlers/tools.d.ts.map +1 -0
- package/build/handlers/tools.js +29 -0
- package/build/handlers/tools.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +82 -0
- package/build/index.js.map +1 -0
- package/build/schemas/toolSchemas.d.ts +50 -0
- package/build/schemas/toolSchemas.d.ts.map +1 -0
- package/build/schemas/toolSchemas.js +48 -0
- package/build/schemas/toolSchemas.js.map +1 -0
- package/build/services/backendApiService.d.ts +81 -0
- package/build/services/backendApiService.d.ts.map +1 -0
- package/build/services/backendApiService.js +265 -0
- package/build/services/backendApiService.js.map +1 -0
- package/build/services/commitReviewService.d.ts +71 -0
- package/build/services/commitReviewService.d.ts.map +1 -0
- package/build/services/commitReviewService.js +506 -0
- package/build/services/commitReviewService.js.map +1 -0
- package/build/services/gitService.d.ts +159 -0
- package/build/services/gitService.d.ts.map +1 -0
- package/build/services/gitService.js +778 -0
- package/build/services/gitService.js.map +1 -0
- package/build/services/loggingService.d.ts +64 -0
- package/build/services/loggingService.d.ts.map +1 -0
- package/build/services/loggingService.js +185 -0
- package/build/services/loggingService.js.map +1 -0
- package/build/services/zipService.d.ts +9 -0
- package/build/services/zipService.d.ts.map +1 -0
- package/build/services/zipService.js +47 -0
- package/build/services/zipService.js.map +1 -0
- package/build/tests/analysisFormatter.test.d.ts +2 -0
- package/build/tests/analysisFormatter.test.d.ts.map +1 -0
- package/build/tests/analysisFormatter.test.js +92 -0
- package/build/tests/analysisFormatter.test.js.map +1 -0
- package/build/tests/analyzeCommitHandler.test.d.ts +2 -0
- package/build/tests/analyzeCommitHandler.test.d.ts.map +1 -0
- package/build/tests/analyzeCommitHandler.test.js +111 -0
- package/build/tests/analyzeCommitHandler.test.js.map +1 -0
- package/build/tests/backendApiService.test.d.ts +2 -0
- package/build/tests/backendApiService.test.d.ts.map +1 -0
- package/build/tests/backendApiService.test.js +109 -0
- package/build/tests/backendApiService.test.js.map +1 -0
- package/build/tests/commitReviewService.test.d.ts +2 -0
- package/build/tests/commitReviewService.test.d.ts.map +1 -0
- package/build/tests/commitReviewService.test.js +120 -0
- package/build/tests/commitReviewService.test.js.map +1 -0
- package/build/tests/errorExtractor.test.d.ts +2 -0
- package/build/tests/errorExtractor.test.d.ts.map +1 -0
- package/build/tests/errorExtractor.test.js +61 -0
- package/build/tests/errorExtractor.test.js.map +1 -0
- package/build/tests/loggingService.test.d.ts +2 -0
- package/build/tests/loggingService.test.d.ts.map +1 -0
- package/build/tests/loggingService.test.js +153 -0
- package/build/tests/loggingService.test.js.map +1 -0
- package/build/tests/setup.test.d.ts +2 -0
- package/build/tests/setup.test.d.ts.map +1 -0
- package/build/tests/setup.test.js +7 -0
- package/build/tests/setup.test.js.map +1 -0
- package/build/tests/tools.test.d.ts +2 -0
- package/build/tests/tools.test.d.ts.map +1 -0
- package/build/tests/tools.test.js +58 -0
- package/build/tests/tools.test.js.map +1 -0
- package/build/utils/analysisFormatter.d.ts +40 -0
- package/build/utils/analysisFormatter.d.ts.map +1 -0
- package/build/utils/analysisFormatter.js +97 -0
- package/build/utils/analysisFormatter.js.map +1 -0
- package/build/utils/errorExtractor.d.ts +36 -0
- package/build/utils/errorExtractor.d.ts.map +1 -0
- package/build/utils/errorExtractor.js +178 -0
- package/build/utils/errorExtractor.js.map +1 -0
- 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
|