refactor: update Config to support multiple directories and add relative path option
Some checks failed
Continuous Integration / Build and Test on ubuntu-latest (push) Failing after 1m27s
Some checks failed
Continuous Integration / Build and Test on ubuntu-latest (push) Failing after 1m27s
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -183,7 +183,7 @@ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
|
||||
|
||||
[[package]]
|
||||
name = "codebase-to-prompt"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "codebase-to-prompt"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
edition = "2024"
|
||||
authors = ["Gabriel Kaszewski <gabrielkaszewski@gmail.com>"]
|
||||
license = "MIT"
|
||||
|
74
src/lib.rs
74
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<PathBuf>,
|
||||
/// The optional output file path. If not provided, output is written to stdout.
|
||||
pub output: Option<PathBuf>,
|
||||
/// 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,7 +103,8 @@ fn append_date_and_git_hash(output_path: &mut Option<PathBuf>, config: &Config)
|
||||
}
|
||||
|
||||
if config.append_git_hash {
|
||||
match Repository::open(&config.directory) {
|
||||
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() {
|
||||
@@ -102,6 +116,7 @@ fn append_date_and_git_hash(output_path: &mut Option<PathBuf>, config: &Config)
|
||||
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('.');
|
||||
@@ -139,23 +154,26 @@ fn determine_output_writer(output_path: &Option<PathBuf>) -> Result<Box<dyn Writ
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Result<()>` - Returns `Ok(())` if successful, or an error if the process fails.
|
||||
fn process_directory(config: &Config, mut writer: Box<dyn Write>) -> 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.
|
||||
|
16
src/main.rs
16
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<PathBuf>,
|
||||
|
||||
#[arg(short, long)]
|
||||
output: Option<PathBuf>,
|
||||
|
||||
#[arg(short, long, use_value_delimiter = true, default_value = "")]
|
||||
#[arg(short, long, value_delimiter = ',', num_args = 0..)]
|
||||
include: Vec<String>,
|
||||
|
||||
#[arg(short, long, use_value_delimiter = true, default_value = "")]
|
||||
#[arg(short, long, value_delimiter = ',', num_args = 0..)]
|
||||
exclude: Vec<String>,
|
||||
|
||||
#[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);
|
||||
|
@@ -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);
|
||||
|
Reference in New Issue
Block a user