make fetching tree files async

This commit is contained in:
extrawurst 2022-11-21 19:56:09 +01:00 committed by extrawurst
parent bc15b5d550
commit 8e8c5fad55
7 changed files with 185 additions and 45 deletions

View file

@ -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

View file

@ -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

73
asyncgit/src/treefiles.rs Normal file
View file

@ -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<Vec<TreeFile>>),
}
///
#[derive(Clone, Default)]
pub struct AsyncTreeFilesJob {
state: Arc<Mutex<Option<JobState>>>,
}
///
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<Result<Vec<TreeFile>>> {
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<Self::Notification, Self::Progress>,
) -> Result<Self::Notification> {
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)
}
}

View file

@ -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

View file

@ -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<TreeFile>,
files: Option<Vec<TreeFile>>,
async_treefiles: AsyncSingleJob<AsyncTreeFilesJob>,
current_file: SyntaxTextComponent,
tree: FileTree,
scroll: VerticalScroll,
@ -60,6 +64,7 @@ impl RevisionFilesComponent {
repo: RepoPathRef,
queue: &Queue,
sender: &Sender<AsyncAppNotification>,
sender_git: Sender<AsyncGitNotification>,
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<PathBuf>) {
@ -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);

View file

@ -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<AsyncAppNotification>,
sender_git: Sender<AsyncGitNotification>,
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)
}
///

View file

@ -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<AsyncAppNotification>,
sender_git: Sender<AsyncGitNotification>,
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<PathBuf>) {