From 3cd218de579fc36f09d44526ce2ff2ee622c0ea2 Mon Sep 17 00:00:00 2001 From: Stephan Dilly Date: Tue, 1 Jun 2021 09:56:55 +0200 Subject: [PATCH] honor showUntrackedFiles config (#753) --- CHANGELOG.md | 3 + asyncgit/src/status.rs | 29 ++------ asyncgit/src/sync/config.rs | 114 +++++++++++++++++++++++++++++++ asyncgit/src/sync/diff.rs | 4 +- asyncgit/src/sync/mod.rs | 11 +-- asyncgit/src/sync/reset.rs | 4 +- asyncgit/src/sync/status.rs | 28 +++++--- asyncgit/src/sync/utils.rs | 91 ++++++++++++------------ src/components/commit.rs | 3 +- src/components/externaleditor.rs | 3 +- src/tabs/stashing.rs | 16 +++-- src/tabs/status.rs | 8 +-- 12 files changed, 214 insertions(+), 100 deletions(-) create mode 100644 asyncgit/src/sync/config.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 61b86cab..3e7aa831 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## Added +- honor `config.showUntrackedFiles` improving speed with a lot of untracked items ([#752](https://github.com/extrawurst/gitui/issues/752)) + ## Fixed - wrong file with same name shown in file tree ([#748](https://github.com/extrawurst/gitui/issues/748)) diff --git a/asyncgit/src/status.rs b/asyncgit/src/status.rs index c5441d4d..811c4533 100644 --- a/asyncgit/src/status.rs +++ b/asyncgit/src/status.rs @@ -31,19 +31,14 @@ pub struct Status { pub struct StatusParams { tick: u128, status_type: StatusType, - include_untracked: bool, } impl StatusParams { /// - pub fn new( - status_type: StatusType, - include_untracked: bool, - ) -> Self { + pub fn new(status_type: StatusType) -> Self { Self { tick: current_tick(), status_type, - include_untracked, } } } @@ -93,10 +88,9 @@ impl AsyncStatus { let hash_request = hash(¶ms); log::trace!( - "request: [hash: {}] (type: {:?}, untracked: {})", + "request: [hash: {}] (type: {:?})", hash_request, params.status_type, - params.include_untracked, ); { @@ -115,14 +109,12 @@ impl AsyncStatus { let sender = self.sender.clone(); let arc_pending = Arc::clone(&self.pending); let status_type = params.status_type; - let include_untracked = params.include_untracked; self.pending.fetch_add(1, Ordering::Relaxed); rayon_core::spawn(move || { let ok = Self::fetch_helper( status_type, - include_untracked, hash_request, &arc_current, &arc_last, @@ -143,17 +135,15 @@ impl AsyncStatus { fn fetch_helper( status_type: StatusType, - include_untracked: bool, hash_request: u64, arc_current: &Arc>>, arc_last: &Arc>, ) -> Result<()> { - let res = Self::get_status(status_type, include_untracked)?; + let res = Self::get_status(status_type)?; log::trace!( - "status fetched: {} (type: {:?}, untracked: {})", + "status fetched: {} (type: {:?})", hash_request, status_type, - include_untracked ); { @@ -171,16 +161,9 @@ impl AsyncStatus { Ok(()) } - fn get_status( - status_type: StatusType, - include_untracked: bool, - ) -> Result { + fn get_status(status_type: StatusType) -> Result { Ok(Status { - items: sync::status::get_status( - CWD, - status_type, - include_untracked, - )?, + items: sync::status::get_status(CWD, status_type)?, }) } } diff --git a/asyncgit/src/sync/config.rs b/asyncgit/src/sync/config.rs new file mode 100644 index 00000000..dc6369bc --- /dev/null +++ b/asyncgit/src/sync/config.rs @@ -0,0 +1,114 @@ +use super::utils::repo; +use crate::error::Result; +use git2::Repository; +use scopetime::scope_time; + +// see https://git-scm.com/docs/git-config#Documentation/git-config.txt-statusshowUntrackedFiles +/// represents the `status.showUntrackedFiles` git config state +pub enum ShowUntrackedFilesConfig { + /// + No, + /// + Normal, + /// + All, +} + +impl ShowUntrackedFilesConfig { + /// + pub const fn include_none(&self) -> bool { + matches!(self, Self::No) + } + + /// + pub const fn include_untracked(&self) -> bool { + matches!(self, Self::Normal | Self::All) + } + + /// + pub const fn recurse_untracked_dirs(&self) -> bool { + matches!(self, Self::All) + } +} + +pub fn untracked_files_config_repo( + repo: &Repository, +) -> Result { + let show_untracked_files = + get_config_string_repo(repo, "status.showUntrackedFiles")?; + + if let Some(show_untracked_files) = show_untracked_files { + if &show_untracked_files == "no" { + return Ok(ShowUntrackedFilesConfig::No); + } else if &show_untracked_files == "normal" { + return Ok(ShowUntrackedFilesConfig::Normal); + } + } + + Ok(ShowUntrackedFilesConfig::All) +} + +/// +pub fn untracked_files_config( + repo_path: &str, +) -> Result { + let repo = repo(repo_path)?; + untracked_files_config_repo(&repo) +} + +/// get string from config +pub fn get_config_string( + repo_path: &str, + key: &str, +) -> Result> { + let repo = repo(repo_path)?; + get_config_string_repo(&repo, key) +} + +pub fn get_config_string_repo( + repo: &Repository, + key: &str, +) -> Result> { + scope_time!("get_config_string_repo"); + + let cfg = repo.config()?; + + // this code doesnt match what the doc says regarding what + // gets returned when but it actually works + let entry_res = cfg.get_entry(key); + + let entry = match entry_res { + Ok(ent) => ent, + Err(_) => return Ok(None), + }; + + if entry.has_value() { + Ok(entry.value().map(std::string::ToString::to_string)) + } else { + Ok(None) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::sync::tests::repo_init; + + #[test] + fn test_get_config() { + let bad_dir_cfg = + get_config_string("oodly_noodly", "this.doesnt.exist"); + assert!(bad_dir_cfg.is_err()); + + let (_td, repo) = repo_init().unwrap(); + let path = repo.path(); + let rpath = path.as_os_str().to_str().unwrap(); + let bad_cfg = get_config_string(rpath, "this.doesnt.exist"); + assert!(bad_cfg.is_ok()); + assert!(bad_cfg.unwrap().is_none()); + // repo init sets user.name + let good_cfg = get_config_string(rpath, "user.name"); + assert!(good_cfg.is_ok()); + assert!(good_cfg.unwrap().is_some()); + } +} diff --git a/asyncgit/src/sync/diff.rs b/asyncgit/src/sync/diff.rs index 1fbbd937..7fe3242a 100644 --- a/asyncgit/src/sync/diff.rs +++ b/asyncgit/src/sync/diff.rs @@ -459,8 +459,8 @@ mod tests { .unwrap(); } - let res = get_status(repo_path, StatusType::WorkingDir, true) - .unwrap(); + let res = + get_status(repo_path, StatusType::WorkingDir).unwrap(); assert_eq!(res.len(), 1); assert_eq!(res[0].path, "bar.txt"); diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index ebeb7509..59a60598 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -9,6 +9,7 @@ mod commit; mod commit_details; mod commit_files; mod commits_info; +mod config; pub mod cred; pub mod diff; mod hooks; @@ -44,6 +45,10 @@ pub use commit_files::get_commit_files; pub use commits_info::{ get_commit_info, get_commits_info, CommitId, CommitInfo, }; +pub use config::{ + get_config_string, untracked_files_config, + ShowUntrackedFilesConfig, +}; pub use diff::get_diff_commit; pub use hooks::{ hooks_commit_msg, hooks_post_commit, hooks_pre_commit, HookResult, @@ -250,12 +255,10 @@ mod tests { /// helper returning amount of files with changes in the (wd,stage) pub fn get_statuses(repo_path: &str) -> (usize, usize) { ( - get_status(repo_path, StatusType::WorkingDir, true) - .unwrap() - .len(), - get_status(repo_path, StatusType::Stage, true) + get_status(repo_path, StatusType::WorkingDir) .unwrap() .len(), + get_status(repo_path, StatusType::Stage).unwrap().len(), ) } diff --git a/asyncgit/src/sync/reset.rs b/asyncgit/src/sync/reset.rs index 93c6eb05..1d414ea0 100644 --- a/asyncgit/src/sync/reset.rs +++ b/asyncgit/src/sync/reset.rs @@ -88,8 +88,8 @@ mod tests { let root = repo.path().parent().unwrap(); let repo_path = root.as_os_str().to_str().unwrap(); - let res = get_status(repo_path, StatusType::WorkingDir, true) - .unwrap(); + let res = + get_status(repo_path, StatusType::WorkingDir).unwrap(); assert_eq!(res.len(), 0); let file_path = root.join("bar.txt"); diff --git a/asyncgit/src/sync/status.rs b/asyncgit/src/sync/status.rs index 328a91a1..077c8e00 100644 --- a/asyncgit/src/sync/status.rs +++ b/asyncgit/src/sync/status.rs @@ -1,6 +1,10 @@ //! sync git api for fetching a status -use crate::{error::Error, error::Result, sync::utils}; +use crate::{ + error::Error, + error::Result, + sync::{config::untracked_files_config_repo, utils}, +}; use git2::{Delta, Status, StatusOptions, StatusShow}; use scopetime::scope_time; use std::path::Path; @@ -92,20 +96,24 @@ impl From for StatusShow { pub fn get_status( repo_path: &str, status_type: StatusType, - include_untracked: bool, ) -> Result> { scope_time!("get_status"); let repo = utils::repo(repo_path)?; - let statuses = repo.statuses(Some( - StatusOptions::default() - .show(status_type.into()) - .update_index(true) - .include_untracked(include_untracked) - .renames_head_to_index(true) - .recurse_untracked_dirs(true), - ))?; + let show_untracked = untracked_files_config_repo(&repo)?; + + let mut options = StatusOptions::default(); + options + .show(status_type.into()) + .update_index(true) + .include_untracked(show_untracked.include_untracked()) + .renames_head_to_index(true) + .recurse_untracked_dirs( + show_untracked.recurse_untracked_dirs(), + ); + + let statuses = repo.statuses(Some(&mut options))?; let mut res = Vec::with_capacity(statuses.len()); diff --git a/asyncgit/src/sync/utils.rs b/asyncgit/src/sync/utils.rs index 17a75073..e1c245eb 100644 --- a/asyncgit/src/sync/utils.rs +++ b/asyncgit/src/sync/utils.rs @@ -1,7 +1,10 @@ //! sync git api (various methods) use super::CommitId; -use crate::error::{Error, Result}; +use crate::{ + error::{Error, Result}, + sync::config::untracked_files_config_repo, +}; use git2::{IndexAddOption, Repository, RepositoryOpenFlags}; use scopetime::scope_time; use std::{ @@ -129,7 +132,18 @@ pub fn stage_add_all(repo_path: &str, pattern: &str) -> Result<()> { let mut index = repo.index()?; - index.add_all(vec![pattern], IndexAddOption::DEFAULT, None)?; + let config = untracked_files_config_repo(&repo)?; + + if config.include_none() { + index.update_all(vec![pattern], None)?; + } else { + index.add_all( + vec![pattern], + IndexAddOption::DEFAULT, + None, + )?; + } + index.write()?; Ok(()) @@ -149,30 +163,6 @@ pub fn stage_addremoved(repo_path: &str, path: &Path) -> Result<()> { Ok(()) } -/// get string from config -pub fn get_config_string( - repo_path: &str, - key: &str, -) -> Result> { - let repo = repo(repo_path)?; - let cfg = repo.config()?; - - // this code doesnt match what the doc says regarding what - // gets returned when but it actually works - let entry_res = cfg.get_entry(key); - - let entry = match entry_res { - Ok(ent) => ent, - Err(_) => return Ok(None), - }; - - if entry.has_value() { - Ok(entry.value().map(std::string::ToString::to_string)) - } else { - Ok(None) - } -} - pub(crate) fn bytes2string(bytes: &[u8]) -> Result { Ok(String::from_utf8(bytes.to_vec())?) } @@ -239,23 +229,7 @@ mod tests { false ); } - #[test] - fn test_get_config() { - let bad_dir_cfg = - get_config_string("oodly_noodly", "this.doesnt.exist"); - assert!(bad_dir_cfg.is_err()); - let (_td, repo) = repo_init().unwrap(); - let path = repo.path(); - let rpath = path.as_os_str().to_str().unwrap(); - let bad_cfg = get_config_string(rpath, "this.doesnt.exist"); - assert!(bad_cfg.is_ok()); - assert!(bad_cfg.unwrap().is_none()); - // repo init sets user.name - let good_cfg = get_config_string(rpath, "user.name"); - assert!(good_cfg.is_ok()); - assert!(good_cfg.unwrap().is_some()); - } #[test] fn test_staging_one_file() { let file_path = Path::new("file1.txt"); @@ -287,7 +261,7 @@ mod tests { let repo_path = root.as_os_str().to_str().unwrap(); let status_count = |s: StatusType| -> usize { - get_status(repo_path, s, true).unwrap().len() + get_status(repo_path, s).unwrap().len() }; fs::create_dir_all(&root.join("a/d"))?; @@ -308,6 +282,33 @@ mod tests { Ok(()) } + #[test] + fn test_not_staging_untracked_folder() -> Result<()> { + let (_td, repo) = repo_init().unwrap(); + let root = repo.path().parent().unwrap(); + let repo_path = root.as_os_str().to_str().unwrap(); + + fs::create_dir_all(&root.join("a/d"))?; + File::create(&root.join(Path::new("a/d/f1.txt")))? + .write_all(b"foo")?; + File::create(&root.join(Path::new("a/d/f2.txt")))? + .write_all(b"foo")?; + File::create(&root.join(Path::new("f3.txt")))? + .write_all(b"foo")?; + + assert_eq!(get_statuses(repo_path), (3, 0)); + + repo.config()?.set_str("status.showUntrackedFiles", "no")?; + + assert_eq!(get_statuses(repo_path), (0, 0)); + + stage_add_all(repo_path, "*").unwrap(); + + assert_eq!(get_statuses(repo_path), (0, 0)); + + Ok(()) + } + #[test] fn test_staging_deleted_file() { let file_path = Path::new("file1.txt"); @@ -316,7 +317,7 @@ mod tests { let repo_path = root.as_os_str().to_str().unwrap(); let status_count = |s: StatusType| -> usize { - get_status(repo_path, s, true).unwrap().len() + get_status(repo_path, s).unwrap().len() }; let full_path = &root.join(file_path); @@ -350,7 +351,7 @@ mod tests { let repo_path = root.as_os_str().to_str().unwrap(); let status_count = |s: StatusType| -> usize { - get_status(repo_path, s, true).unwrap().len() + get_status(repo_path, s).unwrap().len() }; let sub = &root.join("sub"); diff --git a/src/components/commit.rs b/src/components/commit.rs index 8498a1fc..08219d60 100644 --- a/src/components/commit.rs +++ b/src/components/commit.rs @@ -13,8 +13,7 @@ use anyhow::Result; use asyncgit::{ cached, sync::{ - self, utils::get_config_string, CommitId, HookResult, - RepoState, + self, get_config_string, CommitId, HookResult, RepoState, }, CWD, }; diff --git a/src/components/externaleditor.rs b/src/components/externaleditor.rs index 58a112b2..00e2a564 100644 --- a/src/components/externaleditor.rs +++ b/src/components/externaleditor.rs @@ -9,7 +9,8 @@ use crate::{ }; use anyhow::{anyhow, bail, Result}; use asyncgit::{ - sync::utils::get_config_string, sync::utils::repo_work_dir, CWD, + sync::{get_config_string, utils::repo_work_dir}, + CWD, }; use crossterm::{ event::Event, diff --git a/src/tabs/stashing.rs b/src/tabs/stashing.rs index d28879bb..97b0647b 100644 --- a/src/tabs/stashing.rs +++ b/src/tabs/stashing.rs @@ -12,8 +12,8 @@ use crate::{ }; use anyhow::Result; use asyncgit::{ - sync::status::StatusType, AsyncNotification, AsyncStatus, - StatusParams, + sync::{self, status::StatusType}, + AsyncNotification, AsyncStatus, StatusParams, CWD, }; use crossbeam_channel::Sender; use crossterm::event::Event; @@ -73,10 +73,8 @@ impl Stashing { /// pub fn update(&mut self) -> Result<()> { if self.is_visible() { - self.git_status.fetch(&StatusParams::new( - StatusType::Both, - self.options.stash_untracked, - ))?; + self.git_status + .fetch(&StatusParams::new(StatusType::Both))?; } Ok(()) @@ -258,6 +256,12 @@ impl Component for Stashing { } fn show(&mut self) -> Result<()> { + let config_untracked_files = + sync::untracked_files_config(CWD)?; + + self.options.stash_untracked = + !config_untracked_files.include_none(); + self.visible = true; self.update()?; Ok(()) diff --git a/src/tabs/status.rs b/src/tabs/status.rs index baa31bdf..068a3f02 100644 --- a/src/tabs/status.rs +++ b/src/tabs/status.rs @@ -318,12 +318,10 @@ impl Status { if self.is_visible() { self.git_diff.refresh()?; - self.git_status_workdir.fetch(&StatusParams::new( - StatusType::WorkingDir, - true, - ))?; + self.git_status_workdir + .fetch(&StatusParams::new(StatusType::WorkingDir))?; self.git_status_stage - .fetch(&StatusParams::new(StatusType::Stage, true))?; + .fetch(&StatusParams::new(StatusType::Stage))?; self.branch_compare(); }