Compare commits

...

5 Commits

Author SHA1 Message Date
Thomas Forgione 1b15bd678c
Finished cleaning 2019-02-12 14:51:45 +01:00
Thomas Forgione 8cf6a2e556
Add docs 2019-02-12 11:17:17 +01:00
Thomas Forgione 8480ae88f0
Cleaning 2019-02-12 11:11:32 +01:00
Thomas Forgione 949490b350
Added docs for cache 2019-02-12 11:08:39 +01:00
Thomas Forgione 56e39a33f9
Cleaning 2019-02-12 11:04:04 +01:00
7 changed files with 384 additions and 312 deletions

1
Cargo.lock generated
View File

@ -11,6 +11,7 @@ name = "gclone"
version = "0.1.0"
dependencies = [
"colored 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"walkdir 2.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
]

View File

@ -7,6 +7,7 @@ edition = "2018"
[dependencies]
colored = "1.7.0"
walkdir = "2.2.7"
lazy_static = "1.2.0"
[[bin]]
name = "gclone"

136
src/cache.rs Normal file
View File

@ -0,0 +1,136 @@
//! This module contains the cache manager.
use std::num::Wrapping;
use std::path::PathBuf;
use std::fs::File;
use std::io::{stdin, stdout, Write, BufRead, BufReader};
use walkdir::WalkDir;
use colored::*;
use crate::{Error, Result};
use crate::git::GCLONE_PATH;
/// Flushes the stdout.
fn flush_stdout() {
stdout().flush().ok();
}
/// The cache of gclone.
///
/// When running the command `cdg`, if the computer just booted, finding all the directories can be
/// quite slow. The cache contains the list of entries in the git directory to avoid re-finding
/// them every time.
pub struct Cache(Vec<String>);
impl Cache {
/// Generates the cache by traversing the files in the git directory.
pub fn generate() -> Cache {
Cache(WalkDir::new(&*GCLONE_PATH)
.max_depth(3)
.into_iter()
.filter_map(|x| x.ok())
.map(|x| x.path().display().to_string())
.collect())
}
/// Reads the cache file.
pub fn read() -> Result<Cache> {
let mut path = PathBuf::from(&*GCLONE_PATH);
path.push(".cdgcache");
let file = File::open(&path)?;
let file = BufReader::new(file);
let mut values = vec![];
for line in file.lines() {
if let Ok(l) = line {
values.push(l);
}
}
Ok(Cache(values))
}
/// Reads the cache file, and if it failed, generate a new cache.
pub fn read_or_generate() -> Cache {
match Cache::read() {
Ok(c) => c,
Err(_) => Cache::generate(),
}
}
/// Writes the current content of the cache to the cache file.
pub fn write(&self) -> Result<()> {
let mut path = PathBuf::from(&*GCLONE_PATH);
path.push(".cdgcache");
let mut file = File::create(path)?;
for line in &self.0 {
writeln!(file, "{}", line)?;
}
Ok(())
}
/// Search a directory in the cache, and returns all matching directories.
pub fn find(&self, dirname: &str) -> Vec<String> {
let dirname = &format!("/{}", dirname);
let mut matches = vec![];
for line in &self.0 {
if line.ends_with(dirname) {
matches.push(line.clone());
}
}
matches
}
/// Searches a directory, if multiple where found, prompt user for answer.
pub fn find_interactive(&self, dirname: &str) -> Result<String> {
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::<usize>()?) - 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);
}
}

View File

@ -1,155 +1,49 @@
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::{git_dir, 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::<Vec<_>>();
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<()> {
let git_dir = git_dir()?;
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(git_dir);
let mut path = PathBuf::from(&*GCLONE_PATH);
path.push(&server);
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);
}
}

111
src/git.rs Normal file
View File

@ -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::<Vec<_>>();
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<P: AsRef<Path>>(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<P: AsRef<Path>>(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(()),
}
}

View File

@ -1,11 +1,26 @@
use std::{env, fmt, result};
use std::error::Error;
use std::path::PathBuf;
use std::fs::File;
use std::io::{self, Write, BufRead, BufReader};
//! This crate contains the gclone and pgd commands.
//!
//! The `gclone` command automatically clones a git repository and puts it in the right place.
//! The `pgd` command manages a cache for efficiently being able to print a directory corresponding
//! to a repo or an owner. You can then add the following to automatically `cd` to a directory:
//! ``` zsh
//! cdg() {
//! local newdir
//! newdir=$(pgd $1) && cd $newdir
//! }
//! ```
#![warn(missing_docs)]
use std::{env, fmt, num, error, result};
use std::io;
use colored::*;
use walkdir::WalkDir;
mod cache;
pub use cache::Cache;
pub mod git;
macro_rules! impl_from_error {
($type: ty, $variant: path, $from: ty) => {
@ -18,101 +33,98 @@ macro_rules! impl_from_error {
}
#[derive(Debug)]
pub enum MError {
/// The error type of gclone.
pub enum Error {
/// Couldn't read the GCLONE_PATH env.
NoGclonePath(env::VarError),
/// An error occured while manipulating files.
IoError(io::Error),
/// Couldn't parse git url.
GitUrlParseError,
/// Couldn't clone the respository.
GitCloneError(Option<io::Error>),
/// 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!(MError, MError::NoGclonePath, env::VarError);
impl_from_error!(MError, MError::IoError, io::Error);
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 MError {
impl fmt::Display for Error {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
MError::NoGclonePath(e) =>
Error::NoGclonePath(e) =>
write!(fmt, "{} {} {}",
"error:".bold().red(),
"couldn't read environment variable GCLONE_PATH".bold(),
e),
MError::IoError(e) =>
Error::IoError(e) =>
write!(fmt, "{} {} {}",
"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()),
}
}
}
impl Error for MError {}
impl error::Error for Error {}
pub type Result<T> = result::Result<T, MError>;
/// The result type of gclone.
pub type Result<T> = result::Result<T, Error>;
pub fn git_dir() -> Result<String> {
Ok(env::var("GCLONE_PATH")?)
/// Gets the single argument of the program.
pub fn first_arg() -> Result<String> {
env::args().nth(1).ok_or(Error::WrongArgument)
}
pub struct Cache(Vec<String>);
impl Cache {
pub fn new() -> Result<Cache> {
Ok(Cache(WalkDir::new(git_dir()?)
.max_depth(3)
.into_iter()
.filter_map(|x| x.ok())
.map(|x| x.path().display().to_string())
.collect()))
}
pub fn read() -> Result<Cache> {
let mut path = PathBuf::from(git_dir()?);
path.push(".cdgcache");
let file = match File::open(&path) {
Ok(f) => f,
Err(_) => return Ok(Cache(vec![])),
};
let file = BufReader::new(file);
let mut values = vec![];
for line in file.lines() {
if let Ok(l) = line {
values.push(l);
}
}
Ok(Cache(values))
}
pub fn write(&self) -> Result<()> {
let mut path = PathBuf::from(git_dir()?);
path.push(".cdgcache");
let mut file = File::create(path)?;
for line in &self.0 {
writeln!(file, "{}", line)?;
}
Ok(())
}
pub fn find(&self, dirname: &str) -> Vec<String> {
let dirname = &format!("/{}", dirname);
let mut matches = vec![];
for line in &self.0 {
if line.ends_with(dirname) {
matches.push(line.clone());
}
}
matches
}
pub fn append(&mut self, value: String) {
self.0.push(value);
}
}

View File

@ -1,127 +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 = Cache::read()?;
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::new()?;
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::<usize>() {
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(())
}