use crate::cli::{Commands, Config};
use crate::core::{cache::FileCache, context_builder::ContextOptions};
use crate::utils::git;
use anyhow::{anyhow, Result};
use std::path::PathBuf;
use std::sync::Arc;
use tracing::{debug, info};
pub fn run_diff(config: Config) -> Result<()> {
let (from, to) = match &config.command {
Some(Commands::Diff { from, to }) => (from.clone(), to.clone()),
_ => return Err(anyhow!("Invalid command for diff execution")),
};
let working_dir = std::env::current_dir()?;
if !git::is_git_repository(&working_dir) {
return Err(anyhow!(
"Not in a git repository. Please run this command from within a git repository."
));
}
info!("Analyzing git diff between {} and {}", from, to);
let changed_files = match git::get_changed_files(&working_dir, &from, &to) {
Ok(files) => files,
Err(e) => {
return Err(anyhow!("Failed to get changed files: {}", e));
}
};
if changed_files.is_empty() {
println!("No files changed between {from} and {to}");
return Ok(());
}
info!("Found {} changed files", changed_files.len());
let stats = git::get_diff_stats(&working_dir, &from, &to)?;
let cache = Arc::new(FileCache::new());
let context_options = ContextOptions::from_config(&config)?;
let mut valid_files = Vec::new();
for file in changed_files {
if file.exists() && file.is_file() {
valid_files.push(file);
} else {
debug!("Skipping non-existent or non-file: {:?}", file);
}
}
if valid_files.is_empty() {
println!("No valid files to process in the diff.");
return Ok(());
}
let files_to_process = if let Some(max_tokens) = context_options.max_tokens {
debug!("Token limit enabled: {}", max_tokens);
let max_files = (max_tokens / 1000).max(1).min(valid_files.len());
valid_files.into_iter().take(max_files).collect()
} else {
valid_files
};
let mut markdown = generate_diff_markdown(DiffMarkdownParams {
from: &from,
to: &to,
stats: &stats,
files: &files_to_process,
cache,
})?;
if config.trace_imports || config.include_callers || config.include_types {
info!("Performing semantic analysis on changed files");
markdown.push_str("\n\n## Semantic Analysis\n\n");
markdown.push_str("*Semantic analysis integration is in development*\n");
}
if let Some(output_file) = &config.output_file {
std::fs::write(output_file, &markdown)?;
info!("Diff analysis written to: {:?}", output_file);
} else {
print!("{markdown}");
}
Ok(())
}
struct DiffMarkdownParams<'a> {
from: &'a str,
to: &'a str,
stats: &'a git::DiffStats,
files: &'a [PathBuf],
cache: Arc<FileCache>,
}
fn generate_diff_markdown(params: DiffMarkdownParams) -> Result<String> {
let mut markdown = String::new();
markdown.push_str(&format!(
"# Git Diff Analysis: {} → {}\n\n",
params.from, params.to
));
markdown.push_str("## Diff Statistics\n\n");
markdown.push_str(&format!(
"- **Files changed**: {}\n",
params.stats.files_changed
));
markdown.push_str(&format!("- **Lines added**: {}\n", params.stats.insertions));
markdown.push_str(&format!(
"- **Lines removed**: {}\n",
params.stats.deletions
));
markdown.push('\n');
markdown.push_str("## Changed Files\n\n");
for file in params.files {
let relative_path = file.strip_prefix(std::env::current_dir()?).unwrap_or(file);
markdown.push_str(&format!("- `{}`\n", relative_path.display()));
}
markdown.push('\n');
markdown.push_str("## File Contents\n\n");
for file in params.files {
let relative_path = file.strip_prefix(std::env::current_dir()?).unwrap_or(file);
markdown.push_str(&format!("### {}\n\n", relative_path.display()));
let extension = file.extension().and_then(|ext| ext.to_str()).unwrap_or("");
let language = match extension {
"rs" => "rust",
"py" => "python",
"js" => "javascript",
"ts" => "typescript",
"go" => "go",
"java" => "java",
"cpp" | "cc" | "cxx" => "cpp",
"c" => "c",
"h" | "hpp" => "c",
"sh" => "bash",
"yml" | "yaml" => "yaml",
"json" => "json",
"toml" => "toml",
"md" => "markdown",
_ => "",
};
match params.cache.get_or_load(file) {
Ok(content) => {
markdown.push_str(&format!("```{language}\n{content}\n```\n\n"));
}
Err(e) => {
markdown.push_str(&format!("*Error reading file: {e}*\n\n"));
}
}
}
let total_tokens = estimate_token_count(&markdown);
markdown.push_str("## Context Statistics\n\n");
markdown.push_str(&format!("- **Files processed**: {}\n", params.files.len()));
markdown.push_str(&format!("- **Estimated tokens**: {total_tokens}\n"));
Ok(markdown)
}
fn estimate_token_count(text: &str) -> usize {
text.len() / 4
}