Skip to main content

gitee_cli_rs/api/
releases.rs

1use super::client::Client;
2use crate::error::Result;
3use crate::models::{Release, ReleaseAsset};
4use crate::repo::Repo;
5
6pub struct Releases<'a> {
7    client: &'a Client,
8    repo: &'a Repo,
9}
10
11
12pub struct EditRelease<'a> {
13    pub name: Option<&'a str>,
14    pub notes: Option<&'a str>,
15    pub prerelease: Option<bool>,
16}
17
18pub struct CreateRelease<'a> {
19    pub tag: &'a str,
20    pub name: Option<&'a str>,
21    pub notes: Option<&'a str>,
22    pub target: Option<&'a str>,
23    pub prerelease: bool,
24}
25
26impl Releases<'_> {
27    pub(crate) fn new<'a>(client: &'a Client, repo: &'a Repo) -> Releases<'a> {
28        Releases { client, repo }
29    }
30
31    pub fn list(&self, limit: usize) -> Result<Vec<Release>> {
32        let o = self.repo.owner.as_str();
33        let r = self.repo.name.as_str();
34        let path = format!("/repos/{o}/{r}/releases");
35        self.client.get_paged(&path, &[], limit)
36    }
37
38    /// Gitee quirk: a missing release returns HTTP 200 with a JSON `null`
39    /// body (not 404). Deserialize as Option and map null to NotFound.
40    pub fn get_by_tag(&self, tag: &str) -> Result<Release> {
41        let o = self.repo.owner.as_str();
42        let r = self.repo.name.as_str();
43        let rel: Option<Release> = self
44            .client
45            .get(&format!("/repos/{o}/{r}/releases/tags/{tag}"), &[])?;
46        rel.ok_or_else(|| {
47            crate::error::GiteeError::NotFound(format!("release {tag}"))
48        })
49    }
50
51    /// Gitee quirks: `body` is REQUIRED and must be non-empty (400 otherwise),
52    /// so it defaults to the display name; `prerelease` is always sent as
53    /// `"true"` or `"false"`; `name` defaults to `tag`.
54    pub fn create(&self, req: &CreateRelease<'_>) -> Result<Release> {
55        let o = self.repo.owner.as_str();
56        let r = self.repo.name.as_str();
57        let display_name = req.name.unwrap_or(req.tag);
58        let mut f: Vec<(&str, String)> = vec![
59            ("tag_name", req.tag.to_string()),
60            ("name", display_name.to_string()),
61            ("body", req.notes.unwrap_or(display_name).to_string()),
62        ];
63        if let Some(t) = req.target {
64            f.push(("target_commitish", t.to_string()));
65        }
66        f.push(("prerelease", Client::bool_str(req.prerelease).to_string()));
67        let form = Client::str_refs(&f);
68        self.client.post(&format!("/repos/{o}/{r}/releases"), &form)
69    }
70
71
72    /// Gitee quirk (swagger 2026-07-18): PATCH requires `tag_name`, `name`, and
73    /// `body` on every request — GET-by-tag first, then send the flag value or
74    /// the current value for all three. `prerelease` is sent only when requested.
75    /// `--latest` omitted: PATCH /releases/{id} has no latest param (swagger 2026-07-18).
76    pub fn edit(&self, tag: &str, req: &EditRelease<'_>) -> Result<Release> {
77        let current = self.get_by_tag(tag)?;
78        let o = self.repo.owner.as_str();
79        let r = self.repo.name.as_str();
80        let display_name = req
81            .name
82            .or(current.name.as_deref())
83            .unwrap_or(tag);
84        let body = req
85            .notes
86            .or(current.body.as_deref())
87            .unwrap_or(display_name);
88        let mut f: Vec<(&str, String)> = vec![
89            ("tag_name", tag.to_string()),
90            ("name", display_name.to_string()),
91            ("body", body.to_string()),
92        ];
93        if req.prerelease == Some(true) {
94            f.push(("prerelease", "true".to_string()));
95        }
96        let form = Client::str_refs(&f);
97        self.client
98            .patch(&format!("/repos/{o}/{r}/releases/{}", current.id), &form)
99    }
100
101    pub fn delete(&self, tag: &str) -> Result<()> {
102        let release = self.get_by_tag(tag)?;
103        let o = self.repo.owner.as_str();
104        let r = self.repo.name.as_str();
105        self.client
106            .delete_ok(&format!("/repos/{o}/{r}/releases/{}", release.id))
107    }
108
109    // Follow-up (2026-07-18): asset-level DELETE
110    // /repos/{owner}/{repo}/releases/{rid}/attach_files/{aid} exists in swagger
111    // but is out of scope for this ticket.
112
113    pub fn upload(&self, tag: &str, file_path: &str) -> Result<ReleaseAsset> {
114        let o = self.repo.owner.as_str();
115        let r = self.repo.name.as_str();
116        let release = self.get_by_tag(tag)?;
117        let id = release.id;
118        self.client.post_multipart(
119            &format!("/repos/{o}/{r}/releases/{id}/attach_files"),
120            file_path,
121        )
122    }
123}