codetether_agent/tool/
undo.rs1use super::{Tool, ToolResult};
2use anyhow::Result;
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use serde_json::{Value, json};
6use std::collections::HashMap;
7use std::process::Command;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct UndoInput {
11 #[serde(default = "default_steps")]
13 pub steps: usize,
14 #[serde(default)]
16 pub preview: bool,
17}
18
19fn default_steps() -> usize {
20 1
21}
22
23pub struct UndoTool;
24
25#[async_trait]
26impl Tool for UndoTool {
27 fn id(&self) -> &str {
28 "undo"
29 }
30
31 fn name(&self) -> &str {
32 "Undo"
33 }
34
35 fn description(&self) -> &str {
36 "Undo the last AI-generated changes by reverting git commits"
37 }
38
39 fn parameters(&self) -> Value {
40 json!({
41 "type": "object",
42 "properties": {
43 "steps": {
44 "type": "integer",
45 "minimum": 1,
46 "maximum": 10,
47 "description": "Number of commits to undo",
48 "default": 1
49 },
50 "preview": {
51 "type": "boolean",
52 "description": "Show what would be undone without actually doing it",
53 "default": false
54 }
55 }
56 })
57 }
58
59 async fn execute(&self, input: Value) -> Result<ToolResult> {
60 let params: UndoInput = serde_json::from_value(input)?;
61
62 let cwd = std::env::current_dir()?;
64
65 let status = Command::new("git")
67 .args(["rev-parse", "--git-dir"])
68 .current_dir(&cwd)
69 .status()?;
70
71 if !status.success() {
72 return Ok(ToolResult {
73 output: "Error: Not in a git repository".to_string(),
74 success: false,
75 metadata: HashMap::new(),
76 });
77 }
78
79 let log_output = Command::new("git")
81 .args(["log", "--oneline", "--max-count", ¶ms.steps.to_string()])
82 .current_dir(&cwd)
83 .output()?;
84
85 if !log_output.status.success() {
86 return Ok(ToolResult {
87 output: "Error: Failed to get git log".to_string(),
88 success: false,
89 metadata: HashMap::new(),
90 });
91 }
92
93 let commits = String::from_utf8_lossy(&log_output.stdout);
94 if commits.trim().is_empty() {
95 return Ok(ToolResult {
96 output: "No commits found to undo".to_string(),
97 success: false,
98 metadata: HashMap::new(),
99 });
100 }
101
102 let commit_count = commits.lines().count();
103 let commit_list: Vec<String> = commits.lines().map(|s| s.to_string()).collect();
104
105 if params.preview {
106 let mut preview = format!("Would undo {} commit(s):\n\n", commit_count);
107 for commit in &commit_list {
108 preview.push_str(&format!(" {}\n", commit));
109 }
110
111 let diff_output = Command::new("git")
113 .args(["diff", &format!("HEAD~{}", params.steps), "--name-only"])
114 .current_dir(&cwd)
115 .output()?;
116
117 if diff_output.status.success() {
118 let files = String::from_utf8_lossy(&diff_output.stdout);
119 if !files.trim().is_empty() {
120 preview.push_str("\nFiles that would be affected:\n");
121 for file in files.lines() {
122 preview.push_str(&format!(" {}\n", file));
123 }
124 }
125 }
126
127 return Ok(ToolResult {
128 output: preview,
129 success: true,
130 metadata: HashMap::new(),
131 });
132 }
133
134 let revert_output = Command::new("git")
136 .args(["reset", "--hard", &format!("HEAD~{}", params.steps)])
137 .current_dir(&cwd)
138 .output()?;
139
140 if revert_output.status.success() {
141 let mut result = format!("Successfully undid {} commit(s):\n\n", commit_count);
142 for commit in &commit_list {
143 result.push_str(&format!(" {}\n", commit));
144 }
145
146 Ok(ToolResult {
147 output: result,
148 success: true,
149 metadata: HashMap::new(),
150 })
151 } else {
152 let error = String::from_utf8_lossy(&revert_output.stderr);
153 Ok(ToolResult {
154 output: format!("Failed to undo commits: {}", error),
155 success: false,
156 metadata: HashMap::new(),
157 })
158 }
159 }
160}