1use crate::config::Config;
2use crate::github::cache::CacheConfig;
3use crate::github::types::PullRequest;
4use crate::scoring::{calculate_score, merge_scoring_configs, ScoreResult};
5use crate::snooze::{filter_active_prs, filter_snoozed_prs, SnoozeState};
6use anyhow::Result;
7use futures::stream::{FuturesUnordered, StreamExt};
8use std::collections::{HashMap, HashSet};
9use std::fmt;
10
11#[derive(Debug)]
15pub struct AuthError {
16 pub message: String,
17}
18
19impl fmt::Display for AuthError {
20 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21 write!(f, "{}", self.message)
22 }
23}
24
25impl std::error::Error for AuthError {}
26
27pub async fn fetch_and_score_prs(
33 client: &octocrab::Octocrab,
34 config: &Config,
35 snooze_state: &SnoozeState,
36 cache_config: &CacheConfig,
37 verbose: bool,
38 auth_username: Option<&str>,
39) -> Result<(
40 Vec<(PullRequest, ScoreResult)>,
41 Vec<(PullRequest, ScoreResult)>,
42 Option<u64>,
43)> {
44 if verbose {
45 let cache_status = if cache_config.enabled {
46 "enabled"
47 } else {
48 "disabled (--no-cache)"
49 };
50 eprintln!("Cache: {}", cache_status);
51 }
52
53 let global_scoring = config.scoring.clone().unwrap_or_default();
55
56 let mut all_prs = Vec::new();
58 let mut any_succeeded = false;
59
60 let mut futures = FuturesUnordered::new();
61 let auth_username_owned = auth_username.map(|s| s.to_string());
62 for (query_index, query_config) in config.queries.iter().enumerate() {
63 let client = client.clone();
64 let query = query_config.query.clone();
65 let query_name = query_config.name.clone();
66 let auth_username_clone = auth_username_owned.clone();
67 let merged_scoring = merge_scoring_configs(&global_scoring, query_config.scoring.as_ref());
69 let exclude_patterns = merged_scoring.size.and_then(|s| s.exclude);
70 futures.push(async move {
71 let result = crate::github::search_and_enrich_prs(
72 &client,
73 &query,
74 auth_username_clone.as_deref(),
75 exclude_patterns,
76 )
77 .await;
78 (query_name, query, query_index, result)
79 });
80 }
81
82 while let Some((name, query, query_index, result)) = futures.next().await {
83 match result {
84 Ok(prs) => {
85 if verbose {
86 eprintln!(
87 " Found {} PRs for {}",
88 prs.len(),
89 name.as_deref().unwrap_or(&query)
90 );
91 }
92 all_prs.extend(prs.into_iter().map(|pr| (pr, query_index)));
94 any_succeeded = true;
95 }
96 Err(e) => {
97 if e.downcast_ref::<AuthError>().is_some() {
99 return Err(e);
100 }
101 eprintln!(
102 "Query failed: {} - {}",
103 name.as_deref().unwrap_or(&query),
104 e
105 );
106 }
107 }
108 }
109
110 if !any_succeeded && !config.queries.is_empty() {
112 anyhow::bail!("All queries failed. Check your network connection and GitHub token.");
113 }
114
115 let mut seen_urls = HashSet::new();
118 let mut pr_to_query_index = HashMap::new();
119 let unique_prs: Vec<_> = all_prs
120 .into_iter()
121 .filter_map(|(pr, query_idx)| {
122 if seen_urls.insert(pr.url.clone()) {
123 pr_to_query_index.insert(pr.url.clone(), query_idx);
124 Some(pr)
125 } else {
126 None
127 }
128 })
129 .collect();
130
131 if verbose {
132 eprintln!("After deduplication: {} unique PRs", unique_prs.len());
133 }
134
135 let active_prs = filter_active_prs(unique_prs.clone(), snooze_state);
137 let snoozed_prs = filter_snoozed_prs(unique_prs, snooze_state);
138
139 if verbose {
140 eprintln!(
141 "After filter: {} active, {} snoozed",
142 active_prs.len(),
143 snoozed_prs.len()
144 );
145 }
146
147 let mut active_scored: Vec<_> = active_prs
149 .into_iter()
150 .map(|pr| {
151 let query_idx = pr_to_query_index.get(&pr.url).copied().unwrap_or(0);
153 let scoring =
154 merge_scoring_configs(&global_scoring, config.queries[query_idx].scoring.as_ref());
155 let result = calculate_score(&pr, &scoring);
156 (pr, result)
157 })
158 .collect();
159
160 let mut snoozed_scored: Vec<_> = snoozed_prs
162 .into_iter()
163 .map(|pr| {
164 let query_idx = pr_to_query_index.get(&pr.url).copied().unwrap_or(0);
166 let scoring =
167 merge_scoring_configs(&global_scoring, config.queries[query_idx].scoring.as_ref());
168 let result = calculate_score(&pr, &scoring);
169 (pr, result)
170 })
171 .collect();
172
173 let sort_fn = |a: &(PullRequest, ScoreResult), b: &(PullRequest, ScoreResult)| {
175 let score_cmp =
177 b.1.score
178 .partial_cmp(&a.1.score)
179 .unwrap_or(std::cmp::Ordering::Equal);
180 if score_cmp != std::cmp::Ordering::Equal {
181 return score_cmp;
182 }
183 a.0.created_at.cmp(&b.0.created_at)
185 };
186
187 active_scored.sort_by(sort_fn);
188 snoozed_scored.sort_by(sort_fn);
189
190 let rate_limit_remaining = match client.ratelimit().get().await {
192 Ok(rate_limit) => Some(rate_limit.resources.core.remaining as u64),
193 Err(_) => None,
194 };
195
196 Ok((active_scored, snoozed_scored, rate_limit_remaining))
197}