refactor git widgets to use shared git command helpers

Centralize cwd resolution and git command execution for git widgets, and expand widget-level and utility tests for failure and edge cases.

Closes #176
This commit is contained in:
Matthew Breedlove
2026-02-21 14:49:46 -05:00
parent 20691a70b0
commit 3abc41f122
10 changed files with 548 additions and 103 deletions
+137
View File
@@ -0,0 +1,137 @@
import { execSync } from 'child_process';
import {
beforeEach,
describe,
expect,
it,
vi
} from 'vitest';
import type { RenderContext } from '../../types/RenderContext';
import {
isInsideGitWorkTree,
resolveGitCwd,
runGit
} from '../git';
vi.mock('child_process', () => ({ execSync: vi.fn() }));
const mockExecSync = execSync as unknown as {
mock: { calls: unknown[][] };
mockImplementation: (impl: () => never) => void;
mockReturnValue: (value: string) => void;
};
describe('git utils', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('resolveGitCwd', () => {
it('prefers context.data.cwd when available', () => {
const context: RenderContext = {
data: {
cwd: '/repo/from/cwd',
workspace: {
current_dir: '/repo/from/current-dir',
project_dir: '/repo/from/project-dir'
}
}
};
expect(resolveGitCwd(context)).toBe('/repo/from/cwd');
});
it('falls back to workspace.current_dir', () => {
const context: RenderContext = {
data: {
workspace: {
current_dir: '/repo/from/current-dir',
project_dir: '/repo/from/project-dir'
}
}
};
expect(resolveGitCwd(context)).toBe('/repo/from/current-dir');
});
it('falls back to workspace.project_dir', () => {
const context: RenderContext = { data: { workspace: { project_dir: '/repo/from/project-dir' } } };
expect(resolveGitCwd(context)).toBe('/repo/from/project-dir');
});
it('skips empty candidate values', () => {
const context: RenderContext = {
data: {
cwd: ' ',
workspace: {
current_dir: '',
project_dir: '/repo/from/project-dir'
}
}
};
expect(resolveGitCwd(context)).toBe('/repo/from/project-dir');
});
it('returns undefined when no candidates are available', () => {
expect(resolveGitCwd({})).toBeUndefined();
});
});
describe('runGit', () => {
it('runs git command with resolved cwd and trims output', () => {
mockExecSync.mockReturnValue(' feature/worktree \n');
const context: RenderContext = { data: { cwd: '/tmp/repo' } };
const result = runGit('branch --show-current', context);
expect(result).toBe('feature/worktree');
expect(mockExecSync.mock.calls[0]?.[0]).toBe('git branch --show-current');
expect(mockExecSync.mock.calls[0]?.[1]).toEqual({
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
cwd: '/tmp/repo'
});
});
it('runs git command without cwd when no context directory exists', () => {
mockExecSync.mockReturnValue('true\n');
const result = runGit('rev-parse --is-inside-work-tree', {});
expect(result).toBe('true');
expect(mockExecSync.mock.calls[0]?.[1]).toEqual({
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
});
});
it('returns null when the command fails', () => {
mockExecSync.mockImplementation(() => { throw new Error('git failed'); });
expect(runGit('status --short', {})).toBeNull();
});
});
describe('isInsideGitWorkTree', () => {
it('returns true when git reports true', () => {
mockExecSync.mockReturnValue('true\n');
expect(isInsideGitWorkTree({})).toBe(true);
});
it('returns false when git reports false', () => {
mockExecSync.mockReturnValue('false\n');
expect(isInsideGitWorkTree({})).toBe(false);
});
it('returns false when git command fails', () => {
mockExecSync.mockImplementation(() => { throw new Error('git failed'); });
expect(isInsideGitWorkTree({})).toBe(false);
});
});
});
+38
View File
@@ -0,0 +1,38 @@
import { execSync } from 'child_process';
import type { RenderContext } from '../types/RenderContext';
export function resolveGitCwd(context: RenderContext): string | undefined {
const candidates = [
context.data?.cwd,
context.data?.workspace?.current_dir,
context.data?.workspace?.project_dir
];
for (const candidate of candidates) {
if (typeof candidate === 'string' && candidate.trim().length > 0) {
return candidate;
}
}
return undefined;
}
export function runGit(command: string, context: RenderContext): string | null {
try {
const cwd = resolveGitCwd(context);
const output = execSync(`git ${command}`, {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
...(cwd ? { cwd } : {})
}).trim();
return output.length > 0 ? output : null;
} catch {
return null;
}
}
export function isInsideGitWorkTree(context: RenderContext): boolean {
return runGit('rev-parse --is-inside-work-tree', context) === 'true';
}