codecritique 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +82 -114
  2. package/package.json +10 -9
  3. package/src/content-retrieval.test.js +775 -0
  4. package/src/custom-documents.test.js +440 -0
  5. package/src/feedback-loader.test.js +529 -0
  6. package/src/llm.test.js +256 -0
  7. package/src/project-analyzer.test.js +747 -0
  8. package/src/rag-analyzer.js +12 -0
  9. package/src/rag-analyzer.test.js +1109 -0
  10. package/src/rag-review.test.js +317 -0
  11. package/src/setupTests.js +131 -0
  12. package/src/zero-shot-classifier-open.test.js +278 -0
  13. package/src/embeddings/cache-manager.js +0 -364
  14. package/src/embeddings/constants.js +0 -40
  15. package/src/embeddings/database.js +0 -921
  16. package/src/embeddings/errors.js +0 -208
  17. package/src/embeddings/factory.js +0 -447
  18. package/src/embeddings/file-processor.js +0 -851
  19. package/src/embeddings/model-manager.js +0 -337
  20. package/src/embeddings/similarity-calculator.js +0 -97
  21. package/src/embeddings/types.js +0 -113
  22. package/src/pr-history/analyzer.js +0 -579
  23. package/src/pr-history/bot-detector.js +0 -123
  24. package/src/pr-history/cli-utils.js +0 -204
  25. package/src/pr-history/comment-processor.js +0 -549
  26. package/src/pr-history/database.js +0 -819
  27. package/src/pr-history/github-client.js +0 -629
  28. package/src/technology-keywords.json +0 -753
  29. package/src/utils/command.js +0 -48
  30. package/src/utils/constants.js +0 -263
  31. package/src/utils/context-inference.js +0 -364
  32. package/src/utils/document-detection.js +0 -105
  33. package/src/utils/file-validation.js +0 -271
  34. package/src/utils/git.js +0 -232
  35. package/src/utils/language-detection.js +0 -170
  36. package/src/utils/logging.js +0 -24
  37. package/src/utils/markdown.js +0 -132
  38. package/src/utils/mobilebert-tokenizer.js +0 -141
  39. package/src/utils/pr-chunking.js +0 -276
  40. package/src/utils/string-utils.js +0 -28
