diff --git a/assets/vim_style_key_config.ron b/assets/vim_style_key_config.ron index 1515cf752c..d819488229 100644 --- a/assets/vim_style_key_config.ron +++ b/assets/vim_style_key_config.ron @@ -64,7 +64,8 @@ log_tag_commit: ( code: Char('t'), modifiers: ( bits: 0,),), commit_amend: ( code: Char('A'), modifiers: ( bits: 1,),), copy: ( code: Char('y'), modifiers: ( bits: 0,),), - create_branch: ( code: Char('b'), modifiers: ( bits: 0,),), + create_branch: ( code: Char('c'), modifiers: ( bits: 0,),), + select_branch: ( code: Char('b'), modifiers: ( bits: 0,),), push: ( code: Char('p'), modifiers: ( bits: 0,),), fetch: ( code: Char('f'), modifiers: ( bits: 0,),), ) diff --git a/asyncgit/src/sync/branch.rs b/asyncgit/src/sync/branch.rs index b77fd26067..ab172cf6c4 100644 --- a/asyncgit/src/sync/branch.rs +++ b/asyncgit/src/sync/branch.rs @@ -4,6 +4,7 @@ use crate::{ error::{Error, Result}, sync::utils, }; +use git2::BranchType; use scopetime::scope_time; use utils::get_head_repo; @@ -28,6 +29,86 @@ pub(crate) fn get_branch_name(repo_path: &str) -> Result { Err(Error::NoHead) } +/// +pub struct BranchForDisplay { + /// + pub name: String, + /// + pub reference: String, + /// + pub top_commit_message: String, + /// + pub top_commit_reference: String, + /// + pub is_head: bool, +} + +/// TODO make this cached +/// Used to return only the nessessary information for displaying a branch +/// rather than an iterator over the actual branches +pub fn get_branches_to_display( + repo_path: &str, +) -> Result> { + scope_time!("get_branches_to_display"); + let cur_repo = utils::repo(repo_path)?; + let mut branches_for_display = vec![]; + + for b in cur_repo.branches(Some(BranchType::Local))? { + let branch = &b?.0; + let top_commit = branch.get().peel_to_commit()?; + let mut commit_id = top_commit.id().to_string(); + commit_id.truncate(7); + + branches_for_display.push(BranchForDisplay { + name: String::from_utf8(Vec::from(branch.name_bytes()?))?, + reference: String::from_utf8(Vec::from( + branch.get().name_bytes(), + ))?, + top_commit_message: String::from_utf8(Vec::from( + top_commit.summary_bytes().unwrap_or(&[]), + ))?, + top_commit_reference: commit_id, + is_head: branch.is_head(), + }) + } + Ok(branches_for_display) +} + +/// Modify HEAD to point to a branch then checkout head, does not work if there are uncommitted changes +pub fn checkout_branch( + repo_path: &str, + branch_ref: &str, +) -> Result<()> { + scope_time!("checkout_branch"); + // This defaults to a safe checkout, so don't delete anything that + // hasn't been committed or stashed, in this case it will Err + let repo = utils::repo(repo_path)?; + let cur_ref = repo.head()?; + if repo + .statuses(Some( + git2::StatusOptions::new().include_ignored(false), + ))? + .is_empty() + { + repo.set_head(branch_ref)?; + + if let Err(e) = repo.checkout_head(Some( + git2::build::CheckoutBuilder::new().force(), + )) { + // This is safe beacuse cur_ref was just found + repo.set_head(cur_ref.name().unwrap_or(""))?; + return Err(Error::Git(e)); + } + Ok(()) + } else { + Err(Error::Generic( + format!("Cannot change branch. There are unstaged/staged changes which have not been committed/stashed. There is {:?} changes preventing checking out a different branch.", repo.statuses(Some( + git2::StatusOptions::new().include_ignored(false), + ))?.len()), + )) + } +} + /// creates a new branch pointing to current HEAD commit and updating HEAD to new branch pub fn create_branch(repo_path: &str, name: &str) -> Result<()> { scope_time!("create_branch"); diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index 87251e398e..8cf657a777 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -17,8 +17,11 @@ pub mod status; mod tags; pub mod utils; -pub use branch::create_branch; pub(crate) use branch::get_branch_name; +pub use branch::{ + checkout_branch, create_branch, get_branches_to_display, + BranchForDisplay, +}; pub use commit::{amend, commit, tag}; pub use commit_details::{ get_commit_details, CommitDetails, CommitMessage, diff --git a/src/app.rs b/src/app.rs index c8d23428b6..3febcdb62f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -6,7 +6,8 @@ use crate::{ Component, CreateBranchComponent, DrawableComponent, ExternalEditorComponent, HelpComponent, InspectCommitComponent, MsgComponent, PushComponent, - ResetComponent, StashMsgComponent, TagCommitComponent, + ResetComponent, SelectBranchComponent, StashMsgComponent, + TagCommitComponent, }, input::{Input, InputEvent, InputState}, keys::{KeyConfig, SharedKeyConfig}, @@ -45,6 +46,7 @@ pub struct App { push_popup: PushComponent, tag_commit_popup: TagCommitComponent, create_branch_popup: CreateBranchComponent, + select_branch_popup: SelectBranchComponent, cmdbar: RefCell, tab: usize, revlog: Revlog, @@ -116,6 +118,11 @@ impl App { theme.clone(), key_config.clone(), ), + select_branch_popup: SelectBranchComponent::new( + queue.clone(), + theme.clone(), + key_config.clone(), + ), do_quit: false, cmdbar: RefCell::new(CommandBar::new( theme.clone(), @@ -335,6 +342,7 @@ impl App { push_popup, tag_commit_popup, create_branch_popup, + select_branch_popup, help, revlog, status_tab, @@ -487,6 +495,9 @@ impl App { InternalEvent::CreateBranch => { self.create_branch_popup.open()?; } + InternalEvent::SelectBranch => { + self.select_branch_popup.open()?; + } InternalEvent::TabSwitch => self.set_tab(0)?, InternalEvent::InspectCommit(id, tags) => { self.inspect_commit_popup.open(id, tags)?; @@ -562,6 +573,7 @@ impl App { || self.tag_commit_popup.is_visible() || self.create_branch_popup.is_visible() || self.push_popup.is_visible() + || self.select_branch_popup.is_visible() } fn draw_popups( @@ -587,6 +599,7 @@ impl App { self.msg.draw(f, size)?; self.external_editor_popup.draw(f, size)?; self.tag_commit_popup.draw(f, size)?; + self.select_branch_popup.draw(f, size)?; self.create_branch_popup.draw(f, size)?; self.push_popup.draw(f, size)?; diff --git a/src/components/mod.rs b/src/components/mod.rs index 088ab7bf80..78d3713b99 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -12,6 +12,7 @@ mod inspect_commit; mod msg; mod push; mod reset; +mod select_branch; mod stashmsg; mod tag_commit; mod textinput; @@ -34,6 +35,7 @@ pub use inspect_commit::InspectCommitComponent; pub use msg::MsgComponent; pub use push::PushComponent; pub use reset::ResetComponent; +pub use select_branch::SelectBranchComponent; pub use stashmsg::StashMsgComponent; pub use tag_commit::TagCommitComponent; pub use textinput::TextInputComponent; diff --git a/src/components/select_branch.rs b/src/components/select_branch.rs new file mode 100644 index 0000000000..991db32084 --- /dev/null +++ b/src/components/select_branch.rs @@ -0,0 +1,321 @@ +use super::{ + visibility_blocking, CommandBlocking, CommandInfo, Component, + DrawableComponent, +}; +use crate::{ + keys::SharedKeyConfig, + queue::{InternalEvent, NeedsUpdate, Queue}, + strings, ui, +}; +use asyncgit::{ + sync::{ + checkout_branch, get_branches_to_display, BranchForDisplay, + }, + CWD, +}; +use crossterm::event::Event; +use std::{cmp, convert::TryFrom}; +use tui::{ + backend::Backend, + layout::{Alignment, Constraint, Direction, Layout, Rect}, + text::{Span, Spans, Text}, + widgets::{Block, BorderType, Borders, Clear, Paragraph}, + Frame, +}; + +use anyhow::Result; +use ui::style::SharedTheme; + +/// +pub struct SelectBranchComponent { + branch_names: Vec, + visible: bool, + selection: u16, + queue: Queue, + theme: SharedTheme, + key_config: SharedKeyConfig, +} + +impl DrawableComponent for SelectBranchComponent { + fn draw( + &self, + f: &mut Frame, + _rect: Rect, + ) -> Result<()> { + // Render a scrolllist of branches inside a box + + if self.visible { + const SIZE: (u16, u16) = (50, 45); + let scroll_threshold = SIZE.1 / 3; + let scroll = + self.selection.saturating_sub(scroll_threshold); + + let area = + ui::centered_rect_absolute(SIZE.0, SIZE.1, f.size()); + + f.render_widget(Clear, area); + f.render_widget( + Block::default() + .title(strings::SELECT_BRANCH_POPUP_MSG) + .borders(Borders::ALL) + .border_type(BorderType::Thick), + area, + ); + + let chunk = Layout::default() + .vertical_margin(1) + .horizontal_margin(1) + .direction(Direction::Vertical) + .constraints( + [Constraint::Min(1), Constraint::Length(1)] + .as_ref(), + ) + .split(area)[0]; + f.render_widget( + Paragraph::new( + self.get_text(&self.theme, area.width)?, + ) + .scroll((scroll, 0)) + .alignment(Alignment::Left), + chunk, + ); + } + + Ok(()) + } +} + +impl Component for SelectBranchComponent { + fn commands( + &self, + out: &mut Vec, + force_all: bool, + ) -> CommandBlocking { + if self.visible || force_all { + out.clear(); + + out.push(CommandInfo::new( + strings::commands::scroll(&self.key_config), + true, + true, + )); + + out.push(CommandInfo::new( + strings::commands::close_popup(&self.key_config), + true, + true, + )); + + out.push(CommandInfo::new( + strings::commands::open_branch_create_popup( + &self.key_config, + ), + true, + true, + )); + } + visibility_blocking(self) + } + + fn event(&mut self, ev: Event) -> Result { + if self.visible { + if let Event::Key(e) = ev { + if e == self.key_config.exit_popup { + self.hide() + } else if e == self.key_config.move_down { + self.move_selection(true) + } else if e == self.key_config.move_up { + self.move_selection(false) + } else if e == self.key_config.enter { + if let Err(e) = self.switch_to_selected_branch() { + log::error!("switch branch error: {}", e); + self.queue.borrow_mut().push_back( + InternalEvent::ShowErrorMsg(format!( + "switch branch error:\n{}", + e + )), + ); + } + self.hide() + } else if e == self.key_config.create_branch { + self.queue + .borrow_mut() + .push_back(InternalEvent::CreateBranch); + self.hide(); + } + } + + Ok(true) + } else { + Ok(false) + } + } + + fn is_visible(&self) -> bool { + self.visible + } + + fn hide(&mut self) { + self.visible = false + } + + fn show(&mut self) -> Result<()> { + self.visible = true; + + Ok(()) + } +} + +impl SelectBranchComponent { + pub fn new( + queue: Queue, + theme: SharedTheme, + key_config: SharedKeyConfig, + ) -> Self { + Self { + branch_names: Vec::new(), + visible: false, + selection: 0, + queue, + theme, + key_config, + } + } + /// Get all the names of the branches in the repo + pub fn get_branch_names() -> Result> { + get_branches_to_display(CWD).map_err(anyhow::Error::new) + } + + /// + pub fn open(&mut self) -> Result<()> { + self.update_branches()?; + self.show()?; + + Ok(()) + } + + //// + pub fn update_branches(&mut self) -> Result<()> { + self.branch_names = Self::get_branch_names()?; + Ok(()) + } + + /// + fn move_selection(&mut self, inc: bool) { + let mut new_selection = self.selection; + + new_selection = if inc { + new_selection.saturating_add(1) + } else { + new_selection.saturating_sub(1) + }; + new_selection = cmp::max(new_selection, 0); + + if let Ok(max) = + u16::try_from(self.branch_names.len().saturating_sub(1)) + { + self.selection = cmp::min(new_selection, max); + } + } + + /// Get branches to display + fn get_text( + &self, + theme: &SharedTheme, + width_available: u16, + ) -> Result { + const BRANCH_NAME_LENGTH: usize = 15; + // total width - commit hash - branch name -"* " - "..." = remaining width + let commit_message_length: usize = + width_available as usize - 8 - BRANCH_NAME_LENGTH - 3 - 3; + let mut txt = Vec::new(); + + for (i, displaybranch) in self.branch_names.iter().enumerate() + { + let mut commit_message = + displaybranch.top_commit_message.clone(); + if commit_message.len() > commit_message_length { + commit_message.truncate(commit_message_length - 3); + commit_message += "..."; + } + + let mut branch_name = displaybranch.name.clone(); + if branch_name.len() > BRANCH_NAME_LENGTH { + branch_name.truncate(BRANCH_NAME_LENGTH - 3); + branch_name += "..."; + } + + let is_head_str = + if displaybranch.is_head { "*" } else { " " }; + + txt.push(Spans::from(if self.selection as usize == i { + vec![ + Span::styled( + format!("{} ", is_head_str), + theme.commit_author(true), + ), + Span::styled( + format!( + ">{:w$} ", + branch_name, + w = BRANCH_NAME_LENGTH + ), + theme.commit_author(true), + ), + Span::styled( + format!( + "{} ", + displaybranch.top_commit_reference + ), + theme.commit_hash(true), + ), + Span::styled( + commit_message.to_string(), + theme.text(true, true), + ), + ] + } else { + vec![ + Span::styled( + format!("{} ", is_head_str), + theme.commit_author(false), + ), + Span::styled( + format!( + " {:w$} ", + branch_name, + w = BRANCH_NAME_LENGTH + ), + theme.commit_author(false), + ), + Span::styled( + format!( + "{} ", + displaybranch.top_commit_reference + ), + theme.commit_hash(false), + ), + Span::styled( + commit_message.to_string(), + theme.text(true, false), + ), + ] + })); + } + + Ok(Text::from(txt)) + } + + /// + fn switch_to_selected_branch(&self) -> Result<()> { + checkout_branch( + asyncgit::CWD, + &self.branch_names[self.selection as usize].reference, + )?; + self.queue + .borrow_mut() + .push_back(InternalEvent::Update(NeedsUpdate::ALL)); + + Ok(()) + } +} diff --git a/src/keys.rs b/src/keys.rs index 6083e408a2..9faad7a674 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -60,6 +60,7 @@ pub struct KeyConfig { pub commit_amend: KeyEvent, pub copy: KeyEvent, pub create_branch: KeyEvent, + pub select_branch: KeyEvent, pub push: KeyEvent, pub fetch: KeyEvent, } @@ -110,7 +111,8 @@ impl Default for KeyConfig { log_tag_commit: KeyEvent { code: KeyCode::Char('t'), modifiers: KeyModifiers::empty()}, commit_amend: KeyEvent { code: KeyCode::Char('a'), modifiers: KeyModifiers::CONTROL}, copy: KeyEvent { code: KeyCode::Char('y'), modifiers: KeyModifiers::empty()}, - create_branch: KeyEvent { code: KeyCode::Char('b'), modifiers: KeyModifiers::empty()}, + create_branch: KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::NONE}, + select_branch: KeyEvent { code: KeyCode::Char('b'), modifiers: KeyModifiers::NONE}, push: KeyEvent { code: KeyCode::Char('p'), modifiers: KeyModifiers::empty()}, fetch: KeyEvent { code: KeyCode::Char('f'), modifiers: KeyModifiers::empty()}, } diff --git a/src/queue.rs b/src/queue.rs index 23af8f4073..6802434db4 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -53,6 +53,8 @@ pub enum InternalEvent { /// CreateBranch, /// + SelectBranch, + /// OpenExternalEditor(Option), /// Push(String), diff --git a/src/strings.rs b/src/strings.rs index bbedebe881..2ca6fb4aed 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -10,6 +10,8 @@ pub static PUSH_POPUP_STATES_ADDING: &str = "adding objects (1/3)"; pub static PUSH_POPUP_STATES_DELTAS: &str = "deltas (2/3)"; pub static PUSH_POPUP_STATES_PUSHING: &str = "pushing (3/3)"; +pub static SELECT_BRANCH_POPUP_MSG: &str = "Switch Branch"; + pub fn title_status(key_config: &SharedKeyConfig) -> String { format!( "Unstaged Changes [{}]", @@ -595,13 +597,26 @@ pub mod commands { ) -> CommandText { CommandText::new( format!( - "Branch [{}]", + "Create [{}]", get_hint(key_config.create_branch), ), "open create branch popup", CMD_GROUP_GENERAL, ) } + pub fn open_branch_select_popup( + key_config: &SharedKeyConfig, + ) -> CommandText { + CommandText::new( + format!( + "Checkout [{}]", + get_hint(key_config.select_branch), + ), + "open select branch popup", + CMD_GROUP_GENERAL, + ) + } + pub fn status_push(key_config: &SharedKeyConfig) -> CommandText { CommandText::new( format!("Push [{}]", get_hint(key_config.push),), diff --git a/src/tabs/status.rs b/src/tabs/status.rs index ba54c6db58..1a013c163e 100644 --- a/src/tabs/status.rs +++ b/src/tabs/status.rs @@ -396,12 +396,13 @@ impl Component for Status { } out.push(CommandInfo::new( - strings::commands::open_branch_create_popup( + strings::commands::open_branch_select_popup( &self.key_config, ), true, true, )); + out.push(CommandInfo::new( strings::commands::status_push(&self.key_config), self.index_wd.branch_name().is_some(), @@ -484,10 +485,10 @@ impl Component for Status { && !self.index_wd.is_empty() { self.switch_focus(Focus::WorkDir) - } else if k == self.key_config.create_branch { + } else if k == self.key_config.select_branch { self.queue .borrow_mut() - .push_back(InternalEvent::CreateBranch); + .push_back(InternalEvent::SelectBranch); Ok(true) } else if k == self.key_config.push { self.push();