misc: initial code

This commit is contained in:
hdbg
2026-02-27 10:27:24 +01:00
commit 91036f4188
32 changed files with 36435 additions and 0 deletions

View File

@@ -0,0 +1,628 @@
use chrono::{DateTime, Utc};
use reqwest::StatusCode;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
pub type GiteaResult<T> = Result<T, GiteaError>;
#[derive(Debug)]
pub enum GiteaError {
Http(reqwest::Error),
Api { status: StatusCode, body: String },
}
impl std::fmt::Display for GiteaError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Http(err) => write!(f, "http error: {err}"),
Self::Api { status, body } => write!(f, "gitea api error {status}: {body}"),
}
}
}
impl std::error::Error for GiteaError {}
impl From<reqwest::Error> for GiteaError {
fn from(value: reqwest::Error) -> Self {
Self::Http(value)
}
}
#[derive(Debug, Clone)]
pub struct GiteaClient {
http: reqwest::Client,
api_base: String,
token: Option<String>,
}
impl GiteaClient {
pub fn new(instance_base_url: impl Into<String>) -> Self {
let base = instance_base_url.into().trim_end_matches('/').to_owned();
let api_base = if base.ends_with("/api/v1") {
base
} else {
format!("{base}/api/v1")
};
Self {
http: reqwest::Client::new(),
api_base,
token: None,
}
}
pub fn with_token(mut self, token: impl Into<String>) -> Self {
self.token = Some(token.into());
self
}
pub fn with_http_client(mut self, http: reqwest::Client) -> Self {
self.http = http;
self
}
pub async fn list_pull_requests(
&self,
owner: &str,
repo: &str,
query: &ListPullRequestsQuery,
) -> GiteaResult<Vec<PullRequest>> {
let path = format!("/repos/{owner}/{repo}/pulls");
self.get_json(&path, query).await
}
pub async fn get_pull_request(
&self,
owner: &str,
repo: &str,
index: i64,
) -> GiteaResult<PullRequest> {
let path = format!("/repos/{owner}/{repo}/pulls/{index}");
self.get_json_no_query(&path).await
}
pub async fn edit_pull_request(
&self,
owner: &str,
repo: &str,
index: i64,
body: &EditPullRequestOption,
) -> GiteaResult<PullRequest> {
let path = format!("/repos/{owner}/{repo}/pulls/{index}");
self.send_json(self.request(reqwest::Method::PATCH, &path).json(body))
.await
}
pub async fn list_pull_request_commits(
&self,
owner: &str,
repo: &str,
index: i64,
query: &ListPullRequestCommitsQuery,
) -> GiteaResult<Vec<Commit>> {
let path = format!("/repos/{owner}/{repo}/pulls/{index}/commits");
self.get_json(&path, query).await
}
pub async fn list_pull_request_files(
&self,
owner: &str,
repo: &str,
index: i64,
query: &ListPullRequestFilesQuery,
) -> GiteaResult<Vec<ChangedFile>> {
let path = format!("/repos/{owner}/{repo}/pulls/{index}/files");
self.get_json(&path, query).await
}
pub async fn list_pull_reviews(
&self,
owner: &str,
repo: &str,
index: i64,
query: &ListPullReviewsQuery,
) -> GiteaResult<Vec<PullReview>> {
let path = format!("/repos/{owner}/{repo}/pulls/{index}/reviews");
self.get_json(&path, query).await
}
pub async fn create_pull_review(
&self,
owner: &str,
repo: &str,
index: i64,
body: &CreatePullReviewOptions,
) -> GiteaResult<PullReview> {
let path = format!("/repos/{owner}/{repo}/pulls/{index}/reviews");
self.send_json(self.request(reqwest::Method::POST, &path).json(body))
.await
}
pub async fn get_pull_review(
&self,
owner: &str,
repo: &str,
index: i64,
review_id: i64,
) -> GiteaResult<PullReview> {
let path = format!("/repos/{owner}/{repo}/pulls/{index}/reviews/{review_id}");
self.get_json_no_query(&path).await
}
pub async fn submit_pull_review(
&self,
owner: &str,
repo: &str,
index: i64,
review_id: i64,
body: &SubmitPullReviewOptions,
) -> GiteaResult<PullReview> {
let path = format!("/repos/{owner}/{repo}/pulls/{index}/reviews/{review_id}");
self.send_json(self.request(reqwest::Method::POST, &path).json(body))
.await
}
pub async fn delete_pull_review(
&self,
owner: &str,
repo: &str,
index: i64,
review_id: i64,
) -> GiteaResult<()> {
let path = format!("/repos/{owner}/{repo}/pulls/{index}/reviews/{review_id}");
self.send_empty(self.request(reqwest::Method::DELETE, &path))
.await
}
pub async fn dismiss_pull_review(
&self,
owner: &str,
repo: &str,
index: i64,
review_id: i64,
body: &DismissPullReviewOptions,
) -> GiteaResult<PullReview> {
let path = format!("/repos/{owner}/{repo}/pulls/{index}/reviews/{review_id}/dismissals");
self.send_json(self.request(reqwest::Method::POST, &path).json(body))
.await
}
pub async fn undismiss_pull_review(
&self,
owner: &str,
repo: &str,
index: i64,
review_id: i64,
) -> GiteaResult<PullReview> {
let path = format!("/repos/{owner}/{repo}/pulls/{index}/reviews/{review_id}/undismissals");
self.send_json(self.request(reqwest::Method::POST, &path))
.await
}
pub async fn list_pull_review_comments(
&self,
owner: &str,
repo: &str,
index: i64,
review_id: i64,
) -> GiteaResult<Vec<PullReviewComment>> {
let path = format!("/repos/{owner}/{repo}/pulls/{index}/reviews/{review_id}/comments");
self.get_json_no_query(&path).await
}
pub async fn list_issue_comments(
&self,
owner: &str,
repo: &str,
index: i64,
query: &ListIssueCommentsQuery,
) -> GiteaResult<Vec<Comment>> {
let path = format!("/repos/{owner}/{repo}/issues/{index}/comments");
self.get_json(&path, query).await
}
pub async fn create_issue_comment(
&self,
owner: &str,
repo: &str,
index: i64,
body: &CreateIssueCommentOption,
) -> GiteaResult<Comment> {
let path = format!("/repos/{owner}/{repo}/issues/{index}/comments");
self.send_json(self.request(reqwest::Method::POST, &path).json(body))
.await
}
pub async fn get_issue_comment(
&self,
owner: &str,
repo: &str,
comment_id: i64,
) -> GiteaResult<Comment> {
let path = format!("/repos/{owner}/{repo}/issues/comments/{comment_id}");
self.get_json_no_query(&path).await
}
pub async fn edit_issue_comment(
&self,
owner: &str,
repo: &str,
comment_id: i64,
body: &EditIssueCommentOption,
) -> GiteaResult<Comment> {
let path = format!("/repos/{owner}/{repo}/issues/comments/{comment_id}");
self.send_json(self.request(reqwest::Method::PATCH, &path).json(body))
.await
}
pub async fn delete_issue_comment(
&self,
owner: &str,
repo: &str,
comment_id: i64,
) -> GiteaResult<()> {
let path = format!("/repos/{owner}/{repo}/issues/comments/{comment_id}");
self.send_empty(self.request(reqwest::Method::DELETE, &path))
.await
}
fn request(&self, method: reqwest::Method, path: &str) -> reqwest::RequestBuilder {
let mut req = self
.http
.request(method, format!("{}{}", self.api_base, path));
if let Some(token) = &self.token {
req = req.header("Authorization", format!("token {token}"));
}
req
}
async fn get_json<Q, T>(&self, path: &str, query: &Q) -> GiteaResult<T>
where
Q: Serialize + ?Sized,
T: DeserializeOwned,
{
let query_string = serde_urlencoded::to_string(query).map_err(|err| GiteaError::Api {
status: StatusCode::BAD_REQUEST,
body: format!("failed to encode query parameters: {err}"),
})?;
let full_path = if query_string.is_empty() {
path.to_owned()
} else {
format!("{path}?{query_string}")
};
self.send_json(self.request(reqwest::Method::GET, &full_path))
.await
}
async fn get_json_no_query<T>(&self, path: &str) -> GiteaResult<T>
where
T: DeserializeOwned,
{
self.send_json(self.request(reqwest::Method::GET, path))
.await
}
async fn send_json<T>(&self, req: reqwest::RequestBuilder) -> GiteaResult<T>
where
T: DeserializeOwned,
{
let resp = req.send().await?;
if resp.status().is_success() {
return Ok(resp.json::<T>().await?);
}
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
Err(GiteaError::Api { status, body })
}
async fn send_empty(&self, req: reqwest::RequestBuilder) -> GiteaResult<()> {
let resp = req.send().await?;
if resp.status().is_success() {
return Ok(());
}
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
Err(GiteaError::Api { status, body })
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct ListPullRequestsQuery {
#[serde(skip_serializing_if = "Option::is_none")]
pub base_branch: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sort: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub milestone: Option<i64>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub labels: Vec<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub poster: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<i64>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct ListPullRequestCommitsQuery {
#[serde(skip_serializing_if = "Option::is_none")]
pub page: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub verification: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub files: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct ListPullRequestFilesQuery {
#[serde(rename = "skip-to", skip_serializing_if = "Option::is_none")]
pub skip_to: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub whitespace: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<i64>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct ListPullReviewsQuery {
#[serde(skip_serializing_if = "Option::is_none")]
pub page: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<i64>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct ListIssueCommentsQuery {
#[serde(skip_serializing_if = "Option::is_none")]
pub since: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub before: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: Option<i64>,
pub login: Option<String>,
pub full_name: Option<String>,
pub email: Option<String>,
pub avatar_url: Option<String>,
pub html_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Label {
pub id: Option<i64>,
pub name: Option<String>,
pub color: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Milestone {
pub id: Option<i64>,
pub title: Option<String>,
pub state: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Team {
pub id: Option<i64>,
pub name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrBranchInfo {
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PullRequest {
pub id: Option<i64>,
pub number: Option<i64>,
pub title: Option<String>,
pub body: Option<String>,
pub state: Option<String>,
pub draft: Option<bool>,
pub merged: Option<bool>,
pub mergeable: Option<bool>,
pub merge_base: Option<String>,
pub merge_commit_sha: Option<String>,
pub additions: Option<i64>,
pub deletions: Option<i64>,
pub changed_files: Option<i64>,
pub comments: Option<i64>,
pub review_comments: Option<i64>,
pub html_url: Option<String>,
pub diff_url: Option<String>,
pub patch_url: Option<String>,
pub url: Option<String>,
pub created_at: Option<DateTime<Utc>>,
pub updated_at: Option<DateTime<Utc>>,
pub closed_at: Option<DateTime<Utc>>,
pub merged_at: Option<DateTime<Utc>>,
pub due_date: Option<DateTime<Utc>>,
pub assignee: Option<User>,
pub assignees: Option<Vec<User>>,
pub user: Option<User>,
pub merged_by: Option<User>,
pub base: Option<PrBranchInfo>,
pub head: Option<PrBranchInfo>,
pub labels: Option<Vec<Label>>,
pub milestone: Option<Milestone>,
pub requested_reviewers: Option<Vec<User>>,
pub requested_reviewers_teams: Option<Vec<Team>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChangedFile {
pub additions: Option<i64>,
pub changes: Option<i64>,
pub contents_url: Option<String>,
pub deletions: Option<i64>,
pub filename: Option<String>,
pub html_url: Option<String>,
pub previous_filename: Option<String>,
pub raw_url: Option<String>,
pub status: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Commit {
pub sha: Option<String>,
pub html_url: Option<String>,
pub url: Option<String>,
pub author: Option<User>,
pub committer: Option<User>,
#[serde(default)]
pub files: Vec<CommitAffectedFile>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommitAffectedFile {
pub filename: Option<String>,
pub status: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PullReview {
pub id: Option<i64>,
pub body: Option<String>,
pub state: Option<String>,
pub commit_id: Option<String>,
pub comments_count: Option<i64>,
pub dismissed: Option<bool>,
pub stale: Option<bool>,
pub official: Option<bool>,
pub html_url: Option<String>,
pub pull_request_url: Option<String>,
pub submitted_at: Option<DateTime<Utc>>,
pub updated_at: Option<DateTime<Utc>>,
pub user: Option<User>,
pub team: Option<Team>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PullReviewComment {
pub id: Option<i64>,
pub body: Option<String>,
pub path: Option<String>,
pub position: Option<u64>,
pub original_position: Option<u64>,
pub commit_id: Option<String>,
pub original_commit_id: Option<String>,
pub diff_hunk: Option<String>,
pub pull_request_review_id: Option<i64>,
pub pull_request_url: Option<String>,
pub html_url: Option<String>,
pub created_at: Option<DateTime<Utc>>,
pub updated_at: Option<DateTime<Utc>>,
pub user: Option<User>,
pub resolver: Option<User>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Comment {
pub id: Option<i64>,
pub body: Option<String>,
pub html_url: Option<String>,
pub issue_url: Option<String>,
pub pull_request_url: Option<String>,
pub original_author: Option<String>,
pub original_author_id: Option<i64>,
pub created_at: Option<DateTime<Utc>>,
pub updated_at: Option<DateTime<Utc>>,
pub user: Option<User>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReviewState(pub String);
impl ReviewState {
pub fn approve() -> Self {
Self("APPROVE".to_owned())
}
pub fn request_changes() -> Self {
Self("REQUEST_CHANGES".to_owned())
}
pub fn comment() -> Self {
Self("COMMENT".to_owned())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreatePullReviewComment {
pub body: Option<String>,
pub path: Option<String>,
pub new_position: Option<i64>,
pub old_position: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreatePullReviewOptions {
pub body: Option<String>,
pub comments: Option<Vec<CreatePullReviewComment>>,
pub commit_id: Option<String>,
pub event: Option<ReviewState>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubmitPullReviewOptions {
pub body: Option<String>,
pub event: Option<ReviewState>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DismissPullReviewOptions {
pub message: Option<String>,
pub priors: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateIssueCommentOption {
pub body: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EditIssueCommentOption {
pub body: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CreatePullRequestOption {
pub title: Option<String>,
pub body: Option<String>,
pub head: Option<String>,
pub base: Option<String>,
pub assignee: Option<String>,
pub assignees: Option<Vec<String>>,
pub labels: Option<Vec<i64>>,
pub milestone: Option<i64>,
pub reviewers: Option<Vec<String>>,
pub team_reviewers: Option<Vec<String>>,
pub due_date: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EditPullRequestOption {
pub title: Option<String>,
pub body: Option<String>,
pub base: Option<String>,
pub state: Option<String>,
pub assignee: Option<String>,
pub assignees: Option<Vec<String>>,
pub labels: Option<Vec<i64>>,
pub milestone: Option<i64>,
pub due_date: Option<DateTime<Utc>>,
pub unset_due_date: Option<bool>,
pub allow_maintainer_edit: Option<bool>,
}