Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 33 additions & 35 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,26 @@

/**
* git2txt - A command-line tool to convert GitHub repositories into readable text files
*
*
* This tool clones a GitHub repository, processes its text files, and combines them
* into a single output file. It's useful for code review, analysis, and documentation
* purposes.
*
*
* Features:
* - Supports public GitHub repositories
* - Filters binary and large files
* - Customizable file size threshold
* - Debug mode for troubleshooting
* - Progress indicators for long operations
*
*
* @module git2txt
*/

import meow from 'meow';
import ora from 'ora';
import chalk from 'chalk';
import { glob } from 'glob';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import { filesize as formatFileSize } from 'filesize';
import { isBinaryFile } from 'isbinaryfile';
import os from 'os';
Expand Down Expand Up @@ -58,9 +56,8 @@ const helpText = `
const exit = (code) => {
if (process.env.NODE_ENV === 'test') {
throw new Error(`Exit called with code: ${code}`);
} else {
process.exit(code);
}
process.exit(code);
};

// Initialize CLI parser with meow
Expand Down Expand Up @@ -97,22 +94,22 @@ function normalizeGitHubUrl(url) {
try {
// Remove trailing slashes
url = url.replace(/\/+$/, '');

// Handle git@ URLs
if (url.startsWith('git@github.com:')) {
return url;
}

// Handle full HTTPS URLs
if (url.startsWith('https://github.com/')) {
return url;
}

// Handle short format (user/repo)
if (url.match(/^[\w-]+\/[\w-]+$/)) {
return `https://github.com/${url}`;
}

throw new Error('Invalid GitHub repository URL format');
} catch (error) {
throw new Error(`Invalid GitHub URL: ${url}`);
Expand Down Expand Up @@ -152,7 +149,7 @@ export async function downloadRepository(url) {
// Normalize the GitHub URL
const normalizedUrl = normalizeGitHubUrl(url);
const repoName = url.split('/').pop().replace('.git', '');

if (cli.flags.debug) {
console.log(chalk.blue('Debug: Normalized URL:'), normalizedUrl);
console.log(chalk.blue('Debug: Temp directory:'), tempDir);
Expand All @@ -163,15 +160,15 @@ export async function downloadRepository(url) {

// Clone the repository
const cloneCommand = `git clone --depth 1 ${normalizedUrl} ${tempDir}`;

if (cli.flags.debug) {
console.log(chalk.blue('Debug: Executing command:'), cloneCommand);
}

await execAsync(cloneCommand, {
maxBuffer: 1024 * 1024 * 100 // 100MB buffer
});

// Verify the download
const files = await fs.readdir(tempDir);
if (files.length === 0) {
Expand All @@ -182,19 +179,19 @@ export async function downloadRepository(url) {
return { tempDir, repoName };
} catch (error) {
if (spinner) spinner.fail('Failed to download repository');

if (cli.flags.debug) {
console.log(chalk.blue('Debug: Full error:'), error);
}

if (process.env.NODE_ENV !== 'test') {
console.error(chalk.red('Error: Could not access the repository. Please check:'));
console.error(chalk.yellow(' 1. The repository exists and is public'));
console.error(chalk.yellow(' 2. You have the correct repository URL'));
console.error(chalk.yellow(' 3. GitHub is accessible from your network'));
console.error(chalk.yellow(' 4. Git is installed and accessible from command line'));
}

await cleanup(tempDir);
throw error;
}
Expand Down Expand Up @@ -222,10 +219,10 @@ export async function processFiles(directory, options) {
*/
async function processDirectory(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });

for (const entry of entries) {
const fullPath = path.join(dir, entry.name);

if (entry.isDirectory() && entry.name !== 'node_modules' && entry.name !== '.git') {
// Recursively process subdirectories
await processDirectory(fullPath);
Expand Down Expand Up @@ -255,13 +252,13 @@ export async function processFiles(directory, options) {

const content = await fs.readFile(fullPath, 'utf8');
const relativePath = path.relative(directory, fullPath);

output += `\n${'='.repeat(80)}\n`;
output += `File: ${relativePath}\n`;
output += `Size: ${formatFileSize(stats.size)}\n`;
output += `${'='.repeat(80)}\n\n`;
output += `${content}\n`;

processedFiles++;

if (process.env.DEBUG) {
Expand Down Expand Up @@ -343,22 +340,23 @@ export async function main() {
let tempDir;
try {
const url = await validateInput(cli.input);
if (process.env.NODE_ENV !== 'test') {
const result = await downloadRepository(url);
tempDir = result.tempDir;

const outputPath = cli.flags.output || `${result.repoName}.txt`;
const content = await processFiles(tempDir, {
threshold: cli.flags.threshold,
includeAll: cli.flags.includeAll
});

if (!content) {
throw new Error('No content was generated from the repository');
}
if (process.env.NODE_ENV === 'test') {
return;
}

await writeOutput(content, outputPath);
const result = await downloadRepository(url);
tempDir = result.tempDir;
const outputPath = cli.flags.output || `${result.repoName}.txt`;
const content = await processFiles(tempDir, {
threshold: cli.flags.threshold,
includeAll: cli.flags.includeAll
});

if (!content) {
throw new Error('No content was generated from the repository');
}

await writeOutput(content, outputPath);
} catch (error) {
if (process.env.NODE_ENV === 'test') {
throw error;
Expand Down
58 changes: 0 additions & 58 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,12 @@ import { promisify } from 'util';
// Import the functions and CLI to test
import {
validateInput,
processFiles,
writeOutput,
cleanup,
main,
cli
} from '../index.js';

const execFileAsync = promisify(execFile);
const __dirname = path.dirname(fileURLToPath(import.meta.url));

// Helper function to create test file and verify its existence
async function createTestFile(filepath, content) {
await fs.mkdir(path.dirname(filepath), { recursive: true });
Expand Down Expand Up @@ -106,60 +102,6 @@ test('cleanup removes temporary directory', async t => {
);
});

// test('processFiles processes repository files', async t => {
// const testDir = t.context.tempDir;
// const testContent = 'Hello, world!';
// const testFile = path.join(testDir, 'test.txt');

// try {
// // Create and verify test file
// await fs.writeFile(testFile, testContent);

// // Wait briefly to ensure file is written
// await new Promise(resolve => setTimeout(resolve, 100));

// // Verify file exists and has correct content
// const fileStats = await fs.stat(testFile);
// t.true(fileStats.isFile(), 'Test file should exist and be a file');

// const actualContent = await fs.readFile(testFile, 'utf8');
// t.is(actualContent, testContent, 'File should contain correct content');

// // Process files
// const output = await processFiles(testDir, {
// threshold: 1,
// includeAll: true
// });

// // Log output for debugging
// if (process.env.DEBUG) {
// console.log('Test state:', {
// testDir,
// testFile,
// fileExists: await fs.access(testFile).then(() => true).catch(() => false),
// dirContents: await fs.readdir(testDir),
// fileContent: actualContent,
// processOutput: output
// });
// }

// // Verify process output
// t.regex(output, /File: test\.txt/, 'Output should contain file name');
// t.regex(output, /Hello, world!/, 'Output should contain file content');

// } catch (error) {
// console.error('Test error:', {
// message: error.message,
// stack: error.stack,
// testDir,
// exists: await fs.access(testDir).then(() => true).catch(() => false),
// dirContents: await fs.readdir(testDir).catch(e => e.message),
// fileExists: await fs.access(testFile).then(() => true).catch(() => false)
// });
// throw error;
// }
// });

test('main function handles missing URL', async t => {
// Ensure test environment
process.env.NODE_ENV = 'test';
Expand Down