Skip to main content

gix_commitgraph/file/
commit.rs

1//! Low-level operations on individual commits.
2use crate::{
3    File, Position,
4    file::{self, EXTENDED_EDGES_MASK, LAST_EXTENDED_EDGE_MASK, NO_PARENT},
5};
6use gix_error::{Result, message};
7use std::{
8    fmt::{Debug, Formatter},
9    slice::Iter,
10};
11
12/// A commit as stored in a [`File`].
13#[derive(Copy, Clone)]
14pub struct Commit<'a> {
15    file: &'a File,
16    pos: file::Position,
17    // We can parse the below fields lazily if needed.
18    commit_timestamp: u64,
19    generation: u32,
20    parent1: ParentEdge,
21    parent2: ParentEdge,
22    root_tree_id: &'a gix_hash::oid,
23}
24
25#[inline]
26fn read_u32(b: &[u8]) -> u32 {
27    u32::from_be_bytes(b.try_into().unwrap())
28}
29
30impl<'a> Commit<'a> {
31    pub(crate) fn new(file: &'a File, pos: file::Position) -> Self {
32        let bytes = file.commit_data_bytes(pos);
33        Commit {
34            file,
35            pos,
36            root_tree_id: gix_hash::oid::from_bytes_unchecked(&bytes[..file.hash_len]),
37            parent1: ParentEdge::from_raw(read_u32(&bytes[file.hash_len..][..4])),
38            parent2: ParentEdge::from_raw(read_u32(&bytes[file.hash_len + 4..][..4])),
39            // TODO: Add support for corrected commit date offset overflow.
40            //      See https://github.com/git/git/commit/e8b63005c48696a26f976f5f9b0ccaf1983e439d and
41            //          https://github.com/git/git/commit/f90fca638e99a031dce8e3aca72427b2f9b4bb38 for more details and hints at a test.
42            generation: read_u32(&bytes[file.hash_len + 8..][..4]) >> 2,
43            commit_timestamp: u64::from_be_bytes(bytes[file.hash_len + 8..][..8].try_into().unwrap())
44                & 0x0003_ffff_ffff,
45        }
46    }
47
48    /// Returns the committer timestamp of this commit.
49    ///
50    /// The value is the number of seconds since 1970-01-01 00:00:00 UTC.
51    pub fn committer_timestamp(&self) -> u64 {
52        self.commit_timestamp
53    }
54
55    /// Returns the generation number of this commit.
56    ///
57    /// Commits without parents have generation number 1. Commits with parents have a generation
58    /// number that is the max of their parents' generation numbers + 1.
59    pub fn generation(&self) -> u32 {
60        self.generation
61    }
62
63    /// Returns an iterator over the parent positions for lookup in the owning [Graph][crate::Graph].
64    ///
65    /// Out-of-range parents are reported as corruption, and iteration stops after the first error.
66    /// For standalone split files, only the format's maximum position can be checked until they are
67    /// assembled into a [`Graph`][crate::Graph].
68    pub fn iter_parents(self) -> Parents<'a> {
69        // I didn't find a combinator approach that a) was as strict as ParentIterator, b) supported
70        // fuse-after-first-error behavior, and b) was significantly shorter or more understandable
71        // than ParentIterator. So here we are.
72        Parents {
73            commit_data: self,
74            state: ParentIteratorState::First,
75        }
76    }
77
78    /// Returns the hash of this commit.
79    pub fn id(&self) -> &'a gix_hash::oid {
80        self.file.id_at(self.pos)
81    }
82
83    /// Returns the first parent of this commit.
84    pub fn parent1(&self) -> Result<Option<Position>> {
85        self.iter_parents().next().transpose()
86    }
87
88    /// Returns the position at which this commit is stored in the parent [File].
89    pub fn position(&self) -> file::Position {
90        self.pos
91    }
92
93    /// Return the hash of the tree this commit points to.
94    pub fn root_tree_id(&self) -> &gix_hash::oid {
95        self.root_tree_id
96    }
97}
98
99impl Debug for Commit<'_> {
100    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
101        write!(
102            f,
103            "Commit {{ id: {}, lex_pos: {}, generation: {}, root_tree_id: {}, parent1: {:?}, parent2: {:?} }}",
104            self.id(),
105            self.pos,
106            self.generation(),
107            self.root_tree_id(),
108            self.parent1,
109            self.parent2,
110        )
111    }
112}
113
114impl Eq for Commit<'_> {}
115
116impl PartialEq for Commit<'_> {
117    fn eq(&self, other: &Self) -> bool {
118        std::ptr::eq(self.file, other.file) && self.pos == other.pos
119    }
120}
121
122/// An iterator over parents of a [`Commit`].
123pub struct Parents<'a> {
124    commit_data: Commit<'a>,
125    state: ParentIteratorState<'a>,
126}
127
128impl Iterator for Parents<'_> {
129    type Item = Result<Position>;
130
131    fn next(&mut self) -> Option<Self::Item> {
132        let state = std::mem::replace(&mut self.state, ParentIteratorState::Exhausted);
133        let parent = match state {
134            ParentIteratorState::First => match self.commit_data.parent1 {
135                ParentEdge::None => match self.commit_data.parent2 {
136                    ParentEdge::None => None,
137                    _ => Some(Err(message!(
138                        "commit {} has a second parent but not a first parent",
139                        self.commit_data.id()
140                    )
141                    .corrupted_error())),
142                },
143                ParentEdge::GraphPosition(pos) => {
144                    self.state = ParentIteratorState::Second;
145                    Some(Ok(pos))
146                }
147                ParentEdge::ExtraEdgeIndex(_) => Some(Err(message!(
148                    "commit {}'s first parent is an extra edge index, which is invalid",
149                    self.commit_data.id(),
150                )
151                .corrupted_error())),
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 tail = usize::try_from(extra_edge_index)
159                            .ok()
160                            .and_then(|index| index.checked_mul(4))
161                            .and_then(|start_offset| extra_edges_list.get(start_offset..));
162                        if let Some(tail) = tail {
163                            self.state = ParentIteratorState::Extra(tail.as_chunks::<4>().0.iter());
164                            // This recursive call is what blocks me from replacing ParentIterator
165                            // with a std::iter::from_fn closure.
166                            self.next()
167                        } else {
168                            Some(Err(message!(
169                                "commit {}'s extra edges overflows the commit-graph file's extra edges list",
170                                self.commit_data.id()
171                            )
172                            .corrupted_error()))
173                        }
174                    } else {
175                        Some(Err(message!(
176                            "commit {} has extra edges, but commit-graph file has no extra edges list",
177                            self.commit_data.id()
178                        )
179                        .corrupted_error()))
180                    }
181                }
182            },
183            ParentIteratorState::Extra(mut chunks) => {
184                if let Some(chunk) = chunks.next() {
185                    let extra_edge = read_u32(chunk);
186                    match ExtraEdge::from_raw(extra_edge) {
187                        ExtraEdge::Internal(pos) => {
188                            self.state = ParentIteratorState::Extra(chunks);
189                            Some(Ok(pos))
190                        }
191                        ExtraEdge::Last(pos) => Some(Ok(pos)),
192                    }
193                } else {
194                    Some(Err(message!(
195                        "commit {}'s extra edges overflows the commit-graph file's extra edges list",
196                        self.commit_data.id()
197                    )
198                    .corrupted_error()))
199                }
200            }
201            ParentIteratorState::Exhausted => None,
202        };
203        if let Some(Ok(pos)) = &parent
204            && pos.0 >= self.commit_data.file.parent_position_limit
205        {
206            self.state = ParentIteratorState::Exhausted;
207            return Some(Err(message!(
208                "Commit {} has parent position {pos} that is out of range (should be less than {})",
209                self.commit_data.id(),
210                self.commit_data.file.parent_position_limit
211            )
212            .corrupted_error()));
213        }
214        parent
215    }
216
217    fn size_hint(&self) -> (usize, Option<usize>) {
218        match (&self.state, self.commit_data.parent1, self.commit_data.parent2) {
219            (ParentIteratorState::First, ParentEdge::None, ParentEdge::None) => (0, Some(0)),
220            (ParentIteratorState::First, ParentEdge::None, _) => (1, Some(1)),
221            (ParentIteratorState::First, ParentEdge::GraphPosition(_), ParentEdge::None) => (1, Some(1)),
222            (ParentIteratorState::First, ParentEdge::GraphPosition(_), ParentEdge::GraphPosition(_)) => (1, Some(2)),
223            (ParentIteratorState::First, ParentEdge::GraphPosition(_), ParentEdge::ExtraEdgeIndex(_)) => (1, None),
224            (ParentIteratorState::First, ParentEdge::ExtraEdgeIndex(_), _) => (1, Some(1)),
225            (ParentIteratorState::Second, _, ParentEdge::None) => (0, Some(0)),
226            (ParentIteratorState::Second, _, ParentEdge::GraphPosition(_)) => (1, Some(1)),
227            (ParentIteratorState::Second, _, ParentEdge::ExtraEdgeIndex(_)) => (1, None),
228            (ParentIteratorState::Extra(_), _, _) => (1, None),
229            (ParentIteratorState::Exhausted, _, _) => (0, Some(0)),
230        }
231    }
232}
233
234#[derive(Debug)]
235enum ParentIteratorState<'a> {
236    /// Read the first inline parent edge, or finish if both inline edges indicate no parents.
237    First,
238    /// Read the second inline parent edge, which may point to a parent or into the EDGE chunk.
239    Second,
240    /// Read remaining big-endian parent entries from the EDGE chunk until the last-edge marker.
241    /// Reaching the end of the slice without that marker is corruption.
242    Extra(Iter<'a, [u8; 4]>),
243    /// Yield no further items after the last parent, the absence of a parent, or an error.
244    Exhausted,
245}
246
247#[derive(Clone, Copy, Debug)]
248enum ParentEdge {
249    None,
250    GraphPosition(Position),
251    ExtraEdgeIndex(u32),
252}
253
254impl ParentEdge {
255    pub fn from_raw(raw: u32) -> ParentEdge {
256        if raw == NO_PARENT {
257            return ParentEdge::None;
258        }
259        if raw & EXTENDED_EDGES_MASK != 0 {
260            ParentEdge::ExtraEdgeIndex(raw & !EXTENDED_EDGES_MASK)
261        } else {
262            ParentEdge::GraphPosition(Position(raw))
263        }
264    }
265}
266
267enum ExtraEdge {
268    Internal(Position),
269    Last(Position),
270}
271
272impl ExtraEdge {
273    pub fn from_raw(raw: u32) -> Self {
274        if raw & LAST_EXTENDED_EDGE_MASK != 0 {
275            Self::Last(Position(raw & !LAST_EXTENDED_EDGE_MASK))
276        } else {
277            Self::Internal(Position(raw))
278        }
279    }
280}