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 | 267 |
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()), } } |