132 lines
3.4 KiB
TypeScript
132 lines
3.4 KiB
TypeScript
import type { FileEntry } from "./types";
|
|
|
|
export async function isGitRepo(): Promise<boolean> {
|
|
try {
|
|
await Bun.$`git rev-parse --is-inside-work-tree`.quiet();
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export async function getRepoRoot(): Promise<string> {
|
|
const result = await Bun.$`git rev-parse --show-toplevel`.quiet().text();
|
|
return result.trim();
|
|
}
|
|
|
|
function statusToLabel(status: string): string {
|
|
switch (status[0]) {
|
|
case "A":
|
|
return "new";
|
|
case "D":
|
|
return "deleted";
|
|
case "R":
|
|
return "renamed";
|
|
case "C":
|
|
return "copied";
|
|
case "M":
|
|
return "modified";
|
|
case "T":
|
|
return "type change";
|
|
default:
|
|
return "modified";
|
|
}
|
|
}
|
|
|
|
function parseNameStatus(output: string): FileEntry[] {
|
|
return output
|
|
.trim()
|
|
.split("\n")
|
|
.filter(Boolean)
|
|
.map((line) => {
|
|
const [status, ...pathParts] = line.split("\t");
|
|
const path = pathParts[pathParts.length - 1] ?? "";
|
|
return { path, status: status!, label: statusToLabel(status!) };
|
|
});
|
|
}
|
|
|
|
export async function getStagedFiles(): Promise<FileEntry[]> {
|
|
try {
|
|
const result =
|
|
await Bun.$`git diff --cached --name-status`.quiet().text();
|
|
return parseNameStatus(result);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function getUnstagedFiles(): Promise<FileEntry[]> {
|
|
const files: FileEntry[] = [];
|
|
|
|
try {
|
|
const result = await Bun.$`git diff --name-status`.quiet().text();
|
|
files.push(...parseNameStatus(result));
|
|
} catch {}
|
|
|
|
try {
|
|
const result =
|
|
await Bun.$`git ls-files --others --exclude-standard`.quiet().text();
|
|
for (const path of result.trim().split("\n").filter(Boolean)) {
|
|
files.push({ path, status: "??", label: "new" });
|
|
}
|
|
} catch {}
|
|
|
|
return files;
|
|
}
|
|
|
|
export async function getStagedDiff(): Promise<string> {
|
|
try {
|
|
const result = await Bun.$`git diff --staged`.quiet().text();
|
|
return result.trim();
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
export async function getRecentCommits(count = 10): Promise<string[]> {
|
|
try {
|
|
const n = String(count);
|
|
const result = await Bun.$`git log --oneline -n ${n}`.quiet().text();
|
|
return result.trim().split("\n").filter(Boolean);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function stageFiles(paths: string[]): Promise<void> {
|
|
if (paths.length === 0) return;
|
|
await Bun.$`git add -- ${paths}`;
|
|
}
|
|
|
|
export async function commit(
|
|
message: string,
|
|
): Promise<{ branch: string; hash: string; files: number; insertions: number; deletions: number }> {
|
|
const proc = Bun.spawn(["git", "commit", "-m", message], {
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
const exitCode = await proc.exited;
|
|
const stdout = await new Response(proc.stdout).text();
|
|
const stderr = await new Response(proc.stderr).text();
|
|
|
|
if (exitCode !== 0) {
|
|
throw new Error(stderr.trim() || `git commit failed (exit code ${exitCode})`);
|
|
}
|
|
|
|
const branchHashMatch = stdout.match(/\[(\S+)\s+([0-9a-f]{7,})/);
|
|
const branch = branchHashMatch?.[1] ?? "";
|
|
const hash = branchHashMatch?.[2] ?? "";
|
|
|
|
const filesMatch = stdout.match(/(\d+)\s+file/);
|
|
const insertionsMatch = stdout.match(/(\d+)\s+insertion/);
|
|
const deletionsMatch = stdout.match(/(\d+)\s+deletion/);
|
|
|
|
return {
|
|
branch,
|
|
hash,
|
|
files: parseInt(filesMatch?.[1] ?? "0"),
|
|
insertions: parseInt(insertionsMatch?.[1] ?? "0"),
|
|
deletions: parseInt(deletionsMatch?.[1] ?? "0"),
|
|
};
|
|
}
|