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/src (8 of 53 files). Show all

Unified · Split

8 files
  1. git-commitgraph/src/graph/access.rs +83 −0
  2. git-commitgraph/src/graph/init.rs +104 −0
  3. git-commitgraph/src/graph/mod.rs +23 −0
  4. git-commitgraph/src/graph_file/access.rs +128 −0
  5. git-commitgraph/src/graph_file/commit_data.rs +255 −0
  6. git-commitgraph/src/graph_file/init.rs +312 −0
  7. git-commitgraph/src/graph_file/mod.rs +53 −0
  8. git-commitgraph/src/lib.rs +9 −0
git-commitgraph/src/graph/access.rs+83−0
@@ -0,0 +1,83 @@
1use crate::graph::GraphPosition;
2use crate::graph_file::LexPosition;
3use crate::{CommitData, Graph, GraphFile};
4use git_object::borrowed;
5
6impl 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
41impl 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
74struct LookupByIdResult<'a> {
75 pub file: &'a GraphFile,
76 pub graph_pos: GraphPosition,
77 pub lex_pos: LexPosition,
78}
79
80struct 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 @@
1use crate::{graph_file, Graph, GraphFile, MAX_COMMITS};
2use git_object::HashKind;
3use quick_error::quick_error;
4use std::io::{BufRead, BufReader};
5use std::path::{Path, PathBuf};
6
7quick_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
50impl 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 @@
1mod access;
2mod init;
3
4use crate::GraphFile;
5use 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.
12pub struct Graph {
13 files: Vec<GraphFile>,
14}
15
16#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
17pub struct GraphPosition(pub u32);
18
19impl 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 @@
1use crate::graph_file::{Kind, LexPosition, COMMIT_DATA_ENTRY_SIZE};
2use crate::{CommitData, GraphFile};
3use git_object::{borrowed, HashKind, SHA1_SIZE};
4use std::convert::{TryFrom, TryInto};
5use std::fmt::{Debug, Formatter};
6use std::path::Path;
7
8impl 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
101impl 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
124impl 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 @@
1use crate::graph::GraphPosition;
2use crate::graph_file::LexPosition;
3use crate::GraphFile;
4use byteorder::{BigEndian, ByteOrder};
5use git_object::{borrowed, owned, SHA1_SIZE};
6use quick_error::quick_error;
7use std::convert::{TryFrom, TryInto};
8use std::fmt::{Debug, Formatter};
9use std::slice::Chunks;
10
11quick_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.
40const NO_PARENT: u32 = 0x7000_0000;
41const EXTENDED_EDGES_MASK: u32 = 0x8000_0000;
42
43pub 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
54impl<'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
106impl<'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
121impl<'a> Eq for CommitData<'a> {}
122
123impl<'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
129pub struct ParentIterator<'a> {
130 commit_data: &'a CommitData<'a>,
131 state: ParentIteratorState<'a>,
132}
133
134impl<'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)]
213enum ParentIteratorState<'a> {
214 First,
215 Second,
216 Extra(Chunks<'a, u8>),
217 Exhausted,
218}
219
220#[derive(Clone, Copy, Debug)]
221enum ParentEdge {
222 None,
223 GraphPosition(GraphPosition),
224 ExtraEdgeIndex(u32),
225}
226
227impl 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
240const LAST_EXTENDED_EDGE_MASK: u32 = 0x8000_0000;
241
242enum ExtraEdge {
243 Internal(GraphPosition),
244 Last(GraphPosition),
245}
246
247impl 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 @@
1use crate::graph_file::{GraphFile, COMMIT_DATA_ENTRY_SIZE, FAN_LEN, SIGNATURE};
2use bstr::ByteSlice;
3use byteorder::{BigEndian, ByteOrder};
4use filebuffer::FileBuffer;
5use git_object::SHA1_SIZE;
6use quick_error::quick_error;
7use std::convert::{TryFrom, TryInto};
8use std::ops::Range;
9use std::path::Path;
10
11type ChunkId = [u8; 4];
12
13quick_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
62const CHUNK_LOOKUP_SIZE: usize = 12;
63const HEADER_LEN: usize = 8;
64const MIN_FILE_SIZE: usize = HEADER_LEN + ((MIN_CHUNKS + 1) * CHUNK_LOOKUP_SIZE);
65const OID_LOOKUP_ENTRY_SIZE: usize = SHA1_SIZE;
66
67// Required chunks: OIDF, OIDL, CDAT
68const MIN_CHUNKS: usize = 3;
69const BASE_GRAPHS_LIST_CHUNK_ID: ChunkId = *b"BASE";
70const COMMIT_DATA_CHUNK_ID: ChunkId = *b"CDAT";
71const EXTENDED_EDGES_LIST_CHUNK_ID: ChunkId = *b"EDGE";
72const OID_FAN_CHUNK_ID: ChunkId = *b"OIDF";
73const OID_LOOKUP_CHUNK_ID: ChunkId = *b"OIDL";
74const SENTINEL_CHUNK_ID: ChunkId = [0u8; 4];
75
76impl GraphFile {
77 pub fn at(path: impl AsRef<Path>) -> Result<GraphFile, Error> {
78 Self::try_from(path.as_ref())
79 }
80}
81
82impl 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
306fn 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 @@
1use filebuffer::FileBuffer;
2use git_object::SHA1_SIZE;
3use std::ops::Range;
4use std::path::PathBuf;
5
6mod access;
7pub mod commit_data;
8mod init;
9
10pub use init::Error;
11use std::fmt::{Display, Formatter};
12
13const COMMIT_DATA_ENTRY_SIZE: usize = SHA1_SIZE + 16;
14const FAN_LEN: usize = 256;
15const 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`.
21pub 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)]
41pub struct LexPosition(pub u32);
42
43impl 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)]
51pub enum Kind {
52 V1,
53}
git-commitgraph/src/lib.rs+9−0
@@ -1 +1,10 @@
1#![forbid(unsafe_code)]1#![forbid(unsafe_code)]
2
3pub mod graph;
4pub mod graph_file;
5
6pub use graph::{Graph, GraphPosition};
7pub use graph_file::commit_data::CommitData;
8pub use graph_file::{GraphFile, LexPosition};
9
10pub const MAX_COMMITS: u32 = (1 << 30) + (1 << 29) + (1 << 28) - 1;