//! ## Feature Flags#![cfg_attr( all(doc, feature = "document-features"), doc = ::document_features::document_features!())]#![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg))]#![deny(unsafe_code, missing_docs)] use std::{ops::Range, path::PathBuf}; use bstr::{BStr, ByteSlice};use filetime::FileTime;/// `gix_hash` is made available as it's part of the public API in various places.pub use gix_hash as hash;/// A re-export to allow calling [`State::from_tree()`].pub use gix_validate as validate; ///pub mod file; ///pub mod extension; ///pub mod entry; mod access; ///pub mod init; ///pub mod decode; ///pub mod verify; ///pub mod write; pub mod fs; /// All known versions of a git index file.#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]pub enum Version { /// Supports entries and various extensions. V2 = 2, /// Adds support for additional flags for each entry, called extended entries. V3 = 3, /// Supports deltified entry paths. V4 = 4,} /// An entry in the index, identifying a non-tree item on disk.#[derive(Clone, Eq, PartialEq)]pub struct Entry { /// The filesystem stat information for the file on disk. pub stat: entry::Stat, /// The object id for this entry's ODB representation (assuming it's up-to-date with it). pub id: gix_hash::ObjectId, /// Additional flags for use in algorithms and for efficiently storing stage information. pub flags: entry::Flags, /// The kind of item this entry represents - it's not all blobs in the index anymore. pub mode: entry::Mode, /// The range to lookup in the path backing to obtain the entry path relative to the repository. /// This costs additional memory but is probably worth it given that paths can stay in one big allocation. path: Range<usize>,} /// An index file whose state was read from a file on disk.#[derive(Clone)]pub struct File { /// The state containing the actual index data. pub(crate) state: State, /// The path from which the index was read or to which it is supposed to be written. pub(crate) path: PathBuf, /// The checksum of all bytes prior to the checksum itself. pub(crate) checksum: Option<gix_hash::ObjectId>,} /// The type to use and store paths to all entries.pub type PathStorage = Vec<u8>;/// The type to use and store paths to all entries, as referencepub type PathStorageRef = [u8]; struct DirEntry<'a> { /// The first entry in the directory entry: &'a Entry, /// One past the last byte of the directory in the path-backing dir_end: usize,} impl DirEntry<'_> { fn path<'a>(&self, state: &'a State) -> &'a BStr { let range = self.entry.path.start..self.dir_end; state.path_backing[range].as_bstr() }} /// A backing store for accelerating lookups of entries in a case-sensitive and case-insensitive manner.pub struct AccelerateLookup<'a> { /// The entries themselves, hashed by their full icase path. /// Icase-clashes are handled in order of occurrence and are all available for iteration. icase_entries: hashbrown::HashTable<&'a Entry>, /// Each hash in this table corresponds to a directory containing one or more entries. icase_dirs: hashbrown::HashTable<DirEntry<'a>>,} /// An in-memory cache of a fully parsed git index file.////// As opposed to a snapshot, it's meant to be altered and eventually be written back to disk or converted into a tree./// We treat index and its state synonymous.////// # Path Format////// All entry paths stored by [`State`], and all path-like arguments used to access entries, are repository-relative byte/// strings with `/` as separator. They are not platform-native filesystem paths, must not be absolute, and must not use/// separators like `\`; convert worktree or absolute paths to this representation before lookup or insertion.////// # A note on safety////// An index (i.e. [`State`]) created by hand is not guaranteed to have valid entry paths as they are entirely controlled/// by the caller, without applying any level of validation.////// This means that before using these paths to recreate files on disk, *they must be validated*.////// It's notable that it's possible to manufacture tree objects which contain names like `.git/hooks/pre-commit`/// which then will look like `.git/hooks/pre-commit` in the index, which doesn't care that the name came from a single/// tree instead of from trees named `.git`, `hooks` and a blob named `pre-commit`. The effect is still the same - an invalid/// path is presented in the index and its consumer must validate each path component before usage.////// It's recommended to do that using `gix_worktree::Stack` which has it built-in if it's created `for_checkout()`. Alternatively/// one can validate component names with `gix_validate::path::component()`.#[derive(Clone)]pub struct State { /// The kind of object hash used when storing the underlying file. /// /// Empty states for example won't have a single object id, so deduction of the hash used isn't always possible. object_hash: gix_hash::Kind, /// The time at which the state was created, indicating its freshness compared to other files on disk. /// /// Note that on platforms that only have a precisions of a second for this time, we will treat all entries with the /// same timestamp as this as potentially changed, checking more thoroughly if a change actually happened. timestamp: FileTime, version: Version, entries: Vec<Entry>, /// A memory area keeping all index paths, in full length, independently of the index version. /// /// Ranges into this storage are referred to by parts of `entries`. path_backing: PathStorage, /// True if one entry in the index has a special marker mode is_sparse: bool, // Extensions end_of_index_at_decode_time: bool, offset_table_at_decode_time: bool, tree: Option<extension::Tree>, link: Option<extension::Link>, resolve_undo: Option<extension::resolve_undo::Paths>, untracked: Option<extension::UntrackedCache>, fs_monitor: Option<extension::FsMonitor>,} mod impls { use std::fmt::{Debug, Formatter}; 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 { for entry in &self.entries { writeln!( f, "{} {}{:?} {} {}", match entry.flags.stage() { Stage::Unconflicted => " ", Stage::Base => "BASE ", Stage::Ours => "OURS ", Stage::Theirs => "THEIRS ", }, if entry.flags.is_empty() { "".to_string() } else { format!("{:?} ", entry.flags) }, entry.mode, entry.id, entry.path(self) )?; } Ok(()) } }} pub(crate) mod util { #[inline] pub fn var_int(data: &[u8]) -> Option<(u64, &[u8])> { let (num, consumed) = gix_features::decode::leb64_from_read(data).ok()?; let data = &data[consumed..]; (num, data).into() } #[inline] pub fn read_u32(data: &[u8]) -> Option<(u32, &[u8])> { data.split_at_checked(4) .map(|(num, data)| (u32::from_be_bytes(num.try_into().unwrap()), data)) } #[inline] pub fn read_u64(data: &[u8]) -> Option<(u64, &[u8])> { data.split_at_checked(8) .map(|(num, data)| (u64::from_be_bytes(num.try_into().unwrap()), data)) } #[inline] pub fn from_be_u32(b: &[u8]) -> u32 { u32::from_be_bytes(b.try_into().unwrap()) } #[inline] pub fn split_at_byte_exclusive(data: &[u8], byte: u8) -> Option<(&[u8], &[u8])> { if data.len() < 2 { return None; } data.iter().enumerate().find_map(|(idx, b)| { (*b == byte).then(|| { if idx == 0 { (&[] as &[u8], &data[1..]) } else { let (a, b) = data.split_at(idx); (a, &b[1..]) } }) }) }}
//! ## Feature Flags
#![cfg_attr(
all(doc, feature = "document-features"),
doc = ::document_features::document_features!()
)]
#![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg))]
#![deny(unsafe_code, missing_docs)]
use std::{ops::Range, path::PathBuf};
use bstr::{BStr, ByteSlice};
use filetime::FileTime;
/// `gix_hash` is made available as it's part of the public API in various places.
pub use gix_hash as hash;
/// A re-export to allow calling [`State::from_tree()`].
pub use gix_validate as validate;
///
pub mod file;
///
pub mod extension;
///
pub mod entry;
mod access;
///
pub mod init;
///
pub mod decode;
///
pub mod verify;
///
pub mod write;
pub mod fs;
/// All known versions of a git index file.
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Version {
/// Supports entries and various extensions.
V2 = 2,
/// Adds support for additional flags for each entry, called extended entries.
V3 = 3,
/// Supports deltified entry paths.
V4 = 4,
}
/// An entry in the index, identifying a non-tree item on disk.
#[derive(Clone, Eq, PartialEq)]
pub struct Entry {
/// The filesystem stat information for the file on disk.
pub stat: entry::Stat,
/// The object id for this entry's ODB representation (assuming it's up-to-date with it).
pub id: gix_hash::ObjectId,
/// Additional flags for use in algorithms and for efficiently storing stage information.
pub flags: entry::Flags,
/// The kind of item this entry represents - it's not all blobs in the index anymore.
pub mode: entry::Mode,
/// The range to lookup in the path backing to obtain the entry path relative to the repository.
/// This costs additional memory but is probably worth it given that paths can stay in one big allocation.
path: Range<usize>,
}
/// An index file whose state was read from a file on disk.
#[derive(Clone)]
pub struct File {
/// The state containing the actual index data.
pub(crate) state: State,
/// The path from which the index was read or to which it is supposed to be written.
pub(crate) path: PathBuf,
/// The checksum of all bytes prior to the checksum itself.
pub(crate) checksum: Option<gix_hash::ObjectId>,
}
/// The type to use and store paths to all entries.
pub type PathStorage = Vec<u8>;
/// The type to use and store paths to all entries, as reference
pub type PathStorageRef = [u8];
struct DirEntry<'a> {
/// The first entry in the directory
entry: &'a Entry,
/// One past the last byte of the directory in the path-backing
dir_end: usize,
}
impl DirEntry<'_> {
fn path<'a>(&self, state: &'a State) -> &'a BStr {
let range = self.entry.path.start..self.dir_end;
state.path_backing[range].as_bstr()
}
}
/// A backing store for accelerating lookups of entries in a case-sensitive and case-insensitive manner.
pub struct AccelerateLookup<'a> {
/// The entries themselves, hashed by their full icase path.
/// Icase-clashes are handled in order of occurrence and are all available for iteration.
icase_entries: hashbrown::HashTable<&'a Entry>,
/// Each hash in this table corresponds to a directory containing one or more entries.
icase_dirs: hashbrown::HashTable<DirEntry<'a>>,
}
/// An in-memory cache of a fully parsed git index file.
///
/// As opposed to a snapshot, it's meant to be altered and eventually be written back to disk or converted into a tree.
/// We treat index and its state synonymous.
///
/// # Path Format
///
/// All entry paths stored by [`State`], and all path-like arguments used to access entries, are repository-relative byte
/// strings with `/` as separator. They are not platform-native filesystem paths, must not be absolute, and must not use
/// separators like `\`; convert worktree or absolute paths to this representation before lookup or insertion.
///
/// # A note on safety
///
/// An index (i.e. [`State`]) created by hand is not guaranteed to have valid entry paths as they are entirely controlled
/// by the caller, without applying any level of validation.
///
/// This means that before using these paths to recreate files on disk, *they must be validated*.
///
/// It's notable that it's possible to manufacture tree objects which contain names like `.git/hooks/pre-commit`
/// which then will look like `.git/hooks/pre-commit` in the index, which doesn't care that the name came from a single
/// tree instead of from trees named `.git`, `hooks` and a blob named `pre-commit`. The effect is still the same - an invalid
/// path is presented in the index and its consumer must validate each path component before usage.
///
/// It's recommended to do that using `gix_worktree::Stack` which has it built-in if it's created `for_checkout()`. Alternatively
/// one can validate component names with `gix_validate::path::component()`.
#[derive(Clone)]
pub struct State {
/// The kind of object hash used when storing the underlying file.
///
/// Empty states for example won't have a single object id, so deduction of the hash used isn't always possible.
object_hash: gix_hash::Kind,
/// The time at which the state was created, indicating its freshness compared to other files on disk.
///
/// Note that on platforms that only have a precisions of a second for this time, we will treat all entries with the
/// same timestamp as this as potentially changed, checking more thoroughly if a change actually happened.
timestamp: FileTime,
version: Version,
entries: Vec<Entry>,
/// A memory area keeping all index paths, in full length, independently of the index version.
///
/// Ranges into this storage are referred to by parts of `entries`.
path_backing: PathStorage,
/// True if one entry in the index has a special marker mode
is_sparse: bool,
// Extensions
end_of_index_at_decode_time: bool,
offset_table_at_decode_time: bool,
tree: Option<extension::Tree>,
link: Option<extension::Link>,
resolve_undo: Option<extension::resolve_undo::Paths>,
untracked: Option<extension::UntrackedCache>,
fs_monitor: Option<extension::FsMonitor>,
}
mod impls {
use std::fmt::{Debug, Formatter};
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 {
for entry in &self.entries {
writeln!(
f,
"{} {}{:?} {} {}",
match entry.flags.stage() {
Stage::Unconflicted => " ",
Stage::Base => "BASE ",
Stage::Ours => "OURS ",
Stage::Theirs => "THEIRS ",
},
if entry.flags.is_empty() {
"".to_string()
} else {
format!("{:?} ", entry.flags)
},
entry.mode,
entry.id,
entry.path(self)
)?;
}
Ok(())
}
}
}
pub(crate) mod util {
#[inline]
pub fn var_int(data: &[u8]) -> Option<(u64, &[u8])> {
let (num, consumed) = gix_features::decode::leb64_from_read(data).ok()?;
let data = &data[consumed..];
(num, data).into()
}
#[inline]
pub fn read_u32(data: &[u8]) -> Option<(u32, &[u8])> {
data.split_at_checked(4)
.map(|(num, data)| (u32::from_be_bytes(num.try_into().unwrap()), data))
}
#[inline]
pub fn read_u64(data: &[u8]) -> Option<(u64, &[u8])> {
data.split_at_checked(8)
.map(|(num, data)| (u64::from_be_bytes(num.try_into().unwrap()), data))
}
#[inline]
pub fn from_be_u32(b: &[u8]) -> u32 {
u32::from_be_bytes(b.try_into().unwrap())
}
#[inline]
pub fn split_at_byte_exclusive(data: &[u8], byte: u8) -> Option<(&[u8], &[u8])> {
if data.len() < 2 {
return None;
}
data.iter().enumerate().find_map(|(idx, b)| {
(*b == byte).then(|| {
if idx == 0 {
(&[] as &[u8], &data[1..])
} else {
let (a, b) = data.split_at(idx);
(a, &b[1..])
}
})
})
}
}