Skip to main content

st/smart/
git_relay.rs

1//! 🔄 GiT Relay - Smart Git CLI Integration with Compression
2//!
3//! This module provides a compressed, intelligent interface to Git CLI
4//! operations without requiring API keys or vendor lock-in. It leverages
5//! our quantum compression and context awareness for maximum efficiency.
6
7use super::context::ContextAnalyzer;
8use super::{SmartResponse, TaskContext, TokenSavings};
9use anyhow::{anyhow, Result};
10use serde::{Deserialize, Serialize};
11use std::path::Path;
12use std::process::{Command, Output};
13
14/// 🔄 Git relay with smart compression and context awareness
15pub struct GitRelay {
16    #[allow(dead_code)]
17    context_analyzer: ContextAnalyzer,
18}
19
20/// 📊 Git operation result with compression
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct GitResult {
23    /// Operation type
24    pub operation: GitOperation,
25    /// Compressed output
26    pub output: String,
27    /// Exit code
28    pub exit_code: i32,
29    /// Context-aware summary
30    pub summary: String,
31    /// Suggested next actions
32    pub suggestions: Vec<String>,
33}
34
35/// 🏷️ Git operation types
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37pub enum GitOperation {
38    Status,
39    Log,
40    Diff,
41    Branch,
42    Remote,
43    Commit,
44    Push,
45    Pull,
46    Clone,
47    Add,
48    Reset,
49    Stash,
50    Tag,
51    Merge,
52    Rebase,
53    Custom(String),
54}
55
56/// 📈 Git relay response with smart compression
57pub type GitRelayResponse = SmartResponse<GitResult>;
58
59impl GitRelay {
60    /// Create new Git relay
61    pub fn new() -> Self {
62        Self {
63            context_analyzer: ContextAnalyzer::new(),
64        }
65    }
66
67    /// 🔄 Execute git command with smart compression
68    pub fn execute(
69        &self,
70        repo_path: &Path,
71        operation: GitOperation,
72        args: &[String],
73        context: Option<&TaskContext>,
74    ) -> Result<GitRelayResponse> {
75        // Build git command
76        let mut cmd = Command::new("git");
77        cmd.current_dir(repo_path);
78
79        // Add operation-specific arguments
80        match &operation {
81            GitOperation::Status => {
82                cmd.args(["status", "--porcelain", "--branch"]);
83            }
84            GitOperation::Log => {
85                cmd.args(["log", "--oneline", "--graph", "--decorate", "-10"]);
86            }
87            GitOperation::Diff => {
88                cmd.args(["diff", "--stat", "--color=never"]);
89            }
90            GitOperation::Branch => {
91                cmd.args(["branch", "-v", "-a"]);
92            }
93            GitOperation::Remote => {
94                cmd.args(["remote", "-v"]);
95            }
96            GitOperation::Custom(op) => {
97                cmd.arg(op);
98            }
99            _ => {
100                return Err(anyhow!("Operation {:?} not yet implemented", operation));
101            }
102        }
103
104        // Add user-provided arguments
105        cmd.args(args);
106
107        // Execute command
108        let output = cmd.output()?;
109
110        // Process and compress output
111        let git_result = self.process_output(operation, output, context)?;
112
113        // Calculate token savings
114        let original_tokens = git_result.output.len() / 4; // Rough estimation
115        let compressed_tokens = git_result.summary.len() / 4;
116        let token_savings = TokenSavings::new(original_tokens, compressed_tokens, "git-relay");
117
118        // Create response
119        let response = GitRelayResponse {
120            primary: vec![git_result.clone()],
121            secondary: vec![],
122            context_summary: format!(
123                "Git {} operation completed",
124                self.operation_name(&git_result.operation)
125            ),
126            token_savings,
127            suggestions: git_result.suggestions.clone(),
128        };
129
130        Ok(response)
131    }
132
133    /// 📊 Smart git status with context awareness
134    pub fn smart_status(
135        &self,
136        repo_path: &Path,
137        context: Option<&TaskContext>,
138    ) -> Result<GitRelayResponse> {
139        self.execute(repo_path, GitOperation::Status, &[], context)
140    }
141
142    /// 📜 Smart git log with relevance filtering
143    pub fn smart_log(
144        &self,
145        repo_path: &Path,
146        limit: Option<usize>,
147        context: Option<&TaskContext>,
148    ) -> Result<GitRelayResponse> {
149        let limit_str = limit.unwrap_or(10).to_string();
150        let args = vec![format!("-{}", limit_str)];
151        self.execute(repo_path, GitOperation::Log, &args, context)
152    }
153
154    /// 🔍 Smart git diff with context filtering
155    pub fn smart_diff(
156        &self,
157        repo_path: &Path,
158        target: Option<&str>,
159        context: Option<&TaskContext>,
160    ) -> Result<GitRelayResponse> {
161        let args = if let Some(t) = target {
162            vec![t.to_string()]
163        } else {
164            vec![]
165        };
166        self.execute(repo_path, GitOperation::Diff, &args, context)
167    }
168
169    /// 🌿 Smart branch information
170    pub fn smart_branches(
171        &self,
172        repo_path: &Path,
173        context: Option<&TaskContext>,
174    ) -> Result<GitRelayResponse> {
175        self.execute(repo_path, GitOperation::Branch, &[], context)
176    }
177
178    /// 🔗 Smart remote information
179    pub fn smart_remotes(
180        &self,
181        repo_path: &Path,
182        context: Option<&TaskContext>,
183    ) -> Result<GitRelayResponse> {
184        self.execute(repo_path, GitOperation::Remote, &[], context)
185    }
186
187    /// 🎯 Execute custom git command with compression
188    pub fn custom_command(
189        &self,
190        repo_path: &Path,
191        command: &str,
192        args: &[String],
193        context: Option<&TaskContext>,
194    ) -> Result<GitRelayResponse> {
195        self.execute(
196            repo_path,
197            GitOperation::Custom(command.to_string()),
198            args,
199            context,
200        )
201    }
202
203    /// Process git command output with smart compression
204    fn process_output(
205        &self,
206        operation: GitOperation,
207        output: Output,
208        context: Option<&TaskContext>,
209    ) -> Result<GitResult> {
210        let stdout = String::from_utf8_lossy(&output.stdout);
211        let stderr = String::from_utf8_lossy(&output.stderr);
212
213        // Combine stdout and stderr
214        let full_output = if stderr.is_empty() {
215            stdout.to_string()
216        } else {
217            format!("{}\nERROR: {}", stdout, stderr)
218        };
219
220        // Generate context-aware summary
221        let summary = self.generate_summary(&operation, &full_output, context);
222
223        // Generate suggestions
224        let suggestions =
225            self.generate_suggestions(&operation, &full_output, output.status.code().unwrap_or(-1));
226
227        Ok(GitResult {
228            operation,
229            output: full_output,
230            exit_code: output.status.code().unwrap_or(-1),
231            summary,
232            suggestions,
233        })
234    }
235
236    /// Generate context-aware summary
237    fn generate_summary(
238        &self,
239        operation: &GitOperation,
240        output: &str,
241        _context: Option<&TaskContext>,
242    ) -> String {
243        match operation {
244            GitOperation::Status => self.summarize_status(output),
245            GitOperation::Log => self.summarize_log(output),
246            GitOperation::Diff => self.summarize_diff(output),
247            GitOperation::Branch => self.summarize_branches(output),
248            GitOperation::Remote => self.summarize_remotes(output),
249            _ => {
250                format!(
251                    "Git {} completed with {} characters of output",
252                    self.operation_name(operation),
253                    output.len()
254                )
255            }
256        }
257    }
258
259    /// Summarize git status output
260    fn summarize_status(&self, output: &str) -> String {
261        let lines: Vec<&str> = output.lines().collect();
262        if lines.is_empty() {
263            return "Repository is clean - no changes detected".to_string();
264        }
265
266        let mut modified = 0;
267        let mut added = 0;
268        let mut deleted = 0;
269        let mut untracked = 0;
270        let mut branch_info = String::new();
271
272        for line in lines {
273            if line.starts_with("##") {
274                branch_info = line.trim_start_matches("## ").to_string();
275            } else if line.starts_with(" M") || line.starts_with("M ") {
276                modified += 1;
277            } else if line.starts_with("A ") || line.starts_with(" A") {
278                added += 1;
279            } else if line.starts_with(" D") || line.starts_with("D ") {
280                deleted += 1;
281            } else if line.starts_with("??") {
282                untracked += 1;
283            }
284        }
285
286        let mut summary = format!("Branch: {}", branch_info);
287        if modified > 0 {
288            summary.push_str(&format!(", {} modified", modified));
289        }
290        if added > 0 {
291            summary.push_str(&format!(", {} added", added));
292        }
293        if deleted > 0 {
294            summary.push_str(&format!(", {} deleted", deleted));
295        }
296        if untracked > 0 {
297            summary.push_str(&format!(", {} untracked", untracked));
298        }
299
300        summary
301    }
302
303    /// Summarize git log output
304    fn summarize_log(&self, output: &str) -> String {
305        let lines: Vec<&str> = output.lines().collect();
306        let commit_count = lines.iter().filter(|line| line.contains("*")).count();
307
308        if commit_count == 0 {
309            "No commits found".to_string()
310        } else {
311            format!("Last {} commits shown", commit_count)
312        }
313    }
314
315    /// Summarize git diff output
316    fn summarize_diff(&self, output: &str) -> String {
317        if output.trim().is_empty() {
318            "No differences found".to_string()
319        } else {
320            let lines: Vec<&str> = output.lines().collect();
321            let file_count = lines.iter().filter(|line| line.contains("|")).count();
322            format!("Changes in {} files", file_count)
323        }
324    }
325
326    /// Summarize git branch output
327    fn summarize_branches(&self, output: &str) -> String {
328        let lines: Vec<&str> = output.lines().collect();
329        let local_branches = lines
330            .iter()
331            .filter(|line| !line.contains("remotes/"))
332            .count();
333        let remote_branches = lines
334            .iter()
335            .filter(|line| line.contains("remotes/"))
336            .count();
337
338        format!(
339            "{} local branches, {} remote branches",
340            local_branches, remote_branches
341        )
342    }
343
344    /// Summarize git remote output
345    fn summarize_remotes(&self, output: &str) -> String {
346        let lines: Vec<&str> = output.lines().collect();
347        let remote_count = lines.len() / 2; // Each remote has fetch and push URLs
348
349        if remote_count == 0 {
350            "No remotes configured".to_string()
351        } else {
352            format!("{} remote(s) configured", remote_count)
353        }
354    }
355
356    /// Generate operation-specific suggestions
357    fn generate_suggestions(
358        &self,
359        operation: &GitOperation,
360        output: &str,
361        exit_code: i32,
362    ) -> Vec<String> {
363        let mut suggestions = Vec::new();
364
365        if exit_code != 0 {
366            suggestions.push("Command failed - check git status and repository state".to_string());
367            return suggestions;
368        }
369
370        match operation {
371            GitOperation::Status => {
372                if output.contains("??") {
373                    suggestions.push("Use 'git add .' to stage untracked files".to_string());
374                }
375                if output.contains(" M") || output.contains("M ") {
376                    suggestions.push("Use 'git add -u' to stage modified files".to_string());
377                }
378                if output.contains("ahead") {
379                    suggestions.push("Use 'git push' to push local commits".to_string());
380                }
381                if output.contains("behind") {
382                    suggestions.push("Use 'git pull' to fetch remote changes".to_string());
383                }
384            }
385            GitOperation::Log => {
386                suggestions.push("Use smart_diff to see changes in recent commits".to_string());
387                suggestions.push("Use smart_branches to see branch information".to_string());
388            }
389            GitOperation::Diff => {
390                if !output.trim().is_empty() {
391                    suggestions.push("Review changes before committing".to_string());
392                    suggestions.push("Use 'git add' to stage specific changes".to_string());
393                }
394            }
395            GitOperation::Branch => {
396                suggestions.push("Use 'git checkout <branch>' to switch branches".to_string());
397                suggestions
398                    .push("Use 'git branch -d <branch>' to delete merged branches".to_string());
399            }
400            _ => {}
401        }
402
403        suggestions
404    }
405
406    /// Get human-readable operation name
407    fn operation_name<'a>(&self, operation: &'a GitOperation) -> &'a str {
408        match operation {
409            GitOperation::Status => "status",
410            GitOperation::Log => "log",
411            GitOperation::Diff => "diff",
412            GitOperation::Branch => "branch",
413            GitOperation::Remote => "remote",
414            GitOperation::Commit => "commit",
415            GitOperation::Push => "push",
416            GitOperation::Pull => "pull",
417            GitOperation::Clone => "clone",
418            GitOperation::Add => "add",
419            GitOperation::Reset => "reset",
420            GitOperation::Stash => "stash",
421            GitOperation::Tag => "tag",
422            GitOperation::Merge => "merge",
423            GitOperation::Rebase => "rebase",
424            GitOperation::Custom(op) => op,
425        }
426    }
427}
428
429impl Default for GitRelay {
430    fn default() -> Self {
431        Self::new()
432    }
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438    // use std::path::PathBuf;  // Commented out as unused
439
440    #[test]
441    fn test_git_relay_creation() {
442        let relay = GitRelay::new();
443        assert_eq!(relay.operation_name(&GitOperation::Status), "status");
444    }
445
446    #[test]
447    fn test_status_summary() {
448        let relay = GitRelay::new();
449        let output = "## main...origin/main\n M file1.rs\n?? file2.rs\n";
450        let summary = relay.summarize_status(output);
451        assert!(summary.contains("main"));
452        assert!(summary.contains("modified"));
453        assert!(summary.contains("untracked"));
454    }
455}