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