feat: initialize codebase-to-prompt project with core functionality
- Added Cargo.toml for project dependencies and metadata. - Implemented main library logic in src/lib.rs to process files in a directory. - Introduced configuration struct to manage input parameters. - Added command-line argument parsing in src/main.rs using clap. - Implemented output formatting options (Markdown, Text, Console). - Integrated tracing for logging and error handling. - Added support for including/excluding file types and respecting .gitignore.
This commit is contained in:
194
src/lib.rs
Normal file
194
src/lib.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, BufWriter, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use chrono::Local;
|
||||
use clap::ValueEnum;
|
||||
use git2::Repository;
|
||||
use ignore::gitignore::Gitignore;
|
||||
use tracing::{error, info, warn};
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
|
||||
#[derive(Debug, Clone, ValueEnum, Default)]
|
||||
pub enum Format {
|
||||
Markdown,
|
||||
Text,
|
||||
#[default]
|
||||
Console,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Config {
|
||||
pub directory: PathBuf,
|
||||
pub output: Option<PathBuf>,
|
||||
pub include: Vec<String>,
|
||||
pub exclude: Vec<String>,
|
||||
pub format: Format,
|
||||
pub append_date: bool,
|
||||
pub append_git_hash: bool,
|
||||
pub line_numbers: bool,
|
||||
pub ignore_hidden: bool,
|
||||
pub respect_gitignore: bool,
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the output writer (file or stdout)
|
||||
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)
|
||||
}
|
||||
|
||||
fn process_directory(config: &Config, mut writer: Box<dyn Write>) -> 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));
|
||||
|
||||
for result in walker {
|
||||
let entry = match result {
|
||||
Ok(entry) => entry,
|
||||
Err(err) => {
|
||||
error!("Failed to access entry: {}", err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let path = entry.path();
|
||||
if !path.is_file() {
|
||||
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.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Writes the content of a single file to the writer based on the specified format.
|
||||
fn write_file_content(
|
||||
writer: &mut dyn Write,
|
||||
path: &Path,
|
||||
content: &str,
|
||||
extension: &str,
|
||||
config: &Config,
|
||||
) -> Result<()> {
|
||||
match config.format {
|
||||
Format::Markdown => {
|
||||
writeln!(writer, "### `{}`\n", path.display())?;
|
||||
writeln!(writer, "```{}", extension)?;
|
||||
write_content_lines(writer, content, config.line_numbers)?;
|
||||
writeln!(writer, "```\n")?;
|
||||
}
|
||||
Format::Text | Format::Console => {
|
||||
// In Console mode, we could add colors or other specific formatting later
|
||||
writeln!(writer, "./{}\n---", path.display())?;
|
||||
write_content_lines(writer, content, config.line_numbers)?;
|
||||
writeln!(writer, "---")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper to write content line by line, optionally with line numbers.
|
||||
fn write_content_lines(writer: &mut dyn Write, content: &str, line_numbers: bool) -> Result<()> {
|
||||
if line_numbers {
|
||||
for (i, line) in content.lines().enumerate() {
|
||||
writeln!(writer, "{:4} | {}", i + 1, line)?;
|
||||
}
|
||||
} else {
|
||||
writeln!(writer, "{}", content)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper function to check if a directory entry is hidden.
|
||||
fn is_hidden(entry: &DirEntry, config: &Config) -> bool {
|
||||
config.ignore_hidden
|
||||
&& entry
|
||||
.file_name()
|
||||
.to_str()
|
||||
.map(|s| s.starts_with('.'))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Helper function to check if a directory entry is ignored by .gitignore.
|
||||
fn is_ignored(entry: &DirEntry, gitignore: &Gitignore, config: &Config) -> bool {
|
||||
if !config.respect_gitignore {
|
||||
return false;
|
||||
}
|
||||
gitignore
|
||||
.matched(entry.path(), entry.file_type().is_dir())
|
||||
.is_ignore()
|
||||
}
|
64
src/main.rs
Normal file
64
src/main.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use codebase_to_prompt::Format;
|
||||
use std::path::PathBuf;
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_subscriber::FmtSubscriber;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Args {
|
||||
#[arg(default_value = ".")]
|
||||
directory: PathBuf,
|
||||
|
||||
#[arg(short, long)]
|
||||
output: Option<PathBuf>,
|
||||
|
||||
#[arg(short, long, use_value_delimiter = true, default_value = "")]
|
||||
include: Vec<String>,
|
||||
|
||||
#[arg(short, long, use_value_delimiter = true, default_value = "")]
|
||||
exclude: Vec<String>,
|
||||
|
||||
#[arg(long, value_enum, default_value_t = Format::Console)]
|
||||
format: Format,
|
||||
|
||||
#[arg(long)]
|
||||
append_date: bool,
|
||||
|
||||
#[arg(long)]
|
||||
append_git_hash: bool,
|
||||
|
||||
#[arg(long)]
|
||||
line_numbers: bool,
|
||||
|
||||
#[arg(long)]
|
||||
ignore_hidden: bool,
|
||||
|
||||
#[arg(long, default_value_t = true)]
|
||||
respect_gitignore: bool,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let subscriber = FmtSubscriber::builder()
|
||||
.with_max_level(LevelFilter::INFO)
|
||||
.finish();
|
||||
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
let config = codebase_to_prompt::Config {
|
||||
directory: args.directory,
|
||||
output: args.output,
|
||||
include: args.include,
|
||||
exclude: args.exclude,
|
||||
format: args.format,
|
||||
append_date: args.append_date,
|
||||
append_git_hash: args.append_git_hash,
|
||||
line_numbers: args.line_numbers,
|
||||
ignore_hidden: args.ignore_hidden,
|
||||
respect_gitignore: args.respect_gitignore,
|
||||
};
|
||||
|
||||
codebase_to_prompt::run(config)
|
||||
}
|
Reference in New Issue
Block a user