korekt-cli 0.2.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 +130 -0
- package/package.json +50 -0
- package/src/config.js +75 -0
- package/src/formatter.js +143 -0
- package/src/git-logic.js +440 -0
- package/src/git-logic.test.js +542 -0
- package/src/index.js +398 -0
package/src/git-logic.js
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Truncate content to a maximum number of lines using "head and tail".
|
|
7
|
+
* @param {string} content - The string content to truncate
|
|
8
|
+
* @param {number} maxLines - The maximum number of lines to allow (default: 2000)
|
|
9
|
+
* @returns {string} - Truncated content string
|
|
10
|
+
*/
|
|
11
|
+
export function truncateContent(content, maxLines = 2000) {
|
|
12
|
+
const lines = content.split('\n');
|
|
13
|
+
if (lines.length <= maxLines) {
|
|
14
|
+
return content;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const halfMax = Math.floor(maxLines / 2);
|
|
18
|
+
const head = lines.slice(0, halfMax).join('\n');
|
|
19
|
+
const tail = lines.slice(-halfMax).join('\n');
|
|
20
|
+
return `${head}\n\n... [truncated] ...\n\n${tail}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Normalize git remote URL to HTTPS format
|
|
25
|
+
* Converts SSH URLs to HTTPS URLs for consistency
|
|
26
|
+
* @param {string} url - The git remote URL
|
|
27
|
+
* @returns {string} - Normalized HTTPS URL
|
|
28
|
+
*/
|
|
29
|
+
export function normalizeRepoUrl(url) {
|
|
30
|
+
// Handle Azure DevOps SSH format: git@ssh.dev.azure.com:v3/org/project/repo
|
|
31
|
+
const azureDevOpsSshMatch = url.match(/git@ssh\.dev\.azure\.com:v3\/([^\/]+)\/([^\/]+)\/(.+)/);
|
|
32
|
+
if (azureDevOpsSshMatch) {
|
|
33
|
+
const [, org, project, repo] = azureDevOpsSshMatch;
|
|
34
|
+
return `https://dev.azure.com/${org}/${project}/_git/${repo}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Handle GitHub SSH format: git@github.com:user/repo.git
|
|
38
|
+
const githubSshMatch = url.match(/git@github\.com:([^\/]+)\/(.+?)(?:\.git)?$/);
|
|
39
|
+
if (githubSshMatch) {
|
|
40
|
+
const [, user, repo] = githubSshMatch;
|
|
41
|
+
return `https://github.com/${user}/${repo}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Handle GitLab SSH format: git@gitlab.com:user/repo.git
|
|
45
|
+
const gitlabSshMatch = url.match(/git@gitlab\.com:([^\/]+)\/(.+?)(?:\.git)?$/);
|
|
46
|
+
if (gitlabSshMatch) {
|
|
47
|
+
const [, user, repo] = gitlabSshMatch;
|
|
48
|
+
return `https://gitlab.com/${user}/${repo}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// If already HTTPS or other format, return as-is (possibly removing .git suffix)
|
|
52
|
+
return url.replace(/\.git$/, '');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if a file path should be ignored based on patterns
|
|
57
|
+
* Supports glob patterns like *.lock, dist/*
|
|
58
|
+
* @param {string} filePath - The file path to check
|
|
59
|
+
* @param {string[]} patterns - Array of glob patterns to match against
|
|
60
|
+
* @returns {boolean} - True if the file should be ignored
|
|
61
|
+
*/
|
|
62
|
+
export function shouldIgnoreFile(filePath, patterns) {
|
|
63
|
+
if (!patterns || patterns.length === 0) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const pattern of patterns) {
|
|
68
|
+
// Convert glob pattern to regex
|
|
69
|
+
// Replace * with [^/]* (matches anything except /)
|
|
70
|
+
// Replace ** with .* (matches anything including /)
|
|
71
|
+
let regexPattern = pattern
|
|
72
|
+
.replace(/\./g, '\\.') // Escape dots
|
|
73
|
+
.replace(/\*\*/g, '___DOUBLESTAR___') // Temporarily replace **
|
|
74
|
+
.replace(/\*/g, '[^/]*') // Replace single * with [^/]*
|
|
75
|
+
.replace(/___DOUBLESTAR___/g, '.*') // Replace ** with .*
|
|
76
|
+
.replace(/\?/g, '.'); // Replace ? with .
|
|
77
|
+
|
|
78
|
+
// Handle leading **/ pattern - make it optional so it matches both with and without directory prefix
|
|
79
|
+
// For example, **/*.sql should match both "file.sql" and "dir/file.sql"
|
|
80
|
+
regexPattern = regexPattern.replace(/^\.\*\//, '(?:.*/)?');
|
|
81
|
+
|
|
82
|
+
// Add start and end anchors
|
|
83
|
+
regexPattern = '^' + regexPattern + '$';
|
|
84
|
+
|
|
85
|
+
const regex = new RegExp(regexPattern);
|
|
86
|
+
|
|
87
|
+
if (regex.test(filePath)) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Helper function to parse the complex output of git diff --name-status
|
|
97
|
+
*/
|
|
98
|
+
export function parseNameStatus(output) {
|
|
99
|
+
const files = [];
|
|
100
|
+
const lines = output.split('\n').filter(Boolean);
|
|
101
|
+
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
const parts = line.split('\t');
|
|
104
|
+
const statusRaw = parts[0];
|
|
105
|
+
let oldPath = null;
|
|
106
|
+
let path = null;
|
|
107
|
+
let status = null;
|
|
108
|
+
|
|
109
|
+
if (statusRaw.startsWith('R')) {
|
|
110
|
+
// Renamed files have format R<score>\told-path\tnew-path
|
|
111
|
+
status = 'R';
|
|
112
|
+
oldPath = parts[1];
|
|
113
|
+
path = parts[2];
|
|
114
|
+
} else if (statusRaw.startsWith('C')) {
|
|
115
|
+
// Copied files have format C<score>\told-path\tnew-path
|
|
116
|
+
status = 'C';
|
|
117
|
+
oldPath = parts[1];
|
|
118
|
+
path = parts[2];
|
|
119
|
+
} else {
|
|
120
|
+
// M, A, D files have format <status>\t<path>
|
|
121
|
+
status = statusRaw;
|
|
122
|
+
path = parts[1];
|
|
123
|
+
oldPath = parts[1]; // For consistency, oldPath is the same for M, A, D
|
|
124
|
+
}
|
|
125
|
+
files.push({ status, path, oldPath });
|
|
126
|
+
}
|
|
127
|
+
return files;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Analyze uncommitted changes (staged, unstaged, or all)
|
|
132
|
+
* @param {string} mode - 'staged', 'unstaged', or 'all'
|
|
133
|
+
* @param {string|null} ticketSystem - The ticket system to use (jira or ado), or null to skip ticket extraction
|
|
134
|
+
* @returns {Object|null} - The payload object ready for API submission, or null on error
|
|
135
|
+
*/
|
|
136
|
+
export async function runUncommittedReview(mode = 'all', ticketSystem = null, includeUntracked = false) {
|
|
137
|
+
try {
|
|
138
|
+
// 1. Get Repo URL and current branch name
|
|
139
|
+
const { stdout: repoUrl } = await execa('git', ['remote', 'get-url', 'origin']);
|
|
140
|
+
const { stdout: sourceBranch } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
141
|
+
const branchName = sourceBranch.trim();
|
|
142
|
+
|
|
143
|
+
// 2. Get changed files based on mode
|
|
144
|
+
let nameStatusOutput;
|
|
145
|
+
if (mode === 'staged') {
|
|
146
|
+
const { stdout } = await execa('git', ['diff', '--cached', '--name-status']);
|
|
147
|
+
nameStatusOutput = stdout;
|
|
148
|
+
console.log(chalk.gray('Analyzing staged changes...'));
|
|
149
|
+
} else if (mode === 'unstaged') {
|
|
150
|
+
const { stdout } = await execa('git', ['diff', '--name-status']);
|
|
151
|
+
nameStatusOutput = stdout;
|
|
152
|
+
console.log(chalk.gray('Analyzing unstaged changes...'));
|
|
153
|
+
} else {
|
|
154
|
+
// mode === 'all': combine staged and unstaged
|
|
155
|
+
const { stdout: staged } = await execa('git', ['diff', '--cached', '--name-status']);
|
|
156
|
+
const { stdout: unstaged } = await execa('git', ['diff', '--name-status']);
|
|
157
|
+
nameStatusOutput = [staged, unstaged].filter(Boolean).join('\n');
|
|
158
|
+
console.log(chalk.gray('Analyzing all uncommitted changes...'));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const fileList = parseNameStatus(nameStatusOutput);
|
|
162
|
+
const changedFiles = [];
|
|
163
|
+
|
|
164
|
+
// Handle untracked files if requested
|
|
165
|
+
if (includeUntracked) {
|
|
166
|
+
console.log(chalk.gray('Analyzing untracked files...'));
|
|
167
|
+
const { stdout: untrackedFilesOutput } = await execa('git', ['ls-files', '--others', '--exclude-standard']);
|
|
168
|
+
const untrackedFiles = untrackedFilesOutput.split('\n').filter(Boolean);
|
|
169
|
+
|
|
170
|
+
for (const file of untrackedFiles) {
|
|
171
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
172
|
+
const diff = content.split('\n').map(line => `+${line}`).join('\n');
|
|
173
|
+
changedFiles.push({
|
|
174
|
+
path: file,
|
|
175
|
+
status: 'A', // Untracked files are always additions
|
|
176
|
+
diff: diff,
|
|
177
|
+
content: '', // No old content
|
|
178
|
+
});
|
|
179
|
+
// Add to fileList to prevent duplication if it's also in nameStatusOutput (edge case)
|
|
180
|
+
fileList.push({ status: 'A', path: file, oldPath: file });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Deduplicate file list before processing diffs
|
|
185
|
+
const processedPaths = new Set(changedFiles.map(f => f.path));
|
|
186
|
+
const uniqueFileList = fileList.filter(file => !processedPaths.has(file.path));
|
|
187
|
+
|
|
188
|
+
for (const file of uniqueFileList) {
|
|
189
|
+
const { status, path, oldPath } = file;
|
|
190
|
+
|
|
191
|
+
// Get diff based on mode
|
|
192
|
+
let diff;
|
|
193
|
+
if (mode === 'staged') {
|
|
194
|
+
const { stdout } = await execa('git', ['diff', '--cached', '-U15', '--', path]);
|
|
195
|
+
diff = stdout;
|
|
196
|
+
} else if (mode === 'unstaged') {
|
|
197
|
+
const { stdout } = await execa('git', ['diff', '-U15', '--', path]);
|
|
198
|
+
diff = stdout;
|
|
199
|
+
} else {
|
|
200
|
+
// For 'all', try staged first, then unstaged
|
|
201
|
+
try {
|
|
202
|
+
const { stdout: stagedDiff } = await execa('git', ['diff', '--cached', '-U15', '--', path]);
|
|
203
|
+
if (stagedDiff) {
|
|
204
|
+
diff = stagedDiff;
|
|
205
|
+
} else {
|
|
206
|
+
const { stdout: unstagedDiff } = await execa('git', ['diff', '-U15', '--', path]);
|
|
207
|
+
diff = unstagedDiff;
|
|
208
|
+
}
|
|
209
|
+
} catch {
|
|
210
|
+
const { stdout: unstagedDiff } = await execa('git', ['diff', '-U15', '--', path]);
|
|
211
|
+
diff = unstagedDiff;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Get current content from HEAD (before changes)
|
|
216
|
+
let content = '';
|
|
217
|
+
if (status !== 'A') {
|
|
218
|
+
try {
|
|
219
|
+
const { stdout: headContent } = await execa('git', ['show', `HEAD:${oldPath}`]);
|
|
220
|
+
content = headContent;
|
|
221
|
+
} catch (e) {
|
|
222
|
+
console.warn(chalk.yellow(`Could not get HEAD content for ${oldPath}. Assuming it's new.`));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Truncate content
|
|
227
|
+
content = truncateContent(content);
|
|
228
|
+
|
|
229
|
+
// For deleted files, truncate the diff as well
|
|
230
|
+
if (status === 'D') {
|
|
231
|
+
diff = truncateContent(diff);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
changedFiles.push({
|
|
235
|
+
path: path,
|
|
236
|
+
status: status,
|
|
237
|
+
diff: diff,
|
|
238
|
+
content: content,
|
|
239
|
+
...((status === 'R' || status === 'C') && { old_path: oldPath }),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!nameStatusOutput.trim() && changedFiles.length === 0) {
|
|
244
|
+
console.log(chalk.yellow('No changes found to review.'));
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 3. Assemble payload
|
|
249
|
+
return {
|
|
250
|
+
repo_url: normalizeRepoUrl(repoUrl.trim()),
|
|
251
|
+
commit_messages: [], // No commits for uncommitted changes
|
|
252
|
+
changed_files: changedFiles,
|
|
253
|
+
source_branch: branchName,
|
|
254
|
+
};
|
|
255
|
+
} catch (error) {
|
|
256
|
+
console.error(chalk.red('Failed to analyze uncommitted changes:'), error.message);
|
|
257
|
+
if (error.stderr) {
|
|
258
|
+
console.error(chalk.red('Git Error:'), error.stderr);
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Main function to analyze local git changes and prepare review payload
|
|
266
|
+
* @param {string|null} targetBranch - The branch to compare against. If null, uses git reflog to find fork point.
|
|
267
|
+
* @param {string|null} ticketSystem - The ticket system to use (jira or ado), or null to skip ticket extraction
|
|
268
|
+
* @param {string[]|null} ignorePatterns - Array of glob patterns to ignore files
|
|
269
|
+
* @returns {Object|null} - The payload object ready for API submission, or null on error
|
|
270
|
+
*/
|
|
271
|
+
export async function runLocalReview(targetBranch = null, ticketSystem = null, ignorePatterns = null) {
|
|
272
|
+
try {
|
|
273
|
+
// 1. Get Repo URL, current branch name, and repository root
|
|
274
|
+
const { stdout: repoUrl } = await execa('git', ['remote', 'get-url', 'origin']);
|
|
275
|
+
const { stdout: sourceBranch } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
276
|
+
const branchName = sourceBranch.trim();
|
|
277
|
+
|
|
278
|
+
// Get the repository root directory - we'll run all git commands from there
|
|
279
|
+
const { stdout: repoRoot } = await execa('git', ['rev-parse', '--show-toplevel']);
|
|
280
|
+
const repoRootPath = repoRoot.trim();
|
|
281
|
+
|
|
282
|
+
// If a branch is provided, check it exists and try to fetch latest remote version
|
|
283
|
+
let targetBranchRef = targetBranch; // Will be updated to origin/branch if remote exists
|
|
284
|
+
if (targetBranch) {
|
|
285
|
+
// Check if the branch exists locally
|
|
286
|
+
try {
|
|
287
|
+
await execa('git', ['rev-parse', '--verify', targetBranch]);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.error(chalk.red(`Branch '${targetBranch}' does not exist locally.`));
|
|
290
|
+
console.error(chalk.gray(`Please check out the branch first or specify a different one.`));
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Try to fetch the latest changes from remote (non-destructive)
|
|
295
|
+
try {
|
|
296
|
+
console.log(chalk.gray(`Fetching latest changes for branch '${targetBranch}'...`));
|
|
297
|
+
await execa('git', ['fetch', 'origin', targetBranch]);
|
|
298
|
+
|
|
299
|
+
// If fetch succeeded, use the remote-tracking branch for comparison
|
|
300
|
+
// This is safer as it doesn't modify the user's local branch
|
|
301
|
+
targetBranchRef = `origin/${targetBranch}`;
|
|
302
|
+
console.log(chalk.gray(`Using remote-tracking branch 'origin/${targetBranch}' for comparison.`));
|
|
303
|
+
} catch (fetchError) {
|
|
304
|
+
console.warn(chalk.yellow(`Could not fetch remote branch 'origin/${targetBranch}'.`));
|
|
305
|
+
console.warn(chalk.gray(`Proceeding with local branch '${targetBranch}' for comparison.`));
|
|
306
|
+
// targetBranchRef stays as targetBranch (local branch)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let mergeBase;
|
|
311
|
+
|
|
312
|
+
// 2. If no target branch, use git reflog to find fork point
|
|
313
|
+
if (!targetBranch) {
|
|
314
|
+
try {
|
|
315
|
+
// Use git reflog to find where the branch was created
|
|
316
|
+
const { stdout: reflog } = await execa('git', ['reflog', 'show', '--no-abbrev-commit', branchName]);
|
|
317
|
+
const lines = reflog.split('\n');
|
|
318
|
+
|
|
319
|
+
// Look for the branch creation point (last line in reflog)
|
|
320
|
+
const creationLine = lines[lines.length - 1];
|
|
321
|
+
if (creationLine) {
|
|
322
|
+
const match = creationLine.match(/^([a-f0-9]{40})/);
|
|
323
|
+
if (match) {
|
|
324
|
+
mergeBase = match[1];
|
|
325
|
+
console.log(chalk.gray(`Auto-detected fork point from reflog: ${mergeBase.substring(0, 7)}`));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!mergeBase) {
|
|
330
|
+
throw new Error('Could not find fork point in reflog');
|
|
331
|
+
}
|
|
332
|
+
} catch (error) {
|
|
333
|
+
console.error(chalk.red('Could not auto-detect fork point. Please specify a target branch.'));
|
|
334
|
+
console.error(chalk.gray('Usage: kk review <target-branch>'));
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
// 3. Use specified target branch (either remote-tracking or local)
|
|
339
|
+
const { stdout: base } = await execa('git', ['merge-base', targetBranchRef, 'HEAD']);
|
|
340
|
+
mergeBase = base.trim();
|
|
341
|
+
console.log(
|
|
342
|
+
chalk.gray(
|
|
343
|
+
`Comparing against ${targetBranchRef} (merge-base: ${mergeBase.substring(0, 7)})...`
|
|
344
|
+
)
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const diffRange = `${mergeBase}..HEAD`;
|
|
349
|
+
console.log(chalk.gray(`Analyzing commits from ${mergeBase.substring(0, 7)} to HEAD...`));
|
|
350
|
+
|
|
351
|
+
// 3. Get Commit Messages with proper delimiter
|
|
352
|
+
const { stdout: logOutput } = await execa('git', ['log', '--pretty=%B---EOC---', diffRange], { cwd: repoRootPath });
|
|
353
|
+
const commitMessages = logOutput
|
|
354
|
+
.split('---EOC---')
|
|
355
|
+
.map((msg) => msg.trim())
|
|
356
|
+
.filter(Boolean);
|
|
357
|
+
|
|
358
|
+
// 4. Get changed files and their status
|
|
359
|
+
const { stdout: nameStatusOutput } = await execa('git', ['diff', '--name-status', diffRange], { cwd: repoRootPath });
|
|
360
|
+
const fileList = parseNameStatus(nameStatusOutput);
|
|
361
|
+
|
|
362
|
+
// Filter out ignored files
|
|
363
|
+
let filteredFileList = fileList;
|
|
364
|
+
let ignoredCount = 0;
|
|
365
|
+
if (ignorePatterns && ignorePatterns.length > 0) {
|
|
366
|
+
filteredFileList = fileList.filter(file => {
|
|
367
|
+
const ignored = shouldIgnoreFile(file.path, ignorePatterns);
|
|
368
|
+
if (ignored) {
|
|
369
|
+
ignoredCount++;
|
|
370
|
+
console.log(chalk.gray(` Ignoring: ${file.path}`));
|
|
371
|
+
}
|
|
372
|
+
return !ignored;
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (ignoredCount > 0) {
|
|
377
|
+
console.log(chalk.gray(`Ignored ${ignoredCount} file(s) based on patterns\n`));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
console.log(chalk.gray(`Collecting diffs for ${filteredFileList.length} file(s)...`));
|
|
381
|
+
|
|
382
|
+
const changedFiles = [];
|
|
383
|
+
for (const file of filteredFileList) {
|
|
384
|
+
const { status, path, oldPath } = file;
|
|
385
|
+
|
|
386
|
+
// Run git commands from the repository root to handle all file paths correctly
|
|
387
|
+
// This works regardless of whether we're in a subdirectory or at the repo root
|
|
388
|
+
const { stdout: diff } = await execa('git', ['diff', '-U15', diffRange, '--', path], { cwd: repoRootPath });
|
|
389
|
+
|
|
390
|
+
// Get the original content from the base commit
|
|
391
|
+
let content = '';
|
|
392
|
+
if (status !== 'A') {
|
|
393
|
+
// Added files have no original content
|
|
394
|
+
try {
|
|
395
|
+
const { stdout: originalContent } = await execa('git', [
|
|
396
|
+
'show',
|
|
397
|
+
`${mergeBase.trim()}:${oldPath}`,
|
|
398
|
+
], { cwd: repoRootPath });
|
|
399
|
+
content = originalContent;
|
|
400
|
+
} catch (e) {
|
|
401
|
+
// This can happen if a file was added and modified in the same branch
|
|
402
|
+
console.warn(
|
|
403
|
+
chalk.yellow(`Could not get original content for ${oldPath}. Assuming it was added.`)
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Truncate content
|
|
409
|
+
content = truncateContent(content);
|
|
410
|
+
|
|
411
|
+
// For deleted files, truncate the diff as well
|
|
412
|
+
let truncatedDiff = diff;
|
|
413
|
+
if (status === 'D') {
|
|
414
|
+
truncatedDiff = truncateContent(diff);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
changedFiles.push({
|
|
418
|
+
path: path,
|
|
419
|
+
status: status,
|
|
420
|
+
diff: truncatedDiff,
|
|
421
|
+
content: content,
|
|
422
|
+
...((status === 'R' || status === 'C') && { old_path: oldPath }), // Include old_path for renames and copies
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// 5. Assemble the final payload
|
|
427
|
+
return {
|
|
428
|
+
repo_url: normalizeRepoUrl(repoUrl.trim()),
|
|
429
|
+
commit_messages: commitMessages,
|
|
430
|
+
changed_files: changedFiles,
|
|
431
|
+
source_branch: branchName,
|
|
432
|
+
};
|
|
433
|
+
} catch (error) {
|
|
434
|
+
console.error(chalk.red('Failed to run local review analysis:'), error.message);
|
|
435
|
+
if (error.stderr) {
|
|
436
|
+
console.error(chalk.red('Git Error:'), error.stderr);
|
|
437
|
+
}
|
|
438
|
+
return null; // Return null to indicate failure
|
|
439
|
+
}
|
|
440
|
+
}
|