gitLogHandle.test.ts•9.26 kB
import { beforeEach, describe, expect, test, vi } from 'vitest';
import type { RepomixConfigMerged } from '../../../src/config/configSchema.js';
import {
GIT_LOG_FORMAT_SEPARATOR,
GIT_LOG_RECORD_SEPARATOR,
getGitLog,
getGitLogs,
} from '../../../src/core/git/gitLogHandle.js';
import { RepomixError } from '../../../src/shared/errorHandle.js';
import { logger } from '../../../src/shared/logger.js';
vi.mock('../../../src/shared/logger');
describe('gitLogHandle', () => {
beforeEach(() => {
vi.resetAllMocks();
});
describe('getGitLog', () => {
test('should return git log content when directory is a git repository', async () => {
const mockExecGitLog = vi.fn().mockResolvedValue('mock log content');
const mockIsGitRepository = vi.fn().mockResolvedValue(true);
const result = await getGitLog('/test/dir', 10, {
execGitLog: mockExecGitLog,
isGitRepository: mockIsGitRepository,
});
expect(result).toBe('mock log content');
expect(mockIsGitRepository).toHaveBeenCalledWith('/test/dir');
expect(mockExecGitLog).toHaveBeenCalledWith('/test/dir', 10, GIT_LOG_FORMAT_SEPARATOR);
});
test('should return empty string when directory is not a git repository', async () => {
const mockExecGitLog = vi.fn();
const mockIsGitRepository = vi.fn().mockResolvedValue(false);
const result = await getGitLog('/test/dir', 10, {
execGitLog: mockExecGitLog,
isGitRepository: mockIsGitRepository,
});
expect(result).toBe('');
expect(mockIsGitRepository).toHaveBeenCalledWith('/test/dir');
expect(mockExecGitLog).not.toHaveBeenCalled();
expect(logger.trace).toHaveBeenCalledWith('Directory /test/dir is not a git repository');
});
test('should throw error when git log command fails', async () => {
const mockError = new Error('git command failed');
const mockExecGitLog = vi.fn().mockRejectedValue(mockError);
const mockIsGitRepository = vi.fn().mockResolvedValue(true);
await expect(
getGitLog('/test/dir', 10, {
execGitLog: mockExecGitLog,
isGitRepository: mockIsGitRepository,
}),
).rejects.toThrow('git command failed');
expect(logger.trace).toHaveBeenCalledWith('Failed to get git log:', 'git command failed');
});
});
describe('getGitLogs', () => {
test('should return git logs when includeLogs is enabled', async () => {
const mockLogContent = `${GIT_LOG_RECORD_SEPARATOR}2024-01-01 10:00:00 +0900|Initial commit
file1.txt
file2.txt
${GIT_LOG_RECORD_SEPARATOR}2024-01-02 11:00:00 +0900|Add feature
src/feature.ts`;
const mockGetGitLog = vi.fn().mockResolvedValue(mockLogContent);
const config: RepomixConfigMerged = {
cwd: '/project',
output: {
git: {
includeLogs: true,
includeLogsCount: 25,
},
},
} as RepomixConfigMerged;
const result = await getGitLogs(['/project/src'], config, {
getGitLog: mockGetGitLog,
});
expect(result).toEqual({
logContent: mockLogContent,
commits: [
{
date: '2024-01-01 10:00:00 +0900',
message: 'Initial commit',
files: ['file1.txt', 'file2.txt'],
},
{
date: '2024-01-02 11:00:00 +0900',
message: 'Add feature',
files: ['src/feature.ts'],
},
],
});
expect(mockGetGitLog).toHaveBeenCalledWith('/project/src', 25);
});
test('should return undefined when includeLogs is disabled', async () => {
const config: RepomixConfigMerged = {
cwd: '/project',
output: {
git: {
includeLogs: false,
},
},
} as RepomixConfigMerged;
const result = await getGitLogs(['/project/src'], config);
expect(result).toBeUndefined();
});
test('should use default commit count when includeLogsCount is not specified', async () => {
const mockGetGitLog = vi.fn().mockResolvedValue(`${GIT_LOG_RECORD_SEPARATOR}2024-01-01 10:00:00 +0900|Test commit
test.txt`);
const config: RepomixConfigMerged = {
cwd: '/project',
output: {
git: {
includeLogs: true,
},
},
} as RepomixConfigMerged;
await getGitLogs(['/project/src'], config, {
getGitLog: mockGetGitLog,
});
expect(mockGetGitLog).toHaveBeenCalledWith('/project/src', 50);
});
test('should use first directory as git root', async () => {
const mockGetGitLog = vi.fn().mockResolvedValue('');
const config: RepomixConfigMerged = {
cwd: '/fallback',
output: {
git: {
includeLogs: true,
},
},
} as RepomixConfigMerged;
await getGitLogs(['/first/dir', '/second/dir'], config, {
getGitLog: mockGetGitLog,
});
expect(mockGetGitLog).toHaveBeenCalledWith('/first/dir', 50);
});
test('should fallback to config.cwd when no directories provided', async () => {
const mockGetGitLog = vi.fn().mockResolvedValue('');
const config: RepomixConfigMerged = {
cwd: '/fallback',
output: {
git: {
includeLogs: true,
},
},
} as RepomixConfigMerged;
await getGitLogs([], config, {
getGitLog: mockGetGitLog,
});
expect(mockGetGitLog).toHaveBeenCalledWith('/fallback', 50);
});
test('should throw RepomixError when getGitLog fails', async () => {
const mockError = new Error('git failed');
const mockGetGitLog = vi.fn().mockRejectedValue(mockError);
const config: RepomixConfigMerged = {
cwd: '/project',
output: {
git: {
includeLogs: true,
},
},
} as RepomixConfigMerged;
await expect(
getGitLogs(['/project'], config, {
getGitLog: mockGetGitLog,
}),
).rejects.toThrow(RepomixError);
await expect(
getGitLogs(['/project'], config, {
getGitLog: mockGetGitLog,
}),
).rejects.toThrow('Failed to get git logs: git failed');
});
test('should handle empty git log output', async () => {
const mockGetGitLog = vi.fn().mockResolvedValue('');
const config: RepomixConfigMerged = {
cwd: '/project',
output: {
git: {
includeLogs: true,
},
},
} as RepomixConfigMerged;
const result = await getGitLogs(['/project'], config, {
getGitLog: mockGetGitLog,
});
expect(result).toEqual({
logContent: '',
commits: [],
});
});
test('should parse git log correctly with malformed separator content', async () => {
// Test behavior when log content doesn't match expected separator
const malformedLogContent = 'random content without separator';
const mockGetGitLog = vi.fn().mockResolvedValue(malformedLogContent);
const config: RepomixConfigMerged = {
cwd: '/project',
output: {
git: {
includeLogs: true,
},
},
} as RepomixConfigMerged;
const result = await getGitLogs(['/project'], config, {
getGitLog: mockGetGitLog,
});
// Should return empty commits array when content cannot be parsed properly
expect(result?.commits).toEqual([]);
expect(result?.logContent).toBe(malformedLogContent);
});
test('should handle Windows line endings (CRLF) correctly', async () => {
// Test with Windows-style line endings (\r\n)
const mockLogContent = `${GIT_LOG_RECORD_SEPARATOR}2024-01-01 10:00:00 +0900|Windows commit\r\nfile1.txt\r\nfile2.txt`;
const mockGetGitLog = vi.fn().mockResolvedValue(mockLogContent);
const config: RepomixConfigMerged = {
cwd: '/project',
output: {
git: {
includeLogs: true,
},
},
} as RepomixConfigMerged;
const result = await getGitLogs(['/project'], config, {
getGitLog: mockGetGitLog,
});
expect(result?.commits).toEqual([
{
date: '2024-01-01 10:00:00 +0900',
message: 'Windows commit',
files: ['file1.txt', 'file2.txt'],
},
]);
});
test('should handle mixed line endings correctly', async () => {
// Test with mixed Unix (\n) and Windows (\r\n) line endings
const mockLogContent = `${GIT_LOG_RECORD_SEPARATOR}2024-01-01 10:00:00 +0900|Mixed line endings\nfile1.txt\r\nfile2.txt`;
const mockGetGitLog = vi.fn().mockResolvedValue(mockLogContent);
const config: RepomixConfigMerged = {
cwd: '/project',
output: {
git: {
includeLogs: true,
},
},
} as RepomixConfigMerged;
const result = await getGitLogs(['/project'], config, {
getGitLog: mockGetGitLog,
});
expect(result?.commits).toEqual([
{
date: '2024-01-01 10:00:00 +0900',
message: 'Mixed line endings',
files: ['file1.txt', 'file2.txt'],
},
]);
});
});
});