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