mirror of
https://github.com/extrawurst/gitui.git
synced 2024-11-22 02:12:58 +03:00
parent
7da34eb3e4
commit
4907e8b727
@ -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))
|
||||
|
@ -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,),),
|
||||
)
|
||||
|
@ -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<String> {
|
||||
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<String> {
|
||||
fn fetch(&mut self, head: Head) -> Result<String> {
|
||||
let name = sync::get_branch_name(self.repo_path.as_str())?;
|
||||
self.last_result = Some((head, name.clone()));
|
||||
Ok(name)
|
||||
|
@ -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<T> = std::result::Result<T, Error>;
|
||||
|
@ -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<String> {
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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)]
|
||||
|
@ -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<CommitId> {
|
||||
get_head_repo(&repo)
|
||||
}
|
||||
|
||||
///
|
||||
pub fn get_head_tuple(repo_path: &str) -> Result<Head> {
|
||||
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<String> {
|
||||
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<CommitId> {
|
||||
scope_time!("get_head_repo");
|
||||
|
19
src/app.rs
19
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<CommandBar>,
|
||||
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<B: Backend>(
|
||||
@ -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(())
|
||||
}
|
||||
|
144
src/components/create_branch.rs
Normal file
144
src/components/create_branch.rs
Normal file
@ -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<CommitId>,
|
||||
queue: Queue,
|
||||
key_config: SharedKeyConfig,
|
||||
}
|
||||
|
||||
impl DrawableComponent for CreateBranchComponent {
|
||||
fn draw<B: Backend>(
|
||||
&self,
|
||||
f: &mut Frame<B>,
|
||||
rect: Rect,
|
||||
) -> Result<()> {
|
||||
self.input.draw(f, rect)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for CreateBranchComponent {
|
||||
fn commands(
|
||||
&self,
|
||||
out: &mut Vec<CommandInfo>,
|
||||
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<bool> {
|
||||
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,
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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()},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -51,6 +51,8 @@ pub enum InternalEvent {
|
||||
///
|
||||
TagCommit(CommitId),
|
||||
///
|
||||
CreateBranch,
|
||||
///
|
||||
OpenExternalEditor(Option<String>),
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user