PoC list submodules (#1090)

This commit is contained in:
extrawurst 2022-08-27 17:55:06 +02:00 committed by GitHub
parent bcb565788e
commit ef3ece552d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 554 additions and 3 deletions

View File

@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
**submodules view**
![submodules](assets/submodules.png)
### Added
* submodules support ([#1087](https://github.com/extrawurst/gitui/issues/1087))
## [0.21.0] - 2021-08-17
**popup stacking**

View File

@ -2,7 +2,7 @@
.PHONY: debug build-release release-linux-musl test clippy clippy-pedantic install install-debug
ARGS=-l
# ARGS=-l -d ~/code/git-bare-test.git
# ARGS=-l -d ~/code/extern/pbrt-v4
# ARGS=-l -d ~/code/git-bare-test.git -w ~/code/git-bare-test
profile:

BIN
assets/submodules.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

View File

@ -10,6 +10,12 @@ use unicode_truncate::UnicodeTruncateStr;
)]
pub struct CommitId(Oid);
impl Default for CommitId {
fn default() -> Self {
Self(Oid::zero())
}
}
impl CommitId {
/// create new `CommitId`
pub const fn new(id: Oid) -> Self {

View File

@ -27,6 +27,7 @@ mod staging;
mod stash;
mod state;
pub mod status;
mod submodules;
mod tags;
mod tree;
pub mod utils;
@ -80,6 +81,9 @@ pub use stash::{
};
pub use state::{repo_state, RepoState};
pub use status::is_workdir_clean;
pub use submodules::{
get_submodules, update_submodule, SubmoduleInfo, SubmoduleStatus,
};
pub use tags::{
delete_tag, get_tags, get_tags_with_metadata, CommitTags, Tag,
TagWithMetadata, Tags,

View File

@ -0,0 +1,84 @@
use std::path::PathBuf;
use git2::SubmoduleUpdateOptions;
use scopetime::scope_time;
use super::{repo, CommitId, RepoPath};
use crate::{error::Result, Error};
pub use git2::SubmoduleStatus;
///
pub struct SubmoduleInfo {
///
pub path: PathBuf,
///
pub url: Option<String>,
///
pub id: Option<CommitId>,
///
pub head_id: Option<CommitId>,
///
pub status: SubmoduleStatus,
}
impl SubmoduleInfo {
///
pub fn get_repo_path(
&self,
repo_path: &RepoPath,
) -> Result<RepoPath> {
let repo = repo(repo_path)?;
let wd = repo.workdir().ok_or(Error::NoWorkDir)?;
Ok(RepoPath::Path(wd.join(self.path.clone())))
}
}
///
pub fn get_submodules(
repo_path: &RepoPath,
) -> Result<Vec<SubmoduleInfo>> {
scope_time!("get_submodules");
let (r, repo2) = (repo(repo_path)?, repo(repo_path)?);
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,
}
})
.collect();
Ok(res)
}
///
pub fn update_submodule(
repo_path: &RepoPath,
path: &str,
) -> Result<()> {
let repo = repo(repo_path)?;
let mut submodule = repo.find_submodule(path)?;
let mut options = SubmoduleUpdateOptions::new();
submodule.update(true, Some(&mut options))?;
Ok(())
}

View File

@ -11,7 +11,8 @@ use crate::{
MsgComponent, OptionsPopupComponent, PullComponent,
PushComponent, PushTagsComponent, RenameBranchComponent,
RevisionFilesPopup, SharedOptions, StashMsgComponent,
TagCommitComponent, TagListComponent,
SubmodulesListComponent, TagCommitComponent,
TagListComponent,
},
input::{Input, InputEvent, InputState},
keys::{key_match, KeyConfig, SharedKeyConfig},
@ -70,6 +71,7 @@ pub struct App {
rename_branch_popup: RenameBranchComponent,
select_branch_popup: BranchListComponent,
options_popup: OptionsPopupComponent,
submodule_popup: SubmodulesListComponent,
tags_popup: TagListComponent,
cmdbar: RefCell<CommandBar>,
tab: usize,
@ -231,6 +233,11 @@ impl App {
key_config.clone(),
options.clone(),
),
submodule_popup: SubmodulesListComponent::new(
repo.clone(),
theme.clone(),
key_config.clone(),
),
find_file_popup: FileFindPopup::new(
&queue,
theme.clone(),
@ -543,6 +550,7 @@ impl App {
rename_branch_popup,
select_branch_popup,
revision_files_popup,
submodule_popup,
tags_popup,
options_popup,
help,
@ -567,6 +575,7 @@ impl App {
external_editor_popup,
tag_commit_popup,
select_branch_popup,
submodule_popup,
tags_popup,
create_branch_popup,
rename_branch_popup,
@ -775,6 +784,9 @@ impl App {
InternalEvent::SelectBranch => {
self.select_branch_popup.open()?;
}
InternalEvent::ViewSubmodules => {
self.submodule_popup.open()?;
}
InternalEvent::Tags => {
self.tags_popup.open()?;
}

View File

@ -541,9 +541,9 @@ impl BranchListComponent {
const HEAD_SYMBOL: char = '*';
const EMPTY_SYMBOL: char = ' ';
const THREE_DOTS: &str = "...";
const THREE_DOTS_LENGTH: usize = THREE_DOTS.len(); // "..."
const COMMIT_HASH_LENGTH: usize = 8;
const IS_HEAD_STAR_LENGTH: usize = 3; // "* "
const THREE_DOTS_LENGTH: usize = THREE_DOTS.len(); // "..."
let branch_name_length: usize =
width_available as usize * 40 / 100;

View File

@ -26,6 +26,7 @@ mod revision_files;
mod revision_files_popup;
mod stashmsg;
mod status_tree;
mod submodules;
mod syntax_text;
mod tag_commit;
mod taglist;
@ -61,6 +62,7 @@ pub use reset::ConfirmComponent;
pub use revision_files::RevisionFilesComponent;
pub use revision_files_popup::{FileTreeOpen, RevisionFilesPopup};
pub use stashmsg::StashMsgComponent;
pub use submodules::SubmodulesListComponent;
pub use syntax_text::SyntaxTextComponent;
pub use tag_commit::TagCommitComponent;
pub use taglist::TagListComponent;

View File

@ -0,0 +1,402 @@
use super::{
utils::scroll_vertical::VerticalScroll, visibility_blocking,
CommandBlocking, CommandInfo, Component, DrawableComponent,
EventState, ScrollType,
};
use crate::{
keys::{key_match, SharedKeyConfig},
strings,
ui::{self, Size},
};
use anyhow::Result;
use asyncgit::sync::{get_submodules, RepoPathRef, SubmoduleInfo};
use crossterm::event::Event;
use std::{cell::Cell, convert::TryInto};
use tui::{
backend::Backend,
layout::{
Alignment, Constraint, Direction, Layout, Margin, Rect,
},
text::{Span, Spans, Text},
widgets::{Block, BorderType, Borders, Clear, Paragraph},
Frame,
};
use ui::style::SharedTheme;
use unicode_truncate::UnicodeTruncateStr;
///
pub struct SubmodulesListComponent {
repo: RepoPathRef,
submodules: Vec<SubmoduleInfo>,
visible: bool,
current_height: Cell<u16>,
selection: u16,
scroll: VerticalScroll,
theme: SharedTheme,
key_config: SharedKeyConfig,
}
impl DrawableComponent for SubmodulesListComponent {
fn draw<B: Backend>(
&self,
f: &mut Frame<B>,
rect: Rect,
) -> Result<()> {
if self.is_visible() {
const PERCENT_SIZE: Size = Size::new(80, 80);
const MIN_SIZE: Size = Size::new(60, 30);
let area = ui::centered_rect(
PERCENT_SIZE.width,
PERCENT_SIZE.height,
rect,
);
let area = ui::rect_inside(MIN_SIZE, rect.into(), area);
let area = area.intersection(rect);
f.render_widget(Clear, area);
f.render_widget(
Block::default()
.title(strings::POPUP_TITLE_SUBMODULES)
.border_type(BorderType::Thick)
.borders(Borders::ALL),
area,
);
let area = area.inner(&Margin {
vertical: 1,
horizontal: 1,
});
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(
[Constraint::Min(40), Constraint::Length(40)]
.as_ref(),
)
.split(area);
self.draw_list(f, chunks[0])?;
self.draw_info(f, chunks[1]);
}
Ok(())
}
}
impl Component for SubmodulesListComponent {
fn commands(
&self,
out: &mut Vec<CommandInfo>,
force_all: bool,
) -> CommandBlocking {
if self.visible || force_all {
if !force_all {
out.clear();
}
out.push(CommandInfo::new(
strings::commands::scroll(&self.key_config),
true,
true,
));
out.push(CommandInfo::new(
strings::commands::close_popup(&self.key_config),
true,
true,
));
}
visibility_blocking(self)
}
fn event(&mut self, ev: &Event) -> Result<EventState> {
if !self.visible {
return Ok(EventState::NotConsumed);
}
if let Event::Key(e) = ev {
if key_match(e, self.key_config.keys.exit_popup) {
self.hide();
} else if key_match(e, self.key_config.keys.move_down) {
return self
.move_selection(ScrollType::Up)
.map(Into::into);
} else if key_match(e, self.key_config.keys.move_up) {
return self
.move_selection(ScrollType::Down)
.map(Into::into);
} else if key_match(e, self.key_config.keys.page_down) {
return self
.move_selection(ScrollType::PageDown)
.map(Into::into);
} else if key_match(e, self.key_config.keys.page_up) {
return self
.move_selection(ScrollType::PageUp)
.map(Into::into);
} else if key_match(e, self.key_config.keys.home) {
return self
.move_selection(ScrollType::Home)
.map(Into::into);
} else if key_match(e, self.key_config.keys.end) {
return self
.move_selection(ScrollType::End)
.map(Into::into);
} else if key_match(
e,
self.key_config.keys.cmd_bar_toggle,
) {
//do not consume if its the more key
return Ok(EventState::NotConsumed);
}
}
Ok(EventState::Consumed)
}
fn is_visible(&self) -> bool {
self.visible
}
fn hide(&mut self) {
self.visible = false;
}
fn show(&mut self) -> Result<()> {
self.visible = true;
Ok(())
}
}
impl SubmodulesListComponent {
pub fn new(
repo: RepoPathRef,
theme: SharedTheme,
key_config: SharedKeyConfig,
) -> Self {
Self {
submodules: Vec::new(),
scroll: VerticalScroll::new(),
selection: 0,
visible: false,
theme,
key_config,
current_height: Cell::new(0),
repo,
}
}
///
pub fn open(&mut self) -> Result<()> {
self.show()?;
self.update_submodules()?;
Ok(())
}
///
pub fn update_submodules(&mut self) -> Result<()> {
if self.is_visible() {
self.submodules = get_submodules(&self.repo.borrow())?;
self.set_selection(self.selection)?;
}
Ok(())
}
fn selected_entry(&self) -> Option<&SubmoduleInfo> {
self.submodules.get(self.selection as usize)
}
//TODO: dedup this almost identical with BranchListComponent
fn move_selection(&mut self, scroll: ScrollType) -> Result<bool> {
let new_selection = match scroll {
ScrollType::Up => self.selection.saturating_add(1),
ScrollType::Down => self.selection.saturating_sub(1),
ScrollType::PageDown => self
.selection
.saturating_add(self.current_height.get()),
ScrollType::PageUp => self
.selection
.saturating_sub(self.current_height.get()),
ScrollType::Home => 0,
ScrollType::End => {
let count: u16 = self.submodules.len().try_into()?;
count.saturating_sub(1)
}
};
self.set_selection(new_selection)?;
Ok(true)
}
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 selection = if selection > num_branches {
num_branches
} else {
selection
};
self.selection = selection;
Ok(())
}
fn get_text(
&self,
theme: &SharedTheme,
width_available: u16,
height: usize,
) -> Text {
const THREE_DOTS: &str = "...";
const THREE_DOTS_LENGTH: usize = THREE_DOTS.len(); // "..."
const COMMIT_HASH_LENGTH: usize = 8;
let mut txt = Vec::with_capacity(3);
let name_length: usize = (width_available as usize)
.saturating_sub(COMMIT_HASH_LENGTH)
.saturating_sub(THREE_DOTS_LENGTH);
for (i, submodule) in self
.submodules
.iter()
.skip(self.scroll.get_top())
.take(height)
.enumerate()
{
let mut module_path = submodule
.path
.as_os_str()
.to_string_lossy()
.to_string();
if module_path.len() > name_length {
module_path.unicode_truncate(
name_length.saturating_sub(THREE_DOTS_LENGTH),
);
module_path += THREE_DOTS;
}
let selected = (self.selection as usize
- self.scroll.get_top())
== i;
let span_hash = Span::styled(
format!(
"{} ",
submodule
.head_id
.unwrap_or_default()
.get_short_string()
),
theme.commit_hash(selected),
);
let span_name = Span::styled(
format!("{:w$} ", module_path, w = name_length),
theme.text(true, selected),
);
txt.push(Spans::from(vec![span_name, span_hash]));
}
Text::from(txt)
}
fn get_info_text(&self, theme: &SharedTheme) -> Text {
self.selected_entry().map_or_else(
Text::default,
|submodule| {
let span_title_path =
Span::styled("Path:", theme.text(false, false));
let span_path = Span::styled(
submodule.path.to_string_lossy(),
theme.text(true, false),
);
let span_title_commit =
Span::styled("Commit:", theme.text(false, false));
let span_commit = Span::styled(
submodule.id.unwrap_or_default().to_string(),
theme.commit_hash(false),
);
let span_title_url =
Span::styled("Url:", theme.text(false, false));
let span_url = Span::styled(
submodule.url.clone().unwrap_or_default(),
theme.text(true, false),
);
let span_title_status =
Span::styled("Status:", theme.text(false, false));
let span_status = Span::styled(
format!("{:?}", submodule.status),
theme.text(true, false),
);
Text::from(vec![
Spans::from(vec![span_title_path]),
Spans::from(vec![span_path]),
Spans::from(vec![]),
Spans::from(vec![span_title_commit]),
Spans::from(vec![span_commit]),
Spans::from(vec![]),
Spans::from(vec![span_title_url]),
Spans::from(vec![span_url]),
Spans::from(vec![]),
Spans::from(vec![span_title_status]),
Spans::from(vec![span_status]),
])
},
)
}
fn draw_list<B: Backend>(
&self,
f: &mut Frame<B>,
r: Rect,
) -> Result<()> {
let height_in_lines = r.height as usize;
self.current_height.set(height_in_lines.try_into()?);
self.scroll.update(
self.selection as usize,
self.submodules.len(),
height_in_lines,
);
f.render_widget(
Paragraph::new(self.get_text(
&self.theme,
r.width,
height_in_lines,
))
.alignment(Alignment::Left),
r,
);
let mut r = r;
r.height += 2;
r.y = r.y.saturating_sub(1);
self.scroll.draw(f, r, &self.theme);
Ok(())
}
fn draw_info<B: Backend>(&self, f: &mut Frame<B>, r: Rect) {
f.render_widget(
Paragraph::new(self.get_info_text(&self.theme))
.alignment(Alignment::Left),
r,
);
}
}

View File

@ -107,6 +107,7 @@ pub struct KeysList {
pub undo_commit: GituiKeyEvent,
pub stage_unstage_item: GituiKeyEvent,
pub tag_annotate: GituiKeyEvent,
pub view_submodules: GituiKeyEvent,
}
#[rustfmt::skip]
@ -185,6 +186,8 @@ impl Default for KeysList {
file_find: GituiKeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()),
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 File

@ -79,6 +79,7 @@ pub struct KeysListFile {
pub undo_commit: Option<GituiKeyEvent>,
pub stage_unstage_item: Option<GituiKeyEvent>,
pub tag_annotate: Option<GituiKeyEvent>,
pub view_submodules: Option<GituiKeyEvent>,
}
impl KeysListFile {
@ -166,6 +167,7 @@ impl KeysListFile {
undo_commit: self.undo_commit.unwrap_or(default.undo_commit),
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 File

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

View File

@ -23,6 +23,8 @@ pub static PUSH_TAGS_STATES_FETCHING: &str = "fetching";
pub static PUSH_TAGS_STATES_PUSHING: &str = "pushing";
pub static PUSH_TAGS_STATES_DONE: &str = "done";
pub static POPUP_TITLE_SUBMODULES: &str = "Submodules";
pub mod symbol {
pub const WHITESPACE: &str = "\u{00B7}"; //·
pub const CHECKMARK: &str = "\u{2713}"; //✓
@ -700,6 +702,19 @@ pub mod commands {
)
}
pub fn view_submodules(
key_config: &SharedKeyConfig,
) -> CommandText {
CommandText::new(
format!(
"Submodules [{}]",
key_config.get_hint(key_config.keys.view_submodules),
),
"open submodule view",
CMD_GROUP_GENERAL,
)
}
pub fn continue_rebase(
key_config: &SharedKeyConfig,
) -> CommandText {

View File

@ -781,6 +781,12 @@ impl Component for Status {
true,
self.pending_revert() || force_all,
));
out.push(CommandInfo::new(
strings::commands::view_submodules(&self.key_config),
true,
true,
));
}
{
@ -936,6 +942,12 @@ impl Component for Status {
NeedsUpdate::ALL,
));
Ok(EventState::Consumed)
} else if key_match(
k,
self.key_config.keys.view_submodules,
) {
self.queue.push(InternalEvent::ViewSubmodules);
Ok(EventState::Consumed)
} else {
Ok(EventState::NotConsumed)
};