1use anyhow::Result;
19use std::io::{BufRead, BufReader, IsTerminal, Write};
20use std::path::Path;
21use std::sync::Arc;
22
23use crate::contracts::GitRevertMode;
24use crate::git;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum RevertSource {
28 Auto,
29 User,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum RevertOutcome {
34 Reverted { source: RevertSource },
35 Skipped { reason: String },
36 Continue { message: String },
37 Proceed { reason: String },
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum RevertDecision {
42 Revert,
43 Keep,
44 Continue { message: String },
45 Proceed,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct RevertPromptContext {
50 pub label: String,
51 pub allow_proceed: bool,
52 pub preface: Option<String>,
53}
54
55impl RevertPromptContext {
56 pub fn new(label: &str, allow_proceed: bool) -> Self {
57 Self {
58 label: label.to_string(),
59 allow_proceed,
60 preface: None,
61 }
62 }
63
64 pub fn with_preface(mut self, preface: impl Into<String>) -> Self {
65 let preface = preface.into();
66 if preface.trim().is_empty() {
67 return self;
68 }
69 self.preface = Some(preface);
70 self
71 }
72}
73
74pub type RevertPromptHandler =
75 Arc<dyn Fn(&RevertPromptContext) -> Result<RevertDecision> + Send + Sync>;
76
77pub fn apply_git_revert_mode(
78 repo_root: &Path,
79 mode: GitRevertMode,
80 prompt_label: &str,
81 revert_prompt: Option<&RevertPromptHandler>,
82) -> Result<RevertOutcome> {
83 apply_git_revert_mode_with_context(
84 repo_root,
85 mode,
86 RevertPromptContext::new(prompt_label, false),
87 revert_prompt,
88 )
89}
90
91pub fn apply_git_revert_mode_with_context(
92 repo_root: &Path,
93 mode: GitRevertMode,
94 prompt_context: RevertPromptContext,
95 revert_prompt: Option<&RevertPromptHandler>,
96) -> Result<RevertOutcome> {
97 match mode {
98 GitRevertMode::Enabled => {
99 git::revert_uncommitted(repo_root)?;
100 Ok(RevertOutcome::Reverted {
101 source: RevertSource::Auto,
102 })
103 }
104 GitRevertMode::Disabled => Ok(RevertOutcome::Skipped {
105 reason: "git_revert_mode=disabled".to_string(),
106 }),
107 GitRevertMode::Ask => {
108 if let Some(prompt) = revert_prompt {
109 let decision = prompt(&prompt_context)?;
110 return apply_revert_decision(repo_root, decision, prompt_context.allow_proceed);
111 }
112 let stdin = std::io::stdin();
113 if !stdin.is_terminal() {
114 return Ok(RevertOutcome::Skipped {
115 reason: "stdin is not a TTY; keeping changes".to_string(),
116 });
117 }
118 let choice = prompt_revert_choice(&prompt_context)?;
119 apply_revert_decision(repo_root, choice, prompt_context.allow_proceed)
120 }
121 }
122}
123
124fn apply_revert_decision(
125 repo_root: &Path,
126 decision: RevertDecision,
127 allow_proceed: bool,
128) -> Result<RevertOutcome> {
129 match decision {
130 RevertDecision::Revert => {
131 git::revert_uncommitted(repo_root)?;
132 Ok(RevertOutcome::Reverted {
133 source: RevertSource::User,
134 })
135 }
136 RevertDecision::Keep => Ok(RevertOutcome::Skipped {
137 reason: "user chose to keep changes".to_string(),
138 }),
139 RevertDecision::Continue { message } => Ok(RevertOutcome::Continue {
140 message: message.trim_end_matches(['\n', '\r']).to_string(),
141 }),
142 RevertDecision::Proceed => {
143 if allow_proceed {
144 Ok(RevertOutcome::Proceed {
145 reason: "user chose to proceed".to_string(),
146 })
147 } else {
148 Ok(RevertOutcome::Skipped {
149 reason: "proceed not allowed; keeping changes".to_string(),
150 })
151 }
152 }
153 }
154}
155
156pub fn format_revert_failure_message(base: &str, outcome: RevertOutcome) -> String {
157 match outcome {
158 RevertOutcome::Reverted { .. } => format!("{base} Uncommitted changes were reverted."),
159 RevertOutcome::Skipped { reason } => format!("{base} Revert skipped ({reason})."),
160 RevertOutcome::Continue { .. } => {
161 format!("{base} Continue requested. No changes were reverted.")
162 }
163 RevertOutcome::Proceed { .. } => {
164 format!("{base} Proceed requested. No changes were reverted.")
165 }
166 }
167}
168
169fn prompt_revert_choice(prompt_context: &RevertPromptContext) -> Result<RevertDecision> {
170 let stdin = std::io::stdin();
171 let mut reader = BufReader::new(stdin.lock());
172 let mut stderr = std::io::stderr();
173 prompt_revert_choice_with_io(prompt_context, &mut reader, &mut stderr)
174}
175
176pub fn prompt_revert_choice_with_io<R: BufRead, W: Write>(
177 prompt_context: &RevertPromptContext,
178 reader: &mut R,
179 writer: &mut W,
180) -> Result<RevertDecision> {
181 if let Some(preface) = prompt_context.preface.as_ref()
182 && !preface.trim().is_empty()
183 {
184 write!(writer, "{preface}")?;
185 if !preface.ends_with('\n') {
186 writeln!(writer)?;
187 }
188 writer.flush().ok();
189 }
190
191 let mut prompt = format!(
192 "{}: action? [1=keep (default), 2=revert, 3=other",
193 prompt_context.label
194 );
195 if prompt_context.allow_proceed {
196 prompt.push_str(", 4=keep+continue");
197 }
198 prompt.push_str("]: ");
199 write!(writer, "{prompt}")?;
200 writer.flush().ok();
201
202 let mut input = String::new();
203 reader.read_line(&mut input)?;
204
205 let mut decision = parse_revert_response(&input, prompt_context.allow_proceed);
206
207 if matches!(decision, RevertDecision::Continue { ref message } if message.is_empty()) {
208 write!(
209 writer,
210 "{}: enter message to send (empty => keep): ",
211 prompt_context.label
212 )?;
213 writer.flush().ok();
214
215 let mut msg = String::new();
216 reader.read_line(&mut msg)?;
217 let msg = msg.trim_end_matches(['\n', '\r']);
218 if msg.trim().is_empty() {
219 decision = RevertDecision::Keep;
220 } else {
221 decision = RevertDecision::Continue {
222 message: msg.to_string(),
223 };
224 }
225 }
226
227 Ok(decision)
228}
229
230pub fn parse_revert_response(input: &str, allow_proceed: bool) -> RevertDecision {
231 let raw = input.trim_end_matches(['\n', '\r']);
232 let normalized = raw.trim().to_lowercase();
233
234 match normalized.as_str() {
235 "" => RevertDecision::Keep,
236 "1" | "k" | "keep" => RevertDecision::Keep,
237 "2" | "r" | "revert" => RevertDecision::Revert,
238 "3" => RevertDecision::Continue {
239 message: String::new(),
240 },
241 "4" if allow_proceed => RevertDecision::Proceed,
242 _ => RevertDecision::Continue {
243 message: raw.to_string(),
244 },
245 }
246}