Skip to content
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
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,
},
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,
))
}
49 changes: 48 additions & 1 deletion gix-index/src/file/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,64 @@ mod impls {
mod impl_ {
use std::fmt::Formatter;

use crate::{File, State};
use crate::{Entry, File, PathStorageRef, State};

impl std::fmt::Debug for File {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if f.alternate() {
return f
.debug_struct("File")
.field("path", &self.path.display())
.field("checksum", &self.checksum)
.field("object_hash", &self.state.object_hash)
.field("timestamp", &self.state.timestamp)
.field("version", &self.state.version)
.field(
"entries",
&EntriesDebug {
entries: &self.state.entries,
path_backing: &self.state.path_backing,
},
)
.field("path_backing_size_bytes", &self.state.path_backing.len())
.field("is_sparse", &self.state.is_sparse)
.field("end_of_index_at_decode_time", &self.state.end_of_index_at_decode_time)
.field("offset_table_at_decode_time", &self.state.offset_table_at_decode_time)
.field("tree", &self.state.tree)
.field("has_link", &self.state.link.is_some())
.field("has_resolve_undo", &self.state.resolve_undo.is_some())
.field("untracked", &self.state.untracked)
.field("has_fs_monitor", &self.state.fs_monitor.is_some())
.finish();
}
f.debug_struct("File")
.field("path", &self.path.display())
.field("checksum", &self.checksum)
.finish_non_exhaustive()
}
}

struct EntriesDebug<'a> {
entries: &'a [Entry],
path_backing: &'a PathStorageRef,
}

impl std::fmt::Debug for EntriesDebug<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if !f.alternate() {
return f.debug_list().entries(self.entries).finish();
}

writeln!(f, "[")?;
for entry in self.entries {
write!(f, " ")?;
entry.fmt_debug(f, Some(self.path_backing))?;
writeln!(f, ",")?;
}
write!(f, "]")
}
}

impl From<File> for State {
fn from(f: File) -> Self {
f.state
Expand Down
51 changes: 49 additions & 2 deletions gix-index/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ pub enum Version {
}

/// An entry in the index, identifying a non-tree item on disk.
#[derive(Debug, Clone, Eq, PartialEq)]
#[derive(Clone, Eq, PartialEq)]
pub struct Entry {
/// The filesystem stat information for the file on disk.
pub stat: entry::Stat,
Expand Down Expand Up @@ -165,7 +165,54 @@ pub struct State {
mod impls {
use std::fmt::{Debug, Formatter};

use crate::{State, entry::Stage};
use crate::{Entry, PathStorageRef, State, entry::Stage};

impl Entry {
pub(crate) fn fmt_debug(&self, f: &mut Formatter, path_backing: Option<&PathStorageRef>) -> std::fmt::Result {
if f.alternate() {
write!(
f,
"{} {}{:?} mtime: {:?} {} ",
match self.flags.stage() {
Stage::Unconflicted => " ",
Stage::Base => "BASE ",
Stage::Ours => "OURS ",
Stage::Theirs => "THEIRS ",
},
if self.flags.is_empty() {
"".to_string()
} else {
format!("{:?} ", self.flags)
},
self.mode,
self.stat.mtime,
self.id,
)?;
return match path_backing {
Some(path_backing) => write!(f, "{}", self.path_in(path_backing)),
None => write!(f, "{:?}", self.path),
};
}

let mut entry = f.debug_struct("Entry");
entry
.field("stat", &self.stat)
.field("id", &self.id)
.field("flags", &self.flags)
.field("mode", &self.mode);
match path_backing {
Some(path_backing) => entry.field("path", &self.path_in(path_backing)),
None => entry.field("path", &self.path),
}
.finish()
}
}

impl Debug for Entry {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.fmt_debug(f, None)
}
}

impl Debug for State {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
7 changes: 7 additions & 0 deletions gix-index/tests/fixtures/make_index/shared.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env bash

UNTRACKED_CACHE_MTIME="2038-01-19 03:14:07.123456789Z"

seed_untracked_cache_times() {
touch -d "$UNTRACKED_CACHE_MTIME" "$@"
Comment thread
Byron marked this conversation as resolved.
}
27 changes: 27 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,27 @@
#!/usr/bin/env bash
set -eu -o pipefail

. "$(dirname -- "${BASH_SOURCE[0]}")/shared.sh"

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
seed_untracked_cache_times \
. \
.git/info/exclude \
tracked-dir \
tracked-dir/tracked-file \
untracked-dir-2 \
untracked-dir-2/untracked-file-two \
untracked-dir-3 \
untracked-dir-3/untracked-file-three \
tracked-root-one \
tracked-root-two \
untracked-root-file
Loading
Loading