diff --git a/gix/src/filter.rs b/gix/src/filter.rs index 7c4f93e6f29..d37308e1cdc 100644 --- a/gix/src/filter.rs +++ b/gix/src/filter.rs @@ -19,7 +19,7 @@ pub mod pipeline { pub mod options { use crate::{bstr::BString, config}; - /// The error returned by [Pipeline::options()][crate::filter::Pipeline::options()]. + /// The error returned by [Pipeline::options()](crate::filter::Pipeline::options()). #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { @@ -39,7 +39,7 @@ pub mod pipeline { /// pub mod convert_to_git { - /// The error returned by [Pipeline::convert_to_git()][crate::filter::Pipeline::convert_to_git()]. + /// The error returned by [Pipeline::convert_to_git()](crate::filter::Pipeline::convert_to_git()). #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { @@ -52,7 +52,7 @@ pub mod pipeline { /// pub mod convert_to_worktree { - /// The error returned by [Pipeline::convert_to_worktree()][crate::filter::Pipeline::convert_to_worktree()]. + /// The error returned by [Pipeline::convert_to_worktree()](crate::filter::Pipeline::convert_to_worktree()). #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { @@ -62,6 +62,25 @@ pub mod pipeline { Convert(#[from] gix_filter::pipeline::convert::to_worktree::Error), } } + + /// + pub mod worktree_file_to_object { + use std::path::PathBuf; + + /// The error returned by [Pipeline::worktree_file_to_object()](crate::filter::Pipeline::worktree_file_to_object()). + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("Cannot add worktree files in bare repositories")] + MissingWorktree, + #[error("Failed to perform IO for object creation for '{}'", path.display())] + IO { source: std::io::Error, path: PathBuf }, + #[error(transparent)] + WriteBlob(#[from] crate::object::write::Error), + #[error(transparent)] + ConvertToGit(#[from] crate::filter::pipeline::convert_to_git::Error), + } + } } /// A git pipeline for transforming data *to-git* and *to-worktree*, based @@ -133,7 +152,7 @@ impl Pipeline<'_> { /// Convert a `src` stream (to be found at `rela_path`, a repo-relative path) to a representation suitable for storage in `git` /// by using all attributes at `rela_path` and configuration of the repository to know exactly which filters apply. /// `index` is used in particularly rare cases where the CRLF filter in auto-mode tries to determine whether to apply itself, - /// and it should match the state used when [instantiating this instance][Self::new()]. + /// and it should match the state used when [instantiating this instance](Self::new()). /// Note that the return-type implements [`std::io::Read`]. pub fn convert_to_git( &mut self, @@ -187,6 +206,63 @@ impl Pipeline<'_> { )?) } + /// Add the worktree file at `rela_path` to the object database and return its `(id, entry)` for use in a tree or in the index, for instance. + /// + /// `index` is used in particularly rare cases where the CRLF filter in auto-mode tries to determine whether to apply itself, + /// and it should match the state used when [instantiating this instance](Self::new()). + /// + /// Return `Ok(None)` the file didn't exist in the worktree, or if it was of an untrackable type. + pub fn worktree_file_to_object( + &mut self, + rela_path: &BStr, + index: &gix_index::State, + ) -> Result, pipeline::worktree_file_to_object::Error> + { + use pipeline::worktree_file_to_object::Error; + + let rela_path_as_path = gix_path::from_bstr(rela_path); + let repo = self.repo; + let worktree_dir = repo.work_dir().ok_or(Error::MissingWorktree)?; + let path = worktree_dir.join(&rela_path_as_path); + let md = match std::fs::symlink_metadata(&path) { + Ok(md) => md, + Err(err) => { + if gix_fs::io_err::is_not_found(err.kind(), err.raw_os_error()) { + return Ok(None); + } else { + return Err(Error::IO { source: err, path }); + } + } + }; + let (id, kind) = if md.is_symlink() { + let target = std::fs::read_link(&path).map_err(|source| Error::IO { source, path })?; + let id = repo.write_blob(gix_path::into_bstr(target).as_ref())?; + (id, gix_object::tree::EntryKind::Link) + } else if md.is_file() { + use gix_filter::pipeline::convert::ToGitOutcome; + + let file = std::fs::File::open(&path).map_err(|source| Error::IO { source, path })?; + let file_for_git = self.convert_to_git(file, rela_path_as_path.as_ref(), index)?; + let id = match file_for_git { + ToGitOutcome::Unchanged(mut file) => repo.write_blob_stream(&mut file)?, + ToGitOutcome::Buffer(buf) => repo.write_blob(buf)?, + ToGitOutcome::Process(mut read) => repo.write_blob_stream(&mut read)?, + }; + + let kind = if gix_fs::is_executable(&md) { + gix_object::tree::EntryKind::BlobExecutable + } else { + gix_object::tree::EntryKind::Blob + }; + (id, kind) + } else { + // This is probably a type-change to something we can't track. + return Ok(None); + }; + + Ok(Some((id.detach(), kind))) + } + /// Retrieve the static context that is made available to the process filters. /// /// The context set here is relevant for the [`convert_to_git()`][Self::convert_to_git()] and diff --git a/gix/tests/fixtures/generated-archives/.gitignore b/gix/tests/fixtures/generated-archives/.gitignore index 5b9b8472077..9279e744abb 100644 --- a/gix/tests/fixtures/generated-archives/.gitignore +++ b/gix/tests/fixtures/generated-archives/.gitignore @@ -7,4 +7,5 @@ /make_core_worktree_repo.tar /make_signatures_repo.tar /make_diff_repos.tar -/make_submodule_with_worktree.tar \ No newline at end of file +/make_submodule_with_worktree.tar +/repo_with_untracked_files.tar \ No newline at end of file diff --git a/gix/tests/fixtures/repo_with_untracked_files.sh b/gix/tests/fixtures/repo_with_untracked_files.sh new file mode 100755 index 00000000000..e3bca58c94b --- /dev/null +++ b/gix/tests/fixtures/repo_with_untracked_files.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +git init -q +echo content >file +ln -s file link + +echo binary >exe && chmod +x exe +mkfifo fifo diff --git a/gix/tests/gix/repository/filter.rs b/gix/tests/gix/repository/filter.rs index ea555b9b5a3..d030cf8c085 100644 --- a/gix/tests/gix/repository/filter.rs +++ b/gix/tests/gix/repository/filter.rs @@ -10,7 +10,7 @@ fn pipeline_in_nonbare_repo_without_index() -> crate::Result { use gix::bstr::ByteSlice; use gix_filter::driver::apply::Delay; -use crate::util::{named_repo, named_subrepo_opts}; +use crate::util::{hex_to_id, named_repo, named_subrepo_opts}; #[test] fn pipeline_in_repo_without_special_options() -> crate::Result { @@ -31,6 +31,50 @@ fn pipeline_in_repo_without_special_options() -> crate::Result { Ok(()) } +#[test] +#[cfg(unix)] +fn pipeline_worktree_file_to_object() -> crate::Result { + let repo = named_repo("repo_with_untracked_files.sh")?; + let (mut pipe, index) = repo.filter_pipeline(None)?; + + assert_eq!( + pipe.worktree_file_to_object("file".into(), &index)?, + Some(( + hex_to_id("d95f3ad14dee633a758d2e331151e950dd13e4ed"), + gix::object::tree::EntryKind::Blob + )) + ); + assert_eq!( + pipe.worktree_file_to_object("link".into(), &index)?, + Some(( + hex_to_id("1a010b1c0f081b2e8901d55307a15c29ff30af0e"), + gix::object::tree::EntryKind::Link + )) + ); + assert_eq!( + pipe.worktree_file_to_object("exe".into(), &index)?, + Some(( + hex_to_id("a9128c283485202893f5af379dd9beccb6e79486"), + gix::object::tree::EntryKind::BlobExecutable + )) + ); + assert_eq!( + pipe.worktree_file_to_object("missing".into(), &index)?, + None, + "Missing files are specifically typed and no error" + ); + assert!( + repo.work_dir().expect("non-bare").join("fifo").exists(), + "there is a fifo" + ); + assert_eq!( + pipe.worktree_file_to_object("fifo".into(), &index)?, + None, + "untrackable entries are just ignored as if they didn't exist" + ); + Ok(()) +} + #[test] fn pipeline_with_autocrlf() -> crate::Result { let repo = named_repo("make_config_repo.sh")?;