@sun-asterisk/impact-analyzer 1.0.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 +506 -0
- package/cli.js +38 -0
- package/config/default-config.js +56 -0
- package/index.js +128 -0
- package/modules/change-detector.js +258 -0
- package/modules/detectors/database-detector.js +182 -0
- package/modules/detectors/endpoint-detector.js +52 -0
- package/modules/impact-analyzer.js +124 -0
- package/modules/report-generator.js +373 -0
- package/modules/utils/ast-parser.js +241 -0
- package/modules/utils/dependency-graph.js +159 -0
- package/modules/utils/file-utils.js +116 -0
- package/modules/utils/git-utils.js +198 -0
- package/modules/utils/method-call-graph.js +952 -0
- package/package.json +26 -0
- package/run-impact-analysis.sh +124 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST Parser Utilities
|
|
3
|
+
* Handles parsing and symbol extraction from JavaScript/TypeScript code
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { parse } from '@babel/parser';
|
|
7
|
+
import traverse from '@babel/traverse';
|
|
8
|
+
|
|
9
|
+
export class ASTParser {
|
|
10
|
+
/**
|
|
11
|
+
* Parse code and extract symbols
|
|
12
|
+
*/
|
|
13
|
+
static extractSymbols(content, filePath) {
|
|
14
|
+
try {
|
|
15
|
+
const ast = parse(content, {
|
|
16
|
+
sourceType: 'module',
|
|
17
|
+
plugins: [
|
|
18
|
+
'jsx',
|
|
19
|
+
'typescript',
|
|
20
|
+
'decorators-legacy',
|
|
21
|
+
'classProperties',
|
|
22
|
+
'dynamicImport',
|
|
23
|
+
'optionalChaining',
|
|
24
|
+
'nullishCoalescingOperator',
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return this.traverseAST(ast, filePath);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.warn(`Failed to parse ${filePath}:`, error.message);
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Traverse AST and extract symbols
|
|
37
|
+
*/
|
|
38
|
+
static traverseAST(ast, filePath) {
|
|
39
|
+
const symbols = [];
|
|
40
|
+
const self = ASTParser;
|
|
41
|
+
|
|
42
|
+
traverse.default(ast, {
|
|
43
|
+
// Function declarations
|
|
44
|
+
FunctionDeclaration(path) {
|
|
45
|
+
if (path.node.id) {
|
|
46
|
+
symbols.push({
|
|
47
|
+
type: 'function',
|
|
48
|
+
name: path.node.id.name,
|
|
49
|
+
signature: self.getSignature(path),
|
|
50
|
+
startLine: path.node.loc?.start.line || 0,
|
|
51
|
+
endLine: path.node.loc?.end.line || 0,
|
|
52
|
+
file: filePath,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// Arrow functions and function expressions assigned to variables
|
|
58
|
+
VariableDeclarator(path) {
|
|
59
|
+
if (
|
|
60
|
+
path.node.id.type === 'Identifier' &&
|
|
61
|
+
(path.node.init?.type === 'ArrowFunctionExpression' ||
|
|
62
|
+
path.node.init?.type === 'FunctionExpression')
|
|
63
|
+
) {
|
|
64
|
+
symbols.push({
|
|
65
|
+
type: 'function',
|
|
66
|
+
name: path.node.id.name,
|
|
67
|
+
signature: self.getSignature(path.get('init')),
|
|
68
|
+
startLine: path.node.loc?.start.line || 0,
|
|
69
|
+
endLine: path.node.loc?.end.line || 0,
|
|
70
|
+
file: filePath,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
// Class declarations
|
|
76
|
+
ClassDeclaration(path) {
|
|
77
|
+
if (path.node.id) {
|
|
78
|
+
symbols.push({
|
|
79
|
+
type: 'class',
|
|
80
|
+
name: path.node.id.name,
|
|
81
|
+
startLine: path.node.loc?.start.line || 0,
|
|
82
|
+
endLine: path.node.loc?.end.line || 0,
|
|
83
|
+
file: filePath,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Extract class methods
|
|
87
|
+
path.node.body.body.forEach((member) => {
|
|
88
|
+
if (member.type === 'ClassMethod' && member.key.type === 'Identifier') {
|
|
89
|
+
symbols.push({
|
|
90
|
+
type: 'method',
|
|
91
|
+
name: `${path.node.id.name}.${member.key.name}`,
|
|
92
|
+
signature: self.getMethodSignature(member),
|
|
93
|
+
startLine: member.loc?.start.line || 0,
|
|
94
|
+
endLine: member.loc?.end.line || 0,
|
|
95
|
+
file: filePath,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
// Export declarations
|
|
103
|
+
ExportNamedDeclaration(path) {
|
|
104
|
+
if (path.node.declaration) {
|
|
105
|
+
// Handle exported functions, classes, etc.
|
|
106
|
+
const declaration = path.node.declaration;
|
|
107
|
+
if (declaration.id) {
|
|
108
|
+
symbols.push({
|
|
109
|
+
type: declaration.type.toLowerCase().replace('declaration', ''),
|
|
110
|
+
name: declaration.id.name,
|
|
111
|
+
exported: true,
|
|
112
|
+
startLine: declaration.loc?.start.line || 0,
|
|
113
|
+
endLine: declaration.loc?.end.line || 0,
|
|
114
|
+
file: filePath,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return symbols;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get function signature
|
|
126
|
+
*/
|
|
127
|
+
static getSignature(path) {
|
|
128
|
+
try {
|
|
129
|
+
if (!path || !path.node) {
|
|
130
|
+
return '(...)';
|
|
131
|
+
}
|
|
132
|
+
const params = path.node.params || [];
|
|
133
|
+
const paramNames = params.map(p => {
|
|
134
|
+
if (p.type === 'Identifier') return p.name;
|
|
135
|
+
if (p.type === 'RestElement') return `...${p.argument.name}`;
|
|
136
|
+
return '_';
|
|
137
|
+
});
|
|
138
|
+
return `(${paramNames.join(', ')})`;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
return '(...)';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get method signature
|
|
146
|
+
*/
|
|
147
|
+
static getMethodSignature(node) {
|
|
148
|
+
try {
|
|
149
|
+
const params = node.params || [];
|
|
150
|
+
const paramNames = params.map(p => p.name || '_');
|
|
151
|
+
return `(${paramNames.join(', ')})`;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
return '(...)';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Extract imports from code
|
|
159
|
+
*/
|
|
160
|
+
static extractImports(content) {
|
|
161
|
+
const imports = [];
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const ast = parse(content, {
|
|
165
|
+
sourceType: 'module',
|
|
166
|
+
plugins: ['jsx', 'typescript', 'dynamicImport'],
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
traverse.default(ast, {
|
|
170
|
+
ImportDeclaration(path) {
|
|
171
|
+
imports.push({
|
|
172
|
+
source: path.node.source.value,
|
|
173
|
+
specifiers: path.node.specifiers.map(s => ({
|
|
174
|
+
imported: s.imported?.name || s.local.name,
|
|
175
|
+
local: s.local.name,
|
|
176
|
+
})),
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
// Dynamic imports
|
|
181
|
+
CallExpression(path) {
|
|
182
|
+
if (path.node.callee.type === 'Import') {
|
|
183
|
+
const arg = path.node.arguments[0];
|
|
184
|
+
if (arg?.type === 'StringLiteral') {
|
|
185
|
+
imports.push({
|
|
186
|
+
source: arg.value,
|
|
187
|
+
dynamic: true,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
} catch (error) {
|
|
194
|
+
// Ignore parse errors
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return imports;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Find function calls in code
|
|
202
|
+
*/
|
|
203
|
+
static findFunctionCalls(content, functionName) {
|
|
204
|
+
const calls = [];
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const ast = parse(content, {
|
|
208
|
+
sourceType: 'module',
|
|
209
|
+
plugins: ['jsx', 'typescript'],
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
traverse.default(ast, {
|
|
213
|
+
CallExpression(path) {
|
|
214
|
+
const callee = path.node.callee;
|
|
215
|
+
|
|
216
|
+
// Direct function call
|
|
217
|
+
if (callee.type === 'Identifier' && callee.name === functionName) {
|
|
218
|
+
calls.push({
|
|
219
|
+
line: path.node.loc?.start.line || 0,
|
|
220
|
+
type: 'direct',
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Method call
|
|
225
|
+
if (callee.type === 'MemberExpression' &&
|
|
226
|
+
callee.property.type === 'Identifier' &&
|
|
227
|
+
callee.property.name === functionName) {
|
|
228
|
+
calls.push({
|
|
229
|
+
line: path.node.loc?.start.line || 0,
|
|
230
|
+
type: 'method',
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
} catch (error) {
|
|
236
|
+
// Ignore parse errors
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return calls;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency Graph Builder
|
|
3
|
+
* Builds and queries dependency relationships between code modules
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class DependencyGraph {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.graph = new Map(); // file -> Set of dependencies
|
|
9
|
+
this.reverseGraph = new Map(); // file -> Set of dependents
|
|
10
|
+
this.symbolGraph = new Map(); // symbol -> Set of callers
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Add a dependency edge
|
|
15
|
+
*/
|
|
16
|
+
addDependency(from, to) {
|
|
17
|
+
if (! this.graph.has(from)) {
|
|
18
|
+
this.graph.set(from, new Set());
|
|
19
|
+
}
|
|
20
|
+
this.graph.get(from).add(to);
|
|
21
|
+
|
|
22
|
+
// Build reverse graph
|
|
23
|
+
if (!this.reverseGraph.has(to)) {
|
|
24
|
+
this.reverseGraph.set(to, new Set());
|
|
25
|
+
}
|
|
26
|
+
this.reverseGraph.get(to).add(from);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Add symbol usage
|
|
31
|
+
*/
|
|
32
|
+
addSymbolUsage(symbol, usedIn) {
|
|
33
|
+
if (!this.symbolGraph.has(symbol)) {
|
|
34
|
+
this.symbolGraph.set(symbol, new Set());
|
|
35
|
+
}
|
|
36
|
+
this.symbolGraph.get(symbol).add(usedIn);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get dependencies of a file
|
|
41
|
+
*/
|
|
42
|
+
getDependencies(file) {
|
|
43
|
+
return Array.from(this.graph.get(file) || []);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get dependents of a file (files that depend on this file)
|
|
48
|
+
*/
|
|
49
|
+
getDependents(file) {
|
|
50
|
+
return Array.from(this.reverseGraph.get(file) || []);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get callers of a symbol
|
|
55
|
+
*/
|
|
56
|
+
getSymbolCallers(symbol) {
|
|
57
|
+
return Array.from(this.symbolGraph.get(symbol) || []);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get transitive dependencies up to maxDepth
|
|
62
|
+
*/
|
|
63
|
+
getTransitiveDependencies(file, maxDepth = 3) {
|
|
64
|
+
const visited = new Set();
|
|
65
|
+
const result = new Set();
|
|
66
|
+
|
|
67
|
+
const traverse = (current, depth) => {
|
|
68
|
+
if (depth > maxDepth || visited.has(current)) return;
|
|
69
|
+
|
|
70
|
+
visited.add(current);
|
|
71
|
+
const deps = this.getDependencies(current);
|
|
72
|
+
|
|
73
|
+
for (const dep of deps) {
|
|
74
|
+
result.add(dep);
|
|
75
|
+
traverse(dep, depth + 1);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
traverse(file, 0);
|
|
80
|
+
return Array.from(result);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get transitive dependents (reverse dependencies)
|
|
85
|
+
*/
|
|
86
|
+
getTransitiveDependents(file, maxDepth = 3) {
|
|
87
|
+
const visited = new Set();
|
|
88
|
+
const result = new Set();
|
|
89
|
+
|
|
90
|
+
const traverse = (current, depth) => {
|
|
91
|
+
if (depth > maxDepth || visited.has(current)) return;
|
|
92
|
+
|
|
93
|
+
visited.add(current);
|
|
94
|
+
const dependents = this.getDependents(current);
|
|
95
|
+
|
|
96
|
+
for (const dependent of dependents) {
|
|
97
|
+
result.add(dependent);
|
|
98
|
+
traverse(dependent, depth + 1);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
traverse(file, 0);
|
|
103
|
+
return Array.from(result);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Calculate impact radius for a file
|
|
108
|
+
*/
|
|
109
|
+
getImpactRadius(file) {
|
|
110
|
+
const directDeps = this.getDependents(file);
|
|
111
|
+
const transitiveDeps = this.getTransitiveDependents(file, 2);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
direct: directDeps.length,
|
|
115
|
+
transitive: transitiveDeps.length,
|
|
116
|
+
total: transitiveDeps.length + directDeps.length,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Find path between two files
|
|
122
|
+
*/
|
|
123
|
+
findPath(from, to, maxDepth = 5) {
|
|
124
|
+
const queue = [[from]];
|
|
125
|
+
const visited = new Set([from]);
|
|
126
|
+
|
|
127
|
+
while (queue.length > 0) {
|
|
128
|
+
const path = queue.shift();
|
|
129
|
+
const current = path[path.length - 1];
|
|
130
|
+
|
|
131
|
+
if (path.length > maxDepth) continue;
|
|
132
|
+
if (current === to) return path;
|
|
133
|
+
|
|
134
|
+
const deps = this.getDependencies(current);
|
|
135
|
+
for (const dep of deps) {
|
|
136
|
+
if (!visited.has(dep)) {
|
|
137
|
+
visited.add(dep);
|
|
138
|
+
queue.push([...path, dep]);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return null; // No path found
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get graph statistics
|
|
148
|
+
*/
|
|
149
|
+
getStats() {
|
|
150
|
+
return {
|
|
151
|
+
totalFiles: this.graph.size,
|
|
152
|
+
totalEdges: Array.from(this.graph.values()).reduce((sum, set) => sum + set.size, 0),
|
|
153
|
+
totalSymbols: this.symbolGraph.size,
|
|
154
|
+
avgDependencies: this.graph.size > 0
|
|
155
|
+
? Array.from(this.graph.values()).reduce((sum, set) => sum + set.size, 0) / this.graph.size
|
|
156
|
+
: 0,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Utilities
|
|
3
|
+
* Helper functions for file operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { glob } from 'glob';
|
|
9
|
+
|
|
10
|
+
export class FileUtils {
|
|
11
|
+
/**
|
|
12
|
+
* Check if file is a source code file
|
|
13
|
+
*/
|
|
14
|
+
static isSourceFile(filePath) {
|
|
15
|
+
const extensions = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
|
|
16
|
+
return extensions.some(ext => filePath.endsWith(ext));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if file is a test file
|
|
21
|
+
*/
|
|
22
|
+
static isTestFile(filePath) {
|
|
23
|
+
return (
|
|
24
|
+
filePath.includes('.test.') ||
|
|
25
|
+
filePath.includes('.spec.') ||
|
|
26
|
+
filePath.includes('__tests__') ||
|
|
27
|
+
filePath.includes('__mocks__')
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if file is a config file
|
|
33
|
+
*/
|
|
34
|
+
static isConfigFile(filePath) {
|
|
35
|
+
return (
|
|
36
|
+
filePath.includes('config') ||
|
|
37
|
+
filePath.endsWith('.config.js') ||
|
|
38
|
+
filePath.endsWith('.config.ts') ||
|
|
39
|
+
filePath.includes('jest.') ||
|
|
40
|
+
filePath.includes('webpack.') ||
|
|
41
|
+
filePath.includes('vite.')
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Categorize file type
|
|
47
|
+
*/
|
|
48
|
+
static categorizeFile(filePath) {
|
|
49
|
+
if (this.isTestFile(filePath)) return 'test';
|
|
50
|
+
if (this.isConfigFile(filePath)) return 'config';
|
|
51
|
+
if (this.isSourceFile(filePath)) return 'source';
|
|
52
|
+
return 'other';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get all source files in directory
|
|
57
|
+
*/
|
|
58
|
+
static getAllSourceFiles(sourceDir, excludePaths = []) {
|
|
59
|
+
const pattern = `${sourceDir}/**/*.{js,jsx,ts,tsx,mjs,cjs}`;
|
|
60
|
+
const ignore = excludePaths.map(p => `${sourceDir}/**/${p}/**`);
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
return glob.sync(pattern, { ignore, absolute: true });
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error('Error scanning source files:', error.message);
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Read file content safely
|
|
72
|
+
*/
|
|
73
|
+
static readFile(filePath) {
|
|
74
|
+
try {
|
|
75
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.warn(`Failed to read ${filePath}:`, error.message);
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Calculate line changes between two file versions
|
|
84
|
+
*/
|
|
85
|
+
static calculateLineChanges(oldContent, newContent) {
|
|
86
|
+
const oldLines = oldContent ? oldContent.split('\n').length : 0;
|
|
87
|
+
const newLines = newContent ? newContent.split('\n').length : 0;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
added: Math.max(0, newLines - oldLines),
|
|
91
|
+
deleted: Math.max(0, oldLines - newLines),
|
|
92
|
+
total: Math.abs(newLines - oldLines),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Extract module name from file path
|
|
98
|
+
*/
|
|
99
|
+
static extractModuleName(filePath) {
|
|
100
|
+
const parts = filePath.split(path.sep);
|
|
101
|
+
const srcIndex = parts.indexOf('src');
|
|
102
|
+
|
|
103
|
+
if (srcIndex !== -1 && parts.length > srcIndex + 1) {
|
|
104
|
+
return parts[srcIndex + 1];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return path.basename(filePath, path.extname(filePath));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get relative path from project root
|
|
112
|
+
*/
|
|
113
|
+
static getRelativePath(filePath) {
|
|
114
|
+
return path.relative(process.cwd(), filePath);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Utilities
|
|
3
|
+
* Helper functions for git operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
|
|
10
|
+
export class GitUtils {
|
|
11
|
+
/**
|
|
12
|
+
* Get list of changed files between two refs
|
|
13
|
+
* @param {string} baseRef - Base git reference
|
|
14
|
+
* @param {string} headRef - Head git reference
|
|
15
|
+
* @param {string} workDir - Working directory (optional, defaults to cwd)
|
|
16
|
+
*/
|
|
17
|
+
static getChangedFiles(baseRef, headRef, workDir = null) {
|
|
18
|
+
try {
|
|
19
|
+
const cwd = workDir ? path.resolve(workDir) : process.cwd();
|
|
20
|
+
|
|
21
|
+
// Verify the directory exists and is a git repo
|
|
22
|
+
if (! fs.existsSync(cwd)) {
|
|
23
|
+
throw new Error(`Directory does not exist: ${cwd}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const diffCommand = `git diff --name-status ${baseRef}...${headRef}`;
|
|
27
|
+
const diffOutput = execSync(diffCommand, {
|
|
28
|
+
encoding: 'utf-8',
|
|
29
|
+
cwd: cwd // FIXED: Execute git command in the source directory
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const changedFiles = [];
|
|
33
|
+
const lines = diffOutput.trim().split('\n').filter(line => line);
|
|
34
|
+
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
const parts = line.split('\t');
|
|
37
|
+
const status = parts[0];
|
|
38
|
+
const filePath = parts[1];
|
|
39
|
+
|
|
40
|
+
changedFiles.push({
|
|
41
|
+
status: this.parseStatus(status),
|
|
42
|
+
path: filePath,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return changedFiles;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error('Error getting changed files:', error.message);
|
|
49
|
+
console.error(' Working directory:', workDir || process.cwd());
|
|
50
|
+
console.error(' Base ref:', baseRef);
|
|
51
|
+
console.error(' Head ref:', headRef);
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get file content at specific ref
|
|
58
|
+
* @param {string} filePath - Relative file path
|
|
59
|
+
* @param {string} ref - Git reference
|
|
60
|
+
* @param {string} workDir - Working directory (optional)
|
|
61
|
+
*/
|
|
62
|
+
static getFileContent(filePath, ref, workDir = null) {
|
|
63
|
+
try {
|
|
64
|
+
const cwd = workDir ? path.resolve(workDir) : process.cwd();
|
|
65
|
+
|
|
66
|
+
return execSync(`git show ${ref}:${filePath}`, {
|
|
67
|
+
encoding: 'utf-8',
|
|
68
|
+
cwd: cwd // FIXED: Execute in source directory
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
// File might be new or deleted
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get diff between two versions of a file
|
|
78
|
+
* @param {string} filePath - File path relative to git root (e.g., 'src/modules/file.ts')
|
|
79
|
+
* @param {string} baseRef - Base git reference
|
|
80
|
+
* @param {string} headRef - Head git reference
|
|
81
|
+
* @param {string} workDir - Working directory to run git from (optional)
|
|
82
|
+
*/
|
|
83
|
+
static getFileDiff(filePath, baseRef, headRef, workDir = null) {
|
|
84
|
+
try {
|
|
85
|
+
// Determine working directory (should be git root or a path inside it)
|
|
86
|
+
const cwd = workDir ? path.resolve(workDir) : process.cwd();
|
|
87
|
+
|
|
88
|
+
// Get git root from the working directory
|
|
89
|
+
const gitRoot = this.getGitRoot(cwd);
|
|
90
|
+
|
|
91
|
+
if (!gitRoot) {
|
|
92
|
+
console.error('Not a git repository:', cwd);
|
|
93
|
+
return '';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// filePath is already relative to git root (from git diff --name-status)
|
|
97
|
+
// Execute git diff from git root
|
|
98
|
+
const diffCommand = `git diff ${baseRef}...${headRef} -- ${filePath}`;
|
|
99
|
+
|
|
100
|
+
const result = execSync(diffCommand, {
|
|
101
|
+
encoding: 'utf-8',
|
|
102
|
+
cwd: gitRoot,
|
|
103
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large diffs
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return result;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error('Error getting file diff:', error.message);
|
|
109
|
+
console.error(' File:', filePath);
|
|
110
|
+
console.error(' Base:', baseRef);
|
|
111
|
+
console.error(' Head:', headRef);
|
|
112
|
+
console.error(' Working dir:', workDir);
|
|
113
|
+
return '';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Parse git status code
|
|
119
|
+
*/
|
|
120
|
+
static parseStatus(statusCode) {
|
|
121
|
+
const statusMap = {
|
|
122
|
+
'A': 'added',
|
|
123
|
+
'M': 'modified',
|
|
124
|
+
'D': 'deleted',
|
|
125
|
+
'R': 'renamed',
|
|
126
|
+
'C': 'copied',
|
|
127
|
+
};
|
|
128
|
+
return statusMap[statusCode[0]] || 'unknown';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get current branch name
|
|
133
|
+
*/
|
|
134
|
+
static getCurrentBranch(workDir = null) {
|
|
135
|
+
try {
|
|
136
|
+
const cwd = workDir ? path.resolve(workDir) : process.cwd();
|
|
137
|
+
|
|
138
|
+
return execSync('git rev-parse --abbrev-ref HEAD', {
|
|
139
|
+
encoding: 'utf-8',
|
|
140
|
+
cwd: cwd
|
|
141
|
+
}).trim();
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return 'unknown';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Check if ref exists
|
|
149
|
+
*/
|
|
150
|
+
static refExists(ref, workDir = null) {
|
|
151
|
+
try {
|
|
152
|
+
const cwd = workDir ? path.resolve(workDir) : process.cwd();
|
|
153
|
+
|
|
154
|
+
execSync(`git rev-parse --verify ${ref}`, {
|
|
155
|
+
encoding: 'utf-8',
|
|
156
|
+
stdio: 'ignore',
|
|
157
|
+
cwd: cwd
|
|
158
|
+
});
|
|
159
|
+
return true;
|
|
160
|
+
} catch (error) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Check if directory is a git repository
|
|
167
|
+
*/
|
|
168
|
+
static isGitRepo(workDir = null) {
|
|
169
|
+
try {
|
|
170
|
+
const cwd = workDir ? path.resolve(workDir) : process.cwd();
|
|
171
|
+
|
|
172
|
+
execSync('git rev-parse --git-dir', {
|
|
173
|
+
encoding: 'utf-8',
|
|
174
|
+
stdio: 'ignore',
|
|
175
|
+
cwd: cwd
|
|
176
|
+
});
|
|
177
|
+
return true;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get git root directory
|
|
185
|
+
*/
|
|
186
|
+
static getGitRoot(workDir = null) {
|
|
187
|
+
try {
|
|
188
|
+
const cwd = workDir ? path.resolve(workDir) : process.cwd();
|
|
189
|
+
|
|
190
|
+
return execSync('git rev-parse --show-toplevel', {
|
|
191
|
+
encoding: 'utf-8',
|
|
192
|
+
cwd: cwd
|
|
193
|
+
}).trim();
|
|
194
|
+
} catch (error) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|