context_creator/commands/
diff.rs

1//! Git diff command implementation
2
3use 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
11/// Run the git diff command
12pub 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    // Determine the working directory (current directory by default)
19    let working_dir = std::env::current_dir()?;
20
21    // Check if we're in a git repository
22    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    // Get the list of changed files
31    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    // Get diff statistics for summary
46    let stats = git::get_diff_stats(&working_dir, &from, &to)?;
47
48    // Create a cache for file operations
49    let cache = Arc::new(FileCache::new());
50
51    // Create context options
52    let context_options = ContextOptions::from_config(&config)?;
53
54    // Filter to only include changed files that exist and are readable
55    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    // Apply basic token limits if needed (simplified for now)
70    let files_to_process = if let Some(max_tokens) = context_options.max_tokens {
71        debug!("Token limit enabled: {}", max_tokens);
72        // For now, just limit the number of files. A proper implementation would
73        // estimate token usage per file and prioritize accordingly.
74        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    // Generate the diff markdown
81    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    // Handle semantic analysis if requested
90    if config.trace_imports || config.include_callers || config.include_types {
91        info!("Performing semantic analysis on changed files");
92        // For now, add a placeholder - full semantic integration would require more work
93        markdown.push_str("\n\n## Semantic Analysis\n\n");
94        markdown.push_str("*Semantic analysis integration is in development*\n");
95    }
96
97    // Output the result
98    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
108/// Parameters for generating diff markdown
109struct 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
117/// Generate markdown content for the diff
118fn generate_diff_markdown(params: DiffMarkdownParams) -> Result<String> {
119    let mut markdown = String::new();
120
121    // Header
122    markdown.push_str(&format!(
123        "# Git Diff Analysis: {} → {}\n\n",
124        params.from, params.to
125    ));
126
127    // Statistics
128    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    // Changed files summary
141    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    // File contents
149    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        // Determine file extension for syntax highlighting
157        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        // Read file content
178        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    // Context statistics
189    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
197/// Simple token estimation (rough approximation)
198fn estimate_token_count(text: &str) -> usize {
199    // Very rough approximation: 1 token ≈ 4 characters
200    text.len() / 4
201}