support opening submodule (#1298)

This commit is contained in:
extrawurst 2022-08-31 10:51:08 +02:00 committed by GitHub
parent aa9ed3349f
commit 986d34a5ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 392 additions and 51 deletions

20
Cargo.lock generated
View file

@ -67,6 +67,7 @@ version = "0.21.0"
dependencies = [ dependencies = [
"crossbeam-channel", "crossbeam-channel",
"easy-cast", "easy-cast",
"env_logger",
"git2", "git2",
"invalidstring", "invalidstring",
"log", "log",
@ -422,6 +423,19 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
[[package]]
name = "env_logger"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
dependencies = [
"atty",
"humantime",
"log",
"regex",
"termcolor",
]
[[package]] [[package]]
name = "fancy-regex" name = "fancy-regex"
version = "0.7.1" version = "0.7.1"
@ -692,6 +706,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.46" version = "0.1.46"

View file

@ -51,6 +51,7 @@
- Browse commit log, diff committed changes - Browse commit log, diff committed changes
- Scalable terminal UI layout - Scalable terminal UI layout
- Async git API for fluid control - Async git API for fluid control
- Submodule support
## 2. <a name="motivation"></a> Motivation <small><sup>[Top ▲](#table-of-contents)</sup></small> ## 2. <a name="motivation"></a> Motivation <small><sup>[Top ▲](#table-of-contents)</sup></small>

View file

@ -28,6 +28,7 @@ unicode-truncate = "0.2.0"
url = "2.2" url = "2.2"
[dev-dependencies] [dev-dependencies]
env_logger = "0.9"
invalidstring = { path = "../invalidstring", version = "0.1" } invalidstring = { path = "../invalidstring", version = "0.1" }
pretty_assertions = "1.3" pretty_assertions = "1.3"
serial_test = "0.9" serial_test = "0.9"

View file

@ -1,6 +1,9 @@
#![allow(renamed_and_removed_lints, clippy::unknown_clippy_lints)] #![allow(renamed_and_removed_lints, clippy::unknown_clippy_lints)]
use std::{num::TryFromIntError, string::FromUtf8Error}; use std::{
num::TryFromIntError, path::StripPrefixError,
string::FromUtf8Error,
};
use thiserror::Error; use thiserror::Error;
/// ///
@ -50,6 +53,10 @@ pub enum Error {
#[error("git error:{0}")] #[error("git error:{0}")]
Git(#[from] git2::Error), Git(#[from] git2::Error),
///
#[error("strip prefix error: {0}")]
StripPrefix(#[from] StripPrefixError),
/// ///
#[error("utf8 error:{0}")] #[error("utf8 error:{0}")]
Utf8Conversion(#[from] FromUtf8Error), Utf8Conversion(#[from] FromUtf8Error),

View file

@ -82,7 +82,8 @@ pub use stash::{
pub use state::{repo_state, RepoState}; pub use state::{repo_state, RepoState};
pub use status::is_workdir_clean; pub use status::is_workdir_clean;
pub use submodules::{ pub use submodules::{
get_submodules, update_submodule, SubmoduleInfo, SubmoduleStatus, get_submodules, submodule_parent_info, update_submodule,
SubmoduleInfo, SubmoduleParentInfo, SubmoduleStatus,
}; };
pub use tags::{ pub use tags::{
delete_tag, get_tags, get_tags_with_metadata, CommitTags, Tag, delete_tag, get_tags, get_tags_with_metadata, CommitTags, Tag,
@ -209,6 +210,8 @@ mod tests {
/// ///
pub fn repo_init_empty() -> Result<(TempDir, Repository)> { pub fn repo_init_empty() -> Result<(TempDir, Repository)> {
init_log();
sandbox_config_files(); sandbox_config_files();
let td = TempDir::new()?; let td = TempDir::new()?;
@ -223,6 +226,8 @@ mod tests {
/// ///
pub fn repo_init() -> Result<(TempDir, Repository)> { pub fn repo_init() -> Result<(TempDir, Repository)> {
init_log();
sandbox_config_files(); sandbox_config_files();
let td = TempDir::new()?; let td = TempDir::new()?;
@ -266,8 +271,18 @@ mod tests {
Ok((td, repo)) Ok((td, repo))
} }
// init log
fn init_log() {
let _ = env_logger::builder()
.is_test(true)
.filter_level(log::LevelFilter::Trace)
.try_init();
}
/// Same as repo_init, but the repo is a bare repo (--bare) /// Same as repo_init, but the repo is a bare repo (--bare)
pub fn repo_init_bare() -> Result<(TempDir, Repository)> { pub fn repo_init_bare() -> Result<(TempDir, Repository)> {
init_log();
let tmp_repo_dir = TempDir::new()?; let tmp_repo_dir = TempDir::new()?;
let bare_repo = Repository::init_bare(tmp_repo_dir.path())?; let bare_repo = Repository::init_bare(tmp_repo_dir.path())?;
Ok((tmp_repo_dir, bare_repo)) Ok((tmp_repo_dir, bare_repo))

View file

@ -11,7 +11,7 @@ use crate::error::Result;
pub type RepoPathRef = RefCell<RepoPath>; pub type RepoPathRef = RefCell<RepoPath>;
/// ///
#[derive(Clone)] #[derive(Clone, Debug)]
pub enum RepoPath { pub enum RepoPath {
/// ///
Path(PathBuf), Path(PathBuf),

View file

@ -1,15 +1,24 @@
use std::path::PathBuf; //TODO:
// #![allow(unused_variables, dead_code)]
use git2::SubmoduleUpdateOptions; use std::path::{Path, PathBuf};
use git2::{
Repository, RepositoryOpenFlags, Submodule,
SubmoduleUpdateOptions,
};
use scopetime::scope_time; use scopetime::scope_time;
use super::{repo, CommitId, RepoPath}; use super::{repo, CommitId, RepoPath};
use crate::{error::Result, Error}; use crate::{error::Result, sync::utils::work_dir, Error};
pub use git2::SubmoduleStatus; pub use git2::SubmoduleStatus;
/// ///
#[derive(Debug)]
pub struct SubmoduleInfo { pub struct SubmoduleInfo {
///
pub name: String,
/// ///
pub path: PathBuf, pub path: PathBuf,
/// ///
@ -22,6 +31,17 @@ pub struct SubmoduleInfo {
pub status: SubmoduleStatus, pub status: SubmoduleStatus,
} }
///
#[derive(Debug)]
pub struct SubmoduleParentInfo {
/// where to find parent repo
pub parent_gitpath: PathBuf,
/// where to find submodule git path
pub submodule_gitpath: PathBuf,
/// `submodule_info` from perspective of parent repo
pub submodule_info: SubmoduleInfo,
}
impl SubmoduleInfo { impl SubmoduleInfo {
/// ///
pub fn get_repo_path( pub fn get_repo_path(
@ -35,6 +55,24 @@ impl SubmoduleInfo {
} }
} }
fn submodule_to_info(s: &Submodule, r: &Repository) -> SubmoduleInfo {
let status = r
.submodule_status(
s.name().unwrap_or_default(),
git2::SubmoduleIgnore::None,
)
.unwrap_or(SubmoduleStatus::empty());
SubmoduleInfo {
name: s.name().unwrap_or_default().into(),
path: s.path().to_path_buf(),
id: s.workdir_id().map(CommitId::from),
head_id: s.head_id().map(CommitId::from),
url: s.url().map(String::from),
status,
}
}
/// ///
pub fn get_submodules( pub fn get_submodules(
repo_path: &RepoPath, repo_path: &RepoPath,
@ -46,22 +84,7 @@ pub fn get_submodules(
let res = r let res = r
.submodules()? .submodules()?
.iter() .iter()
.map(|s| { .map(|s| submodule_to_info(s, &repo2))
let status = repo2
.submodule_status(
s.name().unwrap_or_default(),
git2::SubmoduleIgnore::None,
)
.unwrap_or(SubmoduleStatus::empty());
SubmoduleInfo {
path: s.path().to_path_buf(),
id: s.workdir_id().map(CommitId::from),
head_id: s.head_id().map(CommitId::from),
url: s.url().map(String::from),
status,
}
})
.collect(); .collect();
Ok(res) Ok(res)
@ -82,3 +105,97 @@ pub fn update_submodule(
Ok(()) Ok(())
} }
/// query whether `repo_path` points to a repo that is part of a parent git which contains it as a submodule
pub fn submodule_parent_info(
repo_path: &RepoPath,
) -> Result<Option<SubmoduleParentInfo>> {
scope_time!("submodule_parent_info");
let repo = repo(repo_path)?;
let repo_wd = work_dir(&repo)?.to_path_buf();
log::trace!("[sub] repo_wd: {:?}", repo_wd);
log::trace!("[sub] repo_path: {:?}", repo.path());
if let Some(parent_path) = repo_wd.parent() {
log::trace!("[sub] parent_path: {:?}", parent_path);
if let Ok(parent) = Repository::open_ext(
parent_path,
RepositoryOpenFlags::empty(),
Vec::<&Path>::new(),
) {
let parent_wd = work_dir(&parent)?.to_path_buf();
log::trace!("[sub] parent_wd: {:?}", parent_wd);
let submodule_name = repo_wd
.strip_prefix(parent_wd)?
.to_string_lossy()
.to_string();
log::trace!("[sub] submodule_name: {:?}", submodule_name);
if let Ok(submodule) =
parent.find_submodule(&submodule_name)
{
return Ok(Some(SubmoduleParentInfo {
parent_gitpath: parent.path().to_path_buf(),
submodule_gitpath: repo.path().to_path_buf(),
submodule_info: submodule_to_info(
&submodule, &parent,
),
}));
}
}
}
Ok(None)
}
#[cfg(test)]
mod tests {
use super::get_submodules;
use crate::sync::{
submodules::submodule_parent_info, tests::repo_init, RepoPath,
};
use git2::Repository;
use pretty_assertions::assert_eq;
use std::path::Path;
#[test]
fn test_smoke() {
let (dir, _r) = repo_init().unwrap();
{
let r = Repository::open(dir.path()).unwrap();
let mut s = r
.submodule(
//TODO: use local git
"https://github.com/extrawurst/brewdump.git",
Path::new("foo/bar"),
false,
)
.unwrap();
let _sub_r = s.clone(None).unwrap();
s.add_finalize().unwrap();
}
let repo_p = RepoPath::Path(dir.into_path());
let subs = get_submodules(&repo_p).unwrap();
assert_eq!(subs.len(), 1);
assert_eq!(&subs[0].name, "foo/bar");
let info = submodule_parent_info(
&subs[0].get_repo_path(&repo_p).unwrap(),
)
.unwrap()
.unwrap();
dbg!(&info);
assert_eq!(&info.submodule_info.name, "foo/bar");
}
}

View file

@ -28,7 +28,7 @@ use crate::{
}; };
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use asyncgit::{ use asyncgit::{
sync::{self, RepoPathRef}, sync::{self, utils::repo_work_dir, RepoPath, RepoPathRef},
AsyncGitNotification, PushType, AsyncGitNotification, PushType,
}; };
use crossbeam_channel::Sender; use crossbeam_channel::Sender;
@ -46,10 +46,17 @@ use tui::{
Frame, Frame,
}; };
#[derive(Clone)]
pub enum QuitState {
None,
Close,
OpenSubmodule(RepoPath),
}
/// the main app type /// the main app type
pub struct App { pub struct App {
repo: RepoPathRef, repo: RepoPathRef,
do_quit: bool, do_quit: QuitState,
help: HelpComponent, help: HelpComponent,
msg: MsgComponent, msg: MsgComponent,
reset: ConfirmComponent, reset: ConfirmComponent,
@ -103,6 +110,8 @@ impl App {
theme: Theme, theme: Theme,
key_config: KeyConfig, key_config: KeyConfig,
) -> Self { ) -> Self {
log::trace!("open repo at: {:?}", repo);
let queue = Queue::new(); let queue = Queue::new();
let theme = Rc::new(theme); let theme = Rc::new(theme);
let key_config = Rc::new(key_config); let key_config = Rc::new(key_config);
@ -235,6 +244,7 @@ impl App {
), ),
submodule_popup: SubmodulesListComponent::new( submodule_popup: SubmodulesListComponent::new(
repo.clone(), repo.clone(),
&queue,
theme.clone(), theme.clone(),
key_config.clone(), key_config.clone(),
), ),
@ -243,7 +253,7 @@ impl App {
theme.clone(), theme.clone(),
key_config.clone(), key_config.clone(),
), ),
do_quit: false, do_quit: QuitState::None,
cmdbar: RefCell::new(CommandBar::new( cmdbar: RefCell::new(CommandBar::new(
theme.clone(), theme.clone(),
key_config.clone(), key_config.clone(),
@ -493,7 +503,13 @@ impl App {
/// ///
pub fn is_quit(&self) -> bool { pub fn is_quit(&self) -> bool {
self.do_quit || self.input.is_aborted() !matches!(self.do_quit, QuitState::None)
|| self.input.is_aborted()
}
///
pub fn quit_state(&self) -> QuitState {
self.do_quit.clone()
} }
/// ///
@ -597,7 +613,7 @@ impl App {
} }
if let Event::Key(e) = ev { if let Event::Key(e) = ev {
if key_match(e, self.key_config.keys.quit) { if key_match(e, self.key_config.keys.quit) {
self.do_quit = true; self.do_quit = QuitState::Close;
return true; return true;
} }
} }
@ -607,7 +623,7 @@ impl App {
fn check_hard_exit(&mut self, ev: &Event) -> bool { fn check_hard_exit(&mut self, ev: &Event) -> bool {
if let Event::Key(e) = ev { if let Event::Key(e) = ev {
if key_match(e, self.key_config.keys.exit) { if key_match(e, self.key_config.keys.exit) {
self.do_quit = true; self.do_quit = QuitState::Close;
return true; return true;
} }
} }
@ -878,6 +894,15 @@ impl App {
flags flags
.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS); .insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS);
} }
InternalEvent::OpenRepo { path } => {
let submodule_repo_path = RepoPath::Path(
Path::new(&repo_work_dir(&self.repo.borrow())?)
.join(path),
);
//TODO: validate this is a valid repo first, so we can show proper error otherwise
self.do_quit =
QuitState::OpenSubmodule(submodule_repo_path);
}
}; };
Ok(flags) Ok(flags)

View file

@ -5,11 +5,15 @@ use super::{
}; };
use crate::{ use crate::{
keys::{key_match, SharedKeyConfig}, keys::{key_match, SharedKeyConfig},
queue::{InternalEvent, Queue},
strings, strings,
ui::{self, Size}, ui::{self, Size},
}; };
use anyhow::Result; use anyhow::Result;
use asyncgit::sync::{get_submodules, RepoPathRef, SubmoduleInfo}; use asyncgit::sync::{
get_submodules, repo_dir, submodule_parent_info, RepoPathRef,
SubmoduleInfo, SubmoduleParentInfo,
};
use crossterm::event::Event; use crossterm::event::Event;
use std::{cell::Cell, convert::TryInto}; use std::{cell::Cell, convert::TryInto};
use tui::{ use tui::{
@ -18,7 +22,7 @@ use tui::{
Alignment, Constraint, Direction, Layout, Margin, Rect, Alignment, Constraint, Direction, Layout, Margin, Rect,
}, },
text::{Span, Spans, Text}, text::{Span, Spans, Text},
widgets::{Block, BorderType, Borders, Clear, Paragraph}, widgets::{Block, Borders, Clear, Paragraph},
Frame, Frame,
}; };
use ui::style::SharedTheme; use ui::style::SharedTheme;
@ -27,7 +31,10 @@ use unicode_truncate::UnicodeTruncateStr;
/// ///
pub struct SubmodulesListComponent { pub struct SubmodulesListComponent {
repo: RepoPathRef, repo: RepoPathRef,
repo_path: String,
queue: Queue,
submodules: Vec<SubmoduleInfo>, submodules: Vec<SubmoduleInfo>,
submodule_parent: Option<SubmoduleParentInfo>,
visible: bool, visible: bool,
current_height: Cell<u16>, current_height: Cell<u16>,
selection: u16, selection: u16,
@ -59,7 +66,6 @@ impl DrawableComponent for SubmodulesListComponent {
f.render_widget( f.render_widget(
Block::default() Block::default()
.title(strings::POPUP_TITLE_SUBMODULES) .title(strings::POPUP_TITLE_SUBMODULES)
.border_type(BorderType::Thick)
.borders(Borders::ALL), .borders(Borders::ALL),
area, area,
); );
@ -69,16 +75,25 @@ impl DrawableComponent for SubmodulesListComponent {
horizontal: 1, horizontal: 1,
}); });
let chunks_vertical = Layout::default()
.direction(Direction::Vertical)
.constraints(
[Constraint::Min(1), Constraint::Length(5)]
.as_ref(),
)
.split(area);
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints( .constraints(
[Constraint::Min(40), Constraint::Length(40)] [Constraint::Min(40), Constraint::Length(40)]
.as_ref(), .as_ref(),
) )
.split(area); .split(chunks_vertical[0]);
self.draw_list(f, chunks[0])?; self.draw_list(f, chunks[0])?;
self.draw_info(f, chunks[1]); self.draw_info(f, chunks[1]);
self.draw_local_info(f, chunks_vertical[1]);
} }
Ok(()) Ok(())
@ -107,6 +122,20 @@ impl Component for SubmodulesListComponent {
true, true,
true, true,
)); ));
out.push(CommandInfo::new(
strings::commands::open_submodule(&self.key_config),
self.is_valid_selection(),
true,
));
out.push(CommandInfo::new(
strings::commands::open_submodule_parent(
&self.key_config,
),
self.submodule_parent.is_some(),
true,
));
} }
visibility_blocking(self) visibility_blocking(self)
} }
@ -143,6 +172,21 @@ impl Component for SubmodulesListComponent {
return self return self
.move_selection(ScrollType::End) .move_selection(ScrollType::End)
.map(Into::into); .map(Into::into);
} else if key_match(e, self.key_config.keys.enter) {
if let Some(submodule) = self.selected_entry() {
self.queue.push(InternalEvent::OpenRepo {
path: submodule.path.clone(),
});
}
} else if key_match(
e,
self.key_config.keys.view_submodule_parent,
) {
if let Some(parent) = &self.submodule_parent {
self.queue.push(InternalEvent::OpenRepo {
path: parent.parent_gitpath.clone(),
});
}
} else if key_match( } else if key_match(
e, e,
self.key_config.keys.cmd_bar_toggle, self.key_config.keys.cmd_bar_toggle,
@ -173,18 +217,22 @@ impl Component for SubmodulesListComponent {
impl SubmodulesListComponent { impl SubmodulesListComponent {
pub fn new( pub fn new(
repo: RepoPathRef, repo: RepoPathRef,
queue: &Queue,
theme: SharedTheme, theme: SharedTheme,
key_config: SharedKeyConfig, key_config: SharedKeyConfig,
) -> Self { ) -> Self {
Self { Self {
submodules: Vec::new(), submodules: Vec::new(),
submodule_parent: None,
scroll: VerticalScroll::new(), scroll: VerticalScroll::new(),
queue: queue.clone(),
selection: 0, selection: 0,
visible: false, visible: false,
theme, theme,
key_config, key_config,
current_height: Cell::new(0), current_height: Cell::new(0),
repo, repo,
repo_path: String::new(),
} }
} }
@ -201,6 +249,13 @@ impl SubmodulesListComponent {
if self.is_visible() { if self.is_visible() {
self.submodules = get_submodules(&self.repo.borrow())?; self.submodules = get_submodules(&self.repo.borrow())?;
self.submodule_parent =
submodule_parent_info(&self.repo.borrow())?;
self.repo_path = repo_dir(&self.repo.borrow())
.map(|e| e.to_string_lossy().to_string())
.unwrap_or_default();
self.set_selection(self.selection)?; self.set_selection(self.selection)?;
} }
Ok(()) Ok(())
@ -210,6 +265,10 @@ impl SubmodulesListComponent {
self.submodules.get(self.selection as usize) self.submodules.get(self.selection as usize)
} }
fn is_valid_selection(&self) -> bool {
self.selected_entry().is_some()
}
//TODO: dedup this almost identical with BranchListComponent //TODO: dedup this almost identical with BranchListComponent
fn move_selection(&mut self, scroll: ScrollType) -> Result<bool> { fn move_selection(&mut self, scroll: ScrollType) -> Result<bool> {
let new_selection = match scroll { let new_selection = match scroll {
@ -234,11 +293,11 @@ impl SubmodulesListComponent {
} }
fn set_selection(&mut self, selection: u16) -> Result<()> { fn set_selection(&mut self, selection: u16) -> Result<()> {
let num_branches: u16 = self.submodules.len().try_into()?; let num_entriess: u16 = self.submodules.len().try_into()?;
let num_branches = num_branches.saturating_sub(1); let num_entriess = num_entriess.saturating_sub(1);
let selection = if selection > num_branches { let selection = if selection > num_entriess {
num_branches num_entriess
} else { } else {
selection selection
}; };
@ -359,6 +418,32 @@ impl SubmodulesListComponent {
) )
} }
fn get_local_info_text(&self, theme: &SharedTheme) -> Text {
let mut spans = vec![
Spans::from(vec![Span::styled(
"Current:",
theme.text(false, false),
)]),
Spans::from(vec![Span::styled(
self.repo_path.to_string(),
theme.text(true, false),
)]),
Spans::from(vec![Span::styled(
"Parent:",
theme.text(false, false),
)]),
];
if let Some(parent_info) = &self.submodule_parent {
spans.push(Spans::from(vec![Span::styled(
parent_info.parent_gitpath.to_string_lossy(),
theme.text(true, false),
)]));
}
Text::from(spans)
}
fn draw_list<B: Backend>( fn draw_list<B: Backend>(
&self, &self,
f: &mut Frame<B>, f: &mut Frame<B>,
@ -399,4 +484,13 @@ impl SubmodulesListComponent {
r, r,
); );
} }
fn draw_local_info<B: Backend>(&self, f: &mut Frame<B>, r: Rect) {
f.render_widget(
Paragraph::new(self.get_local_info_text(&self.theme))
.block(Block::default().borders(Borders::TOP))
.alignment(Alignment::Left),
r,
);
}
} }

View file

@ -28,6 +28,7 @@ pub enum InputEvent {
} }
/// ///
#[derive(Clone)]
pub struct Input { pub struct Input {
desired_state: Arc<NotifyableMutex<bool>>, desired_state: Arc<NotifyableMutex<bool>>,
current_state: Arc<AtomicBool>, current_state: Arc<AtomicBool>,

View file

@ -11,7 +11,7 @@ use super::{
pub type SharedKeyConfig = Rc<KeyConfig>; pub type SharedKeyConfig = Rc<KeyConfig>;
#[derive(Default)] #[derive(Default, Clone)]
pub struct KeyConfig { pub struct KeyConfig {
pub keys: KeysList, pub keys: KeysList,
symbols: KeySymbols, symbols: KeySymbols,

View file

@ -34,6 +34,7 @@ impl From<&GituiKeyEvent> for KeyEvent {
} }
} }
#[derive(Clone)]
pub struct KeysList { pub struct KeysList {
pub tab_status: GituiKeyEvent, pub tab_status: GituiKeyEvent,
pub tab_log: GituiKeyEvent, pub tab_log: GituiKeyEvent,
@ -108,6 +109,7 @@ pub struct KeysList {
pub stage_unstage_item: GituiKeyEvent, pub stage_unstage_item: GituiKeyEvent,
pub tag_annotate: GituiKeyEvent, pub tag_annotate: GituiKeyEvent,
pub view_submodules: GituiKeyEvent, pub view_submodules: GituiKeyEvent,
pub view_submodule_parent: GituiKeyEvent,
} }
#[rustfmt::skip] #[rustfmt::skip]
@ -187,7 +189,7 @@ impl Default for KeysList {
stage_unstage_item: GituiKeyEvent::new(KeyCode::Enter, KeyModifiers::empty()), stage_unstage_item: GituiKeyEvent::new(KeyCode::Enter, KeyModifiers::empty()),
tag_annotate: GituiKeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL), tag_annotate: GituiKeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL),
view_submodules: GituiKeyEvent::new(KeyCode::Char('S'), KeyModifiers::SHIFT), view_submodules: GituiKeyEvent::new(KeyCode::Char('S'), KeyModifiers::SHIFT),
view_submodule_parent: GituiKeyEvent::new(KeyCode::Char('p'), KeyModifiers::empty()),
} }
} }
} }

View file

@ -168,6 +168,7 @@ impl KeysListFile {
stage_unstage_item: self.stage_unstage_item.unwrap_or(default.stage_unstage_item), stage_unstage_item: self.stage_unstage_item.unwrap_or(default.stage_unstage_item),
tag_annotate: self.tag_annotate.unwrap_or(default.tag_annotate), tag_annotate: self.tag_annotate.unwrap_or(default.tag_annotate),
view_submodules: self.view_submodules.unwrap_or(default.view_submodules), view_submodules: self.view_submodules.unwrap_or(default.view_submodules),
view_submodule_parent: self.view_submodules.unwrap_or(default.view_submodule_parent),
} }
} }
} }

View file

@ -3,7 +3,7 @@ use std::{fs::File, io::Read, path::PathBuf};
use anyhow::Result; use anyhow::Result;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct KeySymbols { pub struct KeySymbols {
pub enter: String, pub enter: String,
pub left: String, pub left: String,

View file

@ -39,6 +39,7 @@ mod version;
use crate::{app::App, args::process_cmdline}; use crate::{app::App, args::process_cmdline};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use app::QuitState;
use asyncgit::{sync::RepoPath, AsyncGitNotification}; use asyncgit::{sync::RepoPath, AsyncGitNotification};
use backtrace::Backtrace; use backtrace::Backtrace;
use crossbeam_channel::{tick, unbounded, Receiver, Select}; use crossbeam_channel::{tick, unbounded, Receiver, Select};
@ -114,7 +115,7 @@ fn main() -> Result<()> {
let key_config = KeyConfig::init() let key_config = KeyConfig::init()
.map_err(|e| eprintln!("KeyConfig loading error: {}", e)) .map_err(|e| eprintln!("KeyConfig loading error: {}", e))
.unwrap_or_default(); .unwrap_or_default();
let theme = Theme::init(cliargs.theme) let theme = Theme::init(&cliargs.theme)
.map_err(|e| eprintln!("Theme loading error: {}", e)) .map_err(|e| eprintln!("Theme loading error: {}", e))
.unwrap_or_default(); .unwrap_or_default();
@ -126,21 +127,48 @@ fn main() -> Result<()> {
set_panic_handlers()?; set_panic_handlers()?;
let mut terminal = start_terminal(io::stdout())?; let mut terminal = start_terminal(io::stdout())?;
let mut repo_path = cliargs.repo_path;
let input = Input::new();
loop {
let quit_state = run_app(
repo_path.clone(),
theme,
key_config.clone(),
&input,
&mut terminal,
)?;
match quit_state {
QuitState::OpenSubmodule(p) => {
repo_path = p;
}
_ => break,
}
}
Ok(())
}
fn run_app(
repo: RepoPath,
theme: Theme,
key_config: KeyConfig,
input: &Input,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
) -> Result<QuitState, anyhow::Error> {
let (tx_git, rx_git) = unbounded(); let (tx_git, rx_git) = unbounded();
let (tx_app, rx_app) = unbounded(); let (tx_app, rx_app) = unbounded();
let input = Input::new();
let rx_input = input.receiver(); let rx_input = input.receiver();
let ticker = tick(TICK_INTERVAL); let ticker = tick(TICK_INTERVAL);
let spinner_ticker = tick(SPINNER_INTERVAL); let spinner_ticker = tick(SPINNER_INTERVAL);
let mut app = App::new( let mut app = App::new(
RefCell::new(cliargs.repo_path), RefCell::new(repo),
&tx_git, &tx_git,
&tx_app, &tx_app,
input, input.clone(),
theme, theme,
key_config, key_config,
); );
@ -165,7 +193,7 @@ fn main() -> Result<()> {
{ {
if let QueueEvent::SpinnerUpdate = event { if let QueueEvent::SpinnerUpdate = event {
spinner.update(); spinner.update();
spinner.draw(&mut terminal)?; spinner.draw(terminal)?;
continue; continue;
} }
@ -194,10 +222,10 @@ fn main() -> Result<()> {
QueueEvent::SpinnerUpdate => unreachable!(), QueueEvent::SpinnerUpdate => unreachable!(),
} }
draw(&mut terminal, &app)?; draw(terminal, &app)?;
spinner.set_state(app.any_work_pending()); spinner.set_state(app.any_work_pending());
spinner.draw(&mut terminal)?; spinner.draw(terminal)?;
if app.is_quit() { if app.is_quit() {
break; break;
@ -205,7 +233,7 @@ fn main() -> Result<()> {
} }
} }
Ok(()) Ok(app.quit_state())
} }
fn setup_terminal() -> Result<()> { fn setup_terminal() -> Result<()> {

View file

@ -124,6 +124,8 @@ pub enum InternalEvent {
PopupStackPush(StackablePopupOpen), PopupStackPush(StackablePopupOpen),
/// ///
ViewSubmodules, ViewSubmodules,
///
OpenRepo { path: PathBuf },
} }
/// single threaded simple queue for components to communicate with each other /// single threaded simple queue for components to communicate with each other

View file

@ -716,6 +716,33 @@ pub mod commands {
) )
} }
pub fn open_submodule(
key_config: &SharedKeyConfig,
) -> CommandText {
CommandText::new(
format!(
"Open [{}]",
key_config.get_hint(key_config.keys.enter),
),
"open submodule",
CMD_GROUP_GENERAL,
)
}
pub fn open_submodule_parent(
key_config: &SharedKeyConfig,
) -> CommandText {
CommandText::new(
format!(
"Open Parent [{}]",
key_config
.get_hint(key_config.keys.view_submodule_parent),
),
"open submodule parent repo",
CMD_GROUP_GENERAL,
)
}
pub fn continue_rebase( pub fn continue_rebase(
key_config: &SharedKeyConfig, key_config: &SharedKeyConfig,
) -> CommandText { ) -> CommandText {

View file

@ -15,7 +15,7 @@ use tui::style::{Color, Modifier, Style};
pub type SharedTheme = Rc<Theme>; pub type SharedTheme = Rc<Theme>;
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug, Copy, Clone)]
pub struct Theme { pub struct Theme {
selected_tab: Color, selected_tab: Color,
#[serde(with = "Color")] #[serde(with = "Color")]
@ -279,7 +279,7 @@ impl Theme {
} }
// This will only be called when theme.ron doesn't already exists // This will only be called when theme.ron doesn't already exists
fn save(&self, theme_file: PathBuf) -> Result<()> { fn save(&self, theme_file: &PathBuf) -> Result<()> {
let mut file = File::create(theme_file)?; let mut file = File::create(theme_file)?;
let data = to_string_pretty(self, PrettyConfig::default())?; let data = to_string_pretty(self, PrettyConfig::default())?;
file.write_all(data.as_bytes())?; file.write_all(data.as_bytes())?;
@ -293,7 +293,7 @@ impl Theme {
Ok(from_bytes(&buffer)?) Ok(from_bytes(&buffer)?)
} }
pub fn init(file: PathBuf) -> Result<Self> { pub fn init(file: &PathBuf) -> Result<Self> {
if file.exists() { if file.exists() {
match Self::read_file(file.clone()) { match Self::read_file(file.clone()) {
Err(e) => { Err(e) => {