use crate::utils::{
APP_NAME,
SPINNER_TICK,
TrackingFile,
repo_is_tracked,
path_is_repo
};
use std::fs::{OpenOptions, File};
use std::io::Write as _;
use std::fmt::Write as _;
use std::time::Duration;
use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex};
use walkdir::{WalkDir, DirEntry};
use wait_timeout::ChildExt;
use indicatif::{MultiProgress, ProgressBar};
use colored::Colorize;
#[allow(clippy::redundant_closure_for_method_calls)]
pub fn search_for_repos(dirs: &[String], tracking_file: &TrackingFile, scan_hidden: bool) -> Result<String, String> {
let mut repos = String::new();
let track_file = OpenOptions::new()
.create(true)
.append(true)
.open(tracking_file.path.clone())
.map_err(|e| format!("{}: {e}", tracking_file.path))?;
if scan_hidden {
for dir in dirs {
for entry in WalkDir::new(dir)
.follow_links(true)
.same_file_system(true)
.into_iter()
.filter_map(|n| n.ok()) {
match search_core(&entry, &track_file, tracking_file.path.as_str(), tracking_file.contents.as_str()) {
Ok(s) => repos.push_str(&s),
Err(e) => return Err(e)
}
}
}
}
else {
for dir in dirs {
for entry in WalkDir::new(dir)
.follow_links(true)
.same_file_system(true)
.into_iter()
.filter_entry(|n| !entry_is_hidden(n))
.filter_map(|n| n.ok()) {
match search_core(&entry, &track_file, tracking_file.path.as_str(), tracking_file.contents.as_str()) {
Ok(s) => repos.push_str(&s),
Err(e) => return Err(e)
}
}
}
}
Ok(repos)
}
fn search_core(entry: &DirEntry, mut track_file: &File, track_file_path: &str, track_file_contents: &str) -> Result<String, String> {
let mut repo = String::new();
if let Some(path) = entry.path().to_str() {
if let Some(repo_path) = path.strip_suffix("/.git") {
if repo_is_tracked(repo_path, track_file_contents) {
return Ok(repo)
}
match path_is_repo(repo_path) {
Ok(is_repo) => {
if is_repo {
track_file.write_all(
format!("{repo_path}\n").as_bytes())
.map_err(|e| format!("{track_file_path}: {e}"))?;
repo = format!("{repo_path}\n");
}
},
Err(e) => return Err(e)
};
}
}
Ok(repo)
}
fn entry_is_hidden(entry: &DirEntry) -> bool {
entry.file_name()
.to_str()
.is_some_and(|s| s.starts_with('.') && s != ".git")
}
pub async fn exec_async_check(repos: Vec<String>, flags: Vec<bool>) -> Result<(), String> {
let final_output = Arc::new(Mutex::new(String::new()));
let multi_prog = MultiProgress::new();
let mut tasks = Vec::new();
for repo in repos {
let multi_prog_clone = multi_prog.clone();
let final_output_clone = Arc::clone(&final_output);
let flags_clone = flags.clone();
tasks.push(tokio::spawn(async move {
let spinner = multi_prog_clone.add(ProgressBar::new_spinner());
spinner.set_message(repo.bold().to_string());
spinner.enable_steady_tick(Duration::from_millis(SPINNER_TICK));
match inspect_repo(repo.as_str(), flags_clone.as_slice()) {
Ok(output) => {
spinner.finish_and_clear();
if !output.is_empty() {
writeln!(
final_output_clone
.lock()
.unwrap_or_else(|_| panic!("'{repo}' Mutex lock")),
"{output}"
)
.unwrap_or_else(|_| panic!("'{repo}' output write"));
}
},
Err(e) => spinner.finish_with_message(format!("{APP_NAME}: {e}"))
};
}));
}
for task in tasks {
task.await.map_err(|e| e.to_string())?;
}
print!("{}", final_output.lock().unwrap());
Ok(())
}
fn inspect_repo(repo: &str, flags: &[bool]) -> Result<String, String> {
let print_status = flags[0];
let print_remotes = flags[1];
let mut status_output = String::new();
if !print_remotes {
status_output = repo_status(repo)?;
}
let mut final_output = String::new();
let mut remotes_output = String::new();
if !print_status {
let git_branch_out = Command::new("git")
.args(["-C", repo, "branch"])
.stderr(Stdio::null())
.output()
.map_err(|e| format!("git: {e}"))?
.stdout;
let git_branch_str = String::from_utf8_lossy(git_branch_out.as_slice());
if git_branch_str.is_empty() {
return Ok(final_output)
}
let branches: Vec<String> = git_branch_str
.lines()
.map(|mut s| {
s = s.trim();
s.replace("* ", "")
})
.collect();
let mut remotes: Vec<&str> = Vec::new();
let git_remote_out = Command::new("git")
.args(["-C", repo, "remote"])
.stderr(Stdio::null())
.output()
.map_err(|e| format!("git: {e}"))?
.stdout;
let git_remote_str = String::from_utf8_lossy(git_remote_out.as_slice());
if !git_remote_str.is_empty() {
remotes = git_remote_str.lines().collect();
}
for remote in &remotes {
let mut git_fetch = Command::new("git")
.args(["-C", repo, "fetch", remote])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|e| format!("git: {e}"))?;
if git_fetch.wait_timeout(Duration::from_secs(10))
.map_err(|e| format!("git fetch: {e}"))?
.is_none() {
git_fetch.kill().map_err(|e| format!("git fetch: {e}"))?;
git_fetch.wait().map_err(|e| format!("git fetch: {e}"))?;
}
}
for branch in branches {
write!(
remotes_output,
"{}",
remotes_diff(repo, branch.as_str(), remotes.clone())?
)
.map_err(|e| e.to_string())?;
}
}
if !status_output.is_empty() || !remotes_output.is_empty() {
final_output = format!("\r{}\n{status_output}{remotes_output}", repo.bold());
}
Ok(final_output)
}
fn repo_status(repo: &str) -> Result<String, String> {
let mut output = String::new();
let git_status_out = Command::new("git")
.args(["-C", repo, "status", "-s"])
.stderr(Stdio::null())
.output()
.map_err(|e| format!("git: {e}"))?
.stdout;
let git_status_str = String::from_utf8_lossy(git_status_out.as_slice());
if !git_status_str.is_empty() {
for line in git_status_str.lines() {
writeln!(output, " {}", line.trim())
.map_err(|e| e.to_string())?;
}
}
Ok(output)
}
fn remotes_diff(repo: &str, branch: &str, remotes: Vec<&str>) -> Result<String, String> {
let mut output = String::new();
for remote in remotes {
let remote_branch = format!("{remote}/{branch}");
let git_rev_list_out = Command::new("git")
.args([
"-C",
repo,
"rev-list",
"--left-right",
"--count",
format!("{remote_branch}...{branch}").as_str()
])
.stderr(Stdio::null())
.output()
.map_err(|e| format!("git: {e}"))?
.stdout;
let git_rev_list_str = String::from_utf8_lossy(git_rev_list_out.as_slice());
if git_rev_list_str.is_empty() {
writeln!(output, " missing from '{remote}' remote")
.map_err(|e| e.to_string())?;
continue;
}
let git_rev_list_vec: Vec<&str> = git_rev_list_str.split_whitespace().collect();
let (behind, ahead): (u32, u32) = (
git_rev_list_vec[0].parse().unwrap(),
git_rev_list_vec[1].parse().unwrap()
);
if behind == 0 && ahead == 0 {
continue;
}
if ahead == 0 {
writeln!(output, " {behind} commit(s) behind {remote_branch}")
.map_err(|e| e.to_string())?;
continue;
}
if behind == 0 {
writeln!(output, " {ahead} commit(s) ahead of {remote_branch}")
.map_err(|e| e.to_string())?;
continue;
}
writeln!(output, " {ahead} commit(s) ahead of, {behind} commit(s) behind {remote_branch}")
.map_err(|e| e.to_string())?;
}
if !output.is_empty() {
output.insert_str(0, format!(" {}:\n", branch.underline()).as_str());
}
Ok(output)
}