Skip to content

various improvements #2016

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gix-index/src/entry/mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ impl Mode {
) -> Option<Change> {
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() => (),
Expand Down
31 changes: 30 additions & 1 deletion gix-ref/src/store/file/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ mod access {
}
}

use crate::file;
use crate::{file, Target};

/// Access
impl file::Store {
Expand All @@ -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<bool> {
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)
}
}
}

Expand Down
Binary file not shown.
19 changes: 19 additions & 0 deletions gix-ref/tests/fixtures/make_pristine.sh
Original file line number Diff line number Diff line change
@@ -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
)
8 changes: 6 additions & 2 deletions gix-ref/tests/refs/file/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@ pub fn store_with_packed_refs() -> crate::Result<Store> {
}

pub fn store_at(name: &str) -> crate::Result<Store> {
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<Store> {
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<Item = impl Into<String>>) -> crate::Result<Store> {
Expand Down
21 changes: 20 additions & 1 deletion gix-ref/tests/refs/file/store/access.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::file::store;
use crate::file::{named_store_at, store};

#[test]
fn set_packed_buffer_mmap_threshold() -> crate::Result {
Expand All @@ -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(())
}
1 change: 1 addition & 0 deletions gix-ref/tests/refs/file/store/find.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions gix-ref/tests/refs/file/store/iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions gix-ref/tests/refs/file/worktree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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());
Expand Down
23 changes: 16 additions & 7 deletions gix-status/src/index_as_worktree/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 =
Expand Down
29 changes: 28 additions & 1 deletion gix-status/tests/status/index_as_worktree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ fn nonfile_fixture(name: &str, expected_status: &[Expectation<'_>]) -> Outcome {
false,
Default::default(),
false,
None,
)
}

Expand All @@ -65,6 +66,7 @@ fn fixture_with_index(
false,
Default::default(),
false,
None,
)
}

Expand All @@ -78,6 +80,7 @@ fn submodule_fixture(name: &str, expected_status: &[Expectation<'_>]) -> Outcome
false,
Default::default(),
false,
None,
)
}

Expand All @@ -91,6 +94,7 @@ fn conflict_fixture(name: &str, expected_status: &[Expectation<'_>]) -> Outcome
false,
Default::default(),
false,
None,
)
}

Expand All @@ -104,6 +108,7 @@ fn submodule_fixture_status(name: &str, expected_status: &[Expectation<'_>], sub
submodule_dirty,
Default::default(),
false,
None,
)
}

Expand All @@ -117,6 +122,7 @@ fn fixture_filtered(name: &str, pathspecs: &[&str], expected_status: &[Expectati
false,
Default::default(),
false,
None,
)
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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()
};
Expand Down Expand Up @@ -353,6 +360,7 @@ fn replace_dir_with_file() {
false,
Default::default(),
false,
None,
);
assert_eq!(
out,
Expand Down Expand Up @@ -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(
Expand All @@ -571,6 +597,7 @@ fn unchanged_despite_filter() {
false,
AutoCrlf::Enabled,
true, /* make ODB available */
None,
);

let expected_outcome = Outcome {
Expand Down
17 changes: 17 additions & 0 deletions gix/src/repository/location.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::path::{Path, PathBuf};

use gix_path::realpath::MAX_SYMLINKS;
Expand Down Expand Up @@ -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<bool> {
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())
}
}
20 changes: 14 additions & 6 deletions gix/src/repository/submodule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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<Option<_>, 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),
},
};
Expand Down
7 changes: 0 additions & 7 deletions gix/src/status/index_worktree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 6 additions & 2 deletions gix/src/submodule/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Expand Down
Binary file modified gix/tests/fixtures/generated-archives/make_submodules.tar
Binary file not shown.
Loading
Loading