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/graph_file/commit_data.rs (1 of 53 files). Show all

Unified · Split

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+}