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.
- package/README.md +82 -114
- package/package.json +10 -9
- package/src/content-retrieval.test.js +775 -0
- package/src/custom-documents.test.js +440 -0
- package/src/feedback-loader.test.js +529 -0
- package/src/llm.test.js +256 -0
- package/src/project-analyzer.test.js +747 -0
- package/src/rag-analyzer.js +12 -0
- package/src/rag-analyzer.test.js +1109 -0
- package/src/rag-review.test.js +317 -0
- package/src/setupTests.js +131 -0
- package/src/zero-shot-classifier-open.test.js +278 -0
- package/src/embeddings/cache-manager.js +0 -364
- package/src/embeddings/constants.js +0 -40
- package/src/embeddings/database.js +0 -921
- package/src/embeddings/errors.js +0 -208
- package/src/embeddings/factory.js +0 -447
- package/src/embeddings/file-processor.js +0 -851
- package/src/embeddings/model-manager.js +0 -337
- package/src/embeddings/similarity-calculator.js +0 -97
- package/src/embeddings/types.js +0 -113
- package/src/pr-history/analyzer.js +0 -579
- package/src/pr-history/bot-detector.js +0 -123
- package/src/pr-history/cli-utils.js +0 -204
- package/src/pr-history/comment-processor.js +0 -549
- package/src/pr-history/database.js +0 -819
- package/src/pr-history/github-client.js +0 -629
- package/src/technology-keywords.json +0 -753
- package/src/utils/command.js +0 -48
- package/src/utils/constants.js +0 -263
- package/src/utils/context-inference.js +0 -364
- package/src/utils/document-detection.js +0 -105
- package/src/utils/file-validation.js +0 -271
- package/src/utils/git.js +0 -232
- package/src/utils/language-detection.js +0 -170
- package/src/utils/logging.js +0 -24
- package/src/utils/markdown.js +0 -132
- package/src/utils/mobilebert-tokenizer.js +0 -141
- package/src/utils/pr-chunking.js +0 -276
- 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
|
-
}
|