1use std::path::Path;
2
3use xshell::{cmd, Shell};
4
5#[derive(serde::Deserialize)]
6#[serde(rename_all = "camelCase")]
7struct GhResponse {
8 pub tag_name: String,
9 pub is_draft: bool,
10 pub is_prelease: bool,
11}
12
13pub struct Release {
14 pub name: String,
15 pub version: semver::Version,
16 pub draft: bool,
17 pub prelease: bool,
18}
19
20impl TryFrom<GhResponse> for Release {
21 type Error = ();
22 fn try_from(value: GhResponse) -> Result<Self, Self::Error> {
23 let (name, version) = value.tag_name.rsplit_once('-').ok_or(())?;
24
25 let version = semver::Version::parse(version).map_err(|_| ())?;
26
27 Ok(Release {
28 name: name.to_string(),
29 version,
30 draft: value.is_draft,
31 prelease: value.is_prelease,
32 })
33 }
34}
35
36#[derive(Debug, thiserror::Error)]
37pub enum GetFromGHError {
38 #[error(transparent)]
39 XShellError(#[from] xshell::Error),
40
41 #[error(transparent)]
42 SerdeError(#[from] serde_json::Error),
43}
44
45#[derive(Default)]
46pub enum ReleaseMode {
47 #[default]
48 Normal,
49
50 Draft,
51 Prerelease,
52}
53
54impl Release {
55 pub fn new(name: &str, version: &str) -> Result<Self, semver::Error> {
56 let version = semver::Version::parse(version)?;
57 Ok(Release {
58 name: name.to_string(),
59 version,
60 draft: false,
61 prelease: false,
62 })
63 }
64
65 pub fn with_release_mode(&mut self, mode: ReleaseMode) -> &mut Self {
66 self.draft = matches!(mode, ReleaseMode::Draft);
67 self.prelease = matches!(mode, ReleaseMode::Prerelease);
68 self
69 }
70
71 pub fn get_from_gh() -> Result<Vec<Self>, GetFromGHError> {
72 let sh = Shell::new()?;
73 let previous_releases: Vec<GhResponse> =
74 serde_json::from_str(&cmd!(sh, "gh release list --json tagName").read()?)?;
75
76 Ok(previous_releases
77 .into_iter()
78 .filter_map(|x| Self::try_from(x).ok())
79 .collect())
80 }
81
82 pub fn release<T, I>(&self, files: T) -> Result<String, xshell::Error>
83 where
84 T: IntoIterator<Item = I>,
85 I: AsRef<Path> + Sized,
86 {
87 let sh = Shell::new()?;
88 let files = files
89 .into_iter()
90 .map(|x| x.as_ref().display().to_string())
91 .collect::<Vec<_>>();
92
93 let name = self.name.clone();
94
95 let draft = if self.draft { " --draft" } else { "" };
96
97 sh.cmd("gh")
98 .args(["release", "create", "generate-notes", draft, &name])
99 .args(files)
100 .read()
101 }
102}