codeant-cli 0.5.10 → 0.5.12
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/commands/login.js +20 -6
- package/src/index.js +8 -4
- package/src/mcp/server.js +2 -2
- package/src/utils/gitDiffHelper.js +69 -86
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/commands/login.js
CHANGED
|
@@ -12,6 +12,7 @@ export default function Login() {
|
|
|
12
12
|
const [status, setStatus] = useState('opening');
|
|
13
13
|
const [error, setError] = useState(null);
|
|
14
14
|
const [loginUrl, setLoginUrl] = useState('');
|
|
15
|
+
const [pollResults, setPollResults] = useState([]);
|
|
15
16
|
|
|
16
17
|
useEffect(() => {
|
|
17
18
|
// Check if already logged in
|
|
@@ -25,9 +26,11 @@ export default function Login() {
|
|
|
25
26
|
const token = `cli___${randomUUID()}`;
|
|
26
27
|
const baseUrl = getBaseUrl();
|
|
27
28
|
const pollUrl = `${baseUrl}/extension/login/status?apiKey=${token}`;
|
|
29
|
+
const displayPollUrl = `${baseUrl}/extension/login/status?apiKey=[redacted]`;
|
|
28
30
|
|
|
29
31
|
let intervalId;
|
|
30
32
|
let timeoutId;
|
|
33
|
+
let pollCount = 0;
|
|
31
34
|
|
|
32
35
|
(async () => {
|
|
33
36
|
let dashboardUrl;
|
|
@@ -54,11 +57,14 @@ export default function Login() {
|
|
|
54
57
|
|
|
55
58
|
// Poll for login status
|
|
56
59
|
intervalId = setInterval(async () => {
|
|
60
|
+
const attempt = ++pollCount;
|
|
61
|
+
let response;
|
|
57
62
|
try {
|
|
58
|
-
|
|
63
|
+
response = await fetch(pollUrl);
|
|
59
64
|
const data = await response.json();
|
|
65
|
+
setPollResults((results) => [...results, `Check ${attempt}: GET ${displayPollUrl} → HTTP ${response.status}, status: ${data?.status ?? 'missing'}`]);
|
|
60
66
|
|
|
61
|
-
if (data
|
|
67
|
+
if (data?.status === 'yes') {
|
|
62
68
|
clearInterval(intervalId);
|
|
63
69
|
clearTimeout(timeoutId);
|
|
64
70
|
|
|
@@ -69,7 +75,8 @@ export default function Login() {
|
|
|
69
75
|
setTimeout(() => exit(), 100);
|
|
70
76
|
}
|
|
71
77
|
} catch (err) {
|
|
72
|
-
|
|
78
|
+
const code = err.cause?.code || err.code || err.name;
|
|
79
|
+
setPollResults((results) => [...results, `Check ${attempt}: GET ${displayPollUrl} → ${response ? `HTTP ${response.status}, ` : ''}error: ${code}`]);
|
|
73
80
|
}
|
|
74
81
|
}, POLL_INTERVAL);
|
|
75
82
|
|
|
@@ -89,6 +96,10 @@ export default function Login() {
|
|
|
89
96
|
};
|
|
90
97
|
}, []);
|
|
91
98
|
|
|
99
|
+
const pollLines = pollResults.map((result, index) =>
|
|
100
|
+
React.createElement(Text, { color: 'gray', key: index }, result)
|
|
101
|
+
);
|
|
102
|
+
|
|
92
103
|
if (status === 'already_logged_in') {
|
|
93
104
|
return React.createElement(
|
|
94
105
|
Box,
|
|
@@ -112,7 +123,8 @@ export default function Login() {
|
|
|
112
123
|
{ flexDirection: 'column', padding: 1 },
|
|
113
124
|
React.createElement(Text, { color: 'cyan' }, `Waiting for login. Open this URL in your browser: ${loginUrl}`),
|
|
114
125
|
React.createElement(Text, { color: 'gray' }, 'Complete the login in your browser.'),
|
|
115
|
-
React.createElement(Text, { color: 'gray' }, 'Checking every 10 seconds. Timeout in 10 minutes.')
|
|
126
|
+
React.createElement(Text, { color: 'gray' }, 'Checking every 10 seconds. Timeout in 10 minutes.'),
|
|
127
|
+
...pollLines
|
|
116
128
|
);
|
|
117
129
|
}
|
|
118
130
|
|
|
@@ -120,7 +132,8 @@ export default function Login() {
|
|
|
120
132
|
return React.createElement(
|
|
121
133
|
Box,
|
|
122
134
|
{ flexDirection: 'column', padding: 1 },
|
|
123
|
-
React.createElement(Text, { color: 'green' }, '✓ Login successful!')
|
|
135
|
+
React.createElement(Text, { color: 'green' }, '✓ Login successful!'),
|
|
136
|
+
...pollLines
|
|
124
137
|
);
|
|
125
138
|
}
|
|
126
139
|
|
|
@@ -128,7 +141,8 @@ export default function Login() {
|
|
|
128
141
|
return React.createElement(
|
|
129
142
|
Box,
|
|
130
143
|
{ flexDirection: 'column', padding: 1 },
|
|
131
|
-
React.createElement(Text, { color: 'red' }, '✗ ', error)
|
|
144
|
+
React.createElement(Text, { color: 'red' }, '✗ ', error),
|
|
145
|
+
...pollLines
|
|
132
146
|
);
|
|
133
147
|
}
|
|
134
148
|
|
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
|
},
|
|
@@ -460,41 +460,50 @@ class GitDiffHelper {
|
|
|
460
460
|
|
|
461
461
|
// Handle untracked files by generating synthetic "new file" diffs since git diff can't see them
|
|
462
462
|
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
|
-
}
|
|
463
|
+
const diff = await this._buildUntrackedFileDiff(file);
|
|
464
|
+
if (diff) diffs.push(diff);
|
|
492
465
|
}
|
|
493
466
|
|
|
494
467
|
this.baseCommit = originalBaseCommit;
|
|
495
468
|
return diffs;
|
|
496
469
|
}
|
|
497
470
|
|
|
471
|
+
/**
|
|
472
|
+
* Build a synthetic "new file" diff for an untracked file, which git diff can't see.
|
|
473
|
+
* Returns null if the file can't be read.
|
|
474
|
+
*/
|
|
475
|
+
async _buildUntrackedFileDiff(file) {
|
|
476
|
+
try {
|
|
477
|
+
const absolutePath = path.resolve(this.gitRoot, file);
|
|
478
|
+
const content = await fs.readFile(absolutePath, 'utf8');
|
|
479
|
+
const lines = content.split('\n');
|
|
480
|
+
|
|
481
|
+
let patchStr = `diff --git a/${file} b/${file}\n`;
|
|
482
|
+
patchStr += `new file mode 100644\n`;
|
|
483
|
+
patchStr += `index 0000000..${this._generateRandomHash()}\n`;
|
|
484
|
+
patchStr += `--- /dev/null\n`;
|
|
485
|
+
patchStr += `+++ b/${file}\n`;
|
|
486
|
+
patchStr += `@@ -0,0 +1,${lines.length} @@\n`;
|
|
487
|
+
lines.forEach(line => { patchStr += `+${line}\n`; });
|
|
488
|
+
|
|
489
|
+
return {
|
|
490
|
+
base_file_str: '',
|
|
491
|
+
head_file_str: content,
|
|
492
|
+
patch_str: patchStr,
|
|
493
|
+
filename_str: file,
|
|
494
|
+
edit_type_str: 'ADDED',
|
|
495
|
+
old_filename_str: null,
|
|
496
|
+
num_plus_lines_str: String(lines.length),
|
|
497
|
+
num_minus_lines_str: '0',
|
|
498
|
+
tokens_str: String(content.split(/\s+/).length),
|
|
499
|
+
start_line_str: '1',
|
|
500
|
+
end_line_str: String(lines.length),
|
|
501
|
+
};
|
|
502
|
+
} catch (error) {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
498
507
|
async _getStagedChanges() {
|
|
499
508
|
const { stdout: stagedFiles } = await execAsync(
|
|
500
509
|
'git diff --name-only --cached HEAD',
|
|
@@ -525,7 +534,8 @@ class GitDiffHelper {
|
|
|
525
534
|
|
|
526
535
|
/**
|
|
527
536
|
* Get diff against a specific base branch.
|
|
528
|
-
* Finds the merge-base between the given branch and HEAD, then diffs
|
|
537
|
+
* Finds the merge-base between the given branch and HEAD, then diffs it against the
|
|
538
|
+
* working tree (committed + staged + unstaged + untracked).
|
|
529
539
|
*/
|
|
530
540
|
async _getBaseBranchDiff(branch) {
|
|
531
541
|
if (!this.gitRoot) {
|
|
@@ -561,32 +571,12 @@ class GitDiffHelper {
|
|
|
561
571
|
base = resolvedRef;
|
|
562
572
|
}
|
|
563
573
|
|
|
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
|
-
}
|
|
574
|
+
return this._getWorkingTreeDiffAgainst(base);
|
|
585
575
|
}
|
|
586
576
|
|
|
587
577
|
/**
|
|
588
578
|
* Get diff against a specific base commit.
|
|
589
|
-
* Diffs from the given commit to
|
|
579
|
+
* Diffs from the given commit to the working tree (committed + staged + unstaged + untracked).
|
|
590
580
|
*/
|
|
591
581
|
async _getBaseCommitDiff(commit) {
|
|
592
582
|
if (!this.gitRoot) {
|
|
@@ -605,23 +595,43 @@ class GitDiffHelper {
|
|
|
605
595
|
throw new Error(`Could not resolve commit: ${commit}`);
|
|
606
596
|
}
|
|
607
597
|
|
|
598
|
+
return this._getWorkingTreeDiffAgainst(resolvedCommit);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Diff a base commit against the working tree: commits since base, staged and unstaged
|
|
603
|
+
* changes (including staged new files), plus untracked files not ignored by .gitignore.
|
|
604
|
+
* Content is read from disk so it matches the patch, which getFileDiff also takes against
|
|
605
|
+
* the working tree.
|
|
606
|
+
*/
|
|
607
|
+
async _getWorkingTreeDiffAgainst(base) {
|
|
608
|
+
// No HEAD argument: compares base to the working tree, which covers tracked files that
|
|
609
|
+
// were committed, staged or edited since base
|
|
608
610
|
const { stdout: changedFiles } = await execAsync(
|
|
609
|
-
`git diff --name-only ${
|
|
611
|
+
`git diff --name-only ${base}`,
|
|
612
|
+
{ cwd: this.gitRoot }
|
|
613
|
+
);
|
|
614
|
+
const { stdout: untrackedStr } = await execAsync(
|
|
615
|
+
'git ls-files --others --exclude-standard',
|
|
610
616
|
{ cwd: this.gitRoot }
|
|
611
617
|
);
|
|
612
618
|
|
|
613
619
|
const files = changedFiles.split('\n').map(f => f.trim()).filter(Boolean);
|
|
614
|
-
|
|
620
|
+
const untrackedFiles = untrackedStr.split('\n').map(f => f.trim()).filter(Boolean);
|
|
621
|
+
if (files.length === 0 && untrackedFiles.length === 0) return [];
|
|
615
622
|
|
|
616
623
|
const originalBaseCommit = this.baseCommit;
|
|
617
624
|
try {
|
|
618
|
-
this.baseCommit =
|
|
619
|
-
this._headRef =
|
|
625
|
+
this.baseCommit = base;
|
|
626
|
+
this._headRef = null;
|
|
620
627
|
|
|
621
628
|
const diffs = await Promise.all(
|
|
622
629
|
files.map(fp => this.getDiffInfoForFile(fp))
|
|
623
630
|
);
|
|
624
|
-
|
|
631
|
+
const untrackedDiffs = await Promise.all(
|
|
632
|
+
untrackedFiles.map(fp => this._buildUntrackedFileDiff(fp))
|
|
633
|
+
);
|
|
634
|
+
return [...diffs.flat(), ...untrackedDiffs.filter(Boolean)];
|
|
625
635
|
} finally {
|
|
626
636
|
this.baseCommit = originalBaseCommit;
|
|
627
637
|
this._headRef = null;
|
|
@@ -762,35 +772,8 @@ class GitDiffHelper {
|
|
|
762
772
|
|
|
763
773
|
// Handle untracked files with synthetic diffs
|
|
764
774
|
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
|
-
}
|
|
775
|
+
const diff = await this._buildUntrackedFileDiff(file);
|
|
776
|
+
if (diff) diffs.push(diff);
|
|
794
777
|
}
|
|
795
778
|
} finally {
|
|
796
779
|
this.baseCommit = originalBaseCommit;
|