init
This commit is contained in:
commit
4cf726709f
36 files changed
+8617
No files matched your search
@@ -0,0 +1,193 @@
|
||||
use super::{PrInfo, Provider, RepoInfo, RepoMeta};
|
||||
use anyhow::{Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use reqwest::{header, Client, Response};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
pub struct GitLabProvider {
|
||||
client: Client,
|
||||
namespace: String,
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
impl GitLabProvider {
|
||||
pub fn new(base_url: &str, namespace: &str, token: &str) -> Result<Self> {
|
||||
let mut headers = header::HeaderMap::new();
|
||||
headers.insert(
|
||||
"PRIVATE-TOKEN",
|
||||
header::HeaderValue::from_str(token)?,
|
||||
);
|
||||
headers.insert(
|
||||
header::USER_AGENT,
|
||||
header::HeaderValue::from_static("fxg/0.1"),
|
||||
);
|
||||
let client = Client::builder().default_headers(headers).build()?;
|
||||
Ok(Self {
|
||||
client,
|
||||
namespace: namespace.to_string(),
|
||||
base_url: base_url.trim_end_matches('/').to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn url(&self, path: &str) -> String {
|
||||
format!("{}/{}", self.base_url, path.trim_start_matches('/'))
|
||||
}
|
||||
|
||||
fn repo_id(&self, repo: &str) -> String {
|
||||
// Percent-encode "namespace/repo" for use in URL path segments
|
||||
let raw = format!("{}/{}", self.namespace, repo);
|
||||
raw.replace('/', "%2F")
|
||||
}
|
||||
|
||||
async fn check(resp: Response) -> Result<Value> {
|
||||
let status = resp.status();
|
||||
let text = resp.text().await?;
|
||||
if status.is_success() {
|
||||
Ok(serde_json::from_str(&text).unwrap_or(Value::Null))
|
||||
} else {
|
||||
anyhow::bail!("GitLab API {} — {}", status, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Provider for GitLabProvider {
|
||||
async fn create_repo(&self, name: &str, private: bool, description: &str) -> Result<RepoInfo> {
|
||||
let visibility = if private { "private" } else { "public" };
|
||||
let body = json!({
|
||||
"name": name,
|
||||
"visibility": visibility,
|
||||
"description": description,
|
||||
"namespace_path": self.namespace,
|
||||
});
|
||||
let resp = self.client.post(self.url("projects")).json(&body).send().await?;
|
||||
let v = Self::check(resp).await.context("create_repo")?;
|
||||
Ok(RepoInfo {
|
||||
name: v["name"].as_str().unwrap_or(name).to_string(),
|
||||
url: v["web_url"].as_str().unwrap_or("").to_string(),
|
||||
ssh_url: v["ssh_url_to_repo"].as_str().unwrap_or("").to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn delete_repo(&self, name: &str) -> Result<()> {
|
||||
let resp = self.client
|
||||
.delete(self.url(&format!("projects/{}", self.repo_id(name))))
|
||||
.send().await?;
|
||||
Self::check(resp).await.context("delete_repo")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_topics(&self, repo: &str, topics: &[String]) -> Result<()> {
|
||||
let body = json!({ "topics": topics });
|
||||
let resp = self.client
|
||||
.put(self.url(&format!("projects/{}", self.repo_id(repo))))
|
||||
.json(&body).send().await?;
|
||||
Self::check(resp).await.context("set_topics")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_branch(&self, repo: &str, branch: &str, from: &str) -> Result<()> {
|
||||
let body = json!({ "branch": branch, "ref": from });
|
||||
let resp = self.client
|
||||
.post(self.url(&format!("projects/{}/repository/branches", self.repo_id(repo))))
|
||||
.json(&body).send().await?;
|
||||
Self::check(resp).await.context("create_branch")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_pull_request(
|
||||
&self, repo: &str, title: &str, body: &str, head: &str, base: &str,
|
||||
) -> Result<PrInfo> {
|
||||
let payload = json!({
|
||||
"title": title, "description": body,
|
||||
"source_branch": head, "target_branch": base,
|
||||
});
|
||||
let resp = self.client
|
||||
.post(self.url(&format!("projects/{}/merge_requests", self.repo_id(repo))))
|
||||
.json(&payload).send().await?;
|
||||
let v = Self::check(resp).await.context("create_pull_request")?;
|
||||
Ok(PrInfo {
|
||||
number: v["iid"].as_u64().unwrap_or(0),
|
||||
url: v["web_url"].as_str().unwrap_or("").to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn request_reviewers(
|
||||
&self, repo: &str, pr_number: u64, reviewers: &[String],
|
||||
) -> Result<()> {
|
||||
// Resolve usernames to GitLab user IDs
|
||||
let mut reviewer_ids: Vec<u64> = Vec::new();
|
||||
for username in reviewers {
|
||||
let resp = self.client
|
||||
.get(self.url(&format!("users?username={}", username)))
|
||||
.send().await?;
|
||||
if let Ok(v) = Self::check(resp).await {
|
||||
if let Some(id) = v.as_array()
|
||||
.and_then(|a| a.first())
|
||||
.and_then(|u| u["id"].as_u64())
|
||||
{
|
||||
reviewer_ids.push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !reviewer_ids.is_empty() {
|
||||
let body = json!({ "reviewer_ids": reviewer_ids });
|
||||
let resp = self.client
|
||||
.put(self.url(&format!("projects/{}/merge_requests/{}", self.repo_id(repo), pr_number)))
|
||||
.json(&body).send().await?;
|
||||
Self::check(resp).await.context("request_reviewers")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_labels(
|
||||
&self, repo: &str, pr_number: u64, labels: &[String],
|
||||
) -> Result<()> {
|
||||
let body = json!({ "add_labels": labels.join(",") });
|
||||
let resp = self.client
|
||||
.put(self.url(&format!("projects/{}/merge_requests/{}", self.repo_id(repo), pr_number)))
|
||||
.json(&body).send().await?;
|
||||
Self::check(resp).await.context("add_labels")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_collaborator(&self, repo: &str, user: &str, role: &str) -> Result<()> {
|
||||
// GitLab access levels: 30=developer, 40=maintainer, 50=owner
|
||||
let level: u8 = match role { "read" => 20, "write" => 30, _ => 40 };
|
||||
let body = json!({ "user_id": user, "access_level": level });
|
||||
let resp = self.client
|
||||
.post(self.url(&format!("projects/{}/members", self.repo_id(repo))))
|
||||
.json(&body).send().await?;
|
||||
Self::check(resp).await.context("add_collaborator")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_release(
|
||||
&self, repo: &str, tag: &str, title: &str, body: &str, _draft: bool,
|
||||
) -> Result<String> {
|
||||
let payload = json!({ "tag_name": tag, "name": title, "description": body });
|
||||
let resp = self.client
|
||||
.post(self.url(&format!("projects/{}/releases", self.repo_id(repo))))
|
||||
.json(&payload).send().await?;
|
||||
let v = Self::check(resp).await.context("create_release")?;
|
||||
Ok(v["_links"]["self"].as_str().unwrap_or("").to_string())
|
||||
}
|
||||
|
||||
async fn repo_meta(&self, repo: &str) -> Result<RepoMeta> {
|
||||
let resp = self.client
|
||||
.get(self.url(&format!("projects/{}", self.repo_id(repo))))
|
||||
.send().await?;
|
||||
let v = Self::check(resp).await.context("repo_meta")?;
|
||||
Ok(RepoMeta {
|
||||
open_issues: v["open_issues_count"].as_u64().unwrap_or(0) as u32,
|
||||
open_prs: v["merge_requests_count"].as_u64().unwrap_or(0) as u32,
|
||||
stars: v["star_count"].as_u64().unwrap_or(0) as u32,
|
||||
})
|
||||
}
|
||||
|
||||
async fn whoami(&self) -> Result<String> {
|
||||
let resp = self.client.get(self.url("user")).send().await?;
|
||||
let v = Self::check(resp).await.context("whoami")?;
|
||||
Ok(v["username"].as_str().unwrap_or("unknown").to_string())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user