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}