1use async_trait::async_trait;
2use limit_agent::error::AgentError;
3use limit_agent::Tool;
4use serde_json::Value;
5use std::process::Command;
6
7fn check_git_available() -> Result<(), AgentError> {
9 let result = Command::new("git").arg("--version").output();
10
11 match result {
12 Ok(output) if output.status.success() => Ok(()),
13 Ok(_) => Err(AgentError::ToolError(
14 "git command failed to execute".to_string(),
15 )),
16 Err(_) => Err(AgentError::ToolError(
17 "git not found in PATH. Please install git 2.0 or later.".to_string(),
18 )),
19 }
20}
21
22pub struct GitStatusTool;
23
24impl GitStatusTool {
25 pub fn new() -> Self {
26 GitStatusTool
27 }
28}
29
30impl Default for GitStatusTool {
31 fn default() -> Self {
32 Self::new()
33 }
34}
35
36#[async_trait]
37impl Tool for GitStatusTool {
38 fn name(&self) -> &str {
39 "git_status"
40 }
41
42 async fn execute(&self, _args: Value) -> Result<Value, AgentError> {
43 check_git_available()?;
44
45 let output = Command::new("git")
46 .args(["status", "--porcelain"])
47 .output()
48 .map_err(|e| AgentError::ToolError(format!("Failed to execute git status: {}", e)))?;
49
50 if !output.status.success() {
51 let stderr = String::from_utf8_lossy(&output.stderr);
52 return Err(AgentError::ToolError(format!(
53 "git status failed: {}",
54 stderr
55 )));
56 }
57
58 let stdout = String::from_utf8_lossy(&output.stdout);
59 let lines: Vec<&str> = stdout.lines().collect();
60
61 Ok(serde_json::json!({
62 "changes": lines,
63 "count": lines.len()
64 }))
65 }
66}
67
68pub struct GitDiffTool;
69
70impl GitDiffTool {
71 pub fn new() -> Self {
72 GitDiffTool
73 }
74}
75
76impl Default for GitDiffTool {
77 fn default() -> Self {
78 Self::new()
79 }
80}
81
82#[async_trait]
83impl Tool for GitDiffTool {
84 fn name(&self) -> &str {
85 "git_diff"
86 }
87
88 async fn execute(&self, _args: Value) -> Result<Value, AgentError> {
89 check_git_available()?;
90
91 let output = Command::new("git")
92 .args(["diff"])
93 .output()
94 .map_err(|e| AgentError::ToolError(format!("Failed to execute git diff: {}", e)))?;
95
96 if !output.status.success() {
97 let stderr = String::from_utf8_lossy(&output.stderr);
98 return Err(AgentError::ToolError(format!(
99 "git diff failed: {}",
100 stderr
101 )));
102 }
103
104 let stdout = String::from_utf8_lossy(&output.stdout);
105
106 Ok(serde_json::json!({
107 "diff": stdout,
108 "size": stdout.len()
109 }))
110 }
111}
112
113pub struct GitLogTool;
114
115impl GitLogTool {
116 pub fn new() -> Self {
117 GitLogTool
118 }
119}
120
121impl Default for GitLogTool {
122 fn default() -> Self {
123 Self::new()
124 }
125}
126
127#[async_trait]
128impl Tool for GitLogTool {
129 fn name(&self) -> &str {
130 "git_log"
131 }
132
133 async fn execute(&self, args: Value) -> Result<Value, AgentError> {
134 check_git_available()?;
135
136 let count = args.get("count").and_then(|v| v.as_u64()).unwrap_or(10);
138
139 let output = Command::new("git")
140 .args(["log", &format!("-{}", count), "--oneline"])
141 .output()
142 .map_err(|e| AgentError::ToolError(format!("Failed to execute git log: {}", e)))?;
143
144 if !output.status.success() {
145 let stderr = String::from_utf8_lossy(&output.stderr);
146 return Err(AgentError::ToolError(format!("git log failed: {}", stderr)));
147 }
148
149 let stdout = String::from_utf8_lossy(&output.stdout);
150 let commits: Vec<&str> = stdout.lines().collect();
151
152 Ok(serde_json::json!({
153 "commits": commits,
154 "count": commits.len()
155 }))
156 }
157}
158
159pub struct GitAddTool;
160
161impl GitAddTool {
162 pub fn new() -> Self {
163 GitAddTool
164 }
165}
166
167impl Default for GitAddTool {
168 fn default() -> Self {
169 Self::new()
170 }
171}
172
173#[async_trait]
174impl Tool for GitAddTool {
175 fn name(&self) -> &str {
176 "git_add"
177 }
178
179 async fn execute(&self, args: Value) -> Result<Value, AgentError> {
180 check_git_available()?;
181
182 let files: Vec<String> = serde_json::from_value(args["files"].clone())
183 .map_err(|e| AgentError::ToolError(format!("Invalid files argument: {}", e)))?;
184
185 if files.is_empty() {
186 return Err(AgentError::ToolError(
187 "files argument cannot be empty".to_string(),
188 ));
189 }
190
191 let mut cmd = Command::new("git");
192 cmd.arg("add");
193 for file in &files {
194 cmd.arg(file);
195 }
196
197 let output = cmd
198 .output()
199 .map_err(|e| AgentError::ToolError(format!("Failed to execute git add: {}", e)))?;
200
201 if !output.status.success() {
202 let stderr = String::from_utf8_lossy(&output.stderr);
203 return Err(AgentError::ToolError(format!("git add failed: {}", stderr)));
204 }
205
206 Ok(serde_json::json!({
207 "success": true,
208 "files": files,
209 "count": files.len()
210 }))
211 }
212}
213
214pub struct GitCommitTool;
215
216impl GitCommitTool {
217 pub fn new() -> Self {
218 GitCommitTool
219 }
220}
221
222impl Default for GitCommitTool {
223 fn default() -> Self {
224 Self::new()
225 }
226}
227
228#[async_trait]
229impl Tool for GitCommitTool {
230 fn name(&self) -> &str {
231 "git_commit"
232 }
233
234 async fn execute(&self, args: Value) -> Result<Value, AgentError> {
235 check_git_available()?;
236
237 let message: String = serde_json::from_value(args["message"].clone())
238 .map_err(|e| AgentError::ToolError(format!("Invalid message argument: {}", e)))?;
239
240 if message.trim().is_empty() {
241 return Err(AgentError::ToolError(
242 "message argument cannot be empty".to_string(),
243 ));
244 }
245
246 let output = Command::new("git")
247 .args(["commit", "-m", &message])
248 .output()
249 .map_err(|e| AgentError::ToolError(format!("Failed to execute git commit: {}", e)))?;
250
251 if !output.status.success() {
252 let stderr = String::from_utf8_lossy(&output.stderr);
253 return Err(AgentError::ToolError(format!(
254 "git commit failed: {}",
255 stderr
256 )));
257 }
258
259 let stdout = String::from_utf8_lossy(&output.stdout);
260
261 Ok(serde_json::json!({
262 "success": true,
263 "message": message,
264 "output": stdout
265 }))
266 }
267}
268
269pub struct GitPushTool;
270
271impl GitPushTool {
272 pub fn new() -> Self {
273 GitPushTool
274 }
275}
276
277impl Default for GitPushTool {
278 fn default() -> Self {
279 Self::new()
280 }
281}
282
283#[async_trait]
284impl Tool for GitPushTool {
285 fn name(&self) -> &str {
286 "git_push"
287 }
288
289 async fn execute(&self, args: Value) -> Result<Value, AgentError> {
290 check_git_available()?;
291
292 let remote = args
294 .get("remote")
295 .and_then(|v| v.as_str())
296 .unwrap_or("origin");
297
298 let branch = args.get("branch").and_then(|v| v.as_str()).unwrap_or("");
299
300 let output = if branch.is_empty() {
301 Command::new("git")
302 .args(["push", remote])
303 .output()
304 .map_err(|e| AgentError::ToolError(format!("Failed to execute git push: {}", e)))?
305 } else {
306 Command::new("git")
307 .args(["push", remote, branch])
308 .output()
309 .map_err(|e| AgentError::ToolError(format!("Failed to execute git push: {}", e)))?
310 };
311
312 if !output.status.success() {
313 let stderr = String::from_utf8_lossy(&output.stderr);
314 return Err(AgentError::ToolError(format!(
315 "git push failed: {}",
316 stderr
317 )));
318 }
319
320 let stdout = String::from_utf8_lossy(&output.stdout);
321
322 Ok(serde_json::json!({
323 "success": true,
324 "remote": remote,
325 "branch": if branch.is_empty() { "(default)" } else { branch },
326 "output": stdout
327 }))
328 }
329}
330
331pub struct GitPullTool;
332
333impl GitPullTool {
334 pub fn new() -> Self {
335 GitPullTool
336 }
337}
338
339impl Default for GitPullTool {
340 fn default() -> Self {
341 Self::new()
342 }
343}
344
345#[async_trait]
346impl Tool for GitPullTool {
347 fn name(&self) -> &str {
348 "git_pull"
349 }
350
351 async fn execute(&self, args: Value) -> Result<Value, AgentError> {
352 check_git_available()?;
353
354 let remote = args
356 .get("remote")
357 .and_then(|v| v.as_str())
358 .unwrap_or("origin");
359
360 let branch = args.get("branch").and_then(|v| v.as_str()).unwrap_or("");
361
362 let output = if branch.is_empty() {
363 Command::new("git")
364 .args(["pull", remote])
365 .output()
366 .map_err(|e| AgentError::ToolError(format!("Failed to execute git pull: {}", e)))?
367 } else {
368 Command::new("git")
369 .args(["pull", remote, branch])
370 .output()
371 .map_err(|e| AgentError::ToolError(format!("Failed to execute git pull: {}", e)))?
372 };
373
374 if !output.status.success() {
375 let stderr = String::from_utf8_lossy(&output.stderr);
376 return Err(AgentError::ToolError(format!(
377 "git pull failed: {}",
378 stderr
379 )));
380 }
381
382 let stdout = String::from_utf8_lossy(&output.stdout);
383
384 Ok(serde_json::json!({
385 "success": true,
386 "remote": remote,
387 "branch": if branch.is_empty() { "(default)" } else { branch },
388 "output": stdout
389 }))
390 }
391}
392
393pub struct GitCloneTool;
394
395impl GitCloneTool {
396 pub fn new() -> Self {
397 GitCloneTool
398 }
399}
400
401impl Default for GitCloneTool {
402 fn default() -> Self {
403 Self::new()
404 }
405}
406
407#[async_trait]
408impl Tool for GitCloneTool {
409 fn name(&self) -> &str {
410 "git_clone"
411 }
412
413 async fn execute(&self, args: Value) -> Result<Value, AgentError> {
414 check_git_available()?;
415
416 let url: String = serde_json::from_value(args["url"].clone())
417 .map_err(|e| AgentError::ToolError(format!("Invalid url argument: {}", e)))?;
418
419 if url.trim().is_empty() {
420 return Err(AgentError::ToolError(
421 "url argument cannot be empty".to_string(),
422 ));
423 }
424
425 let directory = args.get("directory").and_then(|v| v.as_str());
427
428 let output = if let Some(dir) = directory {
429 Command::new("git")
430 .args(["clone", &url, dir])
431 .output()
432 .map_err(|e| AgentError::ToolError(format!("Failed to execute git clone: {}", e)))?
433 } else {
434 Command::new("git")
435 .args(["clone", &url])
436 .output()
437 .map_err(|e| AgentError::ToolError(format!("Failed to execute git clone: {}", e)))?
438 };
439
440 if !output.status.success() {
441 let stderr = String::from_utf8_lossy(&output.stderr);
442 return Err(AgentError::ToolError(format!(
443 "git clone failed: {}",
444 stderr
445 )));
446 }
447
448 let stdout = String::from_utf8_lossy(&output.stdout);
449
450 Ok(serde_json::json!({
451 "success": true,
452 "url": url,
453 "directory": directory.unwrap_or("(default)"),
454 "output": stdout
455 }))
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462
463 #[tokio::test]
464 async fn test_git_status_tool_name() {
465 let tool = GitStatusTool::new();
466 assert_eq!(tool.name(), "git_status");
467 }
468
469 #[tokio::test]
470 async fn test_git_status_tool_default() {
471 let tool = GitStatusTool;
472 assert_eq!(tool.name(), "git_status");
473 }
474
475 #[tokio::test]
476 async fn test_git_diff_tool_name() {
477 let tool = GitDiffTool::new();
478 assert_eq!(tool.name(), "git_diff");
479 }
480
481 #[tokio::test]
482 async fn test_git_log_tool_name() {
483 let tool = GitLogTool::new();
484 assert_eq!(tool.name(), "git_log");
485 }
486
487 #[tokio::test]
488 async fn test_git_log_tool_default_count() {
489 let tool = GitLogTool::new();
490 let args = serde_json::json!({});
491
492 let result = tool.execute(args).await;
495
496 if let Err(e) = result {
499 assert!(!e.to_string().contains("Invalid count argument"));
500 }
501 }
502
503 #[tokio::test]
504 async fn test_git_log_tool_custom_count() {
505 let tool = GitLogTool::new();
506 let args = serde_json::json!({"count": 5});
507
508 let result = tool.execute(args).await;
509
510 if let Err(e) = result {
512 assert!(!e.to_string().contains("Invalid count argument"));
513 }
514 }
515
516 #[tokio::test]
517 async fn test_git_add_tool_name() {
518 let tool = GitAddTool::new();
519 assert_eq!(tool.name(), "git_add");
520 }
521
522 #[tokio::test]
523 async fn test_git_add_tool_empty_files() {
524 let tool = GitAddTool::new();
525 let args = serde_json::json!({"files": []});
526
527 let result = tool.execute(args).await;
528 assert!(result.is_err());
529 assert!(result.unwrap_err().to_string().contains("cannot be empty"));
530 }
531
532 #[tokio::test]
533 async fn test_git_add_tool_invalid_files() {
534 let tool = GitAddTool::new();
535 let args = serde_json::json!({}); let result = tool.execute(args).await;
538 assert!(result.is_err());
539 assert!(result.unwrap_err().to_string().contains("Invalid files"));
540 }
541
542 #[tokio::test]
543 async fn test_git_commit_tool_name() {
544 let tool = GitCommitTool::new();
545 assert_eq!(tool.name(), "git_commit");
546 }
547
548 #[tokio::test]
549 async fn test_git_commit_tool_empty_message() {
550 let tool = GitCommitTool::new();
551 let args = serde_json::json!({"message": " "});
552
553 let result = tool.execute(args).await;
554 assert!(result.is_err());
555 assert!(result.unwrap_err().to_string().contains("cannot be empty"));
556 }
557
558 #[tokio::test]
559 async fn test_git_commit_tool_invalid_message() {
560 let tool = GitCommitTool::new();
561 let args = serde_json::json!({}); let result = tool.execute(args).await;
564 assert!(result.is_err());
565 assert!(result.unwrap_err().to_string().contains("Invalid message"));
566 }
567
568 #[tokio::test]
569 async fn test_git_push_tool_name() {
570 let tool = GitPushTool::new();
571 assert_eq!(tool.name(), "git_push");
572 }
573
574 #[tokio::test]
575 async fn test_git_push_tool_default_values() {
576 let tool = GitPushTool::new();
577 let args = serde_json::json!({});
578
579 let result = tool.execute(args).await;
580
581 if let Err(e) = result {
583 assert!(!e.to_string().contains("Invalid"));
585 }
586 }
587
588 #[tokio::test]
589 async fn test_git_push_tool_custom_values() {
590 let tool = GitPushTool::new();
591 let args = serde_json::json!({
592 "remote": "upstream",
593 "branch": "feature"
594 });
595
596 let result = tool.execute(args).await;
597
598 if let Err(e) = result {
600 assert!(!e.to_string().contains("Invalid"));
601 }
602 }
603
604 #[tokio::test]
605 async fn test_git_pull_tool_name() {
606 let tool = GitPullTool::new();
607 assert_eq!(tool.name(), "git_pull");
608 }
609
610 #[tokio::test]
611 async fn test_git_pull_tool_default_values() {
612 let tool = GitPullTool::new();
613 let args = serde_json::json!({});
614
615 let result = tool.execute(args).await;
616
617 if let Err(e) = result {
619 assert!(!e.to_string().contains("Invalid"));
621 }
622 }
623
624 #[tokio::test]
625 async fn test_git_clone_tool_name() {
626 let tool = GitCloneTool::new();
627 assert_eq!(tool.name(), "git_clone");
628 }
629
630 #[tokio::test]
631 async fn test_git_clone_tool_empty_url() {
632 let tool = GitCloneTool::new();
633 let args = serde_json::json!({"url": ""});
634
635 let result = tool.execute(args).await;
636 assert!(result.is_err());
637 assert!(result.unwrap_err().to_string().contains("cannot be empty"));
638 }
639
640 #[tokio::test]
641 async fn test_git_clone_tool_invalid_url() {
642 let tool = GitCloneTool::new();
643 let args = serde_json::json!({}); let result = tool.execute(args).await;
646 assert!(result.is_err());
647 assert!(result.unwrap_err().to_string().contains("Invalid url"));
648 }
649
650 #[tokio::test]
651 async fn test_git_clone_tool_custom_directory() {
652 let tool = GitCloneTool::new();
653 let args = serde_json::json!({
654 "url": "https://github.com/test/repo.git",
655 "directory": "my-repo"
656 });
657
658 let result = tool.execute(args).await;
659
660 if let Err(e) = result {
662 assert!(!e.to_string().contains("Invalid"));
664 }
665 }
666
667 #[tokio::test]
668 async fn test_all_tools_implement_default() {
669 let _status = GitStatusTool;
671 let _diff = GitDiffTool;
672 let _log = GitLogTool;
673 let _add = GitAddTool;
674 let _commit = GitCommitTool;
675 let _push = GitPushTool;
676 let _pull = GitPullTool;
677 let _clone = GitCloneTool;
678 }
679}