#[macro_use]
extern crate prettytable;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::{env, process};
use clap::{crate_authors, crate_version, App, AppSettings, Arg, SubCommand};
use prettytable::{color, Attr, Cell, Row, Table};
use codebase::codebase::{Codebase, Project};
use codebase::project::ProjectOpts;
use codebase::{archetype, hook};
fn main() {
let matches = App::new("codebase")
.version(crate_version!())
.author(crate_authors!())
.about("Manage your codebase organization & configuration across computers.")
.setting(AppSettings::ArgRequiredElseHelp)
.arg(
Arg::with_name("list-archetypes")
.help("Prints available archetypes")
.long("list-archetypes"),
)
.arg(
Arg::with_name("list-hooks")
.help("Prints available hooks")
.long("list-hooks"),
)
.subcommand(
SubCommand::with_name("init")
.about("Initialize a new codebase in given directory")
.arg(
Arg::with_name("directory")
.help("Directory where the codebase should be initialized")
.required(false)
.value_name("DIRECTORY"),
)
.arg(
Arg::with_name("remote")
.help("The codebase remote")
.required(false)
.takes_value(true)
.long("remote"),
)
.arg(
Arg::with_name("import")
.long("import")
.help("Import existing Git repositories into codebase"),
),
)
.subcommand(
SubCommand::with_name("clone")
.about("Clone a remote codebase into given directory")
.arg(
Arg::with_name("remote")
.help("The codebase remote")
.required(true)
.value_name("REMOTE"),
)
.arg(
Arg::with_name("directory")
.help("Directory where the codebase should be cloned")
.required(false)
.value_name("DIRECTORY"),
),
)
.subcommand(
SubCommand::with_name("add")
.about("Add a remote repository as a codebase project")
.arg(
Arg::with_name("remote")
.help("The repository remote")
.required(true)
.value_name("REMOTE"),
)
.arg(
Arg::with_name("directory")
.help("Directory where the repository should be cloned")
.required(false)
.value_name("DIRECTORY"),
)
.arg(
Arg::with_name("branch")
.long("branch")
.short("b")
.help("Repository branch to be checkout")
.required(false)
.default_value("master")
.value_name("BRANCH"),
)
.arg(
Arg::with_name("git-config")
.long("git-config")
.help("Git configuration to be applied")
.required(false)
.value_name("KEY=VALUE")
.multiple(true),
)
.arg(
Arg::with_name("git-hook")
.long("git-hook")
.help("Git hook to apply")
.required(false)
.value_name("HOOK-NAME"),
),
)
.subcommand(
SubCommand::with_name("rm")
.about("Remove a project from the codebase")
.arg(
Arg::with_name("directory")
.help("Project to remove directory's")
.required(true),
)
.arg(
Arg::with_name("remove")
.long("remove")
.help("Also remove from disk"),
),
)
.subcommand(SubCommand::with_name("push").about("Push codebase configuration to remote"))
.subcommand(
SubCommand::with_name("pull")
.about("Pull codebase configuration from remote")
.arg(
Arg::with_name("remove")
.long("remove")
.help("Also remove from disk"),
),
)
.subcommand(
SubCommand::with_name("ls")
.about("Display the codebase projects along with their details"),
)
.subcommand(
SubCommand::with_name("update")
.about("Update the codebase projects to their latest remote version"),
)
.subcommand(
SubCommand::with_name("config")
.about("Update project configuration")
.arg(
Arg::with_name("directory")
.help("Directory of the project to configure")
.required(true)
.value_name("DIRECTORY"),
)
.arg(
Arg::with_name("branch")
.long("branch")
.short("b")
.help("Repository branch to be checkout")
.required(false)
.value_name("BRANCH"),
)
.arg(
Arg::with_name("git-config")
.long("git-config")
.help("Git configuration to be applied")
.required(false)
.value_name("KEY=VALUE")
.multiple(true),
)
.arg(
Arg::with_name("git-hook")
.long("git-hook")
.help("Git hook to apply")
.required(false)
.value_name("HOOK-NAME"),
),
)
.subcommand(
SubCommand::with_name("mv")
.about("Move project from src-directory to dst-directory")
.arg(
Arg::with_name("src-directory")
.help("Directory of the project to move")
.required(true)
.value_name("SRC-DIRECTORY"),
)
.arg(
Arg::with_name("dst-directory")
.help("New project directory")
.required(true)
.value_name("DST-DIRECTORY"),
),
)
.subcommand(
SubCommand::with_name("create")
.about("Create a brand new project")
.arg(
Arg::with_name("directory")
.help("Directory where the repository should be created")
.required(true)
.value_name("DIRECTORY"),
)
.arg(
Arg::with_name("remote")
.long("remote")
.help("The repository remote")
.required(true)
.value_name("REMOTE"),
)
.arg(
Arg::with_name("branch")
.long("branch")
.help("Repository branch to be set")
.required(false)
.value_name("BRANCH"),
)
.arg(
Arg::with_name("git-config")
.long("git-config")
.help("Git configuration to be applied")
.required(false)
.value_name("KEY=VALUE")
.multiple(true),
)
.arg(
Arg::with_name("archetype")
.long("archetype")
.help("The archetype used to create the project")
.required(false)
.value_name("ARCHETYPE-NAME"),
)
.arg(
Arg::with_name("archetype-config")
.long("archetype-config")
.help("Archetype configuration to be applied")
.required(false)
.value_name("KEY=VALUE")
.multiple(true),
)
.arg(
Arg::with_name("git-hook")
.long("git-hook")
.help("Git hook to apply")
.required(false)
.value_name("HOOK-NAME"),
),
)
.subcommand(SubCommand::with_name("pwd").about("Print codebase local working directory"))
.get_matches();
if matches.is_present("list-archetypes") {
let archetypes = archetype::archetypes(codebase_config_directory()).unwrap();
let mut table = Table::new();
table.add_row(row!["Id", "Description", "Author", "Type"]);
for (path, archetype) in archetypes {
let type_cell = match archetype.executable {
Some(executable) => Cell::new(&format!("Wrapper ({})", executable)),
None => Cell::new("Template"),
};
table.add_row(Row::new(vec![
Cell::new(&path),
Cell::new(&archetype.description),
Cell::new(&archetype.author),
type_cell,
]));
}
table.printstd();
}
if matches.is_present("list-hooks") {
let hooks = hook::hooks(codebase_config_directory()).unwrap();
let mut table = Table::new();
table.add_row(row!["Id", "Description", "Author"]);
for (path, archetype) in hooks {
table.add_row(Row::new(vec![
Cell::new(&path),
Cell::new(&archetype.description),
Cell::new(&archetype.author),
]));
}
table.printstd();
}
if let Some(matches) = matches.subcommand_matches("init") {
let directory = matches.value_of("directory").unwrap_or(".");
let remote = matches.value_of("remote");
let import = matches.is_present("import");
match Codebase::init(directory, &remote, import, codebase_config_directory()) {
Ok(_) => println!("Successfully created new codebase at {}", directory),
Err(e) => {
eprintln!("Error while initializing new codebase: {}", e);
process::exit(1);
}
}
}
if let Some(matches) = matches.subcommand_matches("clone") {
let remote = matches.value_of("remote").unwrap();
let directory = matches.value_of("directory").unwrap_or(".");
let callback =
|path: &str, project: &Project| println!("Cloned {} into {}", project.remote, path);
match Codebase::clone(directory, &remote, callback, codebase_config_directory()) {
Ok(_) => println!(
"Successfully cloned codebase from {} into {}",
remote, directory
),
Err(e) => {
eprintln!("Error while cloning remote codebase: {}", e);
process::exit(1);
}
}
}
if let Some(matches) = matches.subcommand_matches("add") {
let codebase = open_codebase();
let remote = matches.value_of("remote").unwrap();
let directory = matches.value_of("directory");
let branch = matches.value_of("branch");
let config = parse_config(matches.values_of("git-config"));
let hook = matches.value_of("git-hook");
let opts = ProjectOpts::new(branch, config, hook);
match codebase.add_project(&directory.map(Path::new), &remote, &opts) {
Ok(project) => println!("Successfully added project {}", project.remote),
Err(e) => {
eprintln!("Error while adding project: {}", e);
process::exit(1);
}
}
}
if let Some(matches) = matches.subcommand_matches("rm") {
let codebase = open_codebase();
let directory = matches.value_of("directory").unwrap();
let remove_from_disk = matches.is_present("remove");
match codebase.remove_project(Path::new(directory), remove_from_disk) {
Ok(_) => println!("Successfully removed project {}", directory),
Err(e) => {
eprintln!("Error while removing project: {}", e);
process::exit(1);
}
}
}
if matches.subcommand_matches("push").is_some() {
let codebase = open_codebase();
match codebase.push() {
Ok(_) => println!("Successfully pushed changes to remote!"),
Err(e) => {
eprintln!("Error while pushing changes to remote: {}", e);
process::exit(1);
}
}
}
if let Some(matches) = matches.subcommand_matches("pull") {
let codebase = open_codebase();
let remove_from_disk = matches.is_present("remove");
match codebase.pull(remove_from_disk) {
Ok((add_projects, rm_projects)) => {
println!("Successfully pulled changes from remote!");
for project in add_projects {
println!("[+] {}", project.remote)
}
for project in rm_projects {
println!("[-] {}", project.remote)
}
}
Err(e) => {
eprintln!("Error while pulling changes from remote: {}", e);
process::exit(1);
}
}
}
if matches.subcommand_matches("ls").is_some() {
let codebase = open_codebase();
let manifest = codebase.read_manifest().unwrap();
if manifest.projects.is_empty() {
println!("There is no project in the codebase.");
return;
}
let mut table = Table::new();
table.add_row(row!["Remote", "Path", "Branch"]);
for (path, project) in manifest.projects {
table.add_row(Row::new(vec![
Cell::new(&project.remote),
Cell::new(&path),
Cell::new(&project.branch).with_style(Attr::ForegroundColor(color::RED)),
]));
}
table.printstd();
}
if matches.subcommand_matches("update").is_some() {
let codebase = open_codebase();
let callback = |p: &Project| println!("Updated project {}", p.remote);
match codebase.update_projects(callback) {
Ok(_) => println!("Successfully updated projects!"),
Err(e) => {
eprintln!("Error while updating projects: {}", e);
process::exit(1);
}
}
}
if let Some(matches) = matches.subcommand_matches("config") {
let codebase = open_codebase();
let directory = matches.value_of("directory").unwrap();
let branch = matches.value_of("branch");
let config = parse_config(matches.values_of("git-config"));
let hook = matches.value_of("git-hook");
let opts = ProjectOpts::new(branch, config, hook);
match codebase.config_project(directory, &opts) {
Ok(_) => println!("Successfully updated project configuration!"),
Err(e) => {
eprintln!("Error while updating project configuration: {}", e);
process::exit(1);
}
}
}
if let Some(matches) = matches.subcommand_matches("mv") {
let codebase = open_codebase();
let src_directory = matches.value_of("src-directory").unwrap();
let dst_directory = matches.value_of("dst-directory").unwrap();
match codebase.move_project(src_directory, dst_directory) {
Ok(_) => println!(
"Successfully moved project from {} to {}",
src_directory, dst_directory
),
Err(e) => {
eprintln!("Error while moving project: {}", e);
process::exit(1);
}
}
}
if let Some(matches) = matches.subcommand_matches("create") {
let codebase = open_codebase();
let directory = matches.value_of("directory").unwrap();
let remote = matches.value_of("remote").unwrap();
let branch = matches.value_of("branch");
let git_config = parse_config(matches.values_of("git-config"));
let archetype = matches.value_of("archetype");
let archetype_config = parse_config(matches.values_of("archetype-config"));
let hook = matches.value_of("git-hook");
let opts = ProjectOpts::new(branch, git_config, hook);
match codebase.create_project(directory, &remote, &opts, &archetype, &archetype_config) {
Ok((path, _)) => println!("Successfully created new project {}", path),
Err(e) => {
eprintln!("Error while creating project: {}", e);
process::exit(1);
}
}
}
if matches.subcommand_matches("pwd").is_some() {
let codebase = open_codebase();
println!("/{}", codebase.local_path().display());
}
}
fn parse_config(options: Option<clap::Values>) -> BTreeMap<String, String> {
let mut config: BTreeMap<String, String> = BTreeMap::new();
if let Some(config_args) = options {
for config_arg in config_args {
let parts: Vec<&str> = config_arg.split('=').collect();
if parts.len() == 2 {
config.insert(String::from(parts[0]), String::from(parts[1]));
}
}
}
config
}
fn open_codebase() -> Codebase {
let cwd = env::current_dir().unwrap();
match Codebase::open(cwd.as_path(), codebase_config_directory()) {
Ok(c) => c,
Err(_c) => {
eprintln!("No codebase found in current or parent directory(ies)");
process::exit(1);
}
}
}
fn codebase_config_directory() -> PathBuf {
let path = format!("{}/{}", env::var("HOME").unwrap(), ".codebase-config");
let path = Path::new(&path);
path.to_path_buf()
}