| 1 | //! Repositories and their webhooks. |
| 2 | |
| 3 | use crate::{ |
| 4 | client::Client, |
| 5 | error::Result, |
| 6 | models::{GitCredential, Hook, Repository}, |
| 7 | }; |
| 8 | |
| 9 | /// Fields accepted by `POST /api/v1/repos`. |
| 10 | #[derive(Debug, Clone, serde::Serialize)] |
| 11 | pub struct NewRepository { |
| 12 | pub name: String, |
| 13 | pub is_private: bool, |
| 14 | #[serde(skip_serializing_if = "Option::is_none")] |
| 15 | pub description: Option<String>, |
| 16 | } |
| 17 | |
| 18 | impl NewRepository { |
| 19 | pub fn new(name: impl Into<String>) -> Self { |
| 20 | Self { |
| 21 | name: name.into(), |
| 22 | is_private: false, |
| 23 | description: None, |
| 24 | } |
| 25 | } |
| 26 | |
| 27 | pub fn private(mut self, private: bool) -> Self { |
| 28 | self.is_private = private; |
| 29 | self |
| 30 | } |
| 31 | |
| 32 | pub fn description(mut self, description: Option<String>) -> Self { |
| 33 | self.description = description.filter(|d| !d.trim().is_empty()); |
| 34 | self |
| 35 | } |
| 36 | } |
| 37 | |
| 38 | impl Client { |
| 39 | /// `GET /api/v1/repos` — the signed-in user's repositories, newest first. |
| 40 | pub async fn repos(&self) -> Result<Vec<Repository>> { |
| 41 | self.get("repos").await |
| 42 | } |
| 43 | |
| 44 | /// `POST /api/v1/repos` — creates an empty bare repo to push to. |
| 45 | pub async fn create_repo(&self, repo: &NewRepository) -> Result<Repository> { |
| 46 | self.post("repos", repo).await |
| 47 | } |
| 48 | |
| 49 | /// `GET /api/v1/repos/:owner/:repo/hooks` |
| 50 | pub async fn hooks(&self, owner: &str, repo: &str) -> Result<Vec<Hook>> { |
| 51 | self.get(&format!("repos/{owner}/{repo}/hooks")).await |
| 52 | } |
| 53 | |
| 54 | /// `POST /api/v1/repos/:owner/:repo/hooks` |
| 55 | /// |
| 56 | /// The response is the only place the signing secret ever appears. |
| 57 | pub async fn create_hook( |
| 58 | &self, |
| 59 | owner: &str, |
| 60 | repo: &str, |
| 61 | url: &str, |
| 62 | events: &[String], |
| 63 | ) -> Result<Hook> { |
| 64 | let body = serde_json::json!({ "url": url, "events": events }); |
| 65 | self.post(&format!("repos/{owner}/{repo}/hooks"), &body) |
| 66 | .await |
| 67 | } |
| 68 | |
| 69 | /// `DELETE /api/v1/repos/:owner/:repo/hooks/:id` |
| 70 | pub async fn delete_hook(&self, owner: &str, repo: &str, id: i64) -> Result<()> { |
| 71 | self.delete(&format!("repos/{owner}/{repo}/hooks/{id}")) |
| 72 | .await |
| 73 | } |
| 74 | |
| 75 | /// `POST /api/v1/git_credentials` — mints a 1-hour, read-scoped git |
| 76 | /// password. This, not the account token, is what git clones with. |
| 77 | pub async fn git_credential(&self) -> Result<GitCredential> { |
| 78 | self.post("git_credentials", &serde_json::json!({})).await |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | #[cfg(test)] |
| 83 | mod tests { |
| 84 | use super::*; |
| 85 | |
| 86 | #[test] |
| 87 | fn a_blank_description_is_omitted_rather_than_sent_as_empty() { |
| 88 | let repo = NewRepository::new("demo").description(Some(" ".into())); |
| 89 | let json = serde_json::to_value(&repo).unwrap(); |
| 90 | assert!(json.get("description").is_none()); |
| 91 | } |
| 92 | |
| 93 | #[test] |
| 94 | fn a_real_description_is_sent() { |
| 95 | let repo = NewRepository::new("demo") |
| 96 | .private(true) |
| 97 | .description(Some("A demo repo".into())); |
| 98 | let json = serde_json::to_value(&repo).unwrap(); |
| 99 | assert_eq!(json["description"], "A demo repo"); |
| 100 | assert_eq!(json["is_private"], true); |
| 101 | } |
| 102 | } |