forgejo-cli/src/release.rs

569 lines
16 KiB
Rust
Raw Normal View History

2023-12-16 19:42:38 -05:00
use clap::Subcommand;
use eyre::{bail, eyre};
2024-03-21 17:48:39 -04:00
use forgejo_api::{
structs::{RepoCreateReleaseAttachmentQuery, RepoListReleasesQuery},
Forgejo,
};
2023-12-16 19:42:38 -05:00
use tokio::io::AsyncWriteExt;
use crate::{keys::KeyInfo, repo::RepoInfo};
#[derive(Subcommand, Clone, Debug)]
pub enum ReleaseCommand {
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 {
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 {
name: String,
#[clap(long, short = 't')]
by_tag: bool,
},
List {
#[clap(long, short = 'p')]
include_prerelease: bool,
#[clap(long, short = 'd')]
include_draft: bool,
},
View {
name: String,
#[clap(long, short = 't')]
by_tag: bool,
},
Browse {
name: Option<String>,
},
#[clap(subcommand)]
Asset(AssetCommand),
}
#[derive(Subcommand, Clone, Debug)]
pub enum AssetCommand {
Create {
release: String,
path: std::path::PathBuf,
name: Option<String>,
},
Delete {
release: String,
asset: String,
},
Download {
release: String,
asset: String,
#[clap(long, short)]
output: Option<std::path::PathBuf>,
},
}
impl ReleaseCommand {
pub async fn run(self, keys: &KeyInfo, remote_name: Option<&str>) -> eyre::Result<()> {
let repo = RepoInfo::get_current(remote_name)?;
let api = keys.get_api(&repo.host_url())?;
match self {
Self::Create {
name,
create_tag,
tag,
attach,
body,
branch,
draft,
prerelease,
} => {
create_release(
&repo, &api, name, create_tag, tag, attach, body, branch, draft, prerelease,
)
.await?
}
Self::Edit {
name,
rename,
tag,
body,
draft,
prerelease,
} => edit_release(&repo, &api, name, rename, tag, body, draft, prerelease).await?,
Self::Delete { name, by_tag } => delete_release(&repo, &api, name, by_tag).await?,
Self::List {
include_prerelease,
include_draft,
} => list_releases(&repo, &api, include_prerelease, include_draft).await?,
Self::View { name, by_tag } => view_release(&repo, &api, name, by_tag).await?,
Self::Browse { name } => browse_release(&repo, &api, name).await?,
Self::Asset(subcommand) => match subcommand {
AssetCommand::Create {
release,
path,
name,
} => create_asset(&repo, &api, release, path, name).await?,
AssetCommand::Delete { release, asset } => {
2023-12-16 22:48:07 -05:00
delete_asset(&repo, &api, release, asset).await?
2023-12-16 19:42:38 -05:00
}
AssetCommand::Download {
release,
asset,
output,
} => download_asset(&repo, &api, release, asset, output).await?,
},
}
Ok(())
}
}
async fn create_release(
repo: &RepoInfo,
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());
2024-03-21 17:48:39 -04:00
let opt = forgejo_api::structs::CreateTagOption {
2023-12-16 19:42:38 -05:00
message: None,
tag_name: tag.clone(),
target: branch,
};
2024-03-21 17:48:39 -04:00
api.repo_create_tag(repo.owner(), repo.name(), opt).await?;
2023-12-16 19:42:38 -05:00
tag
}
(Some(_), Some(_)) => {
bail!("`--tag` and `--create-tag` are mutually exclusive; please pick just one")
}
};
let body = match body {
2024-03-21 17:48:39 -04:00
Some(Some(body)) => Some(body),
2023-12-16 19:42:38 -05:00
Some(None) => {
let mut s = String::new();
crate::editor(&mut s, Some("md")).await?;
2024-03-21 17:48:39 -04:00
Some(s)
2023-12-16 19:42:38 -05:00
}
2024-03-21 17:48:39 -04:00
None => None,
2023-12-16 19:42:38 -05:00
};
2024-03-21 17:48:39 -04:00
let release_opt = forgejo_api::structs::CreateReleaseOption {
2023-12-16 19:42:38 -05:00
body,
2024-03-21 17:48:39 -04:00
draft: Some(draft),
name: Some(name),
prerelease: Some(prerelease),
2023-12-16 19:42:38 -05:00
tag_name,
target_commitish: None,
};
let release = api
2024-03-21 17:48:39 -04:00
.repo_create_release(repo.owner(), repo.name(), release_opt)
2023-12-16 19:42:38 -05:00
.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)
}
};
2024-03-21 17:48:39 -04:00
let query = RepoCreateReleaseAttachmentQuery {
name: Some(asset.into()),
};
let id = release
.id
.ok_or_else(|| eyre::eyre!("release does not have id"))?;
api.repo_create_release_attachment(
2023-12-16 19:42:38 -05:00
repo.owner(),
repo.name(),
2024-03-21 17:48:39 -04:00
id,
2023-12-16 19:42:38 -05:00
tokio::fs::read(file).await?,
2024-03-21 17:48:39 -04:00
query,
2023-12-16 19:42:38 -05:00
)
.await?;
}
Ok(())
}
async fn edit_release(
repo: &RepoInfo,
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) => {
2024-03-21 17:48:39 -04:00
let mut s = release
.body
.clone()
.ok_or_else(|| eyre::eyre!("release does not have body"))?;
2023-12-16 19:42:38 -05:00
crate::editor(&mut s, Some("md")).await?;
Some(s)
}
None => None,
};
2024-03-21 17:48:39 -04:00
let release_edit = forgejo_api::structs::EditReleaseOption {
2023-12-16 19:42:38 -05:00
name: rename,
tag_name: tag,
body,
draft,
prerelease,
target_commitish: None,
};
2024-03-21 17:48:39 -04:00
let id = release
.id
.ok_or_else(|| eyre::eyre!("release does not have id"))?;
api.repo_edit_release(repo.owner(), repo.name(), id, release_edit)
2023-12-16 19:42:38 -05:00
.await?;
Ok(())
}
async fn list_releases(
repo: &RepoInfo,
api: &Forgejo,
prerelease: bool,
draft: bool,
) -> eyre::Result<()> {
2024-03-21 17:48:39 -04:00
let query = forgejo_api::structs::RepoListReleasesQuery {
pre_release: Some(prerelease),
2023-12-16 19:42:38 -05:00
draft: Some(draft),
2024-03-21 17:48:39 -04:00
per_page: None,
page: None,
limit: None,
2023-12-16 19:42:38 -05:00
};
2024-03-21 17:48:39 -04:00
let releases = api
.repo_list_releases(repo.owner(), repo.name(), query)
.await?;
2023-12-16 19:42:38 -05:00
for release in releases {
2024-03-21 17:48:39 -04:00
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) {
2023-12-16 19:42:38 -05:00
(false, false) => (),
(true, false) => print!(" (draft)"),
(false, true) => print!(" (prerelease)"),
(true, true) => print!(" (draft, prerelease)"),
}
println!();
}
Ok(())
}
async fn view_release(
repo: &RepoInfo,
api: &Forgejo,
name: String,
by_tag: bool,
) -> eyre::Result<()> {
let release = if by_tag {
2024-03-21 17:48:39 -04:00
api.repo_get_release_by_tag(repo.owner(), repo.name(), &name)
2023-12-16 19:42:38 -05:00
.await?
} else {
find_release(repo, api, &name).await?
};
2024-03-21 17:48:39 -04:00
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(
2023-12-16 19:42:38 -05:00
&mut std::io::stdout(),
&time::format_description::well_known::Rfc2822,
)?;
println!();
2024-03-21 17:48:39 -04:00
let body = release
.body
.as_ref()
.ok_or_else(|| eyre::eyre!("release does not have body"))?;
if !body.is_empty() {
2023-12-16 19:42:38 -05:00
println!();
2024-03-21 17:48:39 -04:00
for line in body.lines() {
2023-12-16 19:42:38 -05:00
println!("> {line}");
}
println!();
}
2024-03-21 17:48:39 -04:00
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!("- {}", name);
2023-12-16 19:42:38 -05:00
}
println!("- source.zip");
println!("- source.tar.gz");
}
Ok(())
}
async fn browse_release(repo: &RepoInfo, api: &Forgejo, name: Option<String>) -> eyre::Result<()> {
match name {
Some(name) => {
let release = find_release(repo, api, &name).await?;
2024-03-21 17:48:39 -04:00
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())?;
2023-12-16 19:42:38 -05:00
}
None => {
let mut url = repo.url().clone();
url.path_segments_mut().unwrap().push("releases");
open::that(url.as_str())?;
}
}
Ok(())
}
async fn create_asset(
repo: &RepoInfo,
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)
}
};
2024-03-21 17:48:39 -04:00
let id = find_release(repo, api, &release)
.await?
.id
.ok_or_else(|| eyre::eyre!("release does not have id"))?;
let query = RepoCreateReleaseAttachmentQuery {
name: Some(asset.to_owned()),
};
api.repo_create_release_attachment(
2023-12-16 19:42:38 -05:00
repo.owner(),
repo.name(),
id,
tokio::fs::read(file).await?,
2024-03-21 17:48:39 -04:00
query,
2023-12-16 19:42:38 -05:00
)
.await?;
Ok(())
}
async fn delete_asset(
repo: &RepoInfo,
api: &Forgejo,
release: String,
asset: String,
) -> eyre::Result<()> {
let release = find_release(repo, api, &release).await?;
2024-03-21 17:48:39 -04:00
let assets = release
2023-12-16 19:42:38 -05:00
.assets
2024-03-21 17:48:39 -04:00
.as_ref()
.ok_or_else(|| eyre::eyre!("release does not have assets"))?;
let asset = assets
2023-12-16 19:42:38 -05:00
.iter()
2024-03-21 17:48:39 -04:00
.find(|a| a.name.as_ref() == Some(&asset))
2023-12-16 19:42:38 -05:00
.ok_or_else(|| eyre!("asset not found"))?;
2024-03-21 17:48:39 -04:00
let release_id = release
.id
.ok_or_else(|| eyre::eyre!("release does not have id"))?;
let asset_id = asset
.id
.ok_or_else(|| eyre::eyre!("asset does not have id"))?;
api.repo_delete_release_attachment(repo.owner(), repo.name(), release_id, asset_id)
2023-12-16 19:42:38 -05:00
.await?;
Ok(())
}
async fn download_asset(
repo: &RepoInfo,
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 {
2023-12-16 22:48:07 -05:00
"source.zip" => {
2024-03-21 17:48:39 -04:00
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))
2023-12-16 22:48:07 -05:00
.await?
}
"source.tar.gz" => {
2024-03-21 17:48:39 -04:00
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))
2023-12-16 22:48:07 -05:00
.await?
}
2023-12-16 19:42:38 -05:00
name => {
2024-03-21 17:48:39 -04:00
let assets = release
2023-12-16 19:42:38 -05:00
.assets
2024-03-21 17:48:39 -04:00
.as_ref()
.ok_or_else(|| eyre::eyre!("release does not have assets"))?;
let asset = assets
2023-12-16 19:42:38 -05:00
.iter()
2024-03-21 17:48:39 -04:00
.find(|a| a.name.as_deref() == Some(name))
2023-12-16 19:42:38 -05:00
.ok_or_else(|| eyre!("asset not found"))?;
2024-03-21 17:48:39 -04:00
let release_id = release
.id
.ok_or_else(|| eyre::eyre!("release does not have id"))?;
let asset_id = asset
.id
.ok_or_else(|| eyre::eyre!("asset does not have id"))?;
api.download_release_attachment(repo.owner(), repo.name(), release_id, asset_id)
2023-12-16 22:48:07 -05:00
.await?
2024-03-21 17:48:39 -04:00
.to_vec()
2023-12-16 19:42:38 -05:00
}
};
let output = output
.as_deref()
.unwrap_or_else(|| std::path::Path::new(&asset));
tokio::fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(output)
.await?
.write_all(file.as_ref())
.await?;
Ok(())
}
async fn find_release(
repo: &RepoInfo,
api: &Forgejo,
name: &str,
2024-03-21 17:48:39 -04:00
) -> eyre::Result<forgejo_api::structs::Release> {
let query = RepoListReleasesQuery {
draft: None,
pre_release: None,
per_page: None,
page: None,
limit: None,
};
2023-12-16 19:42:38 -05:00
let mut releases = api
2024-03-21 17:48:39 -04:00
.repo_list_releases(repo.owner(), repo.name(), query)
2023-12-16 19:42:38 -05:00
.await?;
let idx = releases
.iter()
2024-03-21 17:48:39 -04:00
.position(|r| r.name.as_deref() == Some(name))
2023-12-16 19:42:38 -05:00
.ok_or_else(|| eyre!("release not found"))?;
Ok(releases.swap_remove(idx))
}
async fn delete_release(
repo: &RepoInfo,
api: &Forgejo,
name: String,
by_tag: bool,
) -> eyre::Result<()> {
if by_tag {
2024-03-21 17:48:39 -04:00
api.repo_delete_release_by_tag(repo.owner(), repo.name(), &name)
2023-12-16 19:42:38 -05:00
.await?;
} else {
2024-03-21 17:48:39 -04:00
let id = find_release(repo, api, &name)
.await?
.id
.ok_or_else(|| eyre::eyre!("release does not have id"))?;
api.repo_delete_release(repo.owner(), repo.name(), id)
.await?;
2023-12-16 19:42:38 -05:00
}
Ok(())
}