Skip to content

Commit 8d1eb94

Browse files
Merge pull request #50 from CodeAnt-AI/pranav/cod-1557-computrition-cli-review-base-base-commit-silently-override
Include uncommitted and untracked files in --base/--base-commit (0.5.12)
2 parents d7e7fdc + aea4bfa commit 8d1eb94

8 files changed

Lines changed: 258 additions & 106 deletions

File tree

‎README.md‎

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@ For installation, authentication, and self-hosted setup, see the [CodeAnt CLI se
1616
# Login to CodeAnt
1717
codeant login
1818

19-
# Scan staged files for secrets
19+
# Scan your changes for secrets
2020
codeant secrets
21+
22+
# Review your changes
23+
codeant review
2124
```
2225

2326
## Usage
@@ -56,22 +59,26 @@ codeant secrets [options]
5659

5760
| Option | Description |
5861
|--------|-------------|
59-
| `--staged` | Scan only staged files (default) |
60-
| `--all` | Scan all changed files compared to base branch |
61-
| `--uncommitted` | Scan all uncommitted changes |
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 |
6266
| `--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>` |
6370
| `--fail-on <level>` | Fail only on HIGH, MEDIUM, or all (default: HIGH) |
6471
| `--include <patterns>` | Comma-separated glob patterns to include files |
6572
| `--exclude <patterns>` | Comma-separated glob patterns to exclude files |
6673

6774
**Examples:**
6875

6976
```bash
70-
# Scan staged files (default)
77+
# Scan all changed files (default)
7178
codeant secrets
7279

73-
# Scan all changed files
74-
codeant secrets --all
80+
# Scan only staged files
81+
codeant secrets --staged
7582

7683
# Scan last commit
7784
codeant secrets --last-commit
@@ -108,6 +115,46 @@ Use `--include` and `--exclude` with glob patterns to filter files:
108115
- `MEDIUM` - Medium confidence, may need review
109116
- `FALSE_POSITIVE` - Detected but likely not a real secret (always ignored)
110117

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+
111158
#### `set-base-url <url>`
112159

113160
Set a custom API base URL.

‎changelog.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## [0.5.12] - 13/10/2026
4+
- `review` / `secrets` with `--base` or `--base-commit` now include staged, unstaged, and untracked files (respecting `.gitignore`), not just commits. Previously `review --all --base main` silently skipped new files
5+
- With `--base` / `--base-commit`, file content is read from the working tree so it matches the reviewed diff
6+
- Help text, the README, and the MCP `codeant_review_local` parameters describe what `--base` / `--base-commit` include and that they take precedence over the other scope flags; the README gains a `review` section and corrects the `secrets` scope options
7+
38
## [0.5.11] - 05/10/2026
49
- `codeant login` shows every status check with the HTTP response, login status, or request error, while masking the token in request diagnostics
510

‎package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codeant-cli",
3-
"version": "0.5.11",
3+
"version": "0.5.12",
44
"description": "Code review CLI tool",
55
"type": "module",
66
"repository": {

‎src/index.js‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ program
7272
.option('--staged', 'Scan only staged files')
7373
.option('--last-commit', 'Scan last commit')
7474
.option('--last-n-commits <n>', 'Scan last n commits (max 5)', toCount)
75-
.option('--base <branch>', 'Compare against a specific base branch (e.g. --base develop)')
76-
.option('--base-commit <commit>', 'Compare against a specific commit (e.g. --base-commit HEAD~3)')
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')
7777
.option('--include <paths>', 'Comma-separated list of file paths glob patterns to include')
7878
.option('--exclude <paths>', 'Comma-separated list of file paths glob patterns to exclude')
7979
.option('--hook', 'Running from pre-commit hook (enables bypass prompt)')
@@ -83,6 +83,8 @@ program
8383
let baseBranch = null;
8484
let baseCommit = null;
8585

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.
8688
if (options.base) {
8789
scanType = 'base-branch';
8890
baseBranch = options.base;
@@ -123,8 +125,8 @@ program
123125
.option('--staged', 'Review only staged files')
124126
.option('--last-commit', 'Review last commit')
125127
.option('--last-n-commits <n>', 'Review last n commits (max 5)', toCount)
126-
.option('--base <branch>', 'Compare against a specific base branch (e.g. --base develop)')
127-
.option('--base-commit <commit>', 'Compare against a specific commit (e.g. --base-commit HEAD~3)')
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')
128130
.option('--fail-on <level>', 'Fail on issues at or above this level: BLOCKER, CRITICAL, MAJOR, MINOR, INFO (default: CRITICAL)', 'CRITICAL')
129131
.option('--include <paths>', 'Comma-separated list of file paths glob patterns to include')
130132
.option('--exclude <paths>', 'Comma-separated list of file paths glob patterns to exclude')
@@ -135,6 +137,8 @@ program
135137
let baseBranch = null;
136138
let baseCommit = null;
137139

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.
138142
if (options.base) {
139143
scanType = 'base-branch';
140144
baseBranch = options.base;

‎src/mcp/server.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -639,8 +639,8 @@ export function createMcpServer() {
639639
.optional()
640640
.describe('Review scope. Default "uncommitted".'),
641641
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.'),
644644
include: z.array(z.string()).optional().describe('Glob patterns to include.'),
645645
exclude: z.array(z.string()).optional().describe('Glob patterns to exclude.'),
646646
},

‎src/utils/gitDiffHelper.js‎

Lines changed: 69 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -460,41 +460,50 @@ class GitDiffHelper {
460460

461461
// Handle untracked files by generating synthetic "new file" diffs since git diff can't see them
462462
for (const file of expandedUntrackedFiles) {
463-
try {
464-
const absolutePath = path.resolve(this.gitRoot, file);
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);
492465
}
493466

494467
this.baseCommit = originalBaseCommit;
495468
return diffs;
496469
}
497470

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+
498507
async _getStagedChanges() {
499508
const { stdout: stagedFiles } = await execAsync(
500509
'git diff --name-only --cached HEAD',
@@ -525,7 +534,8 @@ class GitDiffHelper {
525534

526535
/**
527536
* Get diff against a specific base branch.
528-
* Finds the merge-base between the given branch and HEAD, then diffs from there.
537+
* Finds the merge-base between the given branch and HEAD, then diffs it against the
538+
* working tree (committed + staged + unstaged + untracked).
529539
*/
530540
async _getBaseBranchDiff(branch) {
531541
if (!this.gitRoot) {
@@ -561,32 +571,12 @@ class GitDiffHelper {
561571
base = resolvedRef;
562572
}
563573

564-
const { stdout: changedFiles } = await execAsync(
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);
585575
}
586576

587577
/**
588578
* Get diff against a specific base commit.
589-
* Diffs from the given commit to HEAD.
579+
* Diffs from the given commit to the working tree (committed + staged + unstaged + untracked).
590580
*/
591581
async _getBaseCommitDiff(commit) {
592582
if (!this.gitRoot) {
@@ -605,23 +595,43 @@ class GitDiffHelper {
605595
throw new Error(`Could not resolve commit: ${commit}`);
606596
}
607597

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
608610
const { stdout: changedFiles } = await execAsync(
609-
`git diff --name-only ${resolvedCommit} HEAD`,
611+
`git diff --name-only ${base}`,
612+
{ cwd: this.gitRoot }
613+
);
614+
const { stdout: untrackedStr } = await execAsync(
615+
'git ls-files --others --exclude-standard',
610616
{ cwd: this.gitRoot }
611617
);
612618

613619
const files = changedFiles.split('\n').map(f => f.trim()).filter(Boolean);
614-
if (files.length === 0) return [];
620+
const untrackedFiles = untrackedStr.split('\n').map(f => f.trim()).filter(Boolean);
621+
if (files.length === 0 && untrackedFiles.length === 0) return [];
615622

616623
const originalBaseCommit = this.baseCommit;
617624
try {
618-
this.baseCommit = resolvedCommit;
619-
this._headRef = 'HEAD';
625+
this.baseCommit = base;
626+
this._headRef = null;
620627

621628
const diffs = await Promise.all(
622629
files.map(fp => this.getDiffInfoForFile(fp))
623630
);
624-
return diffs.flat();
631+
const untrackedDiffs = await Promise.all(
632+
untrackedFiles.map(fp => this._buildUntrackedFileDiff(fp))
633+
);
634+
return [...diffs.flat(), ...untrackedDiffs.filter(Boolean)];
625635
} finally {
626636
this.baseCommit = originalBaseCommit;
627637
this._headRef = null;
@@ -762,35 +772,8 @@ class GitDiffHelper {
762772

763773
// Handle untracked files with synthetic diffs
764774
for (const file of expandedUntrackedFiles) {
765-
try {
766-
const absolutePath = path.resolve(this.gitRoot, file);
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);
794777
}
795778
} finally {
796779
this.baseCommit = originalBaseCommit;

0 commit comments

Comments
 (0)