gix_commitgraph/file/
commit.rs

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