Compare commits

...

1 Commits

Author SHA1 Message Date
Dmitriy Kovalenko fc52353506 feat: Fix error on FFFGitRefresh
closes https://github.com/dmtrKovalenko/fff.nvim/issues/88
2025-08-10 23:50:09 +02:00
5 changed files with 71 additions and 49 deletions
+2 -2
View File
@@ -284,9 +284,9 @@ end
--- Refresh git status for the active file lock
function M.refresh_git_status()
local ok, files = pcall(fuzzy.refresh_git_status)
local ok, updated_files_count = pcall(fuzzy.refresh_git_status)
if ok then
print('Refreshed git status for ' .. #files .. ' files')
vim.notify('Refreshed git status for ' .. tostring(updated_files_count) .. ' files', vim.log.levels.INFO)
else
vim.notify('Failed to refresh git status', vim.log.levels.ERROR)
end
+24 -25
View File
@@ -111,7 +111,7 @@ fn handle_debounced_events(events: Vec<DebouncedEvent>, git_workdir: &Option<Pat
return trigger_full_rescan(picker);
}
if is_git_status_change(path, git_workdir.as_ref()) {
if is_dotgit_change_affecting_status(path, &repo) {
need_full_git_rescan = true;
}
@@ -178,36 +178,35 @@ fn is_git_file(path: &Path) -> bool {
.any(|component| component.as_os_str() == ".git")
}
fn is_git_status_change(path: &Path, git_workdir: Option<&PathBuf>) -> bool {
let Some(git_workdir) = git_workdir else {
pub fn is_dotgit_change_affecting_status(changed: &Path, repo: &Option<Repository>) -> bool {
let Some(repo) = repo.as_ref() else {
return false;
};
if let Ok(relative) = path.strip_prefix(git_workdir) {
let components: Vec<_> = relative.components().collect();
if components.is_empty() || components[0].as_os_str() != ".git" {
let git_dir = repo.path();
if let Ok(rel) = changed.strip_prefix(git_dir) {
if rel.starts_with("objects") || rel.starts_with("logs") || rel.starts_with("hooks") {
return false;
}
if rel == Path::new("index") || rel == Path::new("index.lock") {
return true;
}
if rel == Path::new("HEAD") {
return true;
}
if rel.starts_with("refs") || rel == Path::new("packed-refs") {
return true;
}
if rel == Path::new("info/exclude") || rel == Path::new("info/sparse-checkout") {
return true;
}
let file_name = relative.file_name().and_then(|f| f.to_str());
let is_critical_file = matches!(
file_name,
Some(
"index"
| "HEAD"
| "COMMIT_EDITMSG"
| "MERGE_HEAD"
| "CHERRY_PICK_HEAD"
| "index.lock"
)
);
let is_refs_change = components.len() >= 2 && components[1].as_os_str() == "refs";
let is_branch_ref = components.len() >= 3
&& components[1].as_os_str() == "refs"
&& components[2].as_os_str() == "heads";
return is_critical_file || is_refs_change || is_branch_ref;
if let Some(fname) = rel.file_name().and_then(|f| f.to_str()) {
if matches!(fname, "MERGE_HEAD" | "CHERRY_PICK_HEAD" | "REVERT_HEAD") {
return true;
}
}
}
false
+38 -12
View File
@@ -5,7 +5,7 @@ use crate::frecency::FrecencyTracker;
use crate::git::{format_git_status, GitStatusCache};
use crate::score::match_and_score_files;
use crate::types::{FileItem, ScoringContext, SearchResult};
use git2::{Repository, Status};
use git2::{Repository, Status, StatusOptions};
use rayon::prelude::*;
use std::path::{Path, PathBuf};
use std::sync::{
@@ -108,7 +108,6 @@ impl From<&FileItem> for FileKey {
pub struct FilePicker {
base_path: PathBuf,
git_workdir: Option<PathBuf>,
sync_data: FileSync,
is_scanning: Arc<AtomicBool>,
scanned_files_count: Arc<AtomicUsize>,
@@ -119,14 +118,19 @@ impl std::fmt::Debug for FilePicker {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FilePicker")
.field("base_path", &self.base_path)
.field("git_workdir", &self.git_workdir)
.field("sync_data", &self.sync_data)
.field("is_scanning", &self.is_scanning.load(Ordering::Relaxed))
.field(
"scanned_files_count",
&self.scanned_files_count.load(Ordering::Relaxed),
)
.finish_non_exhaustive()
}
}
impl FilePicker {
pub fn git_root(&self) -> Option<&Path> {
self.git_workdir.as_deref()
self.sync_data.git_workdir.as_deref()
}
pub fn get_files(&self) -> &[FileItem] {
@@ -146,7 +150,6 @@ impl FilePicker {
let picker = Self {
base_path: path.clone(),
git_workdir: None,
sync_data: FileSync::new(),
is_scanning: Arc::clone(&scan_signal),
scanned_files_count: Arc::clone(&synced_files_count),
@@ -225,15 +228,13 @@ impl FilePicker {
debug!(
statuses_count = status_cache.statuses_len(),
"GIT STATUS UPDATE WHAT THE"
"Updating git status",
);
let frecency = FRECENCY.read().map_err(|_| Error::AcquireFrecencyLock)?;
status_cache
.into_iter()
.try_for_each(|(path, status)| -> Result<(), Error> {
debug!(?path, ?status, "Updating git status for file");
if let Some(file) = self.get_mut_file_by_path(&path) {
file.git_status = Some(status);
@@ -250,14 +251,29 @@ impl FilePicker {
/// Fetches all the git statuses first and updates the global FILE_PICKER
/// with the new statuses with the smallest possible lock time.
pub fn refresh_git_status_global() -> Result<(), Error> {
pub fn refresh_git_status_global() -> Result<usize, Error> {
let git_status = {
let Some(ref picker) = *FILE_PICKER.read().map_err(|_| Error::AcquireItemLock)? else {
return Err(Error::FilePickerMissing)?;
};
debug!(
"Refreshing git statuses for picker: {:?}",
picker.git_root()
);
// we keep here readonly lock but allowing querying the index while it scan lasts
GitStatusCache::read_git_status(picker.git_root())
GitStatusCache::read_git_status(
picker.git_root(),
StatusOptions::new()
.include_untracked(true)
.recurse_untracked_dirs(true)
// when manually refreshing git status we want to include all unmodified file
// to make sure that their status is correctly updated when user
// commited/stashed/removed changes
.include_unmodified(true)
.exclude_submodules(true),
)
};
let mut file_picker = FILE_PICKER.write().map_err(|_| Error::AcquireItemLock)?;
@@ -265,8 +281,10 @@ impl FilePicker {
.as_mut()
.ok_or_else(|| Error::FilePickerMissing)?;
let statuses_count = git_status.as_ref().map_or(0, |cache| cache.statuses_len());
picker.update_git_statuses(git_status)?;
Ok(())
Ok(statuses_count)
}
pub fn update_single_file_frecency(
@@ -491,7 +509,15 @@ fn scan_filesystem(
debug!("No git repository found for path: {}", base_path.display());
}
let status_cache = GitStatusCache::read_git_status(git_workdir.as_deref());
let status_cache = GitStatusCache::read_git_status(
git_workdir.as_deref(),
// do not include unmodified here to avoid extra cost
// we are treating all missing files as unmodified
StatusOptions::new()
.include_untracked(true)
.recurse_untracked_dirs(true)
.exclude_submodules(true),
);
(git_workdir, status_cache)
});
+5 -7
View File
@@ -57,16 +57,14 @@ impl GitStatusCache {
Some(Self(entries))
}
pub fn read_git_status(git_workdir: Option<&Path>) -> Option<Self> {
pub fn read_git_status(
git_workdir: Option<&Path>,
status_options: &mut StatusOptions,
) -> Option<Self> {
let git_workdir = git_workdir.as_ref()?;
let repository = Repository::open(git_workdir).ok()?;
Self::read_status_impl(
&repository,
StatusOptions::new()
.include_untracked(true)
.recurse_untracked_dirs(true),
)
Self::read_status_impl(&repository, status_options)
}
pub fn git_status_for_paths<TPath: AsRef<Path> + Debug>(
+2 -3
View File
@@ -150,9 +150,8 @@ pub fn is_scanning(_: &Lua, _: ()) -> LuaResult<bool> {
Ok(picker.is_scan_active())
}
pub fn refresh_git_status(_: &Lua, _: ()) -> LuaResult<()> {
FilePicker::refresh_git_status_global()?;
Ok(())
pub fn refresh_git_status(_: &Lua, _: ()) -> LuaResult<usize> {
FilePicker::refresh_git_status_global().map_err(Into::into)
}
pub fn update_single_file_frecency(_: &Lua, file_path: String) -> LuaResult<bool> {