mirror of
https://github.com/extrawurst/gitui.git
synced 2024-11-22 11:03:25 +03:00
support opening submodule (#1298)
This commit is contained in:
parent
aa9ed3349f
commit
986d34a5ac
20
Cargo.lock
generated
20
Cargo.lock
generated
@ -67,6 +67,7 @@ version = "0.21.0"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"easy-cast",
|
||||
"env_logger",
|
||||
"git2",
|
||||
"invalidstring",
|
||||
"log",
|
||||
@ -422,6 +423,19 @@ version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "fancy-regex"
|
||||
version = "0.7.1"
|
||||
@ -692,6 +706,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.46"
|
||||
|
@ -51,6 +51,7 @@
|
||||
- Browse commit log, diff committed changes
|
||||
- Scalable terminal UI layout
|
||||
- Async git API for fluid control
|
||||
- Submodule support
|
||||
|
||||
## 2. <a name="motivation"></a> Motivation <small><sup>[Top ▲](#table-of-contents)</sup></small>
|
||||
|
||||
|
@ -28,6 +28,7 @@ unicode-truncate = "0.2.0"
|
||||
url = "2.2"
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.9"
|
||||
invalidstring = { path = "../invalidstring", version = "0.1" }
|
||||
pretty_assertions = "1.3"
|
||||
serial_test = "0.9"
|
||||
|
@ -1,6 +1,9 @@
|
||||
#![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;
|
||||
|
||||
///
|
||||
@ -50,6 +53,10 @@ pub enum Error {
|
||||
#[error("git error:{0}")]
|
||||
Git(#[from] git2::Error),
|
||||
|
||||
///
|
||||
#[error("strip prefix error: {0}")]
|
||||
StripPrefix(#[from] StripPrefixError),
|
||||
|
||||
///
|
||||
#[error("utf8 error:{0}")]
|
||||
Utf8Conversion(#[from] FromUtf8Error),
|
||||
|
@ -82,7 +82,8 @@ pub use stash::{
|
||||
pub use state::{repo_state, RepoState};
|
||||
pub use status::is_workdir_clean;
|
||||
pub use submodules::{
|
||||
get_submodules, update_submodule, SubmoduleInfo, SubmoduleStatus,
|
||||
get_submodules, submodule_parent_info, update_submodule,
|
||||
SubmoduleInfo, SubmoduleParentInfo, SubmoduleStatus,
|
||||
};
|
||||
pub use tags::{
|
||||
delete_tag, get_tags, get_tags_with_metadata, CommitTags, Tag,
|
||||
@ -209,6 +210,8 @@ mod tests {
|
||||
|
||||
///
|
||||
pub fn repo_init_empty() -> Result<(TempDir, Repository)> {
|
||||
init_log();
|
||||
|
||||
sandbox_config_files();
|
||||
|
||||
let td = TempDir::new()?;
|
||||
@ -223,6 +226,8 @@ mod tests {
|
||||
|
||||
///
|
||||
pub fn repo_init() -> Result<(TempDir, Repository)> {
|
||||
init_log();
|
||||
|
||||
sandbox_config_files();
|
||||
|
||||
let td = TempDir::new()?;
|
||||
@ -266,8 +271,18 @@ mod tests {
|
||||
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)
|
||||
pub fn repo_init_bare() -> Result<(TempDir, Repository)> {
|
||||
init_log();
|
||||
|
||||
let tmp_repo_dir = TempDir::new()?;
|
||||
let bare_repo = Repository::init_bare(tmp_repo_dir.path())?;
|
||||
Ok((tmp_repo_dir, bare_repo))
|
||||
|
@ -11,7 +11,7 @@ use crate::error::Result;
|
||||
pub type RepoPathRef = RefCell<RepoPath>;
|
||||
|
||||
///
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum RepoPath {
|
||||
///
|
||||
Path(PathBuf),
|
||||
|
@ -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 super::{repo, CommitId, RepoPath};
|
||||
use crate::{error::Result, Error};
|
||||
use crate::{error::Result, sync::utils::work_dir, Error};
|
||||
|
||||
pub use git2::SubmoduleStatus;
|
||||
|
||||
///
|
||||
#[derive(Debug)]
|
||||
pub struct SubmoduleInfo {
|
||||
///
|
||||
pub name: String,
|
||||
///
|
||||
pub path: PathBuf,
|
||||
///
|
||||
@ -22,6 +31,17 @@ pub struct SubmoduleInfo {
|
||||
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 {
|
||||
///
|
||||
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(
|
||||
repo_path: &RepoPath,
|
||||
@ -46,22 +84,7 @@ pub fn get_submodules(
|
||||
let res = r
|
||||
.submodules()?
|
||||
.iter()
|
||||
.map(|s| {
|
||||
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,
|
||||
}
|
||||
})
|
||||
.map(|s| submodule_to_info(s, &repo2))
|
||||
.collect();
|
||||
|
||||
Ok(res)
|
||||
@ -82,3 +105,97 @@ pub fn update_submodule(
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
37
src/app.rs
37
src/app.rs
@ -28,7 +28,7 @@ use crate::{
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
use asyncgit::{
|
||||
sync::{self, RepoPathRef},
|
||||
sync::{self, utils::repo_work_dir, RepoPath, RepoPathRef},
|
||||
AsyncGitNotification, PushType,
|
||||
};
|
||||
use crossbeam_channel::Sender;
|
||||
@ -46,10 +46,17 @@ use tui::{
|
||||
Frame,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum QuitState {
|
||||
None,
|
||||
Close,
|
||||
OpenSubmodule(RepoPath),
|
||||
}
|
||||
|
||||
/// the main app type
|
||||
pub struct App {
|
||||
repo: RepoPathRef,
|
||||
do_quit: bool,
|
||||
do_quit: QuitState,
|
||||
help: HelpComponent,
|
||||
msg: MsgComponent,
|
||||
reset: ConfirmComponent,
|
||||
@ -103,6 +110,8 @@ impl App {
|
||||
theme: Theme,
|
||||
key_config: KeyConfig,
|
||||
) -> Self {
|
||||
log::trace!("open repo at: {:?}", repo);
|
||||
|
||||
let queue = Queue::new();
|
||||
let theme = Rc::new(theme);
|
||||
let key_config = Rc::new(key_config);
|
||||
@ -235,6 +244,7 @@ impl App {
|
||||
),
|
||||
submodule_popup: SubmodulesListComponent::new(
|
||||
repo.clone(),
|
||||
&queue,
|
||||
theme.clone(),
|
||||
key_config.clone(),
|
||||
),
|
||||
@ -243,7 +253,7 @@ impl App {
|
||||
theme.clone(),
|
||||
key_config.clone(),
|
||||
),
|
||||
do_quit: false,
|
||||
do_quit: QuitState::None,
|
||||
cmdbar: RefCell::new(CommandBar::new(
|
||||
theme.clone(),
|
||||
key_config.clone(),
|
||||
@ -493,7 +503,13 @@ impl App {
|
||||
|
||||
///
|
||||
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 key_match(e, self.key_config.keys.quit) {
|
||||
self.do_quit = true;
|
||||
self.do_quit = QuitState::Close;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -607,7 +623,7 @@ impl App {
|
||||
fn check_hard_exit(&mut self, ev: &Event) -> bool {
|
||||
if let Event::Key(e) = ev {
|
||||
if key_match(e, self.key_config.keys.exit) {
|
||||
self.do_quit = true;
|
||||
self.do_quit = QuitState::Close;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -878,6 +894,15 @@ impl App {
|
||||
flags
|
||||
.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)
|
||||
|
@ -5,11 +5,15 @@ use super::{
|
||||
};
|
||||
use crate::{
|
||||
keys::{key_match, SharedKeyConfig},
|
||||
queue::{InternalEvent, Queue},
|
||||
strings,
|
||||
ui::{self, Size},
|
||||
};
|
||||
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 std::{cell::Cell, convert::TryInto};
|
||||
use tui::{
|
||||
@ -18,7 +22,7 @@ use tui::{
|
||||
Alignment, Constraint, Direction, Layout, Margin, Rect,
|
||||
},
|
||||
text::{Span, Spans, Text},
|
||||
widgets::{Block, BorderType, Borders, Clear, Paragraph},
|
||||
widgets::{Block, Borders, Clear, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
use ui::style::SharedTheme;
|
||||
@ -27,7 +31,10 @@ use unicode_truncate::UnicodeTruncateStr;
|
||||
///
|
||||
pub struct SubmodulesListComponent {
|
||||
repo: RepoPathRef,
|
||||
repo_path: String,
|
||||
queue: Queue,
|
||||
submodules: Vec<SubmoduleInfo>,
|
||||
submodule_parent: Option<SubmoduleParentInfo>,
|
||||
visible: bool,
|
||||
current_height: Cell<u16>,
|
||||
selection: u16,
|
||||
@ -59,7 +66,6 @@ impl DrawableComponent for SubmodulesListComponent {
|
||||
f.render_widget(
|
||||
Block::default()
|
||||
.title(strings::POPUP_TITLE_SUBMODULES)
|
||||
.border_type(BorderType::Thick)
|
||||
.borders(Borders::ALL),
|
||||
area,
|
||||
);
|
||||
@ -69,16 +75,25 @@ impl DrawableComponent for SubmodulesListComponent {
|
||||
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()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(
|
||||
[Constraint::Min(40), Constraint::Length(40)]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(area);
|
||||
.split(chunks_vertical[0]);
|
||||
|
||||
self.draw_list(f, chunks[0])?;
|
||||
self.draw_info(f, chunks[1]);
|
||||
self.draw_local_info(f, chunks_vertical[1]);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -107,6 +122,20 @@ impl Component for SubmodulesListComponent {
|
||||
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)
|
||||
}
|
||||
@ -143,6 +172,21 @@ impl Component for SubmodulesListComponent {
|
||||
return self
|
||||
.move_selection(ScrollType::End)
|
||||
.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(
|
||||
e,
|
||||
self.key_config.keys.cmd_bar_toggle,
|
||||
@ -173,18 +217,22 @@ impl Component for SubmodulesListComponent {
|
||||
impl SubmodulesListComponent {
|
||||
pub fn new(
|
||||
repo: RepoPathRef,
|
||||
queue: &Queue,
|
||||
theme: SharedTheme,
|
||||
key_config: SharedKeyConfig,
|
||||
) -> Self {
|
||||
Self {
|
||||
submodules: Vec::new(),
|
||||
submodule_parent: None,
|
||||
scroll: VerticalScroll::new(),
|
||||
queue: queue.clone(),
|
||||
selection: 0,
|
||||
visible: false,
|
||||
theme,
|
||||
key_config,
|
||||
current_height: Cell::new(0),
|
||||
repo,
|
||||
repo_path: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -201,6 +249,13 @@ impl SubmodulesListComponent {
|
||||
if self.is_visible() {
|
||||
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)?;
|
||||
}
|
||||
Ok(())
|
||||
@ -210,6 +265,10 @@ impl SubmodulesListComponent {
|
||||
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
|
||||
fn move_selection(&mut self, scroll: ScrollType) -> Result<bool> {
|
||||
let new_selection = match scroll {
|
||||
@ -234,11 +293,11 @@ impl SubmodulesListComponent {
|
||||
}
|
||||
|
||||
fn set_selection(&mut self, selection: u16) -> Result<()> {
|
||||
let num_branches: u16 = self.submodules.len().try_into()?;
|
||||
let num_branches = num_branches.saturating_sub(1);
|
||||
let num_entriess: u16 = self.submodules.len().try_into()?;
|
||||
let num_entriess = num_entriess.saturating_sub(1);
|
||||
|
||||
let selection = if selection > num_branches {
|
||||
num_branches
|
||||
let selection = if selection > num_entriess {
|
||||
num_entriess
|
||||
} else {
|
||||
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>(
|
||||
&self,
|
||||
f: &mut Frame<B>,
|
||||
@ -399,4 +484,13 @@ impl SubmodulesListComponent {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ pub enum InputEvent {
|
||||
}
|
||||
|
||||
///
|
||||
#[derive(Clone)]
|
||||
pub struct Input {
|
||||
desired_state: Arc<NotifyableMutex<bool>>,
|
||||
current_state: Arc<AtomicBool>,
|
||||
|
@ -11,7 +11,7 @@ use super::{
|
||||
|
||||
pub type SharedKeyConfig = Rc<KeyConfig>;
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Clone)]
|
||||
pub struct KeyConfig {
|
||||
pub keys: KeysList,
|
||||
symbols: KeySymbols,
|
||||
|
@ -34,6 +34,7 @@ impl From<&GituiKeyEvent> for KeyEvent {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct KeysList {
|
||||
pub tab_status: GituiKeyEvent,
|
||||
pub tab_log: GituiKeyEvent,
|
||||
@ -108,6 +109,7 @@ pub struct KeysList {
|
||||
pub stage_unstage_item: GituiKeyEvent,
|
||||
pub tag_annotate: GituiKeyEvent,
|
||||
pub view_submodules: GituiKeyEvent,
|
||||
pub view_submodule_parent: GituiKeyEvent,
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
@ -187,7 +189,7 @@ impl Default for KeysList {
|
||||
stage_unstage_item: GituiKeyEvent::new(KeyCode::Enter, KeyModifiers::empty()),
|
||||
tag_annotate: GituiKeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL),
|
||||
view_submodules: GituiKeyEvent::new(KeyCode::Char('S'), KeyModifiers::SHIFT),
|
||||
|
||||
view_submodule_parent: GituiKeyEvent::new(KeyCode::Char('p'), KeyModifiers::empty()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -168,6 +168,7 @@ impl KeysListFile {
|
||||
stage_unstage_item: self.stage_unstage_item.unwrap_or(default.stage_unstage_item),
|
||||
tag_annotate: self.tag_annotate.unwrap_or(default.tag_annotate),
|
||||
view_submodules: self.view_submodules.unwrap_or(default.view_submodules),
|
||||
view_submodule_parent: self.view_submodules.unwrap_or(default.view_submodule_parent),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ use std::{fs::File, io::Read, path::PathBuf};
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KeySymbols {
|
||||
pub enter: String,
|
||||
pub left: String,
|
||||
|
46
src/main.rs
46
src/main.rs
@ -39,6 +39,7 @@ mod version;
|
||||
|
||||
use crate::{app::App, args::process_cmdline};
|
||||
use anyhow::{bail, Result};
|
||||
use app::QuitState;
|
||||
use asyncgit::{sync::RepoPath, AsyncGitNotification};
|
||||
use backtrace::Backtrace;
|
||||
use crossbeam_channel::{tick, unbounded, Receiver, Select};
|
||||
@ -114,7 +115,7 @@ fn main() -> Result<()> {
|
||||
let key_config = KeyConfig::init()
|
||||
.map_err(|e| eprintln!("KeyConfig loading error: {}", e))
|
||||
.unwrap_or_default();
|
||||
let theme = Theme::init(cliargs.theme)
|
||||
let theme = Theme::init(&cliargs.theme)
|
||||
.map_err(|e| eprintln!("Theme loading error: {}", e))
|
||||
.unwrap_or_default();
|
||||
|
||||
@ -126,21 +127,48 @@ fn main() -> Result<()> {
|
||||
set_panic_handlers()?;
|
||||
|
||||
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_app, rx_app) = unbounded();
|
||||
|
||||
let input = Input::new();
|
||||
|
||||
let rx_input = input.receiver();
|
||||
let ticker = tick(TICK_INTERVAL);
|
||||
let spinner_ticker = tick(SPINNER_INTERVAL);
|
||||
|
||||
let mut app = App::new(
|
||||
RefCell::new(cliargs.repo_path),
|
||||
RefCell::new(repo),
|
||||
&tx_git,
|
||||
&tx_app,
|
||||
input,
|
||||
input.clone(),
|
||||
theme,
|
||||
key_config,
|
||||
);
|
||||
@ -165,7 +193,7 @@ fn main() -> Result<()> {
|
||||
{
|
||||
if let QueueEvent::SpinnerUpdate = event {
|
||||
spinner.update();
|
||||
spinner.draw(&mut terminal)?;
|
||||
spinner.draw(terminal)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -194,10 +222,10 @@ fn main() -> Result<()> {
|
||||
QueueEvent::SpinnerUpdate => unreachable!(),
|
||||
}
|
||||
|
||||
draw(&mut terminal, &app)?;
|
||||
draw(terminal, &app)?;
|
||||
|
||||
spinner.set_state(app.any_work_pending());
|
||||
spinner.draw(&mut terminal)?;
|
||||
spinner.draw(terminal)?;
|
||||
|
||||
if app.is_quit() {
|
||||
break;
|
||||
@ -205,7 +233,7 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(app.quit_state())
|
||||
}
|
||||
|
||||
fn setup_terminal() -> Result<()> {
|
||||
|
@ -124,6 +124,8 @@ pub enum InternalEvent {
|
||||
PopupStackPush(StackablePopupOpen),
|
||||
///
|
||||
ViewSubmodules,
|
||||
///
|
||||
OpenRepo { path: PathBuf },
|
||||
}
|
||||
|
||||
/// single threaded simple queue for components to communicate with each other
|
||||
|
@ -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(
|
||||
key_config: &SharedKeyConfig,
|
||||
) -> CommandText {
|
||||
|
@ -15,7 +15,7 @@ use tui::style::{Color, Modifier, Style};
|
||||
|
||||
pub type SharedTheme = Rc<Theme>;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug, Copy, Clone)]
|
||||
pub struct Theme {
|
||||
selected_tab: Color,
|
||||
#[serde(with = "Color")]
|
||||
@ -279,7 +279,7 @@ impl Theme {
|
||||
}
|
||||
|
||||
// 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 data = to_string_pretty(self, PrettyConfig::default())?;
|
||||
file.write_all(data.as_bytes())?;
|
||||
@ -293,7 +293,7 @@ impl Theme {
|
||||
Ok(from_bytes(&buffer)?)
|
||||
}
|
||||
|
||||
pub fn init(file: PathBuf) -> Result<Self> {
|
||||
pub fn init(file: &PathBuf) -> Result<Self> {
|
||||
if file.exists() {
|
||||
match Self::read_file(file.clone()) {
|
||||
Err(e) => {
|
||||
|
Loading…
Reference in New Issue
Block a user