History

[commitgraph] implement basic, low-level read API

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.

Conor Davis committed 6 years ago

Commit d1f0e9c1 parent 1b42b3153 files · +1429 −-23Browse files

Showing the changes to git-commitgraph (52 of 53 files). Show all

Unified · Split

52 files
  1. git-commitgraph/Cargo.toml +6 −0
  2. git-commitgraph/src/graph/access.rs +83 −0
  3. git-commitgraph/src/graph/init.rs +104 −0
  4. git-commitgraph/src/graph/mod.rs +23 −0
  5. git-commitgraph/src/graph_file/access.rs +128 −0
  6. git-commitgraph/src/graph_file/commit_data.rs +255 −0
  7. git-commitgraph/src/graph_file/init.rs +312 −0
  8. git-commitgraph/src/graph_file/mod.rs +53 −0
  9. git-commitgraph/src/lib.rs +9 −0
  10. git-commitgraph/tests/access/mod.rs +4 −0
  11. git-commitgraph/tests/access/octopus_merges.rs +140 −0
  12. git-commitgraph/tests/access/single_commit.rs +26 −0
  13. git-commitgraph/tests/access/single_parent.rs +44 −0
  14. git-commitgraph/tests/access/two_parents.rs +65 −0
  15. git-commitgraph/tests/commitgraph.rs +12 −0
  16. git-commitgraph/tests/fixtures/create_fixtures.sh +34 −0
  17. git-commitgraph/tests/fixtures/octopus_merges.sh +37 −0
  18. git-commitgraph/tests/fixtures/single_commit.sh +21 −0
  19. git-commitgraph/tests/fixtures/single_parent.sh +22 −0
  20. git-commitgraph/tests/fixtures/split_chain.sh +25 −0
  21. git-commitgraph/tests/fixtures/two_parents.sh +25 −0
  22. git-commitgraph/tests/fixtures/v1/bloom/info/commit-graph +0 −0
  23. git-commitgraph/tests/fixtures/v1/bloom/info/packs +2 −0
  24. git-commitgraph/tests/fixtures/v1/bloom/pack/pack-317d17c824d1a0b6991949219f7b6ba9ad51c3e6.idx +0 −0
  25. git-commitgraph/tests/fixtures/v1/bloom/pack/pack-317d17c824d1a0b6991949219f7b6ba9ad51c3e6.pack +0 −0
  26. git-commitgraph/tests/fixtures/v1/bloom_too_large/info/commit-graph +0 −0
  27. git-commitgraph/tests/fixtures/v1/bloom_too_large/info/packs +2 −0
  28. git-commitgraph/tests/fixtures/v1/bloom_too_large/pack/pack-b7a28066d2c1e9511d1a60349d0d6e3bf803d1b6.idx +0 −0
  29. git-commitgraph/tests/fixtures/v1/bloom_too_large/pack/pack-b7a28066d2c1e9511d1a60349d0d6e3bf803d1b6.pack +0 −0
  30. git-commitgraph/tests/fixtures/v1/octopus_merges/info/commit-graph +0 −0
  31. git-commitgraph/tests/fixtures/v1/octopus_merges/info/packs +2 −0
  32. git-commitgraph/tests/fixtures/v1/octopus_merges/pack/pack-976438ba1bde7d9c944d8681cbfad266feec7068.idx +0 −0
  33. git-commitgraph/tests/fixtures/v1/octopus_merges/pack/pack-976438ba1bde7d9c944d8681cbfad266feec7068.pack +0 −0
  34. git-commitgraph/tests/fixtures/v1/single_commit/info/commit-graph +0 −0
  35. git-commitgraph/tests/fixtures/v1/single_commit/info/packs +2 −0
  36. git-commitgraph/tests/fixtures/v1/single_commit/pack/pack-07779428eae7661b1471110dda1efc99e3e954e7.idx +0 −0
  37. git-commitgraph/tests/fixtures/v1/single_commit/pack/pack-07779428eae7661b1471110dda1efc99e3e954e7.pack +0 −0
  38. git-commitgraph/tests/fixtures/v1/single_parent/info/commit-graph +0 −0
  39. git-commitgraph/tests/fixtures/v1/single_parent/info/packs +2 −0
  40. git-commitgraph/tests/fixtures/v1/single_parent/pack/pack-ba89c2e6239315806e37d189aebf4a582e96a6d5.idx +0 −0
  41. git-commitgraph/tests/fixtures/v1/single_parent/pack/pack-ba89c2e6239315806e37d189aebf4a582e96a6d5.pack +0 −0
  42. git-commitgraph/tests/fixtures/v1/split_chain/info/commit-graphs/commit-graph-chain +3 −0
  43. git-commitgraph/tests/fixtures/v1/split_chain/info/commit-graphs/graph-422d4c835a8a22a401b80f8a76f0fa0be550db1b.graph +0 −0
  44. git-commitgraph/tests/fixtures/v1/split_chain/info/commit-graphs/graph-ca9f15bcd6129d304e0f4d4c8c6545467dcc78e1.graph +0 −0
  45. git-commitgraph/tests/fixtures/v1/split_chain/info/commit-graphs/graph-eda6e3e68ac22be2ae8191ed2c80113e7f0344c6.graph +0 −0
  46. git-commitgraph/tests/fixtures/v1/split_chain/info/packs +2 −0
  47. git-commitgraph/tests/fixtures/v1/split_chain/pack/pack-35e0a0e2cb2913c7990d1c6a6aa4747d9b3b5306.idx +0 −0
  48. git-commitgraph/tests/fixtures/v1/split_chain/pack/pack-35e0a0e2cb2913c7990d1c6a6aa4747d9b3b5306.pack +0 −0
  49. git-commitgraph/tests/fixtures/v1/two_parents/info/commit-graph +0 −0
  50. git-commitgraph/tests/fixtures/v1/two_parents/info/packs +2 −0
  51. git-commitgraph/tests/fixtures/v1/two_parents/pack/pack-ca696e967b89b66c96f70bcb9d9e8bba42417bcc.idx +0 −0
  52. git-commitgraph/tests/fixtures/v1/two_parents/pack/pack-ca696e967b89b66c96f70bcb9d9e8bba42417bcc.pack +0 −0
