Skip to main content

github_bot_sdk/client/
repository.rs

1//! Repository Operations
2//!
3//! **Specification**: `docs/specs/interfaces/repository-operations.md`
4
5use crate::{client::InstallationClient, error::ApiError};
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9#[cfg(test)]
10#[path = "repository_tests.rs"]
11mod tests;
12
13/// GitHub repository with metadata.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct Repository {
16    pub id: u64,
17    pub name: String,
18    pub full_name: String,
19    pub owner: RepositoryOwner,
20    pub description: Option<String>,
21    pub private: bool,
22    pub default_branch: String,
23    pub html_url: String,
24    pub clone_url: String,
25    pub ssh_url: String,
26    pub created_at: DateTime<Utc>,
27    pub updated_at: DateTime<Utc>,
28}
29
30/// Repository owner (user or organization).
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct RepositoryOwner {
33    pub login: String,
34    pub id: u64,
35    pub avatar_url: String,
36    #[serde(rename = "type")]
37    pub owner_type: OwnerType,
38}
39
40/// Owner type classification.
41#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
42pub enum OwnerType {
43    User,
44    Organization,
45}
46
47/// Git branch with commit information.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct Branch {
50    pub name: String,
51    pub commit: Commit,
52    pub protected: bool,
53}
54
55/// Commit reference (used in branches, tags, and pull requests).
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct Commit {
58    pub sha: String,
59    pub url: String,
60}
61
62/// Git reference (branch, tag, etc.).
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct GitRef {
65    #[serde(rename = "ref")]
66    pub ref_name: String,
67    pub node_id: String,
68    pub url: String,
69    pub object: GitRefObject,
70}
71
72/// Git reference object information.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct GitRefObject {
75    pub sha: String,
76    #[serde(rename = "type")]
77    pub object_type: GitObjectType,
78    pub url: String,
79}
80
81/// Git object type classification.
82#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
83#[serde(rename_all = "lowercase")]
84pub enum GitObjectType {
85    Commit,
86    Tree,
87    Blob,
88    Tag,
89}
90
91/// Git tag information.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct Tag {
94    pub name: String,
95    pub commit: Commit,
96    pub zipball_url: String,
97    pub tarball_url: String,
98}
99
100/// Request body for creating a Git reference.
101#[derive(Debug, Serialize)]
102struct CreateGitRefRequest {
103    #[serde(rename = "ref")]
104    ref_name: String,
105    sha: String,
106}
107
108/// Request body for updating a Git reference.
109#[derive(Debug, Serialize)]
110struct UpdateGitRefRequest {
111    sha: String,
112    force: bool,
113}
114
115// ============================================================================
116// RepositoriesClient
117// ============================================================================
118
119/// Domain client for repository, branch, tag, git-ref, and commit operations.
120///
121/// Obtained via [`InstallationClient::repositories()`]. Cheap to clone (Arc-backed).
122///
123/// See docs/specs/interfaces/repository-operations.md
124#[derive(Debug, Clone)]
125pub struct RepositoriesClient {
126    pub(crate) client: InstallationClient,
127}
128
129impl RepositoriesClient {
130    pub(crate) fn new(client: InstallationClient) -> Self {
131        Self { client }
132    }
133
134    /// Get repository metadata.
135    ///
136    /// Retrieves complete metadata for a repository including owner information,
137    /// visibility settings, default branch, and timestamps.
138    ///
139    /// # Arguments
140    ///
141    /// * `owner` - Repository owner (username or organization)
142    /// * `repo` - Repository name
143    ///
144    /// # Returns
145    ///
146    /// Returns `Repository` with complete metadata on success.
147    ///
148    /// # Errors
149    ///
150    /// * `ApiError::NotFound` - Repository does not exist or is not accessible
151    /// * `ApiError::AuthorizationFailed` - Insufficient permissions to access repository
152    /// * `ApiError::HttpError` - GitHub API returned an error
153    ///
154    /// # Example
155    ///
156    /// ```no_run
157    /// # use github_bot_sdk::client::RepositoriesClient;
158    /// # async fn example(client: &RepositoriesClient) -> Result<(), Box<dyn std::error::Error>> {
159    /// let repo = client.get("octocat", "Hello-World").await?;
160    /// println!("Repository: {}", repo.full_name);
161    /// println!("Default branch: {}", repo.default_branch);
162    /// # Ok(())
163    /// # }
164    /// ```
165    pub async fn get(&self, owner: &str, repo: &str) -> Result<Repository, ApiError> {
166        let path = format!("/repos/{}/{}", owner, repo);
167        let response = self.client.get(&path).await?;
168
169        // Map HTTP status codes to appropriate errors
170        let status = response.status();
171        if !status.is_success() {
172            return Err(super::map_http_error(status, response).await);
173        }
174
175        // Parse successful response
176        response.json().await.map_err(ApiError::from)
177    }
178
179    /// List all branches in a repository.
180    ///
181    /// Returns an array of all branches with their commit information and protection status.
182    ///
183    /// # Arguments
184    ///
185    /// * `owner` - Repository owner
186    /// * `repo` - Repository name
187    ///
188    /// # Returns
189    ///
190    /// Returns `Vec<Branch>` with all repository branches.
191    ///
192    /// # Errors
193    ///
194    /// * `ApiError::NotFound` - Repository does not exist
195    /// * `ApiError::AuthorizationFailed` - Insufficient permissions
196    ///
197    /// # Example
198    ///
199    /// ```no_run
200    /// # use github_bot_sdk::client::RepositoriesClient;
201    /// # async fn example(client: &RepositoriesClient) -> Result<(), Box<dyn std::error::Error>> {
202    /// let branches = client.list_branches("octocat", "Hello-World").await?;
203    /// for branch in branches {
204    ///     println!("Branch: {} (protected: {})", branch.name, branch.protected);
205    /// }
206    /// # Ok(())
207    /// # }
208    /// ```
209    pub async fn list_branches(&self, owner: &str, repo: &str) -> Result<Vec<Branch>, ApiError> {
210        let path = format!("/repos/{}/{}/branches", owner, repo);
211        let response = self.client.get(&path).await?;
212
213        let status = response.status();
214        if !status.is_success() {
215            return Err(super::map_http_error(status, response).await);
216        }
217
218        response.json().await.map_err(ApiError::from)
219    }
220
221    /// Get a specific branch by name.
222    ///
223    /// Retrieves detailed information about a single branch including commit SHA
224    /// and protection status.
225    ///
226    /// # Arguments
227    ///
228    /// * `owner` - Repository owner
229    /// * `repo` - Repository name
230    /// * `branch` - Branch name
231    ///
232    /// # Returns
233    ///
234    /// Returns `Branch` with branch details.
235    ///
236    /// # Errors
237    ///
238    /// * `ApiError::NotFound` - Branch or repository does not exist
239    /// * `ApiError::AuthorizationFailed` - Insufficient permissions
240    ///
241    /// # Example
242    ///
243    /// ```no_run
244    /// # use github_bot_sdk::client::RepositoriesClient;
245    /// # async fn example(client: &RepositoriesClient) -> Result<(), Box<dyn std::error::Error>> {
246    /// let branch = client.get_branch("octocat", "Hello-World", "main").await?;
247    /// println!("Branch {} at commit {}", branch.name, branch.commit.sha);
248    /// # Ok(())
249    /// # }
250    /// ```
251    pub async fn get_branch(
252        &self,
253        owner: &str,
254        repo: &str,
255        branch: &str,
256    ) -> Result<Branch, ApiError> {
257        let path = format!("/repos/{}/{}/branches/{}", owner, repo, branch);
258        let response = self.client.get(&path).await?;
259
260        let status = response.status();
261        if !status.is_success() {
262            return Err(super::map_http_error(status, response).await);
263        }
264
265        response.json().await.map_err(ApiError::from)
266    }
267
268    /// Get a Git reference (branch or tag).
269    ///
270    /// Retrieves information about a Git reference including the SHA it points to.
271    ///
272    /// # Arguments
273    ///
274    /// * `owner` - Repository owner
275    /// * `repo` - Repository name
276    /// * `ref_name` - Reference name (e.g., "heads/main" or "tags/v1.0.0")
277    ///
278    /// # Returns
279    ///
280    /// Returns `GitRef` with reference details.
281    ///
282    /// # Errors
283    ///
284    /// * `ApiError::NotFound` - Reference does not exist
285    ///
286    /// # Example
287    ///
288    /// ```no_run
289    /// # use github_bot_sdk::client::RepositoriesClient;
290    /// # async fn example(client: &RepositoriesClient) -> Result<(), Box<dyn std::error::Error>> {
291    /// let git_ref = client.get_ref("octocat", "Hello-World", "heads/main").await?;
292    /// println!("Ref {} points to {}", git_ref.ref_name, git_ref.object.sha);
293    /// # Ok(())
294    /// # }
295    /// ```
296    pub async fn get_ref(
297        &self,
298        owner: &str,
299        repo: &str,
300        ref_name: &str,
301    ) -> Result<GitRef, ApiError> {
302        let path = format!("/repos/{}/{}/git/refs/{}", owner, repo, ref_name);
303        let response = self.client.get(&path).await?;
304
305        let status = response.status();
306        if !status.is_success() {
307            return Err(super::map_http_error(status, response).await);
308        }
309
310        response.json().await.map_err(ApiError::from)
311    }
312
313    /// Create a new Git reference (branch or tag).
314    ///
315    /// Creates a new reference pointing to the specified SHA.
316    ///
317    /// # Arguments
318    ///
319    /// * `owner` - Repository owner
320    /// * `repo` - Repository name
321    /// * `ref_name` - Full reference name (e.g., "refs/heads/new-branch")
322    /// * `sha` - SHA that the reference should point to
323    ///
324    /// # Returns
325    ///
326    /// Returns `GitRef` for the newly created reference.
327    ///
328    /// # Errors
329    ///
330    /// * `ApiError::InvalidRequest` - Reference already exists or invalid SHA
331    ///
332    /// # Example
333    ///
334    /// ```no_run
335    /// # use github_bot_sdk::client::RepositoriesClient;
336    /// # async fn example(client: &RepositoriesClient) -> Result<(), Box<dyn std::error::Error>> {
337    /// let git_ref = client.create_ref(
338    ///     "octocat",
339    ///     "Hello-World",
340    ///     "refs/heads/new-feature",
341    ///     "aa218f56b14c9653891f9e74264a383fa43fefbd"
342    /// ).await?;
343    /// # Ok(())
344    /// # }
345    /// ```
346    pub async fn create_ref(
347        &self,
348        owner: &str,
349        repo: &str,
350        ref_name: &str,
351        sha: &str,
352    ) -> Result<GitRef, ApiError> {
353        let path = format!("/repos/{}/{}/git/refs", owner, repo);
354        let request_body = CreateGitRefRequest {
355            ref_name: ref_name.to_string(),
356            sha: sha.to_string(),
357        };
358
359        let response = self.client.post(&path, &request_body).await?;
360
361        let status = response.status();
362        if !status.is_success() {
363            return Err(super::map_http_error(status, response).await);
364        }
365
366        response.json().await.map_err(ApiError::from)
367    }
368
369    /// Update an existing Git reference.
370    ///
371    /// Updates a reference to point to a new SHA. Use `force=true` to allow
372    /// non-fast-forward updates.
373    ///
374    /// # Arguments
375    ///
376    /// * `owner` - Repository owner
377    /// * `repo` - Repository name
378    /// * `ref_name` - Reference name (e.g., "heads/main")
379    /// * `sha` - New SHA for the reference
380    /// * `force` - Allow non-fast-forward updates
381    ///
382    /// # Returns
383    ///
384    /// Returns `GitRef` with updated reference details.
385    ///
386    /// # Errors
387    ///
388    /// * `ApiError::NotFound` - Reference does not exist
389    /// * `ApiError::InvalidRequest` - Invalid SHA or non-fast-forward without force
390    ///
391    /// # Example
392    ///
393    /// ```no_run
394    /// # use github_bot_sdk::client::RepositoriesClient;
395    /// # async fn example(client: &RepositoriesClient) -> Result<(), Box<dyn std::error::Error>> {
396    /// let git_ref = client.update_ref(
397    ///     "octocat",
398    ///     "Hello-World",
399    ///     "heads/feature",
400    ///     "bb218f56b14c9653891f9e74264a383fa43fefbd",
401    ///     false
402    /// ).await?;
403    /// # Ok(())
404    /// # }
405    /// ```
406    pub async fn update_ref(
407        &self,
408        owner: &str,
409        repo: &str,
410        ref_name: &str,
411        sha: &str,
412        force: bool,
413    ) -> Result<GitRef, ApiError> {
414        let path = format!("/repos/{}/{}/git/refs/{}", owner, repo, ref_name);
415        let request_body = UpdateGitRefRequest {
416            sha: sha.to_string(),
417            force,
418        };
419
420        let response = self.client.patch(&path, &request_body).await?;
421
422        let status = response.status();
423        if !status.is_success() {
424            return Err(super::map_http_error(status, response).await);
425        }
426
427        response.json().await.map_err(ApiError::from)
428    }
429
430    /// Delete a Git reference.
431    ///
432    /// Permanently deletes a Git reference. Use with caution as this operation
433    /// cannot be undone.
434    ///
435    /// # Arguments
436    ///
437    /// * `owner` - Repository owner
438    /// * `repo` - Repository name
439    /// * `ref_name` - Reference name (e.g., "heads/old-feature")
440    ///
441    /// # Errors
442    ///
443    /// * `ApiError::NotFound` - Reference does not exist
444    ///
445    /// # Example
446    ///
447    /// ```no_run
448    /// # use github_bot_sdk::client::RepositoriesClient;
449    /// # async fn example(client: &RepositoriesClient) -> Result<(), Box<dyn std::error::Error>> {
450    /// client.delete_ref("octocat", "Hello-World", "heads/old-feature").await?;
451    /// # Ok(())
452    /// # }
453    /// ```
454    pub async fn delete_ref(
455        &self,
456        owner: &str,
457        repo: &str,
458        ref_name: &str,
459    ) -> Result<(), ApiError> {
460        let path = format!("/repos/{}/{}/git/refs/{}", owner, repo, ref_name);
461        let response = self.client.delete(&path).await?;
462
463        let status = response.status();
464        if !status.is_success() {
465            return Err(super::map_http_error(status, response).await);
466        }
467
468        Ok(())
469    }
470
471    /// List all tags in a repository.
472    ///
473    /// Returns an array of all tags with their associated commit information.
474    ///
475    /// # Arguments
476    ///
477    /// * `owner` - Repository owner
478    /// * `repo` - Repository name
479    ///
480    /// # Returns
481    ///
482    /// Returns `Vec<Tag>` with all repository tags. Returns empty vector if no tags exist.
483    ///
484    /// # Errors
485    ///
486    /// * `ApiError::NotFound` - Repository does not exist
487    /// * `ApiError::AuthorizationFailed` - Insufficient permissions
488    ///
489    /// # Example
490    ///
491    /// ```no_run
492    /// # use github_bot_sdk::client::RepositoriesClient;
493    /// # async fn example(client: &RepositoriesClient) -> Result<(), Box<dyn std::error::Error>> {
494    /// let tags = client.list_tags("octocat", "Hello-World").await?;
495    /// for tag in tags {
496    ///     println!("Tag: {} at {}", tag.name, tag.commit.sha);
497    /// }
498    /// # Ok(())
499    /// # }
500    /// ```
501    pub async fn list_tags(&self, owner: &str, repo: &str) -> Result<Vec<Tag>, ApiError> {
502        let path = format!("/repos/{}/{}/tags", owner, repo);
503        let response = self.client.get(&path).await?;
504
505        let status = response.status();
506        if !status.is_success() {
507            return Err(super::map_http_error(status, response).await);
508        }
509
510        response.json().await.map_err(ApiError::from)
511    }
512
513    /// Create a new branch (convenience wrapper around create_git_ref).
514    ///
515    /// Creates a new branch reference pointing to the specified commit.
516    /// This is a convenience method that automatically adds the "refs/heads/" prefix.
517    ///
518    /// # Arguments
519    ///
520    /// * `owner` - Repository owner
521    /// * `repo` - Repository name
522    /// * `branch_name` - Branch name (without "refs/heads/" prefix)
523    /// * `from_sha` - SHA of the commit to branch from
524    ///
525    /// # Returns
526    ///
527    /// Returns `GitRef` for the newly created branch.
528    ///
529    /// # Errors
530    ///
531    /// * `ApiError::InvalidRequest` - Branch already exists or invalid SHA
532    ///
533    /// # Example
534    ///
535    /// ```no_run
536    /// # use github_bot_sdk::client::RepositoriesClient;
537    /// # async fn example(client: &RepositoriesClient) -> Result<(), Box<dyn std::error::Error>> {
538    /// let branch = client.create_branch(
539    ///     "octocat",
540    ///     "Hello-World",
541    ///     "new-feature",
542    ///     "aa218f56b14c9653891f9e74264a383fa43fefbd"
543    /// ).await?;
544    /// println!("Created branch: {}", branch.ref_name);
545    /// # Ok(())
546    /// # }
547    /// ```
548    pub async fn create_branch(
549        &self,
550        owner: &str,
551        repo: &str,
552        branch_name: &str,
553        from_sha: &str,
554    ) -> Result<GitRef, ApiError> {
555        let ref_name = format!("refs/heads/{}", branch_name);
556        self.create_ref(owner, repo, &ref_name, from_sha).await
557    }
558
559    /// Create a new tag (convenience wrapper around create_git_ref).
560    ///
561    /// Creates a new tag reference pointing to the specified commit.
562    /// This is a convenience method that automatically adds the "refs/tags/" prefix.
563    ///
564    /// # Arguments
565    ///
566    /// * `owner` - Repository owner
567    /// * `repo` - Repository name
568    /// * `tag_name` - Tag name (without "refs/tags/" prefix)
569    /// * `from_sha` - SHA of the commit to tag
570    ///
571    /// # Returns
572    ///
573    /// Returns `GitRef` for the newly created tag.
574    ///
575    /// # Errors
576    ///
577    /// * `ApiError::InvalidRequest` - Tag already exists or invalid SHA
578    ///
579    /// # Example
580    ///
581    /// ```no_run
582    /// # use github_bot_sdk::client::RepositoriesClient;
583    /// # async fn example(client: &RepositoriesClient) -> Result<(), Box<dyn std::error::Error>> {
584    /// let tag = client.create_tag(
585    ///     "octocat",
586    ///     "Hello-World",
587    ///     "v1.0.0",
588    ///     "aa218f56b14c9653891f9e74264a383fa43fefbd"
589    /// ).await?;
590    /// println!("Created tag: {}", tag.ref_name);
591    /// # Ok(())
592    /// # }
593    /// ```
594    pub async fn create_tag(
595        &self,
596        owner: &str,
597        repo: &str,
598        tag_name: &str,
599        from_sha: &str,
600    ) -> Result<GitRef, ApiError> {
601        let ref_name = format!("refs/tags/{}", tag_name);
602        self.create_ref(owner, repo, &ref_name, from_sha).await
603    }
604}