expand gitea tools: 17 new operations (24 total)
repos: create, edit, fork, list org repos issues: edit, list comments, create comment PRs: get, create, merge branches: list, create, delete orgs: list user orgs, get org notifications: list added write:repository and read:notification PAT scopes. new response types: Comment, Branch, Organization, Notification. authed_patch and authed_delete helpers with 401 retry. URL-encoded query params throughout.
This commit is contained in:
690
src/sdk/gitea.rs
690
src/sdk/gitea.rs
@@ -13,8 +13,10 @@ const TOKEN_SCOPES: &[&str] = &[
|
||||
"read:issue",
|
||||
"write:issue",
|
||||
"read:repository",
|
||||
"write:repository",
|
||||
"read:user",
|
||||
"read:organization",
|
||||
"read:notification",
|
||||
];
|
||||
|
||||
pub struct GiteaClient {
|
||||
@@ -110,6 +112,72 @@ pub struct FileContent {
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Comment {
|
||||
pub id: u64,
|
||||
#[serde(default)]
|
||||
pub body: String,
|
||||
pub user: UserRef,
|
||||
#[serde(default)]
|
||||
pub html_url: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Branch {
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub commit: BranchCommit,
|
||||
#[serde(default)]
|
||||
pub protected: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
pub struct BranchCommit {
|
||||
#[serde(default)]
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Organization {
|
||||
#[serde(default)]
|
||||
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,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Notification {
|
||||
pub id: u64,
|
||||
#[serde(default)]
|
||||
pub subject: NotificationSubject,
|
||||
#[serde(default)]
|
||||
pub repository: Option<RepoSummary>,
|
||||
#[serde(default)]
|
||||
pub unread: bool,
|
||||
#[serde(default)]
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
pub struct NotificationSubject {
|
||||
#[serde(default)]
|
||||
pub title: String,
|
||||
#[serde(default)]
|
||||
pub url: String,
|
||||
#[serde(default, rename = "type")]
|
||||
pub subject_type: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GiteaUser {
|
||||
login: String,
|
||||
@@ -424,6 +492,77 @@ impl GiteaClient {
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
/// Make an authenticated PATCH request using the user's token.
|
||||
/// On 401, invalidates token and retries once.
|
||||
async fn authed_patch(
|
||||
&self,
|
||||
localpart: &str,
|
||||
path: &str,
|
||||
body: &serde_json::Value,
|
||||
) -> Result<reqwest::Response, String> {
|
||||
let token = self.ensure_token(localpart).await?;
|
||||
let url = format!("{}{}", self.base_url, path);
|
||||
|
||||
let resp = self
|
||||
.http
|
||||
.patch(&url)
|
||||
.header("Authorization", format!("token {token}"))
|
||||
.json(body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("request failed: {e}"))?;
|
||||
|
||||
if resp.status().as_u16() == 401 {
|
||||
debug!(localpart, "Token rejected, re-provisioning");
|
||||
self.token_store.delete(localpart, SERVICE).await;
|
||||
let token = self.ensure_token(localpart).await?;
|
||||
return self
|
||||
.http
|
||||
.patch(&url)
|
||||
.header("Authorization", format!("token {token}"))
|
||||
.json(body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("request failed (retry): {e}"));
|
||||
}
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
/// Make an authenticated DELETE request using the user's token.
|
||||
/// On 401, invalidates token and retries once.
|
||||
async fn authed_delete(
|
||||
&self,
|
||||
localpart: &str,
|
||||
path: &str,
|
||||
) -> Result<reqwest::Response, String> {
|
||||
let token = self.ensure_token(localpart).await?;
|
||||
let url = format!("{}{}", self.base_url, path);
|
||||
|
||||
let resp = self
|
||||
.http
|
||||
.delete(&url)
|
||||
.header("Authorization", format!("token {token}"))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("request failed: {e}"))?;
|
||||
|
||||
if resp.status().as_u16() == 401 {
|
||||
debug!(localpart, "Token rejected, re-provisioning");
|
||||
self.token_store.delete(localpart, SERVICE).await;
|
||||
let token = self.ensure_token(localpart).await?;
|
||||
return self
|
||||
.http
|
||||
.delete(&url)
|
||||
.header("Authorization", format!("token {token}"))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("request failed (retry): {e}"));
|
||||
}
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
// ── Public API methods ──────────────────────────────────────────────────
|
||||
|
||||
pub async fn list_repos(
|
||||
@@ -625,6 +764,429 @@ impl GiteaClient {
|
||||
}
|
||||
resp.json().await.map_err(|e| format!("parse error: {e}"))
|
||||
}
|
||||
|
||||
// ── Repos ───────────────────────────────────────────────────────────────
|
||||
|
||||
pub async fn create_repo(
|
||||
&self,
|
||||
localpart: &str,
|
||||
name: &str,
|
||||
org: Option<&str>,
|
||||
description: Option<&str>,
|
||||
private: Option<bool>,
|
||||
auto_init: Option<bool>,
|
||||
default_branch: Option<&str>,
|
||||
) -> Result<Repo, String> {
|
||||
let mut json = serde_json::json!({ "name": name });
|
||||
if let Some(d) = description {
|
||||
json["description"] = serde_json::json!(d);
|
||||
}
|
||||
if let Some(p) = private {
|
||||
json["private"] = serde_json::json!(p);
|
||||
}
|
||||
if let Some(a) = auto_init {
|
||||
json["auto_init"] = serde_json::json!(a);
|
||||
}
|
||||
if let Some(b) = default_branch {
|
||||
json["default_branch"] = serde_json::json!(b);
|
||||
}
|
||||
|
||||
let path = if let Some(org) = org {
|
||||
format!("/api/v1/orgs/{org}/repos")
|
||||
} else {
|
||||
"/api/v1/user/repos".to_string()
|
||||
};
|
||||
|
||||
let resp = self.authed_post(localpart, &path, &json).await?;
|
||||
if !resp.status().is_success() {
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
return Err(format!("create repo failed: {text}"));
|
||||
}
|
||||
resp.json().await.map_err(|e| format!("parse error: {e}"))
|
||||
}
|
||||
|
||||
pub async fn edit_repo(
|
||||
&self,
|
||||
localpart: &str,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
description: Option<&str>,
|
||||
private: Option<bool>,
|
||||
archived: Option<bool>,
|
||||
default_branch: Option<&str>,
|
||||
) -> Result<Repo, String> {
|
||||
let mut json = serde_json::json!({});
|
||||
if let Some(d) = description {
|
||||
json["description"] = serde_json::json!(d);
|
||||
}
|
||||
if let Some(p) = private {
|
||||
json["private"] = serde_json::json!(p);
|
||||
}
|
||||
if let Some(a) = archived {
|
||||
json["archived"] = serde_json::json!(a);
|
||||
}
|
||||
if let Some(b) = default_branch {
|
||||
json["default_branch"] = serde_json::json!(b);
|
||||
}
|
||||
|
||||
let resp = self
|
||||
.authed_patch(
|
||||
localpart,
|
||||
&format!("/api/v1/repos/{owner}/{repo}"),
|
||||
&json,
|
||||
)
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
return Err(format!("edit repo failed: {text}"));
|
||||
}
|
||||
resp.json().await.map_err(|e| format!("parse error: {e}"))
|
||||
}
|
||||
|
||||
pub async fn fork_repo(
|
||||
&self,
|
||||
localpart: &str,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
new_name: Option<&str>,
|
||||
) -> Result<Repo, String> {
|
||||
let mut json = serde_json::json!({});
|
||||
if let Some(n) = new_name {
|
||||
json["name"] = serde_json::json!(n);
|
||||
}
|
||||
|
||||
let resp = self
|
||||
.authed_post(
|
||||
localpart,
|
||||
&format!("/api/v1/repos/{owner}/{repo}/forks"),
|
||||
&json,
|
||||
)
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
return Err(format!("fork repo failed: {text}"));
|
||||
}
|
||||
resp.json().await.map_err(|e| format!("parse error: {e}"))
|
||||
}
|
||||
|
||||
pub async fn list_org_repos(
|
||||
&self,
|
||||
localpart: &str,
|
||||
org: &str,
|
||||
limit: Option<u32>,
|
||||
) -> Result<Vec<RepoSummary>, String> {
|
||||
let param_str = {
|
||||
let mut encoder = form_urlencoded::Serializer::new(String::new());
|
||||
if let Some(n) = limit {
|
||||
encoder.append_pair("limit", &n.to_string());
|
||||
}
|
||||
let encoded = encoder.finish();
|
||||
if encoded.is_empty() { String::new() } else { format!("?{encoded}") }
|
||||
};
|
||||
|
||||
let path = format!("/api/v1/orgs/{org}/repos{param_str}");
|
||||
let resp = self.authed_get(localpart, &path).await?;
|
||||
if !resp.status().is_success() {
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
return Err(format!("list org repos failed: {text}"));
|
||||
}
|
||||
resp.json().await.map_err(|e| format!("parse error: {e}"))
|
||||
}
|
||||
|
||||
// ── Issues ──────────────────────────────────────────────────────────────
|
||||
|
||||
pub async fn edit_issue(
|
||||
&self,
|
||||
localpart: &str,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
number: u64,
|
||||
title: Option<&str>,
|
||||
body: Option<&str>,
|
||||
state: Option<&str>,
|
||||
assignees: Option<&[String]>,
|
||||
) -> Result<Issue, String> {
|
||||
let mut json = serde_json::json!({});
|
||||
if let Some(t) = title {
|
||||
json["title"] = serde_json::json!(t);
|
||||
}
|
||||
if let Some(b) = body {
|
||||
json["body"] = serde_json::json!(b);
|
||||
}
|
||||
if let Some(s) = state {
|
||||
json["state"] = serde_json::json!(s);
|
||||
}
|
||||
if let Some(a) = assignees {
|
||||
json["assignees"] = serde_json::json!(a);
|
||||
}
|
||||
|
||||
let resp = self
|
||||
.authed_patch(
|
||||
localpart,
|
||||
&format!("/api/v1/repos/{owner}/{repo}/issues/{number}"),
|
||||
&json,
|
||||
)
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
return Err(format!("edit issue failed: {text}"));
|
||||
}
|
||||
resp.json().await.map_err(|e| format!("parse error: {e}"))
|
||||
}
|
||||
|
||||
pub async fn list_comments(
|
||||
&self,
|
||||
localpart: &str,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
number: u64,
|
||||
) -> Result<Vec<Comment>, String> {
|
||||
let resp = self
|
||||
.authed_get(
|
||||
localpart,
|
||||
&format!("/api/v1/repos/{owner}/{repo}/issues/{number}/comments"),
|
||||
)
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
return Err(format!("list comments failed: {text}"));
|
||||
}
|
||||
resp.json().await.map_err(|e| format!("parse error: {e}"))
|
||||
}
|
||||
|
||||
pub async fn create_comment(
|
||||
&self,
|
||||
localpart: &str,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
number: u64,
|
||||
body: &str,
|
||||
) -> Result<Comment, String> {
|
||||
let json = serde_json::json!({ "body": body });
|
||||
|
||||
let resp = self
|
||||
.authed_post(
|
||||
localpart,
|
||||
&format!("/api/v1/repos/{owner}/{repo}/issues/{number}/comments"),
|
||||
&json,
|
||||
)
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
return Err(format!("create comment failed: {text}"));
|
||||
}
|
||||
resp.json().await.map_err(|e| format!("parse error: {e}"))
|
||||
}
|
||||
|
||||
// ── Pull requests ───────────────────────────────────────────────────────
|
||||
|
||||
pub async fn get_pull(
|
||||
&self,
|
||||
localpart: &str,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
number: u64,
|
||||
) -> Result<PullRequest, String> {
|
||||
let resp = self
|
||||
.authed_get(
|
||||
localpart,
|
||||
&format!("/api/v1/repos/{owner}/{repo}/pulls/{number}"),
|
||||
)
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
return Err(format!("get pull failed: {text}"));
|
||||
}
|
||||
resp.json().await.map_err(|e| format!("parse error: {e}"))
|
||||
}
|
||||
|
||||
pub async fn create_pull(
|
||||
&self,
|
||||
localpart: &str,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
title: &str,
|
||||
head: &str,
|
||||
base: &str,
|
||||
body: Option<&str>,
|
||||
) -> Result<PullRequest, String> {
|
||||
let mut json = serde_json::json!({
|
||||
"title": title,
|
||||
"head": head,
|
||||
"base": base,
|
||||
});
|
||||
if let Some(b) = body {
|
||||
json["body"] = serde_json::json!(b);
|
||||
}
|
||||
|
||||
let resp = self
|
||||
.authed_post(
|
||||
localpart,
|
||||
&format!("/api/v1/repos/{owner}/{repo}/pulls"),
|
||||
&json,
|
||||
)
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
return Err(format!("create pull failed: {text}"));
|
||||
}
|
||||
resp.json().await.map_err(|e| format!("parse error: {e}"))
|
||||
}
|
||||
|
||||
pub async fn merge_pull(
|
||||
&self,
|
||||
localpart: &str,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
number: u64,
|
||||
method: Option<&str>,
|
||||
delete_branch: Option<bool>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let mut json = serde_json::json!({
|
||||
"Do": method.unwrap_or("merge"),
|
||||
});
|
||||
if let Some(d) = delete_branch {
|
||||
json["delete_branch_after_merge"] = serde_json::json!(d);
|
||||
}
|
||||
|
||||
let resp = self
|
||||
.authed_post(
|
||||
localpart,
|
||||
&format!("/api/v1/repos/{owner}/{repo}/pulls/{number}/merge"),
|
||||
&json,
|
||||
)
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
return Err(format!("merge pull failed: {text}"));
|
||||
}
|
||||
// Merge returns empty body on success (204/200)
|
||||
Ok(serde_json::json!({"status": "merged", "number": number}))
|
||||
}
|
||||
|
||||
// ── Branches ────────────────────────────────────────────────────────────
|
||||
|
||||
pub async fn list_branches(
|
||||
&self,
|
||||
localpart: &str,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
) -> Result<Vec<Branch>, String> {
|
||||
let resp = self
|
||||
.authed_get(
|
||||
localpart,
|
||||
&format!("/api/v1/repos/{owner}/{repo}/branches"),
|
||||
)
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
return Err(format!("list branches failed: {text}"));
|
||||
}
|
||||
resp.json().await.map_err(|e| format!("parse error: {e}"))
|
||||
}
|
||||
|
||||
pub async fn create_branch(
|
||||
&self,
|
||||
localpart: &str,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
branch_name: &str,
|
||||
from_branch: Option<&str>,
|
||||
) -> Result<Branch, String> {
|
||||
let mut json = serde_json::json!({
|
||||
"new_branch_name": branch_name,
|
||||
});
|
||||
if let Some(f) = from_branch {
|
||||
json["old_branch_name"] = serde_json::json!(f);
|
||||
}
|
||||
|
||||
let resp = self
|
||||
.authed_post(
|
||||
localpart,
|
||||
&format!("/api/v1/repos/{owner}/{repo}/branches"),
|
||||
&json,
|
||||
)
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
return Err(format!("create branch failed: {text}"));
|
||||
}
|
||||
resp.json().await.map_err(|e| format!("parse error: {e}"))
|
||||
}
|
||||
|
||||
pub async fn delete_branch(
|
||||
&self,
|
||||
localpart: &str,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
branch: &str,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let resp = self
|
||||
.authed_delete(
|
||||
localpart,
|
||||
&format!("/api/v1/repos/{owner}/{repo}/branches/{branch}"),
|
||||
)
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
return Err(format!("delete branch failed: {text}"));
|
||||
}
|
||||
Ok(serde_json::json!({"status": "deleted", "branch": branch}))
|
||||
}
|
||||
|
||||
// ── Organizations ───────────────────────────────────────────────────────
|
||||
|
||||
pub async fn list_orgs(
|
||||
&self,
|
||||
localpart: &str,
|
||||
username: &str,
|
||||
) -> Result<Vec<Organization>, String> {
|
||||
let resp = self
|
||||
.authed_get(
|
||||
localpart,
|
||||
&format!("/api/v1/users/{username}/orgs"),
|
||||
)
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
return Err(format!("list orgs failed: {text}"));
|
||||
}
|
||||
resp.json().await.map_err(|e| format!("parse error: {e}"))
|
||||
}
|
||||
|
||||
pub async fn get_org(
|
||||
&self,
|
||||
localpart: &str,
|
||||
org: &str,
|
||||
) -> Result<Organization, String> {
|
||||
let resp = self
|
||||
.authed_get(
|
||||
localpart,
|
||||
&format!("/api/v1/orgs/{org}"),
|
||||
)
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
return Err(format!("get org failed: {text}"));
|
||||
}
|
||||
resp.json().await.map_err(|e| format!("parse error: {e}"))
|
||||
}
|
||||
|
||||
// ── Notifications ───────────────────────────────────────────────────────
|
||||
|
||||
pub async fn list_notifications(
|
||||
&self,
|
||||
localpart: &str,
|
||||
) -> Result<Vec<Notification>, String> {
|
||||
let resp = self
|
||||
.authed_get(localpart, "/api/v1/notifications")
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
return Err(format!("list notifications failed: {text}"));
|
||||
}
|
||||
resp.json().await.map_err(|e| format!("parse error: {e}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -729,4 +1291,132 @@ mod tests {
|
||||
assert_eq!(file.name, "README.md");
|
||||
assert_eq!(file.file_type, "file");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_comment_deserialize() {
|
||||
let json = serde_json::json!({
|
||||
"id": 99,
|
||||
"body": "looks good to me",
|
||||
"user": { "login": "lonni" },
|
||||
"html_url": "https://src.sunbeam.pt/studio/sol/issues/1#issuecomment-99",
|
||||
"created_at": "2026-03-22T10:00:00Z",
|
||||
"updated_at": "2026-03-22T10:00:00Z",
|
||||
});
|
||||
let comment: Comment = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(comment.id, 99);
|
||||
assert_eq!(comment.body, "looks good to me");
|
||||
assert_eq!(comment.user.login, "lonni");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_branch_deserialize() {
|
||||
let json = serde_json::json!({
|
||||
"name": "feature/auth",
|
||||
"commit": { "id": "abc123def456", "message": "add login flow" },
|
||||
"protected": false,
|
||||
});
|
||||
let branch: Branch = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(branch.name, "feature/auth");
|
||||
assert_eq!(branch.commit.id, "abc123def456");
|
||||
assert!(!branch.protected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_branch_minimal() {
|
||||
let json = serde_json::json!({ "name": "main" });
|
||||
let branch: Branch = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(branch.name, "main");
|
||||
assert_eq!(branch.commit.id, ""); // default
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_organization_deserialize() {
|
||||
let json = serde_json::json!({
|
||||
"id": 1,
|
||||
"username": "studio",
|
||||
"full_name": "Sunbeam Studios",
|
||||
"description": "Game studio",
|
||||
"avatar_url": "https://src.sunbeam.pt/avatars/1",
|
||||
});
|
||||
let org: Organization = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(org.username, "studio");
|
||||
assert_eq!(org.full_name, "Sunbeam Studios");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_notification_deserialize() {
|
||||
let json = serde_json::json!({
|
||||
"id": 42,
|
||||
"subject": { "title": "New issue", "url": "/api/v1/...", "type": "Issue" },
|
||||
"repository": { "full_name": "studio/sol", "description": "" },
|
||||
"unread": true,
|
||||
"updated_at": "2026-03-22T10:00:00Z",
|
||||
});
|
||||
let notif: Notification = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(notif.id, 42);
|
||||
assert!(notif.unread);
|
||||
assert_eq!(notif.subject.title, "New issue");
|
||||
assert_eq!(notif.subject.subject_type, "Issue");
|
||||
assert_eq!(notif.repository.as_ref().unwrap().full_name, "studio/sol");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_notification_minimal() {
|
||||
let json = serde_json::json!({ "id": 1 });
|
||||
let notif: Notification = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(notif.id, 1);
|
||||
assert!(!notif.unread);
|
||||
assert!(notif.repository.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_token_scopes_include_write_repo() {
|
||||
// New tools need write:repository for create/edit/fork/branch operations
|
||||
assert!(TOKEN_SCOPES.contains(&"write:repository"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_token_scopes_include_notifications() {
|
||||
assert!(TOKEN_SCOPES.contains(&"read:notification"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pull_request_with_refs() {
|
||||
let json = serde_json::json!({
|
||||
"number": 3,
|
||||
"title": "Add auth",
|
||||
"body": "implements OIDC",
|
||||
"state": "open",
|
||||
"html_url": "https://src.sunbeam.pt/studio/sol/pulls/3",
|
||||
"user": { "login": "sienna" },
|
||||
"head": { "label": "feature/auth", "ref": "feature/auth", "sha": "abc123" },
|
||||
"base": { "label": "main", "ref": "main", "sha": "def456" },
|
||||
"mergeable": true,
|
||||
"created_at": "2026-03-22T10:00:00Z",
|
||||
"updated_at": "2026-03-22T10:00:00Z",
|
||||
});
|
||||
let pr: PullRequest = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(pr.number, 3);
|
||||
assert_eq!(pr.mergeable, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_repo_full_deserialize() {
|
||||
let json = serde_json::json!({
|
||||
"full_name": "studio/marathon",
|
||||
"description": "P2P game engine",
|
||||
"html_url": "https://src.sunbeam.pt/studio/marathon",
|
||||
"default_branch": "mainline",
|
||||
"open_issues_count": 121,
|
||||
"stars_count": 0,
|
||||
"forks_count": 2,
|
||||
"updated_at": "2026-03-06T13:21:24Z",
|
||||
"private": false,
|
||||
});
|
||||
let repo: Repo = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(repo.full_name, "studio/marathon");
|
||||
assert_eq!(repo.default_branch, "mainline");
|
||||
assert_eq!(repo.open_issues_count, 121);
|
||||
assert_eq!(repo.forks_count, 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +136,270 @@ pub async fn execute(
|
||||
Err(e) => Ok(json!({"error": e}).to_string()),
|
||||
}
|
||||
}
|
||||
"gitea_create_repo" => {
|
||||
let name = args["name"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'name'"))?;
|
||||
let org = args["org"].as_str();
|
||||
let description = args["description"].as_str();
|
||||
let private = args["private"].as_bool();
|
||||
let auto_init = args["auto_init"].as_bool();
|
||||
let default_branch = args["default_branch"].as_str();
|
||||
|
||||
match gitea
|
||||
.create_repo(localpart, name, org, description, private, auto_init, default_branch)
|
||||
.await
|
||||
{
|
||||
Ok(r) => Ok(serde_json::to_string(&r).unwrap_or_default()),
|
||||
Err(e) => Ok(json!({"error": e}).to_string()),
|
||||
}
|
||||
}
|
||||
"gitea_edit_repo" => {
|
||||
let owner = args["owner"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'owner'"))?;
|
||||
let repo = args["repo"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'repo'"))?;
|
||||
let description = args["description"].as_str();
|
||||
let private = args["private"].as_bool();
|
||||
let archived = args["archived"].as_bool();
|
||||
let default_branch = args["default_branch"].as_str();
|
||||
|
||||
match gitea
|
||||
.edit_repo(localpart, owner, repo, description, private, archived, default_branch)
|
||||
.await
|
||||
{
|
||||
Ok(r) => Ok(serde_json::to_string(&r).unwrap_or_default()),
|
||||
Err(e) => Ok(json!({"error": e}).to_string()),
|
||||
}
|
||||
}
|
||||
"gitea_fork_repo" => {
|
||||
let owner = args["owner"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'owner'"))?;
|
||||
let repo = args["repo"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'repo'"))?;
|
||||
let new_name = args["new_name"].as_str();
|
||||
|
||||
match gitea.fork_repo(localpart, owner, repo, new_name).await {
|
||||
Ok(r) => Ok(serde_json::to_string(&r).unwrap_or_default()),
|
||||
Err(e) => Ok(json!({"error": e}).to_string()),
|
||||
}
|
||||
}
|
||||
"gitea_list_org_repos" => {
|
||||
let org = args["org"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'org'"))?;
|
||||
let limit = args["limit"].as_u64().map(|n| n as u32);
|
||||
|
||||
match gitea.list_org_repos(localpart, org, limit).await {
|
||||
Ok(repos) => Ok(serde_json::to_string(&repos).unwrap_or_default()),
|
||||
Err(e) => Ok(json!({"error": e}).to_string()),
|
||||
}
|
||||
}
|
||||
"gitea_edit_issue" => {
|
||||
let owner = args["owner"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'owner'"))?;
|
||||
let repo = args["repo"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'repo'"))?;
|
||||
let number = args["number"]
|
||||
.as_u64()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'number'"))?;
|
||||
let title = args["title"].as_str();
|
||||
let body = args["body"].as_str();
|
||||
let state = args["state"].as_str();
|
||||
let assignees: Option<Vec<String>> = args["assignees"]
|
||||
.as_array()
|
||||
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect());
|
||||
|
||||
match gitea
|
||||
.edit_issue(localpart, owner, repo, number, title, body, state, assignees.as_deref())
|
||||
.await
|
||||
{
|
||||
Ok(issue) => Ok(serde_json::to_string(&issue).unwrap_or_default()),
|
||||
Err(e) => Ok(json!({"error": e}).to_string()),
|
||||
}
|
||||
}
|
||||
"gitea_list_comments" => {
|
||||
let owner = args["owner"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'owner'"))?;
|
||||
let repo = args["repo"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'repo'"))?;
|
||||
let number = args["number"]
|
||||
.as_u64()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'number'"))?;
|
||||
|
||||
match gitea.list_comments(localpart, owner, repo, number).await {
|
||||
Ok(comments) => Ok(serde_json::to_string(&comments).unwrap_or_default()),
|
||||
Err(e) => Ok(json!({"error": e}).to_string()),
|
||||
}
|
||||
}
|
||||
"gitea_create_comment" => {
|
||||
let owner = args["owner"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'owner'"))?;
|
||||
let repo = args["repo"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'repo'"))?;
|
||||
let number = args["number"]
|
||||
.as_u64()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'number'"))?;
|
||||
let body = args["body"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'body'"))?;
|
||||
|
||||
match gitea
|
||||
.create_comment(localpart, owner, repo, number, body)
|
||||
.await
|
||||
{
|
||||
Ok(comment) => Ok(serde_json::to_string(&comment).unwrap_or_default()),
|
||||
Err(e) => Ok(json!({"error": e}).to_string()),
|
||||
}
|
||||
}
|
||||
"gitea_get_pull" => {
|
||||
let owner = args["owner"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'owner'"))?;
|
||||
let repo = args["repo"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'repo'"))?;
|
||||
let number = args["number"]
|
||||
.as_u64()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'number'"))?;
|
||||
|
||||
match gitea.get_pull(localpart, owner, repo, number).await {
|
||||
Ok(pr) => Ok(serde_json::to_string(&pr).unwrap_or_default()),
|
||||
Err(e) => Ok(json!({"error": e}).to_string()),
|
||||
}
|
||||
}
|
||||
"gitea_create_pull" => {
|
||||
let owner = args["owner"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'owner'"))?;
|
||||
let repo = args["repo"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'repo'"))?;
|
||||
let title = args["title"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'title'"))?;
|
||||
let head = args["head"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'head'"))?;
|
||||
let base = args["base"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'base'"))?;
|
||||
let body = args["body"].as_str();
|
||||
|
||||
match gitea
|
||||
.create_pull(localpart, owner, repo, title, head, base, body)
|
||||
.await
|
||||
{
|
||||
Ok(pr) => Ok(serde_json::to_string(&pr).unwrap_or_default()),
|
||||
Err(e) => Ok(json!({"error": e}).to_string()),
|
||||
}
|
||||
}
|
||||
"gitea_merge_pull" => {
|
||||
let owner = args["owner"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'owner'"))?;
|
||||
let repo = args["repo"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'repo'"))?;
|
||||
let number = args["number"]
|
||||
.as_u64()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'number'"))?;
|
||||
let method = args["method"].as_str();
|
||||
let delete_branch = args["delete_branch"].as_bool();
|
||||
|
||||
match gitea
|
||||
.merge_pull(localpart, owner, repo, number, method, delete_branch)
|
||||
.await
|
||||
{
|
||||
Ok(result) => Ok(serde_json::to_string(&result).unwrap_or_default()),
|
||||
Err(e) => Ok(json!({"error": e}).to_string()),
|
||||
}
|
||||
}
|
||||
"gitea_list_branches" => {
|
||||
let owner = args["owner"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'owner'"))?;
|
||||
let repo = args["repo"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'repo'"))?;
|
||||
|
||||
match gitea.list_branches(localpart, owner, repo).await {
|
||||
Ok(branches) => Ok(serde_json::to_string(&branches).unwrap_or_default()),
|
||||
Err(e) => Ok(json!({"error": e}).to_string()),
|
||||
}
|
||||
}
|
||||
"gitea_create_branch" => {
|
||||
let owner = args["owner"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'owner'"))?;
|
||||
let repo = args["repo"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'repo'"))?;
|
||||
let branch_name = args["branch_name"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'branch_name'"))?;
|
||||
let from_branch = args["from_branch"].as_str();
|
||||
|
||||
match gitea
|
||||
.create_branch(localpart, owner, repo, branch_name, from_branch)
|
||||
.await
|
||||
{
|
||||
Ok(branch) => Ok(serde_json::to_string(&branch).unwrap_or_default()),
|
||||
Err(e) => Ok(json!({"error": e}).to_string()),
|
||||
}
|
||||
}
|
||||
"gitea_delete_branch" => {
|
||||
let owner = args["owner"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'owner'"))?;
|
||||
let repo = args["repo"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'repo'"))?;
|
||||
let branch = args["branch"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'branch'"))?;
|
||||
|
||||
match gitea.delete_branch(localpart, owner, repo, branch).await {
|
||||
Ok(result) => Ok(serde_json::to_string(&result).unwrap_or_default()),
|
||||
Err(e) => Ok(json!({"error": e}).to_string()),
|
||||
}
|
||||
}
|
||||
"gitea_list_orgs" => {
|
||||
let username = args["username"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'username'"))?;
|
||||
|
||||
match gitea.list_orgs(localpart, username).await {
|
||||
Ok(orgs) => Ok(serde_json::to_string(&orgs).unwrap_or_default()),
|
||||
Err(e) => Ok(json!({"error": e}).to_string()),
|
||||
}
|
||||
}
|
||||
"gitea_get_org" => {
|
||||
let org = args["org"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'org'"))?;
|
||||
|
||||
match gitea.get_org(localpart, org).await {
|
||||
Ok(o) => Ok(serde_json::to_string(&o).unwrap_or_default()),
|
||||
Err(e) => Ok(json!({"error": e}).to_string()),
|
||||
}
|
||||
}
|
||||
"gitea_list_notifications" => {
|
||||
match gitea.list_notifications(localpart).await {
|
||||
Ok(notifs) => Ok(serde_json::to_string(¬ifs).unwrap_or_default()),
|
||||
Err(e) => Ok(json!({"error": e}).to_string()),
|
||||
}
|
||||
}
|
||||
_ => anyhow::bail!("Unknown devtools tool: {name}"),
|
||||
}
|
||||
}
|
||||
@@ -297,7 +561,10 @@ pub fn tool_definitions() -> Vec<mistralai_client::v1::tool::Tool> {
|
||||
),
|
||||
Tool::new(
|
||||
"gitea_get_file".into(),
|
||||
"Get the contents of a file from a repository.".into(),
|
||||
"Get file contents or list directory entries. Use with path='' to list the repo root. \
|
||||
Use with a directory path to list its contents. Use with a file path to get the file. \
|
||||
This is how you explore and browse repositories."
|
||||
.into(),
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -321,5 +588,394 @@ pub fn tool_definitions() -> Vec<mistralai_client::v1::tool::Tool> {
|
||||
"required": ["owner", "repo", "path"]
|
||||
}),
|
||||
),
|
||||
// ── Repos (new) ─────────────────────────────────────────────────────
|
||||
Tool::new(
|
||||
"gitea_create_repo".into(),
|
||||
"Create a new repository for the requesting user, or under an org.".into(),
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Repository name"
|
||||
},
|
||||
"org": {
|
||||
"type": "string",
|
||||
"description": "Organization to create the repo under (omit for personal repo)"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Repository description"
|
||||
},
|
||||
"private": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the repo is private (default: false)"
|
||||
},
|
||||
"auto_init": {
|
||||
"type": "boolean",
|
||||
"description": "Initialize with a README (default: false)"
|
||||
},
|
||||
"default_branch": {
|
||||
"type": "string",
|
||||
"description": "Default branch name (default: main)"
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
}),
|
||||
),
|
||||
Tool::new(
|
||||
"gitea_edit_repo".into(),
|
||||
"Update repository settings (description, visibility, archived, default branch).".into(),
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "Repository owner"
|
||||
},
|
||||
"repo": {
|
||||
"type": "string",
|
||||
"description": "Repository name"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "New description"
|
||||
},
|
||||
"private": {
|
||||
"type": "boolean",
|
||||
"description": "Set visibility"
|
||||
},
|
||||
"archived": {
|
||||
"type": "boolean",
|
||||
"description": "Archive or unarchive the repo"
|
||||
},
|
||||
"default_branch": {
|
||||
"type": "string",
|
||||
"description": "Set default branch"
|
||||
}
|
||||
},
|
||||
"required": ["owner", "repo"]
|
||||
}),
|
||||
),
|
||||
Tool::new(
|
||||
"gitea_fork_repo".into(),
|
||||
"Fork a repository into the requesting user's account.".into(),
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "Repository owner"
|
||||
},
|
||||
"repo": {
|
||||
"type": "string",
|
||||
"description": "Repository name"
|
||||
},
|
||||
"new_name": {
|
||||
"type": "string",
|
||||
"description": "Name for the forked repo (default: same as original)"
|
||||
}
|
||||
},
|
||||
"required": ["owner", "repo"]
|
||||
}),
|
||||
),
|
||||
Tool::new(
|
||||
"gitea_list_org_repos".into(),
|
||||
"List repositories belonging to an organization.".into(),
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"org": {
|
||||
"type": "string",
|
||||
"description": "Organization name"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Max results (default 20)"
|
||||
}
|
||||
},
|
||||
"required": ["org"]
|
||||
}),
|
||||
),
|
||||
// ── Issues (new) ────────────────────────────────────────────────────
|
||||
Tool::new(
|
||||
"gitea_edit_issue".into(),
|
||||
"Update an issue (title, body, state, assignees).".into(),
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "Repository owner"
|
||||
},
|
||||
"repo": {
|
||||
"type": "string",
|
||||
"description": "Repository name"
|
||||
},
|
||||
"number": {
|
||||
"type": "integer",
|
||||
"description": "Issue number"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "New title"
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"description": "New body (markdown)"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"description": "Set state: open or closed"
|
||||
},
|
||||
"assignees": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Usernames to assign"
|
||||
}
|
||||
},
|
||||
"required": ["owner", "repo", "number"]
|
||||
}),
|
||||
),
|
||||
Tool::new(
|
||||
"gitea_list_comments".into(),
|
||||
"List comments on an issue.".into(),
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "Repository owner"
|
||||
},
|
||||
"repo": {
|
||||
"type": "string",
|
||||
"description": "Repository name"
|
||||
},
|
||||
"number": {
|
||||
"type": "integer",
|
||||
"description": "Issue number"
|
||||
}
|
||||
},
|
||||
"required": ["owner", "repo", "number"]
|
||||
}),
|
||||
),
|
||||
Tool::new(
|
||||
"gitea_create_comment".into(),
|
||||
"Add a comment to an issue. Authored by the requesting user.".into(),
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "Repository owner"
|
||||
},
|
||||
"repo": {
|
||||
"type": "string",
|
||||
"description": "Repository name"
|
||||
},
|
||||
"number": {
|
||||
"type": "integer",
|
||||
"description": "Issue number"
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"description": "Comment body (markdown)"
|
||||
}
|
||||
},
|
||||
"required": ["owner", "repo", "number", "body"]
|
||||
}),
|
||||
),
|
||||
// ── Pull requests (new) ─────────────────────────────────────────────
|
||||
Tool::new(
|
||||
"gitea_get_pull".into(),
|
||||
"Get details of a specific pull request by number.".into(),
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "Repository owner"
|
||||
},
|
||||
"repo": {
|
||||
"type": "string",
|
||||
"description": "Repository name"
|
||||
},
|
||||
"number": {
|
||||
"type": "integer",
|
||||
"description": "Pull request number"
|
||||
}
|
||||
},
|
||||
"required": ["owner", "repo", "number"]
|
||||
}),
|
||||
),
|
||||
Tool::new(
|
||||
"gitea_create_pull".into(),
|
||||
"Create a pull request. Authored by the requesting user.".into(),
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "Repository owner"
|
||||
},
|
||||
"repo": {
|
||||
"type": "string",
|
||||
"description": "Repository name"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "PR title"
|
||||
},
|
||||
"head": {
|
||||
"type": "string",
|
||||
"description": "Source branch"
|
||||
},
|
||||
"base": {
|
||||
"type": "string",
|
||||
"description": "Target branch"
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"description": "PR description (markdown)"
|
||||
}
|
||||
},
|
||||
"required": ["owner", "repo", "title", "head", "base"]
|
||||
}),
|
||||
),
|
||||
Tool::new(
|
||||
"gitea_merge_pull".into(),
|
||||
"Merge a pull request.".into(),
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "Repository owner"
|
||||
},
|
||||
"repo": {
|
||||
"type": "string",
|
||||
"description": "Repository name"
|
||||
},
|
||||
"number": {
|
||||
"type": "integer",
|
||||
"description": "Pull request number"
|
||||
},
|
||||
"method": {
|
||||
"type": "string",
|
||||
"description": "Merge method: merge, rebase, or squash (default: merge)"
|
||||
},
|
||||
"delete_branch": {
|
||||
"type": "boolean",
|
||||
"description": "Delete head branch after merge (default: false)"
|
||||
}
|
||||
},
|
||||
"required": ["owner", "repo", "number"]
|
||||
}),
|
||||
),
|
||||
// ── Branches (new) ──────────────────────────────────────────────────
|
||||
Tool::new(
|
||||
"gitea_list_branches".into(),
|
||||
"List branches in a repository.".into(),
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "Repository owner"
|
||||
},
|
||||
"repo": {
|
||||
"type": "string",
|
||||
"description": "Repository name"
|
||||
}
|
||||
},
|
||||
"required": ["owner", "repo"]
|
||||
}),
|
||||
),
|
||||
Tool::new(
|
||||
"gitea_create_branch".into(),
|
||||
"Create a new branch in a repository.".into(),
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "Repository owner"
|
||||
},
|
||||
"repo": {
|
||||
"type": "string",
|
||||
"description": "Repository name"
|
||||
},
|
||||
"branch_name": {
|
||||
"type": "string",
|
||||
"description": "Name for the new branch"
|
||||
},
|
||||
"from_branch": {
|
||||
"type": "string",
|
||||
"description": "Branch to create from (default: default branch)"
|
||||
}
|
||||
},
|
||||
"required": ["owner", "repo", "branch_name"]
|
||||
}),
|
||||
),
|
||||
Tool::new(
|
||||
"gitea_delete_branch".into(),
|
||||
"Delete a branch from a repository.".into(),
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "Repository owner"
|
||||
},
|
||||
"repo": {
|
||||
"type": "string",
|
||||
"description": "Repository name"
|
||||
},
|
||||
"branch": {
|
||||
"type": "string",
|
||||
"description": "Branch name to delete"
|
||||
}
|
||||
},
|
||||
"required": ["owner", "repo", "branch"]
|
||||
}),
|
||||
),
|
||||
// ── Organizations (new) ─────────────────────────────────────────────
|
||||
Tool::new(
|
||||
"gitea_list_orgs".into(),
|
||||
"List organizations a user belongs to.".into(),
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "string",
|
||||
"description": "Username to list orgs for"
|
||||
}
|
||||
},
|
||||
"required": ["username"]
|
||||
}),
|
||||
),
|
||||
Tool::new(
|
||||
"gitea_get_org".into(),
|
||||
"Get details about an organization.".into(),
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"org": {
|
||||
"type": "string",
|
||||
"description": "Organization name"
|
||||
}
|
||||
},
|
||||
"required": ["org"]
|
||||
}),
|
||||
),
|
||||
// ── Notifications (new) ─────────────────────────────────────────────
|
||||
Tool::new(
|
||||
"gitea_list_notifications".into(),
|
||||
"List unread notifications for the requesting user.".into(),
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user