1use crate::core::{git::GitOperations, interactive::Interactive};
2use crate::{GitXError, Result};
3
4pub struct Safety;
6
7impl Safety {
8 pub fn create_backup_branch(prefix: Option<&str>) -> Result<String> {
10 let current_branch = GitOperations::current_branch()?;
11 let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
12 let backup_prefix = prefix.unwrap_or("backup");
13 let backup_name = format!("{backup_prefix}/{current_branch}_{timestamp}");
14
15 crate::core::validation::Validate::branch_name(&backup_name)?;
17
18 let status = std::process::Command::new("git")
20 .args(["branch", &backup_name])
21 .status()?;
22
23 if !status.success() {
24 return Err(GitXError::GitCommand(format!(
25 "Failed to create backup branch '{backup_name}'"
26 )));
27 }
28
29 Ok(backup_name)
30 }
31
32 pub fn ensure_clean_working_directory() -> Result<()> {
34 if Self::is_test_environment() {
36 return Ok(());
37 }
38
39 if !GitOperations::is_working_directory_clean()? {
40 return Err(GitXError::GitCommand(
41 "Working directory is not clean. Please commit or stash your changes first."
42 .to_string(),
43 ));
44 }
45
46 Ok(())
47 }
48
49 pub fn confirm_destructive_operation(operation: &str, details: &str) -> Result<bool> {
51 let prompt = format!(
52 "⚠️ {operation} This is a destructive operation.\n{details}\nDo you want to continue?"
53 );
54
55 if !Interactive::is_interactive() {
56 eprintln!("Warning: Skipping confirmation in non-interactive environment");
59 return Ok(true);
60 }
61
62 Interactive::confirm(&prompt, false)
63 }
64
65 fn is_test_environment() -> bool {
67 std::env::var("CARGO_TARGET_TMPDIR").is_ok() || std::env::var("CI").is_ok() || cfg!(test)
68 }
69
70 pub fn create_checkpoint(message: Option<&str>) -> Result<String> {
72 let checkpoint_msg = message.unwrap_or("git-x safety checkpoint");
73
74 let output = std::process::Command::new("git")
75 .args(["stash", "push", "-m", checkpoint_msg])
76 .output()?;
77
78 if !output.status.success() {
79 let stderr = String::from_utf8_lossy(&output.stderr);
80 return Err(GitXError::GitCommand(format!(
81 "Failed to create safety checkpoint: {stderr}"
82 )));
83 }
84
85 Ok(checkpoint_msg.to_string())
86 }
87
88 pub fn restore_checkpoint() -> Result<()> {
90 let status = std::process::Command::new("git")
91 .args(["stash", "pop"])
92 .status()?;
93
94 if !status.success() {
95 return Err(GitXError::GitCommand(
96 "Failed to restore from safety checkpoint".to_string(),
97 ));
98 }
99
100 Ok(())
101 }
102
103 pub fn list_backup_branches() -> Result<Vec<String>> {
105 let output = std::process::Command::new("git")
106 .args(["branch", "--list", "backup/*"])
107 .output()?;
108
109 if !output.status.success() {
110 return Err(GitXError::GitCommand(
111 "Failed to list backup branches".to_string(),
112 ));
113 }
114
115 let stdout = String::from_utf8_lossy(&output.stdout);
116 let branches: Vec<String> = stdout
117 .lines()
118 .map(|line| line.trim().trim_start_matches("* ").to_string())
119 .filter(|branch| !branch.is_empty())
120 .collect();
121
122 Ok(branches)
123 }
124
125 pub fn cleanup_old_backups(days: u32, dry_run: bool) -> Result<Vec<String>> {
127 let backup_branches = Self::list_backup_branches()?;
128 let mut removed_branches = Vec::new();
129
130 for branch in backup_branches {
131 if Self::is_branch_older_than(&branch, days)? {
132 if dry_run {
133 removed_branches.push(format!("[DRY RUN] Would delete: {branch}"));
134 } else {
135 let status = std::process::Command::new("git")
136 .args(["branch", "-D", &branch])
137 .status()?;
138
139 if status.success() {
140 removed_branches.push(format!("Deleted: {branch}"));
141 } else {
142 removed_branches.push(format!("Failed to delete: {branch}"));
143 }
144 }
145 }
146 }
147
148 Ok(removed_branches)
149 }
150
151 fn is_branch_older_than(branch: &str, days: u32) -> Result<bool> {
153 let output = std::process::Command::new("git")
154 .args(["log", "-1", "--format=%ct", branch])
155 .output()?;
156
157 if !output.status.success() {
158 return Err(GitXError::GitCommand(format!(
159 "Failed to get branch date for '{branch}'"
160 )));
161 }
162
163 let timestamp_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
164 let timestamp: i64 = timestamp_str
165 .parse()
166 .map_err(|_| GitXError::Parse(format!("Invalid timestamp for branch '{branch}'")))?;
167
168 let branch_date = chrono::DateTime::from_timestamp(timestamp, 0)
169 .ok_or_else(|| GitXError::Parse(format!("Invalid date for branch '{branch}'")))?;
170
171 let cutoff_date = chrono::Utc::now() - chrono::Duration::days(days as i64);
172
173 Ok(branch_date < cutoff_date)
174 }
175}
176
177pub struct SafetyBuilder {
179 operation_name: String,
180 backup_needed: bool,
181 checkpoint_needed: bool,
182 confirmation_needed: bool,
183 clean_directory_needed: bool,
184}
185
186impl SafetyBuilder {
187 pub fn new(operation_name: &str) -> Self {
188 Self {
189 operation_name: operation_name.to_string(),
190 backup_needed: false,
191 checkpoint_needed: false,
192 confirmation_needed: false,
193 clean_directory_needed: false,
194 }
195 }
196
197 pub fn with_backup(mut self) -> Self {
198 self.backup_needed = true;
199 self
200 }
201
202 pub fn with_checkpoint(mut self) -> Self {
203 self.checkpoint_needed = true;
204 self
205 }
206
207 pub fn with_confirmation(mut self) -> Self {
208 self.confirmation_needed = true;
209 self
210 }
211
212 pub fn with_clean_directory(mut self) -> Self {
213 self.clean_directory_needed = true;
214 self
215 }
216
217 pub fn execute<F>(self, operation: F) -> Result<String>
218 where
219 F: FnOnce() -> Result<String>,
220 {
221 if self.clean_directory_needed {
223 Safety::ensure_clean_working_directory()?;
224 }
225
226 let backup_name = if self.backup_needed {
227 Some(Safety::create_backup_branch(Some("safety"))?)
228 } else {
229 None
230 };
231
232 if self.checkpoint_needed {
233 Safety::create_checkpoint(Some(&format!("Before {}", self.operation_name)))?;
234 }
235
236 if self.confirmation_needed {
237 let details = if let Some(ref backup) = backup_name {
238 format!("A backup branch '{backup}' has been created.")
239 } else {
240 "No backup will be created.".to_string()
241 };
242
243 if !Safety::confirm_destructive_operation(&self.operation_name, &details)? {
244 return Ok("Operation cancelled by user.".to_string());
245 }
246 }
247
248 match operation() {
250 Ok(result) => Ok(result),
251 Err(e) => {
252 if self.checkpoint_needed {
254 if let Err(restore_err) = Safety::restore_checkpoint() {
255 eprintln!("Warning: Failed to restore checkpoint: {restore_err}");
256 }
257 }
258 Err(e)
259 }
260 }
261 }
262}