Branch popup (#303)

closes #91
This commit is contained in:
Richard Menzies 2020-10-10 00:27:13 +01:00 committed by GitHub
parent 92b1b4c2a8
commit 94bbf3c9fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 449 additions and 8 deletions

View file

@ -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,),),
)

View file

@ -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<String> {
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<Vec<BranchForDisplay>> {
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");

View file

@ -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,

View file

@ -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<CommandBar>,
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<B: Backend>(
@ -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)?;

View file

@ -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;

View file

@ -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<BranchForDisplay>,
visible: bool,
selection: u16,
queue: Queue,
theme: SharedTheme,
key_config: SharedKeyConfig,
}
impl DrawableComponent for SelectBranchComponent {
fn draw<B: Backend>(
&self,
f: &mut Frame<B>,
_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<CommandInfo>,
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<bool> {
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<Vec<BranchForDisplay>> {
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<Text> {
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(())
}
}

View file

@ -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()},
}

View file

@ -53,6 +53,8 @@ pub enum InternalEvent {
///
CreateBranch,
///
SelectBranch,
///
OpenExternalEditor(Option<String>),
///
Push(String),

View file

@ -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),),

View file

@ -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();