Skip to content

Commit

Permalink
Merge pull request #176 from yamadashy/refact/remote-action
Browse files Browse the repository at this point in the history
refactor(core): Git Command Module Extraction
  • Loading branch information
yamadashy authored Nov 23, 2024
2 parents bc4f6fd + 09b6c57 commit 556e8e3
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 58 deletions.
41 changes: 21 additions & 20 deletions src/cli/actions/remoteAction.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import { exec } from 'node:child_process';
import * as fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { promisify } from 'node:util';
import pc from 'picocolors';
import { execGitShallowClone, isGitInstalled } from '../../core/file/gitCommand.js';
import { RepomixError } from '../../shared/errorHandle.js';
import { logger } from '../../shared/logger.js';
import type { CliOptions } from '../cliRun.js';
import Spinner from '../cliSpinner.js';
import { runDefaultAction } from './defaultAction.js';

const execAsync = promisify(exec);

export const runRemoteAction = async (repoUrl: string, options: CliOptions): Promise<void> => {
const gitInstalled = await checkGitInstallation();
if (!gitInstalled) {
export const runRemoteAction = async (
repoUrl: string,
options: CliOptions,
deps = {
isGitInstalled,
execGitShallowClone,
},
): Promise<void> => {
if (!(await deps.isGitInstalled())) {
throw new RepomixError('Git is not installed or not in the system PATH.');
}

Expand All @@ -26,7 +29,9 @@ export const runRemoteAction = async (repoUrl: string, options: CliOptions): Pro
spinner.start();

// Clone the repository
await cloneRepository(formatGitUrl(repoUrl), tempDirPath);
await cloneRepository(formatGitUrl(repoUrl), tempDirPath, {
execGitShallowClone: deps.execGitShallowClone,
});

spinner.succeed('Repository cloned successfully!');
logger.log('');
Expand Down Expand Up @@ -65,12 +70,18 @@ export const createTempDirectory = async (): Promise<string> => {
return tempDir;
};

export const cloneRepository = async (url: string, directory: string): Promise<void> => {
export const cloneRepository = async (
url: string,
directory: string,
deps = {
execGitShallowClone: execGitShallowClone,
},
): Promise<void> => {
logger.log(`Clone repository: ${url} to temporary directory. ${pc.dim(`path: ${directory}`)}`);
logger.log('');

try {
await execAsync(`git clone --depth 1 ${url} ${directory}`);
await deps.execGitShallowClone(url, directory);
} catch (error) {
throw new RepomixError(`Failed to clone repository: ${(error as Error).message}`);
}
Expand All @@ -81,16 +92,6 @@ export const cleanupTempDirectory = async (directory: string): Promise<void> =>
await fs.rm(directory, { recursive: true, force: true });
};

export const checkGitInstallation = async (): Promise<boolean> => {
try {
const result = await execAsync('git --version');
return !result.stderr;
} catch (error) {
logger.debug('Git is not installed:', (error as Error).message);
return false;
}
};

export const copyOutputToCurrentDirectory = async (
sourceDir: string,
targetDir: string,
Expand Down
29 changes: 29 additions & 0 deletions src/core/file/gitCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
import { logger } from '../../shared/logger.js';

const execAsync = promisify(exec);

export const isGitInstalled = async (
deps = {
execAsync,
},
) => {
try {
const result = await deps.execAsync('git --version');
return !result.stderr;
} catch (error) {
logger.trace('Git is not installed:', (error as Error).message);
return false;
}
};

export const execGitShallowClone = async (
url: string,
directory: string,
deps = {
execAsync,
},
) => {
await deps.execAsync(`git clone --depth 1 ${url} ${directory}`);
};
30 changes: 11 additions & 19 deletions src/core/packager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,13 @@ import type { RepomixConfigMerged } from '../config/configSchema.js';
import { logger } from '../shared/logger.js';
import { getProcessConcurrency } from '../shared/processConcurrency.js';
import type { RepomixProgressCallback } from '../shared/types.js';
import { collectFiles as defaultCollectFiles } from './file/fileCollect.js';
import { processFiles as defaultProcessFiles } from './file/fileProcess.js';
import { searchFiles as defaultSearchFiles } from './file/fileSearch.js';
import { generateOutput as defaultGenerateOutput } from './output/outputGenerate.js';
import { type SuspiciousFileResult, runSecurityCheck as defaultRunSecurityCheck } from './security/securityCheck.js';
import { collectFiles } from './file/fileCollect.js';
import { processFiles } from './file/fileProcess.js';
import { searchFiles } from './file/fileSearch.js';
import { generateOutput } from './output/outputGenerate.js';
import { type SuspiciousFileResult, runSecurityCheck } from './security/securityCheck.js';
import { TokenCounter } from './tokenCount/tokenCount.js';

export interface PackDependencies {
searchFiles: typeof defaultSearchFiles;
collectFiles: typeof defaultCollectFiles;
processFiles: typeof defaultProcessFiles;
runSecurityCheck: typeof defaultRunSecurityCheck;
generateOutput: typeof defaultGenerateOutput;
}

export interface PackResult {
totalFiles: number;
totalCharacters: number;
Expand All @@ -36,12 +28,12 @@ export const pack = async (
rootDir: string,
config: RepomixConfigMerged,
progressCallback: RepomixProgressCallback = () => {},
deps: PackDependencies = {
searchFiles: defaultSearchFiles,
collectFiles: defaultCollectFiles,
processFiles: defaultProcessFiles,
runSecurityCheck: defaultRunSecurityCheck,
generateOutput: defaultGenerateOutput,
deps = {
searchFiles,
collectFiles,
processFiles,
runSecurityCheck,
generateOutput,
},
): Promise<PackResult> => {
// Get all file paths considering the config
Expand Down
28 changes: 11 additions & 17 deletions tests/cli/actions/remoteAction.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
import * as fs from 'node:fs/promises';
import * as os from 'node:os';
import path from 'node:path';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import {
checkGitInstallation,
cleanupTempDirectory,
copyOutputToCurrentDirectory,
createTempDirectory,
formatGitUrl,
runRemoteAction,
} from '../../../src/cli/actions/remoteAction.js';
import { copyOutputToCurrentDirectory, formatGitUrl, runRemoteAction } from '../../../src/cli/actions/remoteAction.js';

vi.mock('node:fs/promises', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs/promises')>();
Expand All @@ -28,14 +20,16 @@ describe('remoteAction functions', () => {
describe('runRemoteAction', () => {
test('should clone the repository', async () => {
vi.mocked(fs.copyFile).mockResolvedValue(undefined);
await runRemoteAction('yamadashy/repomix', {});
});
});

describe('checkGitInstallation Integration', () => {
test('should detect git installation in real environment', async () => {
const result = await checkGitInstallation();
expect(result).toBe(true);
await runRemoteAction(
'yamadashy/repomix',
{},
{
isGitInstalled: async () => Promise.resolve(true),
execGitShallowClone: async (url: string, directory: string) => {
await fs.writeFile(path.join(directory, 'README.md'), 'Hello, world!');
},
},
);
});
});

Expand Down
65 changes: 65 additions & 0 deletions tests/core/file/gitCommand.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { execGitShallowClone, isGitInstalled } from '../../../src/core/file/gitCommand.js';
import { logger } from '../../../src/shared/logger.js';

vi.mock('../../../src/shared/logger');

describe('gitCommand', () => {
beforeEach(() => {
vi.resetAllMocks();
});

describe('isGitInstalled', () => {
test('should return true when git is installed', async () => {
const mockExecAsync = vi.fn().mockResolvedValue({ stdout: 'git version 2.34.1', stderr: '' });

const result = await isGitInstalled({ execAsync: mockExecAsync });

expect(result).toBe(true);
expect(mockExecAsync).toHaveBeenCalledWith('git --version');
});

test('should return false when git command fails', async () => {
const mockExecAsync = vi.fn().mockRejectedValue(new Error('Command not found: git'));

const result = await isGitInstalled({ execAsync: mockExecAsync });

expect(result).toBe(false);
expect(mockExecAsync).toHaveBeenCalledWith('git --version');
expect(logger.trace).toHaveBeenCalledWith('Git is not installed:', 'Command not found: git');
});

test('should return false when git command returns stderr', async () => {
const mockExecAsync = vi.fn().mockResolvedValue({ stdout: '', stderr: 'git: command not found' });

const result = await isGitInstalled({ execAsync: mockExecAsync });

expect(result).toBe(false);
expect(mockExecAsync).toHaveBeenCalledWith('git --version');
});
});

describe('execGitShallowClone', () => {
test('should execute git clone with correct parameters', async () => {
const mockExecAsync = vi.fn().mockResolvedValue({ stdout: '', stderr: '' });
const url = 'https://github.com/user/repo.git';
const directory = '/tmp/repo';

await execGitShallowClone(url, directory, { execAsync: mockExecAsync });

expect(mockExecAsync).toHaveBeenCalledWith(`git clone --depth 1 ${url} ${directory}`);
});

test('should throw error when git clone fails', async () => {
const mockExecAsync = vi.fn().mockRejectedValue(new Error('Authentication failed'));
const url = 'https://github.com/user/repo.git';
const directory = '/tmp/repo';

await expect(execGitShallowClone(url, directory, { execAsync: mockExecAsync })).rejects.toThrow(
'Authentication failed',
);

expect(mockExecAsync).toHaveBeenCalledWith(`git clone --depth 1 ${url} ${directory}`);
});
});
});
15 changes: 13 additions & 2 deletions tests/core/packager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import * as fs from 'node:fs/promises';
import path from 'node:path';
import clipboardy from 'clipboardy';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { type PackDependencies, pack } from '../../src/core/packager.js';
import type { collectFiles } from '../../src/core/file/fileCollect.js';
import type { processFiles } from '../../src/core/file/fileProcess.js';
import type { searchFiles } from '../../src/core/file/fileSearch.js';
import type { generateOutput } from '../../src/core/output/outputGenerate.js';
import { pack } from '../../src/core/packager.js';
import type { runSecurityCheck } from '../../src/core/security/securityCheck.js';
import { TokenCounter } from '../../src/core/tokenCount/tokenCount.js';
import { createMockConfig } from '../testing/testUtils.js';

Expand All @@ -17,7 +22,13 @@ vi.mock('clipboardy', () => ({
}));

describe('packager', () => {
let mockDeps: PackDependencies;
let mockDeps: {
searchFiles: typeof searchFiles;
collectFiles: typeof collectFiles;
processFiles: typeof processFiles;
runSecurityCheck: typeof runSecurityCheck;
generateOutput: typeof generateOutput;
};

beforeEach(() => {
vi.resetAllMocks();
Expand Down

0 comments on commit 556e8e3

Please sign in to comment.