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}