This is just the first pass at ensuring we can read commit-graph files correctly. It does not support anything interesting like reachability, merge-base, or ahead-behind queries.
Showing the changes to git-commitgraph (52 of 53 files). Show all
Unified · Split
| @@ -14,3 +14,9 @@ doctest = false · 13 unmodified lines | ||
| 14 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
| 15 | 15 | |
| 16 | 16 | [dependencies] |
| 17 | +git-object = { version = "^0.4.0", path = "../git-object" } | |
| 18 | + | |
| 19 | +bstr = { version = "0.2.13", default-features = false, features = ["std"] } | |
| 20 | +byteorder = "1.2.3" | |
| 21 | +filebuffer = "0.4.0" | |
| 22 | +quick-error = "2.0.0" | |
| @@ -0,0 +1,83 @@ | ||
| 1 | +use crate::graph::GraphPosition; | |
| 2 | +use crate::graph_file::LexPosition; | |
| 3 | +use crate::{CommitData, Graph, GraphFile}; | |
| 4 | +use git_object::borrowed; | |
| 5 | + | |
| 6 | +impl Graph { | |
| 7 | + pub fn commit_at(&self, pos: GraphPosition) -> CommitData { | |
| 8 | + let r = self.lookup_by_pos(pos); | |
| 9 | + r.file.commit_at(r.lex_pos) | |
| 10 | + } | |
| 11 | + | |
| 12 | + pub fn commit_by_id(&self, id: borrowed::Id) -> Option<CommitData> { | |
| 13 | + let r = self.lookup_by_id(id)?; | |
| 14 | + Some(r.file.commit_at(r.lex_pos)) | |
| 15 | + } | |
| 16 | + | |
| 17 | + pub fn id_at(&self, pos: GraphPosition) -> borrowed::Id { | |
| 18 | + let r = self.lookup_by_pos(pos); | |
| 19 | + r.file.id_at(r.lex_pos) | |
| 20 | + } | |
| 21 | + | |
| 22 | + /// Iterate over commits in unsorted order. | |
| 23 | + pub fn iter_commits(&self) -> impl Iterator<Item = CommitData> { | |
| 24 | + self.files.iter().flat_map(|file| file.iter_commits()) | |
| 25 | + } | |
| 26 | + | |
| 27 | + /// Iterate over commit IDs in unsorted order. | |
| 28 | + pub fn iter_ids(&self) -> impl Iterator<Item = borrowed::Id> { | |
| 29 | + self.files.iter().flat_map(|file| file.iter_ids()) | |
| 30 | + } | |
| 31 | + | |
| 32 | + pub fn lookup(&self, id: borrowed::Id) -> Option<GraphPosition> { | |
| 33 | + Some(self.lookup_by_id(id)?.graph_pos) | |
| 34 | + } | |
| 35 | + | |
| 36 | + pub fn num_commits(&self) -> u32 { | |
| 37 | + self.files.iter().map(|f| f.num_commits()).sum() | |
| 38 | + } | |
| 39 | +} | |
| 40 | + | |
| 41 | +impl Graph { | |
| 42 | + fn lookup_by_id(&self, id: borrowed::Id) -> Option<LookupByIdResult> { | |
| 43 | + let mut current_file_start = 0; | |
| 44 | + for file in self.files.iter() { | |
| 45 | + if let Some(lex_pos) = file.lookup(id) { | |
| 46 | + return Some(LookupByIdResult { | |
| 47 | + file, | |
| 48 | + lex_pos, | |
| 49 | + graph_pos: GraphPosition(current_file_start + lex_pos.0), | |
| 50 | + }); | |
| 51 | + } | |
| 52 | + current_file_start += file.num_commits(); | |
| 53 | + } | |
| 54 | + None | |
| 55 | + } | |
| 56 | + | |
| 57 | + fn lookup_by_pos(&self, pos: GraphPosition) -> LookupByPositionResult { | |
| 58 | + let mut remaining = pos.0; | |
| 59 | + for file in self.files.iter() { | |
| 60 | + match remaining.checked_sub(file.num_commits()) { | |
| 61 | + Some(v) => remaining = v, | |
| 62 | + None => { | |
| 63 | + return LookupByPositionResult { | |
| 64 | + file, | |
| 65 | + lex_pos: LexPosition(remaining), | |
| 66 | + } | |
| 67 | + } | |
| 68 | + } | |
| 69 | + } | |
| 70 | + panic!("graph position too large: {}", pos.0); | |
| 71 | + } | |
| 72 | +} | |
| 73 | + | |
| 74 | +struct LookupByIdResult<'a> { | |
| 75 | + pub file: &'a GraphFile, | |
| 76 | + pub graph_pos: GraphPosition, | |
| 77 | + pub lex_pos: LexPosition, | |
| 78 | +} | |
| 79 | + | |
| 80 | +struct LookupByPositionResult<'a> { | |
| 81 | + pub file: &'a GraphFile, | |
| 82 | + pub lex_pos: LexPosition, | |
| 83 | +} | |
| @@ -0,0 +1,104 @@ | ||
| 1 | +use crate::{graph_file, Graph, GraphFile, MAX_COMMITS}; | |
| 2 | +use git_object::HashKind; | |
| 3 | +use quick_error::quick_error; | |
| 4 | +use std::io::{BufRead, BufReader}; | |
| 5 | +use std::path::{Path, PathBuf}; | |
| 6 | + | |
| 7 | +quick_error! { | |
| 8 | + #[derive(Debug)] | |
| 9 | + pub enum Error { | |
| 10 | + Corrupt(msg: String, path: PathBuf) { | |
| 11 | + display("{}", msg) | |
| 12 | + } | |
| 13 | + GraphFile(err: graph_file::Error, path: PathBuf) { | |
| 14 | + display("{}", path.display()) | |
| 15 | + source(err) | |
| 16 | + } | |
| 17 | + HashVersionMismatch(path1: PathBuf, hash1: HashKind, path2: PathBuf, hash2: HashKind) { | |
| 18 | + display( | |
| 19 | + "Commit-graph files mismatch: '{}' uses hash {:?}, but '{}' uses hash {:?}", | |
| 20 | + path1.display(), | |
| 21 | + hash1, | |
| 22 | + path2.display(), | |
| 23 | + hash2, | |
| 24 | + ) | |
| 25 | + } | |
| 26 | + Io(err: std::io::Error, path: PathBuf) { | |
| 27 | + display("Could not open commit-graph graph_file at '{}'", path.display()) | |
| 28 | + source(err) | |
| 29 | + } | |
| 30 | + TooManyCommits(num_commits: u64) { | |
| 31 | + display( | |
| 32 | + "Commit-graph files contain {} commits altogether, but only {} commits are allowed", | |
| 33 | + num_commits, | |
| 34 | + MAX_COMMITS, | |
| 35 | + ) | |
| 36 | + } | |
| 37 | + // This might actually be legal... | |
| 38 | + VersionMismatch(path1: PathBuf, version1: graph_file::Kind, path2: PathBuf, version2: graph_file::Kind) { | |
| 39 | + display( | |
| 40 | + "Commit-graph files mismatch: '{}' is version {:?}, but '{}' is version {:?}", | |
| 41 | + path1.display(), | |
| 42 | + version1, | |
| 43 | + path2.display(), | |
| 44 | + version2, | |
| 45 | + ) | |
| 46 | + } | |
| 47 | + } | |
| 48 | +} | |
| 49 | + | |
| 50 | +impl Graph { | |
| 51 | + pub fn from_object_dir(object_dir: impl AsRef<Path>) -> Result<Self, Error> { | |
| 52 | + Self::from_single_file(object_dir.as_ref()).or_else(|_| Self::from_split_chain(object_dir.as_ref())) | |
| 53 | + } | |
| 54 | + | |
| 55 | + pub fn from_single_file(object_dir: impl AsRef<Path>) -> Result<Self, Error> { | |
| 56 | + let single_graph_file = object_dir.as_ref().join("info").join("commit-graph"); | |
| 57 | + let file = GraphFile::at(&single_graph_file).map_err(|e| Error::GraphFile(e, single_graph_file.clone()))?; | |
| 58 | + Self::new(vec![file]) | |
| 59 | + } | |
| 60 | + | |
| 61 | + pub fn from_split_chain(object_dir: impl AsRef<Path>) -> Result<Self, Error> { | |
| 62 | + let commit_graphs_dir = object_dir.as_ref().join("info").join("commit-graphs"); | |
| 63 | + let chain_file_path = commit_graphs_dir.join("commit-graph-chain"); | |
| 64 | + let chain_file = std::fs::File::open(&chain_file_path).map_err(|e| Error::Io(e, chain_file_path.clone()))?; | |
| 65 | + let mut files: Vec<GraphFile> = Vec::new(); | |
| 66 | + for line in BufReader::new(chain_file).lines() { | |
| 67 | + let line = line.map_err(|e| Error::Io(e, chain_file_path.clone()))?; | |
| 68 | + let graph_filename = format!("graph-{}.graph", line); | |
| 69 | + let graph_file_path = commit_graphs_dir.join(graph_filename); | |
| 70 | + files.push(GraphFile::at(&graph_file_path).map_err(|e| Error::GraphFile(e, graph_file_path.clone()))?); | |
| 71 | + } | |
| 72 | + Self::new(files) | |
| 73 | + } | |
| 74 | + | |
| 75 | + pub fn new(files: Vec<GraphFile>) -> Result<Self, Error> { | |
| 76 | + let num_commits: u64 = files.iter().map(|f| f.num_commits() as u64).sum(); | |
| 77 | + if num_commits > MAX_COMMITS as u64 { | |
| 78 | + return Err(Error::TooManyCommits(num_commits)); | |
| 79 | + } | |
| 80 | + | |
| 81 | + for window in files.windows(2) { | |
| 82 | + let f1 = &window[0]; | |
| 83 | + let f2 = &window[1]; | |
| 84 | + if f1.kind() != f2.kind() { | |
| 85 | + return Err(Error::VersionMismatch( | |
| 86 | + f1.path().to_owned(), | |
| 87 | + f1.kind(), | |
| 88 | + f2.path().to_owned(), | |
| 89 | + f2.kind(), | |
| 90 | + )); | |
| 91 | + } | |
| 92 | + if f1.hash_kind() != f2.hash_kind() { | |
| 93 | + return Err(Error::HashVersionMismatch( | |
| 94 | + f1.path().to_owned(), | |
| 95 | + f1.hash_kind(), | |
| 96 | + f2.path().to_owned(), | |
| 97 | + f2.hash_kind(), | |
| 98 | + )); | |
| 99 | + } | |
| 100 | + } | |
| 101 | + | |
| 102 | + Ok(Self { files }) | |
| 103 | + } | |
| 104 | +} | |
| @@ -0,0 +1,23 @@ | ||
| 1 | +mod access; | |
| 2 | +mod init; | |
| 3 | + | |
| 4 | +use crate::GraphFile; | |
| 5 | +use std::fmt::{Display, Formatter}; | |
| 6 | + | |
| 7 | +/// A complete commit graph. | |
| 8 | +/// | |
| 9 | +/// The data in the commit graph may come from a monolithic `objects/info/commit-graph` file, or it | |
| 10 | +/// may come from one or more `objects/info/commit-graphs/graph-*.graph` files. These files are | |
| 11 | +/// generated via `git commit-graph write ...` commands. | |
| 12 | +pub struct Graph { | |
| 13 | + files: Vec<GraphFile>, | |
| 14 | +} | |
| 15 | + | |
| 16 | +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] | |
| 17 | +pub struct GraphPosition(pub u32); | |
| 18 | + | |
| 19 | +impl Display for GraphPosition { | |
| 20 | + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | |
| 21 | + self.0.fmt(f) | |
| 22 | + } | |
| 23 | +} | |
| @@ -0,0 +1,128 @@ | ||
| 1 | +use crate::graph_file::{Kind, LexPosition, COMMIT_DATA_ENTRY_SIZE}; | |
| 2 | +use crate::{CommitData, GraphFile}; | |
| 3 | +use git_object::{borrowed, HashKind, SHA1_SIZE}; | |
| 4 | +use std::convert::{TryFrom, TryInto}; | |
| 5 | +use std::fmt::{Debug, Formatter}; | |
| 6 | +use std::path::Path; | |
| 7 | + | |
| 8 | +impl GraphFile { | |
| 9 | + /// Returns the commit data for the commit located at the given lex position. | |
| 10 | + /// | |
| 11 | + /// `pos` must range from 0 to self.num_commits(). | |
| 12 | + /// | |
| 13 | + /// # Panics | |
| 14 | + /// | |
| 15 | + /// Panics if `pos` is out of bounds. | |
| 16 | + pub fn commit_at(&self, pos: LexPosition) -> CommitData { | |
| 17 | + CommitData::new(self, pos) | |
| 18 | + } | |
| 19 | + | |
| 20 | + pub fn hash_kind(&self) -> HashKind { | |
| 21 | + HashKind::Sha1 | |
| 22 | + } | |
| 23 | + | |
| 24 | + // copied from git-odb/src/pack/index/access.rs | |
| 25 | + /// Returns 20 bytes sha1 at the given index in our list of (sorted) sha1 hashes. | |
| 26 | + /// The position ranges from 0 to self.num_commits() | |
| 27 | + pub fn id_at(&self, pos: LexPosition) -> borrowed::Id { | |
| 28 | + assert!( | |
| 29 | + pos.0 < self.num_commits(), | |
| 30 | + "expected lex position less than {}, got {}", | |
| 31 | + self.num_commits(), | |
| 32 | + pos.0 | |
| 33 | + ); | |
| 34 | + let pos: usize = pos | |
| 35 | + .0 | |
| 36 | + .try_into() | |
| 37 | + .expect("an architecture able to hold 32 bits of integer"); | |
| 38 | + let start = self.oid_lookup_offset + (pos * SHA1_SIZE); | |
| 39 | + borrowed::Id::try_from(&self.data[start..start + SHA1_SIZE]).expect("20 bytes SHA1 to be alright") | |
| 40 | + } | |
| 41 | + | |
| 42 | + pub fn iter_base_graph_ids(&self) -> impl Iterator<Item = borrowed::Id> { | |
| 43 | + let base_graphs_list = match self.base_graphs_list_offset { | |
| 44 | + Some(v) => &self.data[v..v + (SHA1_SIZE * self.base_graph_count as usize)], | |
| 45 | + None => &[], | |
| 46 | + }; | |
| 47 | + base_graphs_list | |
| 48 | + .chunks_exact(SHA1_SIZE) | |
| 49 | + .map(|bytes| borrowed::Id::try_from(bytes).expect("20 bytes SHA1 to be alright")) | |
| 50 | + } | |
| 51 | + | |
| 52 | + pub fn iter_commits(&self) -> impl Iterator<Item = CommitData> { | |
| 53 | + (0..self.num_commits()).map(move |i| self.commit_at(LexPosition(i))) | |
| 54 | + } | |
| 55 | + | |
| 56 | + pub fn iter_ids(&self) -> impl Iterator<Item = borrowed::Id> { | |
| 57 | + (0..self.num_commits()).map(move |i| self.id_at(LexPosition(i))) | |
| 58 | + } | |
| 59 | + | |
| 60 | + pub fn kind(&self) -> Kind { | |
| 61 | + Kind::V1 | |
| 62 | + } | |
| 63 | + | |
| 64 | + // copied from git-odb/src/pack/index/access.rs | |
| 65 | + pub fn lookup(&self, id: borrowed::Id) -> Option<LexPosition> { | |
| 66 | + let first_byte = id.first_byte() as usize; | |
| 67 | + let mut upper_bound = self.fan[first_byte]; | |
| 68 | + let mut lower_bound = if first_byte != 0 { self.fan[first_byte - 1] } else { 0 }; | |
| 69 | + | |
| 70 | + // Bisect using indices | |
| 71 | + // TODO: Performance of V2 could possibly be better if we would be able to do a binary search | |
| 72 | + // on 20 byte chunks directly, but doing so requires transmuting and that is unsafe, even though | |
| 73 | + // it should not be if the bytes match up and the type has no destructor. | |
| 74 | + while lower_bound < upper_bound { | |
| 75 | + let mid = (lower_bound + upper_bound) / 2; | |
| 76 | + let mid_sha = self.id_at(LexPosition(mid)); | |
| 77 | + | |
| 78 | + use std::cmp::Ordering::*; | |
| 79 | + match id.cmp(&mid_sha) { | |
| 80 | + Less => upper_bound = mid, | |
| 81 | + Equal => return Some(LexPosition(mid)), | |
| 82 | + Greater => lower_bound = mid + 1, | |
| 83 | + } | |
| 84 | + } | |
| 85 | + None | |
| 86 | + } | |
| 87 | + | |
| 88 | + /// Returns the number of commits in this graph file. | |
| 89 | + /// | |
| 90 | + /// The maximum valid `LexPosition` that can be used with this file is one less than | |
| 91 | + /// `num_commits()`. | |
| 92 | + pub fn num_commits(&self) -> u32 { | |
| 93 | + self.fan[255] | |
| 94 | + } | |
| 95 | + | |
| 96 | + pub fn path(&self) -> &Path { | |
| 97 | + &self.path | |
| 98 | + } | |
| 99 | +} | |
| 100 | + | |
| 101 | +impl GraphFile { | |
| 102 | + /// Returns the byte slice for the given commit in this file's Commit Data (CDAT) chunk. | |
| 103 | + pub(crate) fn commit_data_bytes(&self, pos: LexPosition) -> &[u8] { | |
| 104 | + assert!( | |
| 105 | + pos.0 < self.num_commits(), | |
| 106 | + "expected lex position less than {}, got {}", | |
| 107 | + self.num_commits(), | |
| 108 | + pos.0 | |
| 109 | + ); | |
| 110 | + let pos: usize = pos | |
| 111 | + .0 | |
| 112 | + .try_into() | |
| 113 | + .expect("an architecture able to hold 32 bits of integer"); | |
| 114 | + let start = self.commit_data_offset + (pos * COMMIT_DATA_ENTRY_SIZE); | |
| 115 | + &self.data[start..start + COMMIT_DATA_ENTRY_SIZE] | |
| 116 | + } | |
| 117 | + | |
| 118 | + /// Returns the byte slice for this file's entire Extra Edge List (EDGE) chunk. | |
| 119 | + pub(crate) fn extra_edges_data(&self) -> Option<&[u8]> { | |
| 120 | + Some(&self.data[self.extra_edges_list_range.clone()?]) | |
| 121 | + } | |
| 122 | +} | |
| 123 | + | |
| 124 | +impl Debug for GraphFile { | |
| 125 | + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | |
| 126 | + write!(f, r#"GraphFile("{:?}")"#, self.path.display()) | |
| 127 | + } | |
| 128 | +} | |
| @@ -0,0 +1,255 @@ | ||
| 1 | +use crate::graph::GraphPosition; | |
| 2 | +use crate::graph_file::LexPosition; | |
| 3 | +use crate::GraphFile; | |
| 4 | +use byteorder::{BigEndian, ByteOrder}; | |
| 5 | +use git_object::{borrowed, owned, SHA1_SIZE}; | |
| 6 | +use quick_error::quick_error; | |
| 7 | +use std::convert::{TryFrom, TryInto}; | |
| 8 | +use std::fmt::{Debug, Formatter}; | |
| 9 | +use std::slice::Chunks; | |
| 10 | + | |
| 11 | +quick_error! { | |
| 12 | + #[derive(Debug)] | |
| 13 | + pub enum Error { | |
| 14 | + ExtraEdgesListOverflow(commit: owned::Id) { | |
| 15 | + display( | |
| 16 | + "commit {}'s extra edges overflows the commit-graph file's extra edges list", | |
| 17 | + commit, | |
| 18 | + ) | |
| 19 | + } | |
| 20 | + FirstParentIsExtraEdgeIndex(commit: owned::Id) { | |
| 21 | + display( | |
| 22 | + "commit {}'s first parent is an extra edge index, which is invalid", | |
| 23 | + commit, | |
| 24 | + ) | |
| 25 | + } | |
| 26 | + MissingExtraEdgesList(commit: owned::Id) { | |
| 27 | + display( | |
| 28 | + "commit {} has extra edges, but commit-graph file has no extra edges list", | |
| 29 | + commit, | |
| 30 | + ) | |
| 31 | + } | |
| 32 | + SecondParentWithoutFirstParent(commit: owned::Id) { | |
| 33 | + display("commit {} has a second parent but not a first parent", commit) | |
| 34 | + } | |
| 35 | + } | |
| 36 | +} | |
| 37 | + | |
| 38 | +// Note that git's commit-graph-format.txt as of v2.28.0 gives an incorrect value 0x0700_0000 for | |
| 39 | +// NO_PARENT. | |
| 40 | +const NO_PARENT: u32 = 0x7000_0000; | |
| 41 | +const EXTENDED_EDGES_MASK: u32 = 0x8000_0000; | |
| 42 | + | |
| 43 | +pub struct CommitData<'a> { | |
| 44 | + file: &'a GraphFile, | |
| 45 | + lex_pos: LexPosition, | |
| 46 | + // We can parse the below fields lazily if needed. | |
| 47 | + commit_timestamp: u64, | |
| 48 | + generation: u32, | |
| 49 | + parent1: ParentEdge, | |
| 50 | + parent2: ParentEdge, | |
| 51 | + root_tree_id: borrowed::Id<'a>, | |
| 52 | +} | |
| 53 | + | |
| 54 | +impl<'a> CommitData<'a> { | |
| 55 | + pub(crate) fn new(file: &'a GraphFile, pos: LexPosition) -> Self { | |
| 56 | + let bytes = file.commit_data_bytes(pos); | |
| 57 | + CommitData { | |
| 58 | + file, | |
| 59 | + lex_pos: pos, | |
| 60 | + root_tree_id: borrowed::Id::try_from(&bytes[..SHA1_SIZE]).expect("20 bytes SHA1 to be alright"), | |
| 61 | + parent1: ParentEdge::from_raw(BigEndian::read_u32(&bytes[SHA1_SIZE..SHA1_SIZE + 4])), | |
| 62 | + parent2: ParentEdge::from_raw(BigEndian::read_u32(&bytes[SHA1_SIZE + 4..SHA1_SIZE + 8])), | |
| 63 | + generation: BigEndian::read_u32(&bytes[SHA1_SIZE + 8..SHA1_SIZE + 12]) >> 2, | |
| 64 | + commit_timestamp: BigEndian::read_u64(&bytes[SHA1_SIZE + 8..SHA1_SIZE + 16]) & 0x0003_ffff_ffff, | |
| 65 | + } | |
| 66 | + } | |
| 67 | + | |
| 68 | + /// Returns the committer timestamp of this commit. | |
| 69 | + /// | |
| 70 | + /// The value is the number of seconds since 1970-01-01 00:00:00 UTC. | |
| 71 | + pub fn committer_timestamp(&self) -> u64 { | |
| 72 | + self.commit_timestamp | |
| 73 | + } | |
| 74 | + | |
| 75 | + /// Returns the generation number of this commit. | |
| 76 | + /// | |
| 77 | + /// Commits without parents have generation number 1. Commits with parents have a generation | |
| 78 | + /// number that is the max of their parents' generation numbers + 1. | |
| 79 | + pub fn generation(&self) -> u32 { | |
| 80 | + self.generation | |
| 81 | + } | |
| 82 | + | |
| 83 | + pub fn iter_parent_indices(&'a self) -> impl Iterator<Item = Result<GraphPosition, Error>> + 'a { | |
| 84 | + // I didn't find a combinator approach that a) was as strict as ParentIterator, b) supported | |
| 85 | + // fuse-after-first-error behavior, and b) was significantly shorter or more understandable | |
| 86 | + // than ParentIterator. So here we are. | |
| 87 | + ParentIterator { | |
| 88 | + commit_data: self, | |
| 89 | + state: ParentIteratorState::First, | |
| 90 | + } | |
| 91 | + } | |
| 92 | + | |
| 93 | + pub fn id(&self) -> borrowed::Id { | |
| 94 | + self.file.id_at(self.lex_pos) | |
| 95 | + } | |
| 96 | + | |
| 97 | + pub fn parent1(&self) -> Result<Option<GraphPosition>, Error> { | |
| 98 | + self.iter_parent_indices().next().transpose() | |
| 99 | + } | |
| 100 | + | |
| 101 | + pub fn root_tree_id(&self) -> borrowed::Id { | |
| 102 | + self.root_tree_id | |
| 103 | + } | |
| 104 | +} | |
| 105 | + | |
| 106 | +impl<'a> Debug for CommitData<'a> { | |
| 107 | + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | |
| 108 | + write!( | |
| 109 | + f, | |
| 110 | + "CommitData {{ id: {}, lex_pos: {}, generation: {}, root_tree_oid: {}, parent1: {:?}, parent2: {:?} }}", | |
| 111 | + self.id(), | |
| 112 | + self.lex_pos, | |
| 113 | + self.generation(), | |
| 114 | + self.root_tree_id(), | |
| 115 | + self.parent1, | |
| 116 | + self.parent2, | |
| 117 | + ) | |
| 118 | + } | |
| 119 | +} | |
| 120 | + | |
| 121 | +impl<'a> Eq for CommitData<'a> {} | |
| 122 | + | |
| 123 | +impl<'a> PartialEq for CommitData<'a> { | |
| 124 | + fn eq(&self, other: &Self) -> bool { | |
| 125 | + self.file as *const GraphFile == other.file as *const GraphFile && self.lex_pos == other.lex_pos | |
| 126 | + } | |
| 127 | +} | |
| 128 | + | |
| 129 | +pub struct ParentIterator<'a> { | |
| 130 | + commit_data: &'a CommitData<'a>, | |
| 131 | + state: ParentIteratorState<'a>, | |
| 132 | +} | |
| 133 | + | |
| 134 | +impl<'a> Iterator for ParentIterator<'a> { | |
| 135 | + type Item = Result<GraphPosition, Error>; | |
| 136 | + | |
| 137 | + fn next(&mut self) -> Option<Self::Item> { | |
| 138 | + let state = std::mem::replace(&mut self.state, ParentIteratorState::Exhausted); | |
| 139 | + match state { | |
| 140 | + ParentIteratorState::First => match self.commit_data.parent1 { | |
| 141 | + ParentEdge::None => match self.commit_data.parent2 { | |
| 142 | + ParentEdge::None => None, | |
| 143 | + _ => Some(Err(Error::SecondParentWithoutFirstParent(self.commit_data.id().into()))), | |
| 144 | + }, | |
| 145 | + ParentEdge::GraphPosition(pos) => { | |
| 146 | + self.state = ParentIteratorState::Second; | |
| 147 | + Some(Ok(pos)) | |
| 148 | + } | |
| 149 | + ParentEdge::ExtraEdgeIndex(_) => { | |
| 150 | + Some(Err(Error::FirstParentIsExtraEdgeIndex(self.commit_data.id().into()))) | |
| 151 | + } | |
| 152 | + }, | |
| 153 | + ParentIteratorState::Second => match self.commit_data.parent2 { | |
| 154 | + ParentEdge::None => None, | |
| 155 | + ParentEdge::GraphPosition(pos) => Some(Ok(pos)), | |
| 156 | + ParentEdge::ExtraEdgeIndex(extra_edge_index) => { | |
| 157 | + if let Some(extra_edges_list) = self.commit_data.file.extra_edges_data() { | |
| 158 | + let start_offset: usize = extra_edge_index | |
| 159 | + .try_into() | |
| 160 | + .expect("an architecture able to hold 32 bits of integer"); | |
| 161 | + let start_offset = start_offset | |
| 162 | + .checked_mul(4) | |
| 163 | + .expect("an extended edge index small enough to fit in usize"); | |
| 164 | + if let Some(tail) = extra_edges_list.get(start_offset..) { | |
| 165 | + self.state = ParentIteratorState::Extra(tail.chunks(4)); | |
| 166 | + // This recursive call is what blocks me from replacing ParentIterator | |
| 167 | + // with a std::iter::from_fn closure. | |
| 168 | + self.next() | |
| 169 | + } else { | |
| 170 | + Some(Err(Error::ExtraEdgesListOverflow(self.commit_data.id().into()))) | |
| 171 | + } | |
| 172 | + } else { | |
| 173 | + Some(Err(Error::MissingExtraEdgesList(self.commit_data.id().into()))) | |
| 174 | + } | |
| 175 | + } | |
| 176 | + }, | |
| 177 | + ParentIteratorState::Extra(mut chunks) => { | |
| 178 | + if let Some(chunk) = chunks.next() { | |
| 179 | + let extra_edge = BigEndian::read_u32(chunk); | |
| 180 | + match ExtraEdge::from_raw(extra_edge) { | |
| 181 | + ExtraEdge::Internal(pos) => { | |
| 182 | + self.state = ParentIteratorState::Extra(chunks); | |
| 183 | + Some(Ok(pos)) | |
| 184 | + } | |
| 185 | + ExtraEdge::Last(pos) => Some(Ok(pos)), | |
| 186 | + } | |
| 187 | + } else { | |
| 188 | + Some(Err(Error::ExtraEdgesListOverflow(self.commit_data.id().into()))) | |
| 189 | + } | |
| 190 | + } | |
| 191 | + ParentIteratorState::Exhausted => None, | |
| 192 | + } | |
| 193 | + } | |
| 194 | + | |
| 195 | + fn size_hint(&self) -> (usize, Option<usize>) { | |
| 196 | + match (&self.state, self.commit_data.parent1, self.commit_data.parent2) { | |
| 197 | + (ParentIteratorState::First, ParentEdge::None, ParentEdge::None) => (0, Some(0)), | |
| 198 | + (ParentIteratorState::First, ParentEdge::None, _) => (1, Some(1)), | |
| 199 | + (ParentIteratorState::First, ParentEdge::GraphPosition(_), ParentEdge::None) => (1, Some(1)), | |
| 200 | + (ParentIteratorState::First, ParentEdge::GraphPosition(_), ParentEdge::GraphPosition(_)) => (2, Some(2)), | |
| 201 | + (ParentIteratorState::First, ParentEdge::GraphPosition(_), ParentEdge::ExtraEdgeIndex(_)) => (3, None), | |
| 202 | + (ParentIteratorState::First, ParentEdge::ExtraEdgeIndex(_), _) => (1, Some(1)), | |
| 203 | + (ParentIteratorState::Second, _, ParentEdge::None) => (0, Some(0)), | |
| 204 | + (ParentIteratorState::Second, _, ParentEdge::GraphPosition(_)) => (1, Some(1)), | |
| 205 | + (ParentIteratorState::Second, _, ParentEdge::ExtraEdgeIndex(_)) => (2, None), | |
| 206 | + (ParentIteratorState::Extra(_), _, _) => (1, None), | |
| 207 | + (ParentIteratorState::Exhausted, _, _) => (0, Some(0)), | |
| 208 | + } | |
| 209 | + } | |
| 210 | +} | |
| 211 | + | |
| 212 | +#[derive(Debug)] | |
| 213 | +enum ParentIteratorState<'a> { | |
| 214 | + First, | |
| 215 | + Second, | |
| 216 | + Extra(Chunks<'a, u8>), | |
| 217 | + Exhausted, | |
| 218 | +} | |
| 219 | + | |
| 220 | +#[derive(Clone, Copy, Debug)] | |
| 221 | +enum ParentEdge { | |
| 222 | + None, | |
| 223 | + GraphPosition(GraphPosition), | |
| 224 | + ExtraEdgeIndex(u32), | |
| 225 | +} | |
| 226 | + | |
| 227 | +impl ParentEdge { | |
| 228 | + pub fn from_raw(raw: u32) -> ParentEdge { | |
| 229 | + if raw == NO_PARENT { | |
| 230 | + return ParentEdge::None; | |
| 231 | + } | |
| 232 | + if raw & EXTENDED_EDGES_MASK != 0 { | |
| 233 | + ParentEdge::ExtraEdgeIndex(raw & !EXTENDED_EDGES_MASK) | |
| 234 | + } else { | |
| 235 | + ParentEdge::GraphPosition(GraphPosition(raw)) | |
| 236 | + } | |
| 237 | + } | |
| 238 | +} | |
| 239 | + | |
| 240 | +const LAST_EXTENDED_EDGE_MASK: u32 = 0x8000_0000; | |
| 241 | + | |
| 242 | +enum ExtraEdge { | |
| 243 | + Internal(GraphPosition), | |
| 244 | + Last(GraphPosition), | |
| 245 | +} | |
| 246 | + | |
| 247 | +impl ExtraEdge { | |
| 248 | + pub fn from_raw(raw: u32) -> Self { | |
| 249 | + if raw & LAST_EXTENDED_EDGE_MASK != 0 { | |
| 250 | + Self::Last(GraphPosition(raw & !LAST_EXTENDED_EDGE_MASK)) | |
| 251 | + } else { | |
| 252 | + Self::Internal(GraphPosition(raw)) | |
| 253 | + } | |
| 254 | + } | |
| 255 | +} | |
| @@ -0,0 +1,312 @@ | ||
| 1 | +use crate::graph_file::{GraphFile, COMMIT_DATA_ENTRY_SIZE, FAN_LEN, SIGNATURE}; | |
| 2 | +use bstr::ByteSlice; | |
| 3 | +use byteorder::{BigEndian, ByteOrder}; | |
| 4 | +use filebuffer::FileBuffer; | |
| 5 | +use git_object::SHA1_SIZE; | |
| 6 | +use quick_error::quick_error; | |
| 7 | +use std::convert::{TryFrom, TryInto}; | |
| 8 | +use std::ops::Range; | |
| 9 | +use std::path::Path; | |
| 10 | + | |
| 11 | +type ChunkId = [u8; 4]; | |
| 12 | + | |
| 13 | +quick_error! { | |
| 14 | + #[derive(Debug)] | |
| 15 | + pub enum Error { | |
| 16 | + BaseGraphMismatch(from_header: u8, from_chunk: u32) { | |
| 17 | + display( | |
| 18 | + "Commit-graph {} chunk contains {} base graphs, but commit-graph graph_file header claims {} base graphs", | |
| 19 | + BASE_GRAPHS_LIST_CHUNK_ID.as_bstr(), | |
| 20 | + from_chunk, | |
| 21 | + from_header, | |
| 22 | + ) | |
| 23 | + } | |
| 24 | + CommitCountMismatch(chunk1_id: ChunkId, chunk1_commits: u32, chunk2_id: ChunkId, chunk2_commits: u32) { | |
| 25 | + display( | |
| 26 | + "Commit-graph {:?} chunk contains {} commits, but {:?} chunk contains {} commits", | |
| 27 | + chunk1_id.as_bstr(), | |
| 28 | + chunk1_commits, | |
| 29 | + chunk2_id.as_bstr(), | |
| 30 | + chunk2_commits, | |
| 31 | + ) | |
| 32 | + } | |
| 33 | + Corrupt(msg: String) { | |
| 34 | + display("{}", msg) | |
| 35 | + } | |
| 36 | + DuplicateChunk(id: ChunkId) { | |
| 37 | + display("Commit-graph graph_file contains multiple {:?} chunks", id.as_bstr()) | |
| 38 | + } | |
| 39 | + // This error case is disabled, as git allows extra garbage in the extra edges list. | |
| 40 | + // ExtraEdgesOverflow { | |
| 41 | + // display("The last entry in commit-graph's extended edges list does is not marked as being terminal") | |
| 42 | + // } | |
| 43 | + InvalidChunkSize(id: ChunkId, msg: String) { | |
| 44 | + display("Commit-graph chunk {:?} has invalid size: {}", id.as_bstr(), msg) | |
| 45 | + } | |
| 46 | + Io(err: std::io::Error, path: std::path::PathBuf) { | |
| 47 | + display("Could not open commit-graph graph_file at '{}'", path.display()) | |
| 48 | + source(err) | |
| 49 | + } | |
| 50 | + MissingChunk(id: ChunkId) { | |
| 51 | + display("Missing required chunk {:?}", id.as_bstr()) | |
| 52 | + } | |
| 53 | + UnsupportedHashVersion(version: u8) { | |
| 54 | + display("Commit-graph graph_file uses unsupported hash version: {}", version) | |
| 55 | + } | |
| 56 | + UnsupportedVersion(version: u8) { | |
| 57 | + display("Unsupported commit-graph graph_file version: {}", version) | |
| 58 | + } | |
| 59 | + } | |
| 60 | +} | |
| 61 | + | |
| 62 | +const CHUNK_LOOKUP_SIZE: usize = 12; | |
| 63 | +const HEADER_LEN: usize = 8; | |
| 64 | +const MIN_FILE_SIZE: usize = HEADER_LEN + ((MIN_CHUNKS + 1) * CHUNK_LOOKUP_SIZE); | |
| 65 | +const OID_LOOKUP_ENTRY_SIZE: usize = SHA1_SIZE; | |
| 66 | + | |
| 67 | +// Required chunks: OIDF, OIDL, CDAT | |
| 68 | +const MIN_CHUNKS: usize = 3; | |
| 69 | +const BASE_GRAPHS_LIST_CHUNK_ID: ChunkId = *b"BASE"; | |
| 70 | +const COMMIT_DATA_CHUNK_ID: ChunkId = *b"CDAT"; | |
| 71 | +const EXTENDED_EDGES_LIST_CHUNK_ID: ChunkId = *b"EDGE"; | |
| 72 | +const OID_FAN_CHUNK_ID: ChunkId = *b"OIDF"; | |
| 73 | +const OID_LOOKUP_CHUNK_ID: ChunkId = *b"OIDL"; | |
| 74 | +const SENTINEL_CHUNK_ID: ChunkId = [0u8; 4]; | |
| 75 | + | |
| 76 | +impl GraphFile { | |
| 77 | + pub fn at(path: impl AsRef<Path>) -> Result<GraphFile, Error> { | |
| 78 | + Self::try_from(path.as_ref()) | |
| 79 | + } | |
| 80 | +} | |
| 81 | + | |
| 82 | +impl TryFrom<&Path> for GraphFile { | |
| 83 | + type Error = Error; | |
| 84 | + | |
| 85 | + fn try_from(path: &Path) -> Result<Self, Self::Error> { | |
| 86 | + let data = FileBuffer::open(path).map_err(|e| Error::Io(e, path.to_owned()))?; | |
| 87 | + let data_size = data.len(); | |
| 88 | + if data_size < MIN_FILE_SIZE { | |
| 89 | + return Err(Error::Corrupt( | |
| 90 | + "Commit-graph graph_file too small even for an empty graph".to_owned(), | |
| 91 | + )); | |
| 92 | + } | |
| 93 | + | |
| 94 | + let mut ofs = 0; | |
| 95 | + if &data[ofs..ofs + SIGNATURE.len()] != SIGNATURE { | |
| 96 | + return Err(Error::Corrupt( | |
| 97 | + "Commit-graph graph_file does not start with expected signature".to_owned(), | |
| 98 | + )); | |
| 99 | + } | |
| 100 | + ofs += SIGNATURE.len(); | |
| 101 | + | |
| 102 | + match data[ofs] { | |
| 103 | + 1 => (), | |
| 104 | + x => { | |
| 105 | + return Err(Error::UnsupportedVersion(x)); | |
| 106 | + } | |
| 107 | + }; | |
| 108 | + ofs += 1; | |
| 109 | + | |
| 110 | + match data[ofs] { | |
| 111 | + 1 => (), | |
| 112 | + x => { | |
| 113 | + return Err(Error::UnsupportedHashVersion(x)); | |
| 114 | + } | |
| 115 | + }; | |
| 116 | + ofs += 1; | |
| 117 | + | |
| 118 | + let chunk_count = data[ofs]; | |
| 119 | + // Can assert chunk_count >= MIN_CHUNKS here, but later OIDF+OIDL+CDAT presence checks make | |
| 120 | + // it redundant. | |
| 121 | + ofs += 1; | |
| 122 | + | |
| 123 | + let base_graph_count = data[ofs]; | |
| 124 | + ofs += 1; | |
| 125 | + | |
| 126 | + let chunk_lookup_end = ofs + ((chunk_count as usize + 1) * CHUNK_LOOKUP_SIZE); | |
| 127 | + if chunk_lookup_end > data_size { | |
| 128 | + return Err(Error::Corrupt(format!( | |
| 129 | + "Commit-graph graph_file is too small to hold {} chunks", | |
| 130 | + chunk_count | |
| 131 | + ))); | |
| 132 | + } | |
| 133 | + | |
| 134 | + let mut base_graphs_list_offset: Option<usize> = None; | |
| 135 | + let mut commit_data_offset: Option<usize> = None; | |
| 136 | + let mut commit_data_count = 0u32; | |
| 137 | + let mut extended_edges_list_range: Option<Range<usize>> = None; | |
| 138 | + let mut fan_offset: Option<usize> = None; | |
| 139 | + let mut oid_lookup_offset: Option<usize> = None; | |
| 140 | + let mut oid_lookup_count = 0u32; | |
| 141 | + | |
| 142 | + let mut chunk_id: ChunkId = data[ofs..ofs + 4].try_into().expect("ChunkId to accept 4 bytes"); | |
| 143 | + ofs += 4; | |
| 144 | + | |
| 145 | + let mut chunk_offset: usize = BigEndian::read_u64(&data[ofs..ofs + 8]) | |
| 146 | + .try_into() | |
| 147 | + .expect("an offset small enough to fit a usize"); | |
| 148 | + if chunk_offset < chunk_lookup_end { | |
| 149 | + return Err(Error::Corrupt(format!( | |
| 150 | + "Commit-graph chunk 0 has invalid offset {} (must be at least {})", | |
| 151 | + chunk_offset, chunk_lookup_end | |
| 152 | + ))); | |
| 153 | + } | |
| 154 | + ofs += 8; | |
| 155 | + | |
| 156 | + for _ in 0..chunk_count { | |
| 157 | + let next_chunk_id: ChunkId = data[ofs..ofs + 4].try_into().expect("ChunkId to accept 4 bytes"); | |
| 158 | + ofs += 4; | |
| 159 | + | |
| 160 | + let next_chunk_offset: usize = BigEndian::read_u64(&data[ofs..ofs + 8]) | |
| 161 | + .try_into() | |
| 162 | + .expect("an offset small enough to fit a usize"); | |
| 163 | + ofs += 8; | |
| 164 | + | |
| 165 | + let chunk_size: usize = next_chunk_offset | |
| 166 | + .checked_sub(chunk_offset) | |
| 167 | + .ok_or_else(|| Error::InvalidChunkSize(chunk_id, "size is negative".to_string()))?; | |
| 168 | + if next_chunk_offset >= data_size { | |
| 169 | + return Err(Error::InvalidChunkSize( | |
| 170 | + chunk_id, | |
| 171 | + "chunk extends beyond end of graph_file".to_string(), | |
| 172 | + )); | |
| 173 | + } | |
| 174 | + | |
| 175 | + match chunk_id { | |
| 176 | + BASE_GRAPHS_LIST_CHUNK_ID => { | |
| 177 | + if base_graphs_list_offset.is_some() { | |
| 178 | + return Err(Error::DuplicateChunk(chunk_id)); | |
| 179 | + } | |
| 180 | + if chunk_size % SHA1_SIZE != 0 { | |
| 181 | + return Err(Error::InvalidChunkSize( | |
| 182 | + chunk_id, | |
| 183 | + format!("chunk size {} is not a multiple of {}", chunk_size, SHA1_SIZE), | |
| 184 | + )); | |
| 185 | + } | |
| 186 | + let chunk_base_graph_count = (chunk_size / SHA1_SIZE) as u32; | |
| 187 | + if chunk_base_graph_count != base_graph_count as u32 { | |
| 188 | + return Err(Error::BaseGraphMismatch(base_graph_count, chunk_base_graph_count)); | |
| 189 | + } | |
| 190 | + base_graphs_list_offset = Some(chunk_offset); | |
| 191 | + } | |
| 192 | + COMMIT_DATA_CHUNK_ID => { | |
| 193 | + if commit_data_offset.is_some() { | |
| 194 | + return Err(Error::DuplicateChunk(chunk_id)); | |
| 195 | + } | |
| 196 | + if chunk_size % COMMIT_DATA_ENTRY_SIZE != 0 { | |
| 197 | + return Err(Error::InvalidChunkSize( | |
| 198 | + chunk_id, | |
| 199 | + format!( | |
| 200 | + "chunk size {} is not a multiple of {}", | |
| 201 | + chunk_size, COMMIT_DATA_ENTRY_SIZE | |
| 202 | + ), | |
| 203 | + )); | |
| 204 | + } | |
| 205 | + commit_data_offset = Some(chunk_offset); | |
| 206 | + commit_data_count = (chunk_size / COMMIT_DATA_ENTRY_SIZE) as u32; | |
| 207 | + } | |
| 208 | + EXTENDED_EDGES_LIST_CHUNK_ID => { | |
| 209 | + if extended_edges_list_range.is_some() { | |
| 210 | + return Err(Error::DuplicateChunk(chunk_id)); | |
| 211 | + } | |
| 212 | + | |
| 213 | + extended_edges_list_range = Some(Range { | |
| 214 | + start: chunk_offset, | |
| 215 | + end: next_chunk_offset, | |
| 216 | + }) | |
| 217 | + } | |
| 218 | + OID_FAN_CHUNK_ID => { | |
| 219 | + if fan_offset.is_some() { | |
| 220 | + return Err(Error::DuplicateChunk(chunk_id)); | |
| 221 | + } | |
| 222 | + let expected_size = 4 * FAN_LEN; | |
| 223 | + if chunk_size != expected_size { | |
| 224 | + return Err(Error::InvalidChunkSize( | |
| 225 | + chunk_id, | |
| 226 | + format!("expected chunk length {}, got {}", expected_size, chunk_size), | |
| 227 | + )); | |
| 228 | + } | |
| 229 | + fan_offset = Some(chunk_offset); | |
| 230 | + } | |
| 231 | + OID_LOOKUP_CHUNK_ID => { | |
| 232 | + if oid_lookup_offset.is_some() { | |
| 233 | + return Err(Error::DuplicateChunk(chunk_id)); | |
| 234 | + } | |
| 235 | + if chunk_size % OID_LOOKUP_ENTRY_SIZE != 0 { | |
| 236 | + return Err(Error::InvalidChunkSize( | |
| 237 | + chunk_id, | |
| 238 | + format!( | |
| 239 | + "chunk size {} is not a multiple of {}", | |
| 240 | + chunk_size, OID_LOOKUP_ENTRY_SIZE | |
| 241 | + ), | |
| 242 | + )); | |
| 243 | + } | |
| 244 | + oid_lookup_offset = Some(chunk_offset); | |
| 245 | + oid_lookup_count = (chunk_size / OID_LOOKUP_ENTRY_SIZE) as u32; | |
| 246 | + // git allows extra garbage in the extra edges list chunk? | |
| 247 | + // if oid_lookup_count > 0 { | |
| 248 | + // let last_edge = &data[next_chunk_offset - 4..next_chunk_offset]; | |
| 249 | + // let last_edge = BigEndian::read_u32(last_edge); | |
| 250 | + // if let ExtraEdge::Internal(_) = ExtraEdge::from_raw(last_edge) { | |
| 251 | + // return Err(Error::ExtraEdgesListOverflow); | |
| 252 | + // } | |
| 253 | + // } | |
| 254 | + } | |
| 255 | + _ => {} | |
| 256 | + } | |
| 257 | + | |
| 258 | + chunk_id = next_chunk_id; | |
| 259 | + chunk_offset = next_chunk_offset; | |
| 260 | + } | |
| 261 | + if chunk_id != SENTINEL_CHUNK_ID { | |
| 262 | + return Err(Error::Corrupt(format!( | |
| 263 | + "Commit-graph graph_file has invalid last chunk ID: {:?}", | |
| 264 | + chunk_id.as_bstr() | |
| 265 | + ))); | |
| 266 | + } | |
| 267 | + | |
| 268 | + let fan_offset = fan_offset.ok_or_else(|| Error::MissingChunk(OID_FAN_CHUNK_ID))?; | |
| 269 | + let oid_lookup_offset = oid_lookup_offset.ok_or_else(|| Error::MissingChunk(OID_LOOKUP_CHUNK_ID))?; | |
| 270 | + let commit_data_offset = commit_data_offset.ok_or_else(|| Error::MissingChunk(COMMIT_DATA_CHUNK_ID))?; | |
| 271 | + if base_graph_count > 0 && base_graphs_list_offset == None { | |
| 272 | + return Err(Error::MissingChunk(BASE_GRAPHS_LIST_CHUNK_ID)); | |
| 273 | + } | |
| 274 | + | |
| 275 | + let (fan, _) = read_fan(&data[fan_offset..]); | |
| 276 | + if oid_lookup_count != fan[255] { | |
| 277 | + return Err(Error::CommitCountMismatch( | |
| 278 | + OID_FAN_CHUNK_ID, | |
| 279 | + fan[255], | |
| 280 | + OID_LOOKUP_CHUNK_ID, | |
| 281 | + oid_lookup_count, | |
| 282 | + )); | |
| 283 | + } | |
| 284 | + if commit_data_count != fan[255] { | |
| 285 | + return Err(Error::CommitCountMismatch( | |
| 286 | + OID_FAN_CHUNK_ID, | |
| 287 | + fan[255], | |
| 288 | + COMMIT_DATA_CHUNK_ID, | |
| 289 | + commit_data_count, | |
| 290 | + )); | |
| 291 | + } | |
| 292 | + Ok(GraphFile { | |
| 293 | + base_graph_count, | |
| 294 | + base_graphs_list_offset, | |
| 295 | + commit_data_offset, | |
| 296 | + data, | |
| 297 | + extra_edges_list_range: extended_edges_list_range, | |
| 298 | + fan, | |
| 299 | + oid_lookup_offset, | |
| 300 | + path: path.to_owned(), | |
| 301 | + }) | |
| 302 | + } | |
| 303 | +} | |
| 304 | + | |
| 305 | +// Copied from git-odb/pack/index/init.rs | |
| 306 | +fn read_fan(d: &[u8]) -> ([u32; FAN_LEN], usize) { | |
| 307 | + let mut fan = [0; FAN_LEN]; | |
| 308 | + for (c, f) in d.chunks(4).zip(fan.iter_mut()) { | |
| 309 | + *f = BigEndian::read_u32(c); | |
| 310 | + } | |
| 311 | + (fan, FAN_LEN * 4) | |
| 312 | +} | |
| @@ -0,0 +1,53 @@ | ||
| 1 | +use filebuffer::FileBuffer; | |
| 2 | +use git_object::SHA1_SIZE; | |
| 3 | +use std::ops::Range; | |
| 4 | +use std::path::PathBuf; | |
| 5 | + | |
| 6 | +mod access; | |
| 7 | +pub mod commit_data; | |
| 8 | +mod init; | |
| 9 | + | |
| 10 | +pub use init::Error; | |
| 11 | +use std::fmt::{Display, Formatter}; | |
| 12 | + | |
| 13 | +const COMMIT_DATA_ENTRY_SIZE: usize = SHA1_SIZE + 16; | |
| 14 | +const FAN_LEN: usize = 256; | |
| 15 | +const SIGNATURE: &[u8] = b"CGPH"; | |
| 16 | + | |
| 17 | +/// A single commit-graph file. | |
| 18 | +/// | |
| 19 | +/// All operations on a `GraphFile` are local to that graph file. Since a commit graph can span | |
| 20 | +/// multiple files, all interesting graph operations belong on `Graph`. | |
| 21 | +pub struct GraphFile { | |
| 22 | + base_graph_count: u8, | |
| 23 | + base_graphs_list_offset: Option<usize>, | |
| 24 | + commit_data_offset: usize, | |
| 25 | + data: FileBuffer, | |
| 26 | + extra_edges_list_range: Option<Range<usize>>, | |
| 27 | + fan: [u32; FAN_LEN], | |
| 28 | + oid_lookup_offset: usize, | |
| 29 | + path: PathBuf, | |
| 30 | +} | |
| 31 | + | |
| 32 | +/// The position of a given commit within a graph file, starting at 0. | |
| 33 | +/// | |
| 34 | +/// Commits within a graph file are sorted in lexicographical order by OID; a commit's lex position | |
| 35 | +/// is its position in this ordering. If a commit graph spans multiple files, each file's commits | |
| 36 | +/// start at lex position 0, so lex position is unique across a single file but is not unique across | |
| 37 | +/// the whole commit graph. Each commit also has a graph position (`GraphPosition`), which is unique | |
| 38 | +/// across the whole commit graph. In order to avoid accidentally mixing lex positions with graph | |
| 39 | +/// positions, distinct types are used for each. | |
| 40 | +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] | |
| 41 | +pub struct LexPosition(pub u32); | |
| 42 | + | |
| 43 | +impl Display for LexPosition { | |
| 44 | + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | |
| 45 | + self.0.fmt(f) | |
| 46 | + } | |
| 47 | +} | |
| 48 | + | |
| 49 | +/// The version of a commit graph file. | |
| 50 | +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] | |
| 51 | +pub enum Kind { | |
| 52 | + V1, | |
| 53 | +} | |
| @@ -1 +1,10 @@ | ||
| 1 | 1 | #![forbid(unsafe_code)] |
| 2 | + | |
| 3 | +pub mod graph; | |
| 4 | +pub mod graph_file; | |
| 5 | + | |
| 6 | +pub use graph::{Graph, GraphPosition}; | |
| 7 | +pub use graph_file::commit_data::CommitData; | |
| 8 | +pub use graph_file::{GraphFile, LexPosition}; | |
| 9 | + | |
| 10 | +pub const MAX_COMMITS: u32 = (1 << 30) + (1 << 29) + (1 << 28) - 1; | |
| @@ -0,0 +1,4 @@ | ||
| 1 | +mod octopus_merges; | |
| 2 | +mod single_commit; | |
| 3 | +mod single_parent; | |
| 4 | +mod two_parents; | |
| @@ -0,0 +1,140 @@ | ||
| 1 | +use crate::{fixture_path, hex_to_id}; | |
| 2 | +use git_commitgraph::{Graph, GraphPosition}; | |
| 3 | + | |
| 4 | +const ROOT: &[u8] = b"09e3185550fdb352031df65f0e81e74df3b9a556"; | |
| 5 | +const ROOT_INDEX: GraphPosition = GraphPosition(0); | |
| 6 | +const COMMIT1: &[u8] = b"33316871b4ab988ea87ba60aaf0138ddb4fb0d21"; | |
| 7 | +const COMMIT1_INDEX: GraphPosition = GraphPosition(2); | |
| 8 | +const COMMIT2: &[u8] = b"cbfe147d1826a0d2701f7c80dbe0e2cfba385b15"; | |
| 9 | +const COMMIT2_INDEX: GraphPosition = GraphPosition(6); | |
| 10 | +const COMMIT3: &[u8] = b"9dab5d610702ceca4bd889a5ef2662cacfd53178"; | |
| 11 | +const COMMIT3_INDEX: GraphPosition = GraphPosition(5); | |
| 12 | +const COMMIT4: &[u8] = b"2ec748404455c0b0451b186851a815da13918ebb"; | |
| 13 | +const COMMIT4_INDEX: GraphPosition = GraphPosition(1); | |
| 14 | +const THREE_PARENTS: &[u8] = b"7070cd0b59cd46a0f0fa0c50ca9148390ae1e7c7"; | |
| 15 | +const THREE_PARENTS_INDEX: GraphPosition = GraphPosition(4); | |
| 16 | +const FOUR_PARENTS: &[u8] = b"5ca46a72d91817add3a7218f3eaf8b2b6c1caa13"; | |
| 17 | +const FOUR_PARENTS_INDEX: GraphPosition = GraphPosition(3); | |
| 18 | + | |
| 19 | +#[test] | |
| 20 | +fn v1() -> Result<(), Box<dyn std::error::Error>> { | |
| 21 | + let root_id = hex_to_id(ROOT); | |
| 22 | + let commit1_id = hex_to_id(COMMIT1); | |
| 23 | + let commit2_id = hex_to_id(COMMIT2); | |
| 24 | + let commit3_id = hex_to_id(COMMIT3); | |
| 25 | + let commit4_id = hex_to_id(COMMIT4); | |
| 26 | + let three_parents_id = hex_to_id(THREE_PARENTS); | |
| 27 | + let four_parents_id = hex_to_id(FOUR_PARENTS); | |
| 28 | + | |
| 29 | + let cg = Graph::from_object_dir(fixture_path("v1/octopus_merges"))?; | |
| 30 | + assert_eq!(cg.num_commits(), 7); | |
| 31 | + assert_eq!(cg.id_at(ROOT_INDEX), root_id.to_borrowed()); | |
| 32 | + assert_eq!(cg.id_at(COMMIT1_INDEX), commit1_id.to_borrowed()); | |
| 33 | + assert_eq!(cg.id_at(COMMIT2_INDEX), commit2_id.to_borrowed()); | |
| 34 | + assert_eq!(cg.id_at(COMMIT3_INDEX), commit3_id.to_borrowed()); | |
| 35 | + assert_eq!(cg.id_at(COMMIT4_INDEX), commit4_id.to_borrowed()); | |
| 36 | + assert_eq!(cg.id_at(THREE_PARENTS_INDEX), three_parents_id.to_borrowed()); | |
| 37 | + assert_eq!(cg.id_at(FOUR_PARENTS_INDEX), four_parents_id.to_borrowed()); | |
| 38 | + assert_eq!( | |
| 39 | + cg.iter_ids().collect::<Vec<_>>(), | |
| 40 | + vec![ | |
| 41 | + root_id.to_borrowed(), | |
| 42 | + commit4_id.to_borrowed(), | |
| 43 | + commit1_id.to_borrowed(), | |
| 44 | + four_parents_id.to_borrowed(), | |
| 45 | + three_parents_id.to_borrowed(), | |
| 46 | + commit3_id.to_borrowed(), | |
| 47 | + commit2_id.to_borrowed(), | |
| 48 | + ] | |
| 49 | + ); | |
| 50 | + | |
| 51 | + let root = cg.commit_at(ROOT_INDEX); | |
| 52 | + assert_eq!(root.generation(), 1); | |
| 53 | + assert_eq!(root.id(), root_id.to_borrowed()); | |
| 54 | + assert_eq!(root.parent1()?, None); | |
| 55 | + assert_eq!( | |
| 56 | + root.root_tree_id(), | |
| 57 | + hex_to_id(b"4b825dc642cb6eb9a060e54bf8d69288fbee4904").to_borrowed(), | |
| 58 | + ); | |
| 59 | + assert_eq!(root.iter_parent_indices().collect::<Result<Vec<_>, _>>()?, vec![]); | |
| 60 | + | |
| 61 | + let commit1 = cg.commit_at(COMMIT1_INDEX); | |
| 62 | + assert_eq!(commit1.generation(), 2); | |
| 63 | + assert_eq!(commit1.id(), commit1_id.to_borrowed()); | |
| 64 | + assert_eq!(commit1.parent1()?, Some(ROOT_INDEX)); | |
| 65 | + assert_eq!( | |
| 66 | + commit1.root_tree_id(), | |
| 67 | + hex_to_id(b"4b825dc642cb6eb9a060e54bf8d69288fbee4904").to_borrowed(), | |
| 68 | + ); | |
| 69 | + assert_eq!( | |
| 70 | + commit1.iter_parent_indices().collect::<Result<Vec<_>, _>>()?, | |
| 71 | + vec![ROOT_INDEX], | |
| 72 | + ); | |
| 73 | + | |
| 74 | + let commit2 = cg.commit_at(COMMIT2_INDEX); | |
| 75 | + assert_eq!(commit2.generation(), 2); | |
| 76 | + assert_eq!(commit2.id(), commit2_id.to_borrowed()); | |
| 77 | + assert_eq!(commit2.parent1()?, Some(ROOT_INDEX)); | |
| 78 | + assert_eq!( | |
| 79 | + commit2.root_tree_id(), | |
| 80 | + hex_to_id(b"4b825dc642cb6eb9a060e54bf8d69288fbee4904").to_borrowed(), | |
| 81 | + ); | |
| 82 | + assert_eq!( | |
| 83 | + commit2.iter_parent_indices().collect::<Result<Vec<_>, _>>()?, | |
| 84 | + vec![ROOT_INDEX], | |
| 85 | + ); | |
| 86 | + | |
| 87 | + let commit3 = cg.commit_at(COMMIT3_INDEX); | |
| 88 | + assert_eq!(commit3.generation(), 2); | |
| 89 | + assert_eq!(commit3.id(), commit3_id.to_borrowed()); | |
| 90 | + assert_eq!(commit3.parent1()?, Some(ROOT_INDEX)); | |
| 91 | + assert_eq!( | |
| 92 | + commit3.root_tree_id(), | |
| 93 | + hex_to_id(b"4b825dc642cb6eb9a060e54bf8d69288fbee4904").to_borrowed(), | |
| 94 | + ); | |
| 95 | + assert_eq!( | |
| 96 | + commit3.iter_parent_indices().collect::<Result<Vec<_>, _>>()?, | |
| 97 | + vec![ROOT_INDEX], | |
| 98 | + ); | |
| 99 | + | |
| 100 | + let commit4 = cg.commit_at(COMMIT4_INDEX); | |
| 101 | + assert_eq!(commit4.generation(), 2); | |
| 102 | + assert_eq!(commit4.id(), commit4_id.to_borrowed()); | |
| 103 | + assert_eq!(commit4.parent1()?, Some(ROOT_INDEX)); | |
| 104 | + assert_eq!( | |
| 105 | + commit4.root_tree_id(), | |
| 106 | + hex_to_id(b"4b825dc642cb6eb9a060e54bf8d69288fbee4904").to_borrowed(), | |
| 107 | + ); | |
| 108 | + assert_eq!( | |
| 109 | + commit4.iter_parent_indices().collect::<Result<Vec<_>, _>>()?, | |
| 110 | + vec![ROOT_INDEX], | |
| 111 | + ); | |
| 112 | + | |
| 113 | + let three_parents = cg.commit_at(THREE_PARENTS_INDEX); | |
| 114 | + assert_eq!(three_parents.generation(), 3); | |
| 115 | + assert_eq!(three_parents.id(), three_parents_id.to_borrowed()); | |
| 116 | + assert_eq!(three_parents.parent1()?, Some(COMMIT1_INDEX)); | |
| 117 | + assert_eq!( | |
| 118 | + three_parents.root_tree_id(), | |
| 119 | + hex_to_id(b"4b825dc642cb6eb9a060e54bf8d69288fbee4904").to_borrowed(), | |
| 120 | + ); | |
| 121 | + assert_eq!( | |
| 122 | + three_parents.iter_parent_indices().collect::<Result<Vec<_>, _>>()?, | |
| 123 | + vec![COMMIT1_INDEX, COMMIT2_INDEX, COMMIT3_INDEX], | |
| 124 | + ); | |
| 125 | + | |
| 126 | + let four_parents = cg.commit_at(FOUR_PARENTS_INDEX); | |
| 127 | + assert_eq!(four_parents.generation(), 3); | |
| 128 | + assert_eq!(four_parents.id(), four_parents_id.to_borrowed()); | |
| 129 | + assert_eq!(four_parents.parent1()?, Some(COMMIT2_INDEX)); | |
| 130 | + assert_eq!( | |
| 131 | + four_parents.root_tree_id(), | |
| 132 | + hex_to_id(b"4b825dc642cb6eb9a060e54bf8d69288fbee4904").to_borrowed(), | |
| 133 | + ); | |
| 134 | + assert_eq!( | |
| 135 | + four_parents.iter_parent_indices().collect::<Result<Vec<_>, _>>()?, | |
| 136 | + vec![COMMIT2_INDEX, COMMIT1_INDEX, COMMIT3_INDEX, COMMIT4_INDEX], | |
| 137 | + ); | |
| 138 | + | |
| 139 | + Ok(()) | |
| 140 | +} | |
| @@ -0,0 +1,26 @@ | ||
| 1 | +use crate::{fixture_path, hex_to_id}; | |
| 2 | +use git_commitgraph::{Graph, GraphPosition}; | |
| 3 | + | |
| 4 | +const COMMIT: &[u8] = b"771fa42310bb3d221f4cf82613a491b0957d2003"; | |
| 5 | + | |
| 6 | +#[test] | |
| 7 | +fn v1() -> Result<(), Box<dyn std::error::Error>> { | |
| 8 | + let commit_id = hex_to_id(COMMIT); | |
| 9 | + | |
| 10 | + let cg = Graph::from_object_dir(fixture_path("v1/single_commit"))?; | |
| 11 | + assert_eq!(cg.num_commits(), 1); | |
| 12 | + assert_eq!(cg.id_at(GraphPosition(0)), commit_id.to_borrowed()); | |
| 13 | + assert_eq!(cg.iter_ids().collect::<Vec<_>>(), vec![commit_id.to_borrowed()]); | |
| 14 | + | |
| 15 | + let commit = cg.commit_at(GraphPosition(0)); | |
| 16 | + assert_eq!(commit.generation(), 1); | |
| 17 | + assert_eq!(commit.id(), commit_id.to_borrowed()); | |
| 18 | + assert_eq!(commit.parent1()?, None); | |
| 19 | + assert_eq!( | |
| 20 | + commit.root_tree_id(), | |
| 21 | + hex_to_id(b"4b825dc642cb6eb9a060e54bf8d69288fbee4904").to_borrowed(), | |
| 22 | + ); | |
| 23 | + assert_eq!(commit.iter_parent_indices().collect::<Result<Vec<_>, _>>()?, vec![]); | |
| 24 | + | |
| 25 | + Ok(()) | |
| 26 | +} | |
| @@ -0,0 +1,44 @@ | ||
| 1 | +use crate::{fixture_path, hex_to_id}; | |
| 2 | +use git_commitgraph::{Graph, GraphPosition}; | |
| 3 | + | |
| 4 | +const PARENT: &[u8] = b"ddbbe1220676705c5b7a6bb7047e0d63b58cfc28"; | |
| 5 | +const CHILD: &[u8] = b"91bfe17bfdfef01c4c38e34e577b01a14a1ef2d4"; | |
| 6 | + | |
| 7 | +#[test] | |
| 8 | +fn v1() -> Result<(), Box<dyn std::error::Error>> { | |
| 9 | + let parent_id = hex_to_id(PARENT); | |
| 10 | + let child_id = hex_to_id(CHILD); | |
| 11 | + | |
| 12 | + let cg = Graph::from_object_dir(fixture_path("v1/single_parent"))?; | |
| 13 | + assert_eq!(cg.num_commits(), 2); | |
| 14 | + assert_eq!(cg.id_at(GraphPosition(0)), child_id.to_borrowed()); | |
| 15 | + assert_eq!( | |
| 16 | + cg.iter_ids().collect::<Vec<_>>(), | |
| 17 | + vec![child_id.to_borrowed(), parent_id.to_borrowed()] | |
| 18 | + ); | |
| 19 | + | |
| 20 | + let parent = cg.commit_at(GraphPosition(1)); | |
| 21 | + assert_eq!(parent.generation(), 1); | |
| 22 | + assert_eq!(parent.id(), parent_id.to_borrowed()); | |
| 23 | + assert_eq!(parent.parent1()?, None); | |
| 24 | + assert_eq!( | |
| 25 | + parent.root_tree_id(), | |
| 26 | + hex_to_id(b"4b825dc642cb6eb9a060e54bf8d69288fbee4904").to_borrowed(), | |
| 27 | + ); | |
| 28 | + assert_eq!(parent.iter_parent_indices().collect::<Result<Vec<_>, _>>()?, vec![]); | |
| 29 | + | |
| 30 | + let child = cg.commit_at(GraphPosition(0)); | |
| 31 | + assert_eq!(child.generation(), 2); | |
| 32 | + assert_eq!(child.id(), child_id.to_borrowed()); | |
| 33 | + assert_eq!(child.parent1()?, Some(GraphPosition(1))); | |
| 34 | + assert_eq!( | |
| 35 | + child.root_tree_id(), | |
| 36 | + hex_to_id(b"4b825dc642cb6eb9a060e54bf8d69288fbee4904").to_borrowed(), | |
| 37 | + ); | |
| 38 | + assert_eq!( | |
| 39 | + child.iter_parent_indices().collect::<Result<Vec<_>, _>>()?, | |
| 40 | + vec![GraphPosition(1)] | |
| 41 | + ); | |
| 42 | + | |
| 43 | + Ok(()) | |
| 44 | +} | |
| @@ -0,0 +1,65 @@ | ||
| 1 | +use crate::{fixture_path, hex_to_id}; | |
| 2 | +use git_commitgraph::{Graph, GraphPosition}; | |
| 3 | + | |
| 4 | +const PARENT1: &[u8] = b"7506a95b8062f0bee6ccdee717021a0c710a1959"; | |
| 5 | +const PARENT1_INDEX: GraphPosition = GraphPosition(2); | |
| 6 | +const PARENT2: &[u8] = b"12ae061ac737392973e59d5df2103a72f1088e45"; | |
| 7 | +const PARENT2_INDEX: GraphPosition = GraphPosition(0); | |
| 8 | +const MERGE: &[u8] = b"6a23d4bb5ef62fff1b57b898240bf5a9d5813800"; | |
| 9 | +const MERGE_INDEX: GraphPosition = GraphPosition(1); | |
| 10 | + | |
| 11 | +#[test] | |
| 12 | +fn v1() -> Result<(), Box<dyn std::error::Error>> { | |
| 13 | + let parent1_id = hex_to_id(PARENT1); | |
| 14 | + let parent2_id = hex_to_id(PARENT2); | |
| 15 | + let merge_id = hex_to_id(MERGE); | |
| 16 | + | |
| 17 | + let cg = Graph::from_object_dir(fixture_path("v1/two_parents"))?; | |
| 18 | + assert_eq!(cg.num_commits(), 3); | |
| 19 | + assert_eq!(cg.id_at(PARENT1_INDEX), parent1_id.to_borrowed()); | |
| 20 | + assert_eq!(cg.id_at(PARENT2_INDEX), parent2_id.to_borrowed()); | |
| 21 | + assert_eq!(cg.id_at(MERGE_INDEX), merge_id.to_borrowed()); | |
| 22 | + assert_eq!( | |
| 23 | + cg.iter_ids().collect::<Vec<_>>(), | |
| 24 | + vec![ | |
| 25 | + parent2_id.to_borrowed(), | |
| 26 | + merge_id.to_borrowed(), | |
| 27 | + parent1_id.to_borrowed() | |
| 28 | + ] | |
| 29 | + ); | |
| 30 | + | |
| 31 | + let parent1 = cg.commit_at(PARENT1_INDEX); | |
| 32 | + assert_eq!(parent1.generation(), 1); | |
| 33 | + assert_eq!(parent1.id(), parent1_id.to_borrowed()); | |
| 34 | + assert_eq!(parent1.parent1()?, None); | |
| 35 | + assert_eq!( | |
| 36 | + parent1.root_tree_id(), | |
| 37 | + hex_to_id(b"4b825dc642cb6eb9a060e54bf8d69288fbee4904").to_borrowed(), | |
| 38 | + ); | |
| 39 | + assert_eq!(parent1.iter_parent_indices().collect::<Result<Vec<_>, _>>()?, vec![]); | |
| 40 | + | |
| 41 | + let parent2 = cg.commit_at(PARENT2_INDEX); | |
| 42 | + assert_eq!(parent2.generation(), 1); | |
| 43 | + assert_eq!(parent2.id(), parent2_id.to_borrowed()); | |
| 44 | + assert_eq!(parent2.parent1()?, None); | |
| 45 | + assert_eq!( | |
| 46 | + parent2.root_tree_id(), | |
| 47 | + hex_to_id(b"4b825dc642cb6eb9a060e54bf8d69288fbee4904").to_borrowed(), | |
| 48 | + ); | |
| 49 | + assert_eq!(parent2.iter_parent_indices().collect::<Result<Vec<_>, _>>()?, vec![]); | |
| 50 | + | |
| 51 | + let merge = cg.commit_at(MERGE_INDEX); | |
| 52 | + assert_eq!(merge.generation(), 2); | |
| 53 | + assert_eq!(merge.id(), merge_id.to_borrowed()); | |
| 54 | + assert_eq!(merge.parent1()?, Some(PARENT1_INDEX)); | |
| 55 | + assert_eq!( | |
| 56 | + merge.root_tree_id(), | |
| 57 | + hex_to_id(b"4b825dc642cb6eb9a060e54bf8d69288fbee4904").to_borrowed(), | |
| 58 | + ); | |
| 59 | + assert_eq!( | |
| 60 | + merge.iter_parent_indices().collect::<Result<Vec<_>, _>>()?, | |
| 61 | + vec![PARENT1_INDEX, PARENT2_INDEX] | |
| 62 | + ); | |
| 63 | + | |
| 64 | + Ok(()) | |
| 65 | +} | |
| @@ -0,0 +1,12 @@ | ||
| 1 | +use git_object::owned; | |
| 2 | +use std::path::PathBuf; | |
| 3 | + | |
| 4 | +mod access; | |
| 5 | + | |
| 6 | +pub fn fixture_path(path: &str) -> PathBuf { | |
| 7 | + PathBuf::from("tests").join("fixtures").join(path) | |
| 8 | +} | |
| 9 | + | |
| 10 | +pub fn hex_to_id(hex: &[u8]) -> owned::Id { | |
| 11 | + owned::Id::from_40_bytes_in_hex(hex).expect("40 bytes hex") | |
| 12 | +} | |
| @@ -0,0 +1,34 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +set -eu -o pipefail | |
| 3 | + | |
| 4 | +parent_dir="$1" | |
| 5 | +mkdir "$parent_dir" | |
| 6 | + | |
| 7 | +script_dir=$(dirname "$(realpath -e "$0")") | |
| 8 | + | |
| 9 | +run() { | |
| 10 | + local target_dir=$parent_dir/$1 | |
| 11 | + local script=$script_dir/$1.sh | |
| 12 | + | |
| 13 | + local temp_dir=$(mktemp -d create_fixtures-XXXXXXXXXX) | |
| 14 | + trap "rm -rf $temp_dir" EXIT | |
| 15 | + "$script" "$temp_dir" | |
| 16 | + cp -dR "$temp_dir/.git/objects/" "$target_dir/" | |
| 17 | + rm -rf "$temp_dir" | |
| 18 | + trap - EXIT | |
| 19 | +} | |
| 20 | + | |
| 21 | +run bloom | |
| 22 | +run bloom_too_large | |
| 23 | +run octopus_merges | |
| 24 | +run single_commit | |
| 25 | +run single_parent | |
| 26 | +run split_chain | |
| 27 | +run two_parents | |
| 28 | + | |
| 29 | +#"$script_dir"/bloom.sh "$parent_dir/bloom" | |
| 30 | +#"$script_dir"/bloom_too_large.sh "$parent_dir/bloom_to_large" | |
| 31 | +#"$script_dir"/octopus_merges.sh "$parent_dir/octopus_merges" | |
| 32 | +#"$script_dir"/single_commit.sh "$parent_dir/single_commit" | |
| 33 | +#"$script_dir"/single_parent.sh "$parent_dir/single_parent" | |
| 34 | +#"$script_dir"/two_parents.sh "$parent_dir/two_parents" | |
| @@ -0,0 +1,37 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +set -eu -o pipefail | |
| 3 | + | |
| 4 | +export GIT_AUTHOR_DATE="2000-01-01 00:00:00 +0000" | |
| 5 | +export GIT_AUTHOR_EMAIL=author@example.com | |
| 6 | +export GIT_AUTHOR_NAME=author | |
| 7 | +export GIT_COMMITTER_DATE="2000-01-02 00:00:00 +0000" | |
| 8 | +export GIT_COMMITTER_EMAIL=committer@example.com | |
| 9 | +export GIT_COMMITTER_NAME=committer | |
| 10 | + | |
| 11 | +mkdir -p "$1" | |
| 12 | +cd "$1" | |
| 13 | +git init -q | |
| 14 | + | |
| 15 | +git checkout -q --orphan root | |
| 16 | +git commit -q --allow-empty -m root | |
| 17 | + | |
| 18 | +git checkout -q -b commit1 root | |
| 19 | +git commit -q --allow-empty -m commit1 | |
| 20 | + | |
| 21 | +git checkout -q -b commit2 root | |
| 22 | +git commit -q --allow-empty -m commit2 | |
| 23 | + | |
| 24 | +git checkout -q -b commit3 root | |
| 25 | +git commit -q --allow-empty -m commit3 | |
| 26 | + | |
| 27 | +git checkout -q -b commit4 root | |
| 28 | +git commit -q --allow-empty -m commit4 | |
| 29 | + | |
| 30 | +git checkout -q -b three_parents commit1 | |
| 31 | +git merge -q -m three_parents --no-ff commit2 commit3 >/dev/null | |
| 32 | + | |
| 33 | +git checkout -q -b four_parents commit2 | |
| 34 | +git merge -q -m four_parents --no-ff commit1 commit3 commit4 >/dev/null | |
| 35 | + | |
| 36 | +git commit-graph write --no-progress --reachable | |
| 37 | +git repack -adq | |
| @@ -0,0 +1,21 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +set -eu -o pipefail | |
| 3 | + | |
| 4 | +# The goal with this repo is to have the smallest commit-graph file possible, in the hopes that an | |
| 5 | +# off-by-one error | |
| 6 | +export GIT_AUTHOR_DATE="2000-01-01 00:00:00 +0000" | |
| 7 | +export GIT_AUTHOR_EMAIL=author@example.com | |
| 8 | +export GIT_AUTHOR_NAME=author | |
| 9 | +export GIT_COMMITTER_DATE="2000-01-02 00:00:00 +0000" | |
| 10 | +export GIT_COMMITTER_EMAIL=committer@example.com | |
| 11 | +export GIT_COMMITTER_NAME=committer | |
| 12 | + | |
| 13 | +mkdir -p "$1" | |
| 14 | +cd "$1" | |
| 15 | +git init -q | |
| 16 | + | |
| 17 | +git checkout -q -b commit | |
| 18 | +git commit -q --allow-empty -m commit | |
| 19 | + | |
| 20 | +git commit-graph write --no-progress --reachable | |
| 21 | +git repack -adq | |
| @@ -0,0 +1,22 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +set -eu -o pipefail | |
| 3 | + | |
| 4 | +export GIT_AUTHOR_DATE="2000-01-01 00:00:00 +0000" | |
| 5 | +export GIT_AUTHOR_EMAIL=author@example.com | |
| 6 | +export GIT_AUTHOR_NAME=author | |
| 7 | +export GIT_COMMITTER_DATE="2000-01-02 00:00:00 +0000" | |
| 8 | +export GIT_COMMITTER_EMAIL=committer@example.com | |
| 9 | +export GIT_COMMITTER_NAME=committer | |
| 10 | + | |
| 11 | +mkdir -p "$1" | |
| 12 | +cd "$1" | |
| 13 | +git init -q | |
| 14 | + | |
| 15 | +git checkout -q -b parent | |
| 16 | +git commit -q --allow-empty -m parent | |
| 17 | + | |
| 18 | +git checkout -q -b child parent | |
| 19 | +git commit -q --allow-empty -m child | |
| 20 | + | |
| 21 | +git commit-graph write --no-progress --reachable | |
| 22 | +git repack -adq | |
| @@ -0,0 +1,25 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +set -eu -o pipefail | |
| 3 | + | |
| 4 | +export GIT_AUTHOR_DATE="2000-01-01 00:00:00 +0000" | |
| 5 | +export GIT_AUTHOR_EMAIL=author@example.com | |
| 6 | +export GIT_AUTHOR_NAME=author | |
| 7 | +export GIT_COMMITTER_DATE="2000-01-02 00:00:00 +0000" | |
| 8 | +export GIT_COMMITTER_EMAIL=committer@example.com | |
| 9 | +export GIT_COMMITTER_NAME=committer | |
| 10 | + | |
| 11 | +mkdir -p "$1" | |
| 12 | +cd "$1" | |
| 13 | +git init -q | |
| 14 | + | |
| 15 | +git checkout -q -b commit1 | |
| 16 | +git commit -q --allow-empty -m commit1 | |
| 17 | +git checkout -q -b commit2 commit1 | |
| 18 | +git commit -q --allow-empty -m commit2 | |
| 19 | +git checkout -q -b commit3 commit2 | |
| 20 | +git commit -q --allow-empty -m commit3 | |
| 21 | + | |
| 22 | +git show-ref -s commit1 | git commit-graph write --no-progress --split=no-merge --stdin-commits | |
| 23 | +git show-ref -s commit2 | git commit-graph write --no-progress --split=no-merge --stdin-commits | |
| 24 | +git show-ref -s commit3 | git commit-graph write --no-progress --split=no-merge --stdin-commits | |
| 25 | +git repack -adq | |
| @@ -0,0 +1,25 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +set -eu -o pipefail | |
| 3 | + | |
| 4 | +export GIT_AUTHOR_DATE="2000-01-01 00:00:00 +0000" | |
| 5 | +export GIT_AUTHOR_EMAIL=author@example.com | |
| 6 | +export GIT_AUTHOR_NAME=author | |
| 7 | +export GIT_COMMITTER_DATE="2000-01-02 00:00:00 +0000" | |
| 8 | +export GIT_COMMITTER_EMAIL=committer@example.com | |
| 9 | +export GIT_COMMITTER_NAME=committer | |
| 10 | + | |
| 11 | +mkdir -p "$1" | |
| 12 | +cd "$1" | |
| 13 | +git init -q | |
| 14 | + | |
| 15 | +git checkout -q --orphan parent1 | |
| 16 | +git commit -q --allow-empty -m parent1 | |
| 17 | + | |
| 18 | +git checkout -q --orphan parent2 | |
| 19 | +git commit -q --allow-empty -m parent2 | |
| 20 | + | |
| 21 | +git checkout -q -b merge parent1 | |
| 22 | +git merge -q --allow-unrelated-histories --no-ff -m merge parent2 >/dev/null | |
| 23 | + | |
| 24 | +git commit-graph write --no-progress --reachable | |
| 25 | +git repack -adq | |
Binary file not shown.
| @@ -0,0 +1,2 @@ | ||
| 1 | +P pack-317d17c824d1a0b6991949219f7b6ba9ad51c3e6.pack | |
| 2 | + | |
Binary file not shown.
Binary file not shown.
Binary file not shown.
| @@ -0,0 +1,2 @@ | ||
| 1 | +P pack-b7a28066d2c1e9511d1a60349d0d6e3bf803d1b6.pack | |
| 2 | + | |
Binary file not shown.
Binary file not shown.
Binary file not shown.
| @@ -0,0 +1,2 @@ | ||
| 1 | +P pack-976438ba1bde7d9c944d8681cbfad266feec7068.pack | |
| 2 | + | |
Binary file not shown.
Binary file not shown.
Binary file not shown.
| @@ -0,0 +1,2 @@ | ||
| 1 | +P pack-07779428eae7661b1471110dda1efc99e3e954e7.pack | |
| 2 | + | |
Binary file not shown.
Binary file not shown.
Binary file not shown.
| @@ -0,0 +1,2 @@ | ||
| 1 | +P pack-ba89c2e6239315806e37d189aebf4a582e96a6d5.pack | |
| 2 | + | |
Binary file not shown.
Binary file not shown.
| @@ -0,0 +1,3 @@ | ||
| 1 | +ca9f15bcd6129d304e0f4d4c8c6545467dcc78e1 | |
| 2 | +422d4c835a8a22a401b80f8a76f0fa0be550db1b | |
| 3 | +eda6e3e68ac22be2ae8191ed2c80113e7f0344c6 | |
Binary file not shown.
Binary file not shown.
Binary file not shown.
| @@ -0,0 +1,2 @@ | ||
| 1 | +P pack-35e0a0e2cb2913c7990d1c6a6aa4747d9b3b5306.pack | |
| 2 | + | |
Binary file not shown.
Binary file not shown.
Binary file not shown.
| @@ -0,0 +1,2 @@ | ||
| 1 | +P pack-ca696e967b89b66c96f70bcb9d9e8bba42417bcc.pack | |
| 2 | + | |
Binary file not shown.
Binary file not shown.