UNPKG

repomix

Version:

A tool to pack repository contents to single file for AI consumption

155 lines (154 loc) 5.37 kB
import { execFile } from 'node:child_process'; import fs from 'node:fs/promises'; import path from 'node:path'; import { promisify } from 'node:util'; import { RepomixError } from '../../shared/errorHandle.js'; import { logger } from '../../shared/logger.js'; const execFileAsync = promisify(execFile); const GIT_REMOTE_TIMEOUT = 30000; const gitRemoteEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' }; const gitRemoteOpts = { timeout: GIT_REMOTE_TIMEOUT, env: gitRemoteEnv }; export const execGitLogFilenames = async (directory, maxCommits = 100, deps = { execFileAsync, }) => { try { const result = await deps.execFileAsync('git', [ '-C', directory, 'log', '--pretty=format:', '--name-only', '-n', maxCommits.toString(), ]); return result.stdout.split('\n').filter(Boolean); } catch (error) { logger.trace('Failed to get git log filenames:', error.message); return []; } }; export const execGitDiff = async (directory, options = [], deps = { execFileAsync, }) => { try { const result = await deps.execFileAsync('git', [ '-C', directory, 'diff', '--no-color', ...options, ]); return result.stdout || ''; } catch (error) { logger.trace('Failed to execute git diff:', error.message); throw error; } }; export const execGitVersion = async (deps = { execFileAsync, }) => { try { const result = await deps.execFileAsync('git', ['--version']); return result.stdout || ''; } catch (error) { logger.trace('Failed to execute git version:', error.message); throw error; } }; export const execGitRevParse = async (directory, deps = { execFileAsync, }) => { try { const result = await deps.execFileAsync('git', ['-C', directory, 'rev-parse', '--is-inside-work-tree']); return result.stdout || ''; } catch (error) { logger.trace('Failed to execute git rev-parse:', error.message); throw error; } }; export const execLsRemote = async (url, deps = { execFileAsync, }) => { validateGitUrl(url); try { const result = await deps.execFileAsync('git', ['ls-remote', '--heads', '--tags', '--', url], gitRemoteOpts); return result.stdout || ''; } catch (error) { logger.trace('Failed to execute git ls-remote:', error.message); throw error; } }; export const execGitShallowClone = async (url, directory, remoteBranch, deps = { execFileAsync, }) => { validateGitUrl(url); if (remoteBranch) { await deps.execFileAsync('git', ['-C', directory, 'init']); await deps.execFileAsync('git', ['-C', directory, 'remote', 'add', '--', 'origin', url]); try { await deps.execFileAsync('git', ['-C', directory, 'fetch', '--depth', '1', 'origin', remoteBranch], gitRemoteOpts); await deps.execFileAsync('git', ['-C', directory, 'checkout', 'FETCH_HEAD']); } catch (err) { const isRefNotfoundError = err instanceof Error && err.message.includes(`couldn't find remote ref ${remoteBranch}`); if (!isRefNotfoundError) { throw err; } const isNotShortSHA = !remoteBranch.match(/^[0-9a-f]{4,39}$/i); if (isNotShortSHA) { throw err; } await deps.execFileAsync('git', ['-C', directory, 'fetch', 'origin'], gitRemoteOpts); await deps.execFileAsync('git', ['-C', directory, 'checkout', remoteBranch]); } } else { await deps.execFileAsync('git', ['clone', '--depth', '1', '--', url, directory], gitRemoteOpts); } await fs.rm(path.join(directory, '.git'), { recursive: true, force: true }); }; export const execGitLog = async (directory, maxCommits, gitSeparator, deps = { execFileAsync, }) => { try { const result = await deps.execFileAsync('git', [ '-C', directory, 'log', `--pretty=format:${gitSeparator}%ad|%s`, '--date=iso', '--name-only', '-n', maxCommits.toString(), ]); return result.stdout || ''; } catch (error) { logger.trace('Failed to execute git log:', error.message); throw error; } }; export const validateGitUrl = (url) => { const dangerousParams = ['--upload-pack', '--receive-pack', '--config', '--exec']; if (dangerousParams.some((param) => url.includes(param))) { throw new RepomixError(`Invalid repository URL. URL contains potentially dangerous parameters: ${url}`); } if (!(url.startsWith('git@') || url.startsWith('https://'))) { throw new RepomixError(`Invalid URL protocol for '${url}'. URL must start with 'git@' or 'https://'`); } try { if (url.startsWith('https://')) { new URL(url); } } catch (error) { const redactedUrl = url.startsWith('https://') ? url.replace(/^(https?:\/\/)([^@/]+)@/i, '$1***@') : url; logger.trace('Invalid repository URL:', error.message); throw new RepomixError(`Invalid repository URL. Please provide a valid URL: ${redactedUrl}`); } };