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", ¬es)?;
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}