From 8e8c5fad5556d3b6e4afc9f127bf4f955a181287 Mon Sep 17 00:00:00 2001 From: extrawurst Date: Mon, 21 Nov 2022 19:56:09 +0100 Subject: [PATCH] make fetching tree files async --- CHANGELOG.md | 1 + asyncgit/src/lib.rs | 4 + asyncgit/src/treefiles.rs | 73 +++++++++++++++ src/app.rs | 6 +- src/components/revision_files.rs | 119 +++++++++++++++++-------- src/components/revision_files_popup.rs | 11 ++- src/tabs/files.rs | 16 +++- 7 files changed, 185 insertions(+), 45 deletions(-) create mode 100644 asyncgit/src/treefiles.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f84640a2..25990117 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixes * improve performance by requesting branches info asynchronous ([92f63d1](https://github.com/extrawurst/gitui/commit/92f63d107c1dca1f10139668ff5b3ca752261b0f)) * fix app startup delay due to using file watcher ([#1436](https://github.com/extrawurst/gitui/issues/1436)) +* make git tree file fetch async ([#734](https://github.com/extrawurst/gitui/issues/734)) ## [0.22.0] - 2022-11-19 diff --git a/asyncgit/src/lib.rs b/asyncgit/src/lib.rs index f94e6496..538ba100 100644 --- a/asyncgit/src/lib.rs +++ b/asyncgit/src/lib.rs @@ -40,6 +40,7 @@ mod revlog; mod status; pub mod sync; mod tags; +mod treefiles; pub use crate::{ blame::{AsyncBlame, BlameParams}, @@ -61,6 +62,7 @@ pub use crate::{ status::{StatusItem, StatusItemType}, }, tags::AsyncTags, + treefiles::AsyncTreeFilesJob, }; pub use git2::message_prettify; use std::{ @@ -99,6 +101,8 @@ pub enum AsyncGitNotification { Fetch, /// Branches, + /// + TreeFiles, } /// helper function to calculate the hash of an arbitrary type that implements the `Hash` trait diff --git a/asyncgit/src/treefiles.rs b/asyncgit/src/treefiles.rs new file mode 100644 index 00000000..9dd798f0 --- /dev/null +++ b/asyncgit/src/treefiles.rs @@ -0,0 +1,73 @@ +use crate::{ + asyncjob::{AsyncJob, RunParams}, + error::Result, + sync::{tree_files, CommitId, RepoPath, TreeFile}, + AsyncGitNotification, +}; +use std::sync::{Arc, Mutex}; + +enum JobState { + Request { commit: CommitId, repo: RepoPath }, + Response(Result>), +} + +/// +#[derive(Clone, Default)] +pub struct AsyncTreeFilesJob { + state: Arc>>, +} + +/// +impl AsyncTreeFilesJob { + /// + pub fn new(repo: RepoPath, commit: CommitId) -> Self { + Self { + state: Arc::new(Mutex::new(Some(JobState::Request { + repo, + commit, + }))), + } + } + + /// + pub fn result(&self) -> Option>> { + if let Ok(mut state) = self.state.lock() { + if let Some(state) = state.take() { + return match state { + JobState::Request { .. } => None, + JobState::Response(result) => Some(result), + }; + } + } + + None + } +} + +impl AsyncJob for AsyncTreeFilesJob { + type Notification = AsyncGitNotification; + type Progress = (); + + fn run( + &mut self, + _params: RunParams, + ) -> Result { + if let Ok(mut state) = self.state.lock() { + *state = state.take().map(|state| match state { + JobState::Request { commit, repo } => { + let files = tree_files(&repo, commit); + + std::thread::sleep( + std::time::Duration::from_secs(2), + ); + JobState::Response(files) + } + JobState::Response(result) => { + JobState::Response(result) + } + }); + } + + Ok(AsyncGitNotification::TreeFiles) + } +} diff --git a/src/app.rs b/src/app.rs index 6e99b896..ec1294be 100644 --- a/src/app.rs +++ b/src/app.rs @@ -162,6 +162,7 @@ impl App { repo.clone(), &queue, sender_app, + sender.clone(), theme.clone(), key_config.clone(), ), @@ -306,6 +307,7 @@ impl App { files_tab: FilesTab::new( repo.clone(), sender_app, + sender.clone(), &queue, theme.clone(), key_config.clone(), @@ -508,8 +510,8 @@ impl App { self.select_branch_popup.update_git(ev)?; } - self.files_tab.update_async(ev); - self.revision_files_popup.update(ev); + self.files_tab.update_async(ev)?; + self.revision_files_popup.update(ev)?; self.tags_popup.update(ev); //TODO: better system for this diff --git a/src/components/revision_files.rs b/src/components/revision_files.rs index b313a806..691fba37 100644 --- a/src/components/revision_files.rs +++ b/src/components/revision_files.rs @@ -11,14 +11,17 @@ use crate::{ AsyncAppNotification, AsyncNotification, }; use anyhow::Result; -use asyncgit::sync::{ - self, get_commit_info, CommitId, CommitInfo, RepoPathRef, - TreeFile, +use asyncgit::{ + asyncjob::AsyncSingleJob, + sync::{ + get_commit_info, CommitId, CommitInfo, RepoPathRef, TreeFile, + }, + AsyncGitNotification, AsyncTreeFilesJob, }; use crossbeam_channel::Sender; use crossterm::event::Event; use filetreelist::{FileTree, FileTreeItem}; -use std::fmt::Write; +use std::{borrow::Cow, fmt::Write}; use std::{ collections::BTreeSet, convert::From, @@ -44,7 +47,8 @@ pub struct RevisionFilesComponent { queue: Queue, theme: SharedTheme, //TODO: store TreeFiles in `tree` - files: Vec, + files: Option>, + async_treefiles: AsyncSingleJob, current_file: SyntaxTextComponent, tree: FileTree, scroll: VerticalScroll, @@ -60,6 +64,7 @@ impl RevisionFilesComponent { repo: RepoPathRef, queue: &Queue, sender: &Sender, + sender_git: Sender, theme: SharedTheme, key_config: SharedKeyConfig, ) -> Self { @@ -73,8 +78,9 @@ impl RevisionFilesComponent { key_config.clone(), theme.clone(), ), + async_treefiles: AsyncSingleJob::new(sender_git), theme, - files: Vec::new(), + files: None, revision: None, focus: Focus::Tree, key_config, @@ -89,13 +95,15 @@ impl RevisionFilesComponent { let same_id = self.revision.as_ref().map_or(false, |c| c.id == commit); + if !same_id { - self.files = - sync::tree_files(&self.repo.borrow(), commit)?; - let filenames: Vec<&Path> = - self.files.iter().map(|f| f.path.as_path()).collect(); - self.tree = FileTree::new(&filenames, &BTreeSet::new())?; - self.tree.collapse_but_root(); + self.files = None; + + self.async_treefiles.spawn(AsyncTreeFilesJob::new( + self.repo.borrow().clone(), + commit, + )); + self.revision = Some(get_commit_info(&self.repo.borrow(), &commit)?); } @@ -114,13 +122,35 @@ impl RevisionFilesComponent { } /// - pub fn update(&mut self, ev: AsyncNotification) { + pub fn update(&mut self, ev: AsyncNotification) -> Result<()> { self.current_file.update(ev); + + if matches!( + ev, + AsyncNotification::Git(AsyncGitNotification::TreeFiles) + ) { + if let Some(last) = self.async_treefiles.take_last() { + if let Some(Ok(last)) = last.result() { + let filenames: Vec<&Path> = last + .iter() + .map(|f| f.path.as_path()) + .collect(); + self.tree = + FileTree::new(&filenames, &BTreeSet::new())?; + self.tree.collapse_but_root(); + + self.files = Some(last); + } + } + } + + Ok(()) } /// pub fn any_work_pending(&self) -> bool { self.current_file.any_work_pending() + || self.async_treefiles.is_pending() } fn tree_item_to_span<'a>( @@ -190,8 +220,9 @@ impl RevisionFilesComponent { } fn open_finder(&self) { - self.queue - .push(InternalEvent::OpenFileFinder(self.files.clone())); + self.queue.push(InternalEvent::OpenFileFinder( + self.files.clone().unwrap_or_default(), + )); } pub fn find_file(&mut self, file: &Option) { @@ -221,18 +252,20 @@ impl RevisionFilesComponent { fn selection_changed(&mut self) { //TODO: retrieve TreeFile from tree datastructure if let Some(file) = self.selected_file_path_with_prefix() { - let path = Path::new(&file); - if let Some(item) = - self.files.iter().find(|f| f.path == path) - { - if let Ok(path) = path.strip_prefix("./") { - return self.current_file.load_file( - path.to_string_lossy().to_string(), - item, - ); + if let Some(files) = &self.files { + let path = Path::new(&file); + if let Some(item) = + files.iter().find(|f| f.path == path) + { + if let Ok(path) = path.strip_prefix("./") { + return self.current_file.load_file( + path.to_string_lossy().to_string(), + item, + ); + } } + self.current_file.clear(); } - self.current_file.clear(); } } @@ -268,18 +301,30 @@ impl RevisionFilesComponent { let is_tree_focused = matches!(self.focus, Focus::Tree); let title = self.title_within(tree_width); - ui::draw_list_block( - f, - area, - Block::default() - .title(Span::styled( - title, - self.theme.title(is_tree_focused), - )) - .borders(Borders::ALL) - .border_style(self.theme.block(is_tree_focused)), - items, - ); + let block = Block::default() + .title(Span::styled( + title, + self.theme.title(is_tree_focused), + )) + .borders(Borders::ALL) + .border_style(self.theme.block(is_tree_focused)); + + if self.files.is_some() { + ui::draw_list_block(f, area, block, items); + } else { + ui::draw_list_block( + f, + area, + block, + vec![Span::styled( + Cow::from(strings::loading_text( + &self.key_config, + )), + self.theme.text(false, false), + )] + .into_iter(), + ); + } if is_tree_focused { self.scroll.draw(f, area, &self.theme); diff --git a/src/components/revision_files_popup.rs b/src/components/revision_files_popup.rs index 11c153b5..032a7696 100644 --- a/src/components/revision_files_popup.rs +++ b/src/components/revision_files_popup.rs @@ -13,7 +13,10 @@ use crate::{ AsyncAppNotification, AsyncNotification, }; use anyhow::Result; -use asyncgit::sync::{CommitId, RepoPathRef}; +use asyncgit::{ + sync::{CommitId, RepoPathRef}, + AsyncGitNotification, +}; use crossbeam_channel::Sender; use crossterm::event::Event; use tui::{backend::Backend, layout::Rect, widgets::Clear, Frame}; @@ -47,6 +50,7 @@ impl RevisionFilesPopup { repo: RepoPathRef, queue: &Queue, sender: &Sender, + sender_git: Sender, theme: SharedTheme, key_config: SharedKeyConfig, ) -> Self { @@ -55,6 +59,7 @@ impl RevisionFilesPopup { repo, queue, sender, + sender_git, theme, key_config.clone(), ), @@ -75,8 +80,8 @@ impl RevisionFilesPopup { } /// - pub fn update(&mut self, ev: AsyncNotification) { - self.files.update(ev); + pub fn update(&mut self, ev: AsyncNotification) -> Result<()> { + self.files.update(ev) } /// diff --git a/src/tabs/files.rs b/src/tabs/files.rs index c994b213..dbc194dd 100644 --- a/src/tabs/files.rs +++ b/src/tabs/files.rs @@ -11,7 +11,10 @@ use crate::{ AsyncAppNotification, AsyncNotification, }; use anyhow::Result; -use asyncgit::sync::{self, RepoPathRef}; +use asyncgit::{ + sync::{self, RepoPathRef}, + AsyncGitNotification, +}; use crossbeam_channel::Sender; pub struct FilesTab { @@ -25,6 +28,7 @@ impl FilesTab { pub fn new( repo: RepoPathRef, sender: &Sender, + sender_git: Sender, queue: &Queue, theme: SharedTheme, key_config: SharedKeyConfig, @@ -35,6 +39,7 @@ impl FilesTab { repo.clone(), queue, sender, + sender_git, theme, key_config, ), @@ -59,10 +64,15 @@ impl FilesTab { } /// - pub fn update_async(&mut self, ev: AsyncNotification) { + pub fn update_async( + &mut self, + ev: AsyncNotification, + ) -> Result<()> { if self.is_visible() { - self.files.update(ev); + self.files.update(ev)?; } + + Ok(()) } pub fn file_finder_update(&mut self, file: &Option) {