codeant-cli 0.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 +226 -0
- package/package.json +28 -0
- package/src/commands/getBaseUrl.js +25 -0
- package/src/commands/login.js +124 -0
- package/src/commands/logout.js +30 -0
- package/src/commands/secrets.js +251 -0
- package/src/commands/setBaseUrl.js +37 -0
- package/src/index.js +62 -0
- package/src/utils/config.js +42 -0
- package/src/utils/fetchApi.js +42 -0
- package/src/utils/gitDiffHelper.js +762 -0
- package/src/utils/secretsApiHelper.js +127 -0
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
import { exec } from 'child_process';
|
|
5
|
+
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
|
|
8
|
+
class GitDiffHelper {
|
|
9
|
+
constructor(workspacePath) {
|
|
10
|
+
this.workspacePath = workspacePath;
|
|
11
|
+
this.gitRoot = null;
|
|
12
|
+
this.currentBranch = null;
|
|
13
|
+
this.defaultBranch = null;
|
|
14
|
+
this.baseCommit = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Find the git root directory by traversing up from the workspace path
|
|
19
|
+
*/
|
|
20
|
+
async findGitRoot(directory) {
|
|
21
|
+
try {
|
|
22
|
+
// First check current directory
|
|
23
|
+
const files = await fs.readdir(directory);
|
|
24
|
+
if (files.includes('.git')) {
|
|
25
|
+
return directory;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Traverse up to parent directories
|
|
29
|
+
let currentDir = directory;
|
|
30
|
+
while (currentDir !== path.dirname(currentDir)) {
|
|
31
|
+
currentDir = path.dirname(currentDir);
|
|
32
|
+
try {
|
|
33
|
+
const parentFiles = await fs.readdir(currentDir);
|
|
34
|
+
if (parentFiles.includes('.git')) {
|
|
35
|
+
return currentDir;
|
|
36
|
+
}
|
|
37
|
+
} catch (err) {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return null;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error(`Error finding git root: ${error.message}`);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Initialize the helper: finds git root, current branch, default branch, and base commit
|
|
51
|
+
*/
|
|
52
|
+
async init() {
|
|
53
|
+
// Find git root
|
|
54
|
+
this.gitRoot = await this.findGitRoot(this.workspacePath);
|
|
55
|
+
if (!this.gitRoot) {
|
|
56
|
+
throw new Error('Could not find a .git directory.');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
// Try to fetch origin (silently fails if no remote)
|
|
61
|
+
await this.fetchOrigin();
|
|
62
|
+
} catch (err) {
|
|
63
|
+
// No remote, continue anyway
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// Get current branch
|
|
68
|
+
const { stdout: branchName } = await execAsync(
|
|
69
|
+
'git rev-parse --abbrev-ref HEAD',
|
|
70
|
+
{ cwd: this.gitRoot }
|
|
71
|
+
);
|
|
72
|
+
this.currentBranch = branchName.trim();
|
|
73
|
+
} catch (err) {
|
|
74
|
+
this.currentBranch = 'main';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Determine default branch
|
|
78
|
+
try {
|
|
79
|
+
const { stdout: remoteBranch } = await execAsync(
|
|
80
|
+
'git rev-parse --abbrev-ref origin/HEAD',
|
|
81
|
+
{ cwd: this.gitRoot }
|
|
82
|
+
);
|
|
83
|
+
this.defaultBranch = remoteBranch.trim().replace('origin/', '');
|
|
84
|
+
} catch (err) {
|
|
85
|
+
this.defaultBranch = 'main';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Find merge base commit or fallback to HEAD
|
|
89
|
+
try {
|
|
90
|
+
const { stdout: mergeBase } = await execAsync(
|
|
91
|
+
`git merge-base ${this.currentBranch} origin/${this.defaultBranch}`,
|
|
92
|
+
{ cwd: this.gitRoot }
|
|
93
|
+
);
|
|
94
|
+
this.baseCommit = mergeBase.trim();
|
|
95
|
+
} catch (err) {
|
|
96
|
+
// Fallback to HEAD if no merge base found
|
|
97
|
+
try {
|
|
98
|
+
const { stdout: head } = await execAsync(
|
|
99
|
+
'git rev-parse HEAD',
|
|
100
|
+
{ cwd: this.gitRoot }
|
|
101
|
+
);
|
|
102
|
+
this.baseCommit = head.trim();
|
|
103
|
+
} catch (headErr) {
|
|
104
|
+
// No commits yet - set to empty tree
|
|
105
|
+
this.baseCommit = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async fetchOrigin() {
|
|
111
|
+
try {
|
|
112
|
+
await execAsync('git fetch origin', { cwd: this.gitRoot });
|
|
113
|
+
} catch (err) {
|
|
114
|
+
// Silently continue if fetch fails (e.g., offline)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get all branches (local and remote)
|
|
120
|
+
*/
|
|
121
|
+
async getAllBranches() {
|
|
122
|
+
if (!this.gitRoot) {
|
|
123
|
+
throw new Error('GitDiffHelper not initialized. Call init() first.');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const { stdout: branchesStr } = await execAsync(
|
|
127
|
+
'git branch -a --format="%(refname:short)"',
|
|
128
|
+
{ cwd: this.gitRoot }
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const branches = branchesStr
|
|
132
|
+
.split('\n')
|
|
133
|
+
.map(b => b.trim())
|
|
134
|
+
.filter(Boolean)
|
|
135
|
+
.map(b => b.replace('origin/', ''))
|
|
136
|
+
.filter((value, index, self) => self.indexOf(value) === index);
|
|
137
|
+
|
|
138
|
+
return branches;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get recent commits with metadata
|
|
143
|
+
*/
|
|
144
|
+
async getRecentCommits(limit = 10) {
|
|
145
|
+
if (!this.gitRoot) {
|
|
146
|
+
throw new Error('GitDiffHelper not initialized. Call init() first.');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const { stdout: commitsStr } = await execAsync(
|
|
150
|
+
`git log --pretty=format:"%H|%s|%an|%ar" -n ${limit}`,
|
|
151
|
+
{ cwd: this.gitRoot }
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const commits = commitsStr
|
|
155
|
+
.split('\n')
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
.map(line => {
|
|
158
|
+
const [hash, message, author, date] = line.split('|');
|
|
159
|
+
return { hash, message, author, date };
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return commits;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get files changed since merge base
|
|
167
|
+
*/
|
|
168
|
+
async getChangedFiles() {
|
|
169
|
+
if (!this.gitRoot || !this.baseCommit) {
|
|
170
|
+
throw new Error('GitDiffHelper not initialized. Call init() first.');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const cmd = [
|
|
174
|
+
`git diff --name-only --diff-filter=ACMRD ${this.baseCommit}`,
|
|
175
|
+
`git diff --name-only --cached --diff-filter=ACMRD`,
|
|
176
|
+
`git ls-files --others --exclude-standard`
|
|
177
|
+
].join(' && ');
|
|
178
|
+
|
|
179
|
+
const { stdout: changedFiles } = await execAsync(cmd, { cwd: this.gitRoot });
|
|
180
|
+
|
|
181
|
+
const uniqueFiles = Array.from(new Set(
|
|
182
|
+
changedFiles
|
|
183
|
+
.split('\n')
|
|
184
|
+
.map(f => f.trim())
|
|
185
|
+
.filter(Boolean)
|
|
186
|
+
));
|
|
187
|
+
|
|
188
|
+
return uniqueFiles;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get staged files only
|
|
193
|
+
*/
|
|
194
|
+
async getStagedFiles() {
|
|
195
|
+
if (!this.gitRoot) {
|
|
196
|
+
throw new Error('GitDiffHelper not initialized. Call init() first.');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const { stdout: stagedFiles } = await execAsync(
|
|
200
|
+
'git diff --name-only --cached',
|
|
201
|
+
{ cwd: this.gitRoot }
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
return stagedFiles
|
|
205
|
+
.split('\n')
|
|
206
|
+
.map(f => f.trim())
|
|
207
|
+
.filter(Boolean);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get diff based on review configuration
|
|
212
|
+
*/
|
|
213
|
+
async getDiffBasedOnReviewConfig(reviewConfig = null) {
|
|
214
|
+
if (!this.gitRoot) {
|
|
215
|
+
throw new Error('GitDiffHelper not initialized. Call init() first.');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const config = reviewConfig || {
|
|
219
|
+
type: 'branch-diff',
|
|
220
|
+
targetBranch: this.defaultBranch,
|
|
221
|
+
commits: null
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
switch (config.type) {
|
|
225
|
+
case 'branch-diff':
|
|
226
|
+
return this.getAllDiffInfo();
|
|
227
|
+
|
|
228
|
+
case 'last-commit':
|
|
229
|
+
return this._getLastCommitDiff();
|
|
230
|
+
|
|
231
|
+
case 'select-commits':
|
|
232
|
+
if (!config.commits || config.commits.length === 0) {
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
return this._getSpecificCommitsDiff(config.commits);
|
|
236
|
+
|
|
237
|
+
case 'uncommitted':
|
|
238
|
+
return this._getUncommittedChanges();
|
|
239
|
+
|
|
240
|
+
case 'staged-only':
|
|
241
|
+
return this._getStagedChanges();
|
|
242
|
+
|
|
243
|
+
case 'unpushed':
|
|
244
|
+
return this.getUnpushedChangesDiff();
|
|
245
|
+
|
|
246
|
+
default:
|
|
247
|
+
return this.getAllDiffInfo();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async _getLastCommitDiff() {
|
|
252
|
+
if (!this.gitRoot) {
|
|
253
|
+
throw new Error('GitDiffHelper not initialized. Call init() first.');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// First check if there are any commits
|
|
257
|
+
try {
|
|
258
|
+
await execAsync('git rev-parse HEAD', { cwd: this.gitRoot });
|
|
259
|
+
} catch (err) {
|
|
260
|
+
// No commits at all
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
// Check if HEAD~1 exists (more than one commit)
|
|
266
|
+
await execAsync('git rev-parse HEAD~1', { cwd: this.gitRoot });
|
|
267
|
+
|
|
268
|
+
const { stdout: changedFiles } = await execAsync(
|
|
269
|
+
'git diff --name-only HEAD~1 HEAD',
|
|
270
|
+
{ cwd: this.gitRoot }
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
const files = changedFiles
|
|
274
|
+
.split('\n')
|
|
275
|
+
.map(f => f.trim())
|
|
276
|
+
.filter(Boolean);
|
|
277
|
+
|
|
278
|
+
if (files.length === 0) {
|
|
279
|
+
return [];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const originalBaseCommit = this.baseCommit;
|
|
283
|
+
this.baseCommit = 'HEAD~1';
|
|
284
|
+
|
|
285
|
+
const diffs = await Promise.all(
|
|
286
|
+
files.map(fp => this.getDiffInfoForFile(fp))
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
this.baseCommit = originalBaseCommit;
|
|
290
|
+
return diffs.flat();
|
|
291
|
+
} catch (err) {
|
|
292
|
+
// HEAD~1 doesn't exist - this is the first commit
|
|
293
|
+
try {
|
|
294
|
+
const { stdout: changedFiles } = await execAsync(
|
|
295
|
+
'git diff-tree --no-commit-id --name-only -r HEAD',
|
|
296
|
+
{ cwd: this.gitRoot }
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const files = changedFiles
|
|
300
|
+
.split('\n')
|
|
301
|
+
.map(f => f.trim())
|
|
302
|
+
.filter(Boolean);
|
|
303
|
+
|
|
304
|
+
if (files.length === 0) {
|
|
305
|
+
return [];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// For first commit, compare against empty tree
|
|
309
|
+
const originalBaseCommit = this.baseCommit;
|
|
310
|
+
this.baseCommit = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'; // Git's empty tree hash
|
|
311
|
+
|
|
312
|
+
const diffs = await Promise.all(
|
|
313
|
+
files.map(fp => this.getDiffInfoForFile(fp))
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
this.baseCommit = originalBaseCommit;
|
|
317
|
+
return diffs.flat();
|
|
318
|
+
} catch (innerErr) {
|
|
319
|
+
console.error('Error getting last commit diff:', innerErr.message);
|
|
320
|
+
return [];
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async _getSpecificCommitsDiff(commitHashes) {
|
|
326
|
+
const allDiffs = [];
|
|
327
|
+
const originalBaseCommit = this.baseCommit;
|
|
328
|
+
|
|
329
|
+
for (const hash of commitHashes) {
|
|
330
|
+
try {
|
|
331
|
+
const { stdout: changedFiles } = await execAsync(
|
|
332
|
+
`git diff --name-only ${hash}~1 ${hash}`,
|
|
333
|
+
{ cwd: this.gitRoot }
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const files = changedFiles
|
|
337
|
+
.split('\n')
|
|
338
|
+
.map(f => f.trim())
|
|
339
|
+
.filter(Boolean);
|
|
340
|
+
|
|
341
|
+
this.baseCommit = `${hash}~1`;
|
|
342
|
+
const commitDiffs = await Promise.all(
|
|
343
|
+
files.map(fp => this.getDiffInfoForFile(fp))
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
allDiffs.push(...commitDiffs.flat());
|
|
347
|
+
} catch (error) {
|
|
348
|
+
console.error(`Error processing commit ${hash}:`, error.message);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
this.baseCommit = originalBaseCommit;
|
|
353
|
+
return allDiffs;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async _getUncommittedChanges() {
|
|
357
|
+
const { stdout: allModified } = await execAsync(
|
|
358
|
+
'git status --porcelain',
|
|
359
|
+
{ cwd: this.gitRoot }
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
const files = allModified
|
|
363
|
+
.split('\n')
|
|
364
|
+
.map(line => line.substring(3).trim())
|
|
365
|
+
.filter(Boolean);
|
|
366
|
+
|
|
367
|
+
const diffs = [];
|
|
368
|
+
for (const file of files) {
|
|
369
|
+
try {
|
|
370
|
+
const { stdout: patchStr } = await execAsync(
|
|
371
|
+
`git diff HEAD -- "${file}"`,
|
|
372
|
+
{ cwd: this.gitRoot }
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
if (patchStr) {
|
|
376
|
+
const diffInfo = await this.getDiffInfoForFile(file);
|
|
377
|
+
diffs.push(...diffInfo);
|
|
378
|
+
}
|
|
379
|
+
} catch (error) {
|
|
380
|
+
// Skip files that can't be diffed
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return diffs;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async _getStagedChanges() {
|
|
388
|
+
const { stdout: stagedFiles } = await execAsync(
|
|
389
|
+
'git diff --name-only --cached HEAD',
|
|
390
|
+
{ cwd: this.gitRoot }
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
const files = stagedFiles
|
|
394
|
+
.split('\n')
|
|
395
|
+
.map(f => f.trim())
|
|
396
|
+
.filter(Boolean);
|
|
397
|
+
|
|
398
|
+
const diffs = [];
|
|
399
|
+
const originalBaseCommit = this.baseCommit;
|
|
400
|
+
|
|
401
|
+
for (const file of files) {
|
|
402
|
+
try {
|
|
403
|
+
this.baseCommit = 'HEAD';
|
|
404
|
+
const diffInfo = await this.getDiffInfoForFile(file);
|
|
405
|
+
diffs.push(...diffInfo);
|
|
406
|
+
} catch (error) {
|
|
407
|
+
console.error(`Error processing staged file ${file}:`, error.message);
|
|
408
|
+
} finally {
|
|
409
|
+
this.baseCommit = originalBaseCommit;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return diffs;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async getUnpushedChangesDiff() {
|
|
417
|
+
if (!this.gitRoot) {
|
|
418
|
+
throw new Error('GitDiffHelper not initialized. Call init() first.');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
const { stdout: currentBranchRaw } = await execAsync(
|
|
423
|
+
'git rev-parse --abbrev-ref HEAD',
|
|
424
|
+
{ cwd: this.gitRoot }
|
|
425
|
+
);
|
|
426
|
+
const currentBranch = currentBranchRaw.trim();
|
|
427
|
+
|
|
428
|
+
let upstream = null;
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
const { stdout: upstreamRaw } = await execAsync(
|
|
432
|
+
'git rev-parse --abbrev-ref --symbolic-full-name @{u}',
|
|
433
|
+
{ cwd: this.gitRoot }
|
|
434
|
+
);
|
|
435
|
+
upstream = upstreamRaw.trim();
|
|
436
|
+
} catch (upstreamError) {
|
|
437
|
+
// Try to find remote branch
|
|
438
|
+
try {
|
|
439
|
+
const { stdout: remoteBranches } = await execAsync(
|
|
440
|
+
'git branch -r',
|
|
441
|
+
{ cwd: this.gitRoot }
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
if (remoteBranches.includes(`origin/${currentBranch}`)) {
|
|
445
|
+
upstream = `origin/${currentBranch}`;
|
|
446
|
+
}
|
|
447
|
+
} catch (error) {
|
|
448
|
+
// Ignore
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (!upstream) {
|
|
452
|
+
const defaultBranches = ['origin/main', 'origin/master'];
|
|
453
|
+
for (const branch of defaultBranches) {
|
|
454
|
+
try {
|
|
455
|
+
await execAsync(`git rev-parse ${branch}`, { cwd: this.gitRoot });
|
|
456
|
+
upstream = branch;
|
|
457
|
+
break;
|
|
458
|
+
} catch (e) {
|
|
459
|
+
// Try next
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (!upstream) {
|
|
466
|
+
return this._getUncommittedChanges();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Get unpushed files
|
|
470
|
+
const { stdout: unpushedFiles } = await execAsync(
|
|
471
|
+
`git diff --name-only ${upstream}..HEAD`,
|
|
472
|
+
{ cwd: this.gitRoot }
|
|
473
|
+
);
|
|
474
|
+
const unpushedFilesList = unpushedFiles.split('\n').map(f => f.trim()).filter(Boolean);
|
|
475
|
+
|
|
476
|
+
// Get staged files
|
|
477
|
+
const { stdout: stagedFiles } = await execAsync(
|
|
478
|
+
'git diff --name-only --cached',
|
|
479
|
+
{ cwd: this.gitRoot }
|
|
480
|
+
);
|
|
481
|
+
const stagedFilesList = stagedFiles.split('\n').map(f => f.trim()).filter(Boolean);
|
|
482
|
+
|
|
483
|
+
// Get unstaged files
|
|
484
|
+
const { stdout: unstagedFiles } = await execAsync(
|
|
485
|
+
'git diff --name-only',
|
|
486
|
+
{ cwd: this.gitRoot }
|
|
487
|
+
);
|
|
488
|
+
const unstagedFilesList = unstagedFiles.split('\n').map(f => f.trim()).filter(Boolean);
|
|
489
|
+
|
|
490
|
+
// Combine and deduplicate
|
|
491
|
+
const uniqueFiles = Array.from(new Set([
|
|
492
|
+
...unpushedFilesList,
|
|
493
|
+
...stagedFilesList,
|
|
494
|
+
...unstagedFilesList
|
|
495
|
+
]));
|
|
496
|
+
|
|
497
|
+
if (uniqueFiles.length === 0) {
|
|
498
|
+
return [];
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const originalBaseCommit = this.baseCommit;
|
|
502
|
+
try {
|
|
503
|
+
const { stdout: upstreamCommit } = await execAsync(
|
|
504
|
+
`git rev-parse ${upstream}`,
|
|
505
|
+
{ cwd: this.gitRoot }
|
|
506
|
+
);
|
|
507
|
+
this.baseCommit = upstreamCommit.trim();
|
|
508
|
+
|
|
509
|
+
const diffs = await Promise.all(
|
|
510
|
+
uniqueFiles.map(fp => this.getDiffInfoForFile(fp))
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
return diffs.flat();
|
|
514
|
+
} finally {
|
|
515
|
+
this.baseCommit = originalBaseCommit;
|
|
516
|
+
}
|
|
517
|
+
} catch (error) {
|
|
518
|
+
console.error('Error getting unpushed changes:', error.message);
|
|
519
|
+
return this._getUncommittedChanges();
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async getUnpushedCommits() {
|
|
524
|
+
if (!this.gitRoot) {
|
|
525
|
+
throw new Error('GitDiffHelper not initialized. Call init() first.');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
try {
|
|
529
|
+
let upstream;
|
|
530
|
+
try {
|
|
531
|
+
const { stdout } = await execAsync(
|
|
532
|
+
'git rev-parse --abbrev-ref --symbolic-full-name @{u}',
|
|
533
|
+
{ cwd: this.gitRoot }
|
|
534
|
+
);
|
|
535
|
+
upstream = stdout.trim();
|
|
536
|
+
} catch (error) {
|
|
537
|
+
// Find alternative upstream
|
|
538
|
+
const { stdout: currentBranch } = await execAsync(
|
|
539
|
+
'git rev-parse --abbrev-ref HEAD',
|
|
540
|
+
{ cwd: this.gitRoot }
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
const { stdout: remoteBranches } = await execAsync(
|
|
544
|
+
`git ls-remote --heads origin ${currentBranch.trim()}`,
|
|
545
|
+
{ cwd: this.gitRoot }
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
if (remoteBranches.trim()) {
|
|
549
|
+
upstream = `origin/${currentBranch.trim()}`;
|
|
550
|
+
} else {
|
|
551
|
+
upstream = 'origin/main';
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const { stdout: commitsStr } = await execAsync(
|
|
556
|
+
`git log ${upstream}..HEAD --pretty=format:"%H|%s|%an|%ar"`,
|
|
557
|
+
{ cwd: this.gitRoot }
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
if (!commitsStr.trim()) {
|
|
561
|
+
return [];
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return commitsStr
|
|
565
|
+
.split('\n')
|
|
566
|
+
.filter(Boolean)
|
|
567
|
+
.map(line => {
|
|
568
|
+
const [hash, message, author, date] = line.split('|');
|
|
569
|
+
return { hash, message, author, date };
|
|
570
|
+
});
|
|
571
|
+
} catch (error) {
|
|
572
|
+
console.error('Error getting unpushed commits:', error.message);
|
|
573
|
+
return [];
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
_generateRandomHash() {
|
|
578
|
+
return Math.random().toString(36).substring(2, 9) + Math.random().toString(36).substring(2, 9);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async getFileDiff(filePath) {
|
|
582
|
+
if (!this.gitRoot || !this.baseCommit) {
|
|
583
|
+
throw new Error('GitDiffHelper not initialized. Call init() first.');
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
let diff = '';
|
|
587
|
+
|
|
588
|
+
try {
|
|
589
|
+
await execAsync(`git ls-files --error-unmatch "${filePath}"`, {
|
|
590
|
+
cwd: this.gitRoot,
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
const { stdout } = await execAsync(
|
|
594
|
+
`git diff ${this.baseCommit} -- "${filePath}"`,
|
|
595
|
+
{ cwd: this.gitRoot }
|
|
596
|
+
);
|
|
597
|
+
diff = stdout;
|
|
598
|
+
} catch (err) {
|
|
599
|
+
// Handle untracked files
|
|
600
|
+
try {
|
|
601
|
+
const absoluteFilePath = path.resolve(this.gitRoot, filePath);
|
|
602
|
+
const headFileStr = await fs.readFile(absoluteFilePath, 'utf8');
|
|
603
|
+
const lines = headFileStr.split('\n');
|
|
604
|
+
|
|
605
|
+
let patchStr = `diff --git a/${filePath} b/${filePath}\n`;
|
|
606
|
+
patchStr += `new file mode 100644\n`;
|
|
607
|
+
patchStr += `index 0000000..${this._generateRandomHash()}\n`;
|
|
608
|
+
patchStr += `--- /dev/null\n`;
|
|
609
|
+
patchStr += `+++ b/${filePath}\n`;
|
|
610
|
+
patchStr += `@@ -0,0 +1,${lines.length} @@\n`;
|
|
611
|
+
|
|
612
|
+
lines.forEach(line => {
|
|
613
|
+
patchStr += `+${line}\n`;
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
diff = patchStr;
|
|
617
|
+
} catch (manualDiffError) {
|
|
618
|
+
diff = '';
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return diff;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async getAllDiffs() {
|
|
626
|
+
if (!this.gitRoot || !this.baseCommit) {
|
|
627
|
+
throw new Error('GitDiffHelper not initialized. Call init() first.');
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const { stdout: fullDiff } = await execAsync(
|
|
631
|
+
`git diff ${this.baseCommit}`,
|
|
632
|
+
{ cwd: this.gitRoot }
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
return fullDiff;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async getDiffInfoForFile(filePath) {
|
|
639
|
+
if (!this.gitRoot || !this.baseCommit) {
|
|
640
|
+
throw new Error('GitDiffHelper not initialized. Call init() first.');
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Get base and head file contents
|
|
644
|
+
const baseFileStr = await execAsync(
|
|
645
|
+
`git show ${this.baseCommit}:"${filePath}"`,
|
|
646
|
+
{ cwd: this.gitRoot }
|
|
647
|
+
).then(r => r.stdout).catch(() => '');
|
|
648
|
+
|
|
649
|
+
const headFileStr = await fs.readFile(
|
|
650
|
+
path.join(this.gitRoot, filePath), 'utf8'
|
|
651
|
+
).catch(() => '');
|
|
652
|
+
|
|
653
|
+
// Get patch
|
|
654
|
+
const patchStr = await this.getFileDiff(filePath);
|
|
655
|
+
|
|
656
|
+
// Get file status
|
|
657
|
+
const { stdout: nameStatus } = await execAsync(
|
|
658
|
+
`git diff --name-status ${this.baseCommit} -- "${filePath}"`,
|
|
659
|
+
{ cwd: this.gitRoot }
|
|
660
|
+
).catch(() => ({ stdout: '' }));
|
|
661
|
+
|
|
662
|
+
let editTypeStr = 'MODIFIED';
|
|
663
|
+
let oldFilenameStr = null;
|
|
664
|
+
let filenameStr = filePath;
|
|
665
|
+
|
|
666
|
+
if (nameStatus.trim()) {
|
|
667
|
+
const [statusCode, oldName, newName] = nameStatus.trim().split(/\t+/);
|
|
668
|
+
if (statusCode.startsWith('A')) editTypeStr = 'ADDED';
|
|
669
|
+
else if (statusCode.startsWith('D')) editTypeStr = 'DELETED';
|
|
670
|
+
else if (statusCode.startsWith('R')) {
|
|
671
|
+
editTypeStr = 'RENAMED';
|
|
672
|
+
oldFilenameStr = oldName;
|
|
673
|
+
filenameStr = newName;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Get line counts
|
|
678
|
+
let numPlusLinesStr = '0', numMinusLinesStr = '0';
|
|
679
|
+
try {
|
|
680
|
+
const { stdout: ns } = await execAsync(
|
|
681
|
+
`git diff --numstat ${this.baseCommit} -- "${filePath}"`,
|
|
682
|
+
{ cwd: this.gitRoot }
|
|
683
|
+
);
|
|
684
|
+
if (ns.trim()) [numPlusLinesStr, numMinusLinesStr] = ns.split('\t');
|
|
685
|
+
} catch (_) { /* ignore */ }
|
|
686
|
+
|
|
687
|
+
const tokensStr = headFileStr ? String(headFileStr.split(/\s+/).length) : '0';
|
|
688
|
+
|
|
689
|
+
// Parse hunks
|
|
690
|
+
const hunkHeader = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/gm;
|
|
691
|
+
const results = [];
|
|
692
|
+
let headerMatch;
|
|
693
|
+
|
|
694
|
+
while ((headerMatch = hunkHeader.exec(patchStr)) !== null) {
|
|
695
|
+
const newStart = Number(headerMatch[1]);
|
|
696
|
+
const newCount = headerMatch[2] ? Number(headerMatch[2]) : 1;
|
|
697
|
+
const hunkStartIdx = headerMatch.index;
|
|
698
|
+
|
|
699
|
+
const nextHeaderIdx = patchStr.slice(hunkHeader.lastIndex).search(/^@@/m);
|
|
700
|
+
const hunkEndIdx = nextHeaderIdx === -1
|
|
701
|
+
? patchStr.length
|
|
702
|
+
: hunkHeader.lastIndex + nextHeaderIdx;
|
|
703
|
+
const singleHunkPatch = patchStr.slice(hunkStartIdx, hunkEndIdx);
|
|
704
|
+
|
|
705
|
+
results.push({
|
|
706
|
+
base_file_str: baseFileStr,
|
|
707
|
+
head_file_str: headFileStr,
|
|
708
|
+
patch_str: singleHunkPatch,
|
|
709
|
+
filename_str: filenameStr,
|
|
710
|
+
edit_type_str: editTypeStr,
|
|
711
|
+
old_filename_str: oldFilenameStr,
|
|
712
|
+
num_plus_lines_str: numPlusLinesStr,
|
|
713
|
+
num_minus_lines_str: numMinusLinesStr,
|
|
714
|
+
tokens_str: tokensStr,
|
|
715
|
+
start_line_str: String(newStart),
|
|
716
|
+
end_line_str: String(newStart + newCount - 1),
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// If no hunks, return single item
|
|
721
|
+
if (results.length === 0) {
|
|
722
|
+
results.push({
|
|
723
|
+
base_file_str: baseFileStr,
|
|
724
|
+
head_file_str: headFileStr,
|
|
725
|
+
patch_str: patchStr,
|
|
726
|
+
filename_str: filenameStr,
|
|
727
|
+
edit_type_str: editTypeStr,
|
|
728
|
+
old_filename_str: oldFilenameStr,
|
|
729
|
+
num_plus_lines_str: numPlusLinesStr,
|
|
730
|
+
num_minus_lines_str: numMinusLinesStr,
|
|
731
|
+
tokens_str: tokensStr,
|
|
732
|
+
start_line_str: '',
|
|
733
|
+
end_line_str: '',
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return results;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
getLocalBranch() {
|
|
741
|
+
if (!this.currentBranch) {
|
|
742
|
+
throw new Error('GitDiffHelper not initialized. Call init() first.');
|
|
743
|
+
}
|
|
744
|
+
return this.currentBranch;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
getGitRoot() {
|
|
748
|
+
return this.gitRoot;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async getAllDiffInfo() {
|
|
752
|
+
const changedFiles = await this.getChangedFiles();
|
|
753
|
+
|
|
754
|
+
const perFileHunks = await Promise.all(
|
|
755
|
+
changedFiles.map(fp => this.getDiffInfoForFile(fp))
|
|
756
|
+
);
|
|
757
|
+
|
|
758
|
+
return perFileHunks.flat();
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
export default GitDiffHelper;
|