use std::env::{set_current_dir as cd, current_dir as pwd}; use std::path::{Path, PathBuf}; use std::process::Command; #[derive(Debug, Clone)] pub enum Backend { Git, Mercurial, } impl Backend { /// Generates Some(Backend) from an optional String, or defaults to Git, /// or None when the backend is not recognized /// TODO: fallback backend should be customizable pub fn from_setting(setting: Option) -> Option { // Git is the default setting until further notice setting.map_or(Some(Backend::Git), |name| match name.as_ref() { "git" => Some(Backend::Git), "mercurial" => Some(Backend::Mercurial), _ => None, }) } } #[derive(Debug)] pub struct Repo { pub backend: Backend, pub source: String, pub dest: PathBuf, pub subupdates: bool, } impl Repo { pub fn new(backend: Backend, source: &str, dest: &Path, subupdates: bool) -> Repo { Repo { backend, source: source.to_string(), dest: dest.to_path_buf(), subupdates, } } pub fn branch(&self) -> String { match &self.backend { Backend::Git => { let output = Command::new("git") .arg("rev-parse") .arg("--abbrev-ref") .arg("HEAD") .output() .expect("WTF"); if !output.status.success() { panic!("Corrupted git repository???"); } String::from_utf8(output.stdout).unwrap().trim().to_string() } Backend::Mercurial => { unreachable!("UNIMPLEMENTED!"); } } } pub fn clone(&self) -> bool { let success = match &self.backend { Backend::Git => { let output = Command::new("git") .arg("clone") .arg("--recursive") .arg(&self.source) .arg(&self.dest) .output() // To suppress (capture) output .expect("PROCESS ERROR!"); output.status.success() } Backend::Mercurial => { unreachable!("Unimplemented"); } }; // If the clone was successful and subupdates is enabled, // List submodules and check them for updates if success && self.subupdates { self.subupdate(); } return success } /// Checks for updates from submodules. If any update was found, download them and return true. pub fn subupdate(&self) -> bool { let mut found_subupdates = false; for subpath in self.submodules() { // Move into the submodule cd(self.dest.join(subpath)).unwrap(); // Generate a Repo instance for the submodule but don't enable subupdates // Just in case someone would trigger an infinite loop by accident let subrepo = Repo::new(self.backend.clone(), "irrelevant", &pwd().expect("Failed to read pwd"), false); if subrepo.update() { found_subupdates = true; } // Move back into main repo cd(&self.dest).unwrap(); } found_subupdates } pub fn checkout(&self, target: &str) -> bool { match &self.backend { Backend::Git => { let status = Command::new("git") .arg("checkout") .arg(target) .status() .expect("PROCESS ERROR!"); status.success() } Backend::Mercurial => { unreachable!("UNIMPLEMENTED"); } } } /// Parse the list of submodules in the current repository /// When there is no submodules (eg. no .gitmodules file), return an empty Vec pub fn submodules(&self) -> Vec { match &self.backend { Backend::Git => { let cmd = Command::new("git") .arg("config") .arg("--file") .arg(&format!("{}/.gitmodules", self.dest.to_str().unwrap())) .arg("--get-regexp") .arg("path") .output() .expect("WTF"); if cmd.status.success() { // Command succeded, split by the first space character to find the path to the submodule let out = std::string::String::from_utf8(cmd.stdout).expect("Wrong unicode"); let mut results = Vec::new(); for line in out.lines() { results.push(PathBuf::from(line.split_once(" ").expect("Misformed .gitmodules").1)); } results } else { // No submodules found (even if .gitmodules exist but is empty) Vec::new() } }, Backend::Mercurial => { unimplemented!("Soon"); } } } /// Check for updates on the main repository pub fn has_updates(&self) -> bool { match &self.backend { Backend::Git => { // Refresh remote if !Command::new("git") .arg("fetch") .arg("--quiet") .arg("origin") .status() .expect("WTF") .success() { // FAILED, no internet?? // TODO: This should be a forgebuild error message eprintln!("Fetching updates failed"); return false; } let branch = self.branch(); if Command::new("git") .arg("diff") .arg("--quiet") .arg(&format!("remotes/origin/{}", &branch)) .status() .expect("WTF") .success() { // Command succeeeded, no updates return false; } // Updates return true; }, Backend::Mercurial => { unimplemented!(); }, } } /// Checks if the repository has updates, if so download them /// Returns true if updates have been applied, false otherwise /// Also applies submodules updates if self.subupdates is true pub fn update(&self) -> bool { match &self.backend { Backend::Git => { // First try to run submodules updates let had_subupdates = if self.subupdates { self.subupdate() } else { false }; // Now run main repo updates if self.has_updates() { if Command::new("git") .arg("pull") .arg("--ff-only") .arg("origin") .arg(self.branch()) .status() .expect("WTF") .success() { // Main updates succeeded. If new submodules were added, initialize them if !Command::new("git") .arg("submodule") .arg("update") .arg("--init") .arg("--recursive") .status() .expect("WTF") .success() { // TODO: Should be forgebuild error message eprintln!("Failed to initialize submodules which were added to the repo"); } return true; } else { // Main updates failed, return true if there were some submodule updates return had_subupdates; } } // No main updates return had_subupdates; } Backend::Mercurial => { unreachable!("unimplemented"); } } } }