Skip to main content

github_bot_sdk/client/
repository.rs

1//! Repository Operations
2//!
3//! **Specification**: `docs/spec/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
115impl InstallationClient {
116    /// Get repository metadata.
117    ///
118    /// Retrieves complete metadata for a repository including owner information,
119    /// visibility settings, default branch, and timestamps.
120    ///
121    /// # Arguments
122    ///
123    /// * `owner` - Repository owner (username or organization)
124    /// * `repo` - Repository name
125    ///
126    /// # Returns
127    ///
128    /// Returns `Repository` with complete metadata on success.
129    ///
130    /// # Errors
131    ///
132    /// * `ApiError::NotFound` - Repository does not exist or is not accessible
133    /// * `ApiError::AuthorizationFailed` - Insufficient permissions to access repository
134    /// * `ApiError::HttpError` - GitHub API returned an error
135    ///
136    /// # Example
137    ///
138    /// ```no_run
139    /// # use github_bot_sdk::client::InstallationClient;
140    /// # async fn example(client: &InstallationClient) -> Result<(), Box<dyn std::error::Error>> {
141    /// let repo = client.get_repository("octocat", "Hello-World").await?;
142    /// println!("Repository: {}", repo.full_name);
143    /// println!("Default branch: {}", repo.default_branch);
144    /// # Ok(())
145    /// # }
146    /// ```
147    pub async fn get_repository(&self, owner: &str, repo: &str) -> Result<Repository, ApiError> {
148        let path = format!("/repos/{}/{}", owner, repo);
149        let response = self.get(&path).await?;
150
151        // Map HTTP status codes to appropriate errors
152        let status = response.status();
153        if !status.is_success() {
154            return Err(match status.as_u16() {
155                404 => ApiError::NotFound,
156                403 => ApiError::AuthorizationFailed,
157                401 => ApiError::AuthenticationFailed,
158                _ => {
159                    let message = response
160                        .text()
161                        .await
162                        .unwrap_or_else(|_| "Unknown error".to_string());
163                    ApiError::HttpError {
164                        status: status.as_u16(),
165                        message,
166                    }
167                }
168            });
169        }
170
171        // Parse successful response
172        response.json().await.map_err(ApiError::from)
173    }
174
175    /// List all branches in a repository.
176    ///
177    /// Returns an array of all branches with their commit information and protection status.
178    ///
179    /// # Arguments
180    ///
181    /// * `owner` - Repository owner
182    /// * `repo` - Repository name
183    ///
184    /// # Returns
185    ///
186    /// Returns `Vec<Branch>` with all repository branches.
187    ///
188    /// # Errors
189    ///
190    /// * `ApiError::NotFound` - Repository does not exist
191    /// * `ApiError::AuthorizationFailed` - Insufficient permissions
192    ///
193    /// # Example
194    ///
195    /// ```no_run
196    /// # use github_bot_sdk::client::InstallationClient;
197    /// # async fn example(client: &InstallationClient) -> Result<(), Box<dyn std::error::Error>> {
198    /// let branches = client.list_branches("octocat", "Hello-World").await?;
199    /// for branch in branches {
200    ///     println!("Branch: {} (protected: {})", branch.name, branch.protected);
201    /// }
202    /// # Ok(())
203    /// # }
204    /// ```
205    pub async fn list_branches(&self, owner: &str, repo: &str) -> Result<Vec<Branch>, ApiError> {
206        let path = format!("/repos/{}/{}/branches", owner, repo);
207        let response = self.get(&path).await?;
208
209        let status = response.status();
210        if !status.is_success() {
211            return Err(match status.as_u16() {
212                404 => ApiError::NotFound,
213                403 => ApiError::AuthorizationFailed,
214                401 => ApiError::AuthenticationFailed,
215                _ => {
216                    let message = response
217                        .text()
218                        .await
219                        .unwrap_or_else(|_| "Unknown error".to_string());
220                    ApiError::HttpError {
221                        status: status.as_u16(),
222                        message,
223                    }
224                }
225            });
226        }
227
228        response.json().await.map_err(ApiError::from)
229    }
230
231    /// Get a specific branch by name.
232    ///
233    /// Retrieves detailed information about a single branch including commit SHA
234    /// and protection status.
235    ///
236    /// # Arguments
237    ///
238    /// * `owner` - Repository owner
239    /// * `repo` - Repository name
240    /// * `branch` - Branch name
241    ///
242    /// # Returns
243    ///
244    /// Returns `Branch` with branch details.
245    ///
246    /// # Errors
247    ///
248    /// * `ApiError::NotFound` - Branch or repository does not exist
249    /// * `ApiError::AuthorizationFailed` - Insufficient permissions
250    ///
251    /// # Example
252    ///
253    /// ```no_run
254    /// # use github_bot_sdk::client::InstallationClient;
255    /// # async fn example(client: &InstallationClient) -> Result<(), Box<dyn std::error::Error>> {
256    /// let branch = client.get_branch("octocat", "Hello-World", "main").await?;
257    /// println!("Branch {} at commit {}", branch.name, branch.commit.sha);
258    /// # Ok(())
259    /// # }
260    /// ```
261    pub async fn get_branch(
262        &self,
263        owner: &str,
264        repo: &str,
265        branch: &str,
266    ) -> Result<Branch, ApiError> {
267        let path = format!("/repos/{}/{}/branches/{}", owner, repo, branch);
268        let response = self.get(&path).await?;
269
270        let status = response.status();
271        if !status.is_success() {
272            return Err(match status.as_u16() {
273                404 => ApiError::NotFound,
274                403 => ApiError::AuthorizationFailed,
275                401 => ApiError::AuthenticationFailed,
276                _ => {
277                    let message = response
278                        .text()
279                        .await
280                        .unwrap_or_else(|_| "Unknown error".to_string());
281                    ApiError::HttpError {
282                        status: status.as_u16(),
283                        message,
284                    }
285                }
286            });
287        }
288
289        response.json().await.map_err(ApiError::from)
290    }
291
292    /// Get a Git reference (branch or tag).
293    ///
294    /// Retrieves information about a Git reference including the SHA it points to.
295    ///
296    /// # Arguments
297    ///
298    /// * `owner` - Repository owner
299    /// * `repo` - Repository name
300    /// * `ref_name` - Reference name (e.g., "heads/main" or "tags/v1.0.0")
301    ///
302    /// # Returns
303    ///
304    /// Returns `GitRef` with reference details.
305    ///
306    /// # Errors
307    ///
308    /// * `ApiError::NotFound` - Reference does not exist
309    ///
310    /// # Example
311    ///
312    /// ```no_run
313    /// # use github_bot_sdk::client::InstallationClient;
314    /// # async fn example(client: &InstallationClient) -> Result<(), Box<dyn std::error::Error>> {
315    /// let git_ref = client.get_git_ref("octocat", "Hello-World", "heads/main").await?;
316    /// println!("Ref {} points to {}", git_ref.ref_name, git_ref.object.sha);
317    /// # Ok(())
318    /// # }
319    /// ```
320    pub async fn get_git_ref(
321        &self,
322        owner: &str,
323        repo: &str,
324        ref_name: &str,
325    ) -> Result<GitRef, ApiError> {
326        let path = format!("/repos/{}/{}/git/refs/{}", owner, repo, ref_name);
327        let response = self.get(&path).await?;
328
329        let status = response.status();
330        if !status.is_success() {
331            return Err(match status.as_u16() {
332                404 => ApiError::NotFound,
333                403 => ApiError::AuthorizationFailed,
334                401 => ApiError::AuthenticationFailed,
335                _ => {
336                    let message = response
337                        .text()
338                        .await
339                        .unwrap_or_else(|_| "Unknown error".to_string());
340                    ApiError::HttpError {
341                        status: status.as_u16(),
342                        message,
343                    }
344                }
345            });
346        }
347
348        response.json().await.map_err(ApiError::from)
349    }
350
351    /// Create a new Git reference (branch or tag).
352    ///
353    /// Creates a new reference pointing to the specified SHA.
354    ///
355    /// # Arguments
356    ///
357    /// * `owner` - Repository owner
358    /// * `repo` - Repository name
359    /// * `ref_name` - Full reference name (e.g., "refs/heads/new-branch")
360    /// * `sha` - SHA that the reference should point to
361    ///
362    /// # Returns
363    ///
364    /// Returns `GitRef` for the newly created reference.
365    ///
366    /// # Errors
367    ///
368    /// * `ApiError::InvalidRequest` - Reference already exists or invalid SHA
369    ///
370    /// # Example
371    ///
372    /// ```no_run
373    /// # use github_bot_sdk::client::InstallationClient;
374    /// # async fn example(client: &InstallationClient) -> Result<(), Box<dyn std::error::Error>> {
375    /// let git_ref = client.create_git_ref(
376    ///     "octocat",
377    ///     "Hello-World",
378    ///     "refs/heads/new-feature",
379    ///     "aa218f56b14c9653891f9e74264a383fa43fefbd"
380    /// ).await?;
381    /// # Ok(())
382    /// # }
383    /// ```
384    pub async fn create_git_ref(
385        &self,
386        owner: &str,
387        repo: &str,
388        ref_name: &str,
389        sha: &str,
390    ) -> Result<GitRef, ApiError> {
391        let path = format!("/repos/{}/{}/git/refs", owner, repo);
392        let request_body = CreateGitRefRequest {
393            ref_name: ref_name.to_string(),
394            sha: sha.to_string(),
395        };
396
397        let response = self.post(&path, &request_body).await?;
398
399        let status = response.status();
400        if !status.is_success() {
401            return Err(match status.as_u16() {
402                422 => {
403                    let message = response
404                        .text()
405                        .await
406                        .unwrap_or_else(|_| "Validation failed".to_string());
407                    ApiError::InvalidRequest { message }
408                }
409                404 => ApiError::NotFound,
410                403 => ApiError::AuthorizationFailed,
411                401 => ApiError::AuthenticationFailed,
412                _ => {
413                    let message = response
414                        .text()
415                        .await
416                        .unwrap_or_else(|_| "Unknown error".to_string());
417                    ApiError::HttpError {
418                        status: status.as_u16(),
419                        message,
420                    }
421                }
422            });
423        }
424
425        response.json().await.map_err(ApiError::from)
426    }
427
428    /// Update an existing Git reference.
429    ///
430    /// Updates a reference to point to a new SHA. Use `force=true` to allow
431    /// non-fast-forward updates.
432    ///
433    /// # Arguments
434    ///
435    /// * `owner` - Repository owner
436    /// * `repo` - Repository name
437    /// * `ref_name` - Reference name (e.g., "heads/main")
438    /// * `sha` - New SHA for the reference
439    /// * `force` - Allow non-fast-forward updates
440    ///
441    /// # Returns
442    ///
443    /// Returns `GitRef` with updated reference details.
444    ///
445    /// # Errors
446    ///
447    /// * `ApiError::NotFound` - Reference does not exist
448    /// * `ApiError::InvalidRequest` - Invalid SHA or non-fast-forward without force
449    ///
450    /// # Example
451    ///
452    /// ```no_run
453    /// # use github_bot_sdk::client::InstallationClient;
454    /// # async fn example(client: &InstallationClient) -> Result<(), Box<dyn std::error::Error>> {
455    /// let git_ref = client.update_git_ref(
456    ///     "octocat",
457    ///     "Hello-World",
458    ///     "heads/feature",
459    ///     "bb218f56b14c9653891f9e74264a383fa43fefbd",
460    ///     false
461    /// ).await?;
462    /// # Ok(())
463    /// # }
464    /// ```
465    pub async fn update_git_ref(
466        &self,
467        owner: &str,
468        repo: &str,
469        ref_name: &str,
470        sha: &str,
471        force: bool,
472    ) -> Result<GitRef, ApiError> {
473        let path = format!("/repos/{}/{}/git/refs/{}", owner, repo, ref_name);
474        let request_body = UpdateGitRefRequest {
475            sha: sha.to_string(),
476            force,
477        };
478
479        let response = self.patch(&path, &request_body).await?;
480
481        let status = response.status();
482        if !status.is_success() {
483            return Err(match status.as_u16() {
484                422 => {
485                    let message = response
486                        .text()
487                        .await
488                        .unwrap_or_else(|_| "Validation failed".to_string());
489                    ApiError::InvalidRequest { message }
490                }
491                404 => ApiError::NotFound,
492                403 => ApiError::AuthorizationFailed,
493                401 => ApiError::AuthenticationFailed,
494                _ => {
495                    let message = response
496                        .text()
497                        .await
498                        .unwrap_or_else(|_| "Unknown error".to_string());
499                    ApiError::HttpError {
500                        status: status.as_u16(),
501                        message,
502                    }
503                }
504            });
505        }
506
507        response.json().await.map_err(ApiError::from)
508    }
509
510    /// Delete a Git reference.
511    ///
512    /// Permanently deletes a Git reference. Use with caution as this operation
513    /// cannot be undone.
514    ///
515    /// # Arguments
516    ///
517    /// * `owner` - Repository owner
518    /// * `repo` - Repository name
519    /// * `ref_name` - Reference name (e.g., "heads/old-feature")
520    ///
521    /// # Errors
522    ///
523    /// * `ApiError::NotFound` - Reference does not exist
524    ///
525    /// # Example
526    ///
527    /// ```no_run
528    /// # use github_bot_sdk::client::InstallationClient;
529    /// # async fn example(client: &InstallationClient) -> Result<(), Box<dyn std::error::Error>> {
530    /// client.delete_git_ref("octocat", "Hello-World", "heads/old-feature").await?;
531    /// # Ok(())
532    /// # }
533    /// ```
534    pub async fn delete_git_ref(
535        &self,
536        owner: &str,
537        repo: &str,
538        ref_name: &str,
539    ) -> Result<(), ApiError> {
540        let path = format!("/repos/{}/{}/git/refs/{}", owner, repo, ref_name);
541        let response = self.delete(&path).await?;
542
543        let status = response.status();
544        if !status.is_success() {
545            return Err(match status.as_u16() {
546                404 => ApiError::NotFound,
547                403 => ApiError::AuthorizationFailed,
548                401 => ApiError::AuthenticationFailed,
549                _ => {
550                    let message = response
551                        .text()
552                        .await
553                        .unwrap_or_else(|_| "Unknown error".to_string());
554                    ApiError::HttpError {
555                        status: status.as_u16(),
556                        message,
557                    }
558                }
559            });
560        }
561
562        Ok(())
563    }
564
565    /// List all tags in a repository.
566    ///
567    /// Returns an array of all tags with their associated commit information.
568    ///
569    /// # Arguments
570    ///
571    /// * `owner` - Repository owner
572    /// * `repo` - Repository name
573    ///
574    /// # Returns
575    ///
576    /// Returns `Vec<Tag>` with all repository tags. Returns empty vector if no tags exist.
577    ///
578    /// # Errors
579    ///
580    /// * `ApiError::NotFound` - Repository does not exist
581    /// * `ApiError::AuthorizationFailed` - Insufficient permissions
582    ///
583    /// # Example
584    ///
585    /// ```no_run
586    /// # use github_bot_sdk::client::InstallationClient;
587    /// # async fn example(client: &InstallationClient) -> Result<(), Box<dyn std::error::Error>> {
588    /// let tags = client.list_tags("octocat", "Hello-World").await?;
589    /// for tag in tags {
590    ///     println!("Tag: {} at {}", tag.name, tag.commit.sha);
591    /// }
592    /// # Ok(())
593    /// # }
594    /// ```
595    pub async fn list_tags(&self, owner: &str, repo: &str) -> Result<Vec<Tag>, ApiError> {
596        let path = format!("/repos/{}/{}/tags", owner, repo);
597        let response = self.get(&path).await?;
598
599        let status = response.status();
600        if !status.is_success() {
601            return Err(match status.as_u16() {
602                404 => ApiError::NotFound,
603                403 => ApiError::AuthorizationFailed,
604                401 => ApiError::AuthenticationFailed,
605                _ => {
606                    let message = response
607                        .text()
608                        .await
609                        .unwrap_or_else(|_| "Unknown error".to_string());
610                    ApiError::HttpError {
611                        status: status.as_u16(),
612                        message,
613                    }
614                }
615            });
616        }
617
618        response.json().await.map_err(ApiError::from)
619    }
620
621    /// Create a new branch (convenience wrapper around create_git_ref).
622    ///
623    /// Creates a new branch reference pointing to the specified commit.
624    /// This is a convenience method that automatically adds the "refs/heads/" prefix.
625    ///
626    /// # Arguments
627    ///
628    /// * `owner` - Repository owner
629    /// * `repo` - Repository name
630    /// * `branch_name` - Branch name (without "refs/heads/" prefix)
631    /// * `from_sha` - SHA of the commit to branch from
632    ///
633    /// # Returns
634    ///
635    /// Returns `GitRef` for the newly created branch.
636    ///
637    /// # Errors
638    ///
639    /// * `ApiError::InvalidRequest` - Branch already exists or invalid SHA
640    ///
641    /// # Example
642    ///
643    /// ```no_run
644    /// # use github_bot_sdk::client::InstallationClient;
645    /// # async fn example(client: &InstallationClient) -> Result<(), Box<dyn std::error::Error>> {
646    /// let branch = client.create_branch(
647    ///     "octocat",
648    ///     "Hello-World",
649    ///     "new-feature",
650    ///     "aa218f56b14c9653891f9e74264a383fa43fefbd"
651    /// ).await?;
652    /// println!("Created branch: {}", branch.ref_name);
653    /// # Ok(())
654    /// # }
655    /// ```
656    pub async fn create_branch(
657        &self,
658        owner: &str,
659        repo: &str,
660        branch_name: &str,
661        from_sha: &str,
662    ) -> Result<GitRef, ApiError> {
663        let ref_name = format!("refs/heads/{}", branch_name);
664        self.create_git_ref(owner, repo, &ref_name, from_sha).await
665    }
666
667    /// Create a new tag (convenience wrapper around create_git_ref).
668    ///
669    /// Creates a new tag reference pointing to the specified commit.
670    /// This is a convenience method that automatically adds the "refs/tags/" prefix.
671    ///
672    /// # Arguments
673    ///
674    /// * `owner` - Repository owner
675    /// * `repo` - Repository name
676    /// * `tag_name` - Tag name (without "refs/tags/" prefix)
677    /// * `from_sha` - SHA of the commit to tag
678    ///
679    /// # Returns
680    ///
681    /// Returns `GitRef` for the newly created tag.
682    ///
683    /// # Errors
684    ///
685    /// * `ApiError::InvalidRequest` - Tag already exists or invalid SHA
686    ///
687    /// # Example
688    ///
689    /// ```no_run
690    /// # use github_bot_sdk::client::InstallationClient;
691    /// # async fn example(client: &InstallationClient) -> Result<(), Box<dyn std::error::Error>> {
692    /// let tag = client.create_tag(
693    ///     "octocat",
694    ///     "Hello-World",
695    ///     "v1.0.0",
696    ///     "aa218f56b14c9653891f9e74264a383fa43fefbd"
697    /// ).await?;
698    /// println!("Created tag: {}", tag.ref_name);
699    /// # Ok(())
700    /// # }
701    /// ```
702    pub async fn create_tag(
703        &self,
704        owner: &str,
705        repo: &str,
706        tag_name: &str,
707        from_sha: &str,
708    ) -> Result<GitRef, ApiError> {
709        let ref_name = format!("refs/tags/{}", tag_name);
710        self.create_git_ref(owner, repo, &ref_name, from_sha).await
711    }
712}