git-commitgraph/Cargo.toml+6−0
@@ -14,3 +14,9 @@ doctest = false · 13 unmodified lines
1414 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1515
1616 [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"
git-commitgraph/src/graph/access.rs+83−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+}
git-commitgraph/src/graph/init.rs+104−0
@@ -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+}
git-commitgraph/src/graph/mod.rs+23−0
@@ -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+}
git-commitgraph/src/graph_file/access.rs+128−0
@@ -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+}
git-commitgraph/src/graph_file/commit_data.rs+255−0
@@ -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+}
git-commitgraph/src/graph_file/init.rs+312−0
@@ -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+}
git-commitgraph/src/graph_file/mod.rs+53−0
@@ -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+}
git-commitgraph/src/lib.rs+9−0
@@ -1 +1,10 @@
11 #![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;
git-commitgraph/tests/access/mod.rs+4−0
@@ -0,0 +1,4 @@
1+mod octopus_merges;
2+mod single_commit;
3+mod single_parent;
4+mod two_parents;
git-commitgraph/tests/access/octopus_merges.rs+140−0
@@ -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+}
git-commitgraph/tests/access/single_commit.rs+26−0
@@ -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+}
git-commitgraph/tests/access/single_parent.rs+44−0
@@ -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+}
git-commitgraph/tests/access/two_parents.rs+65−0
@@ -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+}
git-commitgraph/tests/commitgraph.rs+12−0
@@ -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+}
git-commitgraph/tests/fixtures/create_fixtures.sh+34−0
@@ -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"
git-commitgraph/tests/fixtures/octopus_merges.sh+37−0
@@ -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
git-commitgraph/tests/fixtures/single_commit.sh+21−0
@@ -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
git-commitgraph/tests/fixtures/single_parent.sh+22−0
@@ -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
git-commitgraph/tests/fixtures/split_chain.sh+25−0
@@ -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
git-commitgraph/tests/fixtures/two_parents.sh+25−0
@@ -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
git-commitgraph/tests/fixtures/v1/bloom/info/commit-graph+0−0

Binary file not shown.

git-commitgraph/tests/fixtures/v1/bloom/info/packs+2−0
@@ -0,0 +1,2 @@
1+P pack-317d17c824d1a0b6991949219f7b6ba9ad51c3e6.pack
2+
git-commitgraph/tests/fixtures/v1/bloom/pack/pack-317d17c824d1a0b6991949219f7b6ba9ad51c3e6.idx+0−0

Binary file not shown.

git-commitgraph/tests/fixtures/v1/bloom/pack/pack-317d17c824d1a0b6991949219f7b6ba9ad51c3e6.pack+0−0

Binary file not shown.

git-commitgraph/tests/fixtures/v1/bloom_too_large/info/commit-graph+0−0

Binary file not shown.

git-commitgraph/tests/fixtures/v1/bloom_too_large/info/packs+2−0
@@ -0,0 +1,2 @@
1+P pack-b7a28066d2c1e9511d1a60349d0d6e3bf803d1b6.pack
2+
git-commitgraph/tests/fixtures/v1/bloom_too_large/pack/pack-b7a28066d2c1e9511d1a60349d0d6e3bf803d1b6.idx+0−0

