Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions gix-index/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ gix-hashtable = { path = "../gix-hashtable" }
gix-odb = { path = "../gix-odb" }
gix-object = { path = "../gix-object" }
filetime = "0.2.27"
insta = { version = "1.46.3", features = ["filters"] }

[package.metadata.docs.rs]
features = ["sha1", "document-features", "serde"]
4 changes: 2 additions & 2 deletions gix-index/src/decode/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,11 +336,11 @@ pub(crate) fn stat(data: &[u8]) -> Option<(entry::Stat, &[u8])> {
let (size, data) = read_u32(data)?;
Some((
entry::Stat {
mtime: entry::stat::Time {
ctime: entry::stat::Time {
secs: ctime_secs,
nsecs: ctime_nsecs,
},
ctime: entry::stat::Time {
mtime: entry::stat::Time {
secs: mtime_secs,
nsecs: mtime_nsecs,
},
Comment thread
Byron marked this conversation as resolved.
Expand Down
2 changes: 1 addition & 1 deletion gix-index/src/extension/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pub struct Link {

/// The extension for untracked files.
#[allow(dead_code)]
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct UntrackedCache {
/// Something identifying the location and machine that this cache is for.
/// Should the repository be copied to a different machine, the entire cache can immediately be invalidated.
Expand Down
117 changes: 100 additions & 17 deletions gix-index/src/extension/untracked_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,61 @@ use crate::{
util::{read_u32, split_at_byte_exclusive, var_int},
};

impl UntrackedCache {
/// Something identifying the location and machine that this cache is for.
pub fn identifier(&self) -> &bstr::BStr {
self.identifier.as_ref()
}

/// Stat and object id for the `.git/info/exclude` file, if available.
pub fn info_exclude(&self) -> Option<&OidStat> {
self.info_exclude.as_ref()
}

/// Stat and object id for the `core.excludesfile`, if available.
pub fn excludes_file(&self) -> Option<&OidStat> {
self.excludes_file.as_ref()
}

/// Usually `.gitignore`.
pub fn exclude_filename_per_dir(&self) -> &bstr::BStr {
self.exclude_filename_per_dir.as_ref()
}

/// The directory flags Git used while populating the cache.
pub fn dir_flags(&self) -> u32 {
self.dir_flags
}

/// A list of directories and sub-directories, with `directories[0]` being the root.
pub fn directories(&self) -> &[Directory] {
&self.directories
}
}

/// A structure to track filesystem stat information along with an object id, linking a worktree file with what's in our ODB.
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct OidStat {
/// The file system stat information
pub stat: entry::Stat,
/// The id of the file in our ODB.
pub id: ObjectId,
}

impl OidStat {
/// The file system stat information.
pub fn stat(&self) -> &entry::Stat {
&self.stat
}

/// The id of the file in our ODB.
pub fn id(&self) -> ObjectId {
self.id
}
}

/// A directory with information about its untracked files, and its sub-directories
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct Directory {
/// The directories name, or an empty string if this is the root directory.
pub name: BString,
Expand All @@ -34,10 +78,42 @@ pub struct Directory {
pub check_only: bool,
}

impl Directory {
/// The directory name, or an empty string if this is the root directory.
/// `/` is always used as path-separator.
pub fn name(&self) -> &bstr::BStr {
self.name.as_ref()
}

/// Untracked files and directory names.
pub fn untracked_entries(&self) -> &[BString] {
&self.untracked_entries
}

/// Indices for sub-directories similar to this one.
pub fn sub_directories(&self) -> &[usize] {
&self.sub_directories
}

/// The directory stat data, if available and valid.
pub fn stat(&self) -> Option<&entry::Stat> {
self.stat.as_ref()
}

/// The oid of a `.gitignore` file, if it exists.
pub fn exclude_file_oid(&self) -> Option<ObjectId> {
self.exclude_file_oid
}

/// Whether Git marked this directory as check-only.
pub fn check_only(&self) -> bool {
self.check_only
}
}

/// Only used as an indicator
pub const SIGNATURE: Signature = *b"UNTR";

// #[allow(unused)]
/// Decode an untracked cache extension from `data`, assuming object hashes are of type `object_hash`.
pub fn decode(data: &[u8], object_hash: gix_hash::Kind, alloc_limit_bytes: Option<usize>) -> Option<UntrackedCache> {
if data.last().is_none_or(|b| *b != 0) {
Expand All @@ -46,10 +122,29 @@ pub fn decode(data: &[u8], object_hash: gix_hash::Kind, alloc_limit_bytes: Optio
let (identifier_len, data) = var_int(data)?;
let (identifier, data) = data.split_at_checked(identifier_len.try_into().ok()?)?;

// The on-disk layout matches git's `ondisk_untracked_cache` struct
// https://github.com/git/git/blob/2855562ca6a9c6b0e7bc780b050c1e83c9fcfbd0/dir.c#L3582-L3586
// https://github.com/git/git/blob/2855562ca6a9c6b0e7bc780b050c1e83c9fcfbd0/dir.c#L3668-L3722
// info_exclude_stat (36 bytes)
// excludes_file_stat (36 bytes)
// dir_flags ( 4 bytes)
// info_exclude hash (hash_len bytes)
// excludes_file hash (hash_len bytes)
// exclude_per_dir (NUL-terminated)
let hash_len = object_hash.len_in_bytes();
let (info_exclude, data) = decode_oid_stat(data, hash_len)?;
let (excludes_file, data) = decode_oid_stat(data, hash_len)?;
let (info_exclude_stat, data) = crate::decode::stat(data)?;
let (excludes_file_stat, data) = crate::decode::stat(data)?;
let (dir_flags, data) = read_u32(data)?;
let (info_exclude_hash, data) = data.split_at_checked(hash_len)?;
let (excludes_file_hash, data) = data.split_at_checked(hash_len)?;
let info_exclude = OidStat {
stat: info_exclude_stat,
id: ObjectId::from_bytes_or_panic(info_exclude_hash),
};
let excludes_file = OidStat {
stat: excludes_file_stat,
id: ObjectId::from_bytes_or_panic(excludes_file_hash),
};
let (exclude_filename_per_dir, data) = split_at_byte_exclusive(data, 0)?;

let (num_directory_blocks, data) = var_int(data)?;
Expand Down Expand Up @@ -174,15 +269,3 @@ fn decode_directory_block<'a>(

data.into()
}

fn decode_oid_stat(data: &[u8], hash_len: usize) -> Option<(OidStat, &[u8])> {
let (stat, data) = crate::decode::stat(data)?;
let (hash, data) = data.split_at_checked(hash_len)?;
Some((
OidStat {
stat,
id: ObjectId::from_bytes_or_panic(hash),
},
data,
))
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
13 changes: 13 additions & 0 deletions gix-index/tests/fixtures/make_index/untracked_cache_empty.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -eu -o pipefail

git init -q

mkdir tracked-dir untracked-dir-2 untracked-dir-3
touch tracked-root-one tracked-root-two untracked-root-file \
tracked-dir/tracked-file \
untracked-dir-2/untracked-file-two \
untracked-dir-3/untracked-file-three
git add tracked-root-one tracked-root-two tracked-dir/tracked-file
: >.git/info/exclude
git update-index --untracked-cache
27 changes: 27 additions & 0 deletions gix-index/tests/fixtures/make_index/untracked_cache_nested.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -eu -o pipefail

GIT_FORCE_UNTRACKED_CACHE=true
export GIT_FORCE_UNTRACKED_CACHE

git init -q
git config core.excludesFile ""

# This fixture extends the populated case with a tracked per-directory
# `.gitignore` and nested untracked directories. The path names are intentionally
# verbose because the test snapshots assert the decoded UNTR directory graph.
mkdir -p tracked-dir-with-ignore/nested-untracked-dir/deep-untracked-dir \
untracked-dir-2 \
untracked-dir-3
touch tracked-root-one tracked-root-two untracked-root-file \
tracked-dir-with-ignore/tracked-file \
tracked-dir-with-ignore/visible-untracked-file \
tracked-dir-with-ignore/nested-untracked-dir/deep-untracked-dir/deep-untracked-file \
untracked-dir-2/untracked-file-two \
untracked-dir-3/untracked-file-three
printf "ignored-by-dir-ignore\nalso-ignored-by-dir-ignore\n" >tracked-dir-with-ignore/.gitignore
git add tracked-root-one tracked-root-two tracked-dir-with-ignore/tracked-file tracked-dir-with-ignore/.gitignore
mkdir -p .git/info
: >.git/info/exclude
git update-index --untracked-cache
git status --porcelain >/dev/null
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -eu -o pipefail

# Reuse the empty UNTR fixture and run status so Git fills the cache with the
# descriptive tracked/untracked path layout from that script.
. "$(dirname -- "${BASH_SOURCE[0]}")/untracked_cache_empty.sh"
# This triggers the untracked cache to be refreshed.
git status > /dev/null
Loading
Loading