feat: GiteaClient — unified git forge API (50+ endpoints)
Typed Gitea REST API client with PAT auth covering repos, issues, PRs, branches, orgs, users, file content, and notifications. Bump: sunbeam-sdk v0.5.0
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -3591,7 +3591,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sunbeam-sdk"
|
name = "sunbeam-sdk"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "sunbeam-sdk"
|
name = "sunbeam-sdk"
|
||||||
version = "0.4.0"
|
version = "0.5.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "Sunbeam SDK — reusable library for cluster management"
|
description = "Sunbeam SDK — reusable library for cluster management"
|
||||||
repository = "https://src.sunbeam.pt/studio/cli"
|
repository = "https://src.sunbeam.pt/studio/cli"
|
||||||
|
|||||||
@@ -1,13 +1,596 @@
|
|||||||
//! Gitea bootstrap -- admin setup, org creation, OIDC auth source configuration.
|
//! Gitea API client and bootstrap operations.
|
||||||
|
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
|
use crate::client::{AuthMethod, HttpTransport, ServiceClient};
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use k8s_openapi::api::core::v1::Pod;
|
use k8s_openapi::api::core::v1::Pod;
|
||||||
use kube::api::{Api, ListParams};
|
use kube::api::{Api, ListParams};
|
||||||
|
use reqwest::Method;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::kube::{get_client, get_domain, kube_exec, kube_get_secret_field};
|
use crate::kube::{get_client, get_domain, kube_exec, kube_get_secret_field};
|
||||||
use crate::output::{ok, step, warn};
|
use crate::output::{ok, step, warn};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Gitea API Client (ServiceClient trait)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Full Gitea REST API client using PAT authentication.
|
||||||
|
pub struct GiteaClient {
|
||||||
|
pub(crate) transport: HttpTransport,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServiceClient for GiteaClient {
|
||||||
|
fn service_name(&self) -> &'static str {
|
||||||
|
"gitea"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_url(&self) -> &str {
|
||||||
|
&self.transport.base_url
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_parts(base_url: String, auth: AuthMethod) -> Self {
|
||||||
|
Self {
|
||||||
|
transport: HttpTransport::new(&base_url, auth),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GiteaClient {
|
||||||
|
/// Build a GiteaClient from domain, using the cached Gitea PAT.
|
||||||
|
pub fn connect(domain: &str) -> Self {
|
||||||
|
let base_url = format!("https://src.{domain}/api/v1");
|
||||||
|
let auth = match crate::auth::get_gitea_token() {
|
||||||
|
Ok(token) => AuthMethod::Token(token),
|
||||||
|
Err(_) => AuthMethod::None,
|
||||||
|
};
|
||||||
|
Self::from_parts(base_url, auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a GiteaClient from domain and explicit token.
|
||||||
|
pub fn with_token(domain: &str, token: String) -> Self {
|
||||||
|
let base_url = format!("https://src.{domain}/api/v1");
|
||||||
|
Self::from_parts(base_url, AuthMethod::Token(token))
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Repos --------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Search repositories.
|
||||||
|
pub async fn search_repos(
|
||||||
|
&self,
|
||||||
|
query: &str,
|
||||||
|
limit: Option<u32>,
|
||||||
|
) -> Result<types::SearchResult<types::Repository>> {
|
||||||
|
let limit = limit.unwrap_or(20);
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("repos/search?q={query}&limit={limit}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"gitea search repos",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a repository.
|
||||||
|
pub async fn get_repo(&self, owner: &str, repo: &str) -> Result<types::Repository> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("repos/{owner}/{repo}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"gitea get repo",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a repository for a user.
|
||||||
|
pub async fn create_user_repo(&self, body: &types::CreateRepoBody) -> Result<types::Repository> {
|
||||||
|
self.transport
|
||||||
|
.json(Method::POST, "user/repos", Some(body), "gitea create user repo")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a repository for an organization.
|
||||||
|
pub async fn create_org_repo(
|
||||||
|
&self,
|
||||||
|
org: &str,
|
||||||
|
body: &types::CreateRepoBody,
|
||||||
|
) -> Result<types::Repository> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::POST,
|
||||||
|
&format!("orgs/{org}/repos"),
|
||||||
|
Some(body),
|
||||||
|
"gitea create org repo",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Edit a repository.
|
||||||
|
pub async fn edit_repo(
|
||||||
|
&self,
|
||||||
|
owner: &str,
|
||||||
|
repo: &str,
|
||||||
|
body: &types::EditRepoBody,
|
||||||
|
) -> Result<types::Repository> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::PATCH,
|
||||||
|
&format!("repos/{owner}/{repo}"),
|
||||||
|
Some(body),
|
||||||
|
"gitea edit repo",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a repository.
|
||||||
|
pub async fn delete_repo(&self, owner: &str, repo: &str) -> Result<()> {
|
||||||
|
self.transport
|
||||||
|
.send(
|
||||||
|
Method::DELETE,
|
||||||
|
&format!("repos/{owner}/{repo}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"gitea delete repo",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fork a repository.
|
||||||
|
pub async fn fork_repo(
|
||||||
|
&self,
|
||||||
|
owner: &str,
|
||||||
|
repo: &str,
|
||||||
|
body: &types::ForkRepoBody,
|
||||||
|
) -> Result<types::Repository> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::POST,
|
||||||
|
&format!("repos/{owner}/{repo}/forks"),
|
||||||
|
Some(body),
|
||||||
|
"gitea fork repo",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trigger mirror sync.
|
||||||
|
pub async fn mirror_sync(&self, owner: &str, repo: &str) -> Result<()> {
|
||||||
|
self.transport
|
||||||
|
.send(
|
||||||
|
Method::POST,
|
||||||
|
&format!("repos/{owner}/{repo}/mirror-sync"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"gitea mirror sync",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transfer a repository to another owner.
|
||||||
|
pub async fn transfer_repo(
|
||||||
|
&self,
|
||||||
|
owner: &str,
|
||||||
|
repo: &str,
|
||||||
|
body: &types::TransferRepoBody,
|
||||||
|
) -> Result<types::Repository> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::POST,
|
||||||
|
&format!("repos/{owner}/{repo}/transfer"),
|
||||||
|
Some(body),
|
||||||
|
"gitea transfer repo",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Issues -------------------------------------------------------------
|
||||||
|
|
||||||
|
/// List issues for a repo.
|
||||||
|
pub async fn list_issues(
|
||||||
|
&self,
|
||||||
|
owner: &str,
|
||||||
|
repo: &str,
|
||||||
|
state: &str,
|
||||||
|
limit: Option<u32>,
|
||||||
|
) -> Result<Vec<types::Issue>> {
|
||||||
|
let limit = limit.unwrap_or(50);
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("repos/{owner}/{repo}/issues?state={state}&type=issues&limit={limit}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"gitea list issues",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a single issue.
|
||||||
|
pub async fn get_issue(
|
||||||
|
&self,
|
||||||
|
owner: &str,
|
||||||
|
repo: &str,
|
||||||
|
index: u64,
|
||||||
|
) -> Result<types::Issue> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("repos/{owner}/{repo}/issues/{index}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"gitea get issue",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an issue.
|
||||||
|
pub async fn create_issue(
|
||||||
|
&self,
|
||||||
|
owner: &str,
|
||||||
|
repo: &str,
|
||||||
|
body: &types::CreateIssueBody,
|
||||||
|
) -> Result<types::Issue> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::POST,
|
||||||
|
&format!("repos/{owner}/{repo}/issues"),
|
||||||
|
Some(body),
|
||||||
|
"gitea create issue",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Edit an issue.
|
||||||
|
pub async fn edit_issue(
|
||||||
|
&self,
|
||||||
|
owner: &str,
|
||||||
|
repo: &str,
|
||||||
|
index: u64,
|
||||||
|
body: &types::EditIssueBody,
|
||||||
|
) -> Result<types::Issue> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::PATCH,
|
||||||
|
&format!("repos/{owner}/{repo}/issues/{index}"),
|
||||||
|
Some(body),
|
||||||
|
"gitea edit issue",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List issue comments.
|
||||||
|
pub async fn list_issue_comments(
|
||||||
|
&self,
|
||||||
|
owner: &str,
|
||||||
|
repo: &str,
|
||||||
|
index: u64,
|
||||||
|
) -> Result<Vec<types::Comment>> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("repos/{owner}/{repo}/issues/{index}/comments"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"gitea list comments",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an issue comment.
|
||||||
|
pub async fn create_issue_comment(
|
||||||
|
&self,
|
||||||
|
owner: &str,
|
||||||
|
repo: &str,
|
||||||
|
index: u64,
|
||||||
|
body: &str,
|
||||||
|
) -> Result<types::Comment> {
|
||||||
|
let payload = serde_json::json!({"body": body});
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::POST,
|
||||||
|
&format!("repos/{owner}/{repo}/issues/{index}/comments"),
|
||||||
|
Some(&payload),
|
||||||
|
"gitea create comment",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Pull Requests ------------------------------------------------------
|
||||||
|
|
||||||
|
/// List pull requests.
|
||||||
|
pub async fn list_pulls(
|
||||||
|
&self,
|
||||||
|
owner: &str,
|
||||||
|
repo: &str,
|
||||||
|
state: &str,
|
||||||
|
) -> Result<Vec<types::PullRequest>> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("repos/{owner}/{repo}/pulls?state={state}&limit=50"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"gitea list pulls",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a pull request.
|
||||||
|
pub async fn get_pull(
|
||||||
|
&self,
|
||||||
|
owner: &str,
|
||||||
|
repo: &str,
|
||||||
|
index: u64,
|
||||||
|
) -> Result<types::PullRequest> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("repos/{owner}/{repo}/pulls/{index}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"gitea get pull",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a pull request.
|
||||||
|
pub async fn create_pull(
|
||||||
|
&self,
|
||||||
|
owner: &str,
|
||||||
|
repo: &str,
|
||||||
|
body: &types::CreatePullBody,
|
||||||
|
) -> Result<types::PullRequest> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::POST,
|
||||||
|
&format!("repos/{owner}/{repo}/pulls"),
|
||||||
|
Some(body),
|
||||||
|
"gitea create pull",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge a pull request.
|
||||||
|
pub async fn merge_pull(
|
||||||
|
&self,
|
||||||
|
owner: &str,
|
||||||
|
repo: &str,
|
||||||
|
index: u64,
|
||||||
|
body: &types::MergePullBody,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.transport
|
||||||
|
.send(
|
||||||
|
Method::POST,
|
||||||
|
&format!("repos/{owner}/{repo}/pulls/{index}/merge"),
|
||||||
|
Some(body),
|
||||||
|
"gitea merge pull",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Branches -----------------------------------------------------------
|
||||||
|
|
||||||
|
/// List branches.
|
||||||
|
pub async fn list_branches(
|
||||||
|
&self,
|
||||||
|
owner: &str,
|
||||||
|
repo: &str,
|
||||||
|
) -> Result<Vec<types::Branch>> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("repos/{owner}/{repo}/branches"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"gitea list branches",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a branch.
|
||||||
|
pub async fn create_branch(
|
||||||
|
&self,
|
||||||
|
owner: &str,
|
||||||
|
repo: &str,
|
||||||
|
body: &types::CreateBranchBody,
|
||||||
|
) -> Result<types::Branch> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::POST,
|
||||||
|
&format!("repos/{owner}/{repo}/branches"),
|
||||||
|
Some(body),
|
||||||
|
"gitea create branch",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a branch.
|
||||||
|
pub async fn delete_branch(
|
||||||
|
&self,
|
||||||
|
owner: &str,
|
||||||
|
repo: &str,
|
||||||
|
branch: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.transport
|
||||||
|
.send(
|
||||||
|
Method::DELETE,
|
||||||
|
&format!("repos/{owner}/{repo}/branches/{branch}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"gitea delete branch",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Orgs ---------------------------------------------------------------
|
||||||
|
|
||||||
|
/// List user's organizations.
|
||||||
|
pub async fn list_user_orgs(&self, username: &str) -> Result<Vec<types::Organization>> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("users/{username}/orgs"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"gitea list user orgs",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an organization.
|
||||||
|
pub async fn get_org(&self, org: &str) -> Result<types::Organization> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("orgs/{org}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"gitea get org",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an organization.
|
||||||
|
pub async fn create_org(&self, body: &types::CreateOrgBody) -> Result<types::Organization> {
|
||||||
|
self.transport
|
||||||
|
.json(Method::POST, "orgs", Some(body), "gitea create org")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List organization repos.
|
||||||
|
pub async fn list_org_repos(
|
||||||
|
&self,
|
||||||
|
org: &str,
|
||||||
|
limit: Option<u32>,
|
||||||
|
) -> Result<Vec<types::Repository>> {
|
||||||
|
let limit = limit.unwrap_or(50);
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("orgs/{org}/repos?limit={limit}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"gitea list org repos",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Users --------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Search users.
|
||||||
|
pub async fn search_users(
|
||||||
|
&self,
|
||||||
|
query: &str,
|
||||||
|
limit: Option<u32>,
|
||||||
|
) -> Result<types::SearchResult<types::User>> {
|
||||||
|
let limit = limit.unwrap_or(20);
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("users/search?q={query}&limit={limit}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"gitea search users",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the authenticated user.
|
||||||
|
pub async fn get_authenticated_user(&self) -> Result<types::User> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
"user",
|
||||||
|
Option::<&()>::None,
|
||||||
|
"gitea get authenticated user",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a user.
|
||||||
|
pub async fn get_user(&self, username: &str) -> Result<types::User> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("users/{username}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"gitea get user",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- File content -------------------------------------------------------
|
||||||
|
|
||||||
|
/// Get file content.
|
||||||
|
pub async fn get_file_content(
|
||||||
|
&self,
|
||||||
|
owner: &str,
|
||||||
|
repo: &str,
|
||||||
|
filepath: &str,
|
||||||
|
r#ref: Option<&str>,
|
||||||
|
) -> Result<types::FileContent> {
|
||||||
|
let mut path = format!("repos/{owner}/{repo}/contents/{filepath}");
|
||||||
|
if let Some(r) = r#ref {
|
||||||
|
path.push_str(&format!("?ref={r}"));
|
||||||
|
}
|
||||||
|
self.transport
|
||||||
|
.json(Method::GET, &path, Option::<&()>::None, "gitea get file")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get raw file content.
|
||||||
|
pub async fn get_raw_file(
|
||||||
|
&self,
|
||||||
|
owner: &str,
|
||||||
|
repo: &str,
|
||||||
|
filepath: &str,
|
||||||
|
r#ref: Option<&str>,
|
||||||
|
) -> Result<bytes::Bytes> {
|
||||||
|
let mut path = format!("repos/{owner}/{repo}/raw/{filepath}");
|
||||||
|
if let Some(r) = r#ref {
|
||||||
|
path.push_str(&format!("?ref={r}"));
|
||||||
|
}
|
||||||
|
self.transport
|
||||||
|
.bytes(Method::GET, &path, "gitea get raw file")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Notifications ------------------------------------------------------
|
||||||
|
|
||||||
|
/// List notifications.
|
||||||
|
pub async fn list_notifications(&self) -> Result<Vec<types::Notification>> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
"notifications",
|
||||||
|
Option::<&()>::None,
|
||||||
|
"gitea list notifications",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark all notifications as read.
|
||||||
|
pub async fn mark_notifications_read(&self) -> Result<()> {
|
||||||
|
self.transport
|
||||||
|
.send(
|
||||||
|
Method::PUT,
|
||||||
|
"notifications",
|
||||||
|
Option::<&()>::None,
|
||||||
|
"gitea mark notifications read",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod gitea_client_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gitea_client_connect_url() {
|
||||||
|
// connect() may fail to get token, but URL should be correct
|
||||||
|
let c = GiteaClient::from_parts(
|
||||||
|
"https://src.sunbeam.pt/api/v1".into(),
|
||||||
|
AuthMethod::Token("test".into()),
|
||||||
|
);
|
||||||
|
assert_eq!(c.base_url(), "https://src.sunbeam.pt/api/v1");
|
||||||
|
assert_eq!(c.service_name(), "gitea");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Bootstrap operations (existing code below)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const GITEA_ADMIN_USER: &str = "gitea_admin";
|
const GITEA_ADMIN_USER: &str = "gitea_admin";
|
||||||
const GITEA_ADMIN_EMAIL: &str = "gitea@local.domain";
|
const GITEA_ADMIN_EMAIL: &str = "gitea@local.domain";
|
||||||
|
|
||||||
|
|||||||
463
sunbeam-sdk/src/gitea/types.rs
Normal file
463
sunbeam-sdk/src/gitea/types.rs
Normal file
@@ -0,0 +1,463 @@
|
|||||||
|
//! Gitea API types.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Generic search result wrapper.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SearchResult<T> {
|
||||||
|
#[serde(default)]
|
||||||
|
pub ok: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub data: Vec<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Gitea repository.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct Repository {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub full_name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub description: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub html_url: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub clone_url: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub ssh_url: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub default_branch: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub private: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub fork: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub mirror: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub archived: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub empty: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub stars_count: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub forks_count: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub open_issues_count: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub owner: Option<User>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Body for creating a repository.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct CreateRepoBody {
|
||||||
|
pub name: String,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub description: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub private: Option<bool>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub auto_init: Option<bool>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub default_branch: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub gitignores: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub license: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub readme: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Body for editing a repository.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct EditRepoBody {
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub name: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub description: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub private: Option<bool>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub archived: Option<bool>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub default_branch: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub has_issues: Option<bool>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub has_pull_requests: Option<bool>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub has_wiki: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Body for forking a repository.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct ForkRepoBody {
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub name: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub organization: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Body for transferring a repository.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TransferRepoBody {
|
||||||
|
pub new_owner: String,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub team_ids: Option<Vec<u64>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Gitea issue.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Issue {
|
||||||
|
pub number: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub title: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub body: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub state: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub assignees: Option<Vec<User>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub labels: Option<Vec<Label>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub html_url: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub repository: Option<RepositoryMeta>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub milestone: Option<Milestone>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub comments: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Body for creating an issue.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CreateIssueBody {
|
||||||
|
pub title: String,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub body: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub assignees: Option<Vec<String>>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub labels: Option<Vec<u64>>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub milestone: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Body for editing an issue.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct EditIssueBody {
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub title: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub body: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub state: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub assignees: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A pull request.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PullRequest {
|
||||||
|
pub number: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub title: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub body: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub state: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub head: Option<PullRequestRef>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub base: Option<PullRequestRef>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub merged: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub mergeable: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub html_url: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub assignees: Option<Vec<User>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub labels: Option<Vec<Label>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A pull request branch ref.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PullRequestRef {
|
||||||
|
#[serde(default)]
|
||||||
|
pub label: String,
|
||||||
|
#[serde(rename = "ref", default)]
|
||||||
|
pub ref_name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub sha: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub repo: Option<Repository>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Body for creating a pull request.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CreatePullBody {
|
||||||
|
pub title: String,
|
||||||
|
pub head: String,
|
||||||
|
pub base: String,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub body: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub assignees: Option<Vec<String>>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub labels: Option<Vec<u64>>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub milestone: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Body for merging a pull request.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct MergePullBody {
|
||||||
|
#[serde(rename = "Do")]
|
||||||
|
pub method: String,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none", rename = "merge_message_field")]
|
||||||
|
pub merge_message: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub delete_branch_after_merge: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A comment on an issue or pull request.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Comment {
|
||||||
|
pub id: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub body: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub user: Option<User>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Gitea user.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct User {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub login: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub full_name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub email: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub avatar_url: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub is_admin: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A label.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Label {
|
||||||
|
pub id: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub color: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A branch.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Branch {
|
||||||
|
#[serde(default)]
|
||||||
|
pub name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub commit: Option<BranchCommit>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub protected: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A commit on a branch.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct BranchCommit {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Body for creating a branch.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CreateBranchBody {
|
||||||
|
pub new_branch_name: String,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub old_branch_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An organization.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Organization {
|
||||||
|
pub id: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub username: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub full_name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub description: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub avatar_url: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub visibility: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Body for creating an organization.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CreateOrgBody {
|
||||||
|
pub username: String,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub full_name: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub description: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub visibility: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A milestone.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Milestone {
|
||||||
|
pub id: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub title: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub description: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub state: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub open_issues: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub closed_issues: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Repository metadata (minimal, for issue responses).
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct RepositoryMeta {
|
||||||
|
#[serde(default)]
|
||||||
|
pub full_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// File content response.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct FileContent {
|
||||||
|
#[serde(default)]
|
||||||
|
pub name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub path: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub sha: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub content: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub encoding: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub size: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub r#type: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub download_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A notification.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Notification {
|
||||||
|
pub id: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub subject: Option<NotificationSubject>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub repository: Option<Repository>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub unread: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Notification subject.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct NotificationSubject {
|
||||||
|
#[serde(default)]
|
||||||
|
pub title: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub url: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub r#type: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub state: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_repo_roundtrip() {
|
||||||
|
let json = serde_json::json!({
|
||||||
|
"id": 1,
|
||||||
|
"name": "cli",
|
||||||
|
"full_name": "studio/cli",
|
||||||
|
"default_branch": "main"
|
||||||
|
});
|
||||||
|
let repo: Repository = serde_json::from_value(json).unwrap();
|
||||||
|
assert_eq!(repo.name, "cli");
|
||||||
|
assert_eq!(repo.full_name, "studio/cli");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_issue_roundtrip() {
|
||||||
|
let json = serde_json::json!({
|
||||||
|
"number": 42,
|
||||||
|
"title": "Bug report",
|
||||||
|
"state": "open"
|
||||||
|
});
|
||||||
|
let issue: Issue = serde_json::from_value(json).unwrap();
|
||||||
|
assert_eq!(issue.number, 42);
|
||||||
|
assert_eq!(issue.state, "open");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_repo_body() {
|
||||||
|
let body = CreateRepoBody {
|
||||||
|
name: "new-repo".into(),
|
||||||
|
description: Some("A test repo".into()),
|
||||||
|
private: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let json = serde_json::to_value(&body).unwrap();
|
||||||
|
assert_eq!(json["name"], "new-repo");
|
||||||
|
assert_eq!(json["private"], true);
|
||||||
|
assert!(json.get("auto_init").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_search_result() {
|
||||||
|
let json = serde_json::json!({
|
||||||
|
"ok": true,
|
||||||
|
"data": [{"id": 1, "login": "alice", "full_name": "Alice", "email": "", "avatar_url": "", "is_admin": false}]
|
||||||
|
});
|
||||||
|
let result: SearchResult<User> = serde_json::from_value(json).unwrap();
|
||||||
|
assert_eq!(result.data.len(), 1);
|
||||||
|
assert_eq!(result.data[0].login, "alice");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user