diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f4f33d8db..f72a07e5b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Added - support rebasing branches with conflicts ([#895](https://github.com/extrawurst/gitui/issues/895)) +- add supporting deletion of referring branches [[@alessandroasm](https://github.com/alessandroasm)] ([#875](https://github.com/extrawurst/gitui/issues/875)) - add a key binding to stage / unstage items [[@alessandroasm](https://github.com/alessandroasm)] ([#909](https://github.com/extrawurst/gitui/issues/909)) - switch to status tab after merging or rebasing with conflicts ([#926](https://github.com/extrawurst/gitui/issues/926)) diff --git a/asyncgit/src/sync/branch/mod.rs b/asyncgit/src/sync/branch/mod.rs index 9f1d62cc77..09ea258fa3 100644 --- a/asyncgit/src/sync/branch/mod.rs +++ b/asyncgit/src/sync/branch/mod.rs @@ -184,6 +184,41 @@ pub fn get_branches_info( Ok(branches_for_display) } +/// returns the local branches that track the provided remote branch +pub fn get_branch_trackers( + repo_path: &str, + branch_name: &str, +) -> Result> { + let repo = utils::repo(repo_path)?; + + let local_branches: HashSet<_> = repo + .branches(Some(BranchType::Local))? + .filter_map(|b| { + let branch = b.ok()?.0; + + branch + .upstream() + .ok() + .filter(|upstream| { + let upstream_name = + bytes2string(upstream.get().name_bytes()) + .ok(); + upstream_name.map_or(false, |upstream_name| { + branch_name == upstream_name + }) + }) + .and_then(|_| { + branch + .name_bytes() + .ok() + .and_then(|bn| bytes2string(bn).ok()) + }) + }) + .collect(); + + Ok(local_branches) +} + /// #[derive(Debug, Default)] pub struct BranchCompare { @@ -228,6 +263,28 @@ pub fn get_branch_remote( } } +/// returns remote of the upstream tracking branch for `branch` (using branch ref) +pub fn get_branch_remote_by_ref( + repo_path: &str, + branch_ref: &str, +) -> Result> { + let repo = utils::repo(repo_path)?; + let remote_name = repo.branch_upstream_remote(branch_ref).ok(); + if let Some(remote_name) = remote_name { + let remote_name_str = bytes2string(remote_name.as_ref())?; + let remote_branch_name = + branch_ref.rsplit('/').next().map(|branch_name| { + format!( + "refs/remotes/{}/{}", + remote_name_str, branch_name + ) + }); + Ok(remote_branch_name) + } else { + Ok(None) + } +} + /// returns whether the pull merge strategy is set to rebase pub fn config_is_pull_rebase(repo_path: &str) -> Result { let repo = utils::repo(repo_path)?; @@ -584,6 +641,98 @@ mod tests_branches { ); } + #[test] + fn test_branch_remote_trackers() { + let (r1_path, _remote1) = repo_init_bare().unwrap(); + let (r2_path, _remote2) = repo_init_bare().unwrap(); + let (_r, repo) = repo_init().unwrap(); + + let r1_path = r1_path.path().to_str().unwrap(); + let r2_path = r2_path.path().to_str().unwrap(); + + //Note: create those test branches in our remotes + clone_branch_commit_push(r1_path, "r1branch"); + clone_branch_commit_push(r2_path, "r2branch"); + + let root = repo.path().parent().unwrap(); + let repo_path = root.as_os_str().to_str().unwrap(); + + //add the remotes + repo.remote("r1", r1_path).unwrap(); + repo.remote("r2", r2_path).unwrap(); + + //pull stuff from the two remotes + debug_cmd_print(repo_path, "git pull r1"); + debug_cmd_print(repo_path, "git pull r2"); + + //create local tracking branches + debug_cmd_print( + repo_path, + "git checkout --track r1/r1branch", + ); + debug_cmd_print( + repo_path, + "git checkout --track r2/r2branch", + ); + + assert_eq!( + get_branch_trackers( + repo_path, + "refs/remotes/r1/r1branch" + ) + .unwrap() + .into_iter() + .collect::>(), + vec![String::from("r1branch")] + ); + + assert_eq!( + get_branch_trackers( + repo_path, + "refs/remotes/r2/r2branch" + ) + .unwrap() + .into_iter() + .collect::>(), + vec![String::from("r2branch")] + ); + } + + #[test] + fn test_get_remote_by_ref() { + let (r_path, _remote) = repo_init_bare().unwrap(); + let (_r, repo) = repo_init().unwrap(); + + let r_path = r_path.path().to_str().unwrap(); + + //Note: create the test branches in our remote + clone_branch_commit_push(r_path, "remotebranch"); + + let root = repo.path().parent().unwrap(); + let repo_path = root.as_os_str().to_str().unwrap(); + + //add the remote + repo.remote("origin", r_path).unwrap(); + + //pull stuff from the remotes + debug_cmd_print(repo_path, "git pull origin"); + + //create local tracking branches + debug_cmd_print( + repo_path, + "git checkout --track origin/remotebranch", + ); + + assert_eq!( + get_branch_remote_by_ref( + repo_path, + "refs/heads/remotebranch" + ) + .unwrap(), + Some(String::from("refs/remotes/origin/remotebranch")) + ); + } + #[test] fn test_branch_remote_no_upstream() { let (_r, repo) = repo_init().unwrap(); @@ -603,6 +752,17 @@ mod tests_branches { let repo_path = root.as_os_str().to_str().unwrap(); assert!(get_branch_remote(repo_path, "foo").is_err()); + + assert_eq!( + get_branch_trackers( + repo_path, + "refs/remotes/foo/foobranch" + ) + .unwrap() + .into_iter() + .collect::>(), + Vec::::new() + ); } } diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index 2eb32bb34b..231d32bd12 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -33,7 +33,8 @@ pub use blame::{blame_file, BlameHunk, FileBlame}; pub use branch::{ branch_compare_upstream, checkout_branch, config_is_pull_rebase, create_branch, delete_branch, get_branch_remote, - get_branches_info, merge_commit::merge_upstream_commit, + get_branch_remote_by_ref, get_branch_trackers, get_branches_info, + merge_commit::merge_upstream_commit, merge_ff::branch_merge_upstream_fastforward, merge_rebase::merge_upstream_rebase, rename::rename_branch, validate_branch_name, BranchCompare, BranchInfo, diff --git a/src/app.rs b/src/app.rs index b6ce9894e5..9b6cb67f4a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -740,6 +740,7 @@ impl App { Ok(flags) } + #[allow(clippy::too_many_lines)] fn process_confirmed_action( &mut self, action: Action, @@ -769,6 +770,11 @@ impl App { flags.insert(NeedsUpdate::ALL); } Action::DeleteLocalBranch(branch_ref) => { + let upstream = + sync::get_branch_remote_by_ref(CWD, &branch_ref) + .ok() + .flatten(); + if let Err(e) = sync::delete_branch(CWD, &branch_ref) { self.queue.push(InternalEvent::ShowErrorMsg( @@ -777,6 +783,13 @@ impl App { } flags.insert(NeedsUpdate::ALL); self.select_branch_popup.update_branches()?; + + // If we have a remote, we must ask if user wants to delete it + if let Some(upstream) = upstream { + self.queue.push(InternalEvent::ConfirmAction( + Action::DeleteUpstreamBranch(upstream), + )); + } } Action::DeleteRemoteBranch(branch_ref) => { self.queue.push( @@ -800,6 +813,26 @@ impl App { flags.insert(NeedsUpdate::ALL); self.select_branch_popup.update_branches()?; } + Action::DeleteUpstreamBranch(upstream) => { + self.queue.push(InternalEvent::ConfirmedAction( + Action::DeleteRemoteBranch(upstream), + )); + } + Action::DeleteTrackingBranches(branches) => { + for branch in &branches { + let branch_ref = format!("refs/heads/{}", branch); + if let Err(e) = + sync::delete_branch(CWD, &branch_ref) + { + self.queue.push(InternalEvent::ShowErrorMsg( + e.to_string(), + )); + } + } + + flags.insert(NeedsUpdate::ALL); + self.select_branch_popup.update_branches()?; + } Action::DeleteTag(tag_name) => { if let Err(error) = sync::delete_tag(CWD, &tag_name) { self.queue.push(InternalEvent::ShowErrorMsg( diff --git a/src/components/push.rs b/src/components/push.rs index e0fc39dd4f..a27d572ba3 100644 --- a/src/components/push.rs +++ b/src/components/push.rs @@ -4,7 +4,7 @@ use crate::{ CommandInfo, Component, DrawableComponent, EventState, }, keys::SharedKeyConfig, - queue::{InternalEvent, Queue}, + queue::{Action, InternalEvent, Queue}, strings, ui::{self, style::SharedTheme}, }; @@ -15,13 +15,14 @@ use asyncgit::{ extract_username_password, need_username_password, BasicAuthCredential, }, - get_branch_remote, get_default_remote, + get_branch_remote, get_branch_trackers, get_default_remote, }, AsyncGitNotification, AsyncPush, PushRequest, RemoteProgress, RemoteProgressState, CWD, }; use crossbeam_channel::Sender; use crossterm::event::Event; +use std::collections::HashSet; use tui::{ backend::Backend, layout::Rect, @@ -60,6 +61,7 @@ pub struct PushComponent { theme: SharedTheme, key_config: SharedKeyConfig, input_cred: CredComponent, + tracking_branches: Option>, } impl PushComponent { @@ -84,6 +86,7 @@ impl PushComponent { ), theme, key_config, + tracking_branches: None, } } @@ -141,6 +144,19 @@ impl PushComponent { remote }; + self.tracking_branches = if self.modifier.delete() { + let remote_ref = + format!("refs/remotes/{}/{}", remote, self.branch); + Some( + get_branch_trackers(CWD, &remote_ref) + .unwrap_or_else(|_| HashSet::new()) + .into_iter() + .collect(), + ) + } else { + None + }; + self.pending = true; self.progress = None; self.git_push.request(PushRequest { @@ -175,6 +191,20 @@ impl PushComponent { self.queue.push(InternalEvent::ShowErrorMsg( format!("push failed:\n{}", err), )); + } else if self.modifier.delete() { + // Check if we need to delete the tracking branches + let tracking_branches = self + .tracking_branches + .take() + .unwrap_or_else(Vec::new); + + if !tracking_branches.is_empty() { + self.queue.push(InternalEvent::ConfirmAction( + Action::DeleteTrackingBranches( + tracking_branches.into_iter().collect(), + ), + )); + } } self.hide(); } diff --git a/src/components/reset.rs b/src/components/reset.rs index bd1fb9dd45..0549a34bce 100644 --- a/src/components/reset.rs +++ b/src/components/reset.rs @@ -175,6 +175,24 @@ impl ConfirmComponent { branch_ref, ), ), + Action::DeleteUpstreamBranch(upstream) => ( + strings::confirm_title_delete_remote_branch( + &self.key_config, + ), + strings::confirm_msg_delete_referring_remote_branch( + &self.key_config, + upstream, + ), + ), + Action::DeleteTrackingBranches(local_branches) => ( + strings::confirm_title_delete_branch( + &self.key_config, + ), + strings::confirm_msg_delete_tracking_branches( + &self.key_config, + local_branches, + ), + ), Action::DeleteTag(tag_name) => ( strings::confirm_title_delete_tag( &self.key_config, diff --git a/src/queue.rs b/src/queue.rs index 1160730e9e..111a663c38 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -36,6 +36,8 @@ pub enum Action { ResetLines(String, Vec), StashDrop(Vec), StashPop(CommitId), + DeleteUpstreamBranch(String), + DeleteTrackingBranches(Vec), DeleteLocalBranch(String), DeleteRemoteBranch(String), DeleteTag(String), diff --git a/src/strings.rs b/src/strings.rs index b6a7e6148e..68f60fbb47 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -207,6 +207,15 @@ pub fn confirm_msg_delete_branch( ) -> String { format!("Confirm deleting branch: '{}' ?", branch_ref) } +pub fn confirm_msg_delete_tracking_branches( + _key_config: &SharedKeyConfig, + branches_ref: &[String], +) -> String { + format!( + "Do you want to delete the referring tracking branches?\n{}", + branches_ref.join(", ") + ) +} pub fn confirm_title_delete_remote_branch( _key_config: &SharedKeyConfig, ) -> String { @@ -218,6 +227,15 @@ pub fn confirm_msg_delete_remote_branch( ) -> String { format!("Confirm deleting remote branch: '{}' ?", branch_ref) } +pub fn confirm_msg_delete_referring_remote_branch( + _key_config: &SharedKeyConfig, + branch_ref: &str, +) -> String { + format!( + "Do you want to delete the referring remote branch: '{}' ?", + branch_ref + ) +} pub fn confirm_title_delete_tag( _key_config: &SharedKeyConfig, ) -> String {