1use std::collections::{HashMap, HashSet};
20use std::convert::From;
21use std::fmt;
22use std::path::PathBuf;
23
24use crate::appui::{AppUi, BranchToDeleteInfo};
25use crate::batchappui::BatchAppUi;
26use crate::cliargs::CliArgs;
27use crate::git::{BranchRestorer, GitError, Repository};
28use crate::interactiveappui::InteractiveAppUi;
29
30pub static DEFAULT_BRANCH_CONFIG_KEY: &str = "git-bonsai.default-branch";
31
32#[derive(Debug, PartialEq, Eq)]
33pub enum AppError {
34 Git(GitError),
35 UnsafeDelete,
36 InterruptedByUser,
37}
38
39impl From<GitError> for AppError {
40 fn from(error: GitError) -> Self {
41 AppError::Git(error)
42 }
43}
44
45impl fmt::Display for AppError {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 match self {
48 AppError::Git(error) => error.fmt(f),
49 AppError::UnsafeDelete => {
50 write!(f, "This branch cannot be deleted safely")
51 }
52 AppError::InterruptedByUser => {
53 write!(f, "Interrupted")
54 }
55 }
56 }
57}
58
59pub struct App {
60 repo: Repository,
61 protected_branches: HashSet<String>,
62 ui: Box<dyn AppUi>,
63 fetch: bool,
64}
65
66impl App {
67 pub fn new(args: &CliArgs, ui: Box<dyn AppUi>, repo_dir: &str) -> App {
68 let repo = Repository::new(&PathBuf::from(repo_dir));
69
70 let mut branches: HashSet<String> = HashSet::new();
71 for branch in repo
72 .get_config_keys("git-bonsai.protected-branches")
73 .unwrap()
74 {
75 branches.insert(branch.to_string());
76 }
77 for branch in &args.excluded {
78 branches.insert(branch.to_string());
79 }
80 App {
81 repo,
82 protected_branches: branches,
83 ui,
84 fetch: !args.no_fetch,
85 }
86 }
87
88 #[allow(dead_code)]
90 pub fn get_protected_branches(&self) -> HashSet<String> {
91 self.protected_branches.clone()
92 }
93
94 pub fn is_working_tree_clean(&self) -> bool {
95 if self.repo.get_current_branch().is_none() {
96 self.ui.log_error("No current branch");
97 return false;
98 }
99 match self.repo.has_changes() {
100 Ok(has_changes) => {
101 if has_changes {
102 self.ui
103 .log_error("Can't work in a tree with uncommitted changes");
104 return false;
105 }
106 true
107 }
108 Err(_) => {
109 self.ui.log_error("Failed to get working tree status");
110 false
111 }
112 }
113 }
114
115 pub fn find_default_branch_from_git(&self) -> Result<String, AppError> {
118 self.ui.log_info("Determining repository default branch");
119 let branch = match self.repo.find_default_branch() {
120 Ok(x) => x,
121 Err(err) => {
122 self.ui.log_error(&format!(
123 "Can't determine default branch: {}",
124 &err.to_string()
125 ));
126 return self.find_default_branch_from_user();
127 }
128 };
129 self.repo
130 .set_config_key(DEFAULT_BRANCH_CONFIG_KEY, &branch)?;
131 self.ui.log_info(&format!("Default branch is {}", branch));
132 Ok(branch)
133 }
134
135 pub fn find_default_branch_from_user(&self) -> Result<String, AppError> {
137 let branch = match self.ui.select_default_branch(&self.repo.list_branches()?) {
138 Some(x) => x,
139 None => {
140 return Err(AppError::InterruptedByUser);
141 }
142 };
143 self.repo
144 .set_config_key(DEFAULT_BRANCH_CONFIG_KEY, &branch)?;
145 Ok(branch)
146 }
147
148 pub fn get_default_branch(&self) -> Result<Option<String>, AppError> {
150 match self.repo.get_config_keys(DEFAULT_BRANCH_CONFIG_KEY) {
151 Ok(values) => Ok(if values.len() != 1 {
152 None
153 } else {
154 Some(values[0].clone())
155 }),
156 Err(x) => Err(AppError::Git(x)),
157 }
158 }
159
160 pub fn fetch_changes(&self) -> Result<(), AppError> {
161 self.ui.log_info("Fetching changes");
162 self.repo.fetch()?;
163 Ok(())
164 }
165
166 pub fn update_tracking_branches(&self) -> Result<(), AppError> {
167 let branches = match self.repo.list_tracking_branches() {
168 Ok(x) => x,
169 Err(x) => {
170 self.ui.log_error("Failed to list tracking branches");
171 return Err(AppError::Git(x));
172 }
173 };
174
175 let _restorer = BranchRestorer::new(&self.repo);
176 for branch in branches {
177 self.ui.log_info(&format!("Updating {}", branch));
178 if let Err(x) = self.repo.checkout(&branch) {
179 self.ui.log_error("Failed to checkout branch");
180 return Err(AppError::Git(x));
181 }
182 if let Err(_x) = self.repo.update_branch() {
183 self.ui.log_warning("Failed to update branch");
184 }
187 }
188 Ok(())
189 }
190 pub fn remove_merged_branches(&self) -> Result<(), AppError> {
191 let to_delete = self.get_deletable_branches()?;
192
193 if to_delete.is_empty() {
194 self.ui.log_info("No deletable branches");
195 return Ok(());
196 }
197
198 let selected_branches = self.ui.select_branches_to_delete(&to_delete);
199 if selected_branches.is_empty() {
200 return Ok(());
201 }
202
203 let branch_names: Vec<String> = selected_branches
204 .iter()
205 .map(|x| x.name.to_string())
206 .collect();
207 self.delete_branches(&branch_names[..])?;
208 Ok(())
209 }
210
211 fn delete_branches(&self, branches: &[String]) -> Result<(), AppError> {
214 let current_branch = self.repo.get_current_branch().unwrap();
215
216 let mut current_branch_deleted = false;
217 let default_branch = self.get_default_branch().unwrap().unwrap();
218
219 match self.repo.checkout(&default_branch) {
220 Ok(()) => (),
221 Err(x) => {
222 let msg = format!("Failed to switch to default branch '{}'", default_branch);
223 self.ui.log_error(&msg);
224 return Err(AppError::Git(x));
225 }
226 }
227
228 for branch in branches {
229 self.ui.log_info(&format!("Deleting {}", branch));
230
231 if self.safe_delete_branch(branch).is_err() {
232 self.ui.log_warning("Failed to delete branch");
233 } else if *branch == current_branch {
234 current_branch_deleted = true;
235 }
236 }
237
238 if !current_branch_deleted {
239 self.repo.checkout(¤t_branch)?;
240 }
241 Ok(())
242 }
243
244 fn get_deletable_branches(&self) -> Result<Vec<BranchToDeleteInfo>, AppError> {
245 let deletable_branches: Vec<BranchToDeleteInfo> = match self.repo.list_branches() {
246 Ok(x) => x,
247 Err(x) => {
248 self.ui.log_error("Failed to list branches");
249 return Err(AppError::Git(x));
250 }
251 }
252 .iter()
253 .filter(|&x| !self.protected_branches.contains(x))
254 .map(|branch| {
255 let contained_in: HashSet<String> = match self.repo.list_branches_containing(branch) {
256 Ok(x) => x,
257 Err(_x) => {
258 self.ui
259 .log_error(&format!("Failed to list branches containing {}", branch));
260 [].to_vec()
261 }
262 }
263 .iter()
264 .filter(|&x| x != branch)
265 .cloned()
266 .collect();
267
268 BranchToDeleteInfo {
269 name: branch.to_string(),
270 contained_in,
271 }
272 })
273 .filter(|x| !x.contained_in.is_empty())
274 .collect();
275
276 Ok(deletable_branches)
277 }
278
279 fn is_sha1_contained_in_another_branch(
280 &self,
281 sha1: &str,
282 branches: &HashSet<String>,
283 ) -> Result<bool, GitError> {
284 for branch in self.repo.list_branches_containing(sha1).unwrap() {
285 if !branches.contains(&branch) {
286 return Ok(true);
287 }
288 }
289 Ok(false)
290 }
291
292 pub fn do_delete_identical_branches(
293 &self,
294 sha1: &str,
295 branch_set: &HashSet<String>,
296 ) -> Result<(), AppError> {
297 let unprotected_branch_set: HashSet<_> =
298 branch_set.difference(&self.protected_branches).collect();
299 if !self
300 .is_sha1_contained_in_another_branch(sha1, branch_set)
301 .unwrap()
302 {
303 let contains_protected_branches = unprotected_branch_set.len() < branch_set.len();
304 let branches: Vec<String> = unprotected_branch_set
305 .iter()
306 .map(|x| x.to_string())
307 .collect();
308 if !contains_protected_branches {
309 let selected_branches: Vec<String> = self
310 .ui
311 .select_identical_branches_to_delete_keep_one(&branches)
312 .iter()
313 .map(|x| x.to_string())
314 .collect();
315 self.delete_branches(&selected_branches)?;
316 return Ok(());
317 }
318 }
319 if unprotected_branch_set.is_empty() {
320 return Ok(());
322 }
323 let branches: Vec<String> = unprotected_branch_set
324 .iter()
325 .map(|x| x.to_string())
326 .collect();
327 let selected_branches: Vec<_> = self
328 .ui
329 .select_identical_branches_to_delete(&branches)
330 .iter()
331 .map(|x| x.to_string())
332 .collect();
333 self.delete_branches(&selected_branches)?;
334 Ok(())
335 }
336
337 pub fn delete_identical_branches(&self) -> Result<(), AppError> {
338 let mut branches_for_sha1: HashMap<String, HashSet<String>> = HashMap::new();
340
341 match self.repo.list_branches_with_sha1s() {
342 Ok(x) => x,
343 Err(x) => {
344 self.ui.log_error("Failed to list branches");
345 return Err(AppError::Git(x));
346 }
347 }
348 .iter()
349 .for_each(|(branch, sha1)| {
350 let branch_set = branches_for_sha1
351 .entry(sha1.to_string())
352 .or_insert_with(HashSet::<String>::new);
353 branch_set.insert(branch.to_string());
354 });
355
356 for (sha1, branch_set) in branches_for_sha1 {
358 if branch_set.len() == 1 {
359 continue;
360 }
361 if let Err(x) = self.do_delete_identical_branches(&sha1, &branch_set) {
362 self.ui.log_error("Failed to list branches");
363 return Err(x);
364 }
365 }
366
367 Ok(())
368 }
369
370 pub fn safe_delete_branch(&self, branch: &str) -> Result<(), AppError> {
371 let contained_in = self.repo.list_branches_containing(branch).unwrap();
373 if contained_in.len() < 2 {
374 self.ui.log_error(&format!(
375 "Not deleting {}, no other branches contain it",
376 branch
377 ));
378 return Err(AppError::UnsafeDelete);
379 }
380 self.repo.delete_branch(branch)?;
381 Ok(())
382 }
383
384 pub fn add_default_branch_to_protected_branches(&mut self) -> Result<(), AppError> {
385 let default_branch = match self.get_default_branch()? {
386 Some(x) => x,
387 None => {
388 if self.fetch {
389 self.find_default_branch_from_git()?
390 } else {
391 self.find_default_branch_from_user()?
392 }
393 }
394 };
395 self.protected_branches.insert(default_branch);
396 Ok(())
397 }
398
399 pub fn run(&mut self) -> Result<(), AppError> {
400 self.add_default_branch_to_protected_branches()?;
401 if self.fetch {
402 self.fetch_changes()?;
403 }
404
405 self.update_tracking_branches()?;
406 self.delete_identical_branches()?;
407 self.remove_merged_branches()?;
408 Ok(())
409 }
410}
411
412pub fn run(args: CliArgs, dir: &str) -> i32 {
413 let ui: Box<dyn AppUi> = match args.yes {
414 false => Box::new(InteractiveAppUi {}),
415 true => Box::new(BatchAppUi {}),
416 };
417 let mut app = App::new(&args, ui, dir);
418
419 if !app.is_working_tree_clean() {
420 return 1;
421 }
422
423 match app.run() {
424 Ok(()) => 0,
425 Err(_) => 1,
426 }
427}