mirror of
https://github.com/gitui-org/gitui
synced 2026-05-24 09:28:21 +00:00
parent
fa8070b1ba
commit
702415c40d
18 changed files with 706 additions and 192 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
use super::{utils::repo, CommitId};
|
use super::{utils::repo, CommitId};
|
||||||
use crate::{error::Result, StatusItem, StatusItemType};
|
use crate::{error::Result, StatusItem, StatusItemType};
|
||||||
use git2::DiffDelta;
|
use git2::{Diff, DiffDelta, DiffOptions, Repository};
|
||||||
use scopetime::scope_time;
|
use scopetime::scope_time;
|
||||||
|
|
||||||
/// get all files that are part of a commit
|
/// get all files that are part of a commit
|
||||||
|
|
@ -12,19 +12,7 @@ pub fn get_commit_files(
|
||||||
|
|
||||||
let repo = repo(repo_path)?;
|
let repo = repo(repo_path)?;
|
||||||
|
|
||||||
let commit = repo.find_commit(id.into())?;
|
let diff = get_commit_diff(&repo, id, None)?;
|
||||||
let commit_tree = commit.tree()?;
|
|
||||||
let parent = if commit.parent_count() > 0 {
|
|
||||||
Some(repo.find_commit(commit.parent_id(0)?)?.tree()?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let diff = repo.diff_tree_to_tree(
|
|
||||||
parent.as_ref(),
|
|
||||||
Some(&commit_tree),
|
|
||||||
None,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut res = Vec::new();
|
let mut res = Vec::new();
|
||||||
|
|
||||||
|
|
@ -48,6 +36,37 @@ pub fn get_commit_files(
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
pub(crate) fn get_commit_diff(
|
||||||
|
repo: &Repository,
|
||||||
|
id: CommitId,
|
||||||
|
pathspec: Option<String>,
|
||||||
|
) -> Result<Diff<'_>> {
|
||||||
|
// scope_time!("get_commit_diff");
|
||||||
|
|
||||||
|
let commit = repo.find_commit(id.into())?;
|
||||||
|
let commit_tree = commit.tree()?;
|
||||||
|
let parent = if commit.parent_count() > 0 {
|
||||||
|
Some(repo.find_commit(commit.parent_id(0)?)?.tree()?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut opt = pathspec.map(|p| {
|
||||||
|
let mut opts = DiffOptions::new();
|
||||||
|
opts.pathspec(p);
|
||||||
|
opts
|
||||||
|
});
|
||||||
|
|
||||||
|
let diff = repo.diff_tree_to_tree(
|
||||||
|
parent.as_ref(),
|
||||||
|
Some(&commit_tree),
|
||||||
|
opt.as_mut(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(diff)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::get_commit_files;
|
use super::get_commit_files;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//! sync git api for fetching a diff
|
//! sync git api for fetching a diff
|
||||||
|
|
||||||
use super::utils;
|
use super::{commit_files::get_commit_diff, utils, CommitId};
|
||||||
use crate::{error::Error, error::Result, hash};
|
use crate::{error::Error, error::Result, hash};
|
||||||
use git2::{
|
use git2::{
|
||||||
Delta, Diff, DiffDelta, DiffFormat, DiffHunk, DiffOptions, Patch,
|
Delta, Diff, DiffDelta, DiffFormat, DiffHunk, DiffOptions, Patch,
|
||||||
|
|
@ -81,6 +81,8 @@ pub(crate) fn get_diff_raw<'a>(
|
||||||
stage: bool,
|
stage: bool,
|
||||||
reverse: bool,
|
reverse: bool,
|
||||||
) -> Result<Diff<'a>> {
|
) -> Result<Diff<'a>> {
|
||||||
|
// scope_time!("get_diff_raw");
|
||||||
|
|
||||||
let mut opt = DiffOptions::new();
|
let mut opt = DiffOptions::new();
|
||||||
opt.pathspec(p);
|
opt.pathspec(p);
|
||||||
opt.reverse(reverse);
|
opt.reverse(reverse);
|
||||||
|
|
@ -119,7 +121,7 @@ pub(crate) fn get_diff_raw<'a>(
|
||||||
Ok(diff)
|
Ok(diff)
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
/// returns diff of a specific file either in `stage` or workdir
|
||||||
pub fn get_diff(
|
pub fn get_diff(
|
||||||
repo_path: &str,
|
repo_path: &str,
|
||||||
p: String,
|
p: String,
|
||||||
|
|
@ -131,6 +133,32 @@ pub fn get_diff(
|
||||||
let work_dir = work_dir(&repo);
|
let work_dir = work_dir(&repo);
|
||||||
let diff = get_diff_raw(&repo, &p, stage, false)?;
|
let diff = get_diff_raw(&repo, &p, stage, false)?;
|
||||||
|
|
||||||
|
raw_diff_to_file_diff(&diff, work_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns diff of a specific file inside a commit
|
||||||
|
/// see `get_commit_diff`
|
||||||
|
pub fn get_diff_commit(
|
||||||
|
repo_path: &str,
|
||||||
|
id: CommitId,
|
||||||
|
p: String,
|
||||||
|
) -> Result<FileDiff> {
|
||||||
|
scope_time!("get_diff_commit");
|
||||||
|
|
||||||
|
let repo = utils::repo(repo_path)?;
|
||||||
|
let work_dir = work_dir(&repo);
|
||||||
|
let diff = get_commit_diff(&repo, id, Some(p))?;
|
||||||
|
|
||||||
|
raw_diff_to_file_diff(&diff, work_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
fn raw_diff_to_file_diff<'a>(
|
||||||
|
diff: &'a Diff,
|
||||||
|
work_dir: &Path,
|
||||||
|
) -> Result<FileDiff> {
|
||||||
|
// scope_time!("raw_diff_to_file_diff");
|
||||||
|
|
||||||
let mut res: FileDiff = FileDiff::default();
|
let mut res: FileDiff = FileDiff::default();
|
||||||
let mut current_lines = Vec::new();
|
let mut current_lines = Vec::new();
|
||||||
let mut current_hunk: Option<HunkHeader> = None;
|
let mut current_hunk: Option<HunkHeader> = None;
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ pub mod utils;
|
||||||
pub use commit_details::{get_commit_details, CommitDetails};
|
pub use commit_details::{get_commit_details, CommitDetails};
|
||||||
pub use commit_files::get_commit_files;
|
pub use commit_files::get_commit_files;
|
||||||
pub use commits_info::{get_commits_info, CommitId, CommitInfo};
|
pub use commits_info::{get_commits_info, CommitId, CommitInfo};
|
||||||
|
pub use diff::get_diff_commit;
|
||||||
pub use hooks::{hooks_commit_msg, hooks_post_commit, HookResult};
|
pub use hooks::{hooks_commit_msg, hooks_post_commit, HookResult};
|
||||||
pub use hunks::{stage_hunk, unstage_hunk};
|
pub use hunks::{stage_hunk, unstage_hunk};
|
||||||
pub use ignore::add_to_ignore;
|
pub use ignore::add_to_ignore;
|
||||||
|
|
|
||||||
35
src/app.rs
35
src/app.rs
|
|
@ -3,8 +3,9 @@ use crate::{
|
||||||
cmdbar::CommandBar,
|
cmdbar::CommandBar,
|
||||||
components::{
|
components::{
|
||||||
event_pump, CommandBlocking, CommandInfo, CommitComponent,
|
event_pump, CommandBlocking, CommandInfo, CommitComponent,
|
||||||
Component, DrawableComponent, HelpComponent, MsgComponent,
|
Component, DrawableComponent, HelpComponent,
|
||||||
ResetComponent, StashMsgComponent,
|
InspectCommitComponent, MsgComponent, ResetComponent,
|
||||||
|
StashMsgComponent,
|
||||||
},
|
},
|
||||||
keys,
|
keys,
|
||||||
queue::{Action, InternalEvent, NeedsUpdate, Queue},
|
queue::{Action, InternalEvent, NeedsUpdate, Queue},
|
||||||
|
|
@ -33,6 +34,7 @@ pub struct App {
|
||||||
reset: ResetComponent,
|
reset: ResetComponent,
|
||||||
commit: CommitComponent,
|
commit: CommitComponent,
|
||||||
stashmsg_popup: StashMsgComponent,
|
stashmsg_popup: StashMsgComponent,
|
||||||
|
inspect_commit_popup: InspectCommitComponent,
|
||||||
cmdbar: CommandBar,
|
cmdbar: CommandBar,
|
||||||
tab: usize,
|
tab: usize,
|
||||||
revlog: Revlog,
|
revlog: Revlog,
|
||||||
|
|
@ -58,12 +60,15 @@ impl App {
|
||||||
queue.clone(),
|
queue.clone(),
|
||||||
&theme,
|
&theme,
|
||||||
),
|
),
|
||||||
|
inspect_commit_popup: InspectCommitComponent::new(
|
||||||
|
&queue, sender, &theme,
|
||||||
|
),
|
||||||
do_quit: false,
|
do_quit: false,
|
||||||
cmdbar: CommandBar::new(&theme),
|
cmdbar: CommandBar::new(&theme),
|
||||||
help: HelpComponent::new(&theme),
|
help: HelpComponent::new(&theme),
|
||||||
msg: MsgComponent::new(&theme),
|
msg: MsgComponent::new(&theme),
|
||||||
tab: 0,
|
tab: 0,
|
||||||
revlog: Revlog::new(sender, &theme),
|
revlog: Revlog::new(&queue, sender, &theme),
|
||||||
status_tab: Status::new(sender, &queue, &theme),
|
status_tab: Status::new(sender, &queue, &theme),
|
||||||
stashing_tab: Stashing::new(sender, &queue, &theme),
|
stashing_tab: Stashing::new(sender, &queue, &theme),
|
||||||
stashlist_tab: StashList::new(&queue, &theme),
|
stashlist_tab: StashList::new(&queue, &theme),
|
||||||
|
|
@ -159,8 +164,11 @@ impl App {
|
||||||
if flags.contains(NeedsUpdate::ALL) {
|
if flags.contains(NeedsUpdate::ALL) {
|
||||||
self.update()?;
|
self.update()?;
|
||||||
}
|
}
|
||||||
|
//TODO: make this a queue event?
|
||||||
|
//NOTE: set when any tree component changed selection
|
||||||
if flags.contains(NeedsUpdate::DIFF) {
|
if flags.contains(NeedsUpdate::DIFF) {
|
||||||
self.status_tab.update_diff()?;
|
self.status_tab.update_diff()?;
|
||||||
|
self.inspect_commit_popup.update_diff()?;
|
||||||
}
|
}
|
||||||
if flags.contains(NeedsUpdate::COMMANDS) {
|
if flags.contains(NeedsUpdate::COMMANDS) {
|
||||||
self.update_commands();
|
self.update_commands();
|
||||||
|
|
@ -191,6 +199,7 @@ impl App {
|
||||||
self.status_tab.update_git(ev)?;
|
self.status_tab.update_git(ev)?;
|
||||||
self.stashing_tab.update_git(ev)?;
|
self.stashing_tab.update_git(ev)?;
|
||||||
self.revlog.update_git(ev)?;
|
self.revlog.update_git(ev)?;
|
||||||
|
self.inspect_commit_popup.update_git(ev)?;
|
||||||
|
|
||||||
if let AsyncNotification::Status = ev {
|
if let AsyncNotification::Status = ev {
|
||||||
//TODO: is that needed?
|
//TODO: is that needed?
|
||||||
|
|
@ -210,6 +219,7 @@ impl App {
|
||||||
self.status_tab.anything_pending()
|
self.status_tab.anything_pending()
|
||||||
|| self.revlog.any_work_pending()
|
|| self.revlog.any_work_pending()
|
||||||
|| self.stashing_tab.anything_pending()
|
|| self.stashing_tab.anything_pending()
|
||||||
|
|| self.inspect_commit_popup.any_work_pending()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,6 +232,7 @@ impl App {
|
||||||
reset,
|
reset,
|
||||||
commit,
|
commit,
|
||||||
stashmsg_popup,
|
stashmsg_popup,
|
||||||
|
inspect_commit_popup,
|
||||||
help,
|
help,
|
||||||
revlog,
|
revlog,
|
||||||
status_tab,
|
status_tab,
|
||||||
|
|
@ -356,6 +367,10 @@ impl App {
|
||||||
self.stashmsg_popup.show()?
|
self.stashmsg_popup.show()?
|
||||||
}
|
}
|
||||||
InternalEvent::TabSwitch => self.set_tab(0)?,
|
InternalEvent::TabSwitch => self.set_tab(0)?,
|
||||||
|
InternalEvent::InspectCommit(id) => {
|
||||||
|
self.inspect_commit_popup.open(id)?;
|
||||||
|
flags.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(flags)
|
Ok(flags)
|
||||||
|
|
@ -407,19 +422,31 @@ impl App {
|
||||||
|| self.help.is_visible()
|
|| self.help.is_visible()
|
||||||
|| self.reset.is_visible()
|
|| self.reset.is_visible()
|
||||||
|| self.msg.is_visible()
|
|| self.msg.is_visible()
|
||||||
|
|| self.stashmsg_popup.is_visible()
|
||||||
|
|| self.inspect_commit_popup.is_visible()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_popups<B: Backend>(
|
fn draw_popups<B: Backend>(
|
||||||
&mut self,
|
&mut self,
|
||||||
f: &mut Frame<B>,
|
f: &mut Frame<B>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let size = f.size();
|
let size = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Min(1),
|
||||||
|
Constraint::Length(self.cmdbar.height()),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(f.size())[0];
|
||||||
|
|
||||||
self.commit.draw(f, size)?;
|
self.commit.draw(f, size)?;
|
||||||
self.stashmsg_popup.draw(f, size)?;
|
self.stashmsg_popup.draw(f, size)?;
|
||||||
self.reset.draw(f, size)?;
|
self.reset.draw(f, size)?;
|
||||||
self.help.draw(f, size)?;
|
self.help.draw(f, size)?;
|
||||||
self.msg.draw(f, size)?;
|
self.msg.draw(f, size)?;
|
||||||
|
self.inspect_commit_popup.draw(f, size)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ impl ChangesComponent {
|
||||||
files: FileTreeComponent::new(
|
files: FileTreeComponent::new(
|
||||||
title,
|
title,
|
||||||
focus,
|
focus,
|
||||||
queue.clone(),
|
Some(queue.clone()),
|
||||||
theme,
|
theme,
|
||||||
),
|
),
|
||||||
is_working_dir,
|
is_working_dir,
|
||||||
|
|
@ -75,7 +75,8 @@ impl ChangesComponent {
|
||||||
|
|
||||||
///
|
///
|
||||||
pub fn focus_select(&mut self, focus: bool) {
|
pub fn focus_select(&mut self, focus: bool) {
|
||||||
self.files.focus_select(focus)
|
self.files.focus(focus);
|
||||||
|
self.files.show_selection(focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// returns true if list is empty
|
/// returns true if list is empty
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,19 @@
|
||||||
use super::{
|
use crate::{
|
||||||
dialog_paragraph, utils::time_to_string, DrawableComponent,
|
components::{
|
||||||
|
dialog_paragraph, utils::time_to_string, CommandBlocking,
|
||||||
|
CommandInfo, Component, DrawableComponent,
|
||||||
|
},
|
||||||
|
strings,
|
||||||
|
ui::style::Theme,
|
||||||
};
|
};
|
||||||
use crate::{strings, ui::style::Theme};
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use asyncgit::{
|
use asyncgit::{
|
||||||
sync::{self, CommitDetails, CommitId},
|
sync::{self, CommitDetails},
|
||||||
AsyncCommitFiles, AsyncNotification, StatusItem, CWD,
|
CWD,
|
||||||
};
|
};
|
||||||
use crossbeam_channel::Sender;
|
use crossterm::event::Event;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use sync::Tags;
|
use sync::{CommitId, Tags};
|
||||||
use tui::{
|
use tui::{
|
||||||
backend::Backend,
|
backend::Backend,
|
||||||
layout::{Constraint, Direction, Layout, Rect},
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
|
|
@ -18,121 +22,39 @@ use tui::{
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct CommitDetailsComponent {
|
pub struct DetailsComponent {
|
||||||
data: Option<CommitDetails>,
|
data: Option<CommitDetails>,
|
||||||
tags: Vec<String>,
|
tags: Vec<String>,
|
||||||
files: Option<Vec<StatusItem>>,
|
|
||||||
theme: Theme,
|
theme: Theme,
|
||||||
git_commit_files: AsyncCommitFiles,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DrawableComponent for CommitDetailsComponent {
|
impl DetailsComponent {
|
||||||
fn draw<B: Backend>(
|
|
||||||
&mut self,
|
|
||||||
f: &mut Frame<B>,
|
|
||||||
rect: Rect,
|
|
||||||
) -> Result<()> {
|
|
||||||
let chunks = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints(
|
|
||||||
[
|
|
||||||
Constraint::Length(8),
|
|
||||||
Constraint::Min(10),
|
|
||||||
Constraint::Length(12),
|
|
||||||
]
|
|
||||||
.as_ref(),
|
|
||||||
)
|
|
||||||
.split(rect);
|
|
||||||
|
|
||||||
f.render_widget(
|
|
||||||
dialog_paragraph(
|
|
||||||
strings::commit::DETAILS_INFO_TITLE,
|
|
||||||
self.get_text_info().iter(),
|
|
||||||
),
|
|
||||||
chunks[0],
|
|
||||||
);
|
|
||||||
|
|
||||||
f.render_widget(
|
|
||||||
dialog_paragraph(
|
|
||||||
strings::commit::DETAILS_MESSAGE_TITLE,
|
|
||||||
self.get_text_message().iter(),
|
|
||||||
)
|
|
||||||
.wrap(true),
|
|
||||||
chunks[1],
|
|
||||||
);
|
|
||||||
|
|
||||||
let files_loading = self.files.is_none();
|
|
||||||
let files_count = self.files.as_ref().map_or(0, Vec::len);
|
|
||||||
|
|
||||||
let txt = self
|
|
||||||
.files
|
|
||||||
.as_ref()
|
|
||||||
.map_or(vec![], |f| self.get_text_files(f));
|
|
||||||
|
|
||||||
let title = if files_loading {
|
|
||||||
strings::commit::DETAILS_FILES_LOADING_TITLE.to_string()
|
|
||||||
} else {
|
|
||||||
format!(
|
|
||||||
"{} {}",
|
|
||||||
strings::commit::DETAILS_FILES_TITLE,
|
|
||||||
files_count
|
|
||||||
)
|
|
||||||
};
|
|
||||||
f.render_widget(
|
|
||||||
dialog_paragraph(title.as_str(), txt.iter()),
|
|
||||||
chunks[2],
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CommitDetailsComponent {
|
|
||||||
///
|
///
|
||||||
pub fn new(
|
pub const fn new(theme: &Theme) -> Self {
|
||||||
sender: &Sender<AsyncNotification>,
|
|
||||||
theme: &Theme,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
theme: *theme,
|
|
||||||
data: None,
|
data: None,
|
||||||
tags: Vec::new(),
|
tags: Vec::new(),
|
||||||
files: None,
|
theme: *theme,
|
||||||
git_commit_files: AsyncCommitFiles::new(sender),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
|
||||||
pub fn set_commit(
|
pub fn set_commit(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: Option<CommitId>,
|
id: Option<CommitId>,
|
||||||
tags: &Tags,
|
tags: &Tags,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
self.tags.clear();
|
||||||
|
|
||||||
self.data = if let Some(id) = id {
|
self.data = if let Some(id) = id {
|
||||||
sync::get_commit_details(CWD, id).ok()
|
sync::get_commit_details(CWD, id).ok()
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
self.tags.clear();
|
|
||||||
self.files = None;
|
|
||||||
|
|
||||||
if let Some(id) = id {
|
if let Some(id) = id {
|
||||||
if let Some(tags) = tags.get(&id) {
|
if let Some(tags) = tags.get(&id) {
|
||||||
self.tags.extend(tags.clone());
|
self.tags.extend(tags.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((fetched_id, res)) =
|
|
||||||
self.git_commit_files.current()?
|
|
||||||
{
|
|
||||||
if fetched_id == id {
|
|
||||||
self.files = Some(res);
|
|
||||||
} else {
|
|
||||||
self.git_commit_files.fetch(id)?;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.git_commit_files.fetch(id)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -161,25 +83,6 @@ impl CommitDetailsComponent {
|
||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_text_files<'a>(
|
|
||||||
&self,
|
|
||||||
files: &'a [StatusItem],
|
|
||||||
) -> Vec<Text<'a>> {
|
|
||||||
let new_line = Text::Raw(Cow::from("\n"));
|
|
||||||
|
|
||||||
let mut res = Vec::with_capacity(files.len());
|
|
||||||
|
|
||||||
for file in files {
|
|
||||||
res.push(Text::Styled(
|
|
||||||
Cow::from(file.path.as_str()),
|
|
||||||
self.theme.text(true, false),
|
|
||||||
));
|
|
||||||
res.push(new_line.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_text_info(&self) -> Vec<Text> {
|
fn get_text_info(&self) -> Vec<Text> {
|
||||||
let new_line = Text::Raw(Cow::from("\n"));
|
let new_line = Text::Raw(Cow::from("\n"));
|
||||||
|
|
||||||
|
|
@ -271,9 +174,57 @@ impl CommitDetailsComponent {
|
||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
///
|
impl DrawableComponent for DetailsComponent {
|
||||||
pub fn any_work_pending(&self) -> bool {
|
fn draw<B: Backend>(
|
||||||
self.git_commit_files.is_pending()
|
&mut self,
|
||||||
|
f: &mut Frame<B>,
|
||||||
|
rect: Rect,
|
||||||
|
) -> Result<()> {
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(
|
||||||
|
[Constraint::Length(8), Constraint::Min(10)].as_ref(),
|
||||||
|
)
|
||||||
|
.split(rect);
|
||||||
|
|
||||||
|
f.render_widget(
|
||||||
|
dialog_paragraph(
|
||||||
|
strings::commit::DETAILS_INFO_TITLE,
|
||||||
|
self.get_text_info().iter(),
|
||||||
|
&self.theme,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
chunks[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
f.render_widget(
|
||||||
|
dialog_paragraph(
|
||||||
|
strings::commit::DETAILS_MESSAGE_TITLE,
|
||||||
|
self.get_text_message().iter(),
|
||||||
|
&self.theme,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.wrap(true),
|
||||||
|
chunks[1],
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for DetailsComponent {
|
||||||
|
fn commands(
|
||||||
|
&self,
|
||||||
|
_out: &mut Vec<CommandInfo>,
|
||||||
|
_force_all: bool,
|
||||||
|
) -> CommandBlocking {
|
||||||
|
// visibility_blocking(self)
|
||||||
|
CommandBlocking::PassingOn
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event(&mut self, _ev: Event) -> Result<bool> {
|
||||||
|
Ok(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
173
src/components/commit_details/mod.rs
Normal file
173
src/components/commit_details/mod.rs
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
mod details;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
command_pump, event_pump, CommandBlocking, CommandInfo,
|
||||||
|
Component, DrawableComponent, FileTreeComponent,
|
||||||
|
};
|
||||||
|
use crate::{accessors, queue::Queue, strings, ui::style::Theme};
|
||||||
|
use anyhow::Result;
|
||||||
|
use asyncgit::{
|
||||||
|
sync::{CommitId, Tags},
|
||||||
|
AsyncCommitFiles, AsyncNotification,
|
||||||
|
};
|
||||||
|
use crossbeam_channel::Sender;
|
||||||
|
use crossterm::event::Event;
|
||||||
|
use details::DetailsComponent;
|
||||||
|
use tui::{
|
||||||
|
backend::Backend,
|
||||||
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct CommitDetailsComponent {
|
||||||
|
details: DetailsComponent,
|
||||||
|
file_tree: FileTreeComponent,
|
||||||
|
git_commit_files: AsyncCommitFiles,
|
||||||
|
visible: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommitDetailsComponent {
|
||||||
|
accessors!(self, [details, file_tree]);
|
||||||
|
|
||||||
|
///
|
||||||
|
pub fn new(
|
||||||
|
queue: &Queue,
|
||||||
|
sender: &Sender<AsyncNotification>,
|
||||||
|
theme: &Theme,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
details: DetailsComponent::new(theme),
|
||||||
|
git_commit_files: AsyncCommitFiles::new(sender),
|
||||||
|
file_tree: FileTreeComponent::new(
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
Some(queue.clone()),
|
||||||
|
theme,
|
||||||
|
),
|
||||||
|
visible: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_files_title(&self) -> String {
|
||||||
|
let files_loading = self.git_commit_files.is_pending();
|
||||||
|
let files_count = self.file_tree.file_count();
|
||||||
|
|
||||||
|
if files_loading {
|
||||||
|
strings::commit::DETAILS_FILES_LOADING_TITLE.to_string()
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"{} {}",
|
||||||
|
strings::commit::DETAILS_FILES_TITLE,
|
||||||
|
files_count
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
pub fn set_commit(
|
||||||
|
&mut self,
|
||||||
|
id: Option<CommitId>,
|
||||||
|
tags: &Tags,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.details.set_commit(id, tags)?;
|
||||||
|
|
||||||
|
if let Some(id) = id {
|
||||||
|
if let Some((fetched_id, res)) =
|
||||||
|
self.git_commit_files.current()?
|
||||||
|
{
|
||||||
|
if fetched_id == id {
|
||||||
|
self.file_tree.update(res.as_slice())?;
|
||||||
|
} else {
|
||||||
|
self.file_tree.clear()?;
|
||||||
|
self.git_commit_files.fetch(id)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.file_tree.clear()?;
|
||||||
|
self.git_commit_files.fetch(id)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.file_tree.set_title(self.get_files_title());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
pub fn any_work_pending(&self) -> bool {
|
||||||
|
self.git_commit_files.is_pending()
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
pub const fn files(&self) -> &FileTreeComponent {
|
||||||
|
&self.file_tree
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DrawableComponent for CommitDetailsComponent {
|
||||||
|
fn draw<B: Backend>(
|
||||||
|
&mut self,
|
||||||
|
f: &mut Frame<B>,
|
||||||
|
rect: Rect,
|
||||||
|
) -> Result<()> {
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Percentage(70),
|
||||||
|
Constraint::Percentage(30),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(rect);
|
||||||
|
|
||||||
|
self.details.draw(f, chunks[0])?;
|
||||||
|
self.file_tree.draw(f, chunks[1])?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for CommitDetailsComponent {
|
||||||
|
fn commands(
|
||||||
|
&self,
|
||||||
|
out: &mut Vec<CommandInfo>,
|
||||||
|
force_all: bool,
|
||||||
|
) -> CommandBlocking {
|
||||||
|
if self.visible || force_all {
|
||||||
|
command_pump(
|
||||||
|
out,
|
||||||
|
force_all,
|
||||||
|
self.components().as_slice(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CommandBlocking::PassingOn
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event(&mut self, ev: Event) -> Result<bool> {
|
||||||
|
if event_pump(ev, self.components_mut().as_mut_slice())? {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_visible(&self) -> bool {
|
||||||
|
self.visible
|
||||||
|
}
|
||||||
|
fn hide(&mut self) {
|
||||||
|
self.visible = false;
|
||||||
|
}
|
||||||
|
fn show(&mut self) -> Result<()> {
|
||||||
|
self.visible = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focused(&self) -> bool {
|
||||||
|
self.details.focused() || self.file_tree.focused()
|
||||||
|
}
|
||||||
|
fn focus(&mut self, focus: bool) {
|
||||||
|
self.file_tree.focus(focus);
|
||||||
|
self.file_tree.show_selection(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -35,13 +35,13 @@ pub struct DiffComponent {
|
||||||
focused: bool,
|
focused: bool,
|
||||||
current: Current,
|
current: Current,
|
||||||
selected_hunk: Option<usize>,
|
selected_hunk: Option<usize>,
|
||||||
queue: Queue,
|
queue: Option<Queue>,
|
||||||
theme: Theme,
|
theme: Theme,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiffComponent {
|
impl DiffComponent {
|
||||||
///
|
///
|
||||||
pub fn new(queue: Queue, theme: &Theme) -> Self {
|
pub fn new(queue: Option<Queue>, theme: &Theme) -> Self {
|
||||||
Self {
|
Self {
|
||||||
focused: false,
|
focused: false,
|
||||||
queue,
|
queue,
|
||||||
|
|
@ -270,12 +270,18 @@ impl DiffComponent {
|
||||||
if let Some(hunk) = self.selected_hunk {
|
if let Some(hunk) = self.selected_hunk {
|
||||||
let hash = self.diff.hunks[hunk].header_hash;
|
let hash = self.diff.hunks[hunk].header_hash;
|
||||||
self.queue
|
self.queue
|
||||||
|
.as_ref()
|
||||||
|
.expect("try using queue in immutable diff")
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.push_back(InternalEvent::AddHunk(hash));
|
.push_back(InternalEvent::AddHunk(hash));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_immutable(&self) -> bool {
|
||||||
|
self.queue.is_none()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DrawableComponent for DiffComponent {
|
impl DrawableComponent for DiffComponent {
|
||||||
|
|
@ -325,16 +331,18 @@ impl Component for DiffComponent {
|
||||||
.hidden(),
|
.hidden(),
|
||||||
);
|
);
|
||||||
|
|
||||||
out.push(CommandInfo::new(
|
if !self.is_immutable() {
|
||||||
commands::DIFF_HUNK_REMOVE,
|
out.push(CommandInfo::new(
|
||||||
self.selected_hunk.is_some(),
|
commands::DIFF_HUNK_REMOVE,
|
||||||
self.focused && self.current.is_stage,
|
self.selected_hunk.is_some(),
|
||||||
));
|
self.focused && self.current.is_stage,
|
||||||
out.push(CommandInfo::new(
|
));
|
||||||
commands::DIFF_HUNK_ADD,
|
out.push(CommandInfo::new(
|
||||||
self.selected_hunk.is_some(),
|
commands::DIFF_HUNK_ADD,
|
||||||
self.focused && !self.current.is_stage,
|
self.selected_hunk.is_some(),
|
||||||
));
|
self.focused && !self.current.is_stage,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
CommandBlocking::PassingOn
|
CommandBlocking::PassingOn
|
||||||
}
|
}
|
||||||
|
|
@ -367,7 +375,7 @@ impl Component for DiffComponent {
|
||||||
self.scroll(ScrollType::PageDown)?;
|
self.scroll(ScrollType::PageDown)?;
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
keys::ENTER => {
|
keys::ENTER if !self.is_immutable() => {
|
||||||
self.add_hunk()?;
|
self.add_hunk()?;
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
@ -382,7 +390,6 @@ impl Component for DiffComponent {
|
||||||
fn focused(&self) -> bool {
|
fn focused(&self) -> bool {
|
||||||
self.focused
|
self.focused
|
||||||
}
|
}
|
||||||
|
|
||||||
fn focus(&mut self, focus: bool) {
|
fn focus(&mut self, focus: bool) {
|
||||||
self.focused = focus
|
self.focused = focus
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ pub struct FileTreeComponent {
|
||||||
current_hash: u64,
|
current_hash: u64,
|
||||||
focused: bool,
|
focused: bool,
|
||||||
show_selection: bool,
|
show_selection: bool,
|
||||||
queue: Queue,
|
queue: Option<Queue>,
|
||||||
theme: Theme,
|
theme: Theme,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,7 +36,7 @@ impl FileTreeComponent {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
title: &str,
|
title: &str,
|
||||||
focus: bool,
|
focus: bool,
|
||||||
queue: Queue,
|
queue: Option<Queue>,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -67,9 +67,19 @@ impl FileTreeComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
pub fn focus_select(&mut self, focus: bool) {
|
pub fn selection_file(&self) -> Option<StatusItem> {
|
||||||
self.focus(focus);
|
self.tree.selected_item().and_then(|f| {
|
||||||
self.show_selection = focus;
|
if let FileTreeItemKind::File(f) = f.kind {
|
||||||
|
Some(f)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
pub fn show_selection(&mut self, show: bool) {
|
||||||
|
self.show_selection = show;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// returns true if list is empty
|
/// returns true if list is empty
|
||||||
|
|
@ -77,6 +87,22 @@ impl FileTreeComponent {
|
||||||
self.tree.is_empty()
|
self.tree.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
pub fn file_count(&self) -> usize {
|
||||||
|
self.tree.tree.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
pub fn set_title(&mut self, title: String) {
|
||||||
|
self.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
pub fn clear(&mut self) -> Result<()> {
|
||||||
|
self.current_hash = 0;
|
||||||
|
self.tree.update(&[])
|
||||||
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
pub fn is_file_seleted(&self) -> bool {
|
pub fn is_file_seleted(&self) -> bool {
|
||||||
if let Some(item) = self.tree.selected_item() {
|
if let Some(item) = self.tree.selected_item() {
|
||||||
|
|
@ -93,9 +119,11 @@ impl FileTreeComponent {
|
||||||
let changed = self.tree.move_selection(dir);
|
let changed = self.tree.move_selection(dir);
|
||||||
|
|
||||||
if changed {
|
if changed {
|
||||||
self.queue
|
if let Some(ref queue) = self.queue {
|
||||||
.borrow_mut()
|
queue.borrow_mut().push_back(InternalEvent::Update(
|
||||||
.push_back(InternalEvent::Update(NeedsUpdate::DIFF));
|
NeedsUpdate::DIFF,
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
changed
|
changed
|
||||||
|
|
@ -286,6 +314,6 @@ impl Component for FileTreeComponent {
|
||||||
self.focused
|
self.focused
|
||||||
}
|
}
|
||||||
fn focus(&mut self, focus: bool) {
|
fn focus(&mut self, focus: bool) {
|
||||||
self.focused = focus
|
self.focused = focus;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
215
src/components/inspect_commit.rs
Normal file
215
src/components/inspect_commit.rs
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
use super::{
|
||||||
|
command_pump, event_pump, visibility_blocking, CommandBlocking,
|
||||||
|
CommandInfo, CommitDetailsComponent, Component, DiffComponent,
|
||||||
|
DrawableComponent,
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
accessors, keys, queue::Queue, strings, ui::style::Theme,
|
||||||
|
};
|
||||||
|
use anyhow::Result;
|
||||||
|
use asyncgit::{sync, AsyncNotification, CWD};
|
||||||
|
use crossbeam_channel::Sender;
|
||||||
|
use crossterm::event::Event;
|
||||||
|
use strings::commands;
|
||||||
|
use sync::{CommitId, Tags};
|
||||||
|
use tui::{
|
||||||
|
backend::Backend,
|
||||||
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
|
widgets::Clear,
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct InspectCommitComponent {
|
||||||
|
commit_id: Option<CommitId>,
|
||||||
|
diff: DiffComponent,
|
||||||
|
details: CommitDetailsComponent,
|
||||||
|
visible: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DrawableComponent for InspectCommitComponent {
|
||||||
|
fn draw<B: Backend>(
|
||||||
|
&mut self,
|
||||||
|
f: &mut Frame<B>,
|
||||||
|
rect: Rect,
|
||||||
|
) -> Result<()> {
|
||||||
|
if self.is_visible() {
|
||||||
|
let percentages = if self.diff.focused() {
|
||||||
|
(30, 70)
|
||||||
|
} else {
|
||||||
|
(50, 50)
|
||||||
|
};
|
||||||
|
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Percentage(percentages.0),
|
||||||
|
Constraint::Percentage(percentages.1),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(rect);
|
||||||
|
|
||||||
|
f.render_widget(Clear, rect);
|
||||||
|
|
||||||
|
self.details.draw(f, chunks[0])?;
|
||||||
|
self.diff.draw(f, chunks[1])?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for InspectCommitComponent {
|
||||||
|
fn commands(
|
||||||
|
&self,
|
||||||
|
out: &mut Vec<CommandInfo>,
|
||||||
|
force_all: bool,
|
||||||
|
) -> CommandBlocking {
|
||||||
|
if self.is_visible() || force_all {
|
||||||
|
command_pump(
|
||||||
|
out,
|
||||||
|
force_all,
|
||||||
|
self.components().as_slice(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push(
|
||||||
|
CommandInfo::new(
|
||||||
|
commands::CLOSE_POPUP,
|
||||||
|
true,
|
||||||
|
self.is_visible(),
|
||||||
|
)
|
||||||
|
.order(1),
|
||||||
|
);
|
||||||
|
|
||||||
|
visibility_blocking(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event(&mut self, ev: Event) -> Result<bool> {
|
||||||
|
if self.is_visible() {
|
||||||
|
if event_pump(ev, self.components_mut().as_mut_slice())? {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Event::Key(e) = ev {
|
||||||
|
match e {
|
||||||
|
keys::EXIT_POPUP => {
|
||||||
|
self.hide();
|
||||||
|
}
|
||||||
|
keys::FOCUS_RIGHT if self.can_focus_diff() => {
|
||||||
|
self.details.focus(false);
|
||||||
|
self.diff.focus(true);
|
||||||
|
}
|
||||||
|
keys::FOCUS_LEFT if self.diff.focused() => {
|
||||||
|
self.details.focus(true);
|
||||||
|
self.diff.focus(false);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop key event propagation
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_visible(&self) -> bool {
|
||||||
|
self.visible
|
||||||
|
}
|
||||||
|
fn hide(&mut self) {
|
||||||
|
self.visible = false;
|
||||||
|
}
|
||||||
|
fn show(&mut self) -> Result<()> {
|
||||||
|
self.visible = true;
|
||||||
|
self.details.show()?;
|
||||||
|
self.details.focus(true);
|
||||||
|
self.diff.focus(false);
|
||||||
|
self.update()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InspectCommitComponent {
|
||||||
|
accessors!(self, [diff, details]);
|
||||||
|
|
||||||
|
///
|
||||||
|
pub fn new(
|
||||||
|
queue: &Queue,
|
||||||
|
sender: &Sender<AsyncNotification>,
|
||||||
|
theme: &Theme,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
details: CommitDetailsComponent::new(
|
||||||
|
queue, sender, theme,
|
||||||
|
),
|
||||||
|
diff: DiffComponent::new(None, theme),
|
||||||
|
commit_id: None,
|
||||||
|
visible: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
pub fn open(&mut self, id: CommitId) -> Result<()> {
|
||||||
|
self.commit_id = Some(id);
|
||||||
|
self.show()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
pub fn any_work_pending(&self) -> bool {
|
||||||
|
self.details.any_work_pending()
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
pub fn update_git(
|
||||||
|
&mut self,
|
||||||
|
ev: AsyncNotification,
|
||||||
|
) -> Result<()> {
|
||||||
|
if self.is_visible() {
|
||||||
|
if let AsyncNotification::CommitFiles = ev {
|
||||||
|
self.update()?
|
||||||
|
} else if let AsyncNotification::Diff = ev {
|
||||||
|
self.update()?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// called when any tree component changed selection
|
||||||
|
pub fn update_diff(&mut self) -> Result<()> {
|
||||||
|
if self.is_visible() {
|
||||||
|
if let Some(id) = self.commit_id {
|
||||||
|
if let Some(f) = self.details.files().selection_file()
|
||||||
|
{
|
||||||
|
self.diff.update(
|
||||||
|
f.path.clone(),
|
||||||
|
false,
|
||||||
|
sync::get_diff_commit(CWD, id, f.path)?,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.diff.clear()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self) -> Result<()> {
|
||||||
|
self.details.set_commit(self.commit_id, &Tags::new())?;
|
||||||
|
self.update_diff()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_focus_diff(&self) -> bool {
|
||||||
|
self.details.files().selection_file().is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ mod commitlist;
|
||||||
mod diff;
|
mod diff;
|
||||||
mod filetree;
|
mod filetree;
|
||||||
mod help;
|
mod help;
|
||||||
|
mod inspect_commit;
|
||||||
mod msg;
|
mod msg;
|
||||||
mod reset;
|
mod reset;
|
||||||
mod stashmsg;
|
mod stashmsg;
|
||||||
|
|
@ -21,15 +22,16 @@ use crossterm::event::Event;
|
||||||
pub use diff::DiffComponent;
|
pub use diff::DiffComponent;
|
||||||
pub use filetree::FileTreeComponent;
|
pub use filetree::FileTreeComponent;
|
||||||
pub use help::HelpComponent;
|
pub use help::HelpComponent;
|
||||||
|
pub use inspect_commit::InspectCommitComponent;
|
||||||
pub use msg::MsgComponent;
|
pub use msg::MsgComponent;
|
||||||
pub use reset::ResetComponent;
|
pub use reset::ResetComponent;
|
||||||
pub use stashmsg::StashMsgComponent;
|
pub use stashmsg::StashMsgComponent;
|
||||||
pub use utils::filetree::FileTreeItemKind;
|
pub use utils::filetree::FileTreeItemKind;
|
||||||
|
|
||||||
|
use crate::ui::style::Theme;
|
||||||
use tui::{
|
use tui::{
|
||||||
backend::Backend,
|
backend::Backend,
|
||||||
layout::Alignment,
|
layout::{Alignment, Rect},
|
||||||
layout::Rect,
|
|
||||||
widgets::{Block, BorderType, Borders, Paragraph, Text},
|
widgets::{Block, BorderType, Borders, Paragraph, Text},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
|
|
@ -152,23 +154,23 @@ pub trait Component {
|
||||||
fn show(&mut self) -> Result<()> {
|
fn show(&mut self) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
fn toggle_visible(&mut self) -> Result<()> {
|
||||||
|
if self.is_visible() {
|
||||||
|
self.hide();
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
self.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dialog_paragraph<'a, 't, T>(
|
fn dialog_paragraph<'a, 't, T>(
|
||||||
title: &'a str,
|
title: &'a str,
|
||||||
content: T,
|
content: T,
|
||||||
) -> Paragraph<'a, 't, T>
|
theme: &Theme,
|
||||||
where
|
focused: bool,
|
||||||
T: Iterator<Item = &'t Text<'t>>,
|
|
||||||
{
|
|
||||||
Paragraph::new(content)
|
|
||||||
.block(Block::default().title(title).borders(Borders::ALL))
|
|
||||||
.alignment(Alignment::Left)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn popup_paragraph<'a, 't, T>(
|
|
||||||
title: &'a str,
|
|
||||||
content: T,
|
|
||||||
) -> Paragraph<'a, 't, T>
|
) -> Paragraph<'a, 't, T>
|
||||||
where
|
where
|
||||||
T: Iterator<Item = &'t Text<'t>>,
|
T: Iterator<Item = &'t Text<'t>>,
|
||||||
|
|
@ -178,7 +180,29 @@ where
|
||||||
Block::default()
|
Block::default()
|
||||||
.title(title)
|
.title(title)
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Thick),
|
.title_style(theme.title(focused))
|
||||||
|
.border_style(theme.block(focused)),
|
||||||
|
)
|
||||||
|
.alignment(Alignment::Left)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn popup_paragraph<'a, 't, T>(
|
||||||
|
title: &'a str,
|
||||||
|
content: T,
|
||||||
|
theme: &Theme,
|
||||||
|
focused: bool,
|
||||||
|
) -> Paragraph<'a, 't, T>
|
||||||
|
where
|
||||||
|
T: Iterator<Item = &'t Text<'t>>,
|
||||||
|
{
|
||||||
|
Paragraph::new(content)
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.title(title)
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Thick)
|
||||||
|
.title_style(theme.title(focused))
|
||||||
|
.border_style(theme.block(focused)),
|
||||||
)
|
)
|
||||||
.alignment(Alignment::Left)
|
.alignment(Alignment::Left)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,10 @@ impl DrawableComponent for ResetComponent {
|
||||||
|
|
||||||
let area = ui::centered_rect(30, 20, f.size());
|
let area = ui::centered_rect(30, 20, f.size());
|
||||||
f.render_widget(Clear, area);
|
f.render_widget(Clear, area);
|
||||||
f.render_widget(popup_paragraph(title, txt.iter()), area);
|
f.render_widget(
|
||||||
|
popup_paragraph(title, txt.iter(), &self.theme, true),
|
||||||
|
area,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,12 @@ impl DrawableComponent for TextInputComponent {
|
||||||
let area = ui::centered_rect(60, 20, f.size());
|
let area = ui::centered_rect(60, 20, f.size());
|
||||||
f.render_widget(Clear, area);
|
f.render_widget(Clear, area);
|
||||||
f.render_widget(
|
f.render_widget(
|
||||||
popup_paragraph(self.title.as_str(), txt.iter()),
|
popup_paragraph(
|
||||||
|
self.title.as_str(),
|
||||||
|
txt.iter(),
|
||||||
|
&self.theme,
|
||||||
|
true,
|
||||||
|
),
|
||||||
area,
|
area,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,12 +41,14 @@ pub enum InternalEvent {
|
||||||
ShowErrorMsg(String),
|
ShowErrorMsg(String),
|
||||||
///
|
///
|
||||||
Update(NeedsUpdate),
|
Update(NeedsUpdate),
|
||||||
///
|
/// open commit msg input
|
||||||
OpenCommit,
|
OpenCommit,
|
||||||
///
|
///
|
||||||
PopupStashing(StashingOptions),
|
PopupStashing(StashingOptions),
|
||||||
///
|
///
|
||||||
TabSwitch,
|
TabSwitch,
|
||||||
|
///
|
||||||
|
InspectCommit(CommitId),
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -249,9 +249,15 @@ pub mod commands {
|
||||||
);
|
);
|
||||||
|
|
||||||
///
|
///
|
||||||
pub static LOG_DETAILS_OPEN: CommandText = CommandText::new(
|
pub static LOG_DETAILS_TOGGLE: CommandText = CommandText::new(
|
||||||
"Details [enter]",
|
"Details [enter]",
|
||||||
"open details of selected commit",
|
"open details of selected commit",
|
||||||
CMD_GROUP_LOG,
|
CMD_GROUP_LOG,
|
||||||
);
|
);
|
||||||
|
///
|
||||||
|
pub static LOG_DETAILS_OPEN: CommandText = CommandText::new(
|
||||||
|
"Inspect [\u{2192}]", //→
|
||||||
|
"inspect selected commit in detail",
|
||||||
|
CMD_GROUP_LOG,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ use crate::{
|
||||||
CommitDetailsComponent, CommitList, Component,
|
CommitDetailsComponent, CommitList, Component,
|
||||||
DrawableComponent,
|
DrawableComponent,
|
||||||
},
|
},
|
||||||
keys, strings,
|
keys,
|
||||||
|
queue::{InternalEvent, Queue},
|
||||||
|
strings,
|
||||||
ui::style::Theme,
|
ui::style::Theme,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
@ -12,6 +14,7 @@ use asyncgit::{sync, AsyncLog, AsyncNotification, FetchStatus, CWD};
|
||||||
use crossbeam_channel::Sender;
|
use crossbeam_channel::Sender;
|
||||||
use crossterm::event::Event;
|
use crossterm::event::Event;
|
||||||
use strings::commands;
|
use strings::commands;
|
||||||
|
use sync::CommitId;
|
||||||
use tui::{
|
use tui::{
|
||||||
backend::Backend,
|
backend::Backend,
|
||||||
layout::{Constraint, Direction, Layout, Rect},
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
|
|
@ -25,24 +28,25 @@ pub struct Revlog {
|
||||||
commit_details: CommitDetailsComponent,
|
commit_details: CommitDetailsComponent,
|
||||||
list: CommitList,
|
list: CommitList,
|
||||||
git_log: AsyncLog,
|
git_log: AsyncLog,
|
||||||
|
queue: Queue,
|
||||||
visible: bool,
|
visible: bool,
|
||||||
details_open: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Revlog {
|
impl Revlog {
|
||||||
///
|
///
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
queue: &Queue,
|
||||||
sender: &Sender<AsyncNotification>,
|
sender: &Sender<AsyncNotification>,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
queue: queue.clone(),
|
||||||
commit_details: CommitDetailsComponent::new(
|
commit_details: CommitDetailsComponent::new(
|
||||||
sender, theme,
|
queue, sender, theme,
|
||||||
),
|
),
|
||||||
list: CommitList::new(strings::LOG_TITLE, theme),
|
list: CommitList::new(strings::LOG_TITLE, theme),
|
||||||
git_log: AsyncLog::new(sender),
|
git_log: AsyncLog::new(sender),
|
||||||
visible: false,
|
visible: false,
|
||||||
details_open: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,9 +76,9 @@ impl Revlog {
|
||||||
self.list.set_tags(sync::get_tags(CWD)?);
|
self.list.set_tags(sync::get_tags(CWD)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.details_open {
|
if self.commit_details.is_visible() {
|
||||||
self.commit_details.set_commit(
|
self.commit_details.set_commit(
|
||||||
self.list.selected_entry().map(|e| e.id),
|
self.selected_commit(),
|
||||||
self.list.tags().expect("tags"),
|
self.list.tags().expect("tags"),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
@ -115,6 +119,10 @@ impl Revlog {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn selected_commit(&self) -> Option<CommitId> {
|
||||||
|
self.list.selected_entry().map(|e| e.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DrawableComponent for Revlog {
|
impl DrawableComponent for Revlog {
|
||||||
|
|
@ -134,7 +142,7 @@ impl DrawableComponent for Revlog {
|
||||||
)
|
)
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
if self.details_open {
|
if self.commit_details.is_visible() {
|
||||||
self.list.draw(f, chunks[0])?;
|
self.list.draw(f, chunks[0])?;
|
||||||
self.commit_details.draw(f, chunks[1])?;
|
self.commit_details.draw(f, chunks[1])?;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -154,9 +162,18 @@ impl Component for Revlog {
|
||||||
self.update()?;
|
self.update()?;
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
} else if let Event::Key(keys::LOG_COMMIT_DETAILS) = ev {
|
} else if let Event::Key(keys::LOG_COMMIT_DETAILS) = ev {
|
||||||
self.details_open = !self.details_open;
|
self.commit_details.toggle_visible()?;
|
||||||
self.update()?;
|
self.update()?;
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
|
} else if let Event::Key(keys::FOCUS_RIGHT) = ev {
|
||||||
|
return if let Some(id) = self.selected_commit() {
|
||||||
|
self.queue
|
||||||
|
.borrow_mut()
|
||||||
|
.push_back(InternalEvent::InspectCommit(id));
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -173,11 +190,18 @@ impl Component for Revlog {
|
||||||
}
|
}
|
||||||
|
|
||||||
out.push(CommandInfo::new(
|
out.push(CommandInfo::new(
|
||||||
commands::LOG_DETAILS_OPEN,
|
commands::LOG_DETAILS_TOGGLE,
|
||||||
true,
|
true,
|
||||||
self.visible,
|
self.visible,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
out.push(CommandInfo::new(
|
||||||
|
commands::LOG_DETAILS_OPEN,
|
||||||
|
true,
|
||||||
|
(self.visible && self.commit_details.is_visible())
|
||||||
|
|| force_all,
|
||||||
|
));
|
||||||
|
|
||||||
visibility_blocking(self)
|
visibility_blocking(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ impl Stashing {
|
||||||
index: FileTreeComponent::new(
|
index: FileTreeComponent::new(
|
||||||
strings::STASHING_FILES_TITLE,
|
strings::STASHING_FILES_TITLE,
|
||||||
true,
|
true,
|
||||||
queue.clone(),
|
Some(queue.clone()),
|
||||||
theme,
|
theme,
|
||||||
),
|
),
|
||||||
visible: false,
|
visible: false,
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ impl Status {
|
||||||
queue.clone(),
|
queue.clone(),
|
||||||
theme,
|
theme,
|
||||||
),
|
),
|
||||||
diff: DiffComponent::new(queue.clone(), theme),
|
diff: DiffComponent::new(Some(queue.clone()), theme),
|
||||||
git_diff: AsyncDiff::new(sender.clone()),
|
git_diff: AsyncDiff::new(sender.clone()),
|
||||||
git_status_workdir: AsyncStatus::new(sender.clone()),
|
git_status_workdir: AsyncStatus::new(sender.clone()),
|
||||||
git_status_stage: AsyncStatus::new(sender.clone()),
|
git_status_stage: AsyncStatus::new(sender.clone()),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue