diff --git a/CHANGELOG.md b/CHANGELOG.md index d0628d27..d63ca570 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ![scrollbar](assets/scrollbar.gif) +- allow creating new branch ([#253](https://github.com/extrawurst/gitui/issues/253)) + ### Fixed - selection error in stashlist when deleting last element ([#223](https://github.com/extrawurst/gitui/issues/223)) diff --git a/assets/vim_style_key_config.ron b/assets/vim_style_key_config.ron index b43c48a5..129c76e6 100644 --- a/assets/vim_style_key_config.ron +++ b/assets/vim_style_key_config.ron @@ -58,4 +58,5 @@ log_tag_commit: ( code: Char('t'), modifiers: ( bits: 0,),), commit_amend: ( code: Char('A'), modifiers: ( bits: 0,),), copy: ( code: Char('y'), modifiers: ( bits: 0,),), + create_branch: ( code: Char('b'), modifiers: ( bits: 0,),), ) diff --git a/asyncgit/src/cached/branchname.rs b/asyncgit/src/cached/branchname.rs index c65a1b69..e5df31aa 100644 --- a/asyncgit/src/cached/branchname.rs +++ b/asyncgit/src/cached/branchname.rs @@ -1,11 +1,9 @@ -use crate::{ - error::Result, - sync::{self, CommitId}, -}; +use crate::{error::Result, sync}; +use sync::Head; /// pub struct BranchName { - last_result: Option<(CommitId, String)>, + last_result: Option<(Head, String)>, repo_path: String, } @@ -20,7 +18,8 @@ impl BranchName { /// pub fn lookup(&mut self) -> Result { - let current_head = sync::get_head(self.repo_path.as_str())?; + let current_head = + sync::get_head_tuple(self.repo_path.as_str())?; if let Some((last_head, branch_name)) = self.last_result.as_ref() @@ -33,7 +32,7 @@ impl BranchName { self.fetch(current_head) } - fn fetch(&mut self, head: CommitId) -> Result { + fn fetch(&mut self, head: Head) -> Result { let name = sync::get_branch_name(self.repo_path.as_str())?; self.last_result = Some((head, name.clone())); Ok(name) diff --git a/asyncgit/src/error.rs b/asyncgit/src/error.rs index f00f587b..b8c44d3e 100644 --- a/asyncgit/src/error.rs +++ b/asyncgit/src/error.rs @@ -1,3 +1,4 @@ +use std::string::FromUtf8Error; use thiserror::Error; #[derive(Error, Debug)] @@ -13,6 +14,9 @@ pub enum Error { #[error("git error:{0}")] Git(#[from] git2::Error), + + #[error("utf8 error:{0}")] + Utf8Error(#[from] FromUtf8Error), } pub type Result = std::result::Result; diff --git a/asyncgit/src/sync/branch.rs b/asyncgit/src/sync/branch.rs index 2d8bdacf..b77fd260 100644 --- a/asyncgit/src/sync/branch.rs +++ b/asyncgit/src/sync/branch.rs @@ -5,6 +5,7 @@ use crate::{ sync::utils, }; use scopetime::scope_time; +use utils::get_head_repo; /// returns the branch-name head is currently pointing to /// this might be expensive, see `cached::BranchName` @@ -27,8 +28,26 @@ pub(crate) fn get_branch_name(repo_path: &str) -> Result { Err(Error::NoHead) } +/// 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"); + + let repo = utils::repo(repo_path)?; + + let head_id = get_head_repo(&repo)?; + let head_commit = repo.find_commit(head_id.into())?; + + let branch = repo.branch(name, &head_commit, false)?; + let branch_ref = branch.into_reference(); + let branch_ref_name = + String::from_utf8(branch_ref.name_bytes().to_vec())?; + repo.set_head(branch_ref_name.as_str())?; + + Ok(()) +} + #[cfg(test)] -mod tests { +mod tests_branch_name { use super::*; use crate::sync::tests::{repo_init, repo_init_empty}; @@ -56,3 +75,23 @@ mod tests { )); } } + +#[cfg(test)] +mod tests_create_branch { + use super::*; + use crate::sync::tests::repo_init; + + #[test] + fn test_smoke() { + let (_td, repo) = repo_init().unwrap(); + let root = repo.path().parent().unwrap(); + let repo_path = root.as_os_str().to_str().unwrap(); + + create_branch(repo_path, "branch1").unwrap(); + + assert_eq!( + get_branch_name(repo_path).unwrap().as_str(), + "branch1" + ); + } +} diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index 20fbac10..58b32148 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -16,8 +16,8 @@ pub mod status; mod tags; pub mod utils; +pub use branch::create_branch; pub(crate) use branch::get_branch_name; - pub use commit::{amend, commit, tag}; pub use commit_details::{ get_commit_details, CommitDetails, CommitMessage, @@ -33,8 +33,8 @@ pub use reset::{reset_stage, reset_workdir}; pub use stash::{get_stashes, stash_apply, stash_drop, stash_save}; pub use tags::{get_tags, CommitTags, Tags}; pub use utils::{ - get_head, is_bare_repo, is_repo, stage_add_all, stage_add_file, - stage_addremoved, + get_head, get_head_tuple, is_bare_repo, is_repo, stage_add_all, + stage_add_file, stage_addremoved, Head, }; #[cfg(test)] diff --git a/asyncgit/src/sync/utils.rs b/asyncgit/src/sync/utils.rs index 0b847a65..1cf090d2 100644 --- a/asyncgit/src/sync/utils.rs +++ b/asyncgit/src/sync/utils.rs @@ -6,6 +6,15 @@ use git2::{IndexAddOption, Repository, RepositoryOpenFlags}; use scopetime::scope_time; use std::path::Path; +/// +#[derive(PartialEq, Debug, Clone)] +pub struct Head { + /// + pub name: String, + /// + pub id: CommitId, +} + /// pub fn is_repo(repo_path: &str) -> bool { Repository::open_ext( @@ -63,6 +72,24 @@ pub fn get_head(repo_path: &str) -> Result { get_head_repo(&repo) } +/// +pub fn get_head_tuple(repo_path: &str) -> Result { + let repo = repo(repo_path)?; + let id = get_head_repo(&repo)?; + let name = get_head_refname(&repo)?; + + Ok(Head { name, id }) +} + +/// +pub fn get_head_refname(repo: &Repository) -> Result { + let head = repo.head()?; + let name_bytes = head.name_bytes(); + let ref_name = String::from_utf8(name_bytes.to_vec())?; + + Ok(ref_name) +} + /// pub fn get_head_repo(repo: &Repository) -> Result { scope_time!("get_head_repo"); diff --git a/src/app.rs b/src/app.rs index c2a3dee9..eb7958ff 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,9 +3,10 @@ use crate::{ cmdbar::CommandBar, components::{ event_pump, CommandBlocking, CommandInfo, CommitComponent, - Component, DrawableComponent, ExternalEditorComponent, - HelpComponent, InspectCommitComponent, MsgComponent, - ResetComponent, StashMsgComponent, TagCommitComponent, + Component, CreateBranchComponent, DrawableComponent, + ExternalEditorComponent, HelpComponent, + InspectCommitComponent, MsgComponent, ResetComponent, + StashMsgComponent, TagCommitComponent, }, input::{Input, InputEvent, InputState}, keys::{KeyConfig, SharedKeyConfig}, @@ -41,6 +42,7 @@ pub struct App { inspect_commit_popup: InspectCommitComponent, external_editor_popup: ExternalEditorComponent, tag_commit_popup: TagCommitComponent, + create_branch_popup: CreateBranchComponent, cmdbar: RefCell, tab: usize, revlog: Revlog, @@ -101,6 +103,11 @@ impl App { theme.clone(), key_config.clone(), ), + create_branch_popup: CreateBranchComponent::new( + queue.clone(), + theme.clone(), + key_config.clone(), + ), do_quit: false, cmdbar: RefCell::new(CommandBar::new( theme.clone(), @@ -331,6 +338,7 @@ impl App { inspect_commit_popup, external_editor_popup, tag_commit_popup, + create_branch_popup, help, revlog, status_tab, @@ -459,6 +467,9 @@ impl App { InternalEvent::TagCommit(id) => { self.tag_commit_popup.open(id)?; } + InternalEvent::CreateBranch => { + self.create_branch_popup.open()?; + } InternalEvent::TabSwitch => self.set_tab(0)?, InternalEvent::InspectCommit(id, tags) => { self.inspect_commit_popup.open(id, tags)?; @@ -527,6 +538,7 @@ impl App { || self.inspect_commit_popup.is_visible() || self.external_editor_popup.is_visible() || self.tag_commit_popup.is_visible() + || self.create_branch_popup.is_visible() } fn draw_popups( @@ -552,6 +564,7 @@ impl App { self.msg.draw(f, size)?; self.external_editor_popup.draw(f, size)?; self.tag_commit_popup.draw(f, size)?; + self.create_branch_popup.draw(f, size)?; Ok(()) } diff --git a/src/components/create_branch.rs b/src/components/create_branch.rs new file mode 100644 index 00000000..d10e4f64 --- /dev/null +++ b/src/components/create_branch.rs @@ -0,0 +1,144 @@ +use super::{ + textinput::TextInputComponent, visibility_blocking, + CommandBlocking, CommandInfo, Component, DrawableComponent, +}; +use crate::{ + keys::SharedKeyConfig, + queue::{InternalEvent, NeedsUpdate, Queue}, + strings, + ui::style::SharedTheme, +}; +use anyhow::Result; +use asyncgit::{ + sync::{self, CommitId}, + CWD, +}; +use crossterm::event::Event; +use tui::{backend::Backend, layout::Rect, Frame}; + +pub struct CreateBranchComponent { + input: TextInputComponent, + commit_id: Option, + queue: Queue, + key_config: SharedKeyConfig, +} + +impl DrawableComponent for CreateBranchComponent { + fn draw( + &self, + f: &mut Frame, + rect: Rect, + ) -> Result<()> { + self.input.draw(f, rect)?; + + Ok(()) + } +} + +impl Component for CreateBranchComponent { + fn commands( + &self, + out: &mut Vec, + force_all: bool, + ) -> CommandBlocking { + if self.is_visible() || force_all { + self.input.commands(out, force_all); + + out.push(CommandInfo::new( + strings::commands::create_branch_confirm_msg( + &self.key_config, + ), + true, + true, + )); + } + + visibility_blocking(self) + } + + fn event(&mut self, ev: Event) -> Result { + if self.is_visible() { + if self.input.event(ev)? { + return Ok(true); + } + + if let Event::Key(e) = ev { + if e == self.key_config.enter { + self.create_branch(); + } + + return Ok(true); + } + } + Ok(false) + } + + fn is_visible(&self) -> bool { + self.input.is_visible() + } + + fn hide(&mut self) { + self.input.hide() + } + + fn show(&mut self) -> Result<()> { + self.input.show()?; + + Ok(()) + } +} + +impl CreateBranchComponent { + /// + pub fn new( + queue: Queue, + theme: SharedTheme, + key_config: SharedKeyConfig, + ) -> Self { + Self { + queue, + input: TextInputComponent::new( + theme, + key_config.clone(), + &strings::create_branch_popup_title(&key_config), + &strings::create_branch_popup_msg(&key_config), + ), + commit_id: None, + key_config, + } + } + + /// + pub fn open(&mut self) -> Result<()> { + self.commit_id = None; + self.show()?; + + Ok(()) + } + + /// + pub fn create_branch(&mut self) { + let res = + sync::create_branch(CWD, self.input.get_text().as_str()); + + self.input.clear(); + self.hide(); + + match res { + Ok(_) => { + self.queue.borrow_mut().push_back( + InternalEvent::Update(NeedsUpdate::ALL), + ); + } + Err(e) => { + log::error!("create branch: {}", e,); + self.queue.borrow_mut().push_back( + InternalEvent::ShowErrorMsg(format!( + "create branch error:\n{}", + e, + )), + ); + } + } + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs index bf316472..b137da10 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -3,6 +3,7 @@ mod command; mod commit; mod commit_details; mod commitlist; +mod create_branch; mod diff; mod externaleditor; mod filetree; @@ -23,6 +24,7 @@ pub use command::{CommandInfo, CommandText}; pub use commit::CommitComponent; pub use commit_details::CommitDetailsComponent; pub use commitlist::CommitList; +pub use create_branch::CreateBranchComponent; pub use diff::DiffComponent; pub use externaleditor::ExternalEditorComponent; pub use filetree::FileTreeComponent; diff --git a/src/keys.rs b/src/keys.rs index a99bab8e..41c1e7ff 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -59,6 +59,7 @@ pub struct KeyConfig { pub log_tag_commit: KeyEvent, pub commit_amend: KeyEvent, pub copy: KeyEvent, + pub create_branch: KeyEvent, } #[rustfmt::skip] @@ -106,7 +107,8 @@ impl Default for KeyConfig { cmd_bar_toggle: KeyEvent { code: KeyCode::Char('.'), modifiers: KeyModifiers::empty()}, 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()}, + copy: KeyEvent { code: KeyCode::Char('y'), modifiers: KeyModifiers::empty()}, + create_branch: KeyEvent { code: KeyCode::Char('b'), modifiers: KeyModifiers::empty()}, } } } diff --git a/src/queue.rs b/src/queue.rs index f519b37d..3e3478f3 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -51,6 +51,8 @@ pub enum InternalEvent { /// TagCommit(CommitId), /// + CreateBranch, + /// OpenExternalEditor(Option), } diff --git a/src/strings.rs b/src/strings.rs index 1444b2bd..07770877 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -110,6 +110,16 @@ pub fn stashing_options_title( pub fn loading_text(_key_config: &SharedKeyConfig) -> String { "Loading ...".to_string() } +pub fn create_branch_popup_title( + _key_config: &SharedKeyConfig, +) -> String { + "Branch".to_string() +} +pub fn create_branch_popup_msg( + _key_config: &SharedKeyConfig, +) -> String { + "type branch name".to_string() +} pub mod commit { use crate::keys::SharedKeyConfig; @@ -565,4 +575,25 @@ pub mod commands { CMD_GROUP_LOG, ) } + pub fn create_branch_confirm_msg( + key_config: &SharedKeyConfig, + ) -> CommandText { + CommandText::new( + format!("Create Branch [{}]", get_hint(key_config.enter),), + "create branch", + CMD_GROUP_GENERAL, + ) + } + pub fn open_branch_create_popup( + key_config: &SharedKeyConfig, + ) -> CommandText { + CommandText::new( + format!( + "Branch [{}]", + get_hint(key_config.create_branch), + ), + "open create branch popup", + CMD_GROUP_GENERAL, + ) + } } diff --git a/src/tabs/revlog.rs b/src/tabs/revlog.rs index 8ee79bac..b80289cb 100644 --- a/src/tabs/revlog.rs +++ b/src/tabs/revlog.rs @@ -231,7 +231,11 @@ impl Component for Revlog { Ok(true) }, ); - } else { + } else if k == self.key_config.create_branch { + self.queue + .borrow_mut() + .push_back(InternalEvent::CreateBranch); + return Ok(true); } } } @@ -267,6 +271,14 @@ impl Component for Revlog { self.visible || force_all, )); + out.push(CommandInfo::new( + strings::commands::open_branch_create_popup( + &self.key_config, + ), + true, + self.visible || force_all, + )); + visibility_blocking(self) } diff --git a/src/tabs/status.rs b/src/tabs/status.rs index 42194e32..120f159a 100644 --- a/src/tabs/status.rs +++ b/src/tabs/status.rs @@ -362,6 +362,14 @@ impl Component for Status { )); } + out.push(CommandInfo::new( + strings::commands::open_branch_create_popup( + &self.key_config, + ), + true, + true, + )); + out.push( CommandInfo::new( strings::commands::select_status(&self.key_config), @@ -438,6 +446,11 @@ impl Component for Status { && !self.index_wd.is_empty() { self.switch_focus(Focus::WorkDir) + } else if k == self.key_config.create_branch { + self.queue + .borrow_mut() + .push_back(InternalEvent::CreateBranch); + Ok(true) } else { Ok(false) };