use crate::error::{SkipStep, TopgradeError}; use crate::executor::{CommandExt, RunType}; use crate::terminal::print_separator; use crate::utils::which; use anyhow::Result; use console::style; use glob::{glob_with, MatchOptions}; use log::{debug, error}; use std::collections::HashSet; use std::io; use std::io::Read; use std::path::{Path, PathBuf}; use std::process::{Child, Command, Stdio}; use std::thread::sleep; use std::time::Duration; #[cfg(windows)] static PATH_PREFIX: &str = "\\\\?\\"; #[derive(Debug)] pub struct Git { git: Option, } struct PullProcess { child: Child, repo: String, before_revision: Option, } pub struct Repositories<'a> { git: &'a Git, repositories: HashSet, glob_match_options: MatchOptions, } fn get_head_revision(git: &Path, repo: &str) -> Option { Command::new(git) .args(&["rev-parse", "HEAD"]) .current_dir(repo) .check_output() .map(|output| output.trim().to_string()) .map_err(|e| { error!("Error getting revision for {}: {}", repo, e); e }) .ok() } impl Git { pub fn new() -> Self { Self { git: which("git") } } pub fn get_repo_root>(&self, path: P) -> Option { match path.as_ref().canonicalize() { Ok(mut path) => { debug_assert!(path.exists()); if path.is_file() { debug!("{} is a file. Checking {}", path.display(), path.parent()?.display()); path = path.parent()?.to_path_buf(); } debug!("Checking if {} is a git repository", path.display()); #[cfg(windows)] let path = { let mut path_string = path.into_os_string().to_string_lossy().into_owned(); if path_string.starts_with(PATH_PREFIX) { path_string.replace_range(0..PATH_PREFIX.len(), ""); } debug!("Transformed path to {}", path_string); path_string }; if let Some(git) = &self.git { let output = Command::new(&git) .args(&["rev-parse", "--show-toplevel"]) .current_dir(path) .check_output() .ok() .map(|output| output.trim().to_string()); return output; } } Err(e) => match e.kind() { io::ErrorKind::NotFound => debug!("{} does not exists", path.as_ref().display()), _ => error!("Error looking for {}: {}", path.as_ref().display(), e), }, } None } pub fn multi_pull( &self, repositories: &Repositories, run_type: RunType, extra_arguments: &Option, ) -> Result<()> { if repositories.repositories.is_empty() { return Err(SkipStep.into()); } let git = self.git.as_ref().unwrap(); print_separator("Git repositories"); if let RunType::Dry = run_type { repositories .repositories .iter() .for_each(|repo| println!("Would pull {}", &repo)); return Ok(()); } let mut processes: Vec<_> = repositories .repositories .iter() .filter_map(|repo| { let repo = repo.clone(); let path = repo.to_string(); let before_revision = get_head_revision(git, &repo); println!("{} {}", style("Pulling").cyan().bold(), path); let mut command = Command::new(git); command.args(&["pull", "--ff-only"]).current_dir(&repo); if let Some(extra_arguments) = extra_arguments { command.args(extra_arguments.split_whitespace()); } command .stdout(Stdio::null()) .stderr(Stdio::piped()) .spawn() .map(|child| PullProcess { child, repo, before_revision, }) .ok() }) .collect(); let mut success = true; while !processes.is_empty() { let mut remaining_processes = Vec::::with_capacity(processes.len()); for mut p in processes { if let Some(status) = p.child.try_wait().unwrap() { if status.success() { let after_revision = get_head_revision(&git, &p.repo); match (&p.before_revision, &after_revision) { (Some(before), Some(after)) if before != after => { println!("{} {}:", style("Changed").yellow().bold(), &p.repo); Command::new(&git) .current_dir(&p.repo) .args(&[ "--no-pager", "log", "--no-decorate", "--oneline", &format!("{}..{}", before, after), ]) .spawn() .unwrap() .wait() .unwrap(); println!(); } _ => { println!("{} {}", style("Up-to-date").green().bold(), &p.repo); } } } else { success = false; println!("{} pulling {}", style("Failed").red().bold(), &p.repo); let mut stderr = String::new(); if p.child.stderr.unwrap().read_to_string(&mut stderr).is_ok() { print!("{}", stderr); } } } else { remaining_processes.push(p); } } processes = remaining_processes; sleep(Duration::from_millis(200)); } if !success { Err(TopgradeError::PullFailed.into()) } else { Ok(()) } } } impl<'a> Repositories<'a> { pub fn new(git: &'a Git) -> Self { let mut glob_match_options = MatchOptions::new(); if cfg!(windows) { glob_match_options.case_sensitive = false; } Self { git, repositories: HashSet::new(), glob_match_options, } } pub fn insert>(&mut self, path: P) { if let Some(repo) = self.git.get_repo_root(path) { self.repositories.insert(repo); } } pub fn glob_insert(&mut self, pattern: &str) { if let Ok(glob) = glob_with(pattern, self.glob_match_options) { for entry in glob { match entry { Ok(path) => self.insert(path), Err(e) => { error!("Error in path {}", e); } } } } else { error!("Bad glob pattern: {}", pattern); } } }