Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-vcs/src/git.rs')
| -rw-r--r-- | helix-vcs/src/git.rs | 98 |
1 files changed, 94 insertions, 4 deletions
diff --git a/helix-vcs/src/git.rs b/helix-vcs/src/git.rs index 995bade0..8d935b5f 100644 --- a/helix-vcs/src/git.rs +++ b/helix-vcs/src/git.rs @@ -5,15 +5,24 @@ use std::io::Read; use std::path::Path; use std::sync::Arc; +use gix::bstr::ByteSlice; +use gix::diff::Rewrites; +use gix::dir::entry::Status; use gix::objs::tree::EntryKind; use gix::sec::trust::DefaultForLevel; +use gix::status::{ + index_worktree::iter::Item, + plumbing::index_as_worktree::{Change, EntryStatus}, + UntrackedFiles, +}; use gix::{Commit, ObjectId, Repository, ThreadSafeRepository}; -use crate::DiffProvider; +use crate::{DiffProvider, FileChange}; #[cfg(test)] mod test; +#[derive(Clone, Copy)] pub struct Git; impl Git { @@ -61,10 +70,77 @@ impl Git { Ok(res) } + + /// Emulates the result of running `git status` from the command line. + fn status(repo: &Repository, f: impl Fn(Result<FileChange>) -> bool) -> Result<()> { + let work_dir = repo + .work_dir() + .ok_or_else(|| anyhow::anyhow!("working tree not found"))? + .to_path_buf(); + + let status_platform = repo + .status(gix::progress::Discard)? + // Here we discard the `status.showUntrackedFiles` config, as it makes little sense in + // our case to not list new (untracked) files. We could have respected this config + // if the default value weren't `Collapsed` though, as this default value would render + // the feature unusable to many. + .untracked_files(UntrackedFiles::Files) + // Turn on file rename detection, which is off by default. + .index_worktree_rewrites(Some(Rewrites { + copies: None, + percentage: Some(0.5), + limit: 1000, + })); + + // No filtering based on path + let empty_patterns = vec![]; + + let status_iter = status_platform.into_index_worktree_iter(empty_patterns)?; + + for item in status_iter { + let Ok(item) = item.map_err(|err| f(Err(err.into()))) else { + continue; + }; + let change = match item { + Item::Modification { + rela_path, status, .. + } => { + let path = work_dir.join(rela_path.to_path()?); + match status { + EntryStatus::Conflict(_) => FileChange::Conflict { path }, + EntryStatus::Change(Change::Removed) => FileChange::Deleted { path }, + EntryStatus::Change(Change::Modification { .. }) => { + FileChange::Modified { path } + } + _ => continue, + } + } + Item::DirectoryContents { entry, .. } if entry.status == Status::Untracked => { + FileChange::Untracked { + path: work_dir.join(entry.rela_path.to_path()?), + } + } + Item::Rewrite { + source, + dirwalk_entry, + .. + } => FileChange::Renamed { + from_path: work_dir.join(source.rela_path().to_path()?), + to_path: work_dir.join(dirwalk_entry.rela_path.to_path()?), + }, + _ => continue, + }; + if !f(Ok(change)) { + break; + } + } + + Ok(()) + } } -impl DiffProvider for Git { - fn get_diff_base(&self, file: &Path) -> Result<Vec<u8>> { +impl Git { + pub fn get_diff_base(&self, file: &Path) -> Result<Vec<u8>> { debug_assert!(!file.exists() || file.is_file()); debug_assert!(file.is_absolute()); @@ -95,7 +171,7 @@ impl DiffProvider for Git { } } - fn get_current_head_name(&self, file: &Path) -> Result<Arc<ArcSwap<Box<str>>>> { + pub fn get_current_head_name(&self, file: &Path) -> Result<Arc<ArcSwap<Box<str>>>> { debug_assert!(!file.exists() || file.is_file()); debug_assert!(file.is_absolute()); let repo_dir = file.parent().context("file has no parent directory")?; @@ -112,6 +188,20 @@ impl DiffProvider for Git { Ok(Arc::new(ArcSwap::from_pointee(name.into_boxed_str()))) } + + pub fn for_each_changed_file( + &self, + cwd: &Path, + f: impl Fn(Result<FileChange>) -> bool, + ) -> Result<()> { + Self::status(&Self::open_repo(cwd, None)?.to_thread_local(), f) + } +} + +impl From<Git> for DiffProvider { + fn from(value: Git) -> Self { + DiffProvider::Git(value) + } } /// Finds the object that contains the contents of a file at a specific commit. |