use anyhow::{bail, Context, Result}; use clap::{Parser, Subcommand}; use colored::Colorize; use fs_extra::{dir, move_items}; use libset::routes::home; use regex::bytes::Regex; use requestty::Answer; use std::fs; use std::path::PathBuf; use crate::constants::messages::*; use crate::utils::fork::ForkAction; use crate::utils::host::Host; use crate::utils::input::{ clone_setup, config_all, config_editor, config_host, config_owner, fork_setup, select_repo, }; use crate::utils::project::{create_paths_reader, find_paths, OpenAction}; use crate::utils::settings::Settings; use crate::{constants::patterns::GIT_URL, utils::clone::CloneAction}; #[derive(Parser, Debug)] #[clap(name = "(Dev)mode", version = "0.3.0")] #[clap(author = "Eduardo F. ")] #[clap(about = "Dev(mode) is a project management utility for developers.")] #[clap(arg_required_else_help = true)] pub struct Cli { #[clap(subcommand)] pub commands: Commands, } #[derive(Subcommand, Debug)] pub enum Commands { #[clap( about = "Clones a repository in a specific folder structure.", alias = "cl" )] Clone { #[clap(help = "Provide either a Git or a Git .")] #[clap(min_values = 1)] #[clap(max_values = 3)] args: Vec, #[clap( help = "Select a workspace to store the repo in.", short = 'w', long = "workspace", takes_value = true )] workspace: Option, }, #[clap( about = "Opens a project on your selected text editor.", alias = "o", arg_required_else_help = true )] Open { #[clap(help = "Provide a project name")] #[clap(takes_value = true, required = true)] project: String, }, #[clap( about = "Clones a repo and sets the upstream to your fork.", alias = "fk" )] Fork { #[clap( help = "Provide either a Git or a Git .", min_values = 1 )] args: Vec, #[clap( help = "Set the upstream to your fork ", short = 'u', long = "upstream" )] #[clap(takes_value = true, required = true)] upstream: String, }, #[clap( about = "Write changes to your configuration.", alias = "cf", arg_required_else_help = true )] Config { #[clap(help = "Map your project paths.", short = 'm', long = "map")] map: bool, #[clap(help = "Show the current configuration.", short = 's', long = "show")] show: bool, #[clap(help = "Configure your settings.", short = 'a', long = "all")] all: bool, #[clap(help = "Set preferred code editor.", short = 'e', long = "editor")] editor: bool, #[clap(help = "Set preferred git username.", short = 'o', long = "owner")] owner: bool, #[clap(help = "Set preferred Git host to clone projects from.", long = "host")] host: bool, }, #[clap( about = "Create workspaces to store your projects.", alias = "ws", arg_required_else_help = true )] Workspace { #[clap(help = "Name for the workspace.")] name: Option, #[clap(help = "Delete a workspace", short = 'd', long = "delete")] delete: bool, #[clap( help = "Rename a workspace", short = 'r', long = "rename", takes_value = true )] rename: Option, #[clap( help = "Add a repo to a workspace", short = 'a', long = "add", takes_value = true )] add: Option, #[clap( help = "Remove a repo from a workspace", short = 'm', long = "remove", takes_value = true )] remove: Option, #[clap(help = "List all workspaces", short = 'l', long = "list")] list: bool, }, } impl Cli { pub fn run(&self) -> Result<()> { let rx = Regex::new(GIT_URL).with_context(|| "Unable to parse Regex.")?; match &self.commands { Commands::Clone { args, workspace } => Cli::clone(args, workspace.to_owned()), Commands::Open { project } => Cli::open(project), Commands::Fork { args, upstream } => Cli::fork(args, upstream, rx), Commands::Config { map, show, all, editor, owner, host, } => Cli::config( *map, *show, *all, *editor, *owner, *host, !map && !show && !*all && !*editor && !*owner && !*host, ), Commands::Workspace { name, delete, rename, add, remove, list, } => Cli::workspace( name.to_owned(), *delete, rename.clone(), add.clone(), remove.clone(), *list, ), } } fn clone(args: &[String], workspace: Option) -> Result<()> { let clone = CloneAction::new().set_workspace(workspace); let mut clone = if args.is_empty() { clone_setup()? } else if args.len() == 1 && args.get(0).unwrap().contains("http") { clone.set_url(args.get(0))? } else if args.len() == 3 { clone .set_host(Some(Host::from(args.get(0).unwrap()))) .set_owner(args.get(1)) .set_repos(Some(vec![args.get(2).unwrap().to_owned()])) } else { let options = Settings::current().with_context(|| APP_OPTIONS_NOT_FOUND)?; clone .set_host(Some(Host::from(&options.host))) .set_owner(Some(&options.owner)) .set_repos(Some(args.to_vec())) }; clone.run() } fn open(project: &str) -> Result<()> { OpenAction::new(project).open() } fn fork(args: &[String], upstream: &str, rx: Regex) -> Result<()> { let action = if args.is_empty() { fork_setup()? } else if rx.is_match(args.get(0).unwrap().as_bytes()) { ForkAction::parse_url(args.get(0).unwrap(), rx, upstream.to_string())? } else if args.len() == 1 { let options = Settings::current().with_context(|| APP_OPTIONS_NOT_FOUND)?; let host = Host::from(&options.host); let repo = args.get(0).map(|a| a.to_string()); ForkAction::from( host, upstream.to_string(), options.owner, repo.with_context(|| "Failed to get repo name.")?, ) } else { let host = Host::from(args.get(0).unwrap()); let owner = args.get(1).map(|a| a.to_string()); let repo = args.get(2).map(|a| a.to_string()); ForkAction::from( host, upstream.to_string(), owner.with_context(|| "Failed to get owner name.")?, repo.with_context(|| "Failed to get repo name")?, ) }; action.run() } fn config( map: bool, show: bool, all: bool, editor: bool, owner: bool, host: bool, none: bool, ) -> Result<()> { if all || none { if get_settings().is_err() { println!("First time setup! 🥳\n"); Settings::init()?; } let settings = config_all()?; settings.write(false)?; } if map { OpenAction::make_dev_paths()? } if editor { let settings = config_editor().with_context(|| "Failed to set editor.")?; settings.write(false)? } if owner { let settings = config_owner().with_context(|| "Failed to set owner.")?; settings.write(false)? } if host { let settings = config_host().with_context(|| "Failed to set host.")?; settings.write(false)? } if show { let settings = get_settings()?; settings.show(); } Ok(()) } fn workspace( name: Option, delete: bool, rename: Option, add: Option, remove: Option, list: bool, ) -> Result<()> { let mut settings = Settings::current().with_context(|| "Failed to get configuration")?; if let Some(name) = name { if settings.workspaces.names.contains(&name) { let index = settings .workspaces .names .iter() .position(|ws| *ws == name) .unwrap(); if delete { let dev = home().join("Developer"); for provider in fs::read_dir(dev)? { for user in fs::read_dir(provider?.path())? { let user = user?; for repo_or_workspace in fs::read_dir(&user.path())? { let repo_or_workspace = repo_or_workspace?; let repo_name = repo_or_workspace.file_name().to_str().unwrap().to_string(); if settings.workspaces.names.contains(&repo_name) && repo_name.eq(&name) { for repo in fs::read_dir(repo_or_workspace.path())? { let repo = repo?; fs_extra::dir::move_dir( repo.path(), &user.path(), &Default::default(), )?; } fs::remove_dir_all(repo_or_workspace.path())?; } } } } settings.workspaces.names.remove(index); settings.write(true)?; println!( "Workspace {} was successfully deleted.", Colorize::yellow(&*name) ) } else if rename.is_some() { let dev = home().join("Developer"); for provider in fs::read_dir(dev)? { for user in fs::read_dir(provider?.path())? { let user = user?; for repo_or_workspace in fs::read_dir(&user.path())? { let repo_or_workspace = repo_or_workspace?; let name = repo_or_workspace.file_name().to_str().unwrap().to_string(); if settings.workspaces.names.contains(&name) { fs::rename( repo_or_workspace.path(), repo_or_workspace .path() .parent() .unwrap() .join(rename.clone().unwrap()), )?; } } } } *settings.workspaces.names.get_mut(index).unwrap() = rename.clone().unwrap(); settings.write(true)?; println!( "Workspace renamed from {} to {}.", Colorize::yellow(&*name), Colorize::blue(&*rename.unwrap()) ); } else if add.is_some() { let add = add.unwrap(); let reader = create_paths_reader()?; let paths: Vec = find_paths(reader, &add)? .iter() .map(|path| path.to_owned()) .filter(|path| !path.contains(name.as_str())) .collect(); let path = if paths.is_empty() { bail!("Could not locate the {add} repository.") } else if paths.len() > 1 { eprintln!("{}", MORE_PROJECTS_FOUND); let paths: Vec<&str> = paths.iter().map(|s| s as &str).collect(); let path = select_repo(paths).with_context(|| "Failed to set repository.")?; path } else { paths[0].clone() }; let mut options = dir::CopyOptions::new(); let to = PathBuf::from(&path).parent().unwrap().join(&name); if to.join(add.as_str()).exists() { let question = requestty::Question::confirm("overwrite") .message("We found an existing repository with the same name, do you want to overwrite the existing repository?") .build(); let answer = requestty::prompt_one(question)?; if let Answer::Bool(overwrite) = answer { if overwrite { options.overwrite = true; move_items(&vec![path.clone()], to, &options)?; } } } else { move_items(&vec![path.clone()], to, &options)?; } } else if remove.is_some() { let remove = remove.unwrap(); let reader = create_paths_reader()?; let paths: Vec = find_paths(reader, &remove)? .iter() .map(|path| path.to_owned()) .filter(|path| path.contains(name.as_str())) .collect(); let path = if paths.is_empty() { bail!("Could not locate the {remove} repository inside {name}") } else if paths.len() > 1 { eprintln!("{}", MORE_PROJECTS_FOUND); let paths: Vec<&str> = paths.iter().map(|s| s as &str).collect(); let path = select_repo(paths).with_context(|| "Failed to set repository.")?; path } else { paths[0].clone() }; let mut options = dir::CopyOptions::new(); let path = PathBuf::from(&path); let to = path.parent().unwrap().parent().unwrap(); if to.join(remove.as_str()).exists() { let question = requestty::Question::confirm("overwrite") .message("We found an existing repository with the same name, do you want to overwrite the existing repository?") .build(); let answer = requestty::prompt_one(question)?; if let Answer::Bool(overwrite) = answer { if overwrite { options.overwrite = true; move_items(&vec![path.clone()], to, &options)?; } } } else { move_items(&vec![path.clone()], to, &options)?; } } else { println!("Workspace `{}` found.", Colorize::green(&*name)); } } else if delete || rename.is_some() { bail!( "Couldn't find a workspace that matches {}.", Colorize::yellow(&*name) ) } else { settings.workspaces.names.push(name.clone()); settings.write(true)?; println!("Workspace {} was added.", Colorize::yellow(&*name)) } } else if list { let workspaces = settings.workspaces.names; println!( "Currently available workspaces: {}", Colorize::yellow(&*format!("{:?}", workspaces)) ); } Ok(()) } } fn get_settings() -> Result { Settings::current().with_context(|| APP_OPTIONS_NOT_FOUND) }