git_bug/entities/issue/snapshot/
mod.rs

1// git-bug-rs - A rust library for interfacing with git-bug repositories
2//
3// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
4// SPDX-License-Identifier: GPL-3.0-or-later
5//
6// This file is part of git-bug-rs/git-gub.
7//
8// You should have received a copy of the License along with this program.
9// If not, see <https://www.gnu.org/licenses/agpl.txt>.
10
11//! Issue specific extension to the generic Snapshot type.
12
13use std::collections::HashSet;
14
15use history_step::IssueHistoryStep;
16use log::warn;
17
18use super::{
19    Issue,
20    data::{comment::Comment, label::Label, status::Status},
21};
22use crate::replica::entity::{
23    id::combined_id::CombinedId,
24    identity::IdentityStub,
25    snapshot::{
26        Snapshot,
27        timeline::{Timeline, history_step::HistoryStep},
28    },
29};
30
31pub mod history_step;
32pub mod timeline;
33
34impl Snapshot<Issue> {
35    /// Returns the Body of the Issue at the time of this snapshot.
36    #[must_use]
37    // Only expects.
38    #[allow(clippy::missing_panics_doc)]
39    pub fn body(&self) -> &str {
40        &self
41            .timeline()
42            .body_history()
43            .last()
44            .expect("This is mandated by the crate op")
45            .message
46    }
47
48    /// Returns the [`Comments`][`Comment`] at the time of this snapshot.
49    #[must_use]
50    // Only expects.
51    #[allow(clippy::missing_panics_doc)]
52    pub fn comments(&self) -> Vec<Comment> {
53        let mut output = Vec::with_capacity(self.timeline().comments().count());
54
55        for comment in self.timeline().comments() {
56            let first = comment.history.first();
57
58            let last = comment.history.last();
59
60            output.push(Comment {
61                combined_id: CombinedId {
62                    primary_id: self.id().as_id(),
63                    secondary_id: comment.id.as_id(),
64                },
65                author: first.author,
66                message: last.message.clone(),
67                files: last.files.clone(),
68                timestamp: first.at,
69            });
70        }
71
72        output
73    }
74
75    /// Returns the active [`Label`][`Label`] at the time of this
76    /// snapshot.
77    #[must_use]
78    pub fn labels(&self) -> HashSet<&Label> {
79        let mut labels = HashSet::new();
80
81        for label in self.timeline().labels_history() {
82            for removed in &label.removed {
83                if !labels.remove(removed) {
84                    warn!("Label {removed} was removed, but was never added.");
85                }
86            }
87            for added in &label.added {
88                if !labels.insert(added) {
89                    warn!("Label {added} was added, but was already in the set of labels.");
90                }
91            }
92        }
93
94        labels
95    }
96
97    /// Returns the active [`Status`]  at the time of this snapshot.
98    #[must_use]
99    pub fn status(&self) -> Status {
100        if let Some(last) = self.timeline().status_history().last() {
101            last.status
102        } else {
103            // FIXME(@bpeetz): I'm not a fan of this implicit default. But git-bug does this
104            // and we need to do it too, if we want to stay compatible.
105            // <2025-04-19>
106            Status::Open
107        }
108    }
109
110    /// Returns the Issue's title at the time of this snapshot.
111    #[must_use]
112    // Only expects.
113    #[allow(clippy::missing_panics_doc)]
114    pub fn title(&self) -> &str {
115        let last = self
116            .timeline()
117            .title_history()
118            .last()
119            .expect("This is mandated by the create op");
120        &last.title
121    }
122
123    /// Return the [`author`][`IdentityStub`] of this [`Issue`][`super::Issue`]
124    ///
125    /// This is the same as the [`author`][`IdentityStub`] of the
126    /// [`Issue`][`super::Issue`], which was used to create this snapshot.
127    #[must_use]
128    // Only expects
129    #[allow(clippy::missing_panics_doc)]
130    pub fn author(&self) -> IdentityStub {
131        self.timeline()
132            .body_history()
133            .next()
134            .expect("A body item must exist")
135            .author
136    }
137
138    /// Get an iterator over all the people that actively participated on this
139    /// issue.
140    pub fn participants(&self) -> impl Iterator<Item = IdentityStub> + use<'_> {
141        // TODO(@bpeetz): This should de-duplicate. <2025-04-20>
142        self.timeline()
143            .history()
144            .iter()
145            .filter(|item| matches!(item, IssueHistoryStep::Body(_)))
146            .map(HistoryStep::author)
147    }
148}