codeant-cli 0.5.11 → 0.5.13
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 +54 -7
- package/package.json +1 -1
- package/src/index.js +8 -4
- package/src/mcp/server.js +2 -2
- package/src/utils/gitDiffHelper.js +134 -96
package/README.md
CHANGED
|
@@ -16,8 +16,11 @@ For installation, authentication, and self-hosted setup, see the [CodeAnt CLI se
|
|
|
16
16
|
# Login to CodeAnt
|
|
17
17
|
codeant login
|
|
18
18
|
|
|
19
|
-
# Scan
|
|
19
|
+
# Scan your changes for secrets
|
|
20
20
|
codeant secrets
|
|
21
|
+
|
|
22
|
+
# Review your changes
|
|
23
|
+
codeant review
|
|
21
24
|
```
|
|
22
25
|
|
|
23
26
|
## Usage
|
|
@@ -56,10 +59,14 @@ codeant secrets [options]
|
|
|
56
59
|
|
|
57
60
|
| Option | Description |
|
|
58
61
|
|--------|-------------|
|
|
59
|
-
| `--
|
|
60
|
-
| `--
|
|
61
|
-
| `--uncommitted` | Scan
|
|
62
|
+
| `--all` | Scan committed (unpushed) + uncommitted changes, including untracked files (default) |
|
|
63
|
+
| `--committed` | Scan only unpushed commits |
|
|
64
|
+
| `--uncommitted` | Scan staged + unstaged + untracked changes |
|
|
65
|
+
| `--staged` | Scan only staged files |
|
|
62
66
|
| `--last-commit` | Scan files from the last commit |
|
|
67
|
+
| `--last-n-commits <n>` | Scan files from the last n commits (max 5) |
|
|
68
|
+
| `--base <branch>` | Scan committed + uncommitted + untracked changes since the merge-base with `<branch>` |
|
|
69
|
+
| `--base-commit <commit>` | Scan committed + uncommitted + untracked changes since `<commit>` |
|
|
63
70
|
| `--fail-on <level>` | Fail only on HIGH, MEDIUM, or all (default: HIGH) |
|
|
64
71
|
| `--include <patterns>` | Comma-separated glob patterns to include files |
|
|
65
72
|
| `--exclude <patterns>` | Comma-separated glob patterns to exclude files |
|
|
@@ -67,11 +74,11 @@ codeant secrets [options]
|
|
|
67
74
|
**Examples:**
|
|
68
75
|
|
|
69
76
|
```bash
|
|
70
|
-
# Scan
|
|
77
|
+
# Scan all changed files (default)
|
|
71
78
|
codeant secrets
|
|
72
79
|
|
|
73
|
-
# Scan
|
|
74
|
-
codeant secrets --
|
|
80
|
+
# Scan only staged files
|
|
81
|
+
codeant secrets --staged
|
|
75
82
|
|
|
76
83
|
# Scan last commit
|
|
77
84
|
codeant secrets --last-commit
|
|
@@ -108,6 +115,46 @@ Use `--include` and `--exclude` with glob patterns to filter files:
|
|
|
108
115
|
- `MEDIUM` - Medium confidence, may need review
|
|
109
116
|
- `FALSE_POSITIVE` - Detected but likely not a real secret (always ignored)
|
|
110
117
|
|
|
118
|
+
#### `review`
|
|
119
|
+
|
|
120
|
+
Run an AI code review on your local changes.
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
codeant review [options]
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Options:**
|
|
127
|
+
|
|
128
|
+
| Option | Description |
|
|
129
|
+
|--------|-------------|
|
|
130
|
+
| `--all` | Review committed (unpushed) + uncommitted changes, including untracked files (default) |
|
|
131
|
+
| `--committed` | Review only unpushed commits |
|
|
132
|
+
| `--uncommitted` | Review staged + unstaged + untracked changes |
|
|
133
|
+
| `--staged` | Review only staged files |
|
|
134
|
+
| `--last-commit` | Review the last commit |
|
|
135
|
+
| `--last-n-commits <n>` | Review the last n commits (max 5) |
|
|
136
|
+
| `--base <branch>` | Review committed + uncommitted + untracked changes since the merge-base with `<branch>` |
|
|
137
|
+
| `--base-commit <commit>` | Review committed + uncommitted + untracked changes since `<commit>` |
|
|
138
|
+
| `--fail-on <level>` | Fail on issues at or above BLOCKER, CRITICAL, MAJOR, MINOR, or INFO (default: CRITICAL) |
|
|
139
|
+
| `--include <patterns>` | Comma-separated glob patterns to include files |
|
|
140
|
+
| `--exclude <patterns>` | Comma-separated glob patterns to exclude files |
|
|
141
|
+
| `--headless` | Print JSON with no spinners (for agents and CI) |
|
|
142
|
+
|
|
143
|
+
`--base` and `--base-commit` take precedence over the other scope flags. They compare the base against your working tree, so staged, unstaged, and untracked files are reviewed along with commits. Files ignored by `.gitignore` are never reviewed. The same applies to `secrets`.
|
|
144
|
+
|
|
145
|
+
**Examples:**
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# Review everything on your branch since it left main, including uncommitted and new files
|
|
149
|
+
codeant review --base main
|
|
150
|
+
|
|
151
|
+
# Review only uncommitted work
|
|
152
|
+
codeant review --uncommitted
|
|
153
|
+
|
|
154
|
+
# CI: fail on MAJOR or worse, JSON output
|
|
155
|
+
codeant review --base main --fail-on MAJOR --headless
|
|
156
|
+
```
|
|
157
|
+
|
|
111
158
|
#### `set-base-url <url>`
|
|
112
159
|
|
|
113
160
|
Set a custom API base URL.
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -72,8 +72,8 @@ program
|
|
|
72
72
|
.option('--staged', 'Scan only staged files')
|
|
73
73
|
.option('--last-commit', 'Scan last commit')
|
|
74
74
|
.option('--last-n-commits <n>', 'Scan last n commits (max 5)', toCount)
|
|
75
|
-
.option('--base <branch>', '
|
|
76
|
-
.option('--base-commit <commit>', '
|
|
75
|
+
.option('--base <branch>', 'Scan committed + uncommitted + untracked changes since the merge-base with a branch (e.g. --base develop). Takes precedence over the other scope flags')
|
|
76
|
+
.option('--base-commit <commit>', 'Scan committed + uncommitted + untracked changes since a commit (e.g. --base-commit HEAD~3). Takes precedence over the other scope flags')
|
|
77
77
|
.option('--include <paths>', 'Comma-separated list of file paths glob patterns to include')
|
|
78
78
|
.option('--exclude <paths>', 'Comma-separated list of file paths glob patterns to exclude')
|
|
79
79
|
.option('--hook', 'Running from pre-commit hook (enables bypass prompt)')
|
|
@@ -83,6 +83,8 @@ program
|
|
|
83
83
|
let baseBranch = null;
|
|
84
84
|
let baseCommit = null;
|
|
85
85
|
|
|
86
|
+
// Precedence: --base > --base-commit > other scope flags. The base modes already diff
|
|
87
|
+
// against the working tree, so they include uncommitted and untracked changes too.
|
|
86
88
|
if (options.base) {
|
|
87
89
|
scanType = 'base-branch';
|
|
88
90
|
baseBranch = options.base;
|
|
@@ -123,8 +125,8 @@ program
|
|
|
123
125
|
.option('--staged', 'Review only staged files')
|
|
124
126
|
.option('--last-commit', 'Review last commit')
|
|
125
127
|
.option('--last-n-commits <n>', 'Review last n commits (max 5)', toCount)
|
|
126
|
-
.option('--base <branch>', '
|
|
127
|
-
.option('--base-commit <commit>', '
|
|
128
|
+
.option('--base <branch>', 'Review committed + uncommitted + untracked changes since the merge-base with a branch (e.g. --base develop). Takes precedence over the other scope flags')
|
|
129
|
+
.option('--base-commit <commit>', 'Review committed + uncommitted + untracked changes since a commit (e.g. --base-commit HEAD~3). Takes precedence over the other scope flags')
|
|
128
130
|
.option('--fail-on <level>', 'Fail on issues at or above this level: BLOCKER, CRITICAL, MAJOR, MINOR, INFO (default: CRITICAL)', 'CRITICAL')
|
|
129
131
|
.option('--include <paths>', 'Comma-separated list of file paths glob patterns to include')
|
|
130
132
|
.option('--exclude <paths>', 'Comma-separated list of file paths glob patterns to exclude')
|
|
@@ -135,6 +137,8 @@ program
|
|
|
135
137
|
let baseBranch = null;
|
|
136
138
|
let baseCommit = null;
|
|
137
139
|
|
|
140
|
+
// Precedence: --base > --base-commit > other scope flags. The base modes already diff
|
|
141
|
+
// against the working tree, so they include uncommitted and untracked changes too.
|
|
138
142
|
if (options.base) {
|
|
139
143
|
scanType = 'base-branch';
|
|
140
144
|
baseBranch = options.base;
|
package/src/mcp/server.js
CHANGED
|
@@ -639,8 +639,8 @@ export function createMcpServer() {
|
|
|
639
639
|
.optional()
|
|
640
640
|
.describe('Review scope. Default "uncommitted".'),
|
|
641
641
|
lastNCommits: z.number().int().positive().max(5).optional(),
|
|
642
|
-
baseBranch: z.string().optional(),
|
|
643
|
-
baseCommit: z.string().optional(),
|
|
642
|
+
baseBranch: z.string().optional().describe('Branch for scope "base-branch". Reviews committed, uncommitted and untracked changes since the merge-base with this branch.'),
|
|
643
|
+
baseCommit: z.string().optional().describe('Commit for scope "base-commit". Reviews committed, uncommitted and untracked changes since this commit.'),
|
|
644
644
|
include: z.array(z.string()).optional().describe('Glob patterns to include.'),
|
|
645
645
|
exclude: z.array(z.string()).optional().describe('Glob patterns to exclude.'),
|
|
646
646
|
},
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
|
+
import { constants as fsConstants } from 'fs';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import { promisify } from 'util';
|
|
4
|
-
import { exec } from 'child_process';
|
|
5
|
+
import { exec, execFile } from 'child_process';
|
|
5
6
|
|
|
6
7
|
const execAsync = promisify(exec);
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Expand untracked paths from `git status --porcelain`.
|
|
@@ -460,41 +462,90 @@ class GitDiffHelper {
|
|
|
460
462
|
|
|
461
463
|
// Handle untracked files by generating synthetic "new file" diffs since git diff can't see them
|
|
462
464
|
for (const file of expandedUntrackedFiles) {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
const content = await fs.readFile(absolutePath, 'utf8');
|
|
466
|
-
const lines = content.split('\n');
|
|
467
|
-
|
|
468
|
-
let patchStr = `diff --git a/${file} b/${file}\n`;
|
|
469
|
-
patchStr += `new file mode 100644\n`;
|
|
470
|
-
patchStr += `index 0000000..${this._generateRandomHash()}\n`;
|
|
471
|
-
patchStr += `--- /dev/null\n`;
|
|
472
|
-
patchStr += `+++ b/${file}\n`;
|
|
473
|
-
patchStr += `@@ -0,0 +1,${lines.length} @@\n`;
|
|
474
|
-
lines.forEach(line => { patchStr += `+${line}\n`; });
|
|
475
|
-
|
|
476
|
-
diffs.push({
|
|
477
|
-
base_file_str: '',
|
|
478
|
-
head_file_str: content,
|
|
479
|
-
patch_str: patchStr,
|
|
480
|
-
filename_str: file,
|
|
481
|
-
edit_type_str: 'ADDED',
|
|
482
|
-
old_filename_str: null,
|
|
483
|
-
num_plus_lines_str: String(lines.length),
|
|
484
|
-
num_minus_lines_str: '0',
|
|
485
|
-
tokens_str: String(content.split(/\s+/).length),
|
|
486
|
-
start_line_str: '1',
|
|
487
|
-
end_line_str: String(lines.length),
|
|
488
|
-
});
|
|
489
|
-
} catch (error) {
|
|
490
|
-
// Skip files that can't be read
|
|
491
|
-
}
|
|
465
|
+
const diff = await this._buildUntrackedFileDiff(file);
|
|
466
|
+
if (diff) diffs.push(diff);
|
|
492
467
|
}
|
|
493
468
|
|
|
494
469
|
this.baseCommit = originalBaseCommit;
|
|
495
470
|
return diffs;
|
|
496
471
|
}
|
|
497
472
|
|
|
473
|
+
/**
|
|
474
|
+
* Read a working-tree file. Returns null if it resolves outside the repository (a symlink
|
|
475
|
+
* pointing elsewhere) or isn't a regular file, so those contents are never uploaded.
|
|
476
|
+
*/
|
|
477
|
+
async _readRepoFile(relPath) {
|
|
478
|
+
const realRoot = await fs.realpath(this.gitRoot);
|
|
479
|
+
const isInside = p => {
|
|
480
|
+
const rel = path.relative(realRoot, p);
|
|
481
|
+
return !!rel && rel !== '..' && !rel.startsWith('..' + path.sep) && !path.isAbsolute(rel);
|
|
482
|
+
};
|
|
483
|
+
const realPath = await fs.realpath(path.resolve(this.gitRoot, relPath));
|
|
484
|
+
if (!isInside(realPath)) return null;
|
|
485
|
+
|
|
486
|
+
// Open first and then check what was actually opened, so a symlink swapped in after the
|
|
487
|
+
// check above can't redirect the read. O_NONBLOCK keeps a FIFO from blocking the open.
|
|
488
|
+
const { O_RDONLY, O_NOFOLLOW = 0, O_NONBLOCK = 0 } = fsConstants;
|
|
489
|
+
const handle = await fs.open(realPath, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
490
|
+
try {
|
|
491
|
+
const opened = await handle.stat();
|
|
492
|
+
if (!opened.isFile()) return null;
|
|
493
|
+
// The opened file must still be the file at this path, and the path must still resolve
|
|
494
|
+
// inside the repo (a parent directory may have been swapped for a symlink)
|
|
495
|
+
const nowPath = await fs.realpath(realPath);
|
|
496
|
+
const now = await fs.stat(nowPath);
|
|
497
|
+
if (!isInside(nowPath) || now.dev !== opened.dev || now.ino !== opened.ino) return null;
|
|
498
|
+
return await handle.readFile('utf8');
|
|
499
|
+
} finally {
|
|
500
|
+
await handle.close();
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Build a synthetic "new file" diff for an untracked file, which git diff can't see.
|
|
506
|
+
* Returns null if the file can't be read.
|
|
507
|
+
*/
|
|
508
|
+
async _buildUntrackedFileDiff(file) {
|
|
509
|
+
try {
|
|
510
|
+
let content = await this._readRepoFile(file).catch(() => null);
|
|
511
|
+
let mode = '100644';
|
|
512
|
+
if (content === null) {
|
|
513
|
+
// A symlink we won't follow (it points outside the repo or nowhere): show the link
|
|
514
|
+
// itself, as git would once it's committed, using its target path and never the
|
|
515
|
+
// target's contents
|
|
516
|
+
const absolutePath = path.resolve(this.gitRoot, file);
|
|
517
|
+
if (!(await fs.lstat(absolutePath)).isSymbolicLink()) return null;
|
|
518
|
+
content = await fs.readlink(absolutePath);
|
|
519
|
+
mode = '120000';
|
|
520
|
+
}
|
|
521
|
+
const lines = content.split('\n');
|
|
522
|
+
|
|
523
|
+
let patchStr = `diff --git a/${file} b/${file}\n`;
|
|
524
|
+
patchStr += `new file mode ${mode}\n`;
|
|
525
|
+
patchStr += `index 0000000..${this._generateRandomHash()}\n`;
|
|
526
|
+
patchStr += `--- /dev/null\n`;
|
|
527
|
+
patchStr += `+++ b/${file}\n`;
|
|
528
|
+
patchStr += `@@ -0,0 +1,${lines.length} @@\n`;
|
|
529
|
+
lines.forEach(line => { patchStr += `+${line}\n`; });
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
base_file_str: '',
|
|
533
|
+
head_file_str: content,
|
|
534
|
+
patch_str: patchStr,
|
|
535
|
+
filename_str: file,
|
|
536
|
+
edit_type_str: 'ADDED',
|
|
537
|
+
old_filename_str: null,
|
|
538
|
+
num_plus_lines_str: String(lines.length),
|
|
539
|
+
num_minus_lines_str: '0',
|
|
540
|
+
tokens_str: String(content.split(/\s+/).length),
|
|
541
|
+
start_line_str: '1',
|
|
542
|
+
end_line_str: String(lines.length),
|
|
543
|
+
};
|
|
544
|
+
} catch (error) {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
498
549
|
async _getStagedChanges() {
|
|
499
550
|
const { stdout: stagedFiles } = await execAsync(
|
|
500
551
|
'git diff --name-only --cached HEAD',
|
|
@@ -525,7 +576,8 @@ class GitDiffHelper {
|
|
|
525
576
|
|
|
526
577
|
/**
|
|
527
578
|
* Get diff against a specific base branch.
|
|
528
|
-
* Finds the merge-base between the given branch and HEAD, then diffs
|
|
579
|
+
* Finds the merge-base between the given branch and HEAD, then diffs it against the
|
|
580
|
+
* working tree (committed + staged + unstaged + untracked).
|
|
529
581
|
*/
|
|
530
582
|
async _getBaseBranchDiff(branch) {
|
|
531
583
|
if (!this.gitRoot) {
|
|
@@ -539,8 +591,7 @@ class GitDiffHelper {
|
|
|
539
591
|
let resolvedRef;
|
|
540
592
|
for (const candidate of [branch, `origin/${branch}`]) {
|
|
541
593
|
try {
|
|
542
|
-
|
|
543
|
-
resolvedRef = stdout.trim();
|
|
594
|
+
resolvedRef = await this._resolveCommitSha(candidate);
|
|
544
595
|
break;
|
|
545
596
|
} catch (err) {
|
|
546
597
|
// try next
|
|
@@ -561,32 +612,12 @@ class GitDiffHelper {
|
|
|
561
612
|
base = resolvedRef;
|
|
562
613
|
}
|
|
563
614
|
|
|
564
|
-
|
|
565
|
-
`git diff --name-only ${base} HEAD`,
|
|
566
|
-
{ cwd: this.gitRoot }
|
|
567
|
-
);
|
|
568
|
-
|
|
569
|
-
const files = changedFiles.split('\n').map(f => f.trim()).filter(Boolean);
|
|
570
|
-
if (files.length === 0) return [];
|
|
571
|
-
|
|
572
|
-
const originalBaseCommit = this.baseCommit;
|
|
573
|
-
try {
|
|
574
|
-
this.baseCommit = base;
|
|
575
|
-
this._headRef = 'HEAD';
|
|
576
|
-
|
|
577
|
-
const diffs = await Promise.all(
|
|
578
|
-
files.map(fp => this.getDiffInfoForFile(fp))
|
|
579
|
-
);
|
|
580
|
-
return diffs.flat();
|
|
581
|
-
} finally {
|
|
582
|
-
this.baseCommit = originalBaseCommit;
|
|
583
|
-
this._headRef = null;
|
|
584
|
-
}
|
|
615
|
+
return this._getWorkingTreeDiffAgainst(base);
|
|
585
616
|
}
|
|
586
617
|
|
|
587
618
|
/**
|
|
588
619
|
* Get diff against a specific base commit.
|
|
589
|
-
* Diffs from the given commit to
|
|
620
|
+
* Diffs from the given commit to the working tree (committed + staged + unstaged + untracked).
|
|
590
621
|
*/
|
|
591
622
|
async _getBaseCommitDiff(commit) {
|
|
592
623
|
if (!this.gitRoot) {
|
|
@@ -599,29 +630,63 @@ class GitDiffHelper {
|
|
|
599
630
|
// Resolve the commit ref (supports HEAD~3, sha, tag, etc.)
|
|
600
631
|
let resolvedCommit;
|
|
601
632
|
try {
|
|
602
|
-
|
|
603
|
-
resolvedCommit = stdout.trim();
|
|
633
|
+
resolvedCommit = await this._resolveCommitSha(commit);
|
|
604
634
|
} catch (err) {
|
|
605
635
|
throw new Error(`Could not resolve commit: ${commit}`);
|
|
606
636
|
}
|
|
607
637
|
|
|
638
|
+
return this._getWorkingTreeDiffAgainst(resolvedCommit);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Resolve a user-supplied ref (from the CLI or MCP) to a commit SHA without going through a
|
|
643
|
+
* shell, so later git commands only ever see the SHA. Throws if it doesn't name a commit.
|
|
644
|
+
*/
|
|
645
|
+
async _resolveCommitSha(ref) {
|
|
646
|
+
// A leading "-" would be parsed as a git option (e.g. --output=<file>)
|
|
647
|
+
if (ref.startsWith('-')) throw new Error(`Invalid ref: ${ref}`);
|
|
648
|
+
const { stdout } = await execFileAsync(
|
|
649
|
+
'git',
|
|
650
|
+
['rev-parse', '--verify', '--quiet', `${ref}^{commit}`],
|
|
651
|
+
{ cwd: this.gitRoot }
|
|
652
|
+
);
|
|
653
|
+
return stdout.trim();
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Diff a base commit against the working tree: commits since base, staged and unstaged
|
|
658
|
+
* changes (including staged new files), plus untracked files not ignored by .gitignore.
|
|
659
|
+
* Content is read from disk so it matches the patch, which getFileDiff also takes against
|
|
660
|
+
* the working tree.
|
|
661
|
+
*/
|
|
662
|
+
async _getWorkingTreeDiffAgainst(base) {
|
|
663
|
+
// No HEAD argument: compares base to the working tree, which covers tracked files that
|
|
664
|
+
// were committed, staged or edited since base
|
|
608
665
|
const { stdout: changedFiles } = await execAsync(
|
|
609
|
-
`git diff --name-only ${
|
|
666
|
+
`git diff --name-only ${base}`,
|
|
667
|
+
{ cwd: this.gitRoot }
|
|
668
|
+
);
|
|
669
|
+
const { stdout: untrackedStr } = await execAsync(
|
|
670
|
+
'git ls-files --others --exclude-standard',
|
|
610
671
|
{ cwd: this.gitRoot }
|
|
611
672
|
);
|
|
612
673
|
|
|
613
674
|
const files = changedFiles.split('\n').map(f => f.trim()).filter(Boolean);
|
|
614
|
-
|
|
675
|
+
const untrackedFiles = untrackedStr.split('\n').map(f => f.trim()).filter(Boolean);
|
|
676
|
+
if (files.length === 0 && untrackedFiles.length === 0) return [];
|
|
615
677
|
|
|
616
678
|
const originalBaseCommit = this.baseCommit;
|
|
617
679
|
try {
|
|
618
|
-
this.baseCommit =
|
|
619
|
-
this._headRef =
|
|
680
|
+
this.baseCommit = base;
|
|
681
|
+
this._headRef = null;
|
|
620
682
|
|
|
621
683
|
const diffs = await Promise.all(
|
|
622
684
|
files.map(fp => this.getDiffInfoForFile(fp))
|
|
623
685
|
);
|
|
624
|
-
|
|
686
|
+
const untrackedDiffs = await Promise.all(
|
|
687
|
+
untrackedFiles.map(fp => this._buildUntrackedFileDiff(fp))
|
|
688
|
+
);
|
|
689
|
+
return [...diffs.flat(), ...untrackedDiffs.filter(Boolean)];
|
|
625
690
|
} finally {
|
|
626
691
|
this.baseCommit = originalBaseCommit;
|
|
627
692
|
this._headRef = null;
|
|
@@ -762,35 +827,8 @@ class GitDiffHelper {
|
|
|
762
827
|
|
|
763
828
|
// Handle untracked files with synthetic diffs
|
|
764
829
|
for (const file of expandedUntrackedFiles) {
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
const content = await fs.readFile(absolutePath, 'utf8');
|
|
768
|
-
const lines = content.split('\n');
|
|
769
|
-
|
|
770
|
-
let patchStr = `diff --git a/${file} b/${file}\n`;
|
|
771
|
-
patchStr += `new file mode 100644\n`;
|
|
772
|
-
patchStr += `index 0000000..${this._generateRandomHash()}\n`;
|
|
773
|
-
patchStr += `--- /dev/null\n`;
|
|
774
|
-
patchStr += `+++ b/${file}\n`;
|
|
775
|
-
patchStr += `@@ -0,0 +1,${lines.length} @@\n`;
|
|
776
|
-
lines.forEach(line => { patchStr += `+${line}\n`; });
|
|
777
|
-
|
|
778
|
-
diffs.push({
|
|
779
|
-
base_file_str: '',
|
|
780
|
-
head_file_str: content,
|
|
781
|
-
patch_str: patchStr,
|
|
782
|
-
filename_str: file,
|
|
783
|
-
edit_type_str: 'ADDED',
|
|
784
|
-
old_filename_str: null,
|
|
785
|
-
num_plus_lines_str: String(lines.length),
|
|
786
|
-
num_minus_lines_str: '0',
|
|
787
|
-
tokens_str: String(content.split(/\s+/).length),
|
|
788
|
-
start_line_str: '1',
|
|
789
|
-
end_line_str: String(lines.length),
|
|
790
|
-
});
|
|
791
|
-
} catch (err) {
|
|
792
|
-
// skip
|
|
793
|
-
}
|
|
830
|
+
const diff = await this._buildUntrackedFileDiff(file);
|
|
831
|
+
if (diff) diffs.push(diff);
|
|
794
832
|
}
|
|
795
833
|
} finally {
|
|
796
834
|
this.baseCommit = originalBaseCommit;
|
|
@@ -1028,8 +1066,8 @@ class GitDiffHelper {
|
|
|
1028
1066
|
} catch (err) {
|
|
1029
1067
|
// Handle untracked files
|
|
1030
1068
|
try {
|
|
1031
|
-
const
|
|
1032
|
-
|
|
1069
|
+
const headFileStr = await this._readRepoFile(filePath);
|
|
1070
|
+
if (headFileStr === null) throw new Error('Not a regular file inside the repository');
|
|
1033
1071
|
const lines = headFileStr.split('\n');
|
|
1034
1072
|
|
|
1035
1073
|
let patchStr = `diff --git a/${filePath} b/${filePath}\n`;
|
|
@@ -1086,9 +1124,9 @@ class GitDiffHelper {
|
|
|
1086
1124
|
{ cwd: this.gitRoot }
|
|
1087
1125
|
).then(r => r.stdout).catch(() => '');
|
|
1088
1126
|
} else {
|
|
1089
|
-
headFileStr = await
|
|
1090
|
-
|
|
1091
|
-
|
|
1127
|
+
headFileStr = await this._readRepoFile(filePath)
|
|
1128
|
+
.then(content => content ?? '')
|
|
1129
|
+
.catch(() => '');
|
|
1092
1130
|
}
|
|
1093
1131
|
|
|
1094
1132
|
// Get patch
|