diff --git a/gix-index/src/entry/mode.rs b/gix-index/src/entry/mode.rs index 817d7a06e0e..3b29548485f 100644 --- a/gix-index/src/entry/mode.rs +++ b/gix-index/src/entry/mode.rs @@ -49,6 +49,7 @@ impl Mode { ) -> Option { match self { Mode::FILE if !stat.is_file() => (), + Mode::SYMLINK if stat.is_symlink() => return None, Mode::SYMLINK if has_symlinks && !stat.is_symlink() => (), Mode::SYMLINK if !has_symlinks && !stat.is_file() => (), Mode::COMMIT | Mode::DIR if !stat.is_dir() => (), diff --git a/gix-ref/src/store/file/mod.rs b/gix-ref/src/store/file/mod.rs index 3c7d20e8c94..982273d4a67 100644 --- a/gix-ref/src/store/file/mod.rs +++ b/gix-ref/src/store/file/mod.rs @@ -53,7 +53,7 @@ mod access { } } - use crate::file; + use crate::{file, Target}; /// Access impl file::Store { @@ -78,6 +78,35 @@ mod access { pub fn common_dir_resolved(&self) -> &Path { self.common_dir.as_deref().unwrap_or(&self.git_dir) } + + /// Return `Some(true)` if this is a freshly initialized ref store without any observable changes. + /// Return `None` if `HEAD` couldn't be read. + /// + /// This is the case if: + /// + /// * the ref-store is valid + /// * `HEAD` exists + /// * `HEAD` still points to `default_ref` + /// * there are no packed refs + /// * There are no observable references in `refs/` + pub fn is_pristine(&self, default_ref: &crate::FullNameRef) -> Option { + let head = self.find_loose("HEAD").ok()?; + match head.target { + Target::Object(_) => return Some(false), + Target::Symbolic(name) => { + if name.as_ref() != default_ref { + return Some(false); + } + } + } + if self.loose_iter().ok()?.filter_map(Result::ok).next().is_some() { + return Some(false); + } + if self.packed_refs_path().is_file() { + return Some(false); + } + Some(true) + } } } diff --git a/gix-ref/tests/fixtures/generated-archives/make_pristine.tar b/gix-ref/tests/fixtures/generated-archives/make_pristine.tar new file mode 100644 index 00000000000..40e705b753d Binary files /dev/null and b/gix-ref/tests/fixtures/generated-archives/make_pristine.tar differ diff --git a/gix-ref/tests/fixtures/make_pristine.sh b/gix-ref/tests/fixtures/make_pristine.sh new file mode 100755 index 00000000000..ed42133f519 --- /dev/null +++ b/gix-ref/tests/fixtures/make_pristine.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +git init untouched + +git init changed-headref +(cd changed-headref + echo "ref: refs/heads/other" >.git/HEAD +) + +git init detached +(cd detached + echo "abcdefabcdefabcdefabcdefabcdefabcdefabcd" >.git/HEAD +) + +git init invalid-loose-ref +(cd invalid-loose-ref + touch .git/refs/heads/empty +) diff --git a/gix-ref/tests/refs/file/mod.rs b/gix-ref/tests/refs/file/mod.rs index 0c71dba67b5..c2e7b201b09 100644 --- a/gix-ref/tests/refs/file/mod.rs +++ b/gix-ref/tests/refs/file/mod.rs @@ -14,8 +14,12 @@ pub fn store_with_packed_refs() -> crate::Result { } pub fn store_at(name: &str) -> crate::Result { - let path = gix_testtools::scripted_fixture_read_only_standalone(name)?; - Ok(Store::at(path.join(".git"), Default::default())) + named_store_at(name, "") +} + +pub fn named_store_at(script_name: &str, name: &str) -> crate::Result { + let path = gix_testtools::scripted_fixture_read_only_standalone(script_name)?; + Ok(Store::at(path.join(name).join(".git"), Default::default())) } pub fn store_at_with_args(name: &str, args: impl IntoIterator>) -> crate::Result { diff --git a/gix-ref/tests/refs/file/store/access.rs b/gix-ref/tests/refs/file/store/access.rs index df1e7d6d0ec..b7d4097d797 100644 --- a/gix-ref/tests/refs/file/store/access.rs +++ b/gix-ref/tests/refs/file/store/access.rs @@ -1,4 +1,4 @@ -use crate::file::store; +use crate::file::{named_store_at, store}; #[test] fn set_packed_buffer_mmap_threshold() -> crate::Result { @@ -20,3 +20,22 @@ fn set_packed_buffer_mmap_threshold() -> crate::Result { ); Ok(()) } + +#[test] +fn is_pristine() -> crate::Result { + let store = named_store_at("make_pristine.sh", "untouched")?; + assert_eq!(store.is_pristine("refs/heads/main".try_into()?), Some(true)); + assert_eq!(store.is_pristine("refs/heads/other".try_into()?), Some(false)); + + let store = named_store_at("make_pristine.sh", "changed-headref")?; + assert_eq!(store.is_pristine("refs/heads/other".try_into()?), Some(true)); + assert_eq!(store.is_pristine("refs/heads/main".try_into()?), Some(false)); + + let store = named_store_at("make_pristine.sh", "detached")?; + assert_eq!(store.is_pristine("refs/heads/main".try_into()?), Some(false)); + + let store = named_store_at("make_pristine.sh", "invalid-loose-ref")?; + assert_eq!(store.is_pristine("refs/heads/main".try_into()?), Some(true)); + + Ok(()) +} diff --git a/gix-ref/tests/refs/file/store/find.rs b/gix-ref/tests/refs/file/store/find.rs index f9cb0a26489..2ecd52cddd9 100644 --- a/gix-ref/tests/refs/file/store/find.rs +++ b/gix-ref/tests/refs/file/store/find.rs @@ -9,6 +9,7 @@ mod existing { "make_packed_ref_repository_for_overlay.sh", ] { let store = store_at(fixture)?; + assert_eq!(store.is_pristine("refs/heads/main".try_into()?), Some(false)); let c1 = hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"); let r = store.find("main")?; assert_eq!(r.target.into_id(), c1); diff --git a/gix-ref/tests/refs/file/store/iter.rs b/gix-ref/tests/refs/file/store/iter.rs index 527fd03c1cb..68259c0b1b9 100644 --- a/gix-ref/tests/refs/file/store/iter.rs +++ b/gix-ref/tests/refs/file/store/iter.rs @@ -576,6 +576,7 @@ fn overlay_partial_prefix_iter_when_prefix_is_dir() -> crate::Result { use gix_ref::Target::*; let store = store_at("make_packed_ref_repository_for_overlay.sh")?; + assert_eq!(store.is_pristine("refs/heads/main".try_into()?), Some(false)); let c1 = hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"); let ref_names = store diff --git a/gix-ref/tests/refs/file/worktree.rs b/gix-ref/tests/refs/file/worktree.rs index d7b61ccf992..5d23c779f8f 100644 --- a/gix-ref/tests/refs/file/worktree.rs +++ b/gix-ref/tests/refs/file/worktree.rs @@ -84,6 +84,7 @@ mod read_only { fn linked() -> crate::Result { for packed in [false, true] { let (store, odb, _tmp) = worktree_store(packed, "w1", Mode::Read)?; + assert_eq!(store.is_pristine("refs/heads/main".try_into()?), Some(false)); let peel = into_peel(&store, odb); let w1_head_id = peel(store.find("HEAD").unwrap()); @@ -132,6 +133,7 @@ mod read_only { fn main() -> crate::Result { for packed in [false, true] { let (store, odb, _tmp) = main_store(packed, Mode::Read)?; + assert_eq!(store.is_pristine("refs/heads/main".try_into()?), Some(false)); let peel = into_peel(&store, odb); let head_id = peel(store.find("HEAD").unwrap()); diff --git a/gix-status/src/index_as_worktree/function.rs b/gix-status/src/index_as_worktree/function.rs index 72e58e8457a..474c9c6c1b0 100644 --- a/gix-status/src/index_as_worktree/function.rs +++ b/gix-status/src/index_as_worktree/function.rs @@ -408,12 +408,12 @@ impl<'index> State<'_, 'index> { None => false, }; - // Here we implement racy-git. See racy-git.txt in the git documentation for a detailed documentation. + // We implement racy-git. See racy-git.txt in the git documentation for detailed documentation. // // A file is racy if: - // 1. its `mtime` is at or after the last index timestamp and its entry stat information - // matches the on-disk file but the file contents are actually modified - // 2. it's size is 0 (set after detecting a file was racy previously) + // 1. Its `mtime` is at or after the last index timestamp and its entry stat information + // matches the on-disk file, but the file contents are actually modified + // 2. Its size is 0 (set after detecting a file was racy previously) // // The first case is detected below by checking the timestamp if the file is marked unmodified. // The second case is usually detected either because the on-disk file is not empty, hence @@ -449,7 +449,16 @@ impl<'index> State<'_, 'index> { file_len: file_size_bytes, filter: &mut self.filter, attr_stack: &mut self.attr_stack, - options: self.options, + core_symlinks: + // If this is legitimately a symlink, then pretend symlinks are enabled as the option seems stale. + // Otherwise, respect the option. + if metadata.is_symlink() + && entry.mode.to_tree_entry_mode().map(|m| m.kind()) == Some(gix_object::tree::EntryKind::Link) + { + true + } else { + self.options.fs.symlink + }, id: &entry.id, objects, worktree_reads: self.worktree_reads, @@ -517,7 +526,7 @@ where entry: &'a gix_index::Entry, filter: &'a mut gix_filter::Pipeline, attr_stack: &'a mut gix_worktree::Stack, - options: &'a Options, + core_symlinks: bool, id: &'a gix_hash::oid, objects: Find, worktree_bytes: &'a AtomicU64, @@ -545,7 +554,7 @@ where // let is_symlink = self.entry.mode == gix_index::entry::Mode::SYMLINK; // TODO: what to do about precompose unicode and ignore_case for symlinks - let out = if is_symlink && self.options.fs.symlink { + let out = if is_symlink && self.core_symlinks { // conversion to bstr can never fail because symlinks are only used // on unix (by git) so no reason to use the try version here let symlink_path = diff --git a/gix-status/tests/status/index_as_worktree.rs b/gix-status/tests/status/index_as_worktree.rs index a5f512f2b5b..d0f7a4a57f0 100644 --- a/gix-status/tests/status/index_as_worktree.rs +++ b/gix-status/tests/status/index_as_worktree.rs @@ -48,6 +48,7 @@ fn nonfile_fixture(name: &str, expected_status: &[Expectation<'_>]) -> Outcome { false, Default::default(), false, + None, ) } @@ -65,6 +66,7 @@ fn fixture_with_index( false, Default::default(), false, + None, ) } @@ -78,6 +80,7 @@ fn submodule_fixture(name: &str, expected_status: &[Expectation<'_>]) -> Outcome false, Default::default(), false, + None, ) } @@ -91,6 +94,7 @@ fn conflict_fixture(name: &str, expected_status: &[Expectation<'_>]) -> Outcome false, Default::default(), false, + None, ) } @@ -104,6 +108,7 @@ fn submodule_fixture_status(name: &str, expected_status: &[Expectation<'_>], sub submodule_dirty, Default::default(), false, + None, ) } @@ -117,6 +122,7 @@ fn fixture_filtered(name: &str, pathspecs: &[&str], expected_status: &[Expectati false, Default::default(), false, + None, ) } @@ -130,6 +136,7 @@ fn fixture_filtered_detailed( submodule_dirty: bool, auto_crlf: gix_filter::eol::AutoCrlf, use_odb: bool, + fs_capabilities: Option<&dyn Fn(&std::path::Path) -> gix_fs::Capabilities>, ) -> Outcome { // This can easily happen in some fixtures, which can cause flakiness. It's time-dependent after all. fn ignore_racyclean(mut out: Outcome) -> Outcome { @@ -179,7 +186,7 @@ fn fixture_filtered_detailed( should_interrupt: &AtomicBool::default(), }; let options = Options { - fs: gix_fs::Capabilities::probe(&git_dir), + fs: fs_capabilities.map_or_else(|| gix_fs::Capabilities::probe(&git_dir), |new| new(&git_dir)), stat: TEST_OPTIONS, ..Options::default() }; @@ -353,6 +360,7 @@ fn replace_dir_with_file() { false, Default::default(), false, + None, ); assert_eq!( out, @@ -560,6 +568,24 @@ fn unchanged() { fixture("status_unchanged", &[]); } +#[test] +fn unchanged_symlinks_present_but_deactivated() { + fixture_filtered_detailed( + "status_unchanged", + "", + &[], + &[], + |_| {}, + false, + Default::default(), + false, + Some(&|dir| gix_fs::Capabilities { + symlink: false, + ..gix_fs::Capabilities::probe(dir) + }), + ); +} + #[test] fn unchanged_despite_filter() { let actual_outcome = fixture_filtered_detailed( @@ -571,6 +597,7 @@ fn unchanged_despite_filter() { false, AutoCrlf::Enabled, true, /* make ODB available */ + None, ); let expected_outcome = Outcome { diff --git a/gix/src/repository/location.rs b/gix/src/repository/location.rs index e7a18138df3..6f38b3c958d 100644 --- a/gix/src/repository/location.rs +++ b/gix/src/repository/location.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::path::{Path, PathBuf}; use gix_path::realpath::MAX_SYMLINKS; @@ -108,4 +109,20 @@ impl crate::Repository { None => crate::repository::Kind::Bare, } } + + /// Returns `Some(true)` if the reference database [is untouched](gix_ref::file::Store::is_pristine()). + /// This typically indicates that the repository is new and empty. + /// Return `None` if a defect in the database makes the answer uncertain. + #[doc(alias = "is_empty", alias = "git2")] + pub fn is_pristine(&self) -> Option { + let name = self + .config + .resolved + .string(crate::config::tree::Init::DEFAULT_BRANCH) + .unwrap_or(Cow::Borrowed("master".into())); + let default_branch_ref_name: gix_ref::FullName = format!("refs/heads/{name}") + .try_into() + .unwrap_or_else(|_| gix_ref::FullName::try_from("refs/heads/master").expect("known to be valid")); + self.refs.is_pristine(default_branch_ref_name.as_ref()) + } } diff --git a/gix/src/repository/submodule.rs b/gix/src/repository/submodule.rs index a605bfbd322..bcd54bc382b 100644 --- a/gix/src/repository/submodule.rs +++ b/gix/src/repository/submodule.rs @@ -26,7 +26,7 @@ impl Repository { )?)) } - /// Return a shared [`.gitmodules` file](crate::submodule::File) which is updated automatically if the in-memory snapshot + /// Return a shared [`.gitmodules` file](submodule::File) which is updated automatically if the in-memory snapshot /// has become stale as the underlying file on disk has changed. The snapshot based on the file on disk is shared across all /// clones of this repository. /// @@ -54,12 +54,20 @@ impl Repository { }) { Some(id) => id, None => match self - .head_commit()? - .tree()? - .find_entry(submodule::MODULES_FILE) - .map(|entry| entry.inner.oid) + .head()? + .try_peel_to_id_in_place()? + .map(|id| -> Result, submodule::modules::Error> { + Ok(id + .object()? + .peel_to_commit()? + .tree()? + .find_entry(submodule::MODULES_FILE) + .map(|entry| entry.inner.oid.to_owned())) + }) + .transpose()? + .flatten() { - Some(id) => id.to_owned(), + Some(id) => id, None => return Ok(None), }, }; diff --git a/gix/src/status/index_worktree.rs b/gix/src/status/index_worktree.rs index 86a7dccc877..1911650a1c2 100644 --- a/gix/src/status/index_worktree.rs +++ b/gix/src/status/index_worktree.rs @@ -226,13 +226,6 @@ mod submodule_status { v } Ok(None) => Vec::new(), - Err(crate::submodule::modules::Error::FindHeadCommit( - crate::reference::head_commit::Error::PeelToCommit( - crate::head::peel::to_commit::Error::PeelToObject( - crate::head::peel::to_object::Error::Unborn { .. }, - ), - ), - )) => Vec::new(), Err(err) => return Err(err), }; Ok(Self { diff --git a/gix/src/submodule/errors.rs b/gix/src/submodule/errors.rs index 4e41337de45..81c76b8fe8a 100644 --- a/gix/src/submodule/errors.rs +++ b/gix/src/submodule/errors.rs @@ -23,8 +23,12 @@ pub mod modules { OpenIndex(#[from] crate::worktree::open_index::Error), #[error("Could not find the .gitmodules file by id in the object database")] FindExistingBlob(#[from] crate::object::find::existing::Error), - #[error("Did not find commit in current HEAD to access its tree")] - FindHeadCommit(#[from] crate::reference::head_commit::Error), + #[error(transparent)] + FindHeadRef(#[from] crate::reference::find::existing::Error), + #[error(transparent)] + PeelHeadRef(#[from] crate::head::peel::Error), + #[error(transparent)] + PeelObjectToCommit(#[from] crate::object::peel::to_kind::Error), #[error(transparent)] TreeFromCommit(#[from] crate::object::commit::Error), } diff --git a/gix/tests/fixtures/generated-archives/make_submodules.tar b/gix/tests/fixtures/generated-archives/make_submodules.tar index 4a3566aabdf..220c4e9a56f 100644 Binary files a/gix/tests/fixtures/generated-archives/make_submodules.tar and b/gix/tests/fixtures/generated-archives/make_submodules.tar differ diff --git a/gix/tests/fixtures/make_submodules.sh b/gix/tests/fixtures/make_submodules.sh index 906170711ed..524faf6282a 100755 --- a/gix/tests/fixtures/make_submodules.sh +++ b/gix/tests/fixtures/make_submodules.sh @@ -144,3 +144,5 @@ git clone with-submodules not-a-submodule mv modules.bak .gitmodules git add m1 && git commit -m "no submodule in index and commit, but in configuration" ) + +git init unborn \ No newline at end of file diff --git a/gix/tests/gix/repository/mod.rs b/gix/tests/gix/repository/mod.rs index cf29cfc1170..4253ff4b121 100644 --- a/gix/tests/gix/repository/mod.rs +++ b/gix/tests/gix/repository/mod.rs @@ -56,6 +56,16 @@ mod index { repo.index_or_load_from_head_or_empty()?.entries().is_empty(), "an empty index is created on the fly" ); + assert_eq!( + repo.is_pristine(), + Some(false), + "not pristine as it things the initial ref was changed to 'main'" + ); + assert_eq!( + repo.refs.is_pristine("refs/heads/main".try_into()?), + Some(true), + "This is a quirk of default values in gix and the way we override the initial branch for test fixtures" + ); Ok(()) } } diff --git a/gix/tests/gix/submodule.rs b/gix/tests/gix/submodule.rs index 597c97948ce..ab29c22c018 100644 --- a/gix/tests/gix/submodule.rs +++ b/gix/tests/gix/submodule.rs @@ -340,6 +340,17 @@ mod open { Ok(()) } + #[test] + fn in_unborn() -> crate::Result { + let repo = repo("unborn")?; + assert_eq!( + repo.submodules()?.into_iter().flatten().count(), + 0, + "there is nothing, and that is fine" + ); + Ok(()) + } + #[test] #[cfg(feature = "revision")] fn submodule_worktrees() -> crate::Result {