Centralize Gitea domain workflows

This commit is contained in:
Georg Bauer
2026-07-31 16:44:16 +02:00
parent b468a2670c
commit f38c3c4939
19 changed files with 2248 additions and 1624 deletions

View File

@@ -0,0 +1,238 @@
use serde::Serialize;
use crate::{
Client, Error, Method, Result,
domain::{DEFAULT_PAGE_SIZE, IssueQuery, MilestoneDetails, MilestoneDraft, Page, RepositoryId},
models,
};
use gitea_openapi::apis;
impl Client {
pub async fn milestones_page(
&self,
repository: &RepositoryId,
page: i32,
limit: i32,
) -> Result<Page<models::Milestone>> {
if page < 1 || limit < 1 {
return Err(Error::InvalidInput(
"milestone page and limit must be positive".into(),
));
}
apis::issue_api::issue_get_milestones_list(
&self.configuration(),
&repository.owner,
&repository.repository,
Some("all"),
None,
Some(page),
Some(limit),
)
.await
.map(|items| Page::from_items(items, limit))
.map_err(Error::generated)
}
pub async fn milestones(&self, repository: &RepositoryId) -> Result<Vec<models::Milestone>> {
let mut milestones = Vec::new();
for page in 1.. {
let batch = self.milestones_page(repository, page, 100).await?;
milestones.extend(batch.items);
if !batch.has_more {
break;
}
}
Ok(milestones)
}
pub async fn resolve_milestone_id(&self, repository: &RepositoryId, name: &str) -> Result<i64> {
self.milestones(repository)
.await?
.into_iter()
.find(|milestone| milestone.title.as_deref() == Some(name))
.and_then(|milestone| milestone.id)
.ok_or_else(|| Error::InvalidInput(format!("unknown milestone {name:?}")))
}
pub async fn milestone(&self, repository: &RepositoryId, id: i64) -> Result<models::Milestone> {
if id < 1 {
return Err(Error::InvalidInput(
"milestone id must be a positive integer".into(),
));
}
apis::issue_api::issue_get_milestone(
&self.configuration(),
&repository.owner,
&repository.repository,
&id.to_string(),
)
.await
.map_err(Error::generated)
}
pub async fn milestone_details(
&self,
repository: &RepositoryId,
id: i64,
page: i32,
) -> Result<MilestoneDetails> {
let milestone = self.milestone(repository, id).await?;
let base = IssueQuery {
state: "all".into(),
milestones: milestone.title.clone(),
page,
limit: DEFAULT_PAGE_SIZE,
..Default::default()
};
let pulls_query = IssueQuery {
kind: "pulls".into(),
..base.clone()
};
let issues = self.issues(repository, &base);
let pulls = self.issues(repository, &pulls_query);
let (issues, pulls) = tokio::try_join!(issues, pulls)?;
let has_more = issues.has_more || pulls.has_more;
let mut pulls = pulls.items;
for pull in &mut pulls {
pull.repository.get_or_insert_with(|| {
Box::new(models::RepositoryMeta {
name: Some(repository.repository.clone()),
owner: Some(repository.owner.clone()),
..Default::default()
})
});
}
Ok(MilestoneDetails {
milestone,
has_more,
issues: issues.items,
pulls,
})
}
pub async fn create_milestone(
&self,
repository: &RepositoryId,
option: models::CreateMilestoneOption,
) -> Result<models::Milestone> {
if option
.title
.as_deref()
.unwrap_or_default()
.trim()
.is_empty()
{
return Err(Error::InvalidInput(
"milestone title must not be empty".into(),
));
}
apis::issue_api::issue_create_milestone(
&self.configuration(),
&repository.owner,
&repository.repository,
Some(option),
)
.await
.map_err(Error::generated)
}
pub async fn edit_milestone(
&self,
repository: &RepositoryId,
id: &str,
option: models::EditMilestoneOption,
) -> Result<models::Milestone> {
apis::issue_api::issue_edit_milestone(
&self.configuration(),
&repository.owner,
&repository.repository,
id,
Some(option),
)
.await
.map_err(Error::generated)
}
pub async fn save_milestone(
&self,
repository: &RepositoryId,
id: Option<i64>,
draft: MilestoneDraft,
) -> Result<models::Milestone> {
if draft.title.trim().is_empty() {
return Err(Error::InvalidInput(
"milestone title must not be empty".into(),
));
}
let endpoint = match id {
Some(id) if id > 0 => format!(
"repos/{}/{}/milestones/{id}",
apis::urlencode(&repository.owner),
apis::urlencode(&repository.repository)
),
Some(_) => {
return Err(Error::InvalidInput(
"milestone id must be a positive integer".into(),
));
}
None => format!(
"repos/{}/{}/milestones",
apis::urlencode(&repository.owner),
apis::urlencode(&repository.repository)
),
};
let request = self
.request(
if id.is_some() {
Method::PATCH
} else {
Method::POST
},
&endpoint,
)?
.json(&MilestoneRequest {
title: draft.title,
description: draft.description,
due_on: draft.due_on,
});
self.execute(request)
.await?
.json()
.await
.map_err(Into::into)
}
pub async fn delete_milestone(&self, repository: &RepositoryId, id: &str) -> Result<()> {
apis::issue_api::issue_delete_milestone(
&self.configuration(),
&repository.owner,
&repository.repository,
id,
)
.await
.map_err(Error::generated)
}
}
#[derive(Serialize)]
struct MilestoneRequest {
title: String,
description: String,
due_on: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn milestone_request_preserves_due_date_clear() {
let value = serde_json::to_value(MilestoneRequest {
title: "Release".into(),
description: String::new(),
due_on: None,
})
.unwrap();
assert!(value.get("due_on").unwrap().is_null());
}
}