use std::collections::{HashMap, VecDeque};
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use crate::git;
use super::App;
use super::types::AppEvent;
fn group_pr_requests<'a>(
requests: impl IntoIterator<Item = (&'a PathBuf, &'a PathBuf, &'a str)>,
) -> HashMap<PathBuf, (PathBuf, Vec<String>)> {
let mut grouped: HashMap<PathBuf, (PathBuf, Vec<String>)> = HashMap::new();
for (key, path, branch) in requests {
let (_, branches) = grouped
.entry(key.clone())
.or_insert_with(|| (path.clone(), Vec::new()));
branches.push(branch.to_string());
}
for (_, branches) in grouped.values_mut() {
branches.sort();
branches.dedup();
}
grouped
}
impl App {
pub(super) fn spawn_git_status_fetch(&self) {
if self
.is_git_fetching
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
return;
}
let tx = self.event_tx.clone();
let is_fetching = self.is_git_fetching.clone();
let main_branch = self.config.main_branch.clone();
let mut paths: Vec<PathBuf> = self.all_agents.iter().map(|a| a.path.clone()).collect();
for wt in &self.worktrees {
if !paths.contains(&wt.path) {
paths.push(wt.path.clone());
}
}
std::thread::spawn(move || {
struct ResetFlag(Arc<AtomicBool>);
impl Drop for ResetFlag {
fn drop(&mut self) {
self.0.store(false, Ordering::SeqCst);
}
}
let _reset = ResetFlag(is_fetching);
for path in paths {
let status = git::get_git_status(&path, main_branch.as_deref());
let _ = tx.send(AppEvent::GitStatus(path, status));
}
});
}
pub(super) fn spawn_pr_status_fetch(&self) -> bool {
if self
.is_pr_fetching
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
return false;
}
let worktree_requests = self
.all_worktrees
.iter()
.map(|wt| (&wt.repo_key, &wt.path, wt.branch.as_str()));
let agent_requests = self.all_agents.iter().filter_map(|agent| {
let branch = self.git_statuses.get(&agent.path)?.branch.as_deref()?;
let key = self.repo_keys.get(&agent.path)?;
Some((key, &agent.path, branch))
});
let repo_branches = group_pr_requests(worktree_requests.chain(agent_requests));
if repo_branches.is_empty() {
self.is_pr_fetching.store(false, Ordering::SeqCst);
return false;
}
let tx = self.event_tx.clone();
let is_fetching = self.is_pr_fetching.clone();
let priority_path = self
.worktree_project_override
.as_ref()
.map(|(_, p)| p.clone())
.or_else(|| self.current_worktree.clone());
std::thread::spawn(move || {
struct ResetFlag(Arc<AtomicBool>);
impl Drop for ResetFlag {
fn drop(&mut self) {
self.0.store(false, Ordering::SeqCst);
}
}
let _reset = ResetFlag(is_fetching);
let priority_repo = priority_path
.as_deref()
.and_then(|path| git::get_repository_key_in(Some(path)).ok());
let mut repos: VecDeque<_> = repo_branches.into_iter().collect();
if let Some(ref priority) = priority_repo {
repos
.make_contiguous()
.sort_by_key(|(repo, _)| repo != priority);
}
let queue = Arc::new(Mutex::new(repos));
let workers = queue.lock().unwrap().len().min(4);
std::thread::scope(|s| {
for _ in 0..workers {
let queue = Arc::clone(&queue);
let tx = tx.clone();
s.spawn(move || {
loop {
let Some((repo_key, (checkout, branches))) =
queue.lock().unwrap().pop_front()
else {
break;
};
match crate::github::list_branch_summaries(&checkout, &branches) {
Ok(summaries) => {
let _ = tx.send(AppEvent::GithubStatus(repo_key, summaries));
}
Err(e) => {
tracing::warn!(
"Failed to fetch GitHub status for {:?}: {}",
checkout,
e
);
}
}
}
});
}
});
});
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn requests_use_stable_keys_and_checkout_paths_without_main_rows() {
let key = PathBuf::from("/project/.git");
let first = PathBuf::from("/project/worktrees/branch-1");
let second = PathBuf::from("/project/worktrees/branch-2");
let agent = second.join("src");
let grouped = group_pr_requests([
(&key, &first, "branch-1"),
(&key, &second, "branch-2"),
(&key, &agent, "branch-2"),
]);
assert_eq!(grouped.len(), 1);
assert_eq!(
grouped[&key],
(first, vec!["branch-1".into(), "branch-2".into()])
);
let refreshed = group_pr_requests([(&key, &second, "branch-2")]);
assert_eq!(refreshed[&key], (second, vec!["branch-2".into()]));
}
#[test]
fn same_named_repositories_have_separate_requests() {
let key_a = PathBuf::from("/a/project/.git");
let key_b = PathBuf::from("/b/project/.git");
let checkout_a = PathBuf::from("/a/project/worktrees/feature");
let checkout_b = PathBuf::from("/b/project/worktrees/feature");
let grouped = group_pr_requests([
(&key_a, &checkout_a, "feature"),
(&key_b, &checkout_b, "feature"),
]);
assert_eq!(grouped.len(), 2);
assert_eq!(grouped[&key_a].0, checkout_a);
assert_eq!(grouped[&key_b].0, checkout_b);
}
}