use anyhow::{anyhow, bail, Result};
use clap::{AppSettings, Clap};
use std::env;
use std::io::{self, Read};
use url::Url;
use crate::config::{save_config, Config};
mod config;
mod github;
mod gitlab;
mod repo;
#[derive(Clap, Debug)]
#[clap(author, about, version)]
#[clap(global_setting = AppSettings::ColoredHelp)]
#[clap(setting = AppSettings::DeriveDisplayOrder)]
#[clap(setting = AppSettings::SubcommandRequiredElseHelp)]
struct Opts {
#[clap(subcommand)]
command: Command,
}
const SCRIPT_HELP: &'static str = r"Script identifier for a script from a repository
For saved repos: `<repo>[@ref]:<script_path>`
Example: `myscripts:hello.bash`
Example (w/ ref): `myscripts@v1.0:hello.bash`
For git repos: `git@<repo_url>[@ref]:<script_path>`
Example: `git@github.com:user/myscripts:hello.bash`
Example (w/ ref): `git@github.com:user/myscripts@main:hello.bash`
";
#[derive(Clap, Debug)]
enum Command {
Repo(Repo),
Run {
#[clap(about = "Script to run")]
#[clap(long_about = SCRIPT_HELP)]
script: String,
#[clap(about = "Args to be passed to the script")]
args: Vec<String>,
},
Import {
#[clap(about = "Script to import")]
#[clap(long_about = SCRIPT_HELP)]
script: String,
},
}
#[derive(Clap, Debug)]
struct Script {
#[clap(long_about = SCRIPT_HELP)]
script: String,
}
#[derive(Clap, Debug)]
struct Repo {
#[clap(subcommand)]
command: RepoCommand,
}
#[derive(Clap, Debug)]
enum RepoCommand {
#[clap(alias = "ls")]
List,
Add {
name: String,
uri: String,
#[clap(long, short)]
username: Option<String>,
#[clap(long, short)]
password: Option<String>,
#[clap(long)]
password_env: Option<String>,
#[clap(long)]
password_stdin: bool,
},
Check {
name: String,
},
#[clap(alias = "rm")]
Remove {
name: String,
},
}
#[derive(PartialEq)]
pub enum Password {
Saved(String),
FromEnv(String, String),
None,
}
#[tokio::main]
async fn main() -> Result<()> {
let mut config = config::load_config().await?;
match Opts::parse().command {
Command::Repo(repo) => match repo.command {
RepoCommand::List => {
println!("Saved repositories:");
for (k, v) in config.repo {
println!(" {} ({}:{})", k, v.provider, v.uri);
}
}
RepoCommand::Add {
name,
uri,
username,
password,
password_env,
password_stdin,
} => {
if config.repo.contains_key(&name) {
bail!("A repository with the name `{}` already exists", &name);
}
let password_for_parse = match (password, password_env, password_stdin) {
(Some(pass), _, _) => Password::Saved(pass),
(_, Some(var), _) => Password::FromEnv(var.clone(), env::var(var)?),
(_, _, true) => {
let mut buf = String::new();
io::stdin().read_to_string(&mut buf)?;
Password::Saved(buf)
}
_ => Password::None,
};
let repo = get_repo(&uri, username, password_for_parse).await?;
config.repo.insert(name.clone(), repo);
println!("Repo `{}` was successfully added", &name);
save_config(&config).await?;
}
RepoCommand::Check { .. } => unimplemented!(),
RepoCommand::Remove { name } => {
if !config.repo.contains_key(&name) {
bail!("Repo `{}` was not found", &name);
}
config.repo.remove(&name);
save_config(&config).await?;
println!("Repo `{}` was removed", &name);
}
},
Command::Run { script, args } => {
let src = parse_script_source(&config, &script, ScriptAction::Run)?;
let contents = get_script_contents(&config, &src).await?;
let args = args.iter().map(|s| &**s).collect();
repo::run_script(&contents, args)?;
}
Command::Import { script } => {
let src = parse_script_source(&config, &script, ScriptAction::Import)?;
let contents = get_script_contents(&config, &src).await?;
repo::import_script(&contents)?;
}
};
Ok(())
}
enum ScriptSource {
Repo(String, String, String),
Git(String, String, String),
}
enum ScriptAction {
Run,
Import,
}
fn parse_script_source(
config: &Config,
script: &str,
action: ScriptAction,
) -> Result<ScriptSource> {
if script.starts_with("git@") {
let (repo, name, rref) = parse_git_source(script)?;
validate_script_name(config, &name, action)?;
Ok(ScriptSource::Git(repo, name, rref))
} else {
let (repo, name, rref) = parse_repo_source(script)?;
validate_script_name(config, &name, action)?;
Ok(ScriptSource::Repo(repo, name, rref))
}
}
fn parse_git_source(_script: &str) -> Result<(String, String, String)> {
unimplemented!()
}
fn parse_repo_source(script: &str) -> Result<(String, String, String)> {
let parts = script.split(":").collect::<Vec<&str>>();
if parts.len() != 2 {
bail!("Script must be in the format `<repo>[@ref]:<script_path>`");
}
let repo_name = parts[0].to_string();
let script_name = parts[1].to_string();
let repo_parts = repo_name.split('@').collect::<Vec<&str>>();
let (repo_name, repo_ref) = match repo_parts.len() {
1 => (repo_name, "HEAD".to_string()),
2 => (repo_parts[0].to_string(), repo_parts[1].to_string()),
_ => bail!("Invalid repo: `{}`", repo_name),
};
Ok((repo_name, script_name, repo_ref))
}
fn validate_script_name(config: &Config, name: &str, action: ScriptAction) -> Result<()> {
match (
&config.require_bash_extension,
&config.require_lib_extension,
action,
) {
(Some(ref ext), _, ScriptAction::Run) => {
if !name.ends_with(ext) {
bail!("Expected executable bash script to end with `{}`", ext);
}
Ok(())
}
(_, Some(ext), ScriptAction::Import) => {
if !name.ends_with(ext) {
bail!("Expected bash library to end with `{}`", ext);
}
Ok(())
}
_ => Ok(()),
}
}
async fn get_script_contents(config: &config::Config, src: &ScriptSource) -> Result<String> {
match src {
ScriptSource::Repo(repo, name, rref) => {
let generic_repo = config
.repo
.get(repo)
.ok_or(anyhow!("Repo `{}` was not found", &repo))?
.clone();
Ok(generic_repo.get_contents(&name, &rref).await?)
}
_ => unimplemented!(),
}
}
async fn get_repo(
uri: &str,
username: Option<String>,
password: Password,
) -> Result<repo::GenericRepo> {
let mut maybe_parsed: Option<Url> = None;
if uri.starts_with("gitlab.com") || uri.starts_with("github.com") {
let with_scheme = format!("https://{}", uri);
maybe_parsed = Some(Url::parse(&with_scheme)?);
}
let mut parsed = match maybe_parsed {
Some(parsed) => parsed,
None => Url::parse(uri)?,
};
if parsed.cannot_be_a_base() {
bail!("Repo URI was not recognized");
}
let _ = parsed.set_scheme("https");
match parsed.host_str() {
Some("gitlab.com") => Ok(gitlab::fetch_project(&parsed, password).await?),
Some("github.com") => Ok(github::fetch_project(&parsed, username, password).await?),
Some(_) => bail!("No provider recognized for passed URI"),
None => bail!("No host on passed URI"),
}
}