jilu/changelog/
release.rs

1use crate::changelog::ChangeSet;
2use crate::git::Tag;
3use crate::Error;
4use chrono::{offset::Utc, DateTime};
5use semver::Version;
6use serde::ser::{SerializeStruct, Serializer};
7use serde::Serialize;
8
9#[derive(Debug)]
10pub struct Release<'a> {
11    /// The version of the release.
12    version: Version,
13
14    /// Internal reference to the Git tag of this release.
15    tag: Tag,
16
17    /// Internal reference to the change set of this release.
18    changeset: ChangeSet<'a>,
19}
20
21impl Serialize for Release<'_> {
22    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
23    where
24        S: Serializer,
25    {
26        let mut state = serializer.serialize_struct("Release", 5)?;
27        state.serialize_field("version", &self.version())?;
28        if let Some(subject) = self.subject() {
29            state.serialize_field("subject", &subject)?;
30        }
31        if let Some(notes) = self.notes() {
32            state.serialize_field("notes", &notes)?;
33        }
34        state.serialize_field("date", &self.date())?;
35        state.serialize_field("changeset", &self.changeset())?;
36        state.end()
37    }
38}
39
40impl<'a> Release<'a> {
41    pub(crate) fn new(tag: Tag) -> Result<Self, Error> {
42        let version = if tag.name.starts_with('v') {
43            &tag.name[1..]
44        } else {
45            &tag.name
46        };
47
48        let version = Version::parse(version)?;
49
50        Ok(Self {
51            version,
52            tag,
53            changeset: ChangeSet::default(),
54        })
55    }
56
57    /// Add a set of changes to the release.
58    pub(crate) fn with_changeset(&mut self, changeset: ChangeSet<'a>) {
59        self.changeset = changeset;
60    }
61
62    /// The SemVer version of the release.
63    pub fn version(&self) -> &Version {
64        &self.version
65    }
66
67    /// The subject of the release.
68    ///
69    /// This is similar to the _first line_ of the Git tag annotated message.
70    ///
71    /// Note that a tag with multiple lines *MUST* have an empty line between
72    /// the subject and the body, otherwise the tag is considered to only have a
73    /// body, but not a subject.
74    ///
75    /// If a lightweight tag was used to tag the release, it will have no
76    /// subject.
77    pub(crate) fn subject(&self) -> Option<&str> {
78        self.tag
79            .message
80            .as_ref()
81            .filter(|m| !m.trim().is_empty())
82            .and_then(|m| {
83                let mut lines = m.lines();
84                let first = lines.next()?;
85                if lines
86                    .next()
87                    // If there is no second line, or the second line is empty
88                    // and there is a third line which is not empty, then the
89                    // first line is the subject.
90                    .is_none_or(|v| v.is_empty() && lines.next().is_some_and(|v| !v.is_empty()))
91                {
92                    return Some(first);
93                }
94
95                None
96            })
97    }
98
99    /// The release notes.
100    ///
101    /// This is similar to all lines _after_ the first line, and _before_ the
102    /// PGP signature of the Git tag annotated message.
103    ///
104    /// If a lightweight tag was used to tag the release, it will have no
105    /// notes.
106    pub(crate) fn notes(&self) -> Option<&str> {
107        self.tag.message.as_ref().and_then(|msg| {
108            let begin = self.subject().and_then(|_| msg.find('\n')).unwrap_or(0);
109            let end = msg
110                .find("-----BEGIN")
111                .unwrap_or(msg.len())
112                .saturating_sub(1);
113
114            msg.get(begin..end).map(str::trim)
115        })
116    }
117
118    /// The release date.
119    ///
120    /// If an annotated tag was used to tag the release, this will be the
121    /// timestamp attached to the tag. If a lightweight tag was used, this will
122    /// be the timestamp of the commit to which the tag points.
123    ///
124    /// # Errors
125    ///
126    /// If an error occurs during Git operations, this method will return an
127    /// error.
128    ///
129    /// If the time returned by Git is not a valid UNIX timestamp, an error is
130    /// returned, but this is highly unlikely.
131    pub(crate) fn date(&self) -> DateTime<Utc> {
132        self.tag
133            .tagger
134            .as_ref()
135            .map(|t| t.time)
136            .unwrap_or_else(|| self.tag.commit.time)
137    }
138
139    /// The Git tag belonging to the release.
140    pub fn tag(&self) -> &Tag {
141        &self.tag
142    }
143
144    /// The change set belonging to the release.
145    pub(crate) fn changeset(&self) -> &ChangeSet<'_> {
146        &self.changeset
147    }
148}