Skip to main content

codetether_agent/tool/
undo.rs

1use 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    /// Number of commits to undo (default: 1)
12    #[serde(default = "default_steps")]
13    pub steps: usize,
14    /// Show what would be undone without actually doing it
15    #[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        // Get current directory
63        let cwd = std::env::current_dir()?;
64
65        // Check if we're in a git repository
66        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        // Get the last N commits
80        let log_output = Command::new("git")
81            .args(["log", "--oneline", "--max-count", &params.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            // Show what files would be affected
112            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        // Actually perform the undo
135        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}