Binary file not shown.

git-commitgraph/tests/fixtures/v1/bloom_too_large/pack/pack-b7a28066d2c1e9511d1a60349d0d6e3bf803d1b6.pack+0−0

Binary file not shown.

git-commitgraph/tests/fixtures/v1/octopus_merges/info/commit-graph+0−0

Binary file not shown.

git-commitgraph/tests/fixtures/v1/octopus_merges/info/packs+2−0
@@ -0,0 +1,2 @@
1+P pack-976438ba1bde7d9c944d8681cbfad266feec7068.pack
2+
git-commitgraph/tests/fixtures/v1/octopus_merges/pack/pack-976438ba1bde7d9c944d8681cbfad266feec7068.idx+0−0

Binary file not shown.

git-commitgraph/tests/fixtures/v1/octopus_merges/pack/pack-976438ba1bde7d9c944d8681cbfad266feec7068.pack+0−0

Binary file not shown.

git-commitgraph/tests/fixtures/v1/single_commit/info/commit-graph+0−0

Binary file not shown.

git-commitgraph/tests/fixtures/v1/single_commit/info/packs+2−0
@@ -0,0 +1,2 @@
1+P pack-07779428eae7661b1471110dda1efc99e3e954e7.pack
2+
git-commitgraph/tests/fixtures/v1/single_commit/pack/pack-07779428eae7661b1471110dda1efc99e3e954e7.idx+0−0

Binary file not shown.

git-commitgraph/tests/fixtures/v1/single_commit/pack/pack-07779428eae7661b1471110dda1efc99e3e954e7.pack+0−0

Binary file not shown.

git-commitgraph/tests/fixtures/v1/single_parent/info/commit-graph+0−0

Binary file not shown.

git-commitgraph/tests/fixtures/v1/single_parent/info/packs+2−0
@@ -0,0 +1,2 @@
1+P pack-ba89c2e6239315806e37d189aebf4a582e96a6d5.pack
2+
git-commitgraph/tests/fixtures/v1/single_parent/pack/pack-ba89c2e6239315806e37d189aebf4a582e96a6d5.idx+0−0

Binary file not shown.

git-commitgraph/tests/fixtures/v1/single_parent/pack/pack-ba89c2e6239315806e37d189aebf4a582e96a6d5.pack+0−0

Binary file not shown.

git-commitgraph/tests/fixtures/v1/split_chain/info/commit-graphs/commit-graph-chain+3−0
@@ -0,0 +1,3 @@
1+ca9f15bcd6129d304e0f4d4c8c6545467dcc78e1
2+422d4c835a8a22a401b80f8a76f0fa0be550db1b
3+eda6e3e68ac22be2ae8191ed2c80113e7f0344c6
git-commitgraph/tests/fixtures/v1/split_chain/info/commit-graphs/graph-422d4c835a8a22a401b80f8a76f0fa0be550db1b.graph+0−0

Binary file not shown.

git-commitgraph/tests/fixtures/v1/split_chain/info/commit-graphs/graph-ca9f15bcd6129d304e0f4d4c8c6545467dcc78e1.graph+0−0

Binary file not shown.

git-commitgraph/tests/fixtures/v1/split_chain/info/commit-graphs/graph-eda6e3e68ac22be2ae8191ed2c80113e7f0344c6.graph+0−0

Binary file not shown.

git-commitgraph/tests/fixtures/v1/split_chain/info/packs+2−0
@@ -0,0 +1,2 @@
1+P pack-35e0a0e2cb2913c7990d1c6a6aa4747d9b3b5306.pack
2+
git-commitgraph/tests/fixtures/v1/split_chain/pack/pack-35e0a0e2cb2913c7990d1c6a6aa4747d9b3b5306.idx+0−0

Binary file not shown.

git-commitgraph/tests/fixtures/v1/split_chain/pack/pack-35e0a0e2cb2913c7990d1c6a6aa4747d9b3b5306.pack+0−0

Binary file not shown.

git-commitgraph/tests/fixtures/v1/two_parents/info/commit-graph+0−0

Binary file not shown.

git-commitgraph/tests/fixtures/v1/two_parents/info/packs+2−0
@@ -0,0 +1,2 @@
1+P pack-ca696e967b89b66c96f70bcb9d9e8bba42417bcc.pack
2+
git-commitgraph/tests/fixtures/v1/two_parents/pack/pack-ca696e967b89b66c96f70bcb9d9e8bba42417bcc.idx+0−0

Binary file not shown.

git-commitgraph/tests/fixtures/v1/two_parents/pack/pack-ca696e967b89b66c96f70bcb9d9e8bba42417bcc.pack+0−0

Binary file not shown.