diff --git a/Cargo.lock b/Cargo.lock index 63d9468..c517e53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -183,7 +183,7 @@ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "codebase-to-prompt" -version = "1.0.0" +version = "1.0.1" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index b840d1d..1a23520 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "codebase-to-prompt" -version = "1.0.0" +version = "1.0.1" edition = "2024" authors = ["Gabriel Kaszewski "] license = "MIT" diff --git a/src/lib.rs b/src/lib.rs index c22a2cf..7eac05c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ use chrono::Local; use clap::ValueEnum; use git2::Repository; use ignore::gitignore::Gitignore; -use tracing::{error, info, warn}; +use tracing::{debug, error, info, warn}; use walkdir::{DirEntry, WalkDir}; /// Represents the output format for the bundled files. @@ -26,8 +26,8 @@ pub enum Format { /// Configuration options for the file bundling process. #[derive(Debug)] pub struct Config { - /// The directory to process. - pub directory: PathBuf, + /// The directories to process. + pub directory: Vec, /// The optional output file path. If not provided, output is written to stdout. pub output: Option, /// File extensions to include in the output. @@ -46,6 +46,8 @@ pub struct Config { pub ignore_hidden: bool, /// Whether to respect `.gitignore` rules. pub respect_gitignore: bool, + /// Whether to use relative paths in the output. + pub relative_path: bool, } /// Runs the file bundling process based on the provided configuration. @@ -62,9 +64,20 @@ pub fn run(config: Config) -> Result<()> { append_date_and_git_hash(&mut output_path, &config)?; } - let writer = determine_output_writer(&output_path)?; + let mut writer = determine_output_writer(&output_path)?; - process_directory(&config, writer) + for dir in &config.directory { + if !dir.is_dir() { + return Err(anyhow::anyhow!( + "The specified path is not a directory: {}", + dir.display() + )); + } + + process_directory(&config, &mut writer, dir)?; + } + + Ok(()) } /// Appends the current date and/or Git hash to the output file name if required. @@ -90,16 +103,18 @@ fn append_date_and_git_hash(output_path: &mut Option, config: &Config) } 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."); + for dir in &config.directory { + match Repository::open(&dir) { + 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."), } - Err(_) => warn!("Not a git repository, cannot append git hash."), } } @@ -139,23 +154,26 @@ fn determine_output_writer(output_path: &Option) -> Result` - Returns `Ok(())` if successful, or an error if the process fails. -fn process_directory(config: &Config, mut writer: Box) -> Result<()> { - let (gitignore, _) = Gitignore::new(config.directory.join(".gitignore")); +fn process_directory(config: &Config, writer: &mut dyn Write, dir: &PathBuf) -> Result<()> { + let (gitignore, _) = Gitignore::new(dir.join(".gitignore")); - let walker = WalkDir::new(&config.directory) + let walker = WalkDir::new(dir) .into_iter() .filter_entry(|e| should_include_entry(e, &gitignore, config)); for result in walker { let entry = match result { - Ok(entry) => entry, + Ok(entry) => { + debug!("Processing entry: {:?}", entry); + entry + } Err(err) => { error!("Failed to access entry: {}", err); continue; } }; - if let Err(err) = process_file_entry(&entry, &mut writer, config) { + if let Err(err) = process_file_entry(&entry, writer, config, dir) { error!("{}", err); } } @@ -186,26 +204,37 @@ fn should_include_entry(entry: &DirEntry, gitignore: &Gitignore, config: &Config /// /// # Returns /// * `Result<()>` - Returns `Ok(())` if successful, or an error if the process fails. -fn process_file_entry(entry: &DirEntry, writer: &mut dyn Write, config: &Config) -> Result<()> { +fn process_file_entry( + entry: &DirEntry, + writer: &mut dyn Write, + config: &Config, + dir: &PathBuf, +) -> Result<()> { + debug!("File process entry process for {:?}", entry.path()); + let path = entry.path(); if !path.is_file() { + debug!("{:?} is not a file.", path); 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()); + debug!("Extension {:?} for {:?}", extension, path); + + let apply_include_filter = !(config.include.is_empty()); if apply_include_filter && !config.include.contains(&extension.to_string()) { return Ok(()); } - if config.exclude.contains(&extension.to_string()) { + let apply_exclude_filter = !(config.exclude.is_empty()); + + if apply_exclude_filter && config.exclude.contains(&extension.to_string()) { return Ok(()); } - let relative_path = path.strip_prefix(&config.directory).unwrap_or(path); + let relative_path = path.strip_prefix(dir).unwrap_or(path); let content = match fs::read_to_string(path) { Ok(content) => content, Err(_) => { @@ -214,8 +243,17 @@ fn process_file_entry(entry: &DirEntry, writer: &mut dyn Write, config: &Config) } }; - write_file_content(writer, relative_path, &content, extension, config) - .with_context(|| format!("Failed to write file content for {}", path.display())) + match &config.relative_path { + true => write_file_content(writer, relative_path, &content, extension, config) + .with_context(|| { + format!( + "Failed to write file content for {}", + relative_path.display() + ) + }), + false => write_file_content(writer, path, &content, extension, config) + .with_context(|| format!("Failed to write file content for {}", path.display())), + } } /// Writes the content of a single file to the writer based on the specified format. diff --git a/src/main.rs b/src/main.rs index 2d02931..ea43a1e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,16 +8,16 @@ use tracing_subscriber::FmtSubscriber; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { - #[arg(default_value = ".")] - directory: PathBuf, + #[arg(default_value = ".", value_delimiter = ' ', num_args=1..)] + directory: Vec, #[arg(short, long)] output: Option, - #[arg(short, long, use_value_delimiter = true, default_value = "")] + #[arg(short, long, value_delimiter = ',', num_args = 0..)] include: Vec, - #[arg(short, long, use_value_delimiter = true, default_value = "")] + #[arg(short, long, value_delimiter = ',', num_args = 0..)] exclude: Vec, #[arg(long, value_enum, default_value_t = Format::Console)] @@ -32,16 +32,19 @@ struct Args { #[arg(short = 'l', long)] line_numbers: bool, - #[arg(short = 'H', long)] + #[arg(short = 'H', long, default_value_t = true)] ignore_hidden: bool, #[arg(short = 'R', long, default_value_t = true)] respect_gitignore: bool, + + #[arg(long, default_value_t = false)] + relative_path: bool, } fn main() -> Result<()> { let subscriber = FmtSubscriber::builder() - .with_max_level(LevelFilter::INFO) + .with_max_level(LevelFilter::DEBUG) .finish(); tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); @@ -69,6 +72,7 @@ fn main() -> Result<()> { line_numbers: args.line_numbers, ignore_hidden: args.ignore_hidden, respect_gitignore: args.respect_gitignore, + relative_path: args.relative_path, }; debug!("Starting codebase to prompt with config: {:?}", config); diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 89859c4..fc90fb8 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -8,7 +8,7 @@ fn test_run_with_markdown_format() { let output_file = temp_dir.path().join("output.md"); let config = Config { - directory: PathBuf::from("tests/fixtures"), + directory: vec![PathBuf::from("tests/fixtures")], output: Some(output_file.clone()), include: vec!["rs".to_string()], exclude: vec![], @@ -18,6 +18,7 @@ fn test_run_with_markdown_format() { line_numbers: false, ignore_hidden: true, respect_gitignore: true, + relative_path: false, }; let result = run(config); @@ -33,7 +34,7 @@ fn test_run_with_text_format() { let output_file = temp_dir.path().join("output.txt"); let config = Config { - directory: PathBuf::from("tests/fixtures"), + directory: vec![PathBuf::from("tests/fixtures")], output: Some(output_file.clone()), include: vec!["txt".to_string()], exclude: vec![], @@ -43,6 +44,7 @@ fn test_run_with_text_format() { line_numbers: true, ignore_hidden: true, respect_gitignore: true, + relative_path: false, }; let result = run(config); @@ -58,7 +60,7 @@ fn test_run_with_git_hash_append() { let output_file = temp_dir.path().join("output.txt"); let config = Config { - directory: PathBuf::from("tests/fixtures"), + directory: vec![PathBuf::from("tests/fixtures")], output: Some(output_file.clone()), include: vec!["txt".to_string()], exclude: vec![], @@ -68,6 +70,7 @@ fn test_run_with_git_hash_append() { line_numbers: false, ignore_hidden: true, respect_gitignore: true, + relative_path: false, }; let result = run(config);