repomix
Version:
A tool to pack repository contents to single file for AI consumption
155 lines (154 loc) • 5.37 kB
JavaScript
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}`);
}
};