1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
use gix_date::SecondsSinceUnixEpoch;
use smallvec::SmallVec;

use super::LazyCommit;
use crate::graph::{Commit, Either, Generation};

impl<'graph> LazyCommit<'graph> {
    /// Return an iterator over the parents of this commit.
    pub fn iter_parents(&self) -> Parents<'graph> {
        let backing = match &self.backing {
            Either::Left(buf) => Either::Left(gix_object::CommitRefIter::from_bytes(buf)),
            Either::Right((cache, pos)) => Either::Right((*cache, cache.commit_at(*pos).iter_parents())),
        };
        Parents { backing }
    }

    /// Returns the timestamp at which this commit was created.
    ///
    /// This is the single-most important date for determining recency of commits.
    /// Note that this can only fail if the commit is backed by the object database *and* parsing fails.
    pub fn committer_timestamp(&self) -> Result<SecondsSinceUnixEpoch, gix_object::decode::Error> {
        Ok(match &self.backing {
            Either::Left(buf) => gix_object::CommitRefIter::from_bytes(buf).committer()?.time.seconds,
            Either::Right((cache, pos)) => cache.commit_at(*pos).committer_timestamp() as SecondsSinceUnixEpoch, // a cast as we cannot represent the error and trying seems overkill
        })
    }

    /// Returns the generation of the commit if it is backed by a commit graph.
    pub fn generation(&self) -> Option<Generation> {
        match &self.backing {
            Either::Left(_) => None,
            Either::Right((cache, pos)) => cache.commit_at(*pos).generation().into(),
        }
    }

    /// Convert ourselves into an owned version, which effectively detaches us from the underlying graph.
    /// Use `new_data()` to provide the `data` field for the owned `Commit`.
    pub fn to_owned<T>(&self, new_data: impl FnOnce() -> T) -> Result<Commit<T>, to_owned::Error> {
        let data = new_data();
        Ok(match &self.backing {
            Either::Left(buf) => {
                use gix_object::commit::ref_iter::Token;
                let iter = gix_object::CommitRefIter::from_bytes(buf);
                let mut parents = SmallVec::default();
                let mut timestamp = None;
                for token in iter {
                    match token? {
                        Token::Tree { .. } => {}
                        Token::Parent { id } => parents.push(id),
                        Token::Author { .. } => {}
                        Token::Committer { signature } => {
                            timestamp = Some(signature.time.seconds);
                            break;
                        }
                        _ => {
                            unreachable!(
                                "we break naturally after seeing the committer which is always at the same spot"
                            )
                        }
                    }
                }
                Commit {
                    parents,
                    commit_time: timestamp.unwrap_or_default(),
                    generation: None,
                    data,
                }
            }
            Either::Right((cache, pos)) => {
                let mut parents = SmallVec::default();
                let commit = cache.commit_at(*pos);
                for pos in commit.iter_parents() {
                    let pos = pos?;
                    parents.push(cache.commit_at(pos).id().to_owned());
                }
                Commit {
                    parents,
                    commit_time: commit.committer_timestamp().try_into().map_err(|_| {
                        to_owned::Error::CommitGraphTime {
                            actual: commit.committer_timestamp(),
                        }
                    })?,
                    generation: Some(commit.generation()),
                    data,
                }
            }
        })
    }
}

/// An iterator over the parents of a commit.
pub struct Parents<'graph> {
    backing: Either<
        gix_object::CommitRefIter<'graph>,
        (
            &'graph gix_commitgraph::Graph,
            gix_commitgraph::file::commit::Parents<'graph>,
        ),
    >,
}

impl<'graph> Iterator for Parents<'graph> {
    type Item = Result<gix_hash::ObjectId, iter_parents::Error>;

    fn next(&mut self) -> Option<Self::Item> {
        match &mut self.backing {
            Either::Left(it) => {
                for token in it {
                    match token {
                        Ok(gix_object::commit::ref_iter::Token::Tree { .. }) => continue,
                        Ok(gix_object::commit::ref_iter::Token::Parent { id }) => return Some(Ok(id)),
                        Ok(_unused_token) => break,
                        Err(err) => return Some(Err(err.into())),
                    }
                }
                None
            }
            Either::Right((cache, it)) => it
                .next()
                .map(|r| r.map(|pos| cache.id_at(pos).to_owned()).map_err(Into::into)),
        }
    }
}

///
#[allow(clippy::empty_docs)]
pub mod iter_parents {
    /// The error returned by the [`Parents`][super::Parents] iterator.
    #[derive(Debug, thiserror::Error)]
    #[allow(missing_docs)]
    pub enum Error {
        #[error("An error occurred when parsing commit parents")]
        DecodeCommit(#[from] gix_object::decode::Error),
        #[error("An error occurred when parsing parents from the commit graph")]
        DecodeCommitGraph(#[from] gix_commitgraph::file::commit::Error),
    }
}

///
#[allow(clippy::empty_docs)]
pub mod to_owned {
    /// The error returned by [`to_owned()`][crate::graph::LazyCommit::to_owned()].
    #[derive(Debug, thiserror::Error)]
    #[allow(missing_docs)]
    pub enum Error {
        #[error("A commit could not be decoded during traversal")]
        Decode(#[from] gix_object::decode::Error),
        #[error("Could not find commit position in graph when traversing parents")]
        CommitGraphParent(#[from] gix_commitgraph::file::commit::Error),
        #[error("Commit-graph time could not be presented as signed integer: {actual}")]
        CommitGraphTime { actual: u64 },
    }
}