agent-gauntlet 0.10.0 → 0.11.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 +25 -23
- package/dist/index.js +9226 -0
- package/dist/index.js.map +65 -0
- package/dist/scripts/status.js +280 -0
- package/dist/scripts/status.js.map +10 -0
- package/package.json +22 -8
- package/src/built-in-reviews/code-quality.md +0 -25
- package/src/built-in-reviews/index.ts +0 -28
- package/src/bun-plugins.d.ts +0 -4
- package/src/cli-adapters/claude.ts +0 -327
- package/src/cli-adapters/codex.ts +0 -290
- package/src/cli-adapters/cursor.ts +0 -128
- package/src/cli-adapters/gemini.ts +0 -510
- package/src/cli-adapters/github-copilot.ts +0 -141
- package/src/cli-adapters/index.ts +0 -250
- package/src/cli-adapters/thinking-budget.ts +0 -23
- package/src/commands/check.ts +0 -311
- package/src/commands/ci/index.ts +0 -15
- package/src/commands/ci/init.ts +0 -96
- package/src/commands/ci/list-jobs.ts +0 -90
- package/src/commands/clean.ts +0 -54
- package/src/commands/detect.ts +0 -173
- package/src/commands/health.ts +0 -169
- package/src/commands/help.ts +0 -34
- package/src/commands/index.ts +0 -13
- package/src/commands/init.ts +0 -1878
- package/src/commands/list.ts +0 -33
- package/src/commands/review.ts +0 -311
- package/src/commands/run.ts +0 -29
- package/src/commands/shared.ts +0 -267
- package/src/commands/stop-hook.ts +0 -567
- package/src/commands/validate.ts +0 -20
- package/src/commands/wait-ci.ts +0 -518
- package/src/config/ci-loader.ts +0 -33
- package/src/config/ci-schema.ts +0 -28
- package/src/config/global.ts +0 -87
- package/src/config/loader.ts +0 -301
- package/src/config/schema.ts +0 -165
- package/src/config/stop-hook-config.ts +0 -130
- package/src/config/types.ts +0 -65
- package/src/config/validator.ts +0 -592
- package/src/core/change-detector.ts +0 -137
- package/src/core/diff-stats.ts +0 -442
- package/src/core/entry-point.ts +0 -190
- package/src/core/job.ts +0 -96
- package/src/core/run-executor.ts +0 -621
- package/src/core/runner.ts +0 -290
- package/src/gates/check.ts +0 -118
- package/src/gates/resolve-check-command.ts +0 -21
- package/src/gates/result.ts +0 -54
- package/src/gates/review.ts +0 -1333
- package/src/hooks/adapters/claude-stop-hook.ts +0 -99
- package/src/hooks/adapters/cursor-stop-hook.ts +0 -122
- package/src/hooks/adapters/types.ts +0 -94
- package/src/hooks/stop-hook-handler.ts +0 -748
- package/src/index.ts +0 -47
- package/src/output/app-logger.ts +0 -214
- package/src/output/console-log.ts +0 -168
- package/src/output/console.ts +0 -359
- package/src/output/logger.ts +0 -126
- package/src/output/sinks/console-sink.ts +0 -59
- package/src/output/sinks/file-sink.ts +0 -110
- package/src/scripts/status.ts +0 -433
- package/src/templates/workflow.yml +0 -79
- package/src/types/gauntlet-status.ts +0 -79
- package/src/utils/debug-log.ts +0 -392
- package/src/utils/diff-parser.ts +0 -103
- package/src/utils/execution-state.ts +0 -472
- package/src/utils/log-parser.ts +0 -696
- package/src/utils/sanitizer.ts +0 -3
- package/src/utils/session-ref.ts +0 -91
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { exec } from "node:child_process";
|
|
2
|
-
import { promisify } from "node:util";
|
|
3
|
-
|
|
4
|
-
const execAsync = promisify(exec);
|
|
5
|
-
|
|
6
|
-
/** Validate that a string is a safe git ref (hex SHA or branch-like name). */
|
|
7
|
-
function isValidGitRef(ref: string): boolean {
|
|
8
|
-
return /^[a-zA-Z0-9._\-/]+$/.test(ref);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface ChangeDetectorOptions {
|
|
12
|
-
commit?: string; // If provided, get diff for this commit vs its parent
|
|
13
|
-
uncommitted?: boolean; // If true, only get uncommitted changes (staged + unstaged)
|
|
14
|
-
fixBase?: string; // If provided, get diff from this ref to current working tree
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export class ChangeDetector {
|
|
18
|
-
constructor(
|
|
19
|
-
private baseBranch: string = "origin/main",
|
|
20
|
-
private options: ChangeDetectorOptions = {},
|
|
21
|
-
) {}
|
|
22
|
-
|
|
23
|
-
async getChangedFiles(): Promise<string[]> {
|
|
24
|
-
// Priority 1: If commit option is provided, use that
|
|
25
|
-
if (this.options.commit) {
|
|
26
|
-
return this.getCommitChangedFiles(this.options.commit);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Priority 2: If uncommitted option is provided, only get uncommitted changes
|
|
30
|
-
if (this.options.uncommitted) {
|
|
31
|
-
return this.getUncommittedChangedFiles();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Priority 3: If fixBase is provided, diff against it
|
|
35
|
-
if (this.options.fixBase && isValidGitRef(this.options.fixBase)) {
|
|
36
|
-
return this.getFixBaseChangedFiles(this.options.fixBase);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Priority 4: CI detection / local base branch diff
|
|
40
|
-
const isCI =
|
|
41
|
-
process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
|
|
42
|
-
|
|
43
|
-
if (isCI) {
|
|
44
|
-
return this.getCIChangedFiles();
|
|
45
|
-
} else {
|
|
46
|
-
return this.getLocalChangedFiles();
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
private async getCIChangedFiles(): Promise<string[]> {
|
|
51
|
-
// In GitHub Actions, GITHUB_SHA is the commit being built
|
|
52
|
-
// Base branch priority is already resolved by caller
|
|
53
|
-
const baseRef = this.baseBranch;
|
|
54
|
-
const headRef = process.env.GITHUB_SHA || "HEAD";
|
|
55
|
-
|
|
56
|
-
// We might need to fetch first in some shallow clones, but assuming strictly for now
|
|
57
|
-
// git diff --name-only base...head
|
|
58
|
-
try {
|
|
59
|
-
const { stdout } = await execAsync(
|
|
60
|
-
`git diff --name-only ${baseRef}...${headRef}`,
|
|
61
|
-
);
|
|
62
|
-
return this.parseOutput(stdout);
|
|
63
|
-
} catch (error) {
|
|
64
|
-
console.warn(
|
|
65
|
-
"Failed to detect changes via git diff in CI, falling back to HEAD^...HEAD",
|
|
66
|
-
error,
|
|
67
|
-
);
|
|
68
|
-
// Fallback for push events where base ref might not be available
|
|
69
|
-
const { stdout } = await execAsync("git diff --name-only HEAD^...HEAD");
|
|
70
|
-
return this.parseOutput(stdout);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Collect uncommitted (staged + unstaged) and untracked file paths. */
|
|
75
|
-
private async getWorkingTreeFiles(): Promise<string[]> {
|
|
76
|
-
const { stdout: staged } = await execAsync("git diff --name-only --cached");
|
|
77
|
-
const { stdout: unstaged } = await execAsync("git diff --name-only");
|
|
78
|
-
const { stdout: untracked } = await execAsync(
|
|
79
|
-
"git ls-files --others --exclude-standard",
|
|
80
|
-
);
|
|
81
|
-
return [
|
|
82
|
-
...this.parseOutput(staged),
|
|
83
|
-
...this.parseOutput(unstaged),
|
|
84
|
-
...this.parseOutput(untracked),
|
|
85
|
-
];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Combine committed diff (against a base ref) with working tree changes. */
|
|
89
|
-
private async getDiffWithWorkingTree(baseRef: string): Promise<string[]> {
|
|
90
|
-
const { stdout: committed } = await execAsync(
|
|
91
|
-
`git diff --name-only ${baseRef}...HEAD`,
|
|
92
|
-
);
|
|
93
|
-
const files = new Set([
|
|
94
|
-
...this.parseOutput(committed),
|
|
95
|
-
...(await this.getWorkingTreeFiles()),
|
|
96
|
-
]);
|
|
97
|
-
return Array.from(files);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
private async getLocalChangedFiles(): Promise<string[]> {
|
|
101
|
-
return this.getDiffWithWorkingTree(this.baseBranch);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
private async getCommitChangedFiles(commit: string): Promise<string[]> {
|
|
105
|
-
try {
|
|
106
|
-
const { stdout } = await execAsync(
|
|
107
|
-
`git diff --name-only ${commit}^..${commit}`,
|
|
108
|
-
);
|
|
109
|
-
return this.parseOutput(stdout);
|
|
110
|
-
} catch (_error) {
|
|
111
|
-
try {
|
|
112
|
-
const { stdout } = await execAsync(
|
|
113
|
-
`git diff --name-only --root ${commit}`,
|
|
114
|
-
);
|
|
115
|
-
return this.parseOutput(stdout);
|
|
116
|
-
} catch {
|
|
117
|
-
throw new Error(`Failed to get changes for commit ${commit}`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
private async getFixBaseChangedFiles(fixBase: string): Promise<string[]> {
|
|
123
|
-
return this.getDiffWithWorkingTree(fixBase);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
private async getUncommittedChangedFiles(): Promise<string[]> {
|
|
127
|
-
const files = new Set(await this.getWorkingTreeFiles());
|
|
128
|
-
return Array.from(files);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
private parseOutput(stdout: string): string[] {
|
|
132
|
-
return stdout
|
|
133
|
-
.split("\n")
|
|
134
|
-
.map((line) => line.trim())
|
|
135
|
-
.filter((line) => line.length > 0);
|
|
136
|
-
}
|
|
137
|
-
}
|
package/src/core/diff-stats.ts
DELETED
|
@@ -1,442 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { promisify } from "node:util";
|
|
3
|
-
|
|
4
|
-
const execFileAsyncOriginal = promisify(execFile);
|
|
5
|
-
export let execFileAsync = execFileAsyncOriginal;
|
|
6
|
-
|
|
7
|
-
export function setExecFileAsync(fn: typeof execFileAsyncOriginal) {
|
|
8
|
-
execFileAsync = fn;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Run a git command safely using execFile (no shell interpolation).
|
|
13
|
-
*/
|
|
14
|
-
async function gitExec(args: string[]): Promise<string> {
|
|
15
|
-
const { stdout } = await execFileAsync("git", args);
|
|
16
|
-
return stdout;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface DiffStats {
|
|
20
|
-
baseRef: string; // e.g., "origin/main", "abc123", "uncommitted"
|
|
21
|
-
total: number; // Total files changed
|
|
22
|
-
newFiles: number; // Files added
|
|
23
|
-
modifiedFiles: number; // Files modified
|
|
24
|
-
deletedFiles: number; // Files deleted
|
|
25
|
-
linesAdded: number; // Total lines added
|
|
26
|
-
linesRemoved: number; // Total lines removed
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface DiffStatsOptions {
|
|
30
|
-
commit?: string; // If provided, get diff for this commit vs its parent
|
|
31
|
-
uncommitted?: boolean; // If true, only get uncommitted changes
|
|
32
|
-
fixBase?: string; // If provided, get diff from this ref to current working tree
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Compute diff statistics for changed files.
|
|
37
|
-
*/
|
|
38
|
-
export async function computeDiffStats(
|
|
39
|
-
baseBranch: string,
|
|
40
|
-
options: DiffStatsOptions = {},
|
|
41
|
-
): Promise<DiffStats> {
|
|
42
|
-
// Determine what we're diffing
|
|
43
|
-
if (options.commit) {
|
|
44
|
-
return computeCommitDiffStats(options.commit);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// If fixBase is provided, compute diff from that ref to current working tree
|
|
48
|
-
// This is used in verification mode to show only NEW changes since the snapshot
|
|
49
|
-
if (options.fixBase) {
|
|
50
|
-
return computeFixBaseDiffStats(options.fixBase);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (options.uncommitted) {
|
|
54
|
-
return computeUncommittedDiffStats();
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const isCI =
|
|
58
|
-
process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
|
|
59
|
-
|
|
60
|
-
if (isCI) {
|
|
61
|
-
return computeCIDiffStats(baseBranch);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return computeLocalDiffStats(baseBranch);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Compute diff stats for a specific commit vs its parent.
|
|
69
|
-
*/
|
|
70
|
-
async function computeCommitDiffStats(commit: string): Promise<DiffStats> {
|
|
71
|
-
try {
|
|
72
|
-
// Get numstat for line counts
|
|
73
|
-
const numstat = await gitExec([
|
|
74
|
-
"diff",
|
|
75
|
-
"--numstat",
|
|
76
|
-
`${commit}^..${commit}`,
|
|
77
|
-
]);
|
|
78
|
-
const lineStats = parseNumstat(numstat);
|
|
79
|
-
|
|
80
|
-
// Get name-status for file categorization
|
|
81
|
-
const nameStatus = await gitExec([
|
|
82
|
-
"diff",
|
|
83
|
-
"--name-status",
|
|
84
|
-
`${commit}^..${commit}`,
|
|
85
|
-
]);
|
|
86
|
-
const fileStats = parseNameStatus(nameStatus);
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
baseRef: `${commit}^`,
|
|
90
|
-
...fileStats,
|
|
91
|
-
...lineStats,
|
|
92
|
-
};
|
|
93
|
-
} catch {
|
|
94
|
-
// If commit has no parent (initial commit), try --root
|
|
95
|
-
try {
|
|
96
|
-
const numstat = await gitExec(["diff", "--numstat", "--root", commit]);
|
|
97
|
-
const lineStats = parseNumstat(numstat);
|
|
98
|
-
|
|
99
|
-
const nameStatus = await gitExec([
|
|
100
|
-
"diff",
|
|
101
|
-
"--name-status",
|
|
102
|
-
"--root",
|
|
103
|
-
commit,
|
|
104
|
-
]);
|
|
105
|
-
const fileStats = parseNameStatus(nameStatus);
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
baseRef: "root",
|
|
109
|
-
...fileStats,
|
|
110
|
-
...lineStats,
|
|
111
|
-
};
|
|
112
|
-
} catch {
|
|
113
|
-
return emptyDiffStats(commit);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Compute diff stats for uncommitted changes (staged + unstaged + untracked).
|
|
120
|
-
*/
|
|
121
|
-
async function computeUncommittedDiffStats(): Promise<DiffStats> {
|
|
122
|
-
// Get stats for staged changes
|
|
123
|
-
const stagedNumstat = await gitExec(["diff", "--numstat", "--cached"]);
|
|
124
|
-
const stagedLines = parseNumstat(stagedNumstat);
|
|
125
|
-
|
|
126
|
-
const stagedStatus = await gitExec(["diff", "--name-status", "--cached"]);
|
|
127
|
-
const stagedFiles = parseNameStatus(stagedStatus);
|
|
128
|
-
|
|
129
|
-
// Get stats for unstaged changes
|
|
130
|
-
const unstagedNumstat = await gitExec(["diff", "--numstat"]);
|
|
131
|
-
const unstagedLines = parseNumstat(unstagedNumstat);
|
|
132
|
-
|
|
133
|
-
const unstagedStatus = await gitExec(["diff", "--name-status"]);
|
|
134
|
-
const unstagedFiles = parseNameStatus(unstagedStatus);
|
|
135
|
-
|
|
136
|
-
// Get untracked files (all count as new, lines unknown)
|
|
137
|
-
const untrackedList = await gitExec([
|
|
138
|
-
"ls-files",
|
|
139
|
-
"--others",
|
|
140
|
-
"--exclude-standard",
|
|
141
|
-
]);
|
|
142
|
-
const untrackedFiles = untrackedList
|
|
143
|
-
.split("\n")
|
|
144
|
-
.filter((f) => f.trim().length > 0);
|
|
145
|
-
|
|
146
|
-
return {
|
|
147
|
-
baseRef: "uncommitted",
|
|
148
|
-
total:
|
|
149
|
-
stagedFiles.total +
|
|
150
|
-
unstagedFiles.total +
|
|
151
|
-
untrackedFiles.length -
|
|
152
|
-
countOverlap(stagedStatus, unstagedStatus),
|
|
153
|
-
newFiles:
|
|
154
|
-
stagedFiles.newFiles + unstagedFiles.newFiles + untrackedFiles.length,
|
|
155
|
-
modifiedFiles: stagedFiles.modifiedFiles + unstagedFiles.modifiedFiles,
|
|
156
|
-
deletedFiles: stagedFiles.deletedFiles + unstagedFiles.deletedFiles,
|
|
157
|
-
linesAdded: stagedLines.linesAdded + unstagedLines.linesAdded,
|
|
158
|
-
linesRemoved: stagedLines.linesRemoved + unstagedLines.linesRemoved,
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Compute diff stats from a fixBase ref (stash or commit) to current working tree.
|
|
164
|
-
* Used in verification mode to show only NEW changes since the snapshot.
|
|
165
|
-
* This includes staged changes, unstaged changes, and new untracked files.
|
|
166
|
-
*/
|
|
167
|
-
async function computeFixBaseDiffStats(fixBase: string): Promise<DiffStats> {
|
|
168
|
-
try {
|
|
169
|
-
// Get line stats for tracked file changes since fixBase
|
|
170
|
-
// We need to diff against working tree (staged + unstaged changes)
|
|
171
|
-
const numstat = await gitExec(["diff", "--numstat", fixBase]);
|
|
172
|
-
const lineStats = parseNumstat(numstat);
|
|
173
|
-
|
|
174
|
-
// Get file categorization for tracked file changes
|
|
175
|
-
const nameStatus = await gitExec(["diff", "--name-status", fixBase]);
|
|
176
|
-
const fileStats = parseNameStatus(nameStatus);
|
|
177
|
-
|
|
178
|
-
// Handle untracked files: only count NEW untracked files that weren't in fixBase
|
|
179
|
-
// Current untracked files
|
|
180
|
-
const currentUntracked = (
|
|
181
|
-
await gitExec(["ls-files", "--others", "--exclude-standard"])
|
|
182
|
-
)
|
|
183
|
-
.split("\n")
|
|
184
|
-
.filter((f) => f.trim().length > 0);
|
|
185
|
-
|
|
186
|
-
// Files that existed in fixBase
|
|
187
|
-
let fixBaseFiles: Set<string>;
|
|
188
|
-
try {
|
|
189
|
-
const treeFiles = await gitExec([
|
|
190
|
-
"ls-tree",
|
|
191
|
-
"-r",
|
|
192
|
-
"--name-only",
|
|
193
|
-
fixBase,
|
|
194
|
-
]);
|
|
195
|
-
fixBaseFiles = new Set(
|
|
196
|
-
treeFiles.split("\n").filter((f) => f.trim().length > 0),
|
|
197
|
-
);
|
|
198
|
-
} catch {
|
|
199
|
-
// If fixBase is invalid or has no tree, assume empty
|
|
200
|
-
fixBaseFiles = new Set();
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// New untracked files = current untracked - files that existed in fixBase
|
|
204
|
-
const newUntrackedFiles = currentUntracked.filter(
|
|
205
|
-
(f) => !fixBaseFiles.has(f),
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
return {
|
|
209
|
-
baseRef: fixBase,
|
|
210
|
-
total: fileStats.total + newUntrackedFiles.length,
|
|
211
|
-
newFiles: fileStats.newFiles + newUntrackedFiles.length,
|
|
212
|
-
modifiedFiles: fileStats.modifiedFiles,
|
|
213
|
-
deletedFiles: fileStats.deletedFiles,
|
|
214
|
-
linesAdded: lineStats.linesAdded,
|
|
215
|
-
linesRemoved: lineStats.linesRemoved,
|
|
216
|
-
};
|
|
217
|
-
} catch {
|
|
218
|
-
return emptyDiffStats(fixBase);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Compute diff stats in CI environment.
|
|
224
|
-
*/
|
|
225
|
-
async function computeCIDiffStats(baseBranch: string): Promise<DiffStats> {
|
|
226
|
-
const headRef = process.env.GITHUB_SHA || "HEAD";
|
|
227
|
-
|
|
228
|
-
try {
|
|
229
|
-
const numstat = await gitExec([
|
|
230
|
-
"diff",
|
|
231
|
-
"--numstat",
|
|
232
|
-
`${baseBranch}...${headRef}`,
|
|
233
|
-
]);
|
|
234
|
-
const lineStats = parseNumstat(numstat);
|
|
235
|
-
|
|
236
|
-
const nameStatus = await gitExec([
|
|
237
|
-
"diff",
|
|
238
|
-
"--name-status",
|
|
239
|
-
`${baseBranch}...${headRef}`,
|
|
240
|
-
]);
|
|
241
|
-
const fileStats = parseNameStatus(nameStatus);
|
|
242
|
-
|
|
243
|
-
return {
|
|
244
|
-
baseRef: baseBranch,
|
|
245
|
-
...fileStats,
|
|
246
|
-
...lineStats,
|
|
247
|
-
};
|
|
248
|
-
} catch {
|
|
249
|
-
// Fallback for push events
|
|
250
|
-
try {
|
|
251
|
-
const numstat = await gitExec(["diff", "--numstat", "HEAD^...HEAD"]);
|
|
252
|
-
const lineStats = parseNumstat(numstat);
|
|
253
|
-
|
|
254
|
-
const nameStatus = await gitExec([
|
|
255
|
-
"diff",
|
|
256
|
-
"--name-status",
|
|
257
|
-
"HEAD^...HEAD",
|
|
258
|
-
]);
|
|
259
|
-
const fileStats = parseNameStatus(nameStatus);
|
|
260
|
-
|
|
261
|
-
return {
|
|
262
|
-
baseRef: "HEAD^",
|
|
263
|
-
...fileStats,
|
|
264
|
-
...lineStats,
|
|
265
|
-
};
|
|
266
|
-
} catch {
|
|
267
|
-
return emptyDiffStats(baseBranch);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Compute diff stats for local development.
|
|
274
|
-
*/
|
|
275
|
-
async function computeLocalDiffStats(baseBranch: string): Promise<DiffStats> {
|
|
276
|
-
// 1. Committed changes relative to base branch
|
|
277
|
-
const committedNumstat = await gitExec([
|
|
278
|
-
"diff",
|
|
279
|
-
"--numstat",
|
|
280
|
-
`${baseBranch}...HEAD`,
|
|
281
|
-
]);
|
|
282
|
-
const committedLines = parseNumstat(committedNumstat);
|
|
283
|
-
|
|
284
|
-
const committedStatus = await gitExec([
|
|
285
|
-
"diff",
|
|
286
|
-
"--name-status",
|
|
287
|
-
`${baseBranch}...HEAD`,
|
|
288
|
-
]);
|
|
289
|
-
const committedFiles = parseNameStatus(committedStatus);
|
|
290
|
-
|
|
291
|
-
// 2. Uncommitted changes (staged and unstaged)
|
|
292
|
-
const uncommittedNumstat = await gitExec(["diff", "--numstat", "HEAD"]);
|
|
293
|
-
const uncommittedLines = parseNumstat(uncommittedNumstat);
|
|
294
|
-
|
|
295
|
-
const uncommittedStatus = await gitExec(["diff", "--name-status", "HEAD"]);
|
|
296
|
-
const uncommittedFiles = parseNameStatus(uncommittedStatus);
|
|
297
|
-
|
|
298
|
-
// 3. Untracked files
|
|
299
|
-
const untrackedList = await gitExec([
|
|
300
|
-
"ls-files",
|
|
301
|
-
"--others",
|
|
302
|
-
"--exclude-standard",
|
|
303
|
-
]);
|
|
304
|
-
const untrackedFiles = untrackedList
|
|
305
|
-
.split("\n")
|
|
306
|
-
.filter((f) => f.trim().length > 0);
|
|
307
|
-
|
|
308
|
-
// Combine counts (with overlap detection)
|
|
309
|
-
const totalNew =
|
|
310
|
-
committedFiles.newFiles + uncommittedFiles.newFiles + untrackedFiles.length;
|
|
311
|
-
const totalModified =
|
|
312
|
-
committedFiles.modifiedFiles + uncommittedFiles.modifiedFiles;
|
|
313
|
-
const totalDeleted =
|
|
314
|
-
committedFiles.deletedFiles + uncommittedFiles.deletedFiles;
|
|
315
|
-
|
|
316
|
-
return {
|
|
317
|
-
baseRef: baseBranch,
|
|
318
|
-
total: totalNew + totalModified + totalDeleted,
|
|
319
|
-
newFiles: totalNew,
|
|
320
|
-
modifiedFiles: totalModified,
|
|
321
|
-
deletedFiles: totalDeleted,
|
|
322
|
-
linesAdded: committedLines.linesAdded + uncommittedLines.linesAdded,
|
|
323
|
-
linesRemoved: committedLines.linesRemoved + uncommittedLines.linesRemoved,
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Parse git diff --numstat output for line counts.
|
|
329
|
-
* Format: <added>\t<removed>\t<file>
|
|
330
|
-
* Binary files show as "-\t-\t<file>"
|
|
331
|
-
*/
|
|
332
|
-
function parseNumstat(output: string): {
|
|
333
|
-
linesAdded: number;
|
|
334
|
-
linesRemoved: number;
|
|
335
|
-
} {
|
|
336
|
-
let linesAdded = 0;
|
|
337
|
-
let linesRemoved = 0;
|
|
338
|
-
|
|
339
|
-
for (const line of output.split("\n")) {
|
|
340
|
-
if (!line.trim()) continue;
|
|
341
|
-
const parts = line.split("\t");
|
|
342
|
-
if (parts.length < 3) continue;
|
|
343
|
-
|
|
344
|
-
const added = parts[0];
|
|
345
|
-
const removed = parts[1];
|
|
346
|
-
// Binary files show as "-"
|
|
347
|
-
if (added && added !== "-") {
|
|
348
|
-
linesAdded += parseInt(added, 10) || 0;
|
|
349
|
-
}
|
|
350
|
-
if (removed && removed !== "-") {
|
|
351
|
-
linesRemoved += parseInt(removed, 10) || 0;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
return { linesAdded, linesRemoved };
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Parse git diff --name-status output for file categorization.
|
|
360
|
-
* Format: <status>\t<file> (and optionally \t<new-file> for renames)
|
|
361
|
-
* Status codes: A=added, M=modified, D=deleted, R=renamed, C=copied, T=type-change
|
|
362
|
-
*/
|
|
363
|
-
function parseNameStatus(output: string): {
|
|
364
|
-
total: number;
|
|
365
|
-
newFiles: number;
|
|
366
|
-
modifiedFiles: number;
|
|
367
|
-
deletedFiles: number;
|
|
368
|
-
} {
|
|
369
|
-
let newFiles = 0;
|
|
370
|
-
let modifiedFiles = 0;
|
|
371
|
-
let deletedFiles = 0;
|
|
372
|
-
|
|
373
|
-
for (const line of output.split("\n")) {
|
|
374
|
-
if (!line.trim()) continue;
|
|
375
|
-
const status = line[0];
|
|
376
|
-
|
|
377
|
-
switch (status) {
|
|
378
|
-
case "A":
|
|
379
|
-
newFiles++;
|
|
380
|
-
break;
|
|
381
|
-
case "M":
|
|
382
|
-
case "R":
|
|
383
|
-
case "C":
|
|
384
|
-
case "T":
|
|
385
|
-
modifiedFiles++;
|
|
386
|
-
break;
|
|
387
|
-
case "D":
|
|
388
|
-
deletedFiles++;
|
|
389
|
-
break;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
return {
|
|
394
|
-
total: newFiles + modifiedFiles + deletedFiles,
|
|
395
|
-
newFiles,
|
|
396
|
-
modifiedFiles,
|
|
397
|
-
deletedFiles,
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Count overlapping files between two name-status outputs.
|
|
403
|
-
* Used to avoid double-counting files that appear in both staged and unstaged.
|
|
404
|
-
*/
|
|
405
|
-
function countOverlap(status1: string, status2: string): number {
|
|
406
|
-
const files1 = new Set<string>();
|
|
407
|
-
for (const line of status1.split("\n")) {
|
|
408
|
-
if (!line.trim()) continue;
|
|
409
|
-
const parts = line.split("\t");
|
|
410
|
-
const file = parts[1];
|
|
411
|
-
if (parts.length >= 2 && file) {
|
|
412
|
-
files1.add(file);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
let overlap = 0;
|
|
417
|
-
for (const line of status2.split("\n")) {
|
|
418
|
-
if (!line.trim()) continue;
|
|
419
|
-
const parts = line.split("\t");
|
|
420
|
-
const file = parts[1];
|
|
421
|
-
if (parts.length >= 2 && file && files1.has(file)) {
|
|
422
|
-
overlap++;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
return overlap;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* Return empty diff stats with the given base ref.
|
|
431
|
-
*/
|
|
432
|
-
function emptyDiffStats(baseRef: string): DiffStats {
|
|
433
|
-
return {
|
|
434
|
-
baseRef,
|
|
435
|
-
total: 0,
|
|
436
|
-
newFiles: 0,
|
|
437
|
-
modifiedFiles: 0,
|
|
438
|
-
deletedFiles: 0,
|
|
439
|
-
linesAdded: 0,
|
|
440
|
-
linesRemoved: 0,
|
|
441
|
-
};
|
|
442
|
-
}
|