feat: refactor output filename handling and enhance argument parsing

This commit is contained in:
2025-08-24 13:33:07 +02:00
parent 1f25bab6a2
commit acaf79b4ee
3 changed files with 123 additions and 80 deletions

View File

@@ -12,3 +12,9 @@ ignore = "0.4.23"
tracing = "0.1.41" tracing = "0.1.41"
tracing-subscriber = "0.3.19" tracing-subscriber = "0.3.19"
walkdir = "2.5.0" walkdir = "2.5.0"
[profile.release]
strip = true
lto = true
codegen-units = 1
opt-level = "z"

View File

@@ -36,61 +36,72 @@ pub fn run(config: Config) -> Result<()> {
let mut output_path = config.output.clone(); let mut output_path = config.output.clone();
if config.append_date || config.append_git_hash { if config.append_date || config.append_git_hash {
if let Some(path) = &mut output_path { append_date_and_git_hash(&mut output_path, &config)?;
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);
}
} }
// Determine the output writer (file or stdout) let writer = determine_output_writer(&output_path)?;
let writer: Box<dyn Write> = 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()))
};
process_directory(&config, writer) 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<PathBuf>, 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<PathBuf>) -> Result<Box<dyn Write>> {
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<dyn Write>) -> Result<()> { fn process_directory(config: &Config, mut writer: Box<dyn Write>) -> Result<()> {
let (gitignore, _) = Gitignore::new(config.directory.join(".gitignore")); let (gitignore, _) = Gitignore::new(config.directory.join(".gitignore"));
let walker = WalkDir::new(&config.directory) let walker = WalkDir::new(&config.directory)
.into_iter() .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 { for result in walker {
let entry = match result { let entry = match result {
@@ -101,42 +112,53 @@ fn process_directory(config: &Config, mut writer: Box<dyn Write>) -> Result<()>
} }
}; };
let path = entry.path(); if let Err(err) = process_file_entry(&entry, &mut writer, config) {
if !path.is_file() { error!("{}", err);
continue;
} }
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."); info!("File bundling complete.");
Ok(()) 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( fn write_file_content(
writer: &mut dyn Write, writer: &mut dyn Write,
path: &Path, path: &Path,

View File

@@ -2,7 +2,7 @@ use anyhow::Result;
use clap::Parser; use clap::Parser;
use codebase_to_prompt::Format; use codebase_to_prompt::Format;
use std::path::PathBuf; use std::path::PathBuf;
use tracing::level_filters::LevelFilter; use tracing::{info, level_filters::LevelFilter};
use tracing_subscriber::FmtSubscriber; use tracing_subscriber::FmtSubscriber;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
@@ -23,19 +23,19 @@ struct Args {
#[arg(long, value_enum, default_value_t = Format::Console)] #[arg(long, value_enum, default_value_t = Format::Console)]
format: Format, format: Format,
#[arg(long)] #[arg(short = 'd', long)]
append_date: bool, append_date: bool,
#[arg(long)] #[arg(short = 'g', long)]
append_git_hash: bool, append_git_hash: bool,
#[arg(long)] #[arg(short = 'l', long)]
line_numbers: bool, line_numbers: bool,
#[arg(long)] #[arg(short = 'H', long)]
ignore_hidden: bool, ignore_hidden: bool,
#[arg(long, default_value_t = true)] #[arg(short = 'R', long, default_value_t = true)]
respect_gitignore: bool, respect_gitignore: bool,
} }
@@ -47,12 +47,25 @@ fn main() -> Result<()> {
let args = Args::parse(); 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 { let config = codebase_to_prompt::Config {
directory: args.directory, directory: args.directory,
output: args.output, output: args.output,
include: args.include, include: args.include,
exclude: args.exclude, exclude: args.exclude,
format: args.format, format,
append_date: args.append_date, append_date: args.append_date,
append_git_hash: args.append_git_hash, append_git_hash: args.append_git_hash,
line_numbers: args.line_numbers, line_numbers: args.line_numbers,
@@ -60,5 +73,7 @@ fn main() -> Result<()> {
respect_gitignore: args.respect_gitignore, respect_gitignore: args.respect_gitignore,
}; };
info!("Starting codebase to prompt with config: {:?}", config);
codebase_to_prompt::run(config) codebase_to_prompt::run(config)
} }