Repository navigation
Expand file tree
/
Copy pathworkspace_changes.ts
More file actions
63 lines (57 loc) · 2.66 KB
/
Copy pathworkspace_changes.ts
File metadata and controls
63 lines (57 loc) · 2.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import type { BugbotGitMutationPort } from '../../../../ports/bugbot_git_ports';
/**
* Extracts repository-relative paths from `git status --porcelain` output.
* Renames are represented by their destination path because that is what will
* be staged by the automated commit.
*/
export function parsePorcelainWorkspacePaths(status: string): string[] {
const paths: string[] = [];
for (const rawLine of status.split(/\r?\n/)) {
if (rawLine.length < 4) continue;
const pathPart = rawLine.slice(3).trim();
if (!pathPart) continue;
const renameSeparator = " -> ";
const path = pathPart.includes(renameSeparator)
? pathPart.slice(pathPart.lastIndexOf(renameSeparator) + renameSeparator.length).trim()
: pathPart;
if (path && !paths.includes(path)) paths.push(path);
}
return paths;
}
/** Returns true for files that must never be included in an automated commit. */
export function isSensitiveWorkspacePath(path: string): boolean {
const normalized = path.replace(/\\\\/g, "/").trim().toLowerCase();
if (!normalized) return true;
if (normalized.startsWith(".github/workflows/")) return true;
const basename = normalized.slice(normalized.lastIndexOf("/") + 1);
if (basename === ".env" || basename.startsWith(".env.")) return true;
if (basename.startsWith("id_rsa") || basename.startsWith("id_ed25519")) return true;
if ([".pem", ".key", ".p12", ".pfx", ".jks"].some((suffix) => basename.endsWith(suffix))) {
return true;
}
return /(credential|secret|token)/.test(basename);
}
/**
* Selects paths introduced by the AI operation and removes sensitive paths.
* The order from the post-operation status is preserved for deterministic git calls.
*/
export function selectWorkspacePathsToCommit(before: string[], after: string[]): string[] {
const beforeSet = new Set(before);
return after.filter((path, index) => {
if (beforeSet.has(path) || after.indexOf(path) !== index) return false;
return !isSensitiveWorkspacePath(path);
});
}
/** Reads the current working tree paths without executing a shell. */
export async function listWorkspacePaths(gitCommitPort: Pick<BugbotGitMutationPort, 'execute'>): Promise<string[]> {
let output = "";
await gitCommitPort.execute("git", ["status", "--porcelain", "--untracked-files=all"], {
stdout: (data: Buffer) => {
output += data.toString();
},
});
return parsePorcelainWorkspacePaths(output);
}
export async function hasWorkspaceChanges(gitCommitPort: Pick<BugbotGitMutationPort, 'execute'>): Promise<boolean> {
return (await listWorkspacePaths(gitCommitPort)).length > 0;
}