1use 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
14pub struct GitRelay {
16 #[allow(dead_code)]
17 context_analyzer: ContextAnalyzer,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct GitResult {
23 pub operation: GitOperation,
25 pub output: String,
27 pub exit_code: i32,
29 pub summary: String,
31 pub suggestions: Vec<String>,
33}
34
35#[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
56pub type GitRelayResponse = SmartResponse<GitResult>;
58
59impl GitRelay {
60 pub fn new() -> Self {
62 Self {
63 context_analyzer: ContextAnalyzer::new(),
64 }
65 }
66
67 pub fn execute(
69 &self,
70 repo_path: &Path,
71 operation: GitOperation,
72 args: &[String],
73 context: Option<&TaskContext>,
74 ) -> Result<GitRelayResponse> {
75 let mut cmd = Command::new("git");
77 cmd.current_dir(repo_path);
78
79 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 cmd.args(args);
106
107 let output = cmd.output()?;
109
110 let git_result = self.process_output(operation, output, context)?;
112
113 let original_tokens = git_result.output.len() / 4; let compressed_tokens = git_result.summary.len() / 4;
116 let token_savings = TokenSavings::new(original_tokens, compressed_tokens, "git-relay");
117
118 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 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 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 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 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 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 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 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 let full_output = if stderr.is_empty() {
215 stdout.to_string()
216 } else {
217 format!("{}\nERROR: {}", stdout, stderr)
218 };
219
220 let summary = self.generate_summary(&operation, &full_output, context);
222
223 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 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 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 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 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 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 fn summarize_remotes(&self, output: &str) -> String {
346 let lines: Vec<&str> = output.lines().collect();
347 let remote_count = lines.len() / 2; if remote_count == 0 {
350 "No remotes configured".to_string()
351 } else {
352 format!("{} remote(s) configured", remote_count)
353 }
354 }
355
356 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 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 #[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}