github_fetch/
lib.rs

1pub mod client;
2pub mod config;
3pub mod discussion;
4pub mod error;
5pub mod filters;
6pub mod types;
7
8pub use client::GitHubClient;
9pub use config::{FetchConfig, GitHubConfig, RateLimitConfig};
10pub use discussion::DiscussionClient;
11pub use error::{GitHubFetchError, Result};
12pub use filters::{
13    extract_error_codes, has_code_blocks, has_rust_error_codes, DateRange, IssueFilters, IssueState,
14};
15pub use types::{
16    CollectionResult, Discussion, DiscussionComment, GitHubComment, GitHubIssue, GitHubLabel,
17    GitHubUser, PrFile, PrReview, PrReviewComment, Repository,
18};
19
20pub struct GitHubFetcher {
21    client: GitHubClient,
22    discussion_client: Option<DiscussionClient>,
23}
24
25impl GitHubFetcher {
26    pub fn new(token: Option<String>) -> Result<Self> {
27        let config = if let Some(token) = token {
28            std::env::set_var("GITHUB_TOKEN", token);
29            FetchConfig::default()
30        } else {
31            FetchConfig::default()
32        };
33
34        Self::with_config(config)
35    }
36
37    pub fn with_config(config: FetchConfig) -> Result<Self> {
38        let client = GitHubClient::with_config(config.clone())?;
39        let discussion_client = DiscussionClient::new(config.github).ok();
40
41        Ok(Self {
42            client,
43            discussion_client,
44        })
45    }
46
47    pub async fn fetch_issues(
48        &self,
49        repo: &Repository,
50        filters: &IssueFilters,
51    ) -> Result<Vec<GitHubIssue>> {
52        let result = self.client.fetch_issues(repo, filters, None).await?;
53        Ok(result.issues)
54    }
55
56    pub async fn fetch_issues_with_limit(
57        &self,
58        repo: &Repository,
59        filters: &IssueFilters,
60        max_issues: usize,
61    ) -> Result<CollectionResult> {
62        self.client
63            .fetch_issues(repo, filters, Some(max_issues))
64            .await
65    }
66
67    pub async fn fetch_issue(&self, repo: &Repository, number: u64) -> Result<GitHubIssue> {
68        self.client.fetch_issue(repo, number).await
69    }
70
71    pub async fn fetch_pr(&self, repo: &Repository, number: u64) -> Result<GitHubIssue> {
72        self.client.fetch_pr(repo, number).await
73    }
74
75    pub async fn fetch_comments(
76        &self,
77        repo: &Repository,
78        issue_number: u64,
79    ) -> Result<Vec<GitHubComment>> {
80        self.client.fetch_comments(repo, issue_number).await
81    }
82
83    pub async fn fetch_pr_files(&self, repo: &Repository, pr_number: u64) -> Result<Vec<PrFile>> {
84        self.client.fetch_pr_files(repo, pr_number).await
85    }
86
87    /// Fetch all reviews for a PR (approved, changes requested, etc.)
88    pub async fn fetch_pr_reviews(
89        &self,
90        repo: &Repository,
91        pr_number: u64,
92    ) -> Result<Vec<PrReview>> {
93        self.client.fetch_pr_reviews(repo, pr_number).await
94    }
95
96    /// Fetch all review comments (inline comments on diff) for a PR
97    pub async fn fetch_pr_review_comments(
98        &self,
99        repo: &Repository,
100        pr_number: u64,
101    ) -> Result<Vec<PrReviewComment>> {
102        self.client.fetch_pr_review_comments(repo, pr_number).await
103    }
104
105    pub async fn fetch_discussion(
106        &self,
107        repo: &Repository,
108        discussion_number: u64,
109    ) -> Result<Discussion> {
110        let discussion_client = self.discussion_client.as_ref().ok_or_else(|| {
111            GitHubFetchError::ConfigError("Discussion client not initialized".to_string())
112        })?;
113
114        discussion_client
115            .fetch_discussion(repo, discussion_number)
116            .await
117    }
118
119    pub async fn fetch_discussion_by_url(&self, discussion_url: &str) -> Result<Discussion> {
120        let discussion_client = self.discussion_client.as_ref().ok_or_else(|| {
121            GitHubFetchError::ConfigError("Discussion client not initialized".to_string())
122        })?;
123
124        discussion_client
125            .fetch_discussion_by_url(discussion_url)
126            .await
127    }
128
129    pub async fn test_connection(&self) -> Result<()> {
130        self.client.test_connection().await
131    }
132
133    pub async fn get_rate_limit(&self) -> Result<String> {
134        self.client.get_rate_limit().await
135    }
136}
137
138pub struct GitHubFetcherBuilder {
139    config: FetchConfig,
140}
141
142impl GitHubFetcherBuilder {
143    pub fn new() -> Self {
144        Self {
145            config: FetchConfig::default(),
146        }
147    }
148
149    pub fn token(self, token: impl Into<String>) -> Self {
150        std::env::set_var(&self.config.github.token_env_var, token.into());
151        self
152    }
153
154    pub fn token_env_var(mut self, var_name: impl Into<String>) -> Self {
155        self.config.github.token_env_var = var_name.into();
156        self
157    }
158
159    pub fn api_base_url(mut self, url: impl Into<String>) -> Self {
160        self.config.github.api_base_url = url.into();
161        self
162    }
163
164    pub fn user_agent(mut self, agent: impl Into<String>) -> Self {
165        self.config.github.user_agent = agent.into();
166        self
167    }
168
169    pub fn timeout(mut self, seconds: u64) -> Self {
170        self.config.github.timeout_seconds = seconds;
171        self
172    }
173
174    pub fn rate_limit(mut self, requests_per_minute: u32) -> Self {
175        self.config.rate_limiting.requests_per_minute = requests_per_minute;
176        self.config.rate_limiting.delay_between_requests_ms =
177            60_000 / requests_per_minute.max(1) as u64;
178        self
179    }
180
181    pub fn max_retries(mut self, retries: u32) -> Self {
182        self.config.rate_limiting.max_retries = retries;
183        self
184    }
185
186    pub fn build(self) -> Result<GitHubFetcher> {
187        GitHubFetcher::with_config(self.config)
188    }
189}
190
191impl Default for GitHubFetcherBuilder {
192    fn default() -> Self {
193        Self::new()
194    }
195}