feat(FN-1614): convert git helper functions to async

Convert all 11 git helper functions from sync execSync calls to async runGitCommand:
- getGitHubRemotes
- isGitRepo
- getGitStatus
- getGitCommits
- getGitCommitsForBranch
- getAheadCommits
- getCommitDiff
- getGitBranches
- getGitWorktrees
- listGitRemotes
- getGitStashList

Each function now uses await runGitCommand([...args], cwd, timeout) instead of execSync.
This commit is contained in:
Fusion
2026-04-15 03:23:56 -07:00
committed by gsxdsm
parent 3cd5d2c610
commit aab990e2a8

View File

@@ -676,11 +676,10 @@ function parseGitHubBadgeUrl(url: string | undefined): { owner: string; repo: st
* Get GitHub remotes from the current git repository.
* Executes `git remote -v` and parses the output.
*/
function getGitHubRemotes(cwd?: string): GitRemote[] {
async function getGitHubRemotes(cwd?: string): Promise<GitRemote[]> {
try {
// Execute git remote -v to get all remotes with their URLs
const execOptions = { encoding: "utf-8" as const, timeout: 5000, cwd };
const output = execSync("git remote -v", execOptions);
const output = await runGitCommand(["remote", "-v"], cwd, 5000);
const remotes: GitRemote[] = [];
const seen = new Set<string>();
@@ -720,10 +719,9 @@ function getGitHubRemotes(cwd?: string): GitRemote[] {
* Check if the current directory is a git repository.
* Used to validate git operations before executing commands.
*/
function isGitRepo(cwd?: string): boolean {
async function isGitRepo(cwd?: string): Promise<boolean> {
try {
const execOptions = { encoding: "utf-8" as const, timeout: 5000, cwd, stdio: "pipe" as const };
execSync("git rev-parse --git-dir", execOptions);
await runGitCommand(["rev-parse", "--git-dir"], cwd, 5000);
return true;
} catch {
return false;
@@ -734,30 +732,30 @@ function isGitRepo(cwd?: string): boolean {
* Get the current git status including branch, commit hash, and dirty state.
* Returns structured data for the Git Manager UI.
*/
function getGitStatus(cwd?: string): {
async function getGitStatus(cwd?: string): Promise<{
branch: string;
commit: string;
isDirty: boolean;
ahead: number;
behind: number;
} | null {
} | null> {
try {
const execOptions = { encoding: "utf-8" as const, timeout: 5000, cwd };
// Get current branch
const branch = execSync("git branch --show-current", execOptions).trim() || "HEAD detached";
const branchOutput = await runGitCommand(["branch", "--show-current"], cwd, 5000);
const branch = branchOutput.trim() || "HEAD detached";
// Get current commit hash (short)
const commit = execSync("git rev-parse --short HEAD", execOptions).trim();
const commit = (await runGitCommand(["rev-parse", "--short", "HEAD"], cwd, 5000)).trim();
// Check if working directory is dirty
const statusOutput = execSync("git status --porcelain", execOptions).trim();
const statusOutput = (await runGitCommand(["status", "--porcelain"], cwd, 5000)).trim();
const isDirty = statusOutput.length > 0;
// Get ahead/behind counts from origin
let ahead = 0;
let behind = 0;
try {
const revListOutput = execSync("git rev-list --left-right --count HEAD...@{u}", execOptions).trim();
const revListOutput = (await runGitCommand(["rev-list", "--left-right", "--count", "HEAD...@{u}"], cwd, 5000)).trim();
const match = revListOutput.match(/(\d+)\s+(\d+)/);
if (match) {
ahead = parseInt(match[1], 10);
@@ -787,12 +785,11 @@ export interface GitCommit {
* Get recent commits from the git log.
* @param limit Maximum number of commits to return (default 20)
*/
function getGitCommits(limit: number = 20, cwd?: string): GitCommit[] {
async function getGitCommits(limit: number = 20, cwd?: string): Promise<GitCommit[]> {
try {
// Format: hash|shortHash|message|author|date|parents
const format = "%H|%h|%s|%an|%aI|%P";
const execOptions = { encoding: "utf-8" as const, timeout: 10000, cwd };
const output = execSync(`git log --max-count=${limit} --pretty=format:"${format}"`, execOptions);
const output = await runGitCommand(["log", `--max-count=${limit}`, `--pretty=format:${format}`], cwd, 10000);
const commits: GitCommit[] = [];
for (const line of output.split("\n")) {
@@ -845,11 +842,10 @@ function isValidGitRef(ref: string): boolean {
* @param limit Maximum number of commits to return
* @param cwd Working directory
*/
function getGitCommitsForBranch(branch: string, limit: number = 10, cwd?: string): GitCommit[] {
async function getGitCommitsForBranch(branch: string, limit: number = 10, cwd?: string): Promise<GitCommit[]> {
try {
const format = "%H|%h|%s|%an|%aI|%P";
const execOptions = { encoding: "utf-8" as const, timeout: 10000, cwd, stdio: "pipe" as const };
const output = execSync(`git log --max-count=${limit} --pretty=format:"${format}" "${branch}"`, execOptions);
const output = await runGitCommand(["log", `--max-count=${limit}`, `--pretty=format:${format}`, branch], cwd, 10000);
const commits: GitCommit[] = [];
for (const line of output.split("\n")) {
@@ -880,12 +876,11 @@ function getGitCommitsForBranch(branch: string, limit: number = 10, cwd?: string
* Returns the list of local commits not yet present on the upstream.
* Returns an empty array if there is no upstream configured.
*/
function getAheadCommits(cwd?: string): GitCommit[] {
async function getAheadCommits(cwd?: string): Promise<GitCommit[]> {
try {
const execOptions = { encoding: "utf-8" as const, timeout: 10000, cwd, stdio: "pipe" as const };
// Check if an upstream is configured
try {
execSync("git rev-parse --abbrev-ref @{u}", execOptions);
await runGitCommand(["rev-parse", "--abbrev-ref", "@{u}"], cwd, 10000);
} catch {
// No upstream configured
return [];
@@ -893,7 +888,7 @@ function getAheadCommits(cwd?: string): GitCommit[] {
// Format: hash|shortHash|message|author|date|parents
const format = "%H|%h|%s|%an|%aI|%P";
const output = execSync(`git log @{u}..HEAD --pretty=format:"${format}"`, execOptions);
const output = await runGitCommand(["log", "@{u}..HEAD", `--pretty=format:${format}`], cwd, 10000);
const commits: GitCommit[] = [];
for (const line of output.split("\n")) {
@@ -970,17 +965,16 @@ async function getRemoteCommits(remoteRef: string, limit: number = 10, cwd?: str
* @param hash The commit hash
* @returns Object with stat and patch
*/
function getCommitDiff(hash: string, cwd?: string): { stat: string; patch: string } | null {
async function getCommitDiff(hash: string, cwd?: string): Promise<{ stat: string; patch: string } | null> {
try {
const execOptions = { encoding: "utf-8" as const, timeout: 10000, cwd, stdio: "pipe" as const };
// Validate the hash is a valid git object
execSync(`git cat-file -t ${hash}`, { encoding: "utf-8", timeout: 5000, cwd, stdio: "pipe" });
await runGitCommand(["cat-file", "-t", hash], cwd, 5000);
// Get diff stat
const stat = execSync(`git show --stat --format="" ${hash}`, execOptions).trim();
const stat = (await runGitCommand(["show", "--stat", `--format=""`, hash], cwd, 10000)).trim();
// Get patch
const patch = execSync(`git show --format="" ${hash}`, execOptions);
const patch = await runGitCommand(["show", `--format=""`, hash], cwd, 10000);
return { stat, patch };
} catch {
@@ -999,27 +993,22 @@ export interface GitBranch {
/**
* Get all local branches with their info.
*/
function getGitBranches(cwd?: string): GitBranch[] {
async function getGitBranches(cwd?: string): Promise<GitBranch[]> {
try {
const execOptions = { encoding: "utf-8" as const, timeout: 5000, cwd };
// Get current branch name
let currentBranch = "";
try {
currentBranch = execSync("git branch --show-current", execOptions).trim();
currentBranch = (await runGitCommand(["branch", "--show-current"], cwd, 5000)).trim();
} catch {
// Detached HEAD - no current branch
}
// Get all branches with info
const format = "%(refname:short)|%(upstream:short)|%(committerdate:iso8601)|%(HEAD)";
const output = execSync(`git for-each-ref --format="${format}" refs/heads/`, {
encoding: "utf-8",
timeout: 10000,
cwd,
});
const output = (await runGitCommand(["for-each-ref", `--format=${format}`, "refs/heads/"], cwd, 10000)).trim();
const branches: GitBranch[] = [];
for (const line of output.trim().split("\n")) {
for (const line of output.split("\n")) {
const parts = line.split("|");
if (parts.length < 4) continue;
@@ -1053,10 +1042,9 @@ export interface GitWorktree {
* Get all git worktrees.
* @param tasks Optional task list to correlate worktrees with tasks
*/
function getGitWorktrees(tasks: { id: string; worktree?: string }[] = [], cwd?: string): GitWorktree[] {
async function getGitWorktrees(tasks: { id: string; worktree?: string }[] = [], cwd?: string): Promise<GitWorktree[]> {
try {
const execOptions = { encoding: "utf-8" as const, timeout: 10000, cwd };
const output = execSync("git worktree list --porcelain", execOptions);
const output = await runGitCommand(["worktree", "list", "--porcelain"], cwd, 10000);
const worktrees: GitWorktree[] = [];
let currentWorktree: Partial<GitWorktree> = {};
@@ -1304,10 +1292,9 @@ function isValidGitUrl(url: string): boolean {
* Get all git remotes with their fetch and push URLs.
* Executes `git remote -v` and parses the output.
*/
function listGitRemotes(cwd?: string): GitRemoteDetailed[] {
async function listGitRemotes(cwd?: string): Promise<GitRemoteDetailed[]> {
try {
const execOptions = { encoding: "utf-8" as const, timeout: 5000, cwd };
const output = execSync("git remote -v", execOptions);
const output = await runGitCommand(["remote", "-v"], cwd, 5000);
const remotes = new Map<string, { fetchUrl: string; pushUrl: string }>();
@@ -1457,13 +1444,9 @@ export interface GitFileChange {
/**
* Get list of stash entries.
*/
function getGitStashList(cwd?: string): GitStash[] {
async function getGitStashList(cwd?: string): Promise<GitStash[]> {
try {
const output = execSync('git stash list --format="%gd|%gs|%ai"', {
encoding: "utf-8",
timeout: 5000,
cwd,
}).trim();
const output = (await runGitCommand(["stash", "list", '--format="%gd|%gs|%ai"'], cwd, 5000)).trim();
if (!output) return [];
const stashes: GitStash[] = [];