diff --git a/src/utils/git.ts b/src/utils/git.ts index bd043c4..30a0941 100644 --- a/src/utils/git.ts +++ b/src/utils/git.ts @@ -1,3 +1,4 @@ +import { spawnSync } from 'child_process'; import fs from 'fs'; import git from 'isomorphic-git'; import path from 'path'; @@ -10,6 +11,14 @@ export interface CommitInfo { origin: string; } +export function getCurrentCommit() { + const result = spawnSync('git', ['rev-parse', 'HEAD']); + if (result.status !== 0) { + throw new Error('Not a git repository'); + } + return result.stdout.toString().trim(); +} + function findGitRoot(dir = process.cwd()) { const gitRoot = fs.readdirSync(dir).find((dir) => dir === '.git'); if (gitRoot) { diff --git a/tests/git.test.ts b/tests/git.test.ts new file mode 100644 index 0000000..3609ef7 --- /dev/null +++ b/tests/git.test.ts @@ -0,0 +1,111 @@ +import { describe, expect, it, mock } from 'bun:test'; + +// Define the mock functions so we can manipulate them in tests +const spawnSyncMock = mock(() => ({ + status: 0, + stdout: Buffer.from('mock-hash-123\n'), +})); + +mock.module('child_process', () => ({ + spawnSync: spawnSyncMock, +})); + +const listRemotesMock = mock(async () => [ + { remote: 'origin', url: 'https://github.com/test/repo.git' }, +]); +const logMock = mock(async () => [ + { + oid: 'mock-commit-hash', + commit: { + message: 'mock commit message', + author: { name: 'Test Author' }, + committer: { name: 'Test Committer', timestamp: 1625097600 }, + }, + }, +]); + +mock.module('isomorphic-git', () => ({ + default: { + listRemotes: listRemotesMock, + log: logMock, + }, +})); + +describe('git utils', async () => { + const { getCommitInfo, getCurrentCommit } = await import('../src/utils/git'); + + describe('getCurrentCommit', () => { + it('should return the commit hash when git command succeeds', () => { + spawnSyncMock.mockImplementationOnce(() => ({ + status: 0, + stdout: Buffer.from('abcdef1234567890\n'), + })); + + const commit = getCurrentCommit(); + expect(commit).toBe('abcdef1234567890'); + expect(spawnSyncMock).toHaveBeenCalledWith('git', ['rev-parse', 'HEAD']); + }); + + it('should throw an error when git command fails', () => { + spawnSyncMock.mockImplementationOnce(() => ({ + status: 128, + stdout: Buffer.from(''), + stderr: Buffer.from( + 'fatal: not a git repository (or any of the parent directories): .git\n', + ), + })); + + expect(() => getCurrentCommit()).toThrow('Not a git repository'); + expect(spawnSyncMock).toHaveBeenCalledWith('git', ['rev-parse', 'HEAD']); + }); + }); + + describe('getCommitInfo', () => { + it('should return correct commit info', async () => { + const info = await getCommitInfo(); + + expect(info).toBeDefined(); + expect(info?.hash).toBe('mock-commit-hash'); + expect(info?.message).toBe('mock commit message'); + expect(info?.author).toBe('Test Author'); + expect(info?.timestamp).toBe('1625097600'); + expect(info?.origin).toBe('https://github.com/test/repo.git'); + + expect(listRemotesMock).toHaveBeenCalled(); + expect(logMock).toHaveBeenCalled(); + }); + + it('should handle missing author name by falling back to committer name', async () => { + logMock.mockImplementationOnce(async () => [ + { + oid: 'mock-commit-hash-2', + commit: { + message: 'another message', + author: { name: '' }, + committer: { name: 'Fallback Committer', timestamp: 1625098000 }, + }, + }, + ]); + + const info = await getCommitInfo(); + expect(info?.author).toBe('Fallback Committer'); + }); + + it('should return undefined and log error when git operations fail', async () => { + const originalConsoleError = console.error; + const consoleErrorMock = mock(); + console.error = consoleErrorMock; + + listRemotesMock.mockImplementationOnce(async () => { + throw new Error('Git operation failed'); + }); + + const info = await getCommitInfo(); + + expect(info).toBeUndefined(); + expect(consoleErrorMock).toHaveBeenCalled(); + + console.error = originalConsoleError; + }); + }); +});