use super::{App, Level};
use crate::git::load::{self, Batch};
use crate::git::{
Progress, RepoStatus, Stage, find_repos_counting, first_line, repo_status, with_remote_limit,
};
use crate::tui::widgets::{Modal, SPINNER};
use rayon::prelude::*;
use std::path::Path;
use std::sync::Arc;
use std::thread;
use std::time::Instant;
#[derive(Clone, Copy, PartialEq)]
pub enum Scope {
Dirty,
All,
Unpushed,
}
pub const SCOPES: [Scope; 3] = [Scope::All, Scope::Dirty, Scope::Unpushed];
pub const DEFAULT_SCOPE: usize = 0;
impl Scope {
pub fn label(self) -> &'static str {
match self {
Scope::Dirty => "dirty",
Scope::All => "all",
Scope::Unpushed => "unpushed",
}
}
pub(super) fn keeps(self, r: &RepoStatus) -> bool {
match self {
Scope::Dirty => r.is_dirty(),
Scope::All => true,
Scope::Unpushed => r.is_unpushed(),
}
}
}
fn haystack(r: &RepoStatus) -> String {
format!("{} {} {}", r.name, r.branch, r.origin)
}
fn relative_name(repo: &Path, base: &Path) -> String {
repo.strip_prefix(base)
.ok()
.filter(|p| !p.as_os_str().is_empty())
.map(|p| p.to_string_lossy().replace('\\', "/"))
.unwrap_or_else(|| {
repo.file_name()
.unwrap_or(repo.as_os_str())
.to_string_lossy()
.into_owned()
})
}
pub(super) fn scan(base: &Path, depth: usize, progress: &Progress) -> Vec<RepoStatus> {
let found = find_repos_counting(base, depth, progress);
progress.counted(found.len());
let mut repos: Vec<RepoStatus> = found
.par_iter()
.map(|r| {
let status = repo_status(r);
progress.one_done();
status
})
.collect();
for (repo, path) in repos.iter_mut().zip(&found) {
repo.name = relative_name(path, base);
}
repos.sort_by(|a, b| a.name.cmp(&b.name));
repos
}
pub(super) struct RepoJob {
verb: &'static str,
progress: Arc<Progress>,
since: Instant,
}
impl RepoJob {
fn new(verb: &'static str, progress: Arc<Progress>) -> RepoJob {
RepoJob {
verb,
progress,
since: Instant::now(),
}
}
pub(super) fn line(&self) -> String {
let at = self.since.elapsed().as_millis() / 80;
let mark = SPINNER[at as usize % SPINNER.len()];
match self.progress.stage() {
Stage::Finding(found) => format!("{mark} finding repos {found}"),
Stage::Working(done, total) => format!("{mark} {} {done}/{total}", self.verb),
}
}
}
impl App {
pub(super) fn rescope_repos(&mut self) {
let scope = SCOPES[self.rsel.scope];
let query = &self.rsel.query;
let visible = (0..self.repos.len())
.filter(|&i| scope.keeps(&self.repos[i]) && query.keeps(&haystack(&self.repos[i])))
.collect();
self.rsel.show(visible);
if let Some(want) = self.rsel.restore.clone()
&& let Some(at) = self
.rsel
.visible
.iter()
.position(|&i| self.repos[i].path == want)
{
self.rsel.restored_at(at);
}
}
pub(super) fn read_repos(&mut self) {
if self.repo_job.is_some() {
return;
}
let progress = Arc::new(Progress::default());
self.repo_job = Some(RepoJob::new("reading", progress.clone()));
let (base, depth, tx) = (self.base.clone(), self.depth, self.tx.clone());
thread::spawn(move || {
let repos = scan(&base, depth, &progress);
let _ = tx.send(Batch::RepoScan { repos });
});
}
pub(super) fn take_scan(&mut self, repos: Vec<RepoStatus>) {
self.repo_job = None;
let keep = self.rsel.idx().map(|i| self.repos[i].path.clone());
self.repos = repos;
self.rsel.restore = keep;
self.rescope_repos();
if self.level == Level::Repos {
self.pending = true;
}
}
pub(super) fn sync_all(&mut self, pull: bool) {
if self.repo_job.is_some() {
return;
}
let repos: Vec<(String, String)> = self
.rsel
.visible
.iter()
.map(|&i| (self.repos[i].name.clone(), self.repos[i].path.clone()))
.collect();
let progress = Arc::new(Progress::of(repos.len()));
let verb = if pull { "pulling" } else { "syncing" };
self.repo_job = Some(RepoJob::new(verb, progress.clone()));
self.note = None;
let tx = self.tx.clone();
thread::spawn(move || {
let outcomes = with_remote_limit(|| {
repos
.par_iter()
.map(|(name, path)| {
let done = load::fetch_prune(path).and_then(|()| {
if pull {
load::fast_forward(path).map(|n| n.unwrap_or(0))
} else {
Ok(0)
}
});
progress.one_done();
(name.clone(), done)
})
.collect()
});
let _ = tx.send(Batch::Synced { pull, outcomes });
});
}
pub(super) fn finish_sync(
&mut self,
pull: bool,
outcomes: Vec<(String, Result<usize, String>)>,
) {
let failed: Vec<String> = outcomes
.iter()
.filter_map(|(name, r)| {
r.as_ref()
.err()
.map(|e| format!("{name}: {}", first_line(e)))
})
.collect();
let pulled: usize = outcomes.iter().filter_map(|(_, r)| r.as_ref().ok()).sum();
let into = outcomes
.iter()
.filter(|(_, r)| r.as_ref().is_ok_and(|&n| n > 0))
.count();
let synced = outcomes.len() - failed.len();
let s = if synced == 1 { "" } else { "s" };
let mut text = format!("synced {synced} repo{s}");
if pull && pulled > 0 {
let c = if pulled == 1 { "" } else { "s" };
let r = if into == 1 { "" } else { "s" };
text.push_str(&format!("; pulled {pulled} commit{c} into {into} repo{r}"));
}
if failed.is_empty() {
self.set_status(text);
} else {
self.note = None;
self.held_alert = Some(Modal::new(
format!("{} of {} repos did not sync", failed.len(), outcomes.len()),
failed.join("\n"),
));
}
self.repo_job = None;
self.read_repos();
}
pub(super) fn enter_repo(&mut self) {
let Some(i) = self.rsel.idx() else {
self.branches.clear();
self.bsel.show(Vec::new());
return;
};
self.set_repo(self.repos[i].path.clone());
}
}
#[cfg(test)]
mod tests {
use super::relative_name;
use std::path::Path;
#[test]
fn a_repo_under_the_root_keeps_its_plain_name() {
assert_eq!(
relative_name(Path::new("pg/hscroll"), Path::new("pg")),
"hscroll"
);
}
#[test]
fn a_nested_repo_is_named_by_its_path() {
assert_eq!(
relative_name(Path::new("pg/itidy/work"), Path::new("pg")),
"itidy/work"
);
assert_eq!(
relative_name(Path::new("pg/pushstate/work"), Path::new("pg")),
"pushstate/work"
);
}
#[test]
fn scanning_the_current_directory_drops_the_dot() {
assert_eq!(
relative_name(Path::new("./crates/widget"), Path::new(".")),
"crates/widget"
);
assert_eq!(
relative_name(Path::new("./hscroll"), Path::new(".")),
"hscroll"
);
}
#[test]
fn a_trailing_slash_on_the_root_changes_nothing() {
assert_eq!(
relative_name(Path::new("/pg/crates/widget"), Path::new("/pg/")),
"crates/widget"
);
}
#[test]
fn a_path_outside_the_root_falls_back_to_its_name() {
assert_eq!(
relative_name(Path::new("/elsewhere/widget"), Path::new("/pg")),
"widget"
);
}
}