diff --git a/Cargo.toml b/Cargo.toml index d3617fa..1d617d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,9 @@ ignore = "0.4.23" tracing = "0.1.41" tracing-subscriber = "0.3.19" walkdir = "2.5.0" + +[profile.release] +strip = true +lto = true +codegen-units = 1 +opt-level = "z" diff --git a/src/lib.rs b/src/lib.rs index c966ec1..f2199ad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,61 +36,72 @@ pub fn run(config: Config) -> Result<()> { let mut output_path = config.output.clone(); if config.append_date || config.append_git_hash { - if let Some(path) = &mut output_path { - let mut new_filename = path - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or_default() - .to_string(); - - if config.append_date { - new_filename.push('_'); - new_filename.push_str(&Local::now().format("%Y%m%d").to_string()); - info!("Appending date to filename."); - } - - if config.append_git_hash { - match Repository::open(&config.directory) { - Ok(repo) => { - let head = repo.head().context("Failed to get repository HEAD")?; - if let Some(oid) = head.target() { - new_filename.push('_'); - new_filename.push_str(&oid.to_string()[..7]); - info!("Appending git hash to filename."); - } - } - Err(_) => warn!("Not a git repository, cannot append git hash."), - } - } - - if let Some(ext) = path.extension().and_then(|s| s.to_str()) { - new_filename.push('.'); - new_filename.push_str(ext); - } - path.set_file_name(new_filename); - } + append_date_and_git_hash(&mut output_path, &config)?; } - // Determine the output writer (file or stdout) - let writer: Box = if let Some(path) = &output_path { - info!("Output will be written to: {}", path.display()); - let file = File::create(path) - .with_context(|| format!("Failed to create output file: {}", path.display()))?; - Box::new(BufWriter::new(file)) - } else { - info!("Output will be written to stdout."); - Box::new(BufWriter::new(io::stdout())) - }; + let writer = determine_output_writer(&output_path)?; process_directory(&config, writer) } +/// Appends date and git hash to the output file name if required. +fn append_date_and_git_hash(output_path: &mut Option, config: &Config) -> Result<()> { + if let Some(path) = output_path { + let mut new_filename = path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or_default() + .to_string(); + + if config.append_date { + new_filename.push('_'); + new_filename.push_str(&Local::now().format("%Y%m%d").to_string()); + info!("Appending date to filename."); + } + + if config.append_git_hash { + match Repository::open(&config.directory) { + Ok(repo) => { + let head = repo.head().context("Failed to get repository HEAD")?; + if let Some(oid) = head.target() { + new_filename.push('_'); + new_filename.push_str(&oid.to_string()[..7]); + info!("Appending git hash to filename."); + } + } + Err(_) => warn!("Not a git repository, cannot append git hash."), + } + } + + if let Some(ext) = path.extension().and_then(|s| s.to_str()) { + new_filename.push('.'); + new_filename.push_str(ext); + } + path.set_file_name(new_filename); + } + Ok(()) +} + +/// Determines the output writer (file or stdout). +fn determine_output_writer(output_path: &Option) -> Result> { + if let Some(path) = output_path { + info!("Output will be written to: {}", path.display()); + let file = File::create(path) + .with_context(|| format!("Failed to create output file: {}", path.display()))?; + Ok(Box::new(BufWriter::new(file))) + } else { + info!("Output will be written to stdout."); + Ok(Box::new(BufWriter::new(io::stdout()))) + } +} + +// Refactored `process_directory` function fn process_directory(config: &Config, mut writer: Box) -> Result<()> { let (gitignore, _) = Gitignore::new(config.directory.join(".gitignore")); let walker = WalkDir::new(&config.directory) .into_iter() - .filter_entry(|e| !is_hidden(e, config) && !is_ignored(e, &gitignore, config)); + .filter_entry(|e| should_include_entry(e, &gitignore, config)); for result in walker { let entry = match result { @@ -101,42 +112,53 @@ fn process_directory(config: &Config, mut writer: Box) -> Result<()> } }; - let path = entry.path(); - if !path.is_file() { - continue; + if let Err(err) = process_file_entry(&entry, &mut writer, config) { + error!("{}", err); } - - let extension = path.extension().and_then(|s| s.to_str()).unwrap_or(""); - - let apply_include_filter = !config.include.is_empty() - && !(config.include.len() == 1 && config.include[0].is_empty()); - - if apply_include_filter && !config.include.contains(&extension.to_string()) { - continue; - } - - if config.exclude.contains(&extension.to_string()) { - continue; - } - - let relative_path = path.strip_prefix(&config.directory).unwrap_or(path); - let content = match fs::read_to_string(path) { - Ok(content) => content, - Err(_) => { - warn!("Skipping non-UTF-8 file: {}", path.display()); - continue; // Skip non-text files - } - }; - - write_file_content(&mut writer, relative_path, &content, extension, config) - .with_context(|| format!("Failed to write file content for {}", path.display()))?; } info!("File bundling complete."); Ok(()) } -/// Writes the content of a single file to the writer based on the specified format. +/// Determines if a directory entry should be included. +fn should_include_entry(entry: &DirEntry, gitignore: &Gitignore, config: &Config) -> bool { + !is_hidden(entry, config) && !is_ignored(entry, gitignore, config) +} + +/// Processes a single file entry. +fn process_file_entry(entry: &DirEntry, writer: &mut dyn Write, config: &Config) -> Result<()> { + let path = entry.path(); + if !path.is_file() { + return Ok(()); + } + + let extension = path.extension().and_then(|s| s.to_str()).unwrap_or(""); + + let apply_include_filter = + !config.include.is_empty() && !(config.include.len() == 1 && config.include[0].is_empty()); + + if apply_include_filter && !config.include.contains(&extension.to_string()) { + return Ok(()); + } + + if config.exclude.contains(&extension.to_string()) { + return Ok(()); + } + + let relative_path = path.strip_prefix(&config.directory).unwrap_or(path); + let content = match fs::read_to_string(path) { + Ok(content) => content, + Err(_) => { + warn!("Skipping non-UTF-8 file: {}", path.display()); + return Ok(()); // Skip non-text files + } + }; + + write_file_content(writer, relative_path, &content, extension, config) + .with_context(|| format!("Failed to write file content for {}", path.display())) +} + fn write_file_content( writer: &mut dyn Write, path: &Path, diff --git a/src/main.rs b/src/main.rs index a0e3eeb..a63d6d4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use anyhow::Result; use clap::Parser; use codebase_to_prompt::Format; use std::path::PathBuf; -use tracing::level_filters::LevelFilter; +use tracing::{info, level_filters::LevelFilter}; use tracing_subscriber::FmtSubscriber; #[derive(Parser, Debug)] @@ -23,19 +23,19 @@ struct Args { #[arg(long, value_enum, default_value_t = Format::Console)] format: Format, - #[arg(long)] + #[arg(short = 'd', long)] append_date: bool, - #[arg(long)] + #[arg(short = 'g', long)] append_git_hash: bool, - #[arg(long)] + #[arg(short = 'l', long)] line_numbers: bool, - #[arg(long)] + #[arg(short = 'H', long)] ignore_hidden: bool, - #[arg(long, default_value_t = true)] + #[arg(short = 'R', long, default_value_t = true)] respect_gitignore: bool, } @@ -47,12 +47,25 @@ fn main() -> Result<()> { let args = Args::parse(); + // Automatically detect format if outputting to a .md file + let mut format = args.format; + if matches!(format, Format::Console) { + if let Some(output_path) = &args.output { + if output_path.extension().and_then(|s| s.to_str()) == Some("md") { + format = Format::Markdown; + } + if output_path.extension().and_then(|s| s.to_str()) == Some("txt") { + format = Format::Text; + } + } + } + let config = codebase_to_prompt::Config { directory: args.directory, output: args.output, include: args.include, exclude: args.exclude, - format: args.format, + format, append_date: args.append_date, append_git_hash: args.append_git_hash, line_numbers: args.line_numbers, @@ -60,5 +73,7 @@ fn main() -> Result<()> { respect_gitignore: args.respect_gitignore, }; + info!("Starting codebase to prompt with config: {:?}", config); + codebase_to_prompt::run(config) }