1use std::fmt::Display;
7use std::fmt::Formatter;
8use std::ops::Deref;
9use std::path::Path;
10
11use owo_colors::OwoColorize;
12use strum::EnumIter;
13use strum::IntoEnumIterator;
14use ytil_git::GitStatusEntry;
15use ytil_git::IndexState;
16use ytil_git::WorktreeState;
17use ytil_sys::cli::Args;
18
19#[ytil_sys::main]
30fn main() -> rootcause::Result<()> {
31 let args = ytil_sys::cli::get();
32 if args.has_help() {
33 println!("{}", include_str!("../help.txt"));
34 return Ok(());
35 }
36 let args: Vec<_> = args.iter().map(String::as_str).collect();
37
38 let git_status_entries = ytil_git::get_status()?;
39 if git_status_entries.is_empty() {
40 println!("Working tree clean");
41 return Ok(());
42 }
43
44 let renderable_entries = git_status_entries.into_iter().map(RenderableGitStatusEntry).collect();
45
46 let Some(selected_entries) =
47 ytil_tui::minimal_multi_select(renderable_entries, ToString::to_string, ToString::to_string)?
48 else {
49 println!("\n\nNo entries selected");
50 return Ok(());
51 };
52
53 let Some(selected_op) = ytil_tui::minimal_select::<Op>(Op::iter().collect())? else {
54 println!("\n\nNothing operation selected");
55 return Ok(());
56 };
57
58 let selected_entries = selected_entries.iter().map(Deref::deref).collect::<Vec<_>>();
59 match selected_op {
60 Op::Discard => restore_entries(&selected_entries, args.first().copied())?,
61 Op::Add => add_entries(&selected_entries)?,
62 }
63
64 Ok(())
65}
66
67pub struct RenderableGitStatusEntry(pub GitStatusEntry);
71
72impl Deref for RenderableGitStatusEntry {
73 type Target = GitStatusEntry;
74
75 fn deref(&self) -> &Self::Target {
76 &self.0
77 }
78}
79
80impl Display for RenderableGitStatusEntry {
81 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
82 if self.conflicted {
84 return write!(f, "{} {}", "CC".red().bold(), self.path.display());
85 }
86
87 write_index_symbol(f, self.index_state.as_ref(), self.ignored)?;
89 write_worktree_symbol(f, self.worktree_state.as_ref(), self.ignored)?;
90
91 write!(f, " {}", self.path.display())
92 }
93}
94
95#[derive(EnumIter)]
97pub enum Op {
98 Add,
100 Discard,
103}
104
105impl Display for Op {
106 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
107 match self {
108 Self::Discard => write!(f, "{}", "Discard".red().bold()),
109 Self::Add => write!(f, "{}", "Add".green().bold()),
110 }
111 }
112}
113
114fn restore_entries(entries: &[&GitStatusEntry], branch: Option<&str>) -> rootcause::Result<()> {
119 let (new_entries, changed_entries): (Vec<&GitStatusEntry>, Vec<&GitStatusEntry>) =
121 entries.iter().copied().partition(|entry| entry.is_new());
122
123 let mut new_entries_in_index = vec![];
124 for new_entry in &new_entries {
125 let absolute_path = new_entry.absolute_path();
126 if absolute_path.is_file() || absolute_path.is_symlink() {
127 std::fs::remove_file(&absolute_path)?;
128 } else if absolute_path.is_dir() {
129 std::fs::remove_dir_all(&absolute_path)?;
130 }
131 println!("{} {}", "Discarded".red().bold(), new_entry.path.display());
132 if new_entry.is_new_in_index() {
133 new_entries_in_index.push(absolute_path.display().to_string());
134 }
135 }
136 ytil_git::unstage(new_entries_in_index.iter().map(String::as_str))?;
138
139 if changed_entries.is_empty() {
141 return Ok(());
142 }
143
144 let staged_changed_paths: Vec<String> = changed_entries
148 .iter()
149 .filter(|entry| entry.is_staged() || entry.conflicted)
150 .map(|entry| entry.absolute_path().to_string_lossy().into_owned())
151 .collect();
152 ytil_git::unstage(staged_changed_paths.iter().map(String::as_str))?;
153
154 let changed_entries_paths = changed_entries
155 .iter()
156 .map(|changed_entry| changed_entry.absolute_path().to_string_lossy().into_owned());
157
158 ytil_git::restore(changed_entries_paths, branch)?;
159
160 for changed_entry in changed_entries {
161 let from_branch = branch.map(|b| format!(" from {b}")).unwrap_or_default();
162 println!(
163 "{} {}{from_branch}",
164 "Restored".yellow().bold(),
165 changed_entry.path.display()
166 );
167 }
168 Ok(())
169}
170
171fn add_entries(entries: &[&GitStatusEntry]) -> rootcause::Result<()> {
177 let mut repo = ytil_git::repo::discover(Path::new("."))?;
178 ytil_git::add_to_index(&mut repo, entries.iter().map(|entry| entry.path.as_path()))?;
179 for entry in entries {
180 println!("{} {}", "Added".green().bold(), entry.path.display());
181 }
182 Ok(())
183}
184
185fn write_index_symbol(f: &mut Formatter<'_>, state: Option<&IndexState>, dimmed: bool) -> std::fmt::Result {
187 match state {
188 None => write!(f, " "),
189 Some(IndexState::New) if dimmed => write!(f, "{}", "A".green().bold().dimmed()),
190 Some(IndexState::New) => write!(f, "{}", "A".green().bold()),
191 Some(IndexState::Modified) if dimmed => write!(f, "{}", "M".yellow().bold().dimmed()),
192 Some(IndexState::Modified) => write!(f, "{}", "M".yellow().bold()),
193 Some(IndexState::Deleted) if dimmed => write!(f, "{}", "D".red().bold().dimmed()),
194 Some(IndexState::Deleted) => write!(f, "{}", "D".red().bold()),
195 Some(IndexState::Renamed) if dimmed => write!(f, "{}", "R".cyan().bold().dimmed()),
196 Some(IndexState::Renamed) => write!(f, "{}", "R".cyan().bold()),
197 Some(IndexState::Typechange) if dimmed => write!(f, "{}", "T".magenta().bold().dimmed()),
198 Some(IndexState::Typechange) => write!(f, "{}", "T".magenta().bold()),
199 }
200}
201
202fn write_worktree_symbol(f: &mut Formatter<'_>, state: Option<&WorktreeState>, dimmed: bool) -> std::fmt::Result {
204 match state {
205 None => write!(f, " "),
206 Some(WorktreeState::New) if dimmed => write!(f, "{}", "A".green().bold().dimmed()),
207 Some(WorktreeState::New) => write!(f, "{}", "A".green().bold()),
208 Some(WorktreeState::Modified) if dimmed => write!(f, "{}", "M".yellow().bold().dimmed()),
209 Some(WorktreeState::Modified) => write!(f, "{}", "M".yellow().bold()),
210 Some(WorktreeState::Deleted) if dimmed => write!(f, "{}", "D".red().bold().dimmed()),
211 Some(WorktreeState::Deleted) => write!(f, "{}", "D".red().bold()),
212 Some(WorktreeState::Renamed) if dimmed => write!(f, "{}", "R".cyan().bold().dimmed()),
213 Some(WorktreeState::Renamed) => write!(f, "{}", "R".cyan().bold()),
214 Some(WorktreeState::Typechange) if dimmed => write!(f, "{}", "T".magenta().bold().dimmed()),
215 Some(WorktreeState::Typechange) => write!(f, "{}", "T".magenta().bold()),
216 Some(WorktreeState::Unreadable) if dimmed => write!(f, "{}", "U".red().bold().dimmed()),
217 Some(WorktreeState::Unreadable) => write!(f, "{}", "U".red().bold()),
218 }
219}