use db::conn::sea_orm::*; use db::repo::repo_release as release_entity; use time::OffsetDateTime; use uuid::Uuid; use crate::error::{RepoError, RepoResult}; use crate::features::RepoService; use crate::path::validate_tag_name; use super::types::{ CreateReleaseOption, EditReleaseOption, ListReleaseQuery, ReleasePage, ReleaseView, }; fn require_authenticated(actor: Uuid) -> RepoResult<()> { if actor.is_nil() { return Err(RepoError::Unauthorized("authentication required".into())); } Ok(()) } async fn load_release( svc: &RepoService, repo_id: Uuid, release_id: Uuid, ) -> RepoResult { let db = svc.db.reader(); let release = release_entity::Entity::find_by_id(release_id) .one(&db) .await? .ok_or_else(|| RepoError::NotFound(format!("release {release_id}")))?; if release.repo_id != repo_id { return Err(RepoError::NotFound(format!("release {release_id}"))); } Ok(release) } pub async fn list_releases( svc: &RepoService, actor: Uuid, repo_id: Uuid, q: ListReleaseQuery, ) -> RepoResult { let _ = actor; let limit = q.limit.unwrap_or(svc.config.default_pagination); let offset = q.page.unwrap_or(0).saturating_mul(limit); let limit = std::cmp::min(limit, svc.config.max_pagination); let db = svc.db.reader(); let mut query = release_entity::Entity::find() .filter(release_entity::Column::RepoId.eq(repo_id)) .filter(release_entity::Column::IsTag.eq(false)); if let Some(d) = q.draft { query = query.filter(release_entity::Column::IsDraft.eq(d)); } if let Some(p) = q.prerelease { query = query.filter(release_entity::Column::IsPrerelease.eq(p)); } let total = query.clone().count(&db).await?; let items = query .order_by_desc(release_entity::Column::CreatedAt) .limit(Some(limit)) .offset(Some(offset)) .all(&db) .await? .into_iter() .map(ReleaseView::from) .collect(); Ok(ReleasePage { items, total: total as i64, }) } pub async fn get_release( svc: &RepoService, actor: Uuid, repo_id: Uuid, release_id: Uuid, ) -> RepoResult { let _ = actor; let release = load_release(svc, repo_id, release_id).await?; Ok(ReleaseView::from(release)) } pub async fn get_latest_release( svc: &RepoService, actor: Uuid, repo_id: Uuid, ) -> RepoResult { let _ = actor; let db = svc.db.reader(); let release = release_entity::Entity::find() .filter(release_entity::Column::RepoId.eq(repo_id)) .filter(release_entity::Column::IsTag.eq(false)) .filter(release_entity::Column::IsDraft.eq(false)) .order_by_desc(release_entity::Column::CreatedAt) .one(&db) .await? .ok_or_else(|| RepoError::NotFound("no published release".into()))?; Ok(ReleaseView::from(release)) } pub async fn get_release_by_tag( svc: &RepoService, actor: Uuid, repo_id: Uuid, tag: &str, ) -> RepoResult { let _ = actor; let db = svc.db.reader(); let release = release_entity::Entity::find() .filter(release_entity::Column::RepoId.eq(repo_id)) .filter(release_entity::Column::TagName.eq(tag)) .one(&db) .await? .ok_or_else(|| RepoError::NotFound(format!("release for tag {tag}")))?; if release.is_tag { return Err(RepoError::NotFound(format!("release for tag {tag}"))); } Ok(ReleaseView::from(release)) } pub async fn create_release( svc: &RepoService, actor: Uuid, repo_id: Uuid, input: CreateReleaseOption, ) -> RepoResult { require_authenticated(actor)?; let tag = validate_tag_name(&input.tag_name, svc.config.max_tag_name_len)?; if let Some(ref c) = input.content { if c.len() > svc.config.max_release_body_len { return Err(RepoError::BadRequest(format!( "release content too long (max {})", svc.config.max_release_body_len ))); } } let _ = input.tag_message; let db = svc.db.writer(); let now = OffsetDateTime::now_utc(); let active: release_entity::ActiveModel = release_entity::ActiveModel { id: Set(Uuid::new_v4()), repo_id: Set(repo_id), publisher_id: Set(actor), publisher_name: Set(String::new()), tag_name: Set(tag.clone()), lower_tag_name: Set(tag.to_lowercase()), target: Set(input.target.unwrap_or_else(|| "main".to_string())), title: Set(input.title), content: Set(input.content), sha: Set(String::new()), num_commits: Set(0), is_draft: Set(input.is_draft.unwrap_or(false)), is_prerelease: Set(input.is_prerelease.unwrap_or(false)), is_tag: Set(false), created_at: Set(now), updated_at: Set(now), }; let saved = active.insert(&db).await?; Ok(ReleaseView::from(saved)) } pub async fn edit_release( svc: &RepoService, actor: Uuid, repo_id: Uuid, release_id: Uuid, input: EditReleaseOption, ) -> RepoResult { require_authenticated(actor)?; let release = load_release(svc, repo_id, release_id).await?; let db = svc.db.writer(); let mut active: release_entity::ActiveModel = release.into(); if let Some(t) = input.tag_name { let validated = validate_tag_name(&t, svc.config.max_tag_name_len)?; active.tag_name = Set(validated.clone()); active.lower_tag_name = Set(validated.to_lowercase()); } if let Some(t) = input.target { active.target = Set(t); } if let Some(t) = input.title { active.title = Set(t); } if let Some(c) = input.content { if let Some(ref body) = c { if body.len() > svc.config.max_release_body_len { return Err(RepoError::BadRequest(format!( "release content too long (max {})", svc.config.max_release_body_len ))); } } active.content = Set(c); } if let Some(d) = input.is_draft { active.is_draft = Set(d); } if let Some(p) = input.is_prerelease { active.is_prerelease = Set(p); } active.updated_at = Set(OffsetDateTime::now_utc()); let saved = active.update(&db).await?; Ok(ReleaseView::from(saved)) } pub async fn delete_release( svc: &RepoService, actor: Uuid, repo_id: Uuid, release_id: Uuid, ) -> RepoResult<()> { require_authenticated(actor)?; let _ = load_release(svc, repo_id, release_id).await?; let db = svc.db.writer(); release_entity::Entity::delete_by_id(release_id) .exec(&db) .await?; Ok(()) } pub async fn delete_release_by_tag( svc: &RepoService, actor: Uuid, repo_id: Uuid, tag: &str, ) -> RepoResult<()> { require_authenticated(actor)?; let db = svc.db.writer(); release_entity::Entity::delete_many() .filter(release_entity::Column::RepoId.eq(repo_id)) .filter(release_entity::Column::TagName.eq(tag)) .filter(release_entity::Column::IsTag.eq(false)) .exec(&db) .await?; Ok(()) }