use anyhow::{Context, Result, anyhow, bail};
use clap::Parser;
use std::{env, io::stdout, path};
use wok_dev as wok;
fn resolve_path(base: &path::Path, value: &path::Path) -> path::PathBuf {
if value.is_absolute() {
path::PathBuf::from(value)
} else {
base.join(value)
}
}
#[derive(Debug, Parser)]
#[clap(
name = "wok",
about = "Wok -- control several git repositories as a single project."
)]
struct Args {
#[clap(
global = true,
short('f'),
long,
value_parser,
default_value = wok::DEFAULT_CONFIG_NAME,
)]
wokfile_path: path::PathBuf,
#[clap(subcommand)]
cmd: Command,
}
#[derive(Debug, Parser)]
enum Command {
Init {},
Assemble {
directory: path::PathBuf,
},
#[clap(flatten)]
App(App),
}
#[derive(Debug, Parser)]
enum App {
#[clap(subcommand)]
Head(Head),
#[clap(subcommand)]
Repo(Repo),
Switch {
#[clap(long)]
create: bool,
#[clap(long)]
all: bool,
#[clap(long)]
branch: Option<String>,
repos: Vec<path::PathBuf>,
},
Lock,
Update {
#[clap(long = "no-commit")]
no_commit: bool,
},
Status,
Push {
#[clap(short('u'), long)]
set_upstream: bool,
#[clap(long)]
all: bool,
#[clap(long)]
branch: Option<String>,
repos: Vec<path::PathBuf>,
},
Tag {
#[clap(long)]
create: Option<String>,
#[clap(long)]
sign: bool,
#[clap(long)]
push: bool,
#[clap(long)]
all: bool,
repos: Vec<path::PathBuf>,
},
}
#[derive(Debug, Parser)]
enum Head {
Switch,
}
#[derive(Debug, Parser)]
enum Repo {
Add {
submodule_path: path::PathBuf,
},
#[clap(name = "rm")]
Remove {
submodule_path: path::PathBuf,
},
}
fn resolve_tag_arguments<'a>(
create: &'a Option<String>,
all: bool,
repos: &'a [path::PathBuf],
config: &wok::config::Config,
) -> Result<(Option<String>, &'a [path::PathBuf])> {
if create.is_some() {
if all && !repos.is_empty() {
bail!("Cannot specify repositories when using --all");
}
return Ok((None, repos));
}
if all {
if let Some((first_arg, rest)) = repos.split_first() {
let tag = first_arg
.to_str()
.ok_or_else(|| {
anyhow!("Tag name '{}' is not valid UTF-8", first_arg.display())
})?
.to_owned();
return Ok((Some(tag), rest));
}
return Ok((None, repos));
}
if let Some((first_arg, rest)) = repos.split_first() {
let matches_repo = config
.repos
.iter()
.any(|config_repo| config_repo.path == *first_arg);
if matches_repo {
Ok((None, repos))
} else {
let tag = first_arg
.to_str()
.ok_or_else(|| {
anyhow!("Tag name '{}' is not valid UTF-8", first_arg.display())
})?
.to_owned();
Ok((Some(tag), rest))
}
} else {
Ok((None, repos))
}
}
fn main() -> Result<()> {
let Args { wokfile_path, cmd } = Args::parse();
let cwd = env::current_dir().context("Cannot access the current directory")?;
let mut output = stdout();
match cmd {
Command::Init {} => {
let config_path = resolve_path(&cwd, &wokfile_path);
if config_path.exists() {
bail!("Wok file already exists at `{}`", config_path.display());
};
let repo_dir = config_path.parent().with_context(|| {
format!("Cannot open work dir for `{}`", config_path.display())
})?;
let umbrella = wok::repo::Repo::new(repo_dir, None)?;
wok::cmd::init(&config_path, &umbrella, &mut output)?
},
Command::Assemble { directory } => {
let workspace_dir = resolve_path(&cwd, &directory);
let config_path = if wokfile_path.is_absolute() {
wokfile_path.clone()
} else {
workspace_dir.join(&wokfile_path)
};
wok::cmd::assemble(&workspace_dir, &config_path, &mut output)?
},
Command::App(app_cmd) => {
let config_path = resolve_path(&cwd, &wokfile_path);
if !config_path.exists() {
bail!("Wok file not found at `{}`", config_path.display());
};
let repo_dir = config_path.parent().with_context(|| {
format!("Cannot open work dir for `{}`", config_path.display())
})?;
let umbrella = wok::repo::Repo::new(repo_dir, None)?;
let mut wok_config = wok::config::Config::load(&config_path)?;
if match app_cmd {
App::Head(head_cmd) => match head_cmd {
Head::Switch => wok::cmd::head::switch(&mut wok_config, &umbrella)?,
},
App::Repo(repo_cmd) => match repo_cmd {
Repo::Add { submodule_path } => wok::cmd::repo::add(
&mut wok_config,
&umbrella,
&submodule_path,
)?,
Repo::Remove { submodule_path } => {
wok::cmd::repo::rm(&mut wok_config, &submodule_path)?
},
},
App::Switch {
create,
all,
branch,
repos,
} => wok::cmd::switch(
&mut wok_config,
&umbrella,
&mut output,
create,
all,
branch.as_deref(),
&repos,
)?,
App::Lock => {
wok::cmd::lock(&mut wok_config, &umbrella, &mut output)?;
false },
App::Update { no_commit } => {
wok::cmd::update(
&mut wok_config,
&umbrella,
&mut output,
no_commit,
)?;
false },
App::Status => {
wok::cmd::status(&mut wok_config, &umbrella, &mut output)?;
false },
App::Push {
set_upstream,
all,
branch,
repos,
} => {
wok::cmd::push(
&mut wok_config,
&umbrella,
&mut output,
set_upstream,
all,
branch.as_deref(),
&repos,
)?;
false },
App::Tag {
create,
sign,
push,
all,
repos,
} => {
let (positional_tag, repo_args) =
resolve_tag_arguments(&create, all, &repos, &wok_config)?;
let tag_name = create.as_deref().or(positional_tag.as_deref());
wok::cmd::tag(
&mut wok_config,
&umbrella,
&mut output,
tag_name,
sign,
push,
all,
repo_args,
)?;
false },
} {
wok_config.save(&config_path)?;
}
},
};
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn config_with_repo(path: &str) -> wok::config::Config {
let mut config = wok::config::Config::new();
config.add_repo(path::Path::new(path), "main");
config
}
#[test]
fn derive_tag_from_positional_when_all() {
let config = config_with_repo("api");
let repos = vec![path::PathBuf::from("v2.0.0")];
let (positional_tag, remaining) =
resolve_tag_arguments(&None, true, &repos, &config).unwrap();
assert_eq!(positional_tag.as_deref(), Some("v2.0.0"));
assert!(remaining.is_empty());
}
#[test]
fn allows_explicit_repos_with_all_when_tag_is_positional() {
let config = config_with_repo("api");
let repos = vec![path::PathBuf::from("v2.0.0"), path::PathBuf::from("api")];
let (positional_tag, remaining) =
resolve_tag_arguments(&None, true, &repos, &config).unwrap();
assert_eq!(positional_tag.as_deref(), Some("v2.0.0"));
assert_eq!(remaining, &repos[1..]);
}
#[test]
fn keeps_repo_arguments_for_listing() {
let config = config_with_repo("api");
let repos = vec![path::PathBuf::from("api")];
let (positional_tag, remaining) =
resolve_tag_arguments(&None, false, &repos, &config).unwrap();
assert!(positional_tag.is_none());
assert_eq!(remaining, repos.as_slice());
}
#[test]
fn rejects_repos_with_all_when_create_present() {
let config = config_with_repo("api");
let repos = vec![path::PathBuf::from("api")];
let create = Some(String::from("v2.0.0"));
let result = resolve_tag_arguments(&create, true, &repos, &config);
assert!(result.is_err());
}
#[test]
fn derives_tag_from_first_non_repo_argument() {
let config = config_with_repo("api");
let repos = vec![path::PathBuf::from("v2.0.0"), path::PathBuf::from("api")];
let (positional_tag, remaining) =
resolve_tag_arguments(&None, false, &repos, &config).unwrap();
assert_eq!(positional_tag.as_deref(), Some("v2.0.0"));
assert_eq!(remaining, &repos[1..]);
}
#[test]
fn allows_multiple_repos_with_all_when_no_create() {
let mut config = config_with_repo("api");
config.add_repo(path::Path::new("docs"), "main");
let repos = vec![
path::PathBuf::from("v2.0.0"),
path::PathBuf::from("api"),
path::PathBuf::from("docs"),
];
let (positional_tag, remaining) =
resolve_tag_arguments(&None, true, &repos, &config).unwrap();
assert_eq!(positional_tag.as_deref(), Some("v2.0.0"));
assert_eq!(remaining, &repos[1..]);
}
}