context_creator/commands/
diff.rs1use crate::cli::{Commands, Config};
4use crate::core::{cache::FileCache, context_builder::ContextOptions};
5use crate::utils::git;
6use anyhow::{anyhow, Result};
7use std::path::PathBuf;
8use std::sync::Arc;
9use tracing::{debug, info};
10
11pub fn run_diff(config: Config) -> Result<()> {
13 let (from, to) = match &config.command {
14 Some(Commands::Diff { from, to }) => (from.clone(), to.clone()),
15 _ => return Err(anyhow!("Invalid command for diff execution")),
16 };
17
18 let working_dir = std::env::current_dir()?;
20
21 if !git::is_git_repository(&working_dir) {
23 return Err(anyhow!(
24 "Not in a git repository. Please run this command from within a git repository."
25 ));
26 }
27
28 info!("Analyzing git diff between {} and {}", from, to);
29
30 let changed_files = match git::get_changed_files(&working_dir, &from, &to) {
32 Ok(files) => files,
33 Err(e) => {
34 return Err(anyhow!("Failed to get changed files: {}", e));
35 }
36 };
37
38 if changed_files.is_empty() {
39 println!("No files changed between {from} and {to}");
40 return Ok(());
41 }
42
43 info!("Found {} changed files", changed_files.len());
44
45 let stats = git::get_diff_stats(&working_dir, &from, &to)?;
47
48 let cache = Arc::new(FileCache::new());
50
51 let context_options = ContextOptions::from_config(&config)?;
53
54 let mut valid_files = Vec::new();
56 for file in changed_files {
57 if file.exists() && file.is_file() {
58 valid_files.push(file);
59 } else {
60 debug!("Skipping non-existent or non-file: {:?}", file);
61 }
62 }
63
64 if valid_files.is_empty() {
65 println!("No valid files to process in the diff.");
66 return Ok(());
67 }
68
69 let files_to_process = if let Some(max_tokens) = context_options.max_tokens {
71 debug!("Token limit enabled: {}", max_tokens);
72 let max_files = (max_tokens / 1000).max(1).min(valid_files.len());
75 valid_files.into_iter().take(max_files).collect()
76 } else {
77 valid_files
78 };
79
80 let mut markdown = generate_diff_markdown(DiffMarkdownParams {
82 from: &from,
83 to: &to,
84 stats: &stats,
85 files: &files_to_process,
86 cache,
87 })?;
88
89 if config.trace_imports || config.include_callers || config.include_types {
91 info!("Performing semantic analysis on changed files");
92 markdown.push_str("\n\n## Semantic Analysis\n\n");
94 markdown.push_str("*Semantic analysis integration is in development*\n");
95 }
96
97 if let Some(output_file) = &config.output_file {
99 std::fs::write(output_file, &markdown)?;
100 info!("Diff analysis written to: {:?}", output_file);
101 } else {
102 print!("{markdown}");
103 }
104
105 Ok(())
106}
107
108struct DiffMarkdownParams<'a> {
110 from: &'a str,
111 to: &'a str,
112 stats: &'a git::DiffStats,
113 files: &'a [PathBuf],
114 cache: Arc<FileCache>,
115}
116
117fn generate_diff_markdown(params: DiffMarkdownParams) -> Result<String> {
119 let mut markdown = String::new();
120
121 markdown.push_str(&format!(
123 "# Git Diff Analysis: {} → {}\n\n",
124 params.from, params.to
125 ));
126
127 markdown.push_str("## Diff Statistics\n\n");
129 markdown.push_str(&format!(
130 "- **Files changed**: {}\n",
131 params.stats.files_changed
132 ));
133 markdown.push_str(&format!("- **Lines added**: {}\n", params.stats.insertions));
134 markdown.push_str(&format!(
135 "- **Lines removed**: {}\n",
136 params.stats.deletions
137 ));
138 markdown.push('\n');
139
140 markdown.push_str("## Changed Files\n\n");
142 for file in params.files {
143 let relative_path = file.strip_prefix(std::env::current_dir()?).unwrap_or(file);
144 markdown.push_str(&format!("- `{}`\n", relative_path.display()));
145 }
146 markdown.push('\n');
147
148 markdown.push_str("## File Contents\n\n");
150
151 for file in params.files {
152 let relative_path = file.strip_prefix(std::env::current_dir()?).unwrap_or(file);
153
154 markdown.push_str(&format!("### {}\n\n", relative_path.display()));
155
156 let extension = file.extension().and_then(|ext| ext.to_str()).unwrap_or("");
158
159 let language = match extension {
160 "rs" => "rust",
161 "py" => "python",
162 "js" => "javascript",
163 "ts" => "typescript",
164 "go" => "go",
165 "java" => "java",
166 "cpp" | "cc" | "cxx" => "cpp",
167 "c" => "c",
168 "h" | "hpp" => "c",
169 "sh" => "bash",
170 "yml" | "yaml" => "yaml",
171 "json" => "json",
172 "toml" => "toml",
173 "md" => "markdown",
174 _ => "",
175 };
176
177 match params.cache.get_or_load(file) {
179 Ok(content) => {
180 markdown.push_str(&format!("```{language}\n{content}\n```\n\n"));
181 }
182 Err(e) => {
183 markdown.push_str(&format!("*Error reading file: {e}*\n\n"));
184 }
185 }
186 }
187
188 let total_tokens = estimate_token_count(&markdown);
190 markdown.push_str("## Context Statistics\n\n");
191 markdown.push_str(&format!("- **Files processed**: {}\n", params.files.len()));
192 markdown.push_str(&format!("- **Estimated tokens**: {total_tokens}\n"));
193
194 Ok(markdown)
195}
196
197fn estimate_token_count(text: &str) -> usize {
199 text.len() / 4
201}