diff --git a/Cargo.lock b/Cargo.lock index 871d0702344..fd9a248dbfe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -477,7 +477,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e33b5c3ee2b4ffa00ac2b00d1645cd9229ade668139bccf95f15fadcf374127b" dependencies = [ "castaway", - "itoa 1.0.2", + "itoa 1.0.3", "ryu", "serde", ] @@ -1069,7 +1069,7 @@ dependencies = [ "git-date", "git-features", "git-testtools", - "itoa 1.0.2", + "itoa 1.0.3", "nom", "pretty_assertions", "quick-error", @@ -1179,7 +1179,7 @@ dependencies = [ "bstr", "document-features", "git-testtools", - "itoa 1.0.2", + "itoa 1.0.3", "serde", "time", ] @@ -1276,6 +1276,7 @@ dependencies = [ "git-hash", "git-object", "git-testtools", + "itoa 1.0.3", "memmap2", "serde", "smallvec", @@ -1324,7 +1325,7 @@ dependencies = [ "git-testtools", "git-validate", "hex", - "itoa 1.0.2", + "itoa 1.0.3", "nom", "pretty_assertions", "quick-error", @@ -1925,9 +1926,9 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "itoa" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" [[package]] name = "jobserver" @@ -2640,7 +2641,7 @@ version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" dependencies = [ - "itoa 1.0.2", + "itoa 1.0.3", "ryu", "serde", ] @@ -2921,7 +2922,7 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217" dependencies = [ - "itoa 1.0.2", + "itoa 1.0.3", "libc", "num_threads", ] diff --git a/git-index/Cargo.toml b/git-index/Cargo.toml index b32a2984d2f..e4931817ed4 100644 --- a/git-index/Cargo.toml +++ b/git-index/Cargo.toml @@ -45,6 +45,7 @@ bstr = { version = "0.2.13", default-features = false } serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } smallvec = "1.7.0" atoi = "1.0.0" +itoa = "1.0.3" bitflags = "1.3.2" document-features = { version = "0.2.0", optional = true } diff --git a/git-index/src/decode/header.rs b/git-index/src/decode/header.rs index eed8595f738..94631006b3b 100644 --- a/git-index/src/decode/header.rs +++ b/git-index/src/decode/header.rs @@ -2,6 +2,8 @@ pub(crate) const SIZE: usize = 4 /*signature*/ + 4 /*version*/ + 4 /* num entrie use crate::{util::from_be_u32, Version}; +pub(crate) const SIGNATURE: &[u8] = b"DIRC"; + mod error { /// The error produced when failing to decode an index header. @@ -23,7 +25,6 @@ pub(crate) fn decode(data: &[u8], object_hash: git_hash::Kind) -> Result<(Versio )); } - const SIGNATURE: &[u8] = b"DIRC"; let (signature, data) = data.split_at(4); if signature != SIGNATURE { return Err(Error::Corrupt( diff --git a/git-index/src/entry/flags.rs b/git-index/src/entry/flags.rs index dec04baeddc..7d690c06fea 100644 --- a/git-index/src/entry/flags.rs +++ b/git-index/src/entry/flags.rs @@ -1,5 +1,4 @@ use crate::entry::Stage; -use crate::Version; use bitflags::bitflags; bitflags! { @@ -7,6 +6,8 @@ bitflags! { pub struct Flags: u32 { /// The mask to apply to obtain the stage number of an entry. const STAGE_MASK = 0x3000; + /// If set, additional bits need to be written to storage. + const EXTENDED = 0x4000; // TODO: could we use the pathlen ourselves to save 8 bytes? And how to handle longer paths than that? 0 as sentinel maybe? /// The mask to obtain the length of the path associated with this entry. const PATH_LEN = 0x0fff; @@ -49,9 +50,6 @@ bitflags! { /// Stored at rest const SKIP_WORKTREE = 1 << 30; - /// flags that need to be stored on disk in a V3 formatted index. - const EXTENDED_FLAGS = 1 << 29 | 1 << 30; - /// For future extension const EXTENDED_2 = 1 << 31; } @@ -64,10 +62,17 @@ impl Flags { } /// Transform ourselves to a storage representation to keep all flags which are to be persisted, - /// with the caller intending to write `version`. - pub fn to_storage(&self, version: Version) -> at_rest::Flags { - assert_eq!(version, Version::V2, "Can only encode V2 flags at the moment"); - at_rest::Flags::from_bits(self.bits() as u16).unwrap() + /// skipping all extended flags. Note that the caller has to check for the `EXTENDED` bit to be present + /// and write extended flags as well if so. + pub fn to_storage(mut self) -> at_rest::Flags { + at_rest::Flags::from_bits( + { + self.remove(Self::PATH_LEN); + self + } + .bits() as u16, + ) + .unwrap() } } @@ -89,8 +94,7 @@ pub(crate) mod at_rest { impl Flags { pub fn to_memory(self) -> super::Flags { - super::Flags::from_bits((self & (Flags::PATH_LEN | Flags::STAGE_MASK | Flags::ASSUME_VALID)).bits as u32) - .expect("PATHLEN is part of memory representation") + super::Flags::from_bits(self.bits as u32).expect("PATHLEN is part of memory representation") } } @@ -103,6 +107,10 @@ pub(crate) mod at_rest { } impl FlagsExtended { + pub fn from_flags(flags: super::Flags) -> Self { + Self::from_bits(((flags & (super::Flags::INTENT_TO_ADD | super::Flags::SKIP_WORKTREE)).bits >> 16) as u16) + .expect("valid") + } pub fn to_flags(self) -> Option { super::Flags::from_bits((self.bits as u32) << 16) } diff --git a/git-index/src/entry/mod.rs b/git-index/src/entry/mod.rs index 05e9ffc197e..0e8ffe9403b 100644 --- a/git-index/src/entry/mod.rs +++ b/git-index/src/entry/mod.rs @@ -82,6 +82,3 @@ mod _impls { } } } - -#[cfg(test)] -mod tests; diff --git a/git-index/src/entry/tests.rs b/git-index/src/entry/tests.rs deleted file mode 100644 index 8765e58eb88..00000000000 --- a/git-index/src/entry/tests.rs +++ /dev/null @@ -1,13 +0,0 @@ -use crate::entry::at_rest; -use crate::Version; - -#[test] -fn in_mem_flags_to_storage_flags_v2() { - let flag_bytes = u16::from_be_bytes(*b"\x00\x01"); - let flags_at_rest = at_rest::Flags::from_bits(flag_bytes).unwrap(); - let in_memory_flags = flags_at_rest.to_memory(); - - let output = in_memory_flags.to_storage(Version::V2); - - assert_eq!(output.bits(), flag_bytes); -} diff --git a/git-index/src/entry/write.rs b/git-index/src/entry/write.rs index 07c36f83701..8a8b00bd987 100644 --- a/git-index/src/entry/write.rs +++ b/git-index/src/entry/write.rs @@ -1,8 +1,8 @@ -use crate::{Entry, State, Version}; +use crate::{entry, Entry, State}; use std::convert::TryInto; impl Entry { - /// Serialize ourselves to `out` with path access via `state`. + /// Serialize ourselves to `out` with path access via `state`, without padding. pub fn write_to(&self, mut out: impl std::io::Write, state: &State) -> std::io::Result<()> { let stat = self.stat; out.write_all(&stat.ctime.secs.to_be_bytes())?; @@ -17,16 +17,21 @@ impl Entry { out.write_all(&stat.size.to_be_bytes())?; out.write_all(self.id.as_bytes())?; let path = self.path(state); - let path_len: u16 = path - .len() - .try_into() - .expect("Cannot handle paths longer than 16bits ever"); - assert!( - path_len <= 0xFFF, - "Paths can't be longer than 12 bits as they share space with bit flags in a u16" - ); - let version = Version::V2; // TODO: don't hardcode once `to_storage()` can do its work without assertion - out.write_all(&(self.flags.to_storage(version).bits() | path_len).to_be_bytes())?; + let path_len: u16 = if path.len() >= entry::Flags::PATH_LEN.bits() as usize { + entry::Flags::PATH_LEN.bits() as u16 + } else { + path.len() + .try_into() + .expect("we just checked that the length is smaller than 0xfff") + }; + out.write_all(&(self.flags.to_storage().bits() | path_len).to_be_bytes())?; + if self.flags.contains(entry::Flags::EXTENDED) { + out.write_all( + &entry::at_rest::FlagsExtended::from_flags(self.flags) + .bits() + .to_be_bytes(), + )?; + } out.write_all(path)?; out.write_all(b"\0") } diff --git a/git-index/src/extension/end_of_index_entry.rs b/git-index/src/extension/end_of_index_entry/decode.rs similarity index 54% rename from git-index/src/extension/end_of_index_entry.rs rename to git-index/src/extension/end_of_index_entry/decode.rs index c44d15b295c..282091dbed2 100644 --- a/git-index/src/extension/end_of_index_entry.rs +++ b/git-index/src/extension/end_of_index_entry/decode.rs @@ -1,20 +1,27 @@ -use crate::{decode::header, extension, extension::Signature, util::from_be_u32}; - -pub const SIGNATURE: Signature = *b"EOIE"; -pub const SIZE: usize = 4 /* offset to extensions */ + git_hash::Kind::Sha1.len_in_bytes(); -pub const SIZE_WITH_HEADER: usize = crate::extension::MIN_SIZE + SIZE; - +use crate::decode::header; +use crate::extension; +use crate::extension::end_of_index_entry::{MIN_SIZE, MIN_SIZE_WITH_HEADER, SIGNATURE}; +use crate::util::from_be_u32; + +/// Decode the end of index entry extension, which is no more than a glorified offset to the first byte of all extensions to allow +/// loading entries and extensions in parallel. +/// +/// Itself it's located at the end of the index file, which allows its location to be known and thus addressable. +/// From there it's possible to traverse the chunks of all set extensions, hash them, and compare that hash with all extensions +/// stored prior to this one to assure they are correct. +/// +/// If the checksum wasn't matched, we will ignoree this extension entirely. pub fn decode(data: &[u8], object_hash: git_hash::Kind) -> Option { let hash_len = object_hash.len_in_bytes(); - if data.len() < SIZE_WITH_HEADER + hash_len { + if data.len() < MIN_SIZE_WITH_HEADER + hash_len { return None; } - let start_of_eoie = data.len() - SIZE_WITH_HEADER - hash_len; + let start_of_eoie = data.len() - MIN_SIZE_WITH_HEADER - hash_len; let ext_data = &data[start_of_eoie..data.len() - hash_len]; let (signature, ext_size, ext_data) = extension::decode::header(ext_data); - if signature != SIGNATURE || ext_size as usize != SIZE { + if signature != SIGNATURE || ext_size as usize != MIN_SIZE { return None; } @@ -26,7 +33,7 @@ pub fn decode(data: &[u8], object_hash: git_hash::Kind) -> Option { let mut hasher = git_features::hash::hasher(git_hash::Kind::Sha1); let mut last_chunk = None; - for (signature, chunk) in extension::Iter::new(&data[offset..data.len() - SIZE_WITH_HEADER - hash_len]) { + for (signature, chunk) in extension::Iter::new(&data[offset..data.len() - MIN_SIZE_WITH_HEADER - hash_len]) { hasher.update(&signature); hasher.update(&(chunk.len() as u32).to_be_bytes()); last_chunk = Some(chunk); diff --git a/git-index/src/extension/end_of_index_entry/mod.rs b/git-index/src/extension/end_of_index_entry/mod.rs new file mode 100644 index 00000000000..8cef0dacfe3 --- /dev/null +++ b/git-index/src/extension/end_of_index_entry/mod.rs @@ -0,0 +1,14 @@ +use crate::{extension, extension::Signature}; + +/// The signature of the end-of-index-entry extension +pub const SIGNATURE: Signature = *b"EOIE"; +/// The minimal size of the extension, depending on the shortest hash. +pub const MIN_SIZE: usize = 4 /* offset to extensions */ + git_hash::Kind::shortest().len_in_bytes(); +/// The smallest size of the extension varying by hash kind, along with the standard extension header. +pub const MIN_SIZE_WITH_HEADER: usize = extension::MIN_SIZE + MIN_SIZE; + +mod decode; +pub use decode::decode; + +mod write; +pub use write::write_to; diff --git a/git-index/src/extension/end_of_index_entry/write.rs b/git-index/src/extension/end_of_index_entry/write.rs new file mode 100644 index 00000000000..e4fb6ba7902 --- /dev/null +++ b/git-index/src/extension/end_of_index_entry/write.rs @@ -0,0 +1,30 @@ +use crate::extension::end_of_index_entry::SIGNATURE; +use crate::extension::Signature; + +/// Write this extension to out and generate a hash of `hash_kind` over all `prior_extensions` which are specified as `(signature, size)` +/// pair. `one_past_entries` is the offset to the first byte past the entries, which is also the first byte of the signature of the +/// first extension in `prior_extensions`. Note that `prior_extensions` must have been written prior to this one, as the name suggests, +/// allowing this extension to be the last one in the index file. +/// +/// Even if there are no `prior_extensions`, this extension will be written unconditionally. +pub fn write_to( + out: &mut impl std::io::Write, + hash_kind: git_hash::Kind, + offset_to_extensions: u32, + prior_extensions: impl IntoIterator, +) -> Result<(), std::io::Error> { + out.write_all(&SIGNATURE)?; + let extension_size: u32 = 4 + hash_kind.len_in_bytes() as u32; + out.write_all(&extension_size.to_be_bytes())?; + + out.write_all(&offset_to_extensions.to_be_bytes())?; + + let mut hasher = git_features::hash::hasher(hash_kind); + for (signature, size) in prior_extensions { + hasher.update(&signature); + hasher.update(&size.to_be_bytes()); + } + out.write_all(&hasher.digest())?; + + Ok(()) +} diff --git a/git-index/src/extension/mod.rs b/git-index/src/extension/mod.rs index 3cd6ad77a31..bf71119c734 100644 --- a/git-index/src/extension/mod.rs +++ b/git-index/src/extension/mod.rs @@ -1,7 +1,8 @@ use bstr::BString; use smallvec::SmallVec; -const MIN_SIZE: usize = 4 /* signature */ + 4 /* size */; +/// The size of the smallest possible exstension, which is no more than a signature and a 0 indicating its size. +pub const MIN_SIZE: usize = 4 /* signature */ + 4 /* size */; /// The kind of index extension. pub type Signature = [u8; 4]; @@ -25,7 +26,8 @@ pub struct Tree { pub id: git_hash::ObjectId, /// The amount of non-tree items in this directory tree, including sub-trees, recursively. /// The value of the top-level tree is thus equal to the value of the total amount of entries. - pub num_entries: u32, + /// If `None`, the tree is considered invalid and needs to be refreshed + pub num_entries: Option, /// The child-trees below the current tree. pub children: Vec, } @@ -77,7 +79,8 @@ pub(crate) mod decode; /// pub mod tree; -pub(crate) mod end_of_index_entry; +/// +pub mod end_of_index_entry; pub(crate) mod index_entry_offset_table; diff --git a/git-index/src/extension/tree/decode.rs b/git-index/src/extension/tree/decode.rs index ca8f42cf774..09f5dc267d6 100644 --- a/git-index/src/extension/tree/decode.rs +++ b/git-index/src/extension/tree/decode.rs @@ -1,6 +1,7 @@ use crate::extension::Tree; use crate::util::{split_at_byte_exclusive, split_at_pos}; use git_hash::ObjectId; +use std::convert::TryInto; /// A recursive data structure pub fn decode(data: &[u8], object_hash: git_hash::Kind) -> Option { @@ -17,13 +18,20 @@ fn one_recursive(data: &[u8], hash_len: usize) -> Option<(Tree, &[u8])> { let (path, data) = split_at_byte_exclusive(data, 0)?; let (entry_count, data) = split_at_byte_exclusive(data, b' ')?; - let num_entries: u32 = atoi::atoi(entry_count)?; + let num_entries: i32 = atoi::atoi(entry_count)?; let (subtree_count, data) = split_at_byte_exclusive(data, b'\n')?; let subtree_count: usize = atoi::atoi(subtree_count)?; - let (hash, mut data) = split_at_pos(data, hash_len)?; - let id = ObjectId::from(hash); + let (id, mut data) = if num_entries >= 0 { + let (hash, data) = split_at_pos(data, hash_len)?; + (ObjectId::from(hash), data) + } else { + ( + ObjectId::null(git_hash::Kind::from_hex_len(hash_len * 2).expect("valid hex_len")), + data, + ) + }; let mut subtrees = Vec::with_capacity(subtree_count); for _ in 0..subtree_count { @@ -42,7 +50,7 @@ fn one_recursive(data: &[u8], hash_len: usize) -> Option<(Tree, &[u8])> { Some(( Tree { id, - num_entries, + num_entries: num_entries.try_into().ok(), name: path.into(), children: subtrees, }, diff --git a/git-index/src/extension/tree/mod.rs b/git-index/src/extension/tree/mod.rs index b0e17be23cf..f2075939978 100644 --- a/git-index/src/extension/tree/mod.rs +++ b/git-index/src/extension/tree/mod.rs @@ -16,6 +16,6 @@ mod tests { #[test] fn size_of_tree() { - assert_eq!(std::mem::size_of::(), 80); + assert_eq!(std::mem::size_of::(), 88); } } diff --git a/git-index/src/extension/tree/verify.rs b/git-index/src/extension/tree/verify.rs index d384a7f04bd..15ff4565365 100644 --- a/git-index/src/extension/tree/verify.rs +++ b/git-index/src/extension/tree/verify.rs @@ -2,7 +2,7 @@ use crate::extension::Tree; use bstr::{BString, ByteSlice}; use std::cmp::Ordering; -/// The error returned by [Tree::verify()][super::Tree::verify()]. +/// The error returned by [Tree::verify()][crate::extension::Tree::verify()]. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { @@ -57,7 +57,7 @@ impl Tree { let mut entries = 0; let mut prev = None::<&Tree>; for child in children { - entries += child.num_entries; + entries += child.num_entries.unwrap_or(0); if let Some(prev) = prev { if prev.name.cmp(&child.name) != Ordering::Less { return Err(Error::OutOfOrder { @@ -98,11 +98,11 @@ impl Tree { // This is actually needed here as it's a mut ref, which isn't copy. We do a re-borrow here. #[allow(clippy::needless_option_as_deref)] let actual_num_entries = verify_recursive(child.id, &child.children, find_buf.as_deref_mut(), find)?; - if let Some(actual) = actual_num_entries { - if actual > child.num_entries { + if let Some((actual, num_entries)) = actual_num_entries.zip(child.num_entries) { + if actual > num_entries { return Err(Error::EntriesCount { actual, - expected: child.num_entries, + expected: num_entries, }); } } @@ -118,11 +118,11 @@ impl Tree { let mut buf = Vec::new(); let declared_entries = verify_recursive(self.id, &self.children, use_find.then(|| &mut buf), &mut find)?; - if let Some(actual) = declared_entries { - if actual > self.num_entries { + if let Some((actual, num_entries)) = declared_entries.zip(self.num_entries) { + if actual > num_entries { return Err(Error::EntriesCount { actual, - expected: self.num_entries, + expected: num_entries, }); } } diff --git a/git-index/src/extension/tree/write.rs b/git-index/src/extension/tree/write.rs index 4effaec1454..08dc47b4d32 100644 --- a/git-index/src/extension/tree/write.rs +++ b/git-index/src/extension/tree/write.rs @@ -5,16 +5,22 @@ impl Tree { /// Serialize this instance to `out`. pub fn write_to(&self, mut out: impl std::io::Write) -> Result<(), std::io::Error> { fn tree_entry(out: &mut impl std::io::Write, tree: &Tree) -> Result<(), std::io::Error> { - let num_entries_ascii = tree.num_entries.to_string(); - let num_children_ascii = tree.children.len().to_string(); + let mut buf = itoa::Buffer::new(); + let num_entries = match tree.num_entries { + Some(num_entries) => buf.format(num_entries), + None => buf.format(-1), + }; out.write_all(tree.name.as_slice())?; out.write_all(b"\0")?; - out.write_all(num_entries_ascii.as_bytes())?; + out.write_all(num_entries.as_bytes())?; out.write_all(b" ")?; - out.write_all(num_children_ascii.as_bytes())?; + let num_children = buf.format(tree.children.len()); + out.write_all(num_children.as_bytes())?; out.write_all(b"\n")?; - out.write_all(tree.id.as_bytes())?; + if tree.num_entries.is_some() { + out.write_all(tree.id.as_bytes())?; + } for child in &tree.children { tree_entry(out, child)?; @@ -25,7 +31,7 @@ impl Tree { let signature = tree::SIGNATURE; - let estimated_size = self.num_entries * (300 + 3 + 1 + 3 + 1 + 20); + let estimated_size = self.num_entries.unwrap_or(0) * (300 + 3 + 1 + 3 + 1 + 20); let mut entries: Vec = Vec::with_capacity(estimated_size as usize); tree_entry(&mut entries, self)?; diff --git a/git-index/src/file/write.rs b/git-index/src/file/write.rs index 39467ce1ca7..04682a176fa 100644 --- a/git-index/src/file/write.rs +++ b/git-index/src/file/write.rs @@ -1,13 +1,15 @@ -use crate::{write, File}; +use crate::{write, File, Version}; use git_features::hash; impl File { - /// Write the index to `out` with `options`, to be readable by [`File::at()`]. - pub fn write_to(&self, mut out: &mut impl std::io::Write, options: write::Options) -> std::io::Result<()> { + /// Write the index to `out` with `options`, to be readable by [`File::at()`], returning the version that was actually written + /// to retain all information of this index. + pub fn write_to(&self, mut out: &mut impl std::io::Write, options: write::Options) -> std::io::Result { let mut hasher = hash::Write::new(&mut out, options.hash_kind); - self.state.write_to(&mut hasher, options)?; + let version = self.state.write_to(&mut hasher, options)?; let hash = hasher.hash.digest(); - out.write_all(&hash) + out.write_all(&hash)?; + Ok(version) } } diff --git a/git-index/src/write.rs b/git-index/src/write.rs index 9986c0e95d0..6de3298d386 100644 --- a/git-index/src/write.rs +++ b/git-index/src/write.rs @@ -1,38 +1,61 @@ use crate::write::util::CountBytes; -use crate::{extension, State, Version}; +use crate::{entry, extension, State, Version}; use std::convert::TryInto; use std::io::Write; +/// A way to specify which extensions to write. +#[derive(Debug, Copy, Clone)] +pub enum Extensions { + /// Writes all available extensions to avoid loosing any information, and to allow accelerated reading of the index file. + All, + /// Only write the given extensions, with each extension being marked by a boolean flag. + Given { + /// Write the tree-cache extension, if present. + tree_cache: bool, + /// Write the end-of-index-entry extension. + end_of_index_entry: bool, + }, + /// Write no extension at all for what should be the smallest possible index + None, +} + +impl Default for Extensions { + fn default() -> Self { + Extensions::All + } +} + +impl Extensions { + /// Returns `Some(signature)` if it should be written out. + pub fn should_write(&self, signature: extension::Signature) -> Option { + match self { + Extensions::None => None, + Extensions::All => Some(signature), + Extensions::Given { + tree_cache, + end_of_index_entry, + } => match signature { + extension::tree::SIGNATURE => tree_cache, + extension::end_of_index_entry::SIGNATURE => end_of_index_entry, + _ => &false, + } + .then(|| signature), + } + } +} + /// The options for use when [writing an index][State::write_to()]. /// -/// Note that default options write -#[derive(Debug, PartialEq, Eq, Clone, Copy)] +/// Note that default options write either index V2 or V3 depending on the content of the entries. +#[derive(Debug, Default, Clone, Copy)] pub struct Options { /// The hash kind to use when writing the index file. /// /// It is not always possible to infer the hash kind when reading an index, so this is required. pub hash_kind: git_hash::Kind, - /// The index version to write. Note that different versions affect the format and ultimately the size. - pub version: Version, - - /// If true, write the tree-cache extension, if present. - // TODO: should we not write all we have by default to be lossless, but provide options to those who seek them? - pub tree_cache_extension: bool, - /// If true, write the end-of-index-entry extension. - // TODO: figure out if this is implied by other options, for instance multi-threading. - pub end_of_index_entry_extension: bool, -} -impl Default for Options { - fn default() -> Self { - Self { - hash_kind: git_hash::Kind::default(), - /// TODO: make this 'automatic' by default to determine the correct index version - not all versions can represent all in-memory states. - version: Version::V2, - tree_cache_extension: true, - end_of_index_entry_extension: true, - } - } + /// Configures which extensions to write + pub extensions: Extensions, } impl State { @@ -40,18 +63,9 @@ impl State { pub fn write_to( &self, out: &mut impl std::io::Write, - Options { - hash_kind, - version, - tree_cache_extension, - end_of_index_entry_extension, - }: Options, - ) -> std::io::Result<()> { - assert_eq!( - version, - Version::V2, - "can only write V2 at the moment, please come back later" - ); + Options { hash_kind, extensions }: Options, + ) -> std::io::Result { + let version = self.detect_required_version(); let mut write = CountBytes::new(out); let num_entries = self @@ -60,20 +74,49 @@ impl State { .try_into() .expect("definitely not 4billion entries"); - let header_offset = header(&mut write, version, num_entries)?; - let entries_offset = entries(&mut write, self, header_offset)?; - let tree_offset = tree_cache_extension - .then(|| self.tree()) - .flatten() - .map(|tree| tree.write_to(&mut write).map(|_| write.count)) - .transpose()? - .unwrap_or(entries_offset); - - if num_entries > 0 && end_of_index_entry_extension { - end_of_index_entry_ext(write.inner, hash_kind, entries_offset, tree_offset)?; + let offset_to_entries = header(&mut write, version, num_entries)?; + let offset_to_extensions = entries(&mut write, self, offset_to_entries)?; + + let extension_toc = { + type WriteExtFn<'a> = &'a dyn Fn(&mut dyn std::io::Write) -> Option>; + let extensions: &[WriteExtFn<'_>] = &[&|write| { + extensions + .should_write(extension::tree::SIGNATURE) + .and_then(|signature| self.tree().map(|tree| tree.write_to(write).map(|_| signature))) + }]; + + let mut offset_to_previous_ext = offset_to_extensions; + let mut out = Vec::with_capacity(5); + for write_ext in extensions { + if let Some(signature) = write_ext(&mut write).transpose()? { + let offset_past_ext = write.count; + let ext_size = offset_past_ext - offset_to_previous_ext - (extension::MIN_SIZE as u32); + offset_to_previous_ext = offset_past_ext; + out.push((signature, ext_size)); + } + } + out + }; + + if num_entries > 0 + && extensions + .should_write(extension::end_of_index_entry::SIGNATURE) + .is_some() + && !extension_toc.is_empty() + { + extension::end_of_index_entry::write_to(write.inner, hash_kind, offset_to_extensions, extension_toc)? } - Ok(()) + Ok(version) + } +} + +impl State { + fn detect_required_version(&self) -> Version { + self.entries + .iter() + .find_map(|e| e.flags.contains(entry::Flags::EXTENDED).then(|| Version::V3)) + .unwrap_or(Version::V2) } } @@ -82,15 +125,13 @@ fn header( version: Version, num_entries: u32, ) -> Result { - let signature = b"DIRC"; - let version = match version { Version::V2 => 2_u32.to_be_bytes(), Version::V3 => 3_u32.to_be_bytes(), Version::V4 => 4_u32.to_be_bytes(), }; - out.write_all(signature)?; + out.write_all(crate::decode::header::SIGNATURE)?; out.write_all(&version)?; out.write_all(&num_entries.to_be_bytes())?; @@ -116,31 +157,6 @@ fn entries( Ok(out.count) } -fn end_of_index_entry_ext( - out: &mut impl std::io::Write, - hash_kind: git_hash::Kind, - entries_offset: u32, - tree_offset: u32, -) -> Result<(), std::io::Error> { - let signature = extension::end_of_index_entry::SIGNATURE; - let extension_size = 4 + hash_kind.len_in_bytes() as u32; - - let mut hasher = git_features::hash::hasher(hash_kind); - let tree_size = (tree_offset - entries_offset).saturating_sub(8); - if tree_size > 0 { - hasher.update(&extension::tree::SIGNATURE); - hasher.update(&tree_size.to_be_bytes()); - } - let hash = hasher.digest(); - - out.write_all(&signature)?; - out.write_all(&extension_size.to_be_bytes())?; - out.write_all(&entries_offset.to_be_bytes())?; - out.write_all(&hash)?; - - Ok(()) -} - mod util { use std::convert::TryFrom; diff --git a/git-index/tests/index-multi-threaded.rs b/git-index/tests/index-multi-threaded.rs index 936bcb7f428..fd3abfb9ce5 100644 --- a/git-index/tests/index-multi-threaded.rs +++ b/git-index/tests/index-multi-threaded.rs @@ -1,2 +1,4 @@ +pub use git_testtools::Result; + mod index; pub use index::*; diff --git a/git-index/tests/index-single-threaded.rs b/git-index/tests/index-single-threaded.rs index 4dcec528c1e..4a90ca77939 100644 --- a/git-index/tests/index-single-threaded.rs +++ b/git-index/tests/index-single-threaded.rs @@ -1,3 +1,5 @@ +pub use git_testtools::Result; + #[cfg(not(feature = "internal-testing-git-features-parallel"))] mod index; #[cfg(not(feature = "internal-testing-git-features-parallel"))] diff --git a/git-index/tests/index/file/read.rs b/git-index/tests/index/file/read.rs index 02356f4ce75..4d983a77f11 100644 --- a/git-index/tests/index/file/read.rs +++ b/git-index/tests/index/file/read.rs @@ -13,8 +13,12 @@ fn verify(index: git_index::File) -> git_index::File { index } -fn loose_file(name: &str) -> git_index::File { - let path = git_testtools::fixture_path(Path::new("loose_index").join(name).with_extension("git-index")); +pub(crate) fn loose_file_path(name: &str) -> PathBuf { + git_testtools::fixture_path(Path::new("loose_index").join(name).with_extension("git-index")) +} + +pub(crate) fn loose_file(name: &str) -> git_index::File { + let path = loose_file_path(name); let file = git_index::File::at(path, git_index::decode::Options::default()).unwrap(); verify(file) } @@ -48,7 +52,7 @@ fn v2_with_single_entry_tree_and_eoie_ext() { assert_eq!(entry.path(&file.state), "a"); let tree = file.tree().unwrap(); - assert_eq!(tree.num_entries, 1); + assert_eq!(tree.num_entries.unwrap_or_default(), 1); assert_eq!(tree.id, hex_to_id("496d6428b9cf92981dc9495211e6e1120fb6f2ba")); assert!(tree.name.is_empty()); assert!(tree.children.is_empty()); @@ -60,7 +64,7 @@ fn v2_empty() { assert_eq!(file.version(), Version::V2); assert_eq!(file.entries().len(), 0); let tree = file.tree().unwrap(); - assert_eq!(tree.num_entries, 0); + assert_eq!(tree.num_entries.unwrap_or_default(), 0); assert!(tree.name.is_empty()); assert!(tree.children.is_empty()); assert_eq!(tree.id, hex_to_id("4b825dc642cb6eb9a060e54bf8d69288fbee4904")); @@ -82,13 +86,13 @@ fn v2_with_multiple_entries_without_eoie_ext() { let tree = file.tree().unwrap(); assert_eq!(tree.id, hex_to_id("c9b29c3168d8e677450cc650238b23d9390801fb")); - assert_eq!(tree.num_entries, 6); + assert_eq!(tree.num_entries.unwrap_or_default(), 6); assert!(tree.name.is_empty()); assert_eq!(tree.children.len(), 1); let tree = &tree.children[0]; assert_eq!(tree.id, hex_to_id("765b32c65d38f04c4f287abda055818ec0f26912")); - assert_eq!(tree.num_entries, 3); + assert_eq!(tree.num_entries.unwrap_or_default(), 3); assert_eq!(tree.name.as_bstr(), "d"); } @@ -139,6 +143,15 @@ fn v2_very_long_path() { .chain(std::iter::once('q')) .collect::() ); + assert!( + file.tree().is_some(), + "Tree has invalid entries, but that shouldn't prevent us from loading it" + ); + let tree = file.tree().expect("present"); + assert_eq!(tree.num_entries, None, "root tree has invalid entries actually"); + assert_eq!(tree.name.as_bstr(), ""); + assert_eq!(tree.num_entries, None, "it's marked invalid actually"); + assert!(tree.id.is_null(), "there is no id for the root") } #[test] diff --git a/git-index/tests/index/file/write.rs b/git-index/tests/index/file/write.rs index fcaae3396f9..03e791cdcb5 100644 --- a/git-index/tests/index/file/write.rs +++ b/git-index/tests/index/file/write.rs @@ -1,150 +1,143 @@ +use crate::fixture_index_path; +use crate::index::file::read::loose_file_path; use filetime::FileTime; use git_index::verify::extensions::no_find; -use git_index::{decode, write, State, Version}; -use std::cmp::{max, min}; +use git_index::write::Options; +use git_index::{decode, entry, extension, write, State, Version}; +/// Round-trips should eventually be possible for all files we have, as we write them back exactly as they were read. #[test] -fn roundtrips() { +fn roundtrips() -> crate::Result { + enum Kind { + Generated(&'static str), + Loose(&'static str), + } + use Kind::*; let input = [ - ("V2_empty", write::Options::default()), - ("v2", write::Options::default()), - ( - "v2_more_files", - write::Options { - end_of_index_entry_extension: false, - ..write::Options::default() - }, - ), + (Loose("extended-flags"), all_ext_but_eoie()), + (Loose("conflicting-file"), all_ext_but_eoie()), + (Loose("very-long-path"), all_ext_but_eoie()), + (Generated("v2"), Options::default()), + (Generated("V2_empty"), Options::default()), + (Generated("v2_more_files"), all_ext_but_eoie()), ]; for (fixture, options) in input { - let path = crate::fixture_index_path(fixture); - let expected_index = git_index::File::at(&path, decode::Options::default()).unwrap(); - let expected_bytes = std::fs::read(&path).unwrap(); - let mut out_bytes = Vec::::new(); - - expected_index.write_to(&mut out_bytes, options).unwrap(); - let (out_index, _) = State::from_bytes(&out_bytes, FileTime::now(), decode::Options::default()).unwrap(); - - compare_states(&out_index, &expected_index, options, fixture); + let (path, fixture) = match fixture { + Generated(name) => (fixture_index_path(name), name), + Loose(name) => (loose_file_path(name), name), + }; + let expected = git_index::File::at(&path, decode::Options::default())?; + let expected_bytes = std::fs::read(&path)?; + let mut out_bytes = Vec::new(); + + let actual_version = expected.write_to(&mut out_bytes, options)?; + assert_eq!( + actual_version, + expected.version(), + "{} didn't write the expected version", + fixture + ); + let (actual, _) = State::from_bytes(&out_bytes, FileTime::now(), decode::Options::default())?; + + compare_states(&actual, actual_version, &expected, options, fixture); compare_raw_bytes(&out_bytes, &expected_bytes, fixture); } + Ok(()) } #[test] -fn v2_index_no_extensions() { - let input = [ - "V2_empty", - "v2", - "v2_more_files", - "v2_split_index", - "v4_more_files_IEOT", - ]; - - for fixture in input { - let path = crate::fixture_index_path(fixture); - let expected = git_index::File::at(&path, decode::Options::default()).unwrap(); - - let mut out = Vec::::new(); - let options = write::Options { +fn state_comparisons_with_various_extension_configurations() { + fn options_with(extensions: write::Extensions) -> Options { + Options { hash_kind: git_hash::Kind::Sha1, - version: Version::V2, - tree_cache_extension: false, - end_of_index_entry_extension: false, - }; - - expected.write_to(&mut out, options).unwrap(); - - let (generated, _) = State::from_bytes(&out, FileTime::now(), decode::Options::default()).unwrap(); - compare_states(&generated, &expected, options, fixture); + extensions, + } } -} - -#[test] -fn v2_index_tree_extensions() { - let input = [ - "V2_empty", - "v2", - "v2_more_files", - "v2_split_index", - "v4_more_files_IEOT", - ]; - - for fixture in input { - let path = crate::fixture_index_path(fixture); - let expected = git_index::File::at(&path, decode::Options::default()).unwrap(); - - let mut out = Vec::::new(); - let options = write::Options { - hash_kind: git_hash::Kind::Sha1, - version: Version::V2, - tree_cache_extension: true, - end_of_index_entry_extension: false, - }; - - expected.write_to(&mut out, options).unwrap(); - let (generated, _) = State::from_bytes(&out, FileTime::now(), decode::Options::default()).unwrap(); - compare_states(&generated, &expected, options, fixture); + enum Kind { + Generated(&'static str), + Loose(&'static str), + } + use Kind::*; + + for fixture in [ + Loose("extended-flags"), + Loose("conflicting-file"), + Loose("very-long-path"), + Loose("FSMN"), + Loose("REUC"), + Loose("UNTR-with-oids"), + Loose("UNTR"), + Generated("V2_empty"), + Generated("v2"), + Generated("v2_more_files"), + Generated("v2_split_index"), + Generated("v4_more_files_IEOT"), + ] { + for options in [ + options_with(write::Extensions::None), + options_with(write::Extensions::All), + options_with(write::Extensions::Given { + tree_cache: true, + end_of_index_entry: true, + }), + options_with(write::Extensions::Given { + tree_cache: false, + end_of_index_entry: true, + }), + ] { + let (path, fixture) = match fixture { + Generated(name) => (fixture_index_path(name), name), + Loose(name) => (loose_file_path(name), name), + }; + let expected = git_index::File::at(&path, Default::default()).unwrap(); + + let mut out = Vec::::new(); + let actual_version = expected.write_to(&mut out, options).unwrap(); + + let (actual, _) = State::from_bytes(&out, FileTime::now(), decode::Options::default()).unwrap(); + compare_states(&actual, actual_version, &expected, options, fixture); + } } } #[test] -fn v2_index_eoie_extensions() { - let input = [ - "V2_empty", - "v2", - "v2_more_files", - "v2_split_index", - "v4_more_files_IEOT", - ]; - - for fixture in input { - let path = crate::fixture_index_path(fixture); - let expected = git_index::File::at(&path, decode::Options::default()).unwrap(); - - let mut out = Vec::::new(); - let options = write::Options { - hash_kind: git_hash::Kind::Sha1, - version: Version::V2, - tree_cache_extension: false, - end_of_index_entry_extension: true, - }; +fn extended_flags_automatically_upgrade_the_version_to_avoid_data_loss() -> crate::Result { + let mut expected = git_index::File::at(fixture_index_path("V2"), Default::default())?; + assert_eq!(expected.version(), Version::V2); + expected.entries_mut()[0].flags.insert(entry::Flags::EXTENDED); - expected.write_to(&mut out, options).unwrap(); + let mut buf = Vec::new(); + let actual_version = expected.write_to(&mut buf, Default::default())?; + assert_eq!(actual_version, Version::V3, "extended flags need V3"); - let (generated, _) = State::from_bytes(&out, FileTime::now(), decode::Options::default()).unwrap(); - compare_states(&generated, &expected, options, fixture); - } + Ok(()) } -fn compare_states(generated: &State, expected: &State, options: write::Options, fixture: &str) { - generated.verify_entries().expect("valid"); - generated.verify_extensions(false, no_find).expect("valid"); - assert_eq!(generated.version(), options.version, "version mismatch in {}", fixture); +fn compare_states(actual: &State, actual_version: Version, expected: &State, options: Options, fixture: &str) { + actual.verify_entries().expect("valid"); + actual.verify_extensions(false, no_find).expect("valid"); + + assert_eq!(actual.version(), actual_version, "version mismatch in {}", fixture); assert_eq!( - generated.tree(), - match options.tree_cache_extension { - true => expected.tree(), - false => None, - }, + actual.tree(), + options + .extensions + .should_write(extension::tree::SIGNATURE) + .and_then(|_| expected.tree()), "tree extension mismatch in {}", fixture ); assert_eq!( - generated.entries().len(), + actual.entries().len(), expected.entries().len(), "entry count mismatch in {}", fixture ); + assert_eq!(actual.entries(), expected.entries(), "entries mismatch in {}", fixture); assert_eq!( - generated.entries(), - expected.entries(), - "entries mismatch in {}", - fixture - ); - assert_eq!( - generated.path_backing(), + actual.path_backing(), expected.path_backing(), "path_backing mismatch in {}", fixture @@ -157,15 +150,25 @@ fn compare_raw_bytes(generated: &[u8], expected: &[u8], fixture: &str) { let print_range = 10; for (index, (a, b)) in generated.iter().zip(expected.iter()).enumerate() { if a != b { - let range_left = max(index - print_range, 0); - let range_right = min(index + print_range, generated.len()); + let range_left = index.saturating_sub(print_range); + let range_right = (index + print_range).min(generated.len()); let generated = &generated[range_left..range_right]; let expected = &expected[range_left..range_right]; panic! {"\n\nRoundtrip failed for index in fixture {:?} at position {:?}\n\ - \t Input: ... {:?} ...\n\ + \t Actual: ... {:?} ...\n\ \tExpected: ... {:?} ...\n\n\ ", &fixture, index, generated, expected} } } } + +fn all_ext_but_eoie() -> Options { + Options { + extensions: write::Extensions::Given { + end_of_index_entry: false, + tree_cache: true, + }, + ..Options::default() + } +} diff --git a/git-repository/src/repository/identity.rs b/git-repository/src/repository/identity.rs index a5f9207fe7f..baa9684d393 100644 --- a/git-repository/src/repository/identity.rs +++ b/git-repository/src/repository/identity.rs @@ -1,8 +1,5 @@ use std::borrow::Cow; -use git_actor::SignatureRef; -use git_config::File; - use crate::{bstr::BString, permission}; /// Identity handling. @@ -15,8 +12,8 @@ impl crate::Repository { /// # Note /// /// The values are cached when the repository is instantiated. - pub fn user_default(&self) -> SignatureRef<'_> { - SignatureRef { + pub fn user_default(&self) -> git_actor::SignatureRef<'_> { + git_actor::SignatureRef { name: "gitoxide".into(), email: "gitoxide@localhost".into(), time: git_date::Time::now_local_or_utc(), @@ -105,7 +102,7 @@ impl Personas { fn env_var(name: &str) -> Option { std::env::var_os(name).map(|value| git_path::into_bstr(Cow::Owned(value.into())).into_owned()) } - fn entity_in_section(name: &str, config: &File<'_>) -> (Option, Option) { + fn entity_in_section(name: &str, config: &git_config::File<'_>) -> (Option, Option) { config .section(name, None) .map(|section| { diff --git a/gitoxide-core/src/index/information.rs b/gitoxide-core/src/index/information.rs index a364eb5ed5c..eddfad2ec36 100644 --- a/gitoxide-core/src/index/information.rs +++ b/gitoxide-core/src/index/information.rs @@ -14,7 +14,7 @@ mod serde_only { /// The id of the directory tree of the associated tree object. id: String, /// The amount of non-tree entries contained within, and definitely not zero. - num_entries: u32, + num_entries: Option, children: Vec, } diff --git a/src/porcelain/main.rs b/src/porcelain/main.rs index af1c7d8b9a5..dd155077972 100644 --- a/src/porcelain/main.rs +++ b/src/porcelain/main.rs @@ -35,7 +35,7 @@ pub fn main() -> Result<()> { ), Subcommands::Init { directory } => core::repository::init(directory).map(|_| ()), #[cfg(feature = "gitoxide-core-tools")] - Subcommands::Tools(tool) => match tool { + Subcommands::Tool(tool) => match tool { crate::porcelain::options::ToolCommands::EstimateHours(crate::porcelain::options::EstimateHours { working_dir, refname, diff --git a/src/porcelain/options.rs b/src/porcelain/options.rs index 9d3a25aaab6..af008a87371 100644 --- a/src/porcelain/options.rs +++ b/src/porcelain/options.rs @@ -35,7 +35,7 @@ pub enum Subcommands { #[cfg(feature = "gitoxide-core-tools")] /// A selection of useful tools #[clap(subcommand)] - Tools(ToolCommands), + Tool(ToolCommands), #[cfg(debug_assertions)] Panic, } diff --git a/tests/journey/ein.sh b/tests/journey/ein.sh index b5edd4644d6..a0d2f7d8e49 100644 --- a/tests/journey/ein.sh +++ b/tests/journey/ein.sh @@ -30,35 +30,35 @@ title "Porcelain ${kind}" ) snapshot="$snapshot/porcelain" (with_program tree - (when "using the 'tools' subcommand" - title "gix tools…" + (when "using the 'tool' subcommand" + title "ein tool" (with "a repo with a tiny commit history" (small-repo-in-sandbox - title "gix tools estimate-hours" + title "ein tool estimate-hours" (when "running 'estimate-hours'" snapshot="$snapshot/estimate-hours" (with "no arguments" it "succeeds and prints only a summary" && { WITH_SNAPSHOT="$snapshot/no-args-success" \ - expect_run_sh $SUCCESSFULLY "$exe tools estimate-hours 2>/dev/null" + expect_run_sh $SUCCESSFULLY "$exe tool estimate-hours 2>/dev/null" } ) (with "the show-pii argument" it "succeeds and shows information identifying people before the summary" && { WITH_SNAPSHOT="$snapshot/show-pii-success" \ - expect_run_sh $SUCCESSFULLY "$exe tools estimate-hours --show-pii 2>/dev/null" + expect_run_sh $SUCCESSFULLY "$exe tool estimate-hours --show-pii 2>/dev/null" } ) (with "the omit-unify-identities argument" it "succeeds and doesn't show unified identities (in this case there is only one author anyway)" && { WITH_SNAPSHOT="$snapshot/no-unify-identities-success" \ - expect_run_sh $SUCCESSFULLY "$exe tools estimate-hours --omit-unify-identities 2>/dev/null" + expect_run_sh $SUCCESSFULLY "$exe t estimate-hours --omit-unify-identities 2>/dev/null" } ) (with "a branch name that doesn't exist" it "fails and shows a decent enough error message" && { WITH_SNAPSHOT="$snapshot/invalid-branch-name-failure" \ - expect_run_sh $WITH_FAILURE "$exe -q tools estimate-hours . foobar" + expect_run_sh $WITH_FAILURE "$exe -q t estimate-hours . foobar" } ) ) @@ -71,25 +71,25 @@ title "Porcelain ${kind}" repo-with-remotes special-origin special-name https://example.com/special-origin repo-with-remotes no-origin repo-with-remotes a-non-bare-repo-with-extension.git origin https://example.com/a-repo-with-extension.git - snapshot="$snapshot/tools" + snapshot="$snapshot/tool" - title "gix tools find" + title "ein tool find" (when "running 'find'" snapshot="$snapshot/find" (with "no arguments" it "succeeds and prints a list of repository work directories" && { WITH_SNAPSHOT="$snapshot/no-args-success" \ - expect_run_sh $SUCCESSFULLY "$exe tools find 2>/dev/null" + expect_run_sh $SUCCESSFULLY "$exe tool find 2>/dev/null" } ) ) - title "gix tools organize" + title "ein tool organize" (when "running 'organize'" snapshot="$snapshot/organize" (with "no arguments" it "succeeds and informs about the operations that it WOULD do" && { WITH_SNAPSHOT="$snapshot/no-args-success" \ - expect_run_sh $SUCCESSFULLY "$exe tools organize 2>/dev/null" + expect_run_sh $SUCCESSFULLY "$exe tool organize 2>/dev/null" } it "does not change the directory structure at all" && { @@ -101,7 +101,7 @@ title "Porcelain ${kind}" (with "--execute" it "succeeds" && { WITH_SNAPSHOT="$snapshot/execute-success" \ - expect_run_sh $SUCCESSFULLY "$exe tools organize --execute 2>/dev/null" + expect_run_sh $SUCCESSFULLY "$exe tool organize --execute 2>/dev/null" } it "changes the directory structure" && { @@ -113,7 +113,7 @@ title "Porcelain ${kind}" (with "--execute again" it "succeeds" && { WITH_SNAPSHOT="$snapshot/execute-success" \ - expect_run_sh $SUCCESSFULLY "$exe tools organize --execute 2>/dev/null" + expect_run_sh $SUCCESSFULLY "$exe tool organize --execute 2>/dev/null" } it "does not alter the directory structure as these are already in place" && { @@ -133,7 +133,7 @@ title "Porcelain ${kind}" ) ) - title "gix init" + title "ein init" (when "running 'init'" snapshot="$snapshot/init" (with "no argument" diff --git a/tests/snapshots/porcelain/tools/find/no-args-success b/tests/snapshots/porcelain/tool/find/no-args-success similarity index 100% rename from tests/snapshots/porcelain/tools/find/no-args-success rename to tests/snapshots/porcelain/tool/find/no-args-success diff --git a/tests/snapshots/porcelain/tool/no-args-failure b/tests/snapshots/porcelain/tool/no-args-failure new file mode 100644 index 00000000000..6ed2fe8d656 --- /dev/null +++ b/tests/snapshots/porcelain/tool/no-args-failure @@ -0,0 +1,6 @@ +error: 'ein tool' requires a subcommand but one was not provided + +USAGE: + ein tool + +For more information try --help \ No newline at end of file diff --git a/tests/snapshots/porcelain/tools/organize/directory-structure-after-organize b/tests/snapshots/porcelain/tool/organize/directory-structure-after-organize similarity index 100% rename from tests/snapshots/porcelain/tools/organize/directory-structure-after-organize rename to tests/snapshots/porcelain/tool/organize/directory-structure-after-organize diff --git a/tests/snapshots/porcelain/tools/organize/execute-success b/tests/snapshots/porcelain/tool/organize/execute-success similarity index 100% rename from tests/snapshots/porcelain/tools/organize/execute-success rename to tests/snapshots/porcelain/tool/organize/execute-success diff --git a/tests/snapshots/porcelain/tools/organize/initial-directory-structure b/tests/snapshots/porcelain/tool/organize/initial-directory-structure similarity index 100% rename from tests/snapshots/porcelain/tools/organize/initial-directory-structure rename to tests/snapshots/porcelain/tool/organize/initial-directory-structure diff --git a/tests/snapshots/porcelain/tools/organize/no-args-success b/tests/snapshots/porcelain/tool/organize/no-args-success similarity index 100% rename from tests/snapshots/porcelain/tools/organize/no-args-success rename to tests/snapshots/porcelain/tool/organize/no-args-success diff --git a/tests/snapshots/porcelain/tools/no-args-failure b/tests/snapshots/porcelain/tools/no-args-failure deleted file mode 100644 index 38568530515..00000000000 --- a/tests/snapshots/porcelain/tools/no-args-failure +++ /dev/null @@ -1,6 +0,0 @@ -error: 'ein tools' requires a subcommand but one was not provided - -USAGE: - ein tools - -For more information try --help \ No newline at end of file