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.rs267
1 files changed, 80 insertions, 187 deletions
diff --git a/helix-vcs/src/git.rs b/helix-vcs/src/git.rs
index 6544c35c..432159b6 100644
--- a/helix-vcs/src/git.rs
+++ b/helix-vcs/src/git.rs
@@ -1,212 +1,105 @@
-use anyhow::{bail, Context, Result};
-use arc_swap::ArcSwap;
-use gix::filter::plumbing::driver::apply::Delay;
-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::Item,
- plumbing::index_as_worktree::{Change, EntryStatus},
- UntrackedFiles,
-};
-use gix::{Commit, ObjectId, Repository, ThreadSafeRepository};
+use git::objs::tree::EntryMode;
+use git::sec::trust::DefaultForLevel;
+use git::{Commit, ObjectId, Repository, ThreadSafeRepository};
+use git_repository as git;
-use crate::FileChange;
+use crate::DiffProvider;
#[cfg(test)]
mod test;
-#[inline]
-fn get_repo_dir(file: &Path) -> Result<&Path> {
- file.parent().context("file has no parent directory")
-}
-
-pub fn get_diff_base(file: &Path) -> Result<Vec<u8>> {
- debug_assert!(!file.exists() || file.is_file());
- debug_assert!(file.is_absolute());
- let file = gix::path::realpath(file).context("resolve symlinks")?;
-
- // TODO cache repository lookup
-
- let repo_dir = get_repo_dir(&file)?;
- let repo = open_repo(repo_dir)
- .context("failed to open git repo")?
- .to_thread_local();
- let head = repo.head_commit()?;
- let file_oid = find_file_in_commit(&repo, &head, &file)?;
-
- let file_object = repo.find_object(file_oid)?;
- let data = file_object.detach().data;
- // Get the actual data that git would make out of the git object.
- // This will apply the user's git config or attributes like crlf conversions.
- if let Some(work_dir) = repo.workdir() {
- let rela_path = file.strip_prefix(work_dir)?;
- let rela_path = gix::path::try_into_bstr(rela_path)?;
- let (mut pipeline, _) = repo.filter_pipeline(None)?;
- let mut worktree_outcome =
- pipeline.convert_to_worktree(&data, rela_path.as_ref(), Delay::Forbid)?;
- let mut buf = Vec::with_capacity(data.len());
- worktree_outcome.read_to_end(&mut buf)?;
- Ok(buf)
- } else {
- Ok(data)
- }
-}
-
-pub fn get_current_head_name(file: &Path) -> Result<Arc<ArcSwap<Box<str>>>> {
- debug_assert!(!file.exists() || file.is_file());
- debug_assert!(file.is_absolute());
- let file = gix::path::realpath(file).context("resolve symlinks")?;
-
- let repo_dir = get_repo_dir(&file)?;
- let repo = open_repo(repo_dir)
- .context("failed to open git repo")?
- .to_thread_local();
- let head_ref = repo.head_ref()?;
- let head_commit = repo.head_commit()?;
-
- let name = match head_ref {
- Some(reference) => reference.name().shorten().to_string(),
- None => head_commit.id.to_hex_with_len(8).to_string(),
- };
-
- Ok(Arc::new(ArcSwap::from_pointee(name.into_boxed_str())))
-}
-
-pub fn for_each_changed_file(cwd: &Path, f: impl Fn(Result<FileChange>) -> bool) -> Result<()> {
- status(&open_repo(cwd)?.to_thread_local(), f)
-}
-
-fn open_repo(path: &Path) -> Result<ThreadSafeRepository> {
- // custom open options
- let mut git_open_opts_map = gix::sec::trust::Mapping::<gix::open::Options>::default();
-
- // On windows various configuration options are bundled as part of the installations
- // This path depends on the install location of git and therefore requires some overhead to lookup
- // This is basically only used on windows and has some overhead hence it's disabled on other platforms.
- // `gitoxide` doesn't use this as default
- let config = gix::open::permissions::Config {
- system: true,
- git: true,
- user: true,
- env: true,
- includes: true,
- git_binary: cfg!(windows),
- };
- // change options for config permissions without touching anything else
- git_open_opts_map.reduced = git_open_opts_map
- .reduced
- .permissions(gix::open::Permissions {
+pub struct Git;
+
+impl Git {
+ fn open_repo(path: &Path, ceiling_dir: Option<&Path>) -> Option<ThreadSafeRepository> {
+ // custom open options
+ let mut git_open_opts_map = git::sec::trust::Mapping::<git::open::Options>::default();
+
+ // On windows various configuration options are bundled as part of the installations
+ // This path depends on the install location of git and therefore requires some overhead to lookup
+ // This is basically only used on windows and has some overhead hence it's disabled on other platforms.
+ // `gitoxide` doesn't use this as default
+ let config = git::permissions::Config {
+ system: true,
+ git: true,
+ user: true,
+ env: true,
+ includes: true,
+ git_binary: cfg!(windows),
+ };
+ // change options for config permissions without touching anything else
+ git_open_opts_map.reduced = git_open_opts_map.reduced.permissions(git::Permissions {
config,
- ..gix::open::Permissions::default_for_level(gix::sec::Trust::Reduced)
+ ..git::Permissions::default_for_level(git::sec::Trust::Reduced)
+ });
+ git_open_opts_map.full = git_open_opts_map.full.permissions(git::Permissions {
+ config,
+ ..git::Permissions::default_for_level(git::sec::Trust::Full)
});
- git_open_opts_map.full = git_open_opts_map.full.permissions(gix::open::Permissions {
- config,
- ..gix::open::Permissions::default_for_level(gix::sec::Trust::Full)
- });
-
- let open_options = gix::discover::upwards::Options {
- dot_git_only: true,
- ..Default::default()
- };
- let res = ThreadSafeRepository::discover_with_environment_overrides_opts(
- path,
- open_options,
- git_open_opts_map,
- )?;
+ let mut open_options = git::discover::upwards::Options::default();
+ if let Some(ceiling_dir) = ceiling_dir {
+ open_options.ceiling_dirs = vec![ceiling_dir.to_owned()];
+ }
- Ok(res)
+ ThreadSafeRepository::discover_with_environment_overrides_opts(
+ path,
+ open_options,
+ git_open_opts_map,
+ )
+ .ok()
+ }
}
-/// 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
- .workdir()
- .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,
- ..Default::default()
- }));
-
- // 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 }
+impl DiffProvider for Git {
+ fn get_diff_base(&self, file: &Path) -> Option<Vec<u8>> {
+ debug_assert!(!file.exists() || file.is_file());
+ debug_assert!(file.is_absolute());
+
+ // TODO cache repository lookup
+ let repo = Git::open_repo(file.parent()?, None)?.to_thread_local();
+ let head = repo.head_commit().ok()?;
+ let file_oid = find_file_in_commit(&repo, &head, file)?;
+
+ let file_object = repo.find_object(file_oid).ok()?;
+ let mut data = file_object.detach().data;
+ // convert LF to CRLF if configured to avoid showing every line as changed
+ if repo
+ .config_snapshot()
+ .boolean("core.autocrlf")
+ .unwrap_or(false)
+ {
+ let mut normalized_file = Vec::with_capacity(data.len());
+ let mut at_cr = false;
+ for &byte in &data {
+ if byte == b'\n' {
+ // if this is a LF instead of a CRLF (last byte was not a CR)
+ // insert a new CR to generate a CRLF
+ if !at_cr {
+ normalized_file.push(b'\r');
}
- _ => continue,
- }
- }
- Item::DirectoryContents { entry, .. } if entry.status == Status::Untracked => {
- FileChange::Untracked {
- path: work_dir.join(entry.rela_path.to_path()?),
}
+ at_cr = byte == b'\r';
+ normalized_file.push(byte)
}
- 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;
+ data = normalized_file
}
+ Some(data)
}
-
- Ok(())
}
/// Finds the object that contains the contents of a file at a specific commit.
-fn find_file_in_commit(repo: &Repository, commit: &Commit, file: &Path) -> Result<ObjectId> {
- let repo_dir = repo.workdir().context("repo has no worktree")?;
- let rel_path = file.strip_prefix(repo_dir)?;
- let tree = commit.tree()?;
- let tree_entry = tree
- .lookup_entry_by_path(rel_path)?
- .context("file is untracked")?;
- match tree_entry.mode().kind() {
+fn find_file_in_commit(repo: &Repository, commit: &Commit, file: &Path) -> Option<ObjectId> {
+ let repo_dir = repo.work_dir()?;
+ let rel_path = file.strip_prefix(repo_dir).ok()?;
+ let tree = commit.tree().ok()?;
+ let tree_entry = tree.lookup_entry_by_path(rel_path).ok()??;
+ match tree_entry.mode() {
// not a file, everything is new, do not show diff
- mode @ (EntryKind::Tree | EntryKind::Commit | EntryKind::Link) => {
- bail!("entry at {} is not a file but a {mode:?}", file.display())
- }
+ EntryMode::Tree | EntryMode::Commit | EntryMode::Link => None,
// found a file
- EntryKind::Blob | EntryKind::BlobExecutable => Ok(tree_entry.object_id()),
+ EntryMode::Blob | EntryMode::BlobExecutable => Some(tree_entry.object_id()),
}
}