Skip to main content

gitee_cli_rs/cmd/
release.rs

1use std::io::Write;
2use std::path::Path;
3
4use super::{confirm, Ctx};
5use crate::api::releases::{CreateRelease, EditRelease};
6use crate::cli::ReleaseCmd;
7use crate::error::Result;
8use crate::out;
9
10pub fn execute(ctx: &Ctx, cmd: ReleaseCmd) -> Result<()> {
11    match cmd {
12        ReleaseCmd::List { limit } => {
13            let repo = ctx.repo()?;
14            let items = ctx.client.releases(repo).list(limit.limit)?;
15            let mut out = std::io::stdout().lock();
16            ctx.out
17                .render(&mut out, &items, |w| out::release_table(w, &items))?;
18        }
19        ReleaseCmd::View { tag, web } => {
20            let repo = ctx.repo()?;
21            if web {
22                let url = crate::web::release_url(&ctx.host, repo, &tag);
23                return crate::web::open_or_print(&url);
24            }
25            let release = ctx.client.releases(repo).get_by_tag(&tag)?;
26            let mut out = std::io::stdout().lock();
27            ctx.out
28                .render(&mut out, &release, |w| out::one_release(w, &release))?;
29        }
30        ReleaseCmd::Create {
31            tag,
32            name,
33            notes,
34            target,
35            prerelease,
36        } => {
37            let repo = ctx.repo()?;
38            let req = CreateRelease {
39                tag: &tag,
40                name: name.as_deref(),
41                notes: notes.as_deref(),
42                target: target.as_deref(),
43                prerelease,
44            };
45            let release = ctx.client.releases(repo).create(&req)?;
46            let mut out = std::io::stdout().lock();
47            ctx.out
48                .render(&mut out, &release, |w| out::one_release(w, &release))?;
49        }
50        ReleaseCmd::Upload { tag, files } => {
51            let repo = ctx.repo()?;
52            let releases = ctx.client.releases(repo);
53            let mut out = std::io::stdout().lock();
54            for file_path in files {
55                let asset = releases.upload(&tag, &file_path)?;
56                writeln!(out, "{}", asset.name)?;
57            }
58        }
59        ReleaseCmd::Download { tag, dir, pattern } => {
60            let repo = ctx.repo()?;
61            let release = ctx.client.releases(repo).get_by_tag(&tag)?;
62            let assets = release.assets.unwrap_or_default();
63            let matching: Vec<_> = assets
64                .iter()
65                .filter(|a| {
66                    pattern
67                        .as_deref()
68                        .is_none_or(|p| glob_match(p, &a.name))
69                })
70                .collect();
71            let mut out = std::io::stdout().lock();
72            if matching.is_empty() {
73                if pattern.is_some() {
74                    writeln!(out, "No assets match pattern")?;
75                } else {
76                    writeln!(out, "No release assets")?;
77                }
78                return Ok(());
79            }
80            std::fs::create_dir_all(&dir)?;
81            for asset in matching {
82                let bytes = ctx.client.get_bytes(&asset.browser_download_url)?;
83                let path = Path::new(&dir).join(&asset.name);
84                std::fs::write(&path, &bytes)?;
85                writeln!(out, "Saved {} ({} bytes)", path.display(), bytes.len())?;
86            }
87        }
88        ReleaseCmd::Edit {
89            tag,
90            name,
91            notes,
92            prerelease,
93        } => {
94            let repo = ctx.repo()?;
95            let req = EditRelease {
96                name: name.as_deref(),
97                notes: notes.as_deref(),
98                prerelease: prerelease.then_some(true),
99            };
100            let release = ctx.client.releases(repo).edit(&tag, &req)?;
101            let mut out = std::io::stdout().lock();
102            ctx.out
103                .render(&mut out, &release, |w| out::one_release(w, &release))?;
104        }
105        ReleaseCmd::Delete { tag, yes } => {
106            confirm(&format!("Delete release {tag}"), yes)?;
107            let repo = ctx.repo()?;
108            ctx.client.releases(repo).delete(&tag)?;
109            let mut out = std::io::stdout().lock();
110            writeln!(out, "Deleted release {tag}")?;
111        }
112    }
113    Ok(())
114}
115
116/// Hand-rolled `*`-wildcard matcher: `*` matches any run (including empty); other
117/// bytes must match literally.
118pub fn glob_match(pattern: &str, name: &str) -> bool {
119    let p: Vec<char> = pattern.chars().collect();
120    let n: Vec<char> = name.chars().collect();
121    glob_at(&p, 0, &n, 0)
122}
123
124fn glob_at(p: &[char], pi: usize, n: &[char], ni: usize) -> bool {
125    if pi == p.len() {
126        return ni == n.len();
127    }
128    if p[pi] == '*' {
129        if pi + 1 == p.len() {
130            return true;
131        }
132        for i in ni..=n.len() {
133            if glob_at(p, pi + 1, n, i) {
134                return true;
135            }
136        }
137        return false;
138    }
139    if ni == n.len() {
140        return false;
141    }
142    if p[pi] == '?' {
143        return glob_at(p, pi + 1, n, ni + 1);
144    }
145    if p[pi] != n[ni] {
146        return false;
147    }
148    glob_at(p, pi + 1, n, ni + 1)
149}
150
151#[cfg(test)]
152mod glob_tests {
153    use super::glob_match;
154
155    #[test]
156    fn star_matches_empty() {
157        assert!(glob_match("*", ""));
158        assert!(glob_match("*.txt", "a.txt"));
159        assert!(glob_match("pre*post", "prepost"));
160    }
161
162    #[test]
163    fn star_matches_run() {
164        assert!(glob_match("*.tar.xz", "gitee-linux-amd64.tar.xz"));
165        assert!(!glob_match("*.tar.xz", "gitee-darwin-arm64.zip"));
166    }
167
168    #[test]
169    fn literal_must_match() {
170        assert!(glob_match("exact", "exact"));
171        assert!(!glob_match("exact", "exactx"));
172    }
173
174    #[test]
175    fn glob_match_question_mark_matches_one_char() {
176        assert!(glob_match("v?.txt", "v1.txt"));
177        assert!(!glob_match("v?.txt", "v12.txt"));
178        assert!(!glob_match("v?.txt", "v.txt"));
179        assert!(glob_match("?.txt", "a.txt"));
180    }
181}