feat: add gitea API client and DTO bundle fetch
This commit is contained in:
Generated
+1399
-9
File diff suppressed because it is too large
Load Diff
@@ -6,10 +6,12 @@ edition = "2024"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
|
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_cmd = "2.0"
|
assert_cmd = "2.0"
|
||||||
|
mockito = "1.7"
|
||||||
tempfile = "3.10"
|
tempfile = "3.10"
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
use anyhow::{Context, Result};
|
||||||
|
use reqwest::blocking::Client;
|
||||||
|
|
||||||
|
use crate::gitea::dto::{
|
||||||
|
ChangedFileDto, CommitDto, PullBundleDto, PullDto, ReviewCommentDto, ReviewDto,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct GiteaClient {
|
||||||
|
base_url: String,
|
||||||
|
token: String,
|
||||||
|
http: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GiteaClient {
|
||||||
|
pub fn new(base_url: String, token: String) -> Self {
|
||||||
|
Self {
|
||||||
|
base_url,
|
||||||
|
token,
|
||||||
|
http: Client::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn endpoint(&self, path: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"{}/{}",
|
||||||
|
self.base_url.trim_end_matches('/'),
|
||||||
|
path.trim_start_matches('/')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_json<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T> {
|
||||||
|
let response = self
|
||||||
|
.http
|
||||||
|
.get(self.endpoint(path))
|
||||||
|
.header("Authorization", format!("token {}", self.token))
|
||||||
|
.send()
|
||||||
|
.with_context(|| format!("request failed for {path}"))?
|
||||||
|
.error_for_status()
|
||||||
|
.with_context(|| format!("gitea api returned error for {path}"))?;
|
||||||
|
|
||||||
|
response
|
||||||
|
.json::<T>()
|
||||||
|
.with_context(|| format!("decode gitea response failed for {path}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch_pr_bundle(&self, repo: &str, pr_index: i64) -> Result<PullBundleDto> {
|
||||||
|
let pull_path = format!("/api/v1/repos/{repo}/pulls/{pr_index}");
|
||||||
|
let reviews_path = format!("{pull_path}/reviews");
|
||||||
|
let commits_path = format!("{pull_path}/commits");
|
||||||
|
let files_path = format!("{pull_path}/files");
|
||||||
|
|
||||||
|
let pull: PullDto = self.get_json(&pull_path)?;
|
||||||
|
let reviews: Vec<ReviewDto> = self.get_json(&reviews_path)?;
|
||||||
|
let mut comments = Vec::new();
|
||||||
|
for review in &reviews {
|
||||||
|
let review_comments_path = format!("{pull_path}/reviews/{}/comments", review.id);
|
||||||
|
let mut review_comments: Vec<ReviewCommentDto> =
|
||||||
|
self.get_json(&review_comments_path)?;
|
||||||
|
comments.append(&mut review_comments);
|
||||||
|
}
|
||||||
|
let commits: Vec<CommitDto> = self.get_json(&commits_path)?;
|
||||||
|
let files: Vec<ChangedFileDto> = self.get_json(&files_path)?;
|
||||||
|
|
||||||
|
Ok(PullBundleDto {
|
||||||
|
pull,
|
||||||
|
reviews,
|
||||||
|
comments,
|
||||||
|
commits,
|
||||||
|
files,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct UserDto {
|
||||||
|
pub login: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct PullBranchDto {
|
||||||
|
#[serde(rename = "ref")]
|
||||||
|
pub ref_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct PullDto {
|
||||||
|
pub number: i64,
|
||||||
|
pub title: String,
|
||||||
|
pub state: String,
|
||||||
|
pub body: Option<String>,
|
||||||
|
pub user: UserDto,
|
||||||
|
pub base: PullBranchDto,
|
||||||
|
pub head: PullBranchDto,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
pub merged_at: Option<String>,
|
||||||
|
pub additions: Option<i64>,
|
||||||
|
pub deletions: Option<i64>,
|
||||||
|
pub changed_files: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct ReviewDto {
|
||||||
|
pub id: i64,
|
||||||
|
pub state: String,
|
||||||
|
pub user: UserDto,
|
||||||
|
pub submitted_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct ReviewCommentDto {
|
||||||
|
pub id: i64,
|
||||||
|
pub body: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
pub user: UserDto,
|
||||||
|
pub path: Option<String>,
|
||||||
|
pub line: Option<i64>,
|
||||||
|
pub pull_request_review_id: Option<i64>,
|
||||||
|
pub original_position: Option<u64>,
|
||||||
|
pub position: Option<u64>,
|
||||||
|
pub commit_id: Option<String>,
|
||||||
|
pub original_commit_id: Option<String>,
|
||||||
|
pub diff_hunk: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct CommitUserDto {
|
||||||
|
pub name: String,
|
||||||
|
pub date: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct RepoCommitDto {
|
||||||
|
pub message: String,
|
||||||
|
pub author: Option<CommitUserDto>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct CommitDto {
|
||||||
|
pub sha: String,
|
||||||
|
pub commit: RepoCommitDto,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct ChangedFileDto {
|
||||||
|
pub filename: String,
|
||||||
|
pub additions: i64,
|
||||||
|
pub deletions: i64,
|
||||||
|
pub changes: Option<i64>,
|
||||||
|
pub status: Option<String>,
|
||||||
|
pub previous_filename: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PullBundleDto {
|
||||||
|
pub pull: PullDto,
|
||||||
|
pub reviews: Vec<ReviewDto>,
|
||||||
|
pub comments: Vec<ReviewCommentDto>,
|
||||||
|
pub commits: Vec<CommitDto>,
|
||||||
|
pub files: Vec<ChangedFileDto>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod client;
|
||||||
|
pub mod dto;
|
||||||
@@ -4,6 +4,7 @@ use clap::Parser;
|
|||||||
|
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod gitea;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
pub mod output;
|
pub mod output;
|
||||||
pub mod render;
|
pub mod render;
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
use gitea_pr_review::gitea::client::GiteaClient;
|
||||||
|
use mockito::Server;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fetch_bundle_hits_required_endpoints_and_aggregates_review_comments() {
|
||||||
|
let mut server = Server::new();
|
||||||
|
|
||||||
|
let _pull = server
|
||||||
|
.mock("GET", "/api/v1/repos/org/repo/pulls/42")
|
||||||
|
.match_header("authorization", "token secret")
|
||||||
|
.with_status(200)
|
||||||
|
.with_body(
|
||||||
|
r#"{
|
||||||
|
"number": 42,
|
||||||
|
"title": "Fix parser",
|
||||||
|
"state": "open",
|
||||||
|
"body": "desc",
|
||||||
|
"user": {"login": "alice"},
|
||||||
|
"base": {"ref": "main"},
|
||||||
|
"head": {"ref": "feature/x"},
|
||||||
|
"created_at": "2026-04-08T10:00:00Z",
|
||||||
|
"updated_at": "2026-04-08T11:00:00Z",
|
||||||
|
"merged_at": null,
|
||||||
|
"additions": 12,
|
||||||
|
"deletions": 3,
|
||||||
|
"changed_files": 2
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let _reviews = server
|
||||||
|
.mock("GET", "/api/v1/repos/org/repo/pulls/42/reviews")
|
||||||
|
.match_header("authorization", "token secret")
|
||||||
|
.with_status(200)
|
||||||
|
.with_body(
|
||||||
|
r#"[
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"state": "COMMENT",
|
||||||
|
"user": {"login": "bob"},
|
||||||
|
"submitted_at": "2026-04-08T12:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"state": "APPROVED",
|
||||||
|
"user": {"login": "carol"},
|
||||||
|
"submitted_at": "2026-04-08T13:00:00Z"
|
||||||
|
}
|
||||||
|
]"#,
|
||||||
|
)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let _review_7_comments = server
|
||||||
|
.mock("GET", "/api/v1/repos/org/repo/pulls/42/reviews/7/comments")
|
||||||
|
.match_header("authorization", "token secret")
|
||||||
|
.with_status(200)
|
||||||
|
.with_body(
|
||||||
|
r#"[
|
||||||
|
{
|
||||||
|
"id": 71,
|
||||||
|
"body": "first comment",
|
||||||
|
"created_at": "2026-04-08T12:01:00Z",
|
||||||
|
"updated_at": "2026-04-08T12:02:00Z",
|
||||||
|
"user": {"login": "bob"},
|
||||||
|
"path": "src/main.rs",
|
||||||
|
"line": 10,
|
||||||
|
"pull_request_review_id": 7,
|
||||||
|
"original_position": 10,
|
||||||
|
"position": 10,
|
||||||
|
"commit_id": "abc123",
|
||||||
|
"original_commit_id": "abc123",
|
||||||
|
"diff_hunk": "@@ -1 +1 @@"
|
||||||
|
}
|
||||||
|
]"#,
|
||||||
|
)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let _review_8_comments = server
|
||||||
|
.mock("GET", "/api/v1/repos/org/repo/pulls/42/reviews/8/comments")
|
||||||
|
.match_header("authorization", "token secret")
|
||||||
|
.with_status(200)
|
||||||
|
.with_body("[]")
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let _commits = server
|
||||||
|
.mock("GET", "/api/v1/repos/org/repo/pulls/42/commits")
|
||||||
|
.match_header("authorization", "token secret")
|
||||||
|
.with_status(200)
|
||||||
|
.with_body(
|
||||||
|
r#"[
|
||||||
|
{
|
||||||
|
"sha": "abcdef1234567890",
|
||||||
|
"commit": {
|
||||||
|
"message": "feat: parser\n\nbody",
|
||||||
|
"author": {
|
||||||
|
"name": "Alice",
|
||||||
|
"date": "2026-04-08T10:10:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]"#,
|
||||||
|
)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let _files = server
|
||||||
|
.mock("GET", "/api/v1/repos/org/repo/pulls/42/files")
|
||||||
|
.match_header("authorization", "token secret")
|
||||||
|
.with_status(200)
|
||||||
|
.with_body(
|
||||||
|
r#"[
|
||||||
|
{
|
||||||
|
"filename": "src/main.rs",
|
||||||
|
"additions": 12,
|
||||||
|
"deletions": 3,
|
||||||
|
"changes": 15,
|
||||||
|
"status": "modified",
|
||||||
|
"previous_filename": null
|
||||||
|
}
|
||||||
|
]"#,
|
||||||
|
)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let client = GiteaClient::new(server.url(), "secret".to_string());
|
||||||
|
let bundle = client.fetch_pr_bundle("org/repo", 42).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(bundle.pull.number, 42);
|
||||||
|
assert_eq!(bundle.pull.title, "Fix parser");
|
||||||
|
assert_eq!(bundle.reviews.len(), 2);
|
||||||
|
assert_eq!(bundle.comments.len(), 1);
|
||||||
|
assert_eq!(bundle.comments[0].pull_request_review_id, Some(7));
|
||||||
|
assert_eq!(bundle.commits.len(), 1);
|
||||||
|
assert_eq!(bundle.files.len(), 1);
|
||||||
|
|
||||||
|
_pull.assert();
|
||||||
|
_reviews.assert();
|
||||||
|
_review_7_comments.assert();
|
||||||
|
_review_8_comments.assert();
|
||||||
|
_commits.assert();
|
||||||
|
_files.assert();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user