kodegen_tools_github/github/
client.rs

1//! GitHub API client wrapper
2//!
3//! Provides clean API for GitHub operations without exposing Octocrab.
4//!
5//! # Examples
6//!
7//! ```rust,no_run
8//! use gitgix::GitHubClient;
9//!
10//! #[tokio::main]
11//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
12//!     let gh = GitHubClient::with_token("ghp_...")?;
13//!
14//!     // Use with any GitHub operation
15//!     let issue = gitgix::create_issue(
16//!         gh,
17//!         "owner",
18//!         "repo",
19//!         "Issue title",
20//!         None, None, None
21//!     ).await?;
22//!
23//!     Ok(())
24//! }
25//! ```
26
27use crate::github::error::{GitHubError, GitHubResult};
28use jsonwebtoken::EncodingKey;
29use octocrab::{Octocrab, models::AppId};
30use std::sync::Arc;
31
32/// GitHub API client wrapper that encapsulates Octocrab.
33///
34/// Provides clean API without exposing Octocrab dependency.
35/// Cloning is cheap (Arc clone).
36#[derive(Clone, Debug)]
37pub struct GitHubClient {
38    inner: Arc<Octocrab>,
39}
40
41impl GitHubClient {
42    /// Create a new client builder
43    #[must_use]
44    pub fn builder() -> GitHubClientBuilder {
45        GitHubClientBuilder::new()
46    }
47
48    /// Convenience: create client with personal access token
49    pub fn with_token(token: impl Into<String>) -> GitHubResult<Self> {
50        Self::builder().personal_token(token).build()
51    }
52
53    /// Get inner Octocrab client
54    #[must_use]
55    pub fn inner(&self) -> &Arc<Octocrab> {
56        &self.inner
57    }
58
59    // ========================================================================
60    // Issues
61    // ========================================================================
62
63    /// Get a single issue
64    pub fn get_issue(
65        &self,
66        owner: impl Into<String>,
67        repo: impl Into<String>,
68        issue_number: u64,
69    ) -> crate::runtime::AsyncTask<Result<octocrab::models::issues::Issue, GitHubError>> {
70        crate::github::get_issue::get_issue(self.inner.clone(), owner, repo, issue_number)
71    }
72
73    /// Create a new issue
74    pub fn create_issue(
75        &self,
76        owner: impl Into<String>,
77        repo: impl Into<String>,
78        title: impl Into<String>,
79        body: Option<String>,
80        assignees: Option<Vec<String>>,
81        labels: Option<Vec<String>>,
82    ) -> crate::runtime::AsyncTask<Result<octocrab::models::issues::Issue, GitHubError>> {
83        crate::github::create_issue::create_issue(
84            self.inner.clone(),
85            owner,
86            repo,
87            title,
88            body,
89            assignees,
90            labels,
91        )
92    }
93
94    /// Add a comment to an issue
95    pub fn add_issue_comment(
96        &self,
97        owner: impl Into<String>,
98        repo: impl Into<String>,
99        issue_number: u64,
100        body: impl Into<String>,
101    ) -> crate::runtime::AsyncTask<Result<octocrab::models::issues::Comment, GitHubError>> {
102        crate::github::add_issue_comment::add_issue_comment(
103            self.inner.clone(),
104            owner,
105            repo,
106            issue_number,
107            body,
108        )
109    }
110
111    /// Get comments for an issue
112    pub fn get_issue_comments(
113        &self,
114        owner: impl Into<String>,
115        repo: impl Into<String>,
116        issue_number: u64,
117    ) -> crate::runtime::AsyncStream<Result<octocrab::models::issues::Comment, GitHubError>> {
118        crate::github::get_issue_comments::get_issue_comments(
119            self.inner.clone(),
120            owner,
121            repo,
122            issue_number,
123        )
124    }
125
126    /// List issues with filters
127    #[must_use]
128    pub fn list_issues(
129        &self,
130        request: crate::github::ListIssuesRequest,
131    ) -> crate::runtime::AsyncStream<Result<octocrab::models::issues::Issue, GitHubError>> {
132        crate::github::list_issues::list_issues(self.inner.clone(), request)
133    }
134
135    /// Update an issue
136    #[must_use]
137    pub fn update_issue(
138        &self,
139        request: crate::github::UpdateIssueRequest,
140    ) -> crate::runtime::AsyncTask<Result<octocrab::models::issues::Issue, GitHubError>> {
141        crate::github::update_issue::update_issue(self.inner.clone(), request)
142    }
143
144    /// Search issues
145    pub fn search_issues(
146        &self,
147        query: impl Into<String>,
148        sort: Option<String>,
149        order: Option<String>,
150        page: Option<u32>,
151        per_page: Option<u8>,
152    ) -> crate::runtime::AsyncStream<Result<octocrab::models::issues::Issue, GitHubError>> {
153        crate::github::search_issues::search_issues(
154            self.inner.clone(),
155            query,
156            sort,
157            order,
158            page,
159            per_page,
160        )
161    }
162
163    // ========================================================================
164    // Pull Requests
165    // ========================================================================
166
167    /// Create a pull request
168    #[must_use]
169    pub fn create_pull_request(
170        &self,
171        request: crate::github::CreatePullRequestRequest,
172    ) -> crate::runtime::AsyncTask<Result<octocrab::models::pulls::PullRequest, GitHubError>> {
173        crate::github::create_pull_request::create_pull_request(self.inner.clone(), request)
174    }
175
176    /// Get pull request status
177    pub fn get_pull_request_status(
178        &self,
179        owner: impl Into<String>,
180        repo: impl Into<String>,
181        pr_number: u64,
182    ) -> crate::runtime::AsyncTask<Result<octocrab::models::CombinedStatus, GitHubError>> {
183        crate::github::get_pull_request_status::get_pull_request_status(
184            self.inner.clone(),
185            owner,
186            repo,
187            pr_number,
188        )
189    }
190
191    /// Get pull request comments
192    pub fn get_pull_request_comments(
193        &self,
194        owner: impl Into<String>,
195        repo: impl Into<String>,
196        pr_number: u64,
197    ) -> crate::runtime::AsyncStream<Result<octocrab::models::pulls::Comment, GitHubError>> {
198        crate::github::get_pull_request_comments::get_pull_request_comments(
199            self.inner.clone(),
200            owner,
201            repo,
202            pr_number,
203        )
204    }
205
206    /// Get pull request files
207    pub fn get_pull_request_files(
208        &self,
209        owner: impl Into<String>,
210        repo: impl Into<String>,
211        pr_number: u64,
212    ) -> crate::runtime::AsyncStream<Result<octocrab::models::repos::DiffEntry, GitHubError>> {
213        crate::github::get_pull_request_files::get_pull_request_files(
214            self.inner.clone(),
215            owner,
216            repo,
217            pr_number,
218        )
219    }
220
221    /// Get pull request reviews
222    pub fn get_pull_request_reviews(
223        &self,
224        owner: impl Into<String>,
225        repo: impl Into<String>,
226        pr_number: u64,
227    ) -> crate::runtime::AsyncStream<Result<octocrab::models::pulls::Review, GitHubError>> {
228        crate::github::get_pull_request_reviews::get_pull_request_reviews(
229            self.inner.clone(),
230            owner,
231            repo,
232            pr_number,
233        )
234    }
235
236    /// Create a pull request review
237    pub fn create_pull_request_review(
238        &self,
239        owner: impl Into<String>,
240        repo: impl Into<String>,
241        pr_number: u64,
242        options: crate::github::CreatePullRequestReviewOptions,
243    ) -> crate::runtime::AsyncTask<Result<octocrab::models::pulls::Review, GitHubError>> {
244        crate::github::create_pull_request_review::create_pull_request_review(
245            self.inner.clone(),
246            owner,
247            repo,
248            pr_number,
249            options,
250        )
251    }
252
253    /// Add a review comment to a pull request
254    #[must_use]
255    pub fn add_pull_request_review_comment(
256        &self,
257        request: crate::github::AddPullRequestReviewCommentRequest,
258    ) -> crate::runtime::AsyncTask<Result<octocrab::models::pulls::ReviewComment, GitHubError>>
259    {
260        crate::github::add_pull_request_review_comment::add_pull_request_review_comment(
261            self.inner.clone(),
262            request,
263        )
264    }
265
266    /// Update a pull request
267    pub fn update_pull_request(
268        &self,
269        owner: impl Into<String>,
270        repo: impl Into<String>,
271        pr_number: u64,
272        options: crate::github::UpdatePullRequestOptions,
273    ) -> crate::runtime::AsyncTask<Result<octocrab::models::pulls::PullRequest, GitHubError>> {
274        crate::github::update_pull_request::update_pull_request(
275            self.inner.clone(),
276            owner,
277            repo,
278            pr_number,
279            options,
280        )
281    }
282
283    /// Merge a pull request
284    pub fn merge_pull_request(
285        &self,
286        owner: impl Into<String>,
287        repo: impl Into<String>,
288        pr_number: u64,
289        options: crate::github::MergePullRequestOptions,
290    ) -> crate::runtime::AsyncTask<Result<serde_json::Value, GitHubError>> {
291        crate::github::merge_pull_request::merge_pull_request(
292            self.inner.clone(),
293            owner,
294            repo,
295            pr_number,
296            options,
297        )
298    }
299
300    // ========================================================================
301    // Repositories
302    // ========================================================================
303
304    /// Get file contents
305    pub fn get_file_contents(
306        &self,
307        owner: impl Into<String>,
308        repo: impl Into<String>,
309        path: impl Into<String>,
310        ref_name: Option<String>,
311    ) -> crate::runtime::AsyncTask<Result<Vec<octocrab::models::repos::Content>, GitHubError>> {
312        crate::github::get_file_contents::get_file_contents(
313            self.inner.clone(),
314            owner,
315            repo,
316            path,
317            ref_name,
318        )
319    }
320
321    /// Create or update a file
322    #[must_use]
323    pub fn create_or_update_file(
324        &self,
325        request: crate::github::CreateOrUpdateFileRequest,
326    ) -> crate::runtime::AsyncTask<Result<octocrab::models::repos::FileUpdate, GitHubError>> {
327        crate::github::create_or_update_file::create_or_update_file(self.inner.clone(), request)
328    }
329
330    /// List branches
331    pub fn list_branches(
332        &self,
333        owner: impl Into<String>,
334        repo: impl Into<String>,
335        page: Option<u32>,
336        per_page: Option<u8>,
337    ) -> crate::runtime::AsyncTask<Result<Vec<octocrab::models::repos::Branch>, GitHubError>> {
338        crate::github::list_branches::list_branches(self.inner.clone(), owner, repo, page, per_page)
339    }
340
341    /// Create a branch
342    pub fn create_branch(
343        &self,
344        owner: impl Into<String>,
345        repo: impl Into<String>,
346        branch_name: impl Into<String>,
347        sha: impl Into<String>,
348    ) -> crate::runtime::AsyncTask<Result<octocrab::models::repos::Ref, GitHubError>> {
349        crate::github::create_branch::create_branch(
350            self.inner.clone(),
351            owner,
352            repo,
353            branch_name,
354            sha,
355        )
356    }
357
358    /// List commits
359    pub fn list_commits(
360        &self,
361        owner: impl Into<String>,
362        repo: impl Into<String>,
363        options: crate::github::ListCommitsOptions,
364    ) -> crate::runtime::AsyncTask<Result<Vec<octocrab::models::repos::RepoCommit>, GitHubError>>
365    {
366        crate::github::list_commits::list_commits(self.inner.clone(), owner, repo, options)
367    }
368
369    /// Get a commit
370    pub fn get_commit(
371        &self,
372        owner: impl Into<String>,
373        repo: impl Into<String>,
374        commit_sha: impl Into<String>,
375        page: Option<u32>,
376        per_page: Option<u8>,
377    ) -> crate::runtime::AsyncTask<Result<octocrab::models::repos::RepoCommit, GitHubError>> {
378        crate::github::get_commit::get_commit(
379            self.inner.clone(),
380            owner,
381            repo,
382            commit_sha,
383            page,
384            per_page,
385        )
386    }
387
388    /// Search code
389    pub fn search_code(
390        &self,
391        query: impl Into<String>,
392        sort: Option<String>,
393        order: Option<String>,
394        page: Option<u32>,
395        per_page: Option<u8>,
396        enrich_stars: bool,
397    ) -> crate::runtime::AsyncTask<Result<octocrab::Page<octocrab::models::Code>, GitHubError>>
398    {
399        crate::github::search_code::search_code(
400            self.inner.clone(),
401            query,
402            sort,
403            order,
404            page,
405            per_page,
406            enrich_stars,
407        )
408    }
409
410    /// Create a repository
411    pub fn create_repository(
412        &self,
413        name: impl Into<String>,
414        description: Option<String>,
415        private: Option<bool>,
416        auto_init: Option<bool>,
417    ) -> crate::runtime::AsyncTask<Result<octocrab::models::Repository, GitHubError>> {
418        crate::github::create_repository::create_repository(
419            self.inner.clone(),
420            name,
421            description,
422            private,
423            auto_init,
424        )
425    }
426
427    /// Fork a repository
428    pub fn fork_repository(
429        &self,
430        owner: impl Into<String>,
431        repo: impl Into<String>,
432        organization: Option<String>,
433    ) -> crate::runtime::AsyncTask<Result<octocrab::models::Repository, GitHubError>> {
434        crate::github::fork_repository::fork_repository(
435            self.inner.clone(),
436            owner,
437            repo,
438            organization,
439        )
440    }
441
442    /// Push files to a repository
443    pub fn push_files(
444        &self,
445        owner: impl Into<String>,
446        repo: impl Into<String>,
447        branch: impl Into<String>,
448        files: std::collections::HashMap<String, String>,
449        commit_message: impl Into<String>,
450    ) -> crate::runtime::AsyncTask<Result<octocrab::models::repos::Commit, GitHubError>> {
451        crate::github::push_files::push_files(
452            self.inner.clone(),
453            owner,
454            repo,
455            branch,
456            files,
457            commit_message,
458        )
459    }
460
461    // ========================================================================
462    // Users
463    // ========================================================================
464
465    /// Get the authenticated user
466    #[must_use]
467    pub fn get_me(
468        &self,
469    ) -> crate::runtime::AsyncTask<Result<octocrab::models::Author, GitHubError>> {
470        crate::github::get_me::get_me(self.inner.clone())
471    }
472
473    /// Search users
474    pub fn search_users(
475        &self,
476        query: impl Into<String>,
477        sort: Option<crate::github::search_users::UserSearchSort>,
478        order: Option<crate::github::search_users::SearchOrder>,
479        page: Option<u32>,
480        per_page: Option<u8>,
481    ) -> crate::runtime::AsyncTask<Result<octocrab::Page<octocrab::models::Author>, GitHubError>>
482    {
483        crate::github::search_users::search_users(
484            self.inner.clone(),
485            query,
486            sort,
487            order,
488            page,
489            per_page,
490        )
491    }
492
493    // ========================================================================
494    // Security
495    // ========================================================================
496
497    /// List code scanning alerts
498    pub fn list_code_scanning_alerts(
499        &self,
500        owner: impl Into<String>,
501        repo: impl Into<String>,
502        state: Option<String>,
503        ref_name: Option<String>,
504        tool_name: Option<String>,
505        severity: Option<String>,
506    ) -> crate::runtime::AsyncTask<Result<Vec<serde_json::Value>, GitHubError>> {
507        crate::github::code_scanning_alerts::list_code_scanning_alerts(
508            self.inner.clone(),
509            owner,
510            repo,
511            state,
512            ref_name,
513            tool_name,
514            severity,
515        )
516    }
517
518    /// Get a code scanning alert
519    pub fn get_code_scanning_alert(
520        &self,
521        owner: impl Into<String>,
522        repo: impl Into<String>,
523        alert_number: u64,
524    ) -> crate::runtime::AsyncTask<Result<serde_json::Value, GitHubError>> {
525        crate::github::code_scanning_alerts::get_code_scanning_alert(
526            self.inner.clone(),
527            owner,
528            repo,
529            alert_number,
530        )
531    }
532
533    /// List secret scanning alerts
534    pub fn list_secret_scanning_alerts(
535        &self,
536        owner: impl Into<String>,
537        repo: impl Into<String>,
538        state: Option<String>,
539        secret_type: Option<String>,
540        resolution: Option<String>,
541    ) -> crate::runtime::AsyncTask<
542        Result<
543            Vec<octocrab::models::repos::secret_scanning_alert::SecretScanningAlert>,
544            GitHubError,
545        >,
546    > {
547        crate::github::secret_scanning_alerts::list_secret_scanning_alerts(
548            self.inner.clone(),
549            owner,
550            repo,
551            state,
552            secret_type,
553            resolution,
554        )
555    }
556
557    /// Get a secret scanning alert
558    pub fn get_secret_scanning_alert(
559        &self,
560        owner: impl Into<String>,
561        repo: impl Into<String>,
562        alert_number: u32,
563    ) -> crate::runtime::AsyncTask<
564        Result<octocrab::models::repos::secret_scanning_alert::SecretScanningAlert, GitHubError>,
565    > {
566        crate::github::secret_scanning_alerts::get_secret_scanning_alert(
567            self.inner.clone(),
568            owner,
569            repo,
570            alert_number,
571        )
572    }
573
574    // ========================================================================
575    // Release Assets
576    // ========================================================================
577
578    /// Upload an asset to a release
579    ///
580    /// Requires the release ID and binary content of the file.
581    /// Returns the uploaded asset information including download URL.
582    pub async fn upload_release_asset(
583        &self,
584        owner: impl Into<String>,
585        repo: impl Into<String>,
586        options: crate::github::upload_release_asset::UploadAssetOptions,
587    ) -> Result<octocrab::models::repos::Asset, crate::github::error::GitHubError> {
588        crate::github::upload_release_asset::upload_release_asset(
589            self.inner.clone(),
590            &owner.into(),
591            &repo.into(),
592            options,
593        )
594        .await
595        .map_err(crate::github::error::GitHubError::from)
596    }
597
598    /// Delete a release asset
599    pub async fn delete_release_asset(
600        &self,
601        owner: impl Into<String>,
602        repo: impl Into<String>,
603        asset_id: u64,
604    ) -> Result<(), crate::github::error::GitHubError> {
605        crate::github::upload_release_asset::delete_release_asset(
606            self.inner.clone(),
607            &owner.into(),
608            &repo.into(),
609            asset_id,
610        )
611        .await
612        .map_err(crate::github::error::GitHubError::from)
613    }
614
615    // ========================================================================
616    // Experimental
617    // ========================================================================
618
619    /// Request a Copilot review
620    pub fn request_copilot_review(
621        &self,
622        owner: impl Into<String>,
623        repo: impl Into<String>,
624        pr_number: u64,
625    ) -> crate::runtime::AsyncTask<Result<(), GitHubError>> {
626        crate::github::request_copilot_review::request_copilot_review(
627            self.inner.clone(),
628            owner,
629            repo,
630            pr_number,
631        )
632    }
633}
634
635/// Builder for creating `GitHubClient` with various authentication methods
636pub struct GitHubClientBuilder {
637    token: Option<String>,
638    app_auth: Option<(AppId, String)>,
639    base_uri: Option<String>,
640}
641
642impl GitHubClientBuilder {
643    /// Create a new builder
644    #[must_use]
645    pub fn new() -> Self {
646        Self {
647            token: None,
648            app_auth: None,
649            base_uri: None,
650        }
651    }
652
653    /// Set personal access token for authentication
654    pub fn personal_token(mut self, token: impl Into<String>) -> Self {
655        self.token = Some(token.into());
656        self
657    }
658
659    /// Set GitHub App authentication (app ID and private key)
660    pub fn app(mut self, app_id: AppId, private_key: impl Into<String>) -> Self {
661        self.app_auth = Some((app_id, private_key.into()));
662        self
663    }
664
665    /// Set base URI (for GitHub Enterprise)
666    pub fn base_uri(mut self, uri: impl Into<String>) -> Self {
667        self.base_uri = Some(uri.into());
668        self
669    }
670
671    /// Build the `GitHubClient`
672    pub fn build(self) -> GitHubResult<GitHubClient> {
673        let mut builder = Octocrab::builder();
674
675        // Set authentication
676        if let Some(token) = self.token {
677            builder = builder.personal_token(token);
678        } else if let Some((app_id, private_key)) = self.app_auth {
679            let key = EncodingKey::from_rsa_pem(private_key.as_bytes())
680                .map_err(|e| GitHubError::ClientSetup(format!("Invalid RSA key: {e}")))?;
681            builder = builder.app(app_id, key);
682        }
683
684        // Set base URI if provided
685        if let Some(uri) = self.base_uri {
686            builder = builder
687                .base_uri(&uri)
688                .map_err(|e| GitHubError::ClientSetup(e.to_string()))?;
689        }
690
691        // Build Octocrab instance
692        let octocrab = builder
693            .build()
694            .map_err(|e| GitHubError::ClientSetup(e.to_string()))?;
695
696        Ok(GitHubClient {
697            inner: Arc::new(octocrab),
698        })
699    }
700}
701
702impl Default for GitHubClientBuilder {
703    fn default() -> Self {
704        Self::new()
705    }
706}