diff --git a/src/cache.rs b/src/cache.rs index 281c6e3..0a71011 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,10 +1,18 @@ //! This module contains the cache manager. +use std::num::Wrapping; use std::path::PathBuf; use std::fs::File; -use std::io::{Write, BufRead, BufReader}; +use std::io::{stdin, stdout, Write, BufRead, BufReader}; use walkdir::WalkDir; -use crate::{Result, GCLONE_PATH}; +use colored::*; +use crate::{Error, Result}; +use crate::git::GCLONE_PATH; + +/// Flushes the stdout. +fn flush_stdout() { + stdout().flush().ok(); +} /// The cache of gclone. /// @@ -79,6 +87,47 @@ impl Cache { matches } + /// Searches a directory, if multiple where found, prompt user for answer. + pub fn find_interactive(&self, dirname: &str) -> Result { + + let matches = self.find(dirname); + match matches.len() { + + 0 => Err(Error::NoSuchDirectory), + 1 => Ok(matches[0].clone()), + + len => { + + eprintln!("{}", "info: multiple entries found, please select one".bold()); + + for (i, j) in matches.iter().enumerate() { + eprintln!("{} {}", + &format!("[{}]", i + 1).bold().blue(), + &format!("{}", j).yellow()); + } + + eprint!("{}", "Enter your choice: ".bold()); + flush_stdout(); + + let mut string = String::new(); + stdin().read_line(&mut string)?; + + // Pop the trailing \n of the string + string.pop(); + + // Use wrapping to move 0 to std::usize::MAX which is ok. + let value = (Wrapping(string.parse::()?) - Wrapping(1)).0; + + if value >= len { + return Err(Error::OutsideRange); + } + + Ok(matches[value].clone()) + }, + } + + } + /// Appends a value to the cache. pub fn append(&mut self, value: String) { self.0.push(value); diff --git a/src/gclone.rs b/src/gclone.rs index 209ec64..c598686 100644 --- a/src/gclone.rs +++ b/src/gclone.rs @@ -1,88 +1,25 @@ -use std::env; -use std::process::{Command, exit}; -use std::fs::{create_dir_all, remove_dir_all}; -use std::error::Error; +use std::process::exit; use std::path::PathBuf; use colored::*; -use gclone::{GCLONE_PATH, Cache, Result}; - -macro_rules! unwrap { - ($e: expr) => { - match $e { - Some(e) => e, - None => return None, - } - } -} - -fn parse_http_url(input: &str) -> Option<(String, String, String)> { - let split = input.split("/").collect::>(); - if split.len() < 3 { - return None; - } - - let repo = String::from(split[split.len() - 1]); - let owner = String::from(split[split.len() - 2]); - let server = split[2 .. split.len() - 2].join("/"); - - Some((server, owner, repo)) -} - -fn parse_ssh_url(input: &str) -> Option<(String, String, String)> { - let url = unwrap!(input.split("@").nth(1)); - let mut split = url.split(":"); - let server = String::from(unwrap!(split.next())); - let rest = unwrap!(split.next()); - - let mut resplit = rest.split("/"); - let owner = String::from(unwrap!(resplit.next())); - let repo = String::from(unwrap!(resplit.next())); - - Some((server, owner, repo)) -} - -fn parse_url(input: &str) -> Option<(String, String, String)> { - if input.starts_with("http") { - parse_http_url(input) - } else { - parse_ssh_url(input) - } -} +use gclone::{first_arg, Cache, Result}; +use gclone::git::{GCLONE_PATH, parse_url, clone}; fn help() { } -fn main() -> Result<()> { +fn main_result() -> Result<()> { // Parse args - let url = match env::args().nth(1) { - Some(arg) => arg, - None => { - eprintln!("{} {}", - "error:".bold().red(), - "gclone expects an argument".bold()); - - exit(1); - }, - }; + let url = first_arg()?; if url == "-h" || url == "--help" { return Ok(help()); } - let (server, owner, repo) = match parse_url(&url) { - Some(parsed) => parsed, - None => { - eprintln!("{} {}", - "error:".bold().red(), - "couldn't guess server, owner and repo names from url".bold()); - - exit(1); - }, - }; + let (server, owner, repo) = parse_url(&url)?; // Build path let mut path = PathBuf::from(&*GCLONE_PATH); @@ -90,64 +27,23 @@ fn main() -> Result<()> { path.push(&owner); path.push(&repo); - if path.exists() { - eprintln!("{} {}", - "error:".red().bold(), - "the corresponding directory already exists".bold()); - - exit(1); - } - - match create_dir_all(&path) { - Ok(_) => (), - Err(e) => { - eprintln!("{} {} {}", - "error:".red().bold(), - "couldn't create the corresponding directory:".bold(), - e.description()); - - remove_dir_all(&path).ok(); - - exit(1); - }, - } - + // Clone repository eprintln!("{} {} {}{}", "info:".bold(), "cloning", url, "..."); - - let command = Command::new("git") - .args(&["clone", &url, &path.display().to_string()]) - .output(); - - match command { - Ok(repo) => repo, - Err(e) => { - eprintln!("{} {} {}", - "error:".bold().red(), - "couldn't clone repository:".bold(), - e.description()); - - remove_dir_all(&path).ok(); - - exit(1); - }, - }; - + clone(&url, &path.display().to_string())?; eprintln!("{} {}", "info:".bold(), "done!"); // Append to cache let mut cache = Cache::read()?; cache.append(path.display().to_string()); - - match cache.write() { - Ok(_) => (), - Err(e) => { - eprintln!("{} {} {}", - "warning:".bold().yellow(), - "couldn't write cache:".bold(), - e.description()); - }, - } + cache.write()?; Ok(()) } + +fn main() { + if let Err(e) = main_result() { + eprintln!("{}", e); + exit(1); + } +} diff --git a/src/git.rs b/src/git.rs new file mode 100644 index 0000000..15bd1d1 --- /dev/null +++ b/src/git.rs @@ -0,0 +1,111 @@ +//! This module contains the utility functions to manage git repository. + +use std::env; +use std::fs::{create_dir_all, remove_dir_all}; +use std::path::Path; +use std::process::{Command, Stdio, exit}; + +use lazy_static::lazy_static; + +use crate::{Error, Result}; + +lazy_static! { + /// The directory in which the repositories will be cloned and searched. + pub static ref GCLONE_PATH: String = { + match env::var("GCLONE_PATH") { + Ok(d) => d, + Err(e) => { + let e: Error = e.into(); + eprintln!("{}", e); + exit(1); + }, + } + }; +} + +macro_rules! unwrap { + ($e: expr) => { + match $e { + Some(e) => e, + None => return Err(Error::GitUrlParseError), + } + } +} + +/// Transforms a git url to a (server, owner, repository) tuple. +/// +/// Returns none if it failed. +pub fn parse_url(input: &str) -> Result<(String, String, String)> { + if input.starts_with("http") { + parse_http_url(input) + } else { + parse_ssh_url(input) + } +} + +/// Parses an HTTP url for a git repository. +fn parse_http_url(input: &str) -> Result<(String, String, String)> { + let split = input.split("/").collect::>(); + if split.len() < 3 { + return Err(Error::GitUrlParseError); + } + + let repo = String::from(split[split.len() - 1]); + let owner = String::from(split[split.len() - 2]); + let server = split[2 .. split.len() - 2].join("/"); + + Ok((server, owner, repo)) +} + +/// Parses an SSH url for a git repository. +fn parse_ssh_url(input: &str) -> Result<(String, String, String)> { + let url = unwrap!(input.split("@").nth(1)); + let mut split = url.split(":"); + let server = String::from(unwrap!(split.next())); + let rest = unwrap!(split.next()); + + let mut resplit = rest.split("/"); + let owner = String::from(unwrap!(resplit.next())); + let repo = String::from(unwrap!(resplit.next())); + + Ok((server, owner, repo)) +} + +/// Clones a git repository in the right place. +/// +/// If an error happens, it deletes the directory created. +pub fn clone>(url: &str, place: P) -> Result<()> { + + match clone_dirty(url, &place) { + Ok(o) => Ok(o), + Err(e) => { + remove_dir_all(&place).ok(); + Err(e) + }, + } + +} + +/// Clones a git repository in the right place. +fn clone_dirty>(url: &str, place: P) -> Result<()> { + + let place = place.as_ref(); + + if place.exists() { + return Err(Error::PathAlreadyExists); + } + + create_dir_all(place)?; + + let command = Command::new("git") + .args(&["clone", &url, &place.display().to_string()]) + .stderr(Stdio::null()) + .status(); + + match command { + Err(e) => Err(Error::GitCloneError(Some(e))), + Ok(o) if ! o.success() => Err(Error::GitCloneError(None)), + Ok(_) => Ok(()), + } + +} diff --git a/src/lib.rs b/src/lib.rs index daef5ee..7d4e514 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,29 +12,15 @@ #![warn(missing_docs)] -use std::{env, fmt, error, result}; -use std::process::exit; +use std::{env, fmt, num, error, result}; use std::io; use colored::*; -use lazy_static::lazy_static; mod cache; pub use cache::Cache; -lazy_static! { - /// The directory in which the repositories will be cloned and searched. - pub static ref GCLONE_PATH: String = { - match env::var("GCLONE_PATH") { - Ok(d) => d, - Err(e) => { - let e: Error = e.into(); - eprintln!("{}", e); - exit(1); - }, - } - }; -} +pub mod git; macro_rules! impl_from_error { ($type: ty, $variant: path, $from: ty) => { @@ -54,10 +40,32 @@ pub enum Error { /// An error occured while manipulating files. IoError(io::Error), + + /// Couldn't parse git url. + GitUrlParseError, + + /// Couldn't clone the respository. + GitCloneError(Option), + + /// The specified directory already exists. + PathAlreadyExists, + + /// The binaries of this crate require exactly one argument. + WrongArgument, + + /// An error while parsing an int. + ParseIntError(num::ParseIntError), + + /// A value is outside a range. + OutsideRange, + + /// No such directory was found. + NoSuchDirectory, } impl_from_error!(Error, Error::NoGclonePath, env::VarError); impl_from_error!(Error, Error::IoError, io::Error); +impl_from_error!(Error, Error::ParseIntError, num::ParseIntError); impl fmt::Display for Error { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { @@ -72,6 +80,41 @@ impl fmt::Display for Error { "error:".bold().red(), "couldn't read or write cache:".bold(), e), + Error::GitUrlParseError => + write!(fmt, "{} {}", + "error:".bold().red(), + "couldn't guess server, owner and repo from url".bold()), + Error::GitCloneError(Some(e)) => + write!(fmt, "{} {} {}", + "error:".bold().red(), + "couldn't clone git repository:".bold(), + e), + Error::GitCloneError(None) => + write!(fmt, "{} {}", + "error:".bold().red(), + "couldn't clone git repository".bold()), + Error::PathAlreadyExists => + write!(fmt, "{} {}", + "error:".bold().red(), + "the path corresponding to the repository already exists".bold()), + Error::WrongArgument => + write!(fmt, "{} {}", + "error:".bold().red(), + "this program expects exactly one argument".bold()), + Error::ParseIntError(e) => + write!(fmt, "{} {} {}", + "error:".bold().red(), + "couldn't parse integer:".bold(), + e), + Error::OutsideRange => + write!(fmt, "{} {}", + "error:".bold().red(), + "integer is outside the range".bold()), + Error::NoSuchDirectory => + write!(fmt, "{} {}", + "error:".bold().red(), + "no such directory was found".bold()), + } } } @@ -81,3 +124,7 @@ impl error::Error for Error {} /// The result type of gclone. pub type Result = result::Result; +/// Gets the single argument of the program. +pub fn first_arg() -> Result { + env::args().nth(1).ok_or(Error::WrongArgument) +} diff --git a/src/pgd.rs b/src/pgd.rs index 909c5fb..41f10fe 100644 --- a/src/pgd.rs +++ b/src/pgd.rs @@ -1,130 +1,44 @@ -use std::env; -use std::error::Error; use std::process::exit; -use std::io::{stdout, stdin, Write}; -use std::num::Wrapping; use colored::*; -use gclone::{Result, Cache}; +use gclone::{first_arg, Cache, Error, Result}; -fn flush_stdout() { - stdout().flush().ok(); +fn main() { + if let Err(e) = main_result() { + eprintln!("{}", e); + exit(1); + } } -fn main() -> Result<()> { - let request = match env::args().nth(1) { - Some(arg) => arg, - None => { - eprintln!("{} {}", - "error:".bold().red(), - "cdg expects an argument".bold()); - - exit(1); - }, - }; - +fn main_result() -> Result<()> { + let request = first_arg()?; main_with_cache(&request, true) } fn main_with_cache(request: &str, regen: bool) -> Result<()> { - let (cache, regen) = match Cache::read() { - Ok(c) => (c, regen), - Err(_) => (Cache::generate(), false), + let (cache, just_generated) = match Cache::read() { + Ok(c) => (c, false), + Err(_) => (Cache::generate(), true), }; - let matches = cache.find(&request); + let only_match = match cache.find_interactive(&request) { - match matches.len() { - 0 => { - - if regen { - eprintln!("{} {}", - "warning:".bold().yellow(), - "directory not found, regenerating cache...".bold()); - - let cache = Cache::generate(); - - match cache.write() { - Ok(_) => (), - Err(e) => { - eprintln!("{} {} {}", - "warning:".bold().yellow(), - "couldn't write cache:".bold(), - e.description()); - }, - } - - return main_with_cache(request, false); - - } else { - eprintln!("{} {}", - "error:".bold().red(), - "no such directory".bold()); - - exit(1); - } + Err(Error::NoSuchDirectory) if regen && ! just_generated => { + eprintln!("{} {}", + "warning:".bold().yellow(), + "directory not found, regenerating cache...".bold()); + let cache = Cache::generate(); + cache.write()?; + return main_with_cache(request, false); }, - 1 => { - println!("{}", matches[0]); - }, + Err(e) => return Err(e), + Ok(x) => x, + }; - len => { - - eprintln!("{}", "info: multiple entries found, please select one".bold()); - - for (i, j) in matches.iter().enumerate() { - eprintln!("{} {}", - &format!("[{}]", i + 1).bold().blue(), - &format!("{}", j).yellow()); - } - - eprint!("{}", "Enter your choice: ".bold()); - flush_stdout(); - - let mut string = String::new(); - match stdin().read_line(&mut string) { - Ok(_) => (), - Err(e) => { - eprintln!("{} {} {}", - "error:".bold().red(), - "couldn't read line:".bold(), - e.description()); - - exit(1); - }, - }; - - // Pop the trailing \n of the string - string.pop(); - - // Use wrapping to move 0 to std::usize::MAX which is ok. - let value = (Wrapping(match string.parse::() { - Ok(v) => v, - Err(e) => { - eprintln!("{} {} {}", - "error:".bold().red(), - "couldn't parse integer:".bold(), - e.description()); - - exit(1); - } - }) - Wrapping(1)).0; - - if value >= len { - eprintln!("{} {}", - "error:".bold().red(), - "integer wasn't in the right range".bold()); - - exit(1); - } - - println!("{}", matches[value]); - }, - } + println!("{}", only_match); Ok(()) - }