git_commitgraph/file/
commit.rs

1//! Low-level operations on individual commits.
2use std::{
3    convert::TryInto,
4    fmt::{Debug, Formatter},
5    slice::Chunks,
6};
7
8use crate::{
9    file::{self, File, EXTENDED_EDGES_MASK, LAST_EXTENDED_EDGE_MASK, NO_PARENT},
10    graph,
11};
12
13/// The error used in the [`file::commit`][self] module.
14#[derive(thiserror::Error, Debug)]
15#[allow(missing_docs)]
16pub enum Error {
17    #[error("commit {0}'s extra edges overflows the commit-graph file's extra edges list")]
18    ExtraEdgesListOverflow(git_hash::ObjectId),
19    #[error("commit {0}'s first parent is an extra edge index, which is invalid")]
20    FirstParentIsExtraEdgeIndex(git_hash::ObjectId),
21    #[error("commit {0} has extra edges, but commit-graph file has no extra edges list")]
22    MissingExtraEdgesList(git_hash::ObjectId),
23    #[error("commit {0} has a second parent but not a first parent")]
24    SecondParentWithoutFirstParent(git_hash::ObjectId),
25}
26
27/// A commit as stored in a [`File`].
28pub struct Commit<'a> {
29    file: &'a File,
30    pos: file::Position,
31    // We can parse the below fields lazily if needed.
32    commit_timestamp: u64,
33    generation: u32,
34    parent1: ParentEdge,
35    parent2: ParentEdge,
36    root_tree_id: &'a git_hash::oid,
37}
38
39#[inline]
40fn read_u32(b: &[u8]) -> u32 {
41    u32::from_be_bytes(b.try_into().unwrap())
42}
43
44impl<'a> Commit<'a> {
45    pub(crate) fn new(file: &'a File, pos: file::Position) -> Self {
46        let bytes = file.commit_data_bytes(pos);
47        Commit {
48            file,
49            pos,
50            root_tree_id: git_hash::oid::from_bytes_unchecked(&bytes[..file.hash_len]),
51            parent1: ParentEdge::from_raw(read_u32(&bytes[file.hash_len..][..4])),
52            parent2: ParentEdge::from_raw(read_u32(&bytes[file.hash_len + 4..][..4])),
53            generation: read_u32(&bytes[file.hash_len + 8..][..4]) >> 2,
54            commit_timestamp: u64::from_be_bytes(bytes[file.hash_len + 8..][..8].try_into().unwrap())
55                & 0x0003_ffff_ffff,
56        }
57    }
58
59    /// Returns the committer timestamp of this commit.
60    ///
61    /// The value is the number of seconds since 1970-01-01 00:00:00 UTC.
62    pub fn committer_timestamp(&self) -> u64 {
63        self.commit_timestamp
64    }
65
66    /// Returns the generation number of this commit.
67    ///
68    /// Commits without parents have generation number 1. Commits with parents have a generation
69    /// number that is the max of their parents' generation numbers + 1.
70    pub fn generation(&self) -> u32 {
71        self.generation
72    }
73
74    /// Returns an iterator over the parent positions for lookup in the owning [Graph][crate::Graph].
75    pub fn iter_parents(&'a self) -> impl Iterator<Item = Result<graph::Position, Error>> + 'a {
76        // I didn't find a combinator approach that a) was as strict as ParentIterator, b) supported
77        // fuse-after-first-error behavior, and b) was significantly shorter or more understandable
78        // than ParentIterator. So here we are.
79        ParentIterator {
80            commit_data: self,
81            state: ParentIteratorState::First,
82        }
83    }
84
85    /// Returns the hash of this commit.
86    pub fn id(&self) -> &'a git_hash::oid {
87        self.file.id_at(self.pos)
88    }
89
90    /// Returns the first parent of this commit.
91    pub fn parent1(&self) -> Result<Option<graph::Position>, Error> {
92        self.iter_parents().next().transpose()
93    }
94
95    /// Returns the position at which this commit is stored in the parent [File].
96    pub fn position(&self) -> file::Position {
97        self.pos
98    }
99
100    /// Return the hash of the tree this commit points to.
101    pub fn root_tree_id(&self) -> &git_hash::oid {
102        self.root_tree_id
103    }
104}
105
106impl<'a> Debug for Commit<'a> {
107    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
108        write!(
109            f,
110            "Commit {{ id: {}, lex_pos: {}, generation: {}, root_tree_id: {}, parent1: {:?}, parent2: {:?} }}",
111            self.id(),
112            self.pos,
113            self.generation(),
114            self.root_tree_id(),
115            self.parent1,
116            self.parent2,
117        )
118    }
119}
120
121impl<'a> Eq for Commit<'a> {}
122
123impl<'a> PartialEq for Commit<'a> {
124    fn eq(&self, other: &Self) -> bool {
125        std::ptr::eq(self.file, other.file) && self.pos == other.pos
126    }
127}
128
129/// An iterator over parents of a [`Commit`].
130pub struct ParentIterator<'a> {
131    commit_data: &'a Commit<'a>,
132    state: ParentIteratorState<'a>,
133}
134
135impl<'a> Iterator for ParentIterator<'a> {
136    type Item = Result<graph::Position, Error>;
137
138    fn next(&mut self) -> Option<Self::Item> {
139        let state = std::mem::replace(&mut self.state, ParentIteratorState::Exhausted);
140        match state {
141            ParentIteratorState::First => match self.commit_data.parent1 {
142                ParentEdge::None => match self.commit_data.parent2 {
143                    ParentEdge::None => None,
144                    _ => Some(Err(Error::SecondParentWithoutFirstParent(self.commit_data.id().into()))),
145                },
146                ParentEdge::GraphPosition(pos) => {
147                    self.state = ParentIteratorState::Second;
148                    Some(Ok(pos))
149                }
150                ParentEdge::ExtraEdgeIndex(_) => {
151                    Some(Err(Error::FirstParentIsExtraEdgeIndex(self.commit_data.id().into())))
152                }
153            },
154            ParentIteratorState::Second => match self.commit_data.parent2 {
155                ParentEdge::None => None,
156                ParentEdge::GraphPosition(pos) => Some(Ok(pos)),
157                ParentEdge::ExtraEdgeIndex(extra_edge_index) => {
158                    if let Some(extra_edges_list) = self.commit_data.file.extra_edges_data() {
159                        let start_offset: usize = extra_edge_index
160                            .try_into()
161                            .expect("an architecture able to hold 32 bits of integer");
162                        let start_offset = start_offset
163                            .checked_mul(4)
164                            .expect("an extended edge index small enough to fit in usize");
165                        if let Some(tail) = extra_edges_list.get(start_offset..) {
166                            self.state = ParentIteratorState::Extra(tail.chunks(4));
167                            // This recursive call is what blocks me from replacing ParentIterator
168                            // with a std::iter::from_fn closure.
169                            self.next()
170                        } else {
171                            Some(Err(Error::ExtraEdgesListOverflow(self.commit_data.id().into())))
172                        }
173                    } else {
174                        Some(Err(Error::MissingExtraEdgesList(self.commit_data.id().into())))
175                    }
176                }
177            },
178            ParentIteratorState::Extra(mut chunks) => {
179                if let Some(chunk) = chunks.next() {
180                    let extra_edge = read_u32(chunk);
181                    match ExtraEdge::from_raw(extra_edge) {
182                        ExtraEdge::Internal(pos) => {
183                            self.state = ParentIteratorState::Extra(chunks);
184                            Some(Ok(pos))
185                        }
186                        ExtraEdge::Last(pos) => Some(Ok(pos)),
187                    }
188                } else {
189                    Some(Err(Error::ExtraEdgesListOverflow(self.commit_data.id().into())))
190                }
191            }
192            ParentIteratorState::Exhausted => None,
193        }
194    }
195
196    fn size_hint(&self) -> (usize, Option<usize>) {
197        match (&self.state, self.commit_data.parent1, self.commit_data.parent2) {
198            (ParentIteratorState::First, ParentEdge::None, ParentEdge::None) => (0, Some(0)),
199            (ParentIteratorState::First, ParentEdge::None, _) => (1, Some(1)),
200            (ParentIteratorState::First, ParentEdge::GraphPosition(_), ParentEdge::None) => (1, Some(1)),
201            (ParentIteratorState::First, ParentEdge::GraphPosition(_), ParentEdge::GraphPosition(_)) => (2, Some(2)),
202            (ParentIteratorState::First, ParentEdge::GraphPosition(_), ParentEdge::ExtraEdgeIndex(_)) => (3, None),
203            (ParentIteratorState::First, ParentEdge::ExtraEdgeIndex(_), _) => (1, Some(1)),
204            (ParentIteratorState::Second, _, ParentEdge::None) => (0, Some(0)),
205            (ParentIteratorState::Second, _, ParentEdge::GraphPosition(_)) => (1, Some(1)),
206            (ParentIteratorState::Second, _, ParentEdge::ExtraEdgeIndex(_)) => (2, None),
207            (ParentIteratorState::Extra(_), _, _) => (1, None),
208            (ParentIteratorState::Exhausted, _, _) => (0, Some(0)),
209        }
210    }
211}
212
213#[derive(Debug)]
214enum ParentIteratorState<'a> {
215    First,
216    Second,
217    Extra(Chunks<'a, u8>),
218    Exhausted,
219}
220
221#[derive(Clone, Copy, Debug)]
222enum ParentEdge {
223    None,
224    GraphPosition(graph::Position),
225    ExtraEdgeIndex(u32),
226}
227
228impl ParentEdge {
229    pub fn from_raw(raw: u32) -> ParentEdge {
230        if raw == NO_PARENT {
231            return ParentEdge::None;
232        }
233        if raw & EXTENDED_EDGES_MASK != 0 {
234            ParentEdge::ExtraEdgeIndex(raw & !EXTENDED_EDGES_MASK)
235        } else {
236            ParentEdge::GraphPosition(graph::Position(raw))
237        }
238    }
239}
240
241enum ExtraEdge {
242    Internal(graph::Position),
243    Last(graph::Position),
244}
245
246impl ExtraEdge {
247    pub fn from_raw(raw: u32) -> Self {
248        if raw & LAST_EXTENDED_EDGE_MASK != 0 {
249            Self::Last(graph::Position(raw & !LAST_EXTENDED_EDGE_MASK))
250        } else {
251            Self::Internal(graph::Position(raw))
252        }
253    }
254}