feat: git initialisation
This commit is contained in:
@@ -7,9 +7,20 @@ const mockStatus = vi.fn();
|
||||
const mockInit = vi.fn();
|
||||
const mockRaw = vi.fn();
|
||||
const mockAdd = vi.fn();
|
||||
const mockCommit = vi.fn();
|
||||
const mockGetRemotes = vi.fn();
|
||||
const mockAddRemote = vi.fn();
|
||||
const mockRemote = vi.fn();
|
||||
const { mockReadFile, mockStat } = vi.hoisted(() => ({
|
||||
mockReadFile: vi.fn(),
|
||||
mockStat: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('fs/promises', () => ({
|
||||
readFile: mockReadFile,
|
||||
stat: mockStat,
|
||||
default: {},
|
||||
}));
|
||||
|
||||
vi.mock('simple-git', () => ({
|
||||
simpleGit: vi.fn(() => ({
|
||||
@@ -20,6 +31,7 @@ vi.mock('simple-git', () => ({
|
||||
init: mockInit,
|
||||
raw: mockRaw,
|
||||
add: mockAdd,
|
||||
commit: mockCommit,
|
||||
getRemotes: mockGetRemotes,
|
||||
addRemote: mockAddRemote,
|
||||
remote: mockRemote,
|
||||
@@ -33,6 +45,9 @@ describe('GitEngine', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockReadFile.mockRejectedValue(new Error('ENOENT'));
|
||||
mockStat.mockResolvedValue({});
|
||||
mockCheckIsRepo.mockResolvedValue(false);
|
||||
gitEngine = new GitEngine();
|
||||
});
|
||||
|
||||
@@ -122,18 +137,114 @@ describe('GitEngine', () => {
|
||||
});
|
||||
|
||||
describe('initializeRepo', () => {
|
||||
it('should initialize git repo, configure lfs and track image patterns', async () => {
|
||||
it('should emit detailed progress updates throughout initialization', async () => {
|
||||
mockVersion.mockResolvedValue({ major: 2, minor: 49, patch: 0, agent: 'git/version', installed: true });
|
||||
mockInit.mockResolvedValue(undefined);
|
||||
mockRaw.mockResolvedValue('ok');
|
||||
mockAdd.mockResolvedValue(undefined);
|
||||
mockCommit.mockResolvedValue(undefined);
|
||||
mockGetRemotes.mockResolvedValue([]);
|
||||
mockAddRemote.mockResolvedValue(undefined);
|
||||
|
||||
const onProgress = vi.fn();
|
||||
const result = await gitEngine.initializeRepo('/tmp/project', 'https://github.com/example/repo.git', onProgress);
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(onProgress).toHaveBeenCalled();
|
||||
expect(onProgress).toHaveBeenCalledWith(expect.objectContaining({ phase: 'checking-git' }));
|
||||
expect(onProgress).toHaveBeenCalledWith(expect.objectContaining({ phase: 'initializing-repo' }));
|
||||
expect(onProgress).toHaveBeenCalledWith(expect.objectContaining({ phase: 'configuring-lfs' }));
|
||||
expect(onProgress).toHaveBeenCalledWith(expect.objectContaining({ phase: 'tracking-lfs-patterns' }));
|
||||
expect(onProgress).toHaveBeenCalledWith(expect.objectContaining({ phase: 'staging-files' }));
|
||||
expect(onProgress).toHaveBeenCalledWith(expect.objectContaining({ phase: 'creating-initial-commit' }));
|
||||
expect(onProgress).toHaveBeenCalledWith(expect.objectContaining({ phase: 'configuring-remote' }));
|
||||
expect(onProgress).toHaveBeenCalledWith(expect.objectContaining({ phase: 'completed', progress: 100 }));
|
||||
});
|
||||
|
||||
it('should emit failed progress state when initialization fails', async () => {
|
||||
mockVersion.mockResolvedValue({ major: 2, minor: 49, patch: 0, agent: 'git/version', installed: true });
|
||||
mockInit.mockRejectedValue(new Error('init failed'));
|
||||
|
||||
const onProgress = vi.fn();
|
||||
const result = await gitEngine.initializeRepo('/tmp/project', undefined, onProgress);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(onProgress).toHaveBeenCalledWith(expect.objectContaining({ phase: 'failed' }));
|
||||
});
|
||||
|
||||
it('should skip already-completed steps on re-run and still succeed', async () => {
|
||||
mockVersion.mockResolvedValue({ major: 2, minor: 49, patch: 0, agent: 'git/version', installed: true });
|
||||
mockCheckIsRepo.mockResolvedValue(true);
|
||||
mockRaw.mockImplementation(async (args: string[]) => {
|
||||
if (args[0] === 'config' && args[4] === 'filter.lfs.clean') {
|
||||
return 'git-lfs clean -- %f';
|
||||
}
|
||||
if (args[0] === 'rev-parse' && args[2] === 'HEAD') {
|
||||
return 'abc123';
|
||||
}
|
||||
return 'ok';
|
||||
});
|
||||
mockReadFile.mockResolvedValue(
|
||||
'*.png filter=lfs diff=lfs merge=lfs -text\n*.jpg filter=lfs diff=lfs merge=lfs -text\n',
|
||||
);
|
||||
mockGetRemotes.mockResolvedValue([{ name: 'origin', refs: { fetch: 'https://github.com/example/repo.git', push: 'https://github.com/example/repo.git' } }]);
|
||||
|
||||
const onProgress = vi.fn();
|
||||
const result = await gitEngine.initializeRepo('/tmp/project', 'https://github.com/example/repo.git', onProgress);
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(mockInit).not.toHaveBeenCalled();
|
||||
expect(mockCommit).not.toHaveBeenCalled();
|
||||
expect(mockAddRemote).not.toHaveBeenCalled();
|
||||
expect(mockRemote).not.toHaveBeenCalled();
|
||||
expect(onProgress).toHaveBeenCalledWith(expect.objectContaining({ phase: 'initializing-repo', detail: 'already initialized' }));
|
||||
expect(onProgress).toHaveBeenCalledWith(expect.objectContaining({ phase: 'configuring-lfs', detail: 'already configured' }));
|
||||
expect(onProgress).toHaveBeenCalledWith(expect.objectContaining({ phase: 'creating-initial-commit', detail: 'already has commits' }));
|
||||
expect(onProgress).toHaveBeenCalledWith(expect.objectContaining({ phase: 'configuring-remote', detail: 'already up to date' }));
|
||||
});
|
||||
|
||||
it('should update existing origin remote when URL differs', async () => {
|
||||
mockVersion.mockResolvedValue({ major: 2, minor: 49, patch: 0, agent: 'git/version', installed: true });
|
||||
mockCheckIsRepo.mockResolvedValue(true);
|
||||
mockRaw.mockImplementation(async (args: string[]) => {
|
||||
if (args[0] === 'config' && args[4] === 'filter.lfs.clean') {
|
||||
return 'git-lfs clean -- %f';
|
||||
}
|
||||
if (args[0] === 'rev-parse' && args[2] === 'HEAD') {
|
||||
return 'abc123';
|
||||
}
|
||||
return 'ok';
|
||||
});
|
||||
mockReadFile.mockResolvedValue('*.png filter=lfs diff=lfs merge=lfs -text\n');
|
||||
mockGetRemotes.mockResolvedValue([{ name: 'origin', refs: { fetch: 'https://github.com/old/repo.git', push: 'https://github.com/old/repo.git' } }]);
|
||||
|
||||
const result = await gitEngine.initializeRepo('/tmp/project', 'https://github.com/new/repo.git');
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(mockRemote).toHaveBeenCalledWith(['set-url', 'origin', 'https://github.com/new/repo.git']);
|
||||
});
|
||||
|
||||
it('should initialize git repo, configure lfs and track image patterns', async () => {
|
||||
mockVersion.mockResolvedValue({ major: 2, minor: 49, patch: 0, agent: 'git/version', installed: true });
|
||||
mockInit.mockResolvedValue(undefined);
|
||||
mockRaw.mockImplementation(async (args: string[]) => {
|
||||
if (args[0] === 'config' && args[3] === 'filter.lfs.clean') {
|
||||
throw new Error('unset');
|
||||
}
|
||||
if (args[0] === 'rev-parse' && args[2] === 'HEAD') {
|
||||
throw new Error('no commits');
|
||||
}
|
||||
return 'ok';
|
||||
});
|
||||
mockAdd.mockResolvedValue(undefined);
|
||||
mockCommit.mockResolvedValue(undefined);
|
||||
|
||||
const result = await gitEngine.initializeRepo('/tmp/project');
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(mockInit).toHaveBeenCalled();
|
||||
expect(mockRaw).toHaveBeenCalledWith(['lfs', 'install', '--local']);
|
||||
expect(mockAdd).toHaveBeenCalledWith('.gitattributes');
|
||||
expect(mockCommit).toHaveBeenCalledWith('initial commit');
|
||||
expect(mockAddRemote).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -142,6 +253,7 @@ describe('GitEngine', () => {
|
||||
mockInit.mockResolvedValue(undefined);
|
||||
mockRaw.mockResolvedValue('ok');
|
||||
mockAdd.mockResolvedValue(undefined);
|
||||
mockCommit.mockResolvedValue(undefined);
|
||||
mockGetRemotes.mockResolvedValue([]);
|
||||
mockAddRemote.mockResolvedValue(undefined);
|
||||
|
||||
@@ -152,6 +264,18 @@ describe('GitEngine', () => {
|
||||
expect(mockAddRemote).toHaveBeenCalledWith('origin', 'https://github.com/example/repo.git');
|
||||
});
|
||||
|
||||
it('should succeed when there is nothing to commit after staging', async () => {
|
||||
mockVersion.mockResolvedValue({ major: 2, minor: 49, patch: 0, agent: 'git/version', installed: true });
|
||||
mockInit.mockResolvedValue(undefined);
|
||||
mockRaw.mockResolvedValue('ok');
|
||||
mockAdd.mockResolvedValue(undefined);
|
||||
mockCommit.mockRejectedValue(new Error('nothing to commit, working tree clean'));
|
||||
|
||||
const result = await gitEngine.initializeRepo('/tmp/project');
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
|
||||
it('should return explicit git missing guidance when git is unavailable', async () => {
|
||||
mockVersion.mockRejectedValue(new Error('git: command not found'));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user