singularity-forge/src/resources/extensions/sf-tui/git.js

158 lines
3.4 KiB
JavaScript

import { execFileSync } from "node:child_process";
import { basename } from "node:path";
let cache = null;
let lastFetch = 0;
function getRepoName(cwd) {
try {
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
cwd,
encoding: "utf-8",
stdio: ["pipe", "pipe", "ignore"],
timeout: 1500,
}).trim();
return root ? basename(root) : basename(cwd) || null;
} catch {
return basename(cwd) || null;
}
}
function getLastCommit(cwd) {
try {
const raw = execFileSync("git", ["log", "-1", "--format=%cr|%s"], {
cwd,
encoding: "utf-8",
stdio: ["pipe", "pipe", "ignore"],
timeout: 1500,
}).trim();
const sep = raw.indexOf("|");
if (sep > 0) {
return {
timeAgo: raw.slice(0, sep).replace(/ ago$/, ""),
message: raw.slice(sep + 1),
};
}
} catch {
/* ignore */
}
return null;
}
function getDiffStats(cwd) {
try {
const raw = execFileSync("git", ["diff", "HEAD", "--stat"], {
cwd,
encoding: "utf-8",
stdio: ["pipe", "pipe", "ignore"],
timeout: 1500,
});
let added = 0;
let deleted = 0;
let modified = 0;
for (const line of raw.split("\n")) {
const addMatch = line.match(/(\d+) insertion/);
const delMatch = line.match(/(\d+) deletion/);
if (addMatch || delMatch) {
const a = addMatch ? parseInt(addMatch[1], 10) : 0;
const d = delMatch ? parseInt(delMatch[1], 10) : 0;
if (a) added += a;
if (d) deleted += d;
if (a || d) modified++;
}
}
return { added, deleted, modified };
} catch {
return { added: 0, deleted: 0, modified: 0 };
}
}
export function refreshGitStatus(cwd) {
const now = Date.now();
if (now - lastFetch < 400 && cache) return cache;
lastFetch = now;
const repo = getRepoName(cwd);
let branch = null;
try {
branch =
execFileSync("git", ["branch", "--show-current"], {
cwd,
encoding: "utf-8",
stdio: ["pipe", "pipe", "ignore"],
timeout: 1500,
}).trim() || null;
} catch {
cache = {
repo,
branch: null,
dirty: false,
untracked: false,
ahead: 0,
behind: 0,
added: 0,
deleted: 0,
modified: 0,
lastCommit: null,
};
return cache;
}
try {
const status = execFileSync("git", ["status", "--porcelain"], {
cwd,
encoding: "utf-8",
stdio: ["pipe", "pipe", "ignore"],
timeout: 1500,
});
const lines = status.split("\n").filter((l) => l.length > 2);
const dirty = lines.some((l) => {
const x = l[0] ?? " ";
const y = l[1] ?? " ";
return (x !== "?" && x !== " " && x !== "!") || (y !== " " && y !== "?");
});
const untracked = lines.some((l) => l.startsWith("??"));
let ahead = 0;
let behind = 0;
try {
const ab = execFileSync(
"git",
["rev-list", "--left-right", "--count", "HEAD...@{u}"],
{
cwd,
encoding: "utf-8",
stdio: ["pipe", "pipe", "ignore"],
timeout: 1500,
},
).trim();
const [a, b] = ab.split("\t").map((n) => parseInt(n, 10));
ahead = Number.isNaN(a) ? 0 : a;
behind = Number.isNaN(b) ? 0 : b;
} catch {
/* no upstream */
}
const diff = getDiffStats(cwd);
const lastCommit = getLastCommit(cwd);
cache = {
repo,
branch,
dirty,
untracked,
ahead,
behind,
...diff,
lastCommit,
};
} catch {
cache = {
repo,
branch,
dirty: false,
untracked: false,
ahead: 0,
behind: 0,
added: 0,
deleted: 0,
modified: 0,
lastCommit: getLastCommit(cwd),
};
}
return cache;
}
export function invalidateGitStatus() {
lastFetch = 0;
}