mirror of
https://github.com/yamadashy/repomix.git
synced 2025-06-11 00:25:54 +03:00
refactor(core): Update GitDiffResult imports and restructure git handling modules
This commit is contained in:
@@ -3,8 +3,9 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import pc from 'picocolors';
|
||||
import { execGitShallowClone } from '../../core/git/gitCommand.js';
|
||||
import { getRemoteRefs, isGitInstalled } from '../../core/git/gitHandle.js';
|
||||
import { getRemoteRefs } from '../../core/git/gitRemoteHandle.js';
|
||||
import { parseRemoteValue } from '../../core/git/gitRemoteParse.js';
|
||||
import { isGitInstalled } from '../../core/git/gitRepositoryHandle.js';
|
||||
import { RepomixError } from '../../shared/errorHandle.js';
|
||||
import { logger } from '../../shared/logger.js';
|
||||
import { Spinner } from '../cliSpinner.js';
|
||||
|
||||
@@ -1,43 +1,14 @@
|
||||
import type { RepomixConfigMerged } from '../../config/configSchema.js';
|
||||
import { RepomixError } from '../../shared/errorHandle.js';
|
||||
import { logger } from '../../shared/logger.js';
|
||||
import {
|
||||
execGitDiff,
|
||||
execGitLogFilenames,
|
||||
execGitRevParse,
|
||||
execGitVersion,
|
||||
execLsRemote,
|
||||
validateGitUrl,
|
||||
} from './gitCommand.js';
|
||||
import { execGitDiff } from './gitCommand.js';
|
||||
import { isGitRepository } from './gitRepositoryHandle.js';
|
||||
|
||||
export interface GitDiffResult {
|
||||
workTreeDiffContent: string;
|
||||
stagedDiffContent: string;
|
||||
}
|
||||
|
||||
export const getFileChangeCount = async (
|
||||
directory: string,
|
||||
maxCommits = 100,
|
||||
deps = {
|
||||
execGitLogFilenames,
|
||||
},
|
||||
): Promise<Record<string, number>> => {
|
||||
try {
|
||||
const filenames = await deps.execGitLogFilenames(directory, maxCommits);
|
||||
|
||||
const fileChangeCounts: Record<string, number> = {};
|
||||
|
||||
for (const filename of filenames) {
|
||||
fileChangeCounts[filename] = (fileChangeCounts[filename] || 0) + 1;
|
||||
}
|
||||
|
||||
return fileChangeCounts;
|
||||
} catch (error) {
|
||||
logger.trace('Failed to get file change counts:', (error as Error).message);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const getWorkTreeDiff = async (
|
||||
directory: string,
|
||||
deps = {
|
||||
@@ -87,68 +58,6 @@ const getDiff = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const isGitRepository = async (
|
||||
directory: string,
|
||||
deps = {
|
||||
execGitRevParse,
|
||||
},
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
await deps.execGitRevParse(directory);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const isGitInstalled = async (
|
||||
deps = {
|
||||
execGitVersion,
|
||||
},
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
const result = await deps.execGitVersion();
|
||||
return !result.includes('error') && result.includes('git version');
|
||||
} catch (error) {
|
||||
logger.trace('Git is not installed:', (error as Error).message);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getRemoteRefs = async (
|
||||
url: string,
|
||||
deps = {
|
||||
execLsRemote,
|
||||
},
|
||||
): Promise<string[]> => {
|
||||
validateGitUrl(url);
|
||||
|
||||
try {
|
||||
const stdout = await deps.execLsRemote(url);
|
||||
|
||||
// Extract ref names from the output
|
||||
// Format is: hash\tref_name
|
||||
const refs = stdout
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
// Skip the hash part and extract only the ref name
|
||||
const parts = line.split('\t');
|
||||
if (parts.length < 2) return '';
|
||||
|
||||
// Remove 'refs/heads/' or 'refs/tags/' prefix
|
||||
return parts[1].replace(/^refs\/(heads|tags)\//, '');
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
logger.trace(`Found ${refs.length} refs in repository: ${url}`);
|
||||
return refs;
|
||||
} catch (error) {
|
||||
logger.trace('Failed to get remote refs:', (error as Error).message);
|
||||
throw new RepomixError(`Failed to get remote refs: ${(error as Error).message}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const getGitDiffs = async (
|
||||
rootDirs: string[],
|
||||
config: RepomixConfigMerged,
|
||||
37
src/core/git/gitRemoteHandle.ts
Normal file
37
src/core/git/gitRemoteHandle.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { RepomixError } from '../../shared/errorHandle.js';
|
||||
import { logger } from '../../shared/logger.js';
|
||||
import { execLsRemote, validateGitUrl } from './gitCommand.js';
|
||||
|
||||
export const getRemoteRefs = async (
|
||||
url: string,
|
||||
deps = {
|
||||
execLsRemote,
|
||||
},
|
||||
): Promise<string[]> => {
|
||||
validateGitUrl(url);
|
||||
|
||||
try {
|
||||
const stdout = await deps.execLsRemote(url);
|
||||
|
||||
// Extract ref names from the output
|
||||
// Format is: hash\tref_name
|
||||
const refs = stdout
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
// Skip the hash part and extract only the ref name
|
||||
const parts = line.split('\t');
|
||||
if (parts.length < 2) return '';
|
||||
|
||||
// Remove 'refs/heads/' or 'refs/tags/' prefix
|
||||
return parts[1].replace(/^refs\/(heads|tags)\//, '');
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
logger.trace(`Found ${refs.length} refs in repository: ${url}`);
|
||||
return refs;
|
||||
} catch (error) {
|
||||
logger.trace('Failed to get remote refs:', (error as Error).message);
|
||||
throw new RepomixError(`Failed to get remote refs: ${(error as Error).message}`);
|
||||
}
|
||||
};
|
||||
53
src/core/git/gitRepositoryHandle.ts
Normal file
53
src/core/git/gitRepositoryHandle.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { logger } from '../../shared/logger.js';
|
||||
import { execGitLogFilenames, execGitRevParse, execGitVersion } from './gitCommand.js';
|
||||
|
||||
export const getFileChangeCount = async (
|
||||
directory: string,
|
||||
maxCommits = 100,
|
||||
deps = {
|
||||
execGitLogFilenames,
|
||||
},
|
||||
): Promise<Record<string, number>> => {
|
||||
try {
|
||||
const filenames = await deps.execGitLogFilenames(directory, maxCommits);
|
||||
|
||||
const fileChangeCounts: Record<string, number> = {};
|
||||
|
||||
for (const filename of filenames) {
|
||||
fileChangeCounts[filename] = (fileChangeCounts[filename] || 0) + 1;
|
||||
}
|
||||
|
||||
return fileChangeCounts;
|
||||
} catch (error) {
|
||||
logger.trace('Failed to get file change counts:', (error as Error).message);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const isGitRepository = async (
|
||||
directory: string,
|
||||
deps = {
|
||||
execGitRevParse,
|
||||
},
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
await deps.execGitRevParse(directory);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const isGitInstalled = async (
|
||||
deps = {
|
||||
execGitVersion,
|
||||
},
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
const result = await deps.execGitVersion();
|
||||
return !result.includes('error') && result.includes('git version');
|
||||
} catch (error) {
|
||||
logger.trace('Git is not installed:', (error as Error).message);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { RepomixConfigMerged } from '../../config/configSchema.js';
|
||||
import type { RepomixProgressCallback } from '../../shared/types.js';
|
||||
import type { ProcessedFile } from '../file/fileTypes.js';
|
||||
import type { GitDiffResult } from '../git/gitHandle.js';
|
||||
import type { GitDiffResult } from '../git/gitDiffHandle.js';
|
||||
import { calculateAllFileMetrics } from './calculateAllFileMetrics.js';
|
||||
import { calculateOutputMetrics } from './calculateOutputMetrics.js';
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { RepomixError } from '../../shared/errorHandle.js';
|
||||
import { type FileSearchResult, searchFiles } from '../file/fileSearch.js';
|
||||
import { generateTreeString } from '../file/fileTreeGenerate.js';
|
||||
import type { ProcessedFile } from '../file/fileTypes.js';
|
||||
import type { GitDiffResult } from '../git/gitHandle.js';
|
||||
import type { GitDiffResult } from '../git/gitDiffHandle.js';
|
||||
import type { OutputGeneratorContext, RenderContext } from './outputGeneratorTypes.js';
|
||||
import { sortOutputFiles } from './outputSort.js';
|
||||
import {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { RepomixConfigMerged } from '../../config/configSchema.js';
|
||||
import type { ProcessedFile } from '../file/fileTypes.js';
|
||||
import type { GitDiffResult } from '../git/gitHandle.js';
|
||||
import type { GitDiffResult } from '../git/gitDiffHandle.js';
|
||||
|
||||
export interface OutputGeneratorContext {
|
||||
generationDate: string;
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from 'node:path';
|
||||
import type { RepomixConfigMerged } from '../../config/configSchema.js';
|
||||
import { logger } from '../../shared/logger.js';
|
||||
import type { ProcessedFile } from '../file/fileTypes.js';
|
||||
import { getFileChangeCount, isGitInstalled } from '../git/gitHandle.js';
|
||||
import { getFileChangeCount, isGitInstalled } from '../git/gitRepositoryHandle.js';
|
||||
|
||||
// Sort files by git change count for output
|
||||
export const sortOutputFiles = async (
|
||||
|
||||
@@ -5,7 +5,7 @@ import { sortPaths } from './file/filePathSort.js';
|
||||
import { processFiles } from './file/fileProcess.js';
|
||||
import { searchFiles } from './file/fileSearch.js';
|
||||
import type { RawFile } from './file/fileTypes.js';
|
||||
import { GitDiffResult, getGitDiffs } from './git/gitHandle.js';
|
||||
import { GitDiffResult, getGitDiffs } from './git/gitDiffHandle.js';
|
||||
import { calculateMetrics } from './metrics/calculateMetrics.js';
|
||||
import { generateOutput } from './output/outputGenerate.js';
|
||||
import { copyToClipboardIfEnabled } from './packager/copyToClipboardIfEnabled.js';
|
||||
|
||||
@@ -3,7 +3,7 @@ import { logger } from '../../shared/logger.js';
|
||||
import { initPiscina } from '../../shared/processConcurrency.js';
|
||||
import type { RepomixProgressCallback } from '../../shared/types.js';
|
||||
import type { RawFile } from '../file/fileTypes.js';
|
||||
import type { GitDiffResult } from '../git/gitHandle.js';
|
||||
import type { GitDiffResult } from '../git/gitDiffHandle.js';
|
||||
import type { SecurityCheckTask, SecurityCheckType } from './workers/securityCheckWorker.js';
|
||||
|
||||
export interface SuspiciousFileResult {
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { RepomixConfigMerged } from '../../config/configSchema.js';
|
||||
import { logger } from '../../shared/logger.js';
|
||||
import type { RepomixProgressCallback } from '../../shared/types.js';
|
||||
import type { ProcessedFile, RawFile } from '../file/fileTypes.js';
|
||||
import type { GitDiffResult } from '../git/gitHandle.js';
|
||||
import type { GitDiffResult } from '../git/gitDiffHandle.js';
|
||||
import { filterOutUntrustedFiles } from './filterOutUntrustedFiles.js';
|
||||
import { type SuspiciousFileResult, runSecurityCheck } from './securityCheck.js';
|
||||
|
||||
|
||||
@@ -1,64 +1,15 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import {
|
||||
getFileChangeCount,
|
||||
getGitDiffs,
|
||||
getRemoteRefs,
|
||||
getStagedDiff,
|
||||
getWorkTreeDiff,
|
||||
isGitInstalled,
|
||||
isGitRepository,
|
||||
} from '../../../src/core/git/gitHandle.js';
|
||||
import { getGitDiffs, getStagedDiff, getWorkTreeDiff } from '../../../src/core/git/gitDiffHandle.js';
|
||||
import { logger } from '../../../src/shared/logger.js';
|
||||
import { createMockConfig } from '../../testing/testUtils.js';
|
||||
|
||||
vi.mock('../../../src/shared/logger');
|
||||
|
||||
describe('gitHandle', () => {
|
||||
describe('gitDiffHandle', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('getFileChangeCount', () => {
|
||||
test('should count file changes correctly', async () => {
|
||||
const mockFilenames = ['file1.ts', 'file2.ts', 'file1.ts', 'file3.ts', 'file2.ts'];
|
||||
|
||||
const mockExecGitLogFilenames = vi.fn().mockResolvedValue(mockFilenames);
|
||||
|
||||
const result = await getFileChangeCount('/test/dir', 5, {
|
||||
execGitLogFilenames: mockExecGitLogFilenames,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
'file1.ts': 2,
|
||||
'file2.ts': 2,
|
||||
'file3.ts': 1,
|
||||
});
|
||||
expect(mockExecGitLogFilenames).toHaveBeenCalledWith('/test/dir', 5);
|
||||
});
|
||||
|
||||
test('should return empty object when git command fails', async () => {
|
||||
const mockExecGitLogFilenames = vi.fn().mockRejectedValue(new Error('git command failed'));
|
||||
|
||||
const result = await getFileChangeCount('/test/dir', 5, {
|
||||
execGitLogFilenames: mockExecGitLogFilenames,
|
||||
});
|
||||
|
||||
expect(result).toEqual({});
|
||||
expect(logger.trace).toHaveBeenCalledWith('Failed to get file change counts:', 'git command failed');
|
||||
});
|
||||
|
||||
test('should handle empty git log output', async () => {
|
||||
const mockExecGitLogFilenames = vi.fn().mockResolvedValue([]);
|
||||
|
||||
const result = await getFileChangeCount('/test/dir', 5, {
|
||||
execGitLogFilenames: mockExecGitLogFilenames,
|
||||
});
|
||||
|
||||
expect(result).toEqual({});
|
||||
expect(mockExecGitLogFilenames).toHaveBeenCalledWith('/test/dir', 5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWorkTreeDiff', () => {
|
||||
test('should return diffs when directory is a git repository', async () => {
|
||||
const mockDiff = 'diff --git a/file.txt b/file.txt\n+new line';
|
||||
@@ -122,116 +73,6 @@ describe('gitHandle', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('isGitRepository', () => {
|
||||
test('should return true when directory is a git repository', async () => {
|
||||
const mockExecGitRevParse = vi.fn().mockResolvedValue('true');
|
||||
|
||||
const result = await isGitRepository('/test/dir', {
|
||||
execGitRevParse: mockExecGitRevParse,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockExecGitRevParse).toHaveBeenCalledWith('/test/dir');
|
||||
});
|
||||
|
||||
test('should return false when directory is not a git repository', async () => {
|
||||
const mockExecGitRevParse = vi.fn().mockRejectedValue(new Error('Not a git repository'));
|
||||
|
||||
const result = await isGitRepository('/test/dir', {
|
||||
execGitRevParse: mockExecGitRevParse,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockExecGitRevParse).toHaveBeenCalledWith('/test/dir');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isGitInstalled', () => {
|
||||
test('should return true when git is installed', async () => {
|
||||
const mockExecGitVersion = vi.fn().mockResolvedValue('git version 2.34.1');
|
||||
|
||||
const result = await isGitInstalled({
|
||||
execGitVersion: mockExecGitVersion,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockExecGitVersion).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should return false when git command fails', async () => {
|
||||
const mockExecGitVersion = vi.fn().mockRejectedValue(new Error('Command not found: git'));
|
||||
|
||||
const result = await isGitInstalled({
|
||||
execGitVersion: mockExecGitVersion,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockExecGitVersion).toHaveBeenCalled();
|
||||
expect(logger.trace).toHaveBeenCalledWith('Git is not installed:', 'Command not found: git');
|
||||
});
|
||||
|
||||
test('should return false when git version output contains error', async () => {
|
||||
const mockExecGitVersion = vi.fn().mockResolvedValue('error: git not found');
|
||||
|
||||
const result = await isGitInstalled({
|
||||
execGitVersion: mockExecGitVersion,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockExecGitVersion).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRemoteRefs', () => {
|
||||
test('should return refs when URL is valid', async () => {
|
||||
const mockOutput = `
|
||||
a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6\trefs/heads/main
|
||||
b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7\trefs/heads/develop
|
||||
c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8\trefs/tags/v1.0.0
|
||||
`.trim();
|
||||
const mockExecLsRemote = vi.fn().mockResolvedValue(mockOutput);
|
||||
|
||||
const result = await getRemoteRefs('https://github.com/user/repo.git', {
|
||||
execLsRemote: mockExecLsRemote,
|
||||
});
|
||||
|
||||
expect(result).toEqual(['main', 'develop', 'v1.0.0']);
|
||||
expect(mockExecLsRemote).toHaveBeenCalledWith('https://github.com/user/repo.git');
|
||||
});
|
||||
|
||||
test('should throw error when URL does not start with git@ or https://', async () => {
|
||||
const mockExecLsRemote = vi.fn();
|
||||
|
||||
await expect(getRemoteRefs('invalid-url', { execLsRemote: mockExecLsRemote })).rejects.toThrow(
|
||||
"Invalid URL protocol for 'invalid-url'. URL must start with 'git@' or 'https://'",
|
||||
);
|
||||
|
||||
expect(mockExecLsRemote).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should throw error when URL contains dangerous parameters', async () => {
|
||||
const mockExecLsRemote = vi.fn();
|
||||
|
||||
await expect(
|
||||
getRemoteRefs('https://github.com/user/repo.git --upload-pack=evil-command', {
|
||||
execLsRemote: mockExecLsRemote,
|
||||
}),
|
||||
).rejects.toThrow('Invalid repository URL. URL contains potentially dangerous parameters');
|
||||
|
||||
expect(mockExecLsRemote).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should throw error when git command fails', async () => {
|
||||
const mockExecLsRemote = vi.fn().mockRejectedValue(new Error('git command failed'));
|
||||
|
||||
await expect(
|
||||
getRemoteRefs('https://github.com/user/repo.git', { execLsRemote: mockExecLsRemote }),
|
||||
).rejects.toThrow('Failed to get remote refs: git command failed');
|
||||
|
||||
expect(mockExecLsRemote).toHaveBeenCalledWith('https://github.com/user/repo.git');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGitDiffs', () => {
|
||||
test('should return git diffs when includeDiffs is enabled', async () => {
|
||||
const mockWorkTreeDiff = 'diff --git a/file.txt b/file.txt\n+new line';
|
||||
@@ -308,6 +149,7 @@ c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8\trefs/tags/v1.0.0
|
||||
workTreeDiffContent: mockWorkTreeDiff,
|
||||
stagedDiffContent: mockStagedDiff,
|
||||
});
|
||||
// createMockConfig sets cwd to the actual working directory, so we check the actual config value
|
||||
expect(mockGetWorkTreeDiff).toHaveBeenCalledWith(mockConfig.cwd);
|
||||
expect(mockGetStagedDiff).toHaveBeenCalledWith(mockConfig.cwd);
|
||||
});
|
||||
88
tests/core/git/gitRemoteHandle.test.ts
Normal file
88
tests/core/git/gitRemoteHandle.test.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { getRemoteRefs } from '../../../src/core/git/gitRemoteHandle.js';
|
||||
import { logger } from '../../../src/shared/logger.js';
|
||||
|
||||
vi.mock('../../../src/shared/logger');
|
||||
|
||||
describe('gitRemoteHandle', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('getRemoteRefs', () => {
|
||||
test('should return refs when URL is valid', async () => {
|
||||
const mockOutput = `
|
||||
a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 refs/heads/main
|
||||
b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7 refs/heads/develop
|
||||
c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8 refs/tags/v1.0.0
|
||||
`.trim();
|
||||
const mockExecLsRemote = vi.fn().mockResolvedValue(mockOutput);
|
||||
|
||||
const result = await getRemoteRefs('https://github.com/user/repo.git', {
|
||||
execLsRemote: mockExecLsRemote,
|
||||
});
|
||||
|
||||
expect(result).toEqual(['main', 'develop', 'v1.0.0']);
|
||||
expect(mockExecLsRemote).toHaveBeenCalledWith('https://github.com/user/repo.git');
|
||||
expect(logger.trace).toHaveBeenCalledWith('Found 3 refs in repository: https://github.com/user/repo.git');
|
||||
});
|
||||
|
||||
test('should return empty array when no refs found', async () => {
|
||||
const mockExecLsRemote = vi.fn().mockResolvedValue('');
|
||||
|
||||
const result = await getRemoteRefs('https://github.com/user/repo.git', {
|
||||
execLsRemote: mockExecLsRemote,
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(mockExecLsRemote).toHaveBeenCalledWith('https://github.com/user/repo.git');
|
||||
expect(logger.trace).toHaveBeenCalledWith('Found 0 refs in repository: https://github.com/user/repo.git');
|
||||
});
|
||||
|
||||
test('should throw error when ls-remote fails', async () => {
|
||||
const mockExecLsRemote = vi.fn().mockRejectedValue(new Error('Repository not found'));
|
||||
|
||||
await expect(
|
||||
getRemoteRefs('https://github.com/user/nonexistent.git', {
|
||||
execLsRemote: mockExecLsRemote,
|
||||
}),
|
||||
).rejects.toThrow('Failed to get remote refs: Repository not found');
|
||||
|
||||
expect(mockExecLsRemote).toHaveBeenCalledWith('https://github.com/user/nonexistent.git');
|
||||
expect(logger.trace).toHaveBeenCalledWith('Failed to get remote refs:', 'Repository not found');
|
||||
});
|
||||
|
||||
test('should handle malformed output lines', async () => {
|
||||
const mockOutput = `
|
||||
a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 refs/heads/main
|
||||
invalid-line-without-tab
|
||||
b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7 refs/heads/develop
|
||||
`.trim();
|
||||
const mockExecLsRemote = vi.fn().mockResolvedValue(mockOutput);
|
||||
|
||||
const result = await getRemoteRefs('https://github.com/user/repo.git', {
|
||||
execLsRemote: mockExecLsRemote,
|
||||
});
|
||||
|
||||
expect(result).toEqual(['main', 'develop']);
|
||||
expect(mockExecLsRemote).toHaveBeenCalledWith('https://github.com/user/repo.git');
|
||||
expect(logger.trace).toHaveBeenCalledWith('Found 2 refs in repository: https://github.com/user/repo.git');
|
||||
});
|
||||
|
||||
test('should throw error for invalid URL', async () => {
|
||||
await expect(
|
||||
getRemoteRefs('invalid-url', {
|
||||
execLsRemote: vi.fn(),
|
||||
}),
|
||||
).rejects.toThrow('Invalid URL protocol');
|
||||
});
|
||||
|
||||
test('should throw error for dangerous URL parameters', async () => {
|
||||
await expect(
|
||||
getRemoteRefs('https://github.com/user/repo.git --upload-pack=evil', {
|
||||
execLsRemote: vi.fn(),
|
||||
}),
|
||||
).rejects.toThrow('Invalid repository URL. URL contains potentially dangerous parameters');
|
||||
});
|
||||
});
|
||||
});
|
||||
112
tests/core/git/gitRepositoryHandle.test.ts
Normal file
112
tests/core/git/gitRepositoryHandle.test.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { getFileChangeCount, isGitInstalled, isGitRepository } from '../../../src/core/git/gitRepositoryHandle.js';
|
||||
import { logger } from '../../../src/shared/logger.js';
|
||||
|
||||
vi.mock('../../../src/shared/logger');
|
||||
|
||||
describe('gitRepositoryHandle', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('getFileChangeCount', () => {
|
||||
test('should count file changes correctly', async () => {
|
||||
const mockFilenames = ['file1.ts', 'file2.ts', 'file1.ts', 'file3.ts', 'file2.ts'];
|
||||
|
||||
const mockExecGitLogFilenames = vi.fn().mockResolvedValue(mockFilenames);
|
||||
|
||||
const result = await getFileChangeCount('/test/dir', 5, {
|
||||
execGitLogFilenames: mockExecGitLogFilenames,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
'file1.ts': 2,
|
||||
'file2.ts': 2,
|
||||
'file3.ts': 1,
|
||||
});
|
||||
expect(mockExecGitLogFilenames).toHaveBeenCalledWith('/test/dir', 5);
|
||||
});
|
||||
|
||||
test('should return empty object when git command fails', async () => {
|
||||
const mockExecGitLogFilenames = vi.fn().mockRejectedValue(new Error('git command failed'));
|
||||
|
||||
const result = await getFileChangeCount('/test/dir', 5, {
|
||||
execGitLogFilenames: mockExecGitLogFilenames,
|
||||
});
|
||||
|
||||
expect(result).toEqual({});
|
||||
expect(logger.trace).toHaveBeenCalledWith('Failed to get file change counts:', 'git command failed');
|
||||
});
|
||||
|
||||
test('should handle empty git log output', async () => {
|
||||
const mockExecGitLogFilenames = vi.fn().mockResolvedValue([]);
|
||||
|
||||
const result = await getFileChangeCount('/test/dir', 5, {
|
||||
execGitLogFilenames: mockExecGitLogFilenames,
|
||||
});
|
||||
|
||||
expect(result).toEqual({});
|
||||
expect(mockExecGitLogFilenames).toHaveBeenCalledWith('/test/dir', 5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isGitRepository', () => {
|
||||
test('should return true when directory is a git repository', async () => {
|
||||
const mockExecGitRevParse = vi.fn().mockResolvedValue('true');
|
||||
|
||||
const result = await isGitRepository('/test/dir', {
|
||||
execGitRevParse: mockExecGitRevParse,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockExecGitRevParse).toHaveBeenCalledWith('/test/dir');
|
||||
});
|
||||
|
||||
test('should return false when directory is not a git repository', async () => {
|
||||
const mockExecGitRevParse = vi.fn().mockRejectedValue(new Error('Not a git repository'));
|
||||
|
||||
const result = await isGitRepository('/test/dir', {
|
||||
execGitRevParse: mockExecGitRevParse,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockExecGitRevParse).toHaveBeenCalledWith('/test/dir');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isGitInstalled', () => {
|
||||
test('should return true when git is installed', async () => {
|
||||
const mockExecGitVersion = vi.fn().mockResolvedValue('git version 2.34.1');
|
||||
|
||||
const result = await isGitInstalled({
|
||||
execGitVersion: mockExecGitVersion,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockExecGitVersion).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should return false when git command fails', async () => {
|
||||
const mockExecGitVersion = vi.fn().mockRejectedValue(new Error('Command not found: git'));
|
||||
|
||||
const result = await isGitInstalled({
|
||||
execGitVersion: mockExecGitVersion,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockExecGitVersion).toHaveBeenCalled();
|
||||
expect(logger.trace).toHaveBeenCalledWith('Git is not installed:', 'Command not found: git');
|
||||
});
|
||||
|
||||
test('should return false when git version output contains error', async () => {
|
||||
const mockExecGitVersion = vi.fn().mockResolvedValue('error: git not found');
|
||||
|
||||
const result = await isGitInstalled({
|
||||
execGitVersion: mockExecGitVersion,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockExecGitVersion).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type Mock, describe, expect, it, vi } from 'vitest';
|
||||
import type { ProcessedFile } from '../../../src/core/file/fileTypes.js';
|
||||
import type { GitDiffResult } from '../../../src/core/git/gitHandle.js';
|
||||
import type { GitDiffResult } from '../../../src/core/git/gitDiffHandle.js';
|
||||
import { TokenCounter } from '../../../src/core/metrics/TokenCounter.js';
|
||||
import { calculateAllFileMetrics } from '../../../src/core/metrics/calculateAllFileMetrics.js';
|
||||
import { calculateMetrics } from '../../../src/core/metrics/calculateMetrics.js';
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import type { RepomixConfigMerged } from '../../../src/config/configSchema.js';
|
||||
import type { ProcessedFile } from '../../../src/core/file/fileTypes.js';
|
||||
import type { GitDiffResult } from '../../../src/core/git/gitHandle.js';
|
||||
import * as gitHandleModule from '../../../src/core/git/gitHandle.js';
|
||||
import type { GitDiffResult } from '../../../src/core/git/gitDiffHandle.js';
|
||||
import * as gitDiffModule from '../../../src/core/git/gitDiffHandle.js';
|
||||
import * as gitRepositoryModule from '../../../src/core/git/gitRepositoryHandle.js';
|
||||
import { buildOutputGeneratorContext, generateOutput } from '../../../src/core/output/outputGenerate.js';
|
||||
import type { RenderContext } from '../../../src/core/output/outputGeneratorTypes.js';
|
||||
import { createMockConfig } from '../../testing/testUtils.js';
|
||||
|
||||
// Mock the gitHandle module
|
||||
vi.mock('../../../src/core/git/gitHandle.js', () => ({
|
||||
// Mock the git modules
|
||||
vi.mock('../../../src/core/git/gitDiffHandle.js', () => ({
|
||||
getWorkTreeDiff: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/core/git/gitRepositoryHandle.js', () => ({
|
||||
isGitRepository: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -28,8 +32,8 @@ index 123..456 100644
|
||||
vi.resetAllMocks();
|
||||
|
||||
// Mock the git command
|
||||
vi.mocked(gitHandleModule.getWorkTreeDiff).mockResolvedValue(sampleDiff);
|
||||
vi.mocked(gitHandleModule.isGitRepository).mockResolvedValue(true);
|
||||
vi.mocked(gitDiffModule.getWorkTreeDiff).mockResolvedValue(sampleDiff);
|
||||
vi.mocked(gitRepositoryModule.isGitRepository).mockResolvedValue(true);
|
||||
|
||||
// Sample minimal config using createMockConfig utility
|
||||
mockConfig = createMockConfig({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
import type { RepomixConfigMerged } from '../../../src/config/configSchema.js';
|
||||
import type { GitDiffResult } from '../../../src/core/git/gitHandle.js';
|
||||
import type { GitDiffResult } from '../../../src/core/git/gitDiffHandle.js';
|
||||
import { generateOutput } from '../../../src/core/output/outputGenerate.js';
|
||||
import type { RenderContext } from '../../../src/core/output/outputGeneratorTypes.js';
|
||||
import { createMockConfig } from '../../testing/testUtils.js';
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import type { RepomixConfigMerged } from '../../../src/config/configSchema.js';
|
||||
import type { ProcessedFile } from '../../../src/core/file/fileTypes.js';
|
||||
import * as gitHandleModule from '../../../src/core/git/gitHandle.js';
|
||||
import * as gitDiffModule from '../../../src/core/git/gitDiffHandle.js';
|
||||
import * as gitRepositoryModule from '../../../src/core/git/gitRepositoryHandle.js';
|
||||
import { pack } from '../../../src/core/packager.js';
|
||||
import { createMockConfig } from '../../testing/testUtils.js';
|
||||
|
||||
// Mock the dependencies
|
||||
vi.mock('../../../src/core/git/gitHandle.js', () => ({
|
||||
vi.mock('../../../src/core/git/gitDiffHandle.js', () => ({
|
||||
getWorkTreeDiff: vi.fn(),
|
||||
getStagedDiff: vi.fn(),
|
||||
isGitRepository: vi.fn(),
|
||||
getGitDiffs: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/core/git/gitRepositoryHandle.js', () => ({
|
||||
isGitRepository: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('Git Diffs Functionality', () => {
|
||||
let mockConfig: RepomixConfigMerged;
|
||||
const mockRootDir = '/test/repo';
|
||||
@@ -41,9 +45,9 @@ index 123..456 100644
|
||||
});
|
||||
|
||||
// Set up our mocks
|
||||
vi.mocked(gitHandleModule.isGitRepository).mockResolvedValue(true);
|
||||
vi.mocked(gitHandleModule.getWorkTreeDiff).mockResolvedValue(sampleDiff);
|
||||
vi.mocked(gitHandleModule.getStagedDiff).mockResolvedValue('');
|
||||
vi.mocked(gitRepositoryModule.isGitRepository).mockResolvedValue(true);
|
||||
vi.mocked(gitDiffModule.getWorkTreeDiff).mockResolvedValue(sampleDiff);
|
||||
vi.mocked(gitDiffModule.getStagedDiff).mockResolvedValue('');
|
||||
});
|
||||
|
||||
test('should not fetch diffs when includeDiffs is disabled', async () => {
|
||||
@@ -86,7 +90,7 @@ index 123..456 100644
|
||||
});
|
||||
|
||||
// Should not call getWorkTreeDiff
|
||||
expect(gitHandleModule.getWorkTreeDiff).not.toHaveBeenCalled();
|
||||
expect(gitDiffModule.getWorkTreeDiff).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should calculate diff token count correctly', async () => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { ProcessedFile } from '../../src/core/file/fileTypes.js';
|
||||
import type { FileCollectTask } from '../../src/core/file/workers/fileCollectWorker.js';
|
||||
import fileCollectWorker from '../../src/core/file/workers/fileCollectWorker.js';
|
||||
import fileProcessWorker from '../../src/core/file/workers/fileProcessWorker.js';
|
||||
import type { GitDiffResult } from '../../src/core/git/gitHandle.js';
|
||||
import type { GitDiffResult } from '../../src/core/git/gitDiffHandle.js';
|
||||
import { generateOutput } from '../../src/core/output/outputGenerate.js';
|
||||
import { pack } from '../../src/core/packager.js';
|
||||
import { copyToClipboardIfEnabled } from '../../src/core/packager/copyToClipboardIfEnabled.js';
|
||||
|
||||
Reference in New Issue
Block a user