Skip to main content

autoschematic_connector_github/
github_ext.rs

1use std::collections::HashMap;
2
3use async_trait::async_trait;
4use octocrab::{Octocrab, Page, Result};
5use serde::{Deserialize, Serialize};
6
7use crate::resource::{CollaboratorPrincipal, Role};
8
9// GitHub API response structures for branch protection
10#[derive(Debug, Serialize, Deserialize)]
11pub struct GitHubBranchProtection {
12    pub required_status_checks: Option<GitHubRequiredStatusChecks>,
13    pub enforce_admins: GitHubEnforceAdmins,
14    pub required_pull_request_reviews: Option<GitHubPullRequestReviewEnforcement>,
15    pub restrictions: Option<GitHubBranchRestrictions>,
16    pub required_linear_history: Option<GitHubBooleanSetting>,
17    pub allow_force_pushes: Option<GitHubBooleanSetting>,
18    pub allow_deletions: Option<GitHubBooleanSetting>,
19    pub block_creations: Option<GitHubBooleanSetting>,
20    pub required_conversation_resolution: Option<GitHubBooleanSetting>,
21    pub lock_branch: Option<GitHubBooleanSetting>,
22    pub allow_fork_syncing: Option<GitHubBooleanSetting>,
23}
24
25#[derive(Debug, Serialize, Deserialize)]
26pub struct GitHubRequiredStatusChecks {
27    pub strict: bool,
28    pub contexts: Vec<String>,
29}
30
31#[derive(Debug, Serialize, Deserialize)]
32pub struct GitHubEnforceAdmins {
33    pub enabled: bool,
34}
35
36#[derive(Debug, Serialize, Deserialize)]
37pub struct GitHubPullRequestReviewEnforcement {
38    pub required_approving_review_count: Option<u32>,
39    pub dismiss_stale_reviews: Option<bool>,
40    pub require_code_owner_reviews: Option<bool>,
41    pub require_last_push_approval: Option<bool>,
42}
43
44#[derive(Debug, Serialize, Deserialize)]
45pub struct GitHubBranchRestrictions {
46    pub users: Vec<GitHubUser>,
47    pub teams: Vec<GitHubTeam>,
48    pub apps: Vec<GitHubApp>,
49}
50
51#[derive(Debug, Serialize, Deserialize)]
52pub struct GitHubUser {
53    pub login: String,
54}
55
56#[derive(Debug, Serialize, Deserialize)]
57pub struct GitHubTeam {
58    pub name: String,
59}
60
61#[derive(Debug, Serialize, Deserialize)]
62pub struct GitHubApp {
63    pub name: String,
64}
65
66#[derive(Debug, Serialize, Deserialize)]
67pub struct GitHubBooleanSetting {
68    pub enabled: bool,
69}
70
71// Collaborator response structure
72#[derive(Debug, Serialize, Deserialize)]
73pub struct GitHubCollaboratorPermissions {
74    pub pull: bool,
75    pub triage: bool,
76    pub push: bool,
77    pub maintain: bool,
78    pub admin: bool,
79}
80
81#[derive(Debug, Serialize, Deserialize)]
82pub struct GitHubCollaborator {
83    pub permissions: GitHubCollaboratorPermissions,
84    pub role_name: String,
85}
86
87#[async_trait]
88pub trait BranchProtectionExt {
89    async fn get_branch_protection(&self, owner: &str, repo: &str, branch: &str) -> Result<GitHubBranchProtection>;
90}
91
92#[async_trait]
93impl BranchProtectionExt for Octocrab {
94    async fn get_branch_protection(&self, owner: &str, repo: &str, branch: &str) -> Result<GitHubBranchProtection> {
95        let route = format!("/repos/{}/{}/branches/{}/protection", owner, repo, branch);
96        self.get(route, None::<&()>).await
97    }
98}
99
100#[async_trait]
101pub trait CollaboratorExt {
102    async fn get_collaborator_permission(&self, owner: &str, repo: &str, username: &str) -> Result<GitHubCollaborator>;
103}
104
105#[async_trait]
106impl CollaboratorExt for Octocrab {
107    async fn get_collaborator_permission(&self, owner: &str, repo: &str, username: &str) -> Result<GitHubCollaborator> {
108        let route = format!("/repos/{}/{}/collaborators/{}/permission", owner, repo, username);
109        self.get(route, None::<&()>).await
110    }
111}
112
113// Additional structures for listing
114#[derive(Debug, Serialize, Deserialize)]
115pub struct GitHubBranch {
116    pub name: String,
117    pub protected: bool,
118}
119
120#[derive(Debug, Serialize, Deserialize)]
121pub struct GitHubCollaboratorInfo {
122    pub login: String,
123    pub role_name: String,
124}
125
126#[async_trait]
127pub trait ListExt {
128    // async fn list_user_repos(&self, username: &str) -> Result<octocrab::Page<octocrab::models::Repository>>;
129    async fn list_repo_branches(&self, owner: &str, repo: &str) -> Result<octocrab::Page<GitHubBranch>>;
130    async fn list_repo_collaborators(
131        &self,
132        owner: &str,
133        repo: &str,
134        affiliation: Option<&str>,
135    ) -> Result<HashMap<CollaboratorPrincipal, Role>>;
136}
137
138#[async_trait]
139impl ListExt for Octocrab {
140    // async fn list_user_repos(&self, username: &str) -> Result<octocrab::Page<octocrab::models::Repository>> {
141    //     let route = format!("/users/{}/repos", username);
142    //     self.get(route, None::<&()>).await
143    // }
144
145    async fn list_repo_branches(&self, owner: &str, repo: &str) -> Result<octocrab::Page<GitHubBranch>> {
146        let route = format!("/repos/{}/{}/branches", owner, repo);
147        self.get(route, None::<&()>).await
148    }
149
150    async fn list_repo_collaborators(
151        &self,
152        owner: &str,
153        repo: &str,
154        affiliation: Option<&str>,
155    ) -> Result<HashMap<CollaboratorPrincipal, Role>> {
156        let mut res = HashMap::new();
157
158        #[derive(serde::Serialize)]
159        struct CollabQuery<'a> {
160            affiliation: &'a str, // "direct" | "outside" | "all"
161            per_page: u8,
162            page: u32,
163        }
164
165        let route = format!("/repos/{}/{}/collaborators", owner, repo);
166        let users: Page<GitHubCollaboratorInfo> = self
167            .get(
168                route,
169                affiliation
170                    .map(|affiliation| CollabQuery {
171                        affiliation: affiliation,
172                        per_page: 100,
173                        page: 1,
174                    })
175                    .as_ref(),
176            )
177            .await?;
178
179        let users = self.all_pages(users).await?;
180
181        for user in users {
182            res.insert(CollaboratorPrincipal::User(user.login), Role::from_str(&user.role_name));
183        }
184
185        Ok(res)
186    }
187
188    // async fn list_repo_collaborators(
189    //     &self,
190    //     owner: &str,
191    //     repo: &str,
192    //     affiliation: Option<&str>,
193    // ) -> Result<octocrab::Page<GitHubCollaboratorInfo>> {
194    //     #[derive(serde::Serialize)]
195    //     struct CollabQuery<'a> {
196    //         affiliation: &'a str, // "direct" | "outside" | "all"
197    //         per_page: u8,
198    //         page: u32,
199    //     }
200
201    //     let route = format!("/repos/{}/{}/collaborators", owner, repo);
202    //     self.get(
203    //         route,
204    //         affiliation
205    //             .map(|affiliation| CollabQuery {
206    //                 affiliation: affiliation,
207    //                 per_page: 100,
208    //                 page: 1,
209    //             })
210    //             .as_ref(),
211    //     )
212    //     .await
213    // }
214}
215
216// Structures for repository operations
217#[derive(Debug, Serialize, Deserialize)]
218pub struct CreateRepositoryRequest {
219    pub name: String,
220    pub description: Option<String>,
221    pub homepage: Option<String>,
222    pub private: bool,
223    pub has_issues: bool,
224    pub has_projects: bool,
225    pub has_wiki: bool,
226    pub allow_squash_merge: bool,
227    pub allow_merge_commit: bool,
228    pub allow_rebase_merge: bool,
229    pub allow_auto_merge: bool,
230    pub delete_branch_on_merge: bool,
231    pub default_branch: Option<String>,
232}
233
234#[derive(Debug, Serialize, Deserialize)]
235pub struct UpdateRepositoryRequest {
236    pub name: Option<String>,
237    pub description: Option<String>,
238    pub homepage: Option<String>,
239    pub private: Option<bool>,
240    pub has_issues: Option<bool>,
241    pub has_projects: Option<bool>,
242    pub has_wiki: Option<bool>,
243    pub allow_squash_merge: Option<bool>,
244    pub allow_merge_commit: Option<bool>,
245    pub allow_rebase_merge: Option<bool>,
246    pub allow_auto_merge: Option<bool>,
247    pub delete_branch_on_merge: Option<bool>,
248    pub default_branch: Option<String>,
249    pub archived: Option<bool>,
250}
251
252// Structures for branch protection operations
253#[derive(Debug, Serialize, Deserialize)]
254pub struct CreateBranchProtectionRequest {
255    pub required_status_checks: Option<GitHubRequiredStatusChecks>,
256    pub enforce_admins: bool,
257    pub required_pull_request_reviews: Option<GitHubPullRequestReviewEnforcement>,
258    pub restrictions: Option<GitHubBranchRestrictions>,
259    pub required_linear_history: Option<bool>,
260    pub allow_force_pushes: Option<bool>,
261    pub allow_deletions: Option<bool>,
262    pub block_creations: Option<bool>,
263    pub required_conversation_resolution: Option<bool>,
264    pub lock_branch: Option<bool>,
265    pub allow_fork_syncing: Option<bool>,
266}
267
268// Structures for collaborator operations
269#[derive(Debug, Serialize, Deserialize)]
270pub struct AddCollaboratorRequest {
271    pub permission: String, // "pull", "triage", "push", "maintain", "admin"
272}
273
274#[derive(Debug, Serialize, Deserialize)]
275pub struct AddTeamCollaboratorRequest {
276    pub permission: String, // "pull", "triage", "push", "maintain", "admin"
277}
278
279#[async_trait]
280pub trait RepositoryOpsExt {
281    async fn create_repository(&self, owner: &str, repo_data: &CreateRepositoryRequest)
282    -> Result<octocrab::models::Repository>;
283    async fn update_repository(
284        &self,
285        owner: &str,
286        repo: &str,
287        repo_data: &UpdateRepositoryRequest,
288    ) -> Result<octocrab::models::Repository>;
289    async fn delete_repository(&self, owner: &str, repo: &str) -> Result<()>;
290}
291
292#[async_trait]
293impl RepositoryOpsExt for Octocrab {
294    async fn create_repository(
295        &self,
296        _owner: &str,
297        repo_data: &CreateRepositoryRequest,
298    ) -> Result<octocrab::models::Repository> {
299        let route = format!("/user/repos");
300        self.post(route, Some(repo_data)).await
301    }
302
303    async fn update_repository(
304        &self,
305        owner: &str,
306        repo: &str,
307        repo_data: &UpdateRepositoryRequest,
308    ) -> Result<octocrab::models::Repository> {
309        let route = format!("/repos/{}/{}", owner, repo);
310        self.patch(route, Some(repo_data)).await
311    }
312
313    async fn delete_repository(&self, owner: &str, repo: &str) -> Result<()> {
314        let route = format!("/repos/{}/{}", owner, repo);
315        self.delete(route, None::<&()>).await
316    }
317}
318
319#[async_trait]
320pub trait BranchProtectionOpsExt {
321    async fn create_branch_protection(
322        &self,
323        owner: &str,
324        repo: &str,
325        branch: &str,
326        protection_data: &CreateBranchProtectionRequest,
327    ) -> Result<GitHubBranchProtection>;
328    async fn update_branch_protection(
329        &self,
330        owner: &str,
331        repo: &str,
332        branch: &str,
333        protection_data: &CreateBranchProtectionRequest,
334    ) -> Result<GitHubBranchProtection>;
335    async fn delete_branch_protection(&self, owner: &str, repo: &str, branch: &str) -> Result<()>;
336}
337
338#[async_trait]
339impl BranchProtectionOpsExt for Octocrab {
340    async fn create_branch_protection(
341        &self,
342        owner: &str,
343        repo: &str,
344        branch: &str,
345        protection_data: &CreateBranchProtectionRequest,
346    ) -> Result<GitHubBranchProtection> {
347        let route = format!("/repos/{}/{}/branches/{}/protection", owner, repo, branch);
348        self.put(route, Some(protection_data)).await
349    }
350
351    async fn update_branch_protection(
352        &self,
353        owner: &str,
354        repo: &str,
355        branch: &str,
356        protection_data: &CreateBranchProtectionRequest,
357    ) -> Result<GitHubBranchProtection> {
358        let route = format!("/repos/{}/{}/branches/{}/protection", owner, repo, branch);
359        self.put(route, Some(protection_data)).await
360    }
361
362    async fn delete_branch_protection(&self, owner: &str, repo: &str, branch: &str) -> Result<()> {
363        let route = format!("/repos/{}/{}/branches/{}/protection", owner, repo, branch);
364        self.delete(route, None::<&()>).await
365    }
366}
367
368#[async_trait]
369pub trait CollaboratorOpsExt {
370    async fn add_collaborator(
371        &self,
372        owner: &str,
373        repo: &str,
374        username: &str,
375        permission_data: &AddCollaboratorRequest,
376    ) -> Result<()>;
377    async fn update_collaborator_permission(
378        &self,
379        owner: &str,
380        repo: &str,
381        username: &str,
382        permission_data: &AddCollaboratorRequest,
383    ) -> Result<()>;
384    async fn remove_collaborator(&self, owner: &str, repo: &str, username: &str) -> Result<()>;
385    async fn add_team_to_repository(
386        &self,
387        owner: &str,
388        repo: &str,
389        team_slug: &str,
390        permission_data: &AddTeamCollaboratorRequest,
391    ) -> Result<()>;
392    async fn update_team_permission(
393        &self,
394        owner: &str,
395        repo: &str,
396        team_slug: &str,
397        permission_data: &AddTeamCollaboratorRequest,
398    ) -> Result<()>;
399    async fn remove_team_from_repository(&self, owner: &str, repo: &str, team_slug: &str) -> Result<()>;
400}
401
402#[async_trait]
403impl CollaboratorOpsExt for Octocrab {
404    async fn add_collaborator(
405        &self,
406        owner: &str,
407        repo: &str,
408        username: &str,
409        permission_data: &AddCollaboratorRequest,
410    ) -> Result<()> {
411        let route = format!("/repos/{}/{}/collaborators/{}", owner, repo, username);
412        self.put(route, Some(permission_data)).await
413    }
414
415    async fn update_collaborator_permission(
416        &self,
417        owner: &str,
418        repo: &str,
419        username: &str,
420        permission_data: &AddCollaboratorRequest,
421    ) -> Result<()> {
422        let route = format!("/repos/{}/{}/collaborators/{}", owner, repo, username);
423        self.put(route, Some(permission_data)).await
424    }
425
426    async fn remove_collaborator(&self, owner: &str, repo: &str, username: &str) -> Result<()> {
427        let route = format!("/repos/{}/{}/collaborators/{}", owner, repo, username);
428        self.delete(route, None::<&()>).await
429    }
430
431    async fn add_team_to_repository(
432        &self,
433        owner: &str,
434        repo: &str,
435        team_slug: &str,
436        permission_data: &AddTeamCollaboratorRequest,
437    ) -> Result<()> {
438        let route = format!("/orgs/{}/teams/{}/repos/{}/{}", owner, team_slug, owner, repo);
439        self.put(route, Some(permission_data)).await
440    }
441
442    async fn update_team_permission(
443        &self,
444        owner: &str,
445        repo: &str,
446        team_slug: &str,
447        permission_data: &AddTeamCollaboratorRequest,
448    ) -> Result<()> {
449        let route = format!("/orgs/{}/teams/{}/repos/{}/{}", owner, team_slug, owner, repo);
450        self.put(route, Some(permission_data)).await
451    }
452
453    async fn remove_team_from_repository(&self, owner: &str, repo: &str, team_slug: &str) -> Result<()> {
454        let route = format!("/orgs/{}/teams/{}/repos/{}/{}", owner, team_slug, owner, repo);
455        self.delete(route, None::<&()>).await
456    }
457}