Skip to content

Commit

Permalink
Merge branch 'write-index-files' into rev-parse-delegate
Browse files Browse the repository at this point in the history
  • Loading branch information
Byron committed Aug 2, 2022
2 parents 502d8c9 + e5cb6d9 commit 370110d
Show file tree
Hide file tree
Showing 6 changed files with 413 additions and 213 deletions.
2 changes: 2 additions & 0 deletions git-index/src/lib.rs
Expand Up @@ -22,6 +22,8 @@ pub mod decode;

pub mod verify;

pub mod write;

/// All known versions of a git index file.
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
Expand Down
160 changes: 160 additions & 0 deletions git-index/src/write.rs
@@ -0,0 +1,160 @@
use crate::{extension, State, Version};
use bstr::ByteVec;
use std::{
collections::{hash_map, HashMap},
ops::Range,
};

impl State {
pub fn write_to(&self, options: Options) -> Vec<u8> {
let mut writer = Writer::new(self, options);
writer.generate();
writer.data
}
}

#[derive(Default)]
pub struct Options {
hash_kind: git_hash::Kind,
}

struct Writer<'a> {
state: &'a State,
options: Options,
data: Vec<u8>,
index_table: HashMap<&'static str, Range<usize>>,
}

impl<'a> Writer<'a> {
pub fn new(state: &'a State, options: Options) -> Self {
Self {
state,
options,
data: Vec::default(),
index_table: Default::default(),
}
}

pub fn generate(&mut self) {
self.header();
self.entries();

// TODO: Tree extension is always included, I think
if let Some(t) = self.state.tree() {
self.tree(t)
}

self.end_of_index();
}

fn push(&mut self, data: &[u8], key: &'static str) {
let start = self.data.len();
let end = start + data.len();

match self.index_table.entry(key) {
hash_map::Entry::Occupied(mut e) => e.get_mut().end = end,
hash_map::Entry::Vacant(e) => {
e.insert(start..end);
}
};

self.data.push_str(data);
}

fn header(&mut self) {
let signature = b"DIRC";
let version = match self.state.version() {
Version::V2 => 2_u32.to_be_bytes(),
Version::V3 => 3_u32.to_be_bytes(),
Version::V4 => 4_u32.to_be_bytes(),
};
let num_entries = self.state.entries().len() as u32;

self.push(signature, "header");
self.push(&version, "header");
self.push(&(num_entries).to_be_bytes(), "header");
}

fn entries(&mut self) {
for e in self.state.entries() {
self.push(&e.stat.ctime.secs.to_be_bytes(), "entries");
self.push(&e.stat.ctime.nsecs.to_be_bytes(), "entries");
self.push(&e.stat.mtime.secs.to_be_bytes(), "entries");
self.push(&e.stat.mtime.nsecs.to_be_bytes(), "entries");
self.push(&e.stat.dev.to_be_bytes(), "entries");
self.push(&e.stat.ino.to_be_bytes(), "entries");
self.push(&e.mode.bits().to_be_bytes(), "entries");
self.push(&e.stat.uid.to_be_bytes(), "entries");
self.push(&e.stat.gid.to_be_bytes(), "entries");
self.push(&e.stat.size.to_be_bytes(), "entries");
self.push(e.id.as_bytes(), "entries");
//FIXME: correct flag values
// probably convert 'in-memory' Flags to at_rest::Flags
// self.push(&e.flags.bits().to_be_bytes(), "entries");
self.push(b"\x00\x01\x61\x00", "entries");

println!("{:?}", e.flags.bits());
}
}

fn tree(&mut self, tree: &extension::Tree) {
let signature = b"TREE";
let mut size: u32 = 0;

self.push(signature, "tree");
self.push(&size.to_be_bytes(), "tree");

self.tree_entry(tree);

if let Some(range) = self.index_table.get("tree") {
size = (range.end - (range.start + 8)) as u32;
self.data[range.start + 4..range.start + 8].copy_from_slice(&size.to_be_bytes());
}
}

fn tree_entry(&mut self, tree: &extension::Tree) {
let path = [tree.name.as_slice(), b"\0"].concat();

let num_entries_ascii = tree.num_entries.to_string();
let num_children_ascii = tree.children.len().to_string();

self.push(path.as_slice(), "tree");
self.push(num_entries_ascii.as_bytes(), "tree");
self.push(b" ", "tree");
self.push(num_children_ascii.as_bytes(), "tree");
self.push(b"\n", "tree");
self.push(tree.id.as_bytes(), "tree");

for child in &tree.children {
self.tree_entry(child);
}
}

fn end_of_index(&mut self) {
match self.index_table.get("entries") {
Some(range) => {
let signature = b"EOIE";
let extension_size = 4 + self.options.hash_kind.len_in_bytes() as u32;
let offset: u32 = range.end as u32;

let mut hasher = git_features::hash::hasher(self.options.hash_kind);

match self.index_table.get("tree") {
Some(range) => {
hasher.update(b"TREE");
hasher.update(&self.data[range.start + 4..range.start + 8]);
}
None => {}
}

let hash = hasher.digest();

self.data.push_str(signature);
self.data.push_str(extension_size.to_be_bytes());
self.data.push_str(offset.to_be_bytes());
self.data.push_str(hash);
}
None => {}
}
}
}
214 changes: 2 additions & 212 deletions git-index/tests/index/file/mod.rs
@@ -1,212 +1,2 @@
mod init {
use std::path::{Path, PathBuf};

use bstr::ByteSlice;
use git_index::{entry, Version};
use git_testtools::hex_to_id;

fn verify(index: git_index::File) -> git_index::File {
index.verify_integrity().unwrap();
index.verify_entries().unwrap();
index
.verify_extensions(false, git_index::verify::extensions::no_find)
.unwrap();
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"));
let file = git_index::File::at(path, git_index::decode::Options::default()).unwrap();
verify(file)
}
fn file(name: &str) -> git_index::File {
let file = git_index::File::at(crate::fixture_path(name), git_index::decode::Options::default()).unwrap();
verify(file)
}
fn file_opt(name: &str, opts: git_index::decode::Options) -> git_index::File {
let file = git_index::File::at(crate::fixture_path(name), opts).unwrap();
verify(file)
}

#[test]
fn read_v2_with_single_entry_tree_and_eoie_ext() {
let file_disallow_threaded_loading = file_opt(
"v2",
git_index::decode::Options {
min_extension_block_in_bytes_for_threading: 100000,
..Default::default()
},
);
for file in [file("v2"), file_disallow_threaded_loading] {
assert_eq!(file.version(), Version::V2);

assert_eq!(file.entries().len(), 1);

let entry = &file.entries()[0];
assert_eq!(entry.id, hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"));
assert!(entry.flags.is_empty());
assert_eq!(entry.mode, entry::Mode::FILE);
assert_eq!(entry.path(&file.state), "a");

let tree = file.tree().unwrap();
assert_eq!(tree.num_entries, 1);
assert_eq!(tree.id, hex_to_id("496d6428b9cf92981dc9495211e6e1120fb6f2ba"));
assert!(tree.name.is_empty());
assert!(tree.children.is_empty());
}
}
#[test]
fn read_v2_empty() {
let file = file("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!(tree.name.is_empty());
assert!(tree.children.is_empty());
assert_eq!(tree.id, hex_to_id("4b825dc642cb6eb9a060e54bf8d69288fbee4904"));
}

#[test]
fn read_v2_with_multiple_entries_without_eoie_ext() {
let file = file("v2_more_files");
assert_eq!(file.version(), Version::V2);

assert_eq!(file.entries().len(), 6);
for (idx, path) in ["a", "b", "c", "d/a", "d/b", "d/c"].iter().enumerate() {
let e = &file.entries()[idx];
assert_eq!(e.path(&file), path);
assert!(e.flags.is_empty());
assert_eq!(e.mode, entry::Mode::FILE);
assert_eq!(e.id, hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"))
}

let tree = file.tree().unwrap();
assert_eq!(tree.id, hex_to_id("c9b29c3168d8e677450cc650238b23d9390801fb"));
assert_eq!(tree.num_entries, 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.name.as_bstr(), "d");
}

fn find_shared_index_for(index: impl AsRef<Path>) -> PathBuf {
let mut matches = std::fs::read_dir(index.as_ref().parent().unwrap())
.unwrap()
.map(Result::unwrap)
.filter(|e: &std::fs::DirEntry| e.file_name().into_string().unwrap().starts_with("sharedindex."));
let res = matches.next().unwrap();
assert!(matches.next().is_none(), "found more than one shared indices");
res.path()
}

#[test]
fn read_split_index_without_any_extension() {
let file = git_index::File::at(
find_shared_index_for(crate::fixture_path("v2_split_index")),
git_index::decode::Options::default(),
)
.unwrap();
assert_eq!(file.version(), Version::V2);
}

#[test]
fn read_v2_split_index() {
let file = file("v2_split_index");
assert_eq!(file.version(), Version::V2);

assert!(file.link().is_some());
}

#[test]
fn read_v3_extended_flags() {
let file = loose_file("extended-flags");
assert_eq!(file.version(), Version::V3);
}

#[test]
fn read_v2_very_long_path() {
let file = loose_file("very-long-path");
assert_eq!(file.version(), Version::V2);

assert_eq!(file.state.entries().len(), 9);
assert_eq!(
file.state.entries()[0].path(&file.state),
std::iter::repeat('a')
.take(4096)
.chain(std::iter::once('q'))
.collect::<String>()
);
}

#[test]
fn read_reuc_extension() {
let file = loose_file("REUC");
assert_eq!(file.version(), Version::V2);

assert!(file.resolve_undo().is_some());
}

#[test]
fn read_untr_extension() {
let file = loose_file("UNTR");
assert_eq!(file.version(), Version::V2);

assert!(file.untracked().is_some());
}

#[test]
fn read_untr_extension_with_oids() {
let file = loose_file("UNTR-with-oids");
assert_eq!(file.version(), Version::V2);

assert!(file.untracked().is_some());
}

#[test]
fn read_fsmn_v1() {
let file = loose_file("FSMN");
assert_eq!(file.version(), Version::V2);

assert!(file.fs_monitor().is_some());
}

#[test]
fn read_file_with_conflicts() {
let file = loose_file("conflicting-file");
assert_eq!(file.version(), Version::V2);
assert_eq!(file.entries().len(), 3);
}

#[test]
fn read_v4_with_delta_paths_and_ieot_ext() {
let file = file("v4_more_files_IEOT");
assert_eq!(file.version(), Version::V4);

assert_eq!(file.entries().len(), 10);
for (idx, path) in [
"a",
"b",
"c",
"d/a",
"d/b",
"d/c",
"d/last/123",
"d/last/34",
"d/last/6",
"x",
]
.iter()
.enumerate()
{
let e = &file.entries()[idx];
assert_eq!(e.path(&file), path);
assert!(e.flags.is_empty());
assert_eq!(e.mode, entry::Mode::FILE);
assert_eq!(e.id, hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"))
}
}
}
mod read;
mod write;

0 comments on commit 370110d

Please sign in to comment.