629 lines
18 KiB
Rust
629 lines
18 KiB
Rust
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>,
|
|
}
|