git-bug 0.2.4

A rust library for interfacing with git-bug repositories
Documentation
// git-bug-rs - A rust library for interfacing with git-bug repositories
//
// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This file is part of git-bug-rs/git-gub.
//
// You should have received a copy of the License along with this program.
// If not, see <https://www.gnu.org/licenses/agpl.txt>.

//! Issue specific extension to the generic Snapshot type.

use std::collections::HashSet;

use history_step::IssueHistoryStep;
use log::warn;

use super::{
    Issue,
    data::{comment::Comment, label::Label, status::Status},
};
use crate::replica::entity::{
    id::combined_id::CombinedId,
    identity::IdentityStub,
    snapshot::{
        Snapshot,
        timeline::{Timeline, history_step::HistoryStep},
    },
};

pub mod history_step;
pub mod timeline;

impl Snapshot<Issue> {
    /// Returns the Body of the Issue at the time of this snapshot.
    #[must_use]
    // Only expects.
    #[allow(clippy::missing_panics_doc)]
    pub fn body(&self) -> &str {
        &self
            .timeline()
            .body_history()
            .last()
            .expect("This is mandated by the crate op")
            .message
    }

    /// Returns the [`Comments`][`Comment`] at the time of this snapshot.
    #[must_use]
    // Only expects.
    #[allow(clippy::missing_panics_doc)]
    pub fn comments(&self) -> Vec<Comment> {
        let mut output = Vec::with_capacity(self.timeline().comments().count());

        for comment in self.timeline().comments() {
            let first = comment.history.first();

            let last = comment.history.last();

            output.push(Comment {
                combined_id: CombinedId {
                    primary_id: self.id().as_id(),
                    secondary_id: comment.id.as_id(),
                },
                author: first.author,
                message: last.message.clone(),
                files: last.files.clone(),
                timestamp: first.at,
            });
        }

        output
    }

    /// Returns the active [`Label`][`Label`] at the time of this
    /// snapshot.
    #[must_use]
    pub fn labels(&self) -> HashSet<&Label> {
        let mut labels = HashSet::new();

        for label in self.timeline().labels_history() {
            for removed in &label.removed {
                if !labels.remove(removed) {
                    warn!("Label {removed} was removed, but was never added.");
                }
            }
            for added in &label.added {
                if !labels.insert(added) {
                    warn!("Label {added} was added, but was already in the set of labels.");
                }
            }
        }

        labels
    }

    /// Returns the active [`Status`]  at the time of this snapshot.
    #[must_use]
    pub fn status(&self) -> Status {
        if let Some(last) = self.timeline().status_history().last() {
            last.status
        } else {
            // FIXME(@bpeetz): I'm not a fan of this implicit default. But git-bug does this
            // and we need to do it too, if we want to stay compatible.
            // <2025-04-19>
            Status::Open
        }
    }

    /// Returns the Issue's title at the time of this snapshot.
    #[must_use]
    // Only expects.
    #[allow(clippy::missing_panics_doc)]
    pub fn title(&self) -> &str {
        let last = self
            .timeline()
            .title_history()
            .last()
            .expect("This is mandated by the create op");
        &last.title
    }

    /// Return the [`author`][`IdentityStub`] of this [`Issue`][`super::Issue`]
    ///
    /// This is the same as the [`author`][`IdentityStub`] of the
    /// [`Issue`][`super::Issue`], which was used to create this snapshot.
    #[must_use]
    // Only expects
    #[allow(clippy::missing_panics_doc)]
    pub fn author(&self) -> IdentityStub {
        self.timeline()
            .body_history()
            .next()
            .expect("A body item must exist")
            .author
    }

    /// Get an iterator over all the people that actively participated on this
    /// issue.
    pub fn participants(&self) -> impl Iterator<Item = IdentityStub> + use<'_> {
        // TODO(@bpeetz): This should de-duplicate. <2025-04-20>
        self.timeline()
            .history()
            .iter()
            .filter(|item| matches!(item, IssueHistoryStep::Body(_)))
            .map(HistoryStep::author)
    }
}