diff options
author | Daniel Baumann <daniel@debian.org> | 2024-11-04 11:30:10 +0100 |
---|---|---|
committer | Daniel Baumann <daniel@debian.org> | 2024-11-20 07:38:21 +0100 |
commit | 34f503aa3bfba930fd7978a0071786884d73749f (patch) | |
tree | 7e3c8e2506fdd93e29958d9f8cb36fbed4a5af7d /src/release.rs | |
parent | Initial commit. (diff) | |
download | forgejo-cli-upstream.tar.xz forgejo-cli-upstream.zip |
Adding upstream version 0.1.1.upstream/0.1.1upstream
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to 'src/release.rs')
-rw-r--r-- | src/release.rs | 614 |
1 files changed, 614 insertions, 0 deletions
diff --git a/src/release.rs b/src/release.rs new file mode 100644 index 0000000..fa9d7d7 --- /dev/null +++ b/src/release.rs @@ -0,0 +1,614 @@ +use clap::{Args, Subcommand}; +use eyre::{bail, eyre, OptionExt}; +use forgejo_api::{ + structs::{RepoCreateReleaseAttachmentQuery, RepoListReleasesQuery}, + Forgejo, +}; +use tokio::io::AsyncWriteExt; + +use crate::{ + keys::KeyInfo, + repo::{RepoArg, RepoInfo, RepoName}, + SpecialRender, +}; + +#[derive(Args, Clone, Debug)] +pub struct ReleaseCommand { + /// The local git remote that points to the repo to operate on. + #[clap(long, short = 'R')] + remote: Option<String>, + /// The name of the repository to operate on. + #[clap(long, short, id = "[HOST/]OWNER/REPO")] + repo: Option<RepoArg>, + #[clap(subcommand)] + command: ReleaseSubcommand, +} + +#[derive(Subcommand, Clone, Debug)] +pub enum ReleaseSubcommand { + /// Create a new release + Create { + name: String, + #[clap(long, short = 'T')] + /// Create a new cooresponding tag for this release. Defaults to release's name. + create_tag: Option<Option<String>>, + #[clap(long, short = 't')] + /// Pre-existing tag to use + /// + /// If you need to create a new tag for this release, use `--create-tag` + tag: Option<String>, + #[clap( + long, + short, + help = "Include a file as an attachment", + long_help = "Include a file as an attachment + +`--attach=<FILE>` will set the attachment's name to the file name +`--attach=<FILE>:<ASSET>` will use the provided name for the attachment" + )] + attach: Vec<String>, + #[clap(long, short)] + /// Text of the release body. + /// + /// Using this flag without an argument will open your editor. + body: Option<Option<String>>, + #[clap(long, short = 'B')] + branch: Option<String>, + #[clap(long, short)] + draft: bool, + #[clap(long, short)] + prerelease: bool, + }, + /// Edit a release's info + Edit { + name: String, + #[clap(long, short = 'n')] + rename: Option<String>, + #[clap(long, short = 't')] + /// Corresponding tag for this release. + tag: Option<String>, + #[clap(long, short)] + /// Text of the release body. + /// + /// Using this flag without an argument will open your editor. + body: Option<Option<String>>, + #[clap(long, short)] + draft: Option<bool>, + #[clap(long, short)] + prerelease: Option<bool>, + }, + /// Delete a release + Delete { + name: String, + #[clap(long, short = 't')] + by_tag: bool, + }, + /// List all the releases on a repo + List { + #[clap(long, short = 'p')] + include_prerelease: bool, + #[clap(long, short = 'd')] + include_draft: bool, + }, + /// View a release's info + View { + name: String, + #[clap(long, short = 't')] + by_tag: bool, + }, + /// Open a release in your browser + Browse { name: Option<String> }, + /// Commands on a release's attached files + #[clap(subcommand)] + Asset(AssetCommand), +} + +#[derive(Subcommand, Clone, Debug)] +pub enum AssetCommand { + /// Create a new attachment on a release + Create { + release: String, + path: std::path::PathBuf, + name: Option<String>, + }, + /// Remove an attachment from a release + Delete { release: String, asset: String }, + /// Download an attached file + /// + /// Use `source.zip` or `source.tar.gz` to download the repo archive + Download { + release: String, + asset: String, + #[clap(long, short)] + output: Option<std::path::PathBuf>, + }, +} + +impl ReleaseCommand { + pub async fn run(self, keys: &mut KeyInfo, remote_name: Option<&str>) -> eyre::Result<()> { + let repo = RepoInfo::get_current(remote_name, self.repo.as_ref(), self.remote.as_deref())?; + let api = keys.get_api(&repo.host_url()).await?; + let repo = repo + .name() + .ok_or_eyre("couldn't get repo name, try specifying with --repo")?; + match self.command { + ReleaseSubcommand::Create { + name, + create_tag, + tag, + attach, + body, + branch, + draft, + prerelease, + } => { + create_release( + &repo, &api, name, create_tag, tag, attach, body, branch, draft, prerelease, + ) + .await? + } + ReleaseSubcommand::Edit { + name, + rename, + tag, + body, + draft, + prerelease, + } => edit_release(&repo, &api, name, rename, tag, body, draft, prerelease).await?, + ReleaseSubcommand::Delete { name, by_tag } => { + delete_release(&repo, &api, name, by_tag).await? + } + ReleaseSubcommand::List { + include_prerelease, + include_draft, + } => list_releases(&repo, &api, include_prerelease, include_draft).await?, + ReleaseSubcommand::View { name, by_tag } => { + view_release(&repo, &api, name, by_tag).await? + } + ReleaseSubcommand::Browse { name } => browse_release(&repo, &api, name).await?, + ReleaseSubcommand::Asset(subcommand) => match subcommand { + AssetCommand::Create { + release, + path, + name, + } => create_asset(&repo, &api, release, path, name).await?, + AssetCommand::Delete { release, asset } => { + delete_asset(&repo, &api, release, asset).await? + } + AssetCommand::Download { + release, + asset, + output, + } => download_asset(&repo, &api, release, asset, output).await?, + }, + } + Ok(()) + } +} + +async fn create_release( + repo: &RepoName, + api: &Forgejo, + name: String, + create_tag: Option<Option<String>>, + tag: Option<String>, + attachments: Vec<String>, + body: Option<Option<String>>, + branch: Option<String>, + draft: bool, + prerelease: bool, +) -> eyre::Result<()> { + let tag_name = match (tag, create_tag) { + (None, None) => bail!("must select tag with `--tag` or `--create-tag`"), + (Some(tag), None) => tag, + (None, Some(tag)) => { + let tag = tag.unwrap_or_else(|| name.clone()); + let opt = forgejo_api::structs::CreateTagOption { + message: None, + tag_name: tag.clone(), + target: branch, + }; + api.repo_create_tag(repo.owner(), repo.name(), opt).await?; + tag + } + (Some(_), Some(_)) => { + bail!("`--tag` and `--create-tag` are mutually exclusive; please pick just one") + } + }; + + let body = match body { + Some(Some(body)) => Some(body), + Some(None) => { + let mut s = String::new(); + crate::editor(&mut s, Some("md")).await?; + Some(s) + } + None => None, + }; + + let release_opt = forgejo_api::structs::CreateReleaseOption { + hide_archive_links: None, + body, + draft: Some(draft), + name: Some(name.clone()), + prerelease: Some(prerelease), + tag_name, + target_commitish: None, + }; + let release = api + .repo_create_release(repo.owner(), repo.name(), release_opt) + .await?; + + for attachment in attachments { + let (file, asset) = match attachment.split_once(':') { + Some((file, asset)) => (std::path::Path::new(file), asset), + None => { + let file = std::path::Path::new(&attachment); + let asset = file + .file_name() + .ok_or_else(|| eyre!("{attachment} does not have a file name"))? + .to_str() + .unwrap(); + (file, asset) + } + }; + let query = RepoCreateReleaseAttachmentQuery { + name: Some(asset.into()), + }; + let id = release + .id + .ok_or_else(|| eyre::eyre!("release does not have id"))? as u64; + api.repo_create_release_attachment( + repo.owner(), + repo.name(), + id, + tokio::fs::read(file).await?, + query, + ) + .await?; + } + + println!("Created release {name}"); + + Ok(()) +} + +async fn edit_release( + repo: &RepoName, + api: &Forgejo, + name: String, + rename: Option<String>, + tag: Option<String>, + body: Option<Option<String>>, + draft: Option<bool>, + prerelease: Option<bool>, +) -> eyre::Result<()> { + let release = find_release(repo, api, &name).await?; + let body = match body { + Some(Some(body)) => Some(body), + Some(None) => { + let mut s = release + .body + .clone() + .ok_or_else(|| eyre::eyre!("release does not have body"))?; + crate::editor(&mut s, Some("md")).await?; + Some(s) + } + None => None, + }; + let release_edit = forgejo_api::structs::EditReleaseOption { + hide_archive_links: None, + name: rename, + tag_name: tag, + body, + draft, + prerelease, + target_commitish: None, + }; + let id = release + .id + .ok_or_else(|| eyre::eyre!("release does not have id"))? as u64; + api.repo_edit_release(repo.owner(), repo.name(), id, release_edit) + .await?; + Ok(()) +} + +async fn list_releases( + repo: &RepoName, + api: &Forgejo, + prerelease: bool, + draft: bool, +) -> eyre::Result<()> { + let query = forgejo_api::structs::RepoListReleasesQuery { + pre_release: Some(prerelease), + draft: Some(draft), + page: None, + limit: None, + }; + let releases = api + .repo_list_releases(repo.owner(), repo.name(), query) + .await?; + for release in releases { + let name = release + .name + .as_ref() + .ok_or_else(|| eyre::eyre!("release does not have name"))?; + let draft = release + .draft + .as_ref() + .ok_or_else(|| eyre::eyre!("release does not have draft"))?; + let prerelease = release + .prerelease + .as_ref() + .ok_or_else(|| eyre::eyre!("release does not have prerelease"))?; + print!("{}", name); + match (draft, prerelease) { + (false, false) => (), + (true, false) => print!(" (draft)"), + (false, true) => print!(" (prerelease)"), + (true, true) => print!(" (draft, prerelease)"), + } + println!(); + } + Ok(()) +} + +async fn view_release( + repo: &RepoName, + api: &Forgejo, + name: String, + by_tag: bool, +) -> eyre::Result<()> { + let release = if by_tag { + api.repo_get_release_by_tag(repo.owner(), repo.name(), &name) + .await? + } else { + find_release(repo, api, &name).await? + }; + let name = release + .name + .as_ref() + .ok_or_else(|| eyre::eyre!("release does not have name"))?; + let author = release + .author + .as_ref() + .ok_or_else(|| eyre::eyre!("release does not have author"))?; + let login = author + .login + .as_ref() + .ok_or_else(|| eyre::eyre!("autho does not have login"))?; + let created_at = release + .created_at + .ok_or_else(|| eyre::eyre!("release does not have created_at"))?; + println!("{}", name); + print!("By {} on ", login); + created_at.format_into( + &mut std::io::stdout(), + &time::format_description::well_known::Rfc2822, + )?; + println!(); + let SpecialRender { bullet, .. } = crate::special_render(); + let body = release + .body + .as_ref() + .ok_or_else(|| eyre::eyre!("release does not have body"))?; + if !body.is_empty() { + println!(); + println!("{}", crate::markdown(&body)); + println!(); + } + let assets = release + .assets + .as_ref() + .ok_or_else(|| eyre::eyre!("release does not have assets"))?; + if !assets.is_empty() { + println!("{} assets", assets.len() + 2); + for asset in assets { + let name = asset + .name + .as_ref() + .ok_or_else(|| eyre::eyre!("asset does not have name"))?; + println!("{bullet} {}", name); + } + println!("{bullet} source.zip"); + println!("{bullet} source.tar.gz"); + } + Ok(()) +} + +async fn browse_release(repo: &RepoName, api: &Forgejo, name: Option<String>) -> eyre::Result<()> { + match name { + Some(name) => { + let release = find_release(repo, api, &name).await?; + let html_url = release + .html_url + .as_ref() + .ok_or_else(|| eyre::eyre!("release does not have html_url"))?; + open::that(html_url.as_str())?; + } + None => { + let repo_data = api.repo_get(repo.owner(), repo.name()).await?; + let mut html_url = repo_data + .html_url + .clone() + .ok_or_else(|| eyre::eyre!("repository does not have html_url"))?; + html_url.path_segments_mut().unwrap().push("releases"); + open::that(html_url.as_str())?; + } + } + Ok(()) +} + +async fn create_asset( + repo: &RepoName, + api: &Forgejo, + release: String, + file: std::path::PathBuf, + asset: Option<String>, +) -> eyre::Result<()> { + let (file, asset) = match asset { + Some(ref asset) => (&*file, &**asset), + None => { + let asset = file + .file_name() + .ok_or_else(|| eyre!("{} does not have a file name", file.display()))? + .to_str() + .unwrap(); + (&*file, asset) + } + }; + let id = find_release(repo, api, &release) + .await? + .id + .ok_or_else(|| eyre::eyre!("release does not have id"))? as u64; + let query = RepoCreateReleaseAttachmentQuery { + name: Some(asset.to_owned()), + }; + api.repo_create_release_attachment( + repo.owner(), + repo.name(), + id, + tokio::fs::read(file).await?, + query, + ) + .await?; + + println!("Added attachment `{}` to {}", asset, release); + + Ok(()) +} + +async fn delete_asset( + repo: &RepoName, + api: &Forgejo, + release_name: String, + asset_name: String, +) -> eyre::Result<()> { + let release = find_release(repo, api, &release_name).await?; + let assets = release + .assets + .as_ref() + .ok_or_else(|| eyre::eyre!("release does not have assets"))?; + let asset = assets + .iter() + .find(|a| a.name.as_ref() == Some(&asset_name)) + .ok_or_else(|| eyre!("asset not found"))?; + let release_id = release + .id + .ok_or_else(|| eyre::eyre!("release does not have id"))? as u64; + let asset_id = asset + .id + .ok_or_else(|| eyre::eyre!("asset does not have id"))? as u64; + api.repo_delete_release_attachment(repo.owner(), repo.name(), release_id, asset_id) + .await?; + println!("Removed attachment `{}` from {}", asset_name, release_name); + Ok(()) +} + +async fn download_asset( + repo: &RepoName, + api: &Forgejo, + release: String, + asset: String, + output: Option<std::path::PathBuf>, +) -> eyre::Result<()> { + let release = find_release(repo, api, &release).await?; + let file = match &*asset { + "source.zip" => { + let tag_name = release + .tag_name + .as_ref() + .ok_or_else(|| eyre::eyre!("release does not have tag_name"))?; + api.repo_get_archive(repo.owner(), repo.name(), &format!("{}.zip", tag_name)) + .await? + } + "source.tar.gz" => { + let tag_name = release + .tag_name + .as_ref() + .ok_or_else(|| eyre::eyre!("release does not have tag_name"))?; + api.repo_get_archive(repo.owner(), repo.name(), &format!("{}.tar.gz", tag_name)) + .await? + } + name => { + let assets = release + .assets + .as_ref() + .ok_or_else(|| eyre::eyre!("release does not have assets"))?; + let asset = assets + .iter() + .find(|a| a.name.as_deref() == Some(name)) + .ok_or_else(|| eyre!("asset not found"))?; + let release_id = release + .id + .ok_or_else(|| eyre::eyre!("release does not have id"))? + as u64; + let asset_id = asset + .id + .ok_or_else(|| eyre::eyre!("asset does not have id"))? + as u64; + api.download_release_attachment(repo.owner(), repo.name(), release_id, asset_id) + .await? + .to_vec() + } + }; + let real_output = output + .as_deref() + .unwrap_or_else(|| std::path::Path::new(&asset)); + tokio::fs::OpenOptions::new() + .create_new(true) + .write(true) + .open(real_output) + .await? + .write_all(file.as_ref()) + .await?; + + if output.is_some() { + println!("Downloaded {asset} into {}", real_output.display()); + } else { + println!("Downloaded {asset}"); + } + + Ok(()) +} + +async fn find_release( + repo: &RepoName, + api: &Forgejo, + name: &str, +) -> eyre::Result<forgejo_api::structs::Release> { + let query = RepoListReleasesQuery { + draft: None, + pre_release: None, + page: None, + limit: None, + }; + let mut releases = api + .repo_list_releases(repo.owner(), repo.name(), query) + .await?; + let idx = releases + .iter() + .position(|r| r.name.as_deref() == Some(name)) + .ok_or_else(|| eyre!("release not found"))?; + Ok(releases.swap_remove(idx)) +} + +async fn delete_release( + repo: &RepoName, + api: &Forgejo, + name: String, + by_tag: bool, +) -> eyre::Result<()> { + if by_tag { + api.repo_delete_release_by_tag(repo.owner(), repo.name(), &name) + .await?; + } else { + let id = find_release(repo, api, &name) + .await? + .id + .ok_or_else(|| eyre::eyre!("release does not have id"))? as u64; + api.repo_delete_release(repo.owner(), repo.name(), id) + .await?; + } + Ok(()) +} |