use serde::Deserialize;
use crate::api::client::{ApiError, GitHub};
use crate::api::model::RepoId;
use crate::schema::schema;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NewRelease {
pub repo: RepoId,
pub tag: String,
pub target: Option<String>,
pub name: String,
pub notes: String,
pub draft: bool,
pub prerelease: bool,
pub latest: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct Made {
pub tag_name: String,
pub draft: bool,
pub html_url: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct Notes {
pub name: String,
pub body: String,
}
impl GitHub {
pub async fn create_release(&self, new: &NewRelease) -> Result<Made, ApiError> {
let mut body = serde_json::json!({
"tag_name": new.tag,
"name": new.name,
"body": new.notes,
"draft": new.draft,
"prerelease": new.prerelease,
"make_latest": if new.latest && !new.prerelease { "true" } else { "false" },
});
if let (Some(target), Some(map)) = (&new.target, body.as_object_mut()) {
map.insert("target_commitish".into(), target.clone().into());
}
let path = format!("/repos/{}/releases", new.repo);
self.rest_write_json(http::Method::POST, &path, Some(&body))
.await
}
pub async fn update_release(&self, id: u64, new: &NewRelease) -> Result<Made, ApiError> {
let mut body = serde_json::json!({
"tag_name": new.tag,
"name": new.name,
"body": new.notes,
"draft": new.draft,
"prerelease": new.prerelease,
});
if let Some(map) = body.as_object_mut() {
if !new.draft {
let latest = new.latest && !new.prerelease;
map.insert("make_latest".into(), latest.to_string().into());
}
if let Some(target) = &new.target {
map.insert("target_commitish".into(), target.clone().into());
}
}
let path = format!("/repos/{}/releases/{id}", new.repo);
self.rest_write_json(http::Method::PATCH, &path, Some(&body))
.await
}
pub async fn delete_release(&self, repo: &RepoId, id: u64) -> Result<(), ApiError> {
let path = format!("/repos/{repo}/releases/{id}");
self.rest_write(http::Method::DELETE, &path, None).await
}
pub async fn delete_tag(&self, repo: &RepoId, tag: &str) -> Result<(), ApiError> {
let tag = crate::ui::pages::url::encode_path(tag);
let path = format!("/repos/{repo}/git/refs/tags/{tag}");
self.rest_write(http::Method::DELETE, &path, None).await
}
async fn asset_id(&self, repo: &RepoId, id: u64, name: &str) -> Result<u64, ApiError> {
#[derive(Deserialize)]
struct Listed {
id: u64,
name: String,
}
for page in 1..=10 {
let path = format!("/repos/{repo}/releases/{id}/assets?per_page=100&page={page}");
let listed = self.rest_get(&path, format!("{repo}'s release assets"));
let listed: Vec<Listed> = serde_json::from_str(&listed.await?)?;
if let Some(found) = listed.iter().find(|a| a.name == name) {
return Ok(found.id);
}
if listed.len() < 100 {
break;
}
}
Err(ApiError::NotFound(format!("{name} on the release")))
}
pub async fn download_asset(
&self,
repo: &RepoId,
id: u64,
name: &str,
dest: &std::path::Path,
replace: bool,
) -> Result<u64, ApiError> {
let asset = self.asset_id(repo, id, name).await?;
let path = format!("/repos/{repo}/releases/assets/{asset}");
self.download_as(&path, "application/octet-stream", dest, replace)
.await
}
pub async fn delete_asset(&self, repo: &RepoId, id: u64, name: &str) -> Result<(), ApiError> {
let asset = self.asset_id(repo, id, name).await?;
let path = format!("/repos/{repo}/releases/assets/{asset}");
self.rest_write(http::Method::DELETE, &path, None).await
}
pub async fn upload_asset(
&self,
repo: &RepoId,
id: u64,
file: &std::path::Path,
name: &str,
) -> Result<(), ApiError> {
#[derive(Deserialize)]
struct Uploads {
upload_url: String,
}
let path = format!("/repos/{repo}/releases/{id}");
let at = self.rest_get(&path, format!("{repo}'s release"));
let at: Uploads = serde_json::from_str(&at.await?)?;
let base = at.upload_url.split('{').next().unwrap_or_default();
let encoded: String = url::form_urlencoded::byte_serialize(name.as_bytes()).collect();
let url = format!("{base}?name={encoded}");
self.upload(&url, content_type(name), file).await.map(drop)
}
pub async fn release_notes(
&self,
repo: &RepoId,
tag: &str,
target: Option<&str>,
) -> Result<Notes, ApiError> {
let mut body = serde_json::json!({ "tag_name": tag });
if let (Some(target), Some(map)) = (target, body.as_object_mut()) {
map.insert("target_commitish".into(), target.into());
}
let path = format!("/repos/{repo}/releases/generate-notes");
self.rest_write_json(http::Method::POST, &path, Some(&body))
.await
}
pub async fn tag_look(&self, repo: &RepoId, tag: &str) -> Result<TagLook, ApiError> {
use cynic::QueryBuilder;
let op = TagQuery::build(TagVariables {
owner: repo.owner.clone(),
name: repo.name.clone(),
qualified: format!("refs/tags/{tag}"),
tag: tag.to_owned(),
});
self.find(op, repo, |q| {
let r = q.repository?;
Some(TagLook {
exists: r.tag.is_some(),
release: r.release.map(|r| (r.is_draft, r.url.0)),
})
})
.await
}
}
pub fn content_type(name: &str) -> &'static str {
let ext = name.rsplit_once('.').map(|(_, e)| e.to_ascii_lowercase());
match ext.as_deref() {
Some("gz" | "tgz") => "application/gzip",
Some("zip") => "application/zip",
Some("xz") => "application/x-xz",
Some("zst") => "application/zstd",
Some("bz2") => "application/x-bzip2",
Some("tar") => "application/x-tar",
Some("json") => "application/json",
Some("txt" | "sha256" | "sha512" | "asc" | "sig") => "text/plain",
Some("pdf") => "application/pdf",
Some("png") => "image/png",
Some("jpg" | "jpeg") => "image/jpeg",
Some("deb") => "application/vnd.debian.binary-package",
Some("rpm") => "application/x-rpm",
_ => "application/octet-stream",
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TagLook {
pub exists: bool,
pub release: Option<(bool, String)>,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct TagVariables {
pub owner: String,
pub name: String,
pub qualified: String,
pub tag: String,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "TagVariables")]
pub struct TagQuery {
#[arguments(owner: $owner, name: $name)]
pub repository: Option<TagRepo>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Repository", variables = "TagVariables")]
pub struct TagRepo {
#[arguments(qualifiedName: $qualified)]
#[cynic(rename = "ref")]
pub tag: Option<TagRef>,
#[arguments(tagName: $tag)]
pub release: Option<TagRelease>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Ref")]
pub struct TagRef {
pub name: String,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Release")]
pub struct TagRelease {
pub is_draft: bool,
pub url: crate::api::queries::Uri,
}