@@ -1,105 +0,0 @@
1
- /**
2
- * Document Detection Module
3
- *
4
- * This module provides utilities for detecting different types of documents,
5
- * particularly focusing on generic documentation files and their classification.
6
- */
7
-
8
- import path from 'path';
9
- import { GENERIC_DOC_REGEX } from './constants.js';
10
-
11
- /**
12
- * Check if a document is a generic documentation file (README, RUNBOOK, etc.)
13
- *
14
- * @param {string} docPath - Document file path
15
- * @param {string} docH1 - Document H1 title (optional)
16
- * @returns {boolean} True if document is generic documentation, false otherwise
17
- *
18
- * @example
19
- * const isGeneric = isGenericDocument('README.md');
20
- * // Returns: true
21
- *
22
- * const isGeneric2 = isGenericDocument('docs/api-guide.md', 'API Guide');
23
- * // Returns: false
24
- */
25
- export function isGenericDocument(docPath, docH1 = null) {
26
- if (!docPath) return false;
27
-
28
- // Check filename pattern
29
- if (GENERIC_DOC_REGEX.test(docPath)) {
30
- return true;
31
- }
32
-
33
- // Check H1 title if provided
34
- if (docH1) {
35
- const lowerH1 = docH1.toLowerCase();
36
- const genericTitlePatterns = ['readme', 'runbook', 'changelog', 'contributing', 'license', 'setup', 'installation', 'getting started'];
37
-
38
- return genericTitlePatterns.some((pattern) => lowerH1.includes(pattern));
39
- }
40
-
41
- return false;
42
- }
43
-
44
- /**
45
- * Get pre-computed context for generic documents to avoid expensive inference
46
- *
47
- * @param {string} docPath - Document file path
48
- * @returns {Object} Pre-computed generic document context with area, tech, and metadata
49
- *
50
- * @example
51
- * const context = getGenericDocumentContext('README.md');
52
- * // Returns: { area: 'Documentation', dominantTech: ['markdown', 'documentation'], ... }
53
- */
54
- export function getGenericDocumentContext(docPath) {
55
- const fileName = path.basename(docPath).toLowerCase();
56
-
57
- const baseContext = {
58
- area: 'General',
59
- dominantTech: [],
60
- isGeneralPurposeReadmeStyle: true,
61
- fastPath: true, // Mark as optimized fast-path
62
- docPath: docPath,
63
- };
64
-
65
- // Customize context based on document type
66
- if (fileName.includes('readme')) {
67
- return {
68
- ...baseContext,
69
- area: 'Documentation',
70
- dominantTech: ['markdown', 'documentation'],
71
- };
72
- } else if (fileName.includes('runbook')) {
73
- return {
74
- ...baseContext,
75
- area: 'Operations',
76
- dominantTech: ['operations', 'deployment', 'devops'],
77
- };
78
- } else if (fileName.includes('changelog')) {
79
- return {
80
- ...baseContext,
81
- area: 'Documentation',
82
- dominantTech: ['versioning', 'releases'],
83
- };
84
- } else if (fileName.includes('contributing')) {
85
- return {
86
- ...baseContext,
87
- area: 'Development',
88
- dominantTech: ['git', 'development', 'contribution'],
89
- };
90
- } else if (fileName.includes('license')) {
91
- return {
92
- ...baseContext,
93
- area: 'Legal',
94
- dominantTech: ['licensing'],
95
- };
96
- } else if (fileName.includes('setup') || fileName.includes('install')) {
97
- return {
98
- ...baseContext,
99
- area: 'Setup',
100
- dominantTech: ['installation', 'setup', 'configuration'],
101
- };
102
- }
103
-
104
- return baseContext;
105
- }
@@ -1,271 +0,0 @@
1
- /**
2
- * File Validation Module
3
- *
4
- * This module provides utilities for validating, filtering, and determining
5
- * if files should be processed based on various criteria such as file type,
6
- * size, patterns, and gitignore rules.
7
- */
8
-
9
- import fs from 'fs';
10
- import path from 'path';
11
- import { minimatch } from 'minimatch';
12
- import { execGitSafe } from './command.js';
13
- import {
14
- CODE_EXTENSIONS,
15
- DOCUMENTATION_EXTENSIONS,
16
- BINARY_EXTENSIONS,
17
- SKIP_DIRECTORIES,
18
- SKIP_FILENAMES,
19
- SKIP_FILE_PATTERNS,
20
- } from './constants.js';
21
-
22
- /**
23
- * Checks if a file path looks like a test file based on common patterns.
24
- * Tries to be relatively language/framework agnostic.
25
- *
26
- * @param {string} filePath - Path to the file.
27
- * @returns {boolean} True if the path matches test patterns, false otherwise.
28
- *
29
- * @example
30
- * isTestFile('src/components/__tests__/Button.test.js'); // true
31
- * isTestFile('test/unit/validator.spec.js'); // true
32
- * isTestFile('src/utils.js'); // false
33
- */
34
- export function isTestFile(filePath) {
35
- if (!filePath) return false;
36
- const lowerPath = filePath.toLowerCase();
37
- // Common patterns: /__tests__/, /tests/, /specs/, _test., _spec., .test., .spec.
38
- // Ensure delimiters are present or it's in a specific test directory.
39
- // Checks for directory names or common patterns immediately preceding the file extension.
40
- const testPattern = /(\/__tests__\/|\/tests?\/|\/specs?\/|_test\.|_spec\.|\.test\.|\.spec\.)/i;
41
- return testPattern.test(lowerPath);
42
- }
43
-
44
- /**
45
- * Checks if a file is a documentation file based on extension, path patterns, and filename
46
- *
47
- * @param {string} filePath - Path to the file
48
- * @returns {boolean} True if the file is documentation, false otherwise
49
- *
50
- * @example
51
- * isDocumentationFile('README.md'); // true
52
- * isDocumentationFile('docs/api.md'); // true
53
- * isDocumentationFile('src/utils.js'); // false
54
- */
55
- export function isDocumentationFile(filePath) {
56
- const lowerPath = filePath.toLowerCase();
57
- const filename = lowerPath.split('/').pop();
58
- const extension = path.extname(lowerPath);
59
-
60
- // 1. Explicitly identify common code file extensions as NOT documentation
61
- if (CODE_EXTENSIONS.includes(extension)) {
62
- return false;
63
- }
64
-
65
- // 2. Check for specific documentation extensions
66
- if (DOCUMENTATION_EXTENSIONS.includes(extension)) {
67
- return true;
68
- }
69
-
70
- // 3. Check for universally accepted file names (case-insensitive)
71
- const docFilenames = ['readme', 'license', 'contributing', 'changelog', 'copying'];
72
- const filenameWithoutExt = filename.substring(0, filename.length - (extension.length || 0));
73
- if (docFilenames.includes(filenameWithoutExt)) {
74
- return true;
75
- }
76
-
77
- // 4. Check for common documentation directories (less reliable but useful)
78
- const docDirs = ['/docs/', '/documentation/', '/doc/', '/wiki/', '/examples/', '/guides/'];
79
- if (docDirs.some((dir) => lowerPath.includes(dir))) {
80
- return true;
81
- }
82
-
83
- // 5. Check for other common documentation terms in filename (lowest priority)
84
- const docTerms = ['guide', 'tutorial', 'manual', 'howto'];
85
- if (docTerms.some((term) => filename.includes(term))) {
86
- return true;
87
- }
88
-
89
- // 6. Special case for plain text files that look like docs
90
- if (extension === '.txt') {
91
- if (docFilenames.includes(filenameWithoutExt) || docTerms.some((term) => filename.includes(term))) {
92
- return true;
93
- }
94
- }
95
-
96
- return false;
97
- }
98
-
99
- /**
100
- * Check if a file should be processed based on its path and content
101
- *
102
- * @param {string} filePath - Path to the file
103
- * @param {string} content - Content of the file (optional, unused but kept for API compatibility)
104
- * @param {Object} options - Additional options
105
- * @param {Array<string>} options.excludePatterns - Patterns to exclude
106
- * @param {boolean} options.respectGitignore - Whether to respect .gitignore files
107
- * @param {string} options.baseDir - Base directory for relative paths
108
- * @param {Map<string, boolean>} options.gitignoreCache - Optional cache for gitignore results
109
- * @param {fs.Stats} options.fileStats - Optional pre-computed file stats to avoid re-reading
110
- * @returns {boolean} Whether the file should be processed
111
- *
112
- * @example
113
- * const shouldProcess = shouldProcessFile('src/utils.js', '', {
114
- * excludePatterns: ['*.test.js'],
115
- * respectGitignore: true
116
- * });
117
- */
118
- export function shouldProcessFile(filePath, _, options = {}) {
119
- const { excludePatterns = [], respectGitignore = true, baseDir = process.cwd(), gitignoreCache = null, fileStats = null } = options;
120
-
121
- // Skip files that are too large (>1MB) - use provided stats if available
122
- if (fileStats) {
123
- if (fileStats.size > 1024 * 1024) {
124
- return false;
125
- }
126
- } else {
127
- try {
128
- const stats = fs.statSync(filePath);
129
- if (stats.size > 1024 * 1024) {
130
- return false;
131
- }
132
- } catch {
133
- // If we can't get file stats, assume it's processable
134
- }
135
- }
136
-
137
- // Skip binary files
138
- const extension = path.extname(filePath).toLowerCase();
139
- if (BINARY_EXTENSIONS.includes(extension)) {
140
- return false;
141
- }
142
-
143
- // Skip node_modules, dist, build directories
144
- if (SKIP_DIRECTORIES.some((dir) => filePath.includes(`/${dir}/`))) {
145
- return false;
146
- }
147
-
148
- // Skip specific filenames like lock files
149
- if (SKIP_FILENAMES.includes(path.basename(filePath))) {
150
- return false;
151
- }
152
-
153
- // Skip files that are likely to be generated
154
- if (SKIP_FILE_PATTERNS.some((pattern) => pattern.test(filePath))) {
155
- return false;
156
- }
157
-
158
- // Check custom exclude patterns
159
- if (excludePatterns.length > 0) {
160
- const relativePath = path.relative(baseDir, filePath);
161
- if (excludePatterns.some((pattern) => minimatch(relativePath, pattern, { dot: true }))) {
162
- return false;
163
- }
164
- }
165
-
166
- // Check gitignore patterns if enabled
167
- if (respectGitignore) {
168
- const relativePath = path.relative(baseDir, filePath);
169
-
170
- // Use cache if provided
171
- if (gitignoreCache && gitignoreCache.has(relativePath)) {
172
- return !gitignoreCache.get(relativePath); // Cache stores isIgnored, we return shouldProcess
173
- }
174
-
175
- // Fallback to individual check (slow path)
176
- try {
177
- // Use git check-ignore to determine if a file is ignored
178
- // This is the most accurate way to check as it uses Git's own ignore logic
179
- // Use baseDir as cwd to ensure git runs in the correct context
180
- execGitSafe('git check-ignore', ['-q', relativePath], {
181
- stdio: 'ignore',
182
- cwd: baseDir,
183
- });
184
-
185
- // If we get here, the file is ignored by git
186
- if (gitignoreCache) gitignoreCache.set(relativePath, true);
187
- return false;
188
- } catch {
189
- // If git check-ignore exits with non-zero status, the file is not ignored
190
- // This is expected behavior, so we continue processing
191
- if (gitignoreCache) gitignoreCache.set(relativePath, false);
192
- }
193
- }
194
-
195
- return true;
196
- }
197
-
198
- /**
199
- * Batch check multiple files against gitignore in a single git command
200
- * This is much faster than calling git check-ignore for each file individually
201
- *
202
- * @param {string[]} filePaths - Array of file paths to check
203
- * @param {string} baseDir - Base directory for git operations
204
- * @returns {Promise<Map<string, boolean>>} Map of relative paths to isIgnored boolean
205
- */
206
- export async function batchCheckGitignore(filePaths, baseDir = process.cwd()) {
207
- const resultMap = new Map();
208
-
209
- if (filePaths.length === 0) {
210
- return resultMap;
211
- }
212
-
213
- // Convert to relative paths
214
- const relativePaths = filePaths.map((fp) => path.relative(baseDir, fp));
215
-
216
- try {
217
- // Use --stdin flag for batch checking
218
- // git check-ignore exits with:
219
- // 0 = at least one path is ignored (outputs ignored paths)
220
- // 1 = no paths are ignored (outputs nothing) - this is NOT an error
221
- // 128 = fatal error
222
- // execSync throws on non-zero exit, so we need to catch exit code 1
223
- const stdout = execGitSafe('git check-ignore', ['--stdin'], {
224
- stdio: ['pipe', 'pipe', 'ignore'],
225
- cwd: baseDir,
226
- input: relativePaths.join('\n'),
227
- encoding: 'utf8',
228
- });
229
-
230
- // If we get here, exit code was 0 - some files are ignored
231
- const stdoutStr = typeof stdout === 'string' ? stdout : stdout?.toString('utf8') || '';
232
- const ignoredFiles = stdoutStr
233
- .trim()
234
- .split('\n')
235
- .filter((line) => line.length > 0);
236
- const ignoredSet = new Set(ignoredFiles);
237
-
238
- // Build result map
239
- for (const relPath of relativePaths) {
240
- resultMap.set(relPath, ignoredSet.has(relPath));
241
- }
242
-
243
- // Log ignored files
244
- if (ignoredFiles.length > 0) {
245
- console.log(` ℹ️ Found ${ignoredFiles.length} gitignored files to exclude`);
246
- const ignoredSample = ignoredFiles.slice(0, 5);
247
- ignoredSample.forEach((f) => console.log(` - ${f}`));
248
- if (ignoredFiles.length > 5) {
249
- console.log(` ... and ${ignoredFiles.length - 5} more`);
250
- }
251
- }
252
- } catch (error) {
253
- // Check if this is just "no files ignored" (exit code 1) vs actual error
254
- // Exit code 1 from git check-ignore means no paths matched - this is normal
255
- if (error.status === 1) {
256
- // No files in this batch are ignored - mark all as not ignored
257
- for (const relPath of relativePaths) {
258
- resultMap.set(relPath, false);
259
- }
260
- } else {
261
- // Actual error (exit code 128 or other) - log and fall back
262
- console.warn(`⚠️ Batch gitignore check failed: ${error.message}`);
263
- console.warn(' Falling back to individual checks (may be slower)');
264
- for (const relPath of relativePaths) {
265
- resultMap.set(relPath, false);
266
- }
267
- }
268
- }
269
-
270
- return resultMap;
271
- }
package/src/utils/git.js DELETED
@@ -1,232 +0,0 @@
1
- /**
2
- * Git Operations Module
3
- *
4
- * This module provides utilities for git operations including branch management,
5
- * diff analysis, and content retrieval from different branches or commits.
6
- */
7
-
8
- import { execSync } from 'child_process';
9
- import path from 'path';
10
- import chalk from 'chalk';
11
- import { execGitSafe } from './command.js';
12
-
13
- /**
14
- * Check if a git branch exists locally
15
- *
16
- * @param {string} branchName - The name of the branch to check
17
- * @param {string} workingDir - Directory to run git commands in (optional, defaults to cwd)
18
- * @returns {boolean} True if the branch exists, false otherwise
19
- *
20
- * @example
21
- * const exists = checkBranchExists('feature-branch');
22
- * if (exists) {
23
- * console.log('Branch exists locally');
24
- * }
25
- */
26
- function checkBranchExists(branchName, workingDir = process.cwd()) {
27
- try {
28
- execGitSafe('git show-ref', ['--verify', '--quiet', `refs/heads/${branchName}`], { cwd: workingDir });
29
- return true;
30
- } catch {
31
- // Command returns non-zero exit code if branch doesn't exist
32
- return false;
33
- }
34
- }
35
-
36
- /**
37
- * Ensure a branch exists locally, fetching from remote if necessary
38
- *
39
- * @param {string} branchName - The name of the branch to ensure exists
40
- * @param {string} workingDir - Directory to run git commands in (optional, defaults to cwd)
41
- *
42
- * @example
43
- * await ensureBranchExists('main');
44
- * // Branch is now available locally for operations
45
- */
46
- export function ensureBranchExists(branchName, workingDir = process.cwd()) {
47
- try {
48
- // Check if branch exists locally
49
- if (checkBranchExists(branchName, workingDir)) {
50
- console.log(chalk.gray(`Branch '${branchName}' exists locally`));
51
- return;
52
- }
53
-
54
- console.log(chalk.yellow(`Branch '${branchName}' not found locally, attempting to fetch...`));
55
-
56
- // Try to fetch the branch from origin
57
- try {
58
- execGitSafe('git fetch', ['origin', `${branchName}:${branchName}`], { stdio: 'pipe', cwd: workingDir });
59
- console.log(chalk.green(`Successfully fetched branch '${branchName}' from origin`));
60
- } catch {
61
- // If direct fetch fails, try fetching all branches and then checking
62
- console.log(chalk.yellow(`Direct fetch failed, trying to fetch all branches...`));
63
- execSync('git fetch origin', { stdio: 'pipe', cwd: workingDir });
64
-
65
- // Check if branch exists on remote
66
- try {
67
- execGitSafe('git show-ref', ['--verify', '--quiet', `refs/remotes/origin/${branchName}`], { cwd: workingDir });
68
- // Create local tracking branch
69
- execGitSafe('git checkout', ['-b', branchName, `origin/${branchName}`], { stdio: 'pipe', cwd: workingDir });
70
- console.log(chalk.green(`Successfully created local branch '${branchName}' tracking origin/${branchName}`));
71
- } catch {
72
- throw new Error(`Branch '${branchName}' not found locally or on remote origin`);
73
- }
74
- }
75
- } catch (error) {
76
- console.error(chalk.red(`Error ensuring branch '${branchName}' exists:`), error.message);
77
- throw error;
78
- }
79
- }
80
-
81
- /**
82
- * Find the base branch (main or master) that exists in the repository
83
- *
84
- * @param {string} workingDir - Directory to run git commands in (optional, defaults to cwd)
85
- * @returns {string} The name of the base branch (main, master, or develop)
86
- *
87
- * @example
88
- * const baseBranch = findBaseBranch();
89
- * console.log(`Using base branch: ${baseBranch}`);
90
- */
91
- export function findBaseBranch(workingDir = process.cwd()) {
92
- const candidateBranches = ['main', 'master', 'develop'];
93
-
94
- for (const branch of candidateBranches) {
95
- if (checkBranchExists(branch, workingDir)) {
96
- return branch;
97
- }
98
-
99
- // Also check if it exists on remote
100
- try {
101
- execGitSafe('git show-ref', ['--verify', '--quiet', `refs/remotes/origin/${branch}`], { cwd: workingDir });
102
- return branch;
103
- } catch {
104
- // Branch doesn't exist on remote either, continue to next candidate
105
- }
106
- }
107
-
108
- // Fallback to HEAD~1 if no standard base branch found
109
- console.warn(chalk.yellow('No standard base branch (main/master/develop) found, using HEAD~1 as fallback'));
110
- return 'HEAD~1';
111
- }
112
-
113
- /**
114
- * Get git diff content for a specific file between two branches/commits
115
- *
116
- * @param {string} filePath - Path to the file
117
- * @param {string} baseBranch - Base branch (e.g., 'main', 'master')
118
- * @param {string} targetBranch - Target branch (e.g., 'feature-branch')
119
- * @param {string} workingDir - Working directory for git commands
120
- * @returns {string} Git diff content for the file
121
- *
122
- * @example
123
- * const diff = getFileDiff('src/utils.js', 'main', 'feature-branch');
124
- * console.log('Changes:', diff);
125
- */
126
- function getFileDiff(filePath, baseBranch, targetBranch, workingDir = process.cwd()) {
127
- try {
128
- // Use git diff to get changes for the specific file
129
- // Format: git diff base...target -- filepath
130
- const gitCommand = `git diff ${baseBranch}...${targetBranch} -- "${filePath}"`;
131
- const diffOutput = execSync(gitCommand, { cwd: workingDir, encoding: 'utf8' });
132
-
133
- return diffOutput;
134
- } catch (error) {
135
- console.error(chalk.red(`Error getting git diff for ${filePath}: ${error.message}`));
136
- return '';
137
- }
138
- }
139
-
140
- /**
141
- * Get changed lines info for a file between two branches
142
- *
143
- * @param {string} filePath - Path to the file
144
- * @param {string} baseBranch - Base branch
145
- * @param {string} targetBranch - Target branch
146
- * @param {string} workingDir - Working directory for git commands
147
- * @returns {Object} Object with added/removed lines info
148
- *
149
- * @example
150
- * const changes = getChangedLinesInfo('src/utils.js', 'main', 'feature-branch');
151
- * console.log(`Added ${changes.addedLines.length} lines, removed ${changes.removedLines.length} lines`);
152
- */
153
- export function getChangedLinesInfo(filePath, baseBranch, targetBranch, workingDir = process.cwd()) {
154
- try {
155
- const diffOutput = getFileDiff(filePath, baseBranch, targetBranch, workingDir);
156
-
157
- if (!diffOutput) {
158
- return { hasChanges: false, addedLines: [], removedLines: [], contextLines: [] };
159
- }
160
-
161
- const lines = diffOutput.split('\n');
162
- const addedLines = [];
163
- const removedLines = [];
164
- const contextLines = [];
165
-
166
- let currentLineNumber = 0;
167
-
168
- for (const line of lines) {
169
- if (line.startsWith('@@')) {
170
- // Parse line numbers from diff header like "@@ -10,7 +10,8 @@"
171
- const match = line.match(/@@ -(\d+),?\d* \+(\d+),?\d* @@/);
172
- if (match) {
173
- currentLineNumber = parseInt(match[2]);
174
- }
175
- } else if (line.startsWith('+') && !line.startsWith('+++')) {
176
- addedLines.push({ lineNumber: currentLineNumber, content: line.substring(1) });
177
- currentLineNumber++;
178
- } else if (line.startsWith('-') && !line.startsWith('---')) {
179
- removedLines.push({ content: line.substring(1) });
180
- } else if (line.startsWith(' ')) {
181
- contextLines.push({ lineNumber: currentLineNumber, content: line.substring(1) });
182
- currentLineNumber++;
183
- }
184
- }
185
-
186
- return {
187
- hasChanges: addedLines.length > 0 || removedLines.length > 0,
188
- addedLines,
189
- removedLines,
190
- contextLines,
191
- fullDiff: diffOutput,
192
- };
193
- } catch (error) {
194
- console.error(chalk.red(`Error parsing diff for ${filePath}: ${error.message}`));
195
- return { hasChanges: false, addedLines: [], removedLines: [], contextLines: [] };
196
- }
197
- }
198
-
199
- /**
200
- * Get the content of a file from a specific git branch/commit without checking it out
201
- *
202
- * @param {string} filePath - Absolute path to the file in the repository
203
- * @param {string} branchOrCommit - The branch or commit hash to get the file from
204
- * @param {string} workingDir - The git repository directory
205
- * @returns {string} The content of the file
206
- *
207
- * @example
208
- * const content = getFileContentFromGit('/path/to/file.js', 'main', '/repo');
209
- * console.log('File content from main branch:', content);
210
- */
211
- export function getFileContentFromGit(filePath, branchOrCommit, workingDir) {
212
- try {
213
- const gitRoot = execSync('git rev-parse --show-toplevel', { cwd: workingDir }).toString().trim();
214
- const relativePath = path.relative(gitRoot, filePath);
215
- // Use forward slashes for git path
216
- const gitPath = relativePath.split(path.sep).join('/');
217
-
218
- // Command: git show <branch>:<path>
219
- // Use safe execution to prevent command injection
220
- return execGitSafe('git show', [`${branchOrCommit}:${gitPath}`], { cwd: workingDir, encoding: 'utf8' });
221
- } catch (error) {
222
- // Handle cases where the file might not exist in that commit (e.g., a new file in a feature branch)
223
- if (error.stderr && error.stderr.includes('exists on disk, but not in')) {
224
- // This case can be ignored if we are sure the file is new.
225
- // For a robust solution, you might need to check file status (new, modified, deleted).
226
- // For now, we return an empty string, assuming it's a new file not yet in the base.
227
- return '';
228
- }
229
- // Re-throw other errors
230
- throw new Error(`Failed to get content of ${filePath} from ${branchOrCommit}: ${error.message}`);
231
- }
232
- }