use anyhow::{Context, Result};
use clap::Parser;
use git2::{ObjectType, Repository, TreeWalkResult};
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Cli {
#[arg(short, long, value_name = "REPO_PATH", default_value = ".")]
repo: PathBuf,
#[arg(long, value_name = "REVISION", default_value = "HEAD")]
rev: String,
#[arg(
short,
long,
value_name = "OUTPUT_FILE",
default_value = "flattened_files.txt"
)]
output: PathBuf,
#[arg(short, long, value_name = "PATH")]
path: Option<PathBuf>,
}
fn main() -> Result<()> {
let cli = Cli::parse();
eprintln!(
"🔍 Opening repository at: {}",
cli.repo.canonicalize()?.display()
);
let repo = Repository::open(&cli.repo)
.with_context(|| format!("Failed to open repository at '{}'", cli.repo.display()))?;
let rev_obj = repo
.revparse_single(&cli.rev)
.with_context(|| format!("Failed to find revision '{}'", cli.rev))?;
let commit = rev_obj
.peel_to_commit()
.with_context(|| format!("'{}' could not be peeled to a commit", cli.rev))?;
eprintln!(
"🎯 Targeting commit {} ({})",
commit.id(),
commit.summary().unwrap_or("No commit summary")
);
let tree = commit.tree()?;
let output_file = std::fs::File::create(&cli.output)
.with_context(|| format!("Failed to create output file '{}'", cli.output.display()))?;
let mut writer = BufWriter::new(output_file);
eprintln!("🚶 Walking the repository tree and writing to file...");
let normalized_filter_path = cli.path.as_ref().map(|p| {
let mut components = p.components().collect::<Vec<_>>();
if let Some(std::path::Component::CurDir) = components.first() {
components.remove(0);
}
let mut result = PathBuf::new();
for component in components {
result.push(component);
}
if result.as_os_str().is_empty() {
None
} else {
Some(result)
}
}).flatten();
if let Some(ref filter_path) = normalized_filter_path {
eprintln!("🎯 Filtering files under path: {}", filter_path.display());
}
tree.walk(git2::TreeWalkMode::PreOrder, |root, entry| {
if entry.kind() != Some(ObjectType::Blob) {
return TreeWalkResult::Ok;
}
let path = Path::new(root).join(entry.name().unwrap_or("<INVALID_UTF8>"));
if let Some(ref filter_path) = normalized_filter_path {
if !path.starts_with(filter_path) {
return TreeWalkResult::Ok;
}
}
eprintln!(" -> Processing: {}", path.display());
if let Ok(blob) = repo.find_blob(entry.id()) {
if let Err(_) = writeln!(writer, "--- File: {} ---", path.display()) {
return TreeWalkResult::Abort;
}
if blob.is_binary() {
if let Err(_) = writeln!(writer, "[Binary file: content not included]\n") {
eprintln!("Failed to write binary file placeholder");
return TreeWalkResult::Abort;
}
} else {
if let Err(e) = writer.write_all(blob.content()) {
eprintln!("Failed to write file content: {}", e);
return TreeWalkResult::Abort;
}
if let Err(e) = writeln!(writer, "\n") {
eprintln!("Failed to write newline after file content: {}", e);
return TreeWalkResult::Abort;
}
}
}
TreeWalkResult::Ok
})?;
writer.flush()?;
println!(
"\n✅ Success! Repository content flattened to: {}",
cli.output.display()
);
Ok(())
}