forgejo-cli/src/release.rs

620 lines
18 KiB
Rust
Raw Normal View History

2024-04-17 15:58:45 -04:00
use clap::{Args, Subcommand};
2024-11-06 09:22:57 +01:00
use eyre::{bail, eyre, Context, OptionExt};
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;
2024-04-17 15:58:45 -04:00
use crate::{
keys::KeyInfo,
repo::{RepoArg, RepoInfo, RepoName},
2024-05-07 14:28:17 -04:00
SpecialRender,
2024-04-17 15:58:45 -04:00
};
#[derive(Args, Clone, Debug)]
pub struct ReleaseCommand {
2024-08-04 11:12:49 -04:00
/// The local git remote that points to the repo to operate on.
#[clap(long, short = 'R')]
remote: Option<String>,
2024-08-04 11:12:49 -04:00
/// The name of the repository to operate on.
2024-07-30 18:10:11 -04:00
#[clap(long, short, id = "[HOST/]OWNER/REPO")]
repo: Option<RepoArg>,
#[clap(subcommand)]
command: ReleaseSubcommand,
}
2023-12-16 19:42:38 -05:00
#[derive(Subcommand, Clone, Debug)]
pub enum ReleaseSubcommand {
2024-08-04 11:12:02 -04:00
/// Create a new release
2023-12-16 19:42:38 -05:00
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,
},
2024-08-04 11:12:02 -04:00
/// Edit a release's info
2023-12-16 19:42:38 -05:00
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>,
},
2024-08-04 11:12:02 -04:00
/// Delete a release
2023-12-16 19:42:38 -05:00
Delete {
name: String,
#[clap(long, short = 't')]
by_tag: bool,
},
2024-08-04 11:12:02 -04:00
/// List all the releases on a repo
2023-12-16 19:42:38 -05:00
List {
#[clap(long, short = 'p')]
include_prerelease: bool,
#[clap(long, short = 'd')]
include_draft: bool,
},
2024-08-04 11:12:02 -04:00
/// View a release's info
2023-12-16 19:42:38 -05:00
View {
name: String,
#[clap(long, short = 't')]
by_tag: bool,
},
2024-08-04 11:12:02 -04:00
/// Open a release in your browser
Browse { name: Option<String> },
/// Commands on a release's attached files
2023-12-16 19:42:38 -05:00
#[clap(subcommand)]
Asset(AssetCommand),
}
#[derive(Subcommand, Clone, Debug)]
pub enum AssetCommand {
2024-08-04 11:12:02 -04:00
/// Create a new attachment on a release
2023-12-16 19:42:38 -05:00
Create {
release: String,
path: std::path::PathBuf,
name: Option<String>,
},
2024-08-04 11:12:02 -04:00
/// 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
2023-12-16 19:42:38 -05:00
Download {
release: String,
asset: String,
#[clap(long, short)]
output: Option<std::path::PathBuf>,
},
}
impl ReleaseCommand {
2024-05-31 15:25:59 -04:00
pub async fn run(self, keys: &mut KeyInfo, remote_name: Option<&str>) -> eyre::Result<()> {
2024-09-05 13:16:05 -04:00
let repo = RepoInfo::get_current(
remote_name,
self.repo.as_ref(),
self.remote.as_deref(),
&keys,
)?;
let api = keys.get_api(repo.host_url()).await?;
2024-04-17 15:58:45 -04:00
let repo = repo
.name()
.ok_or_eyre("couldn't get repo name, try specifying with --repo")?;
match self.command {
ReleaseSubcommand::Create {
2023-12-16 19:42:38 -05:00
name,
create_tag,
tag,
attach,
body,
branch,
draft,
prerelease,
} => {
create_release(
repo, &api, name, create_tag, tag, attach, body, branch, draft, prerelease,
2023-12-16 19:42:38 -05:00
)
.await?
}
ReleaseSubcommand::Edit {
2023-12-16 19:42:38 -05:00
name,
rename,
tag,
body,
draft,
prerelease,
} => edit_release(repo, &api, name, rename, tag, body, draft, prerelease).await?,
2024-04-17 15:58:45 -04:00
ReleaseSubcommand::Delete { name, by_tag } => {
delete_release(repo, &api, name, by_tag).await?
2024-04-17 15:58:45 -04:00
}
ReleaseSubcommand::List {
2023-12-16 19:42:38 -05:00
include_prerelease,
include_draft,
} => list_releases(repo, &api, include_prerelease, include_draft).await?,
2024-04-17 15:58:45 -04:00
ReleaseSubcommand::View { name, by_tag } => {
view_release(repo, &api, name, by_tag).await?
2024-04-17 15:58:45 -04:00
}
ReleaseSubcommand::Browse { name } => browse_release(repo, &api, name).await?,
ReleaseSubcommand::Asset(subcommand) => match subcommand {
2023-12-16 19:42:38 -05:00
AssetCommand::Create {
release,
path,
name,
} => create_asset(repo, &api, release, path, name).await?,
2023-12-16 19:42:38 -05:00
AssetCommand::Delete { release, asset } => {
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?,
2023-12-16 19:42:38 -05:00
},
}
Ok(())
}
}
async fn create_release(
repo: &RepoName,
2023-12-16 19:42:38 -05:00
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 {
hide_archive_links: None,
2023-12-16 19:42:38 -05:00
body,
2024-03-21 17:48:39 -04:00
draft: Some(draft),
2024-06-10 13:27:43 -04:00
name: Some(name.clone()),
2024-03-21 17:48:39 -04:00
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"))? as u64;
2024-03-21 17:48:39 -04:00
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?;
}
2024-06-10 13:27:43 -04:00
println!("Created release {name}");
2023-12-16 19:42:38 -05:00
Ok(())
}
async fn edit_release(
repo: &RepoName,
2023-12-16 19:42:38 -05:00
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 {
hide_archive_links: None,
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"))? as u64;
2024-03-21 17:48:39 -04:00
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: &RepoName,
2023-12-16 19:42:38 -05:00
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
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: &RepoName,
2023-12-16 19:42:38 -05:00
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-06-08 13:34:05 -04:00
let SpecialRender { bullet, .. } = crate::special_render();
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!();
println!("{}", crate::markdown(body));
2023-12-16 19:42:38 -05:00
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"))?;
2024-05-07 14:28:17 -04:00
println!("{bullet} {}", name);
2023-12-16 19:42:38 -05:00
}
2024-05-07 14:28:17 -04:00
println!("{bullet} source.zip");
println!("{bullet} source.tar.gz");
2023-12-16 19:42:38 -05:00
}
Ok(())
}
async fn browse_release(repo: &RepoName, api: &Forgejo, name: Option<String>) -> eyre::Result<()> {
2023-12-16 19:42:38 -05:00
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"))?;
2024-11-06 09:22:57 +01:00
open::that_detached(html_url.as_str()).wrap_err("Failed to open URL")?;
2023-12-16 19:42:38 -05:00
}
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");
2024-11-06 09:22:57 +01:00
open::that_detached(html_url.as_str()).wrap_err("Failed to open URL")?;
2023-12-16 19:42:38 -05:00
}
}
Ok(())
}
async fn create_asset(
repo: &RepoName,
2023-12-16 19:42:38 -05:00
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"))? as u64;
2024-03-21 17:48:39 -04:00
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?;
2024-06-10 13:27:43 -04:00
println!("Added attachment `{}` to {}", asset, release);
2023-12-16 19:42:38 -05:00
Ok(())
}
async fn delete_asset(
repo: &RepoName,
2023-12-16 19:42:38 -05:00
api: &Forgejo,
2024-06-10 13:27:43 -04:00
release_name: String,
asset_name: String,
2023-12-16 19:42:38 -05:00
) -> eyre::Result<()> {
2024-06-10 13:27:43 -04:00
let release = find_release(repo, api, &release_name).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-06-10 13:27:43 -04:00
.find(|a| a.name.as_ref() == Some(&asset_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"))? as u64;
2024-03-21 17:48:39 -04:00
let asset_id = asset
.id
.ok_or_else(|| eyre::eyre!("asset does not have id"))? as u64;
2024-03-21 17:48:39 -04:00
api.repo_delete_release_attachment(repo.owner(), repo.name(), release_id, asset_id)
2023-12-16 19:42:38 -05:00
.await?;
2024-06-10 13:27:43 -04:00
println!("Removed attachment `{}` from {}", asset_name, release_name);
2023-12-16 19:42:38 -05:00
Ok(())
}
async fn download_asset(
repo: &RepoName,
2023-12-16 19:42:38 -05:00
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"))?
as u64;
2024-03-21 17:48:39 -04:00
let asset_id = asset
.id
.ok_or_else(|| eyre::eyre!("asset does not have id"))?
as u64;
2024-03-21 17:48:39 -04:00
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
}
};
2024-06-10 13:27:43 -04:00
let real_output = output
2023-12-16 19:42:38 -05:00
.as_deref()
.unwrap_or_else(|| std::path::Path::new(&asset));
tokio::fs::OpenOptions::new()
.create_new(true)
.write(true)
2024-06-10 13:27:43 -04:00
.open(real_output)
2023-12-16 19:42:38 -05:00
.await?
.write_all(file.as_ref())
.await?;
2024-06-10 13:27:43 -04:00
if output.is_some() {
println!("Downloaded {asset} into {}", real_output.display());
} else {
println!("Downloaded {asset}");
}
2023-12-16 19:42:38 -05:00
Ok(())
}
async fn find_release(
repo: &RepoName,
2023-12-16 19:42:38 -05:00
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,
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: &RepoName,
2023-12-16 19:42:38 -05:00
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"))? as u64;
2024-03-21 17:48:39 -04:00
api.repo_delete_release(repo.owner(), repo.name(), id)
.await?;
2023-12-16 19:42:38 -05:00
}
Ok(())
}