//! Git repositories and the SSH keys that reach them. //! //! Addressed by repo NAME, matching the CLI's own vocabulary. The browser API //! keys on the repo id because a page has the row loaded; a person at a //! terminal does not. use super::{MnwApiClient, empty_response, json_response}; use serde::{Deserialize, Serialize}; /// A repository as `repo list` renders it. #[derive(Debug, Clone, Deserialize, Serialize)] pub(crate) struct CliRepo { pub name: String, pub visibility: String, pub description: String, pub created_at: String, } /// A repository plus its issue counts, for `repo info`. #[derive(Debug, Clone, Deserialize, Serialize)] pub(crate) struct CliRepoInfo { pub name: String, pub visibility: String, pub description: String, pub created_at: String, pub open_issues: i64, pub closed_issues: i64, } /// An SSH key as `key list` renders it. #[derive(Debug, Clone, Deserialize, Serialize)] pub(crate) struct CliSshKey { pub fingerprint: String, pub label: String, pub created_at: String, } /// A registered SSH key. #[derive(Debug, Clone, Deserialize, Serialize)] pub(crate) struct SshKeyInfo { pub id: String, pub label: String, pub fingerprint: String, pub created_at: String, } /// Response from the git authorize endpoint. #[derive(Debug, Deserialize)] pub(crate) struct GitAuthResponse { pub repo_path: String, } impl MnwApiClient { /// Authorize a git operation and get the on-disk repo path. pub(crate) async fn git_authorize( &self, user_id: &str, operation: &str, owner: &str, repo_name: &str, ) -> anyhow::Result { let url = format!("{}/api/internal/git/authorize", self.base_url); let resp = self .http .post(&url) .bearer_auth(&self.service_token) .header("X-MNW-Actor", self.actor_header()) .json(&serde_json::json!({ "user_id": user_id, "operation": operation, "owner": owner, "repo_name": repo_name, })) .send() .await?; if !resp.status().is_success() { let status = resp.status(); let body = resp.text().await.unwrap_or_else(|e| { tracing::warn!(error = %e, "failed to read git_authorize error body"); String::new() }); // Parse JSON error if available, fall back to status text let msg = serde_json::from_str::(&body) .ok() .and_then(|v| v.get("error").and_then(|e| e.as_str()).map(String::from)) .unwrap_or_else(|| format!("HTTP {status}")); anyhow::bail!("{msg}"); } Ok(resp.json().await?) } /// List registered SSH keys for a user. pub(crate) async fn list_ssh_keys(&self, user_id: &str) -> anyhow::Result> { let url = format!("{}/api/internal/creator/ssh-keys", self.base_url); let resp = self .http .get(&url) .bearer_auth(&self.service_token) .header("X-MNW-Actor", self.actor_header()) .query(&[("user_id", user_id)]) .send() .await?; json_response(resp, "list_ssh_keys").await } pub(crate) async fn repo_list(&self, user_id: &str) -> anyhow::Result> { let url = format!("{}/api/internal/creator/repos", self.base_url); let resp = self .http .get(&url) .bearer_auth(&self.service_token) .header("X-MNW-Actor", self.actor_header()) .query(&[("user_id", user_id)]) .send() .await?; json_response(resp, "repo_list").await } pub(crate) async fn repo_info(&self, user_id: &str, name: &str) -> anyhow::Result { let url = format!("{}/api/internal/creator/repos/{name}", self.base_url); let resp = self .http .get(&url) .bearer_auth(&self.service_token) .header("X-MNW-Actor", self.actor_header()) .query(&[("user_id", user_id)]) .send() .await?; json_response(resp, "repo_info").await } pub(crate) async fn repo_set_visibility( &self, user_id: &str, name: &str, visibility: &str, ) -> anyhow::Result<()> { let url = format!( "{}/api/internal/creator/repos/{name}/visibility", self.base_url ); let resp = self .http .put(&url) .bearer_auth(&self.service_token) .header("X-MNW-Actor", self.actor_header()) .query(&[("user_id", user_id)]) .json(&serde_json::json!({ "visibility": visibility })) .send() .await?; empty_response(resp, "repo_set_visibility").await } pub(crate) async fn repo_set_description( &self, user_id: &str, name: &str, description: &str, ) -> anyhow::Result<()> { let url = format!( "{}/api/internal/creator/repos/{name}/description", self.base_url ); let resp = self .http .put(&url) .bearer_auth(&self.service_token) .header("X-MNW-Actor", self.actor_header()) .query(&[("user_id", user_id)]) .json(&serde_json::json!({ "description": description })) .send() .await?; empty_response(resp, "repo_set_description").await } pub(crate) async fn repo_delete(&self, user_id: &str, name: &str) -> anyhow::Result<()> { let url = format!("{}/api/internal/creator/repos/{name}", self.base_url); let resp = self .http .delete(&url) .bearer_auth(&self.service_token) .header("X-MNW-Actor", self.actor_header()) .query(&[("user_id", user_id)]) .send() .await?; empty_response(resp, "repo_delete").await } pub(crate) async fn key_list(&self, user_id: &str) -> anyhow::Result> { let url = format!("{}/api/internal/creator/ssh-keys", self.base_url); let resp = self .http .get(&url) .bearer_auth(&self.service_token) .header("X-MNW-Actor", self.actor_header()) .query(&[("user_id", user_id)]) .send() .await?; json_response(resp, "key_list").await } pub(crate) async fn key_remove(&self, user_id: &str, fingerprint: &str) -> anyhow::Result<()> { let url = format!( "{}/api/internal/creator/ssh-keys/{fingerprint}", self.base_url ); let resp = self .http .delete(&url) .bearer_auth(&self.service_token) .header("X-MNW-Actor", self.actor_header()) .query(&[("user_id", user_id)]) .send() .await?; empty_response(resp, "key_remove").await } }