Skip to content

Commit 4907e8b

Browse files
author
Stephan Dilly
authored
new 'create branch' popup (#254)
closes #253
1 parent 7da34eb commit 4907e8b

File tree

15 files changed

+307
-16
lines changed

15 files changed

+307
-16
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616

1717
![scrollbar](assets/scrollbar.gif)
1818

19+
- allow creating new branch ([#253](https://github.com/extrawurst/gitui/issues/253))
20+
1921
### Fixed
2022

2123
- selection error in stashlist when deleting last element ([#223](https://github.com/extrawurst/gitui/issues/223))

assets/vim_style_key_config.ron

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,5 @@
5858
log_tag_commit: ( code: Char('t'), modifiers: ( bits: 0,),),
5959
commit_amend: ( code: Char('A'), modifiers: ( bits: 0,),),
6060
copy: ( code: Char('y'), modifiers: ( bits: 0,),),
61+
create_branch: ( code: Char('b'), modifiers: ( bits: 0,),),
6162
)

asyncgit/src/cached/branchname.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
use crate::{
2-
error::Result,
3-
sync::{self, CommitId},
4-
};
1+
use crate::{error::Result, sync};
2+
use sync::Head;
53

64
///
75
pub struct BranchName {
8-
last_result: Option<(CommitId, String)>,
6+
last_result: Option<(Head, String)>,
97
repo_path: String,
108
}
119

@@ -20,7 +18,8 @@ impl BranchName {
2018

2119
///
2220
pub fn lookup(&mut self) -> Result<String> {
23-
let current_head = sync::get_head(self.repo_path.as_str())?;
21+
let current_head =
22+
sync::get_head_tuple(self.repo_path.as_str())?;
2423

2524
if let Some((last_head, branch_name)) =
2625
self.last_result.as_ref()
@@ -33,7 +32,7 @@ impl BranchName {
3332
self.fetch(current_head)
3433
}
3534

36-
fn fetch(&mut self, head: CommitId) -> Result<String> {
35+
fn fetch(&mut self, head: Head) -> Result<String> {
3736
let name = sync::get_branch_name(self.repo_path.as_str())?;
3837
self.last_result = Some((head, name.clone()));
3938
Ok(name)

asyncgit/src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::string::FromUtf8Error;
12
use thiserror::Error;
23

34
#[derive(Error, Debug)]
@@ -13,6 +14,9 @@ pub enum Error {
1314

1415
#[error("git error:{0}")]
1516
Git(#[from] git2::Error),
17+
18+
#[error("utf8 error:{0}")]
19+
Utf8Error(#[from] FromUtf8Error),
1620
}
1721

1822
pub type Result<T> = std::result::Result<T, Error>;

asyncgit/src/sync/branch.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::{
55
sync::utils,
66
};
77
use scopetime::scope_time;
8+
use utils::get_head_repo;
89

910
/// returns the branch-name head is currently pointing to
1011
/// this might be expensive, see `cached::BranchName`
@@ -27,8 +28,26 @@ pub(crate) fn get_branch_name(repo_path: &str) -> Result<String> {
2728
Err(Error::NoHead)
2829
}
2930

31+
/// creates a new branch pointing to current HEAD commit and updating HEAD to new branch
32+
pub fn create_branch(repo_path: &str, name: &str) -> Result<()> {
33+
scope_time!("create_branch");
34+
35+
let repo = utils::repo(repo_path)?;
36+
37+
let head_id = get_head_repo(&repo)?;
38+
let head_commit = repo.find_commit(head_id.into())?;
39+
40+
let branch = repo.branch(name, &head_commit, false)?;
41+
let branch_ref = branch.into_reference();
42+
let branch_ref_name =
43+
String::from_utf8(branch_ref.name_bytes().to_vec())?;
44+
repo.set_head(branch_ref_name.as_str())?;
45+
46+
Ok(())
47+
}
48+
3049
#[cfg(test)]
31-
mod tests {
50+
mod tests_branch_name {
3251
use super::*;
3352
use crate::sync::tests::{repo_init, repo_init_empty};
3453

@@ -56,3 +75,23 @@ mod tests {
5675
));
5776
}
5877
}
78+
79+
#[cfg(test)]
80+
mod tests_create_branch {
81+
use super::*;
82+
use crate::sync::tests::repo_init;
83+
84+
#[test]
85+
fn test_smoke() {
86+
let (_td, repo) = repo_init().unwrap();
87+
let root = repo.path().parent().unwrap();
88+
let repo_path = root.as_os_str().to_str().unwrap();
89+
90+
create_branch(repo_path, "branch1").unwrap();
91+
92+
assert_eq!(
93+
get_branch_name(repo_path).unwrap().as_str(),
94+
"branch1"
95+
);
96+
}
97+
}

asyncgit/src/sync/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ pub mod status;
1616
mod tags;
1717
pub mod utils;
1818

19+
pub use branch::create_branch;
1920
pub(crate) use branch::get_branch_name;
20-
2121
pub use commit::{amend, commit, tag};
2222
pub use commit_details::{
2323
get_commit_details, CommitDetails, CommitMessage,
@@ -33,8 +33,8 @@ pub use reset::{reset_stage, reset_workdir};
3333
pub use stash::{get_stashes, stash_apply, stash_drop, stash_save};
3434
pub use tags::{get_tags, CommitTags, Tags};
3535
pub use utils::{
36-
get_head, is_bare_repo, is_repo, stage_add_all, stage_add_file,
37-
stage_addremoved,
36+
get_head, get_head_tuple, is_bare_repo, is_repo, stage_add_all,
37+
stage_add_file, stage_addremoved, Head,
3838
};
3939

4040
#[cfg(test)]

asyncgit/src/sync/utils.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ use git2::{IndexAddOption, Repository, RepositoryOpenFlags};
66
use scopetime::scope_time;
77
use std::path::Path;
88

9+
///
10+
#[derive(PartialEq, Debug, Clone)]
11+
pub struct Head {
12+
///
13+
pub name: String,
14+
///
15+
pub id: CommitId,
16+
}
17+
918
///
1019
pub fn is_repo(repo_path: &str) -> bool {
1120
Repository::open_ext(
@@ -63,6 +72,24 @@ pub fn get_head(repo_path: &str) -> Result<CommitId> {
6372
get_head_repo(&repo)
6473
}
6574

75+
///
76+
pub fn get_head_tuple(repo_path: &str) -> Result<Head> {
77+
let repo = repo(repo_path)?;
78+
let id = get_head_repo(&repo)?;
79+
let name = get_head_refname(&repo)?;
80+
81+
Ok(Head { name, id })
82+
}
83+
84+
///
85+
pub fn get_head_refname(repo: &Repository) -> Result<String> {
86+
let head = repo.head()?;
87+
let name_bytes = head.name_bytes();
88+
let ref_name = String::from_utf8(name_bytes.to_vec())?;
89+
90+
Ok(ref_name)
91+
}
92+
6693
///
6794
pub fn get_head_repo(repo: &Repository) -> Result<CommitId> {
6895
scope_time!("get_head_repo");

src/app.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ use crate::{
33
cmdbar::CommandBar,
44
components::{
55
event_pump, CommandBlocking, CommandInfo, CommitComponent,
6-
Component, DrawableComponent, ExternalEditorComponent,
7-
HelpComponent, InspectCommitComponent, MsgComponent,
8-
ResetComponent, StashMsgComponent, TagCommitComponent,
6+
Component, CreateBranchComponent, DrawableComponent,
7+
ExternalEditorComponent, HelpComponent,
8+
InspectCommitComponent, MsgComponent, ResetComponent,
9+
StashMsgComponent, TagCommitComponent,
910
},
1011
input::{Input, InputEvent, InputState},
1112
keys::{KeyConfig, SharedKeyConfig},
@@ -41,6 +42,7 @@ pub struct App {
4142
inspect_commit_popup: InspectCommitComponent,
4243
external_editor_popup: ExternalEditorComponent,
4344
tag_commit_popup: TagCommitComponent,
45+
create_branch_popup: CreateBranchComponent,
4446
cmdbar: RefCell<CommandBar>,
4547
tab: usize,
4648
revlog: Revlog,
@@ -101,6 +103,11 @@ impl App {
101103
theme.clone(),
102104
key_config.clone(),
103105
),
106+
create_branch_popup: CreateBranchComponent::new(
107+
queue.clone(),
108+
theme.clone(),
109+
key_config.clone(),
110+
),
104111
do_quit: false,
105112
cmdbar: RefCell::new(CommandBar::new(
106113
theme.clone(),
@@ -331,6 +338,7 @@ impl App {
331338
inspect_commit_popup,
332339
external_editor_popup,
333340
tag_commit_popup,
341+
create_branch_popup,
334342
help,
335343
revlog,
336344
status_tab,
@@ -459,6 +467,9 @@ impl App {
459467
InternalEvent::TagCommit(id) => {
460468
self.tag_commit_popup.open(id)?;
461469
}
470+
InternalEvent::CreateBranch => {
471+
self.create_branch_popup.open()?;
472+
}
462473
InternalEvent::TabSwitch => self.set_tab(0)?,
463474
InternalEvent::InspectCommit(id, tags) => {
464475
self.inspect_commit_popup.open(id, tags)?;
@@ -527,6 +538,7 @@ impl App {
527538
|| self.inspect_commit_popup.is_visible()
528539
|| self.external_editor_popup.is_visible()
529540
|| self.tag_commit_popup.is_visible()
541+
|| self.create_branch_popup.is_visible()
530542
}
531543

532544
fn draw_popups<B: Backend>(
@@ -552,6 +564,7 @@ impl App {
552564
self.msg.draw(f, size)?;
553565
self.external_editor_popup.draw(f, size)?;
554566
self.tag_commit_popup.draw(f, size)?;
567+
self.create_branch_popup.draw(f, size)?;
555568

556569
Ok(())
557570
}

src/components/create_branch.rs

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
use super::{
2+
textinput::TextInputComponent, visibility_blocking,
3+
CommandBlocking, CommandInfo, Component, DrawableComponent,
4+
};
5+
use crate::{
6+
keys::SharedKeyConfig,
7+
queue::{InternalEvent, NeedsUpdate, Queue},
8+
strings,
9+
ui::style::SharedTheme,
10+
};
11+
use anyhow::Result;
12+
use asyncgit::{
13+
sync::{self, CommitId},
14+
CWD,
15+
};
16+
use crossterm::event::Event;
17+
use tui::{backend::Backend, layout::Rect, Frame};
18+
19+
pub struct CreateBranchComponent {
20+
input: TextInputComponent,
21+
commit_id: Option<CommitId>,
22+
queue: Queue,
23+
key_config: SharedKeyConfig,
24+
}
25+
26+
impl DrawableComponent for CreateBranchComponent {
27+
fn draw<B: Backend>(
28+
&self,
29+
f: &mut Frame<B>,
30+
rect: Rect,
31+
) -> Result<()> {
32+
self.input.draw(f, rect)?;
33+
34+
Ok(())
35+
}
36+
}
37+
38+
impl Component for CreateBranchComponent {
39+
fn commands(
40+
&self,
41+
out: &mut Vec<CommandInfo>,
42+
force_all: bool,
43+
) -> CommandBlocking {
44+
if self.is_visible() || force_all {
45+
self.input.commands(out, force_all);
46+
47+
out.push(CommandInfo::new(
48+
strings::commands::create_branch_confirm_msg(
49+
&self.key_config,
50+
),
51+
true,
52+
true,
53+
));
54+
}
55+
56+
visibility_blocking(self)
57+
}
58+
59+
fn event(&mut self, ev: Event) -> Result<bool> {
60+
if self.is_visible() {
61+
if self.input.event(ev)? {
62+
return Ok(true);
63+
}
64+
65+
if let Event::Key(e) = ev {
66+
if e == self.key_config.enter {
67+
self.create_branch();
68+
}
69+
70+
return Ok(true);
71+
}
72+
}
73+
Ok(false)
74+
}
75+
76+
fn is_visible(&self) -> bool {
77+
self.input.is_visible()
78+
}
79+
80+
fn hide(&mut self) {
81+
self.input.hide()
82+
}
83+
84+
fn show(&mut self) -> Result<()> {
85+
self.input.show()?;
86+
87+
Ok(())
88+
}
89+
}
90+
91+
impl CreateBranchComponent {
92+
///
93+
pub fn new(
94+
queue: Queue,
95+
theme: SharedTheme,
96+
key_config: SharedKeyConfig,
97+
) -> Self {
98+
Self {
99+
queue,
100+
input: TextInputComponent::new(
101+
theme,
102+
key_config.clone(),
103+
&strings::create_branch_popup_title(&key_config),
104+
&strings::create_branch_popup_msg(&key_config),
105+
),
106+
commit_id: None,
107+
key_config,
108+
}
109+
}
110+
111+
///
112+
pub fn open(&mut self) -> Result<()> {
113+
self.commit_id = None;
114+
self.show()?;
115+
116+
Ok(())
117+
}
118+
119+
///
120+
pub fn create_branch(&mut self) {
121+
let res =
122+
sync::create_branch(CWD, self.input.get_text().as_str());
123+
124+
self.input.clear();
125+
self.hide();
126+
127+
match res {
128+
Ok(_) => {
129+
self.queue.borrow_mut().push_back(
130+
InternalEvent::Update(NeedsUpdate::ALL),
131+
);
132+
}
133+
Err(e) => {
134+
log::error!("create branch: {}", e,);
135+
self.queue.borrow_mut().push_back(
136+
InternalEvent::ShowErrorMsg(format!(
137+
"create branch error:\n{}",
138+
e,
139+
)),
140+
);
141+
}
142+
}
143+
}
144+
}

0 commit comments

Comments
 (0)