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.rs98
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.