use std::collections::BTreeSet;
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
use clap::CommandFactory;
use semver::Version;
use crate::Result;
use crate::alias::{self, Outcome};
use crate::cli::{Alias, Cli, Completions, Config, Set, Tag, Verb};
use crate::config::{self, Scaffold};
use crate::git;
use crate::version;
use crate::workspace::Workspace;
const REPORTED: &str = "";
pub fn run(verb: Option<Verb>) -> Result<()> {
match verb {
None => list(),
Some(Verb::Patch(args)) => bump_workspace(version::Level::Patch, args.dry_run),
Some(Verb::Minor(args)) => bump_workspace(version::Level::Minor, args.dry_run),
Some(Verb::Major(args)) => bump_workspace(version::Level::Major, args.dry_run),
Some(Verb::Tag(args)) => tag_workspace(args),
Some(Verb::Set(args)) => set_workspace(args),
Some(Verb::Update) => update_tool(),
Some(Verb::Version) => tool_version(),
Some(Verb::Alias(args)) => install_alias(args),
Some(Verb::Completions(args)) => completions(args),
Some(Verb::Config(args)) => write_config(args),
}
}
fn list() -> Result<()> {
let workspace = Workspace::discover()?;
for member in workspace.members() {
out(&format!(" {:<12} {}", member.name, member.version));
}
Ok(())
}
fn tool_version() -> Result<()> {
out(&format!("semver-sync {}", env!("CARGO_PKG_VERSION")));
Ok(())
}
fn update_tool() -> Result<()> {
let status = Command::new("cargo")
.args(["install", "semver-sync", "--locked"])
.status()
.map_err(|err| format!("cannot run `cargo install semver-sync`: {err}"))?;
if !status.success() {
return Err(String::from(REPORTED));
}
now_installed();
Ok(())
}
fn now_installed() {
let report = installed_exe()
.and_then(|exe| Command::new(exe).arg("version").output().ok())
.filter(|done| done.status.success())
.map(|done| String::from_utf8_lossy(&done.stdout).trim_end().to_string())
.unwrap_or_else(|| String::from("run `ss version` to see the version now installed"));
out(&report);
}
fn installed_exe() -> Option<PathBuf> {
let home = match std::env::var_os("CARGO_HOME") {
Some(home) => PathBuf::from(home),
None => PathBuf::from(std::env::var_os("HOME")?).join(".cargo"),
};
let exe = home.join("bin").join("semver-sync");
exe.is_file().then_some(exe)
}
fn bump_workspace(level: version::Level, dry_run: bool) -> Result<()> {
let workspace = Workspace::discover()?;
let current = workspace.shared_version()?;
let next = version::bump(¤t, level)?;
write_workspace(workspace, &next, dry_run)
}
fn tag_workspace(args: Tag) -> Result<()> {
let workspace = Workspace::discover()?;
let current = workspace.shared_version()?;
let next = version::tag(¤t, &args.tag)?;
write_workspace(workspace, &next, args.dry_run)
}
fn set_workspace(args: Set) -> Result<()> {
let workspace = Workspace::discover()?;
let next = version::parse(&args.version)?;
write_workspace(workspace, &next, args.dry_run)
}
fn write_workspace(mut workspace: Workspace, next: &Version, dry_run: bool) -> Result<()> {
version::check_image_tag(&next.to_string())?;
let config = config::Config::from_repo(workspace.root())?.unwrap_or_default();
let before = workspace.snapshot()?;
let crates = workspace.member_names();
let mut failed = false;
let mut written: BTreeSet<PathBuf> = BTreeSet::new();
if config.git_tag {
let mut allowed: BTreeSet<PathBuf> = (0..workspace.target_count())
.map(|index| workspace.target_mut(index).path().to_path_buf())
.collect();
allowed.insert(workspace.root().join("Cargo.lock"));
git::reject_unwritten(workspace.root(), &allowed, &format!("v{next}"))?;
}
for index in 0..workspace.target_count() {
let target = workspace.target_mut(index);
match target.version() {
Ok(from) => {
target.rewrite(next, &crates);
if dry_run {
out(&line(&target.name(), &from, next, true));
} else if let Err(err) = target.save() {
note(&format!("semver-sync: {err}"));
failed = true;
} else {
written.insert(target.path().to_path_buf());
out(&line(&target.name(), &from, next, false));
}
}
Err(err) => {
note(&format!("semver-sync: {err}"));
failed = true;
}
}
}
if dry_run {
workspace.unchanged_since(&before)?;
if config.git_tag {
out(&git_line(&format!("commit, tag v{next} and push"), true));
}
}
if failed {
return Err(String::from(REPORTED));
}
if !dry_run {
workspace.refresh_lock()?;
if config.git_tag {
written.insert(workspace.root().join("Cargo.lock"));
git::release(workspace.root(), next, &written)?;
out(&git_line(
&format!("committed, tagged and pushed v{next}"),
false,
));
}
next_steps(&workspace, next);
}
Ok(())
}
fn line(name: &str, from: &Version, to: &Version, dry_run: bool) -> String {
let marker = if dry_run { " (dry-run)" } else { "" };
format!(" {name:<12} {from} -> {to}{marker}")
}
fn git_line(what: &str, dry_run: bool) -> String {
let marker = if dry_run { " (dry-run)" } else { "" };
format!(" {:<12} {what}{marker}", "git")
}
fn next_steps(workspace: &Workspace, next: &Version) {
if !workspace.root().join("bin/build.sh").is_file() {
return;
}
out("");
out("next steps:");
if workspace.root().join("bin/unit_test.sh").is_file() {
out(" bin/unit_test.sh");
}
out(&format!(" bin/build.sh # pushes images tagged {next}"));
}
fn install_alias(args: Alias) -> Result<()> {
match alias::install(args)? {
Outcome::Written { path, line } => out(&format!(
"wrote `{line}` to {} — reload with: source {}",
path.display(),
path.display()
)),
Outcome::Already { path } => out(&format!("the alias is already in {}", path.display())),
}
Ok(())
}
fn completions(args: Completions) -> Result<()> {
let mut command = Cli::command();
clap_complete::generate(
args.shell,
&mut command,
"semver-sync",
&mut std::io::stdout(),
);
Ok(())
}
fn write_config(args: Config) -> Result<()> {
let path = match config::scaffold(args.global)? {
Scaffold::Written { path } => {
out(&format!("wrote {}", path.display()));
path
}
Scaffold::Existing { path } => {
out(&format!("{} is already there", path.display()));
path
}
};
if let Some(read) = config::Config::discover()? {
let message = if read.path == path {
String::from(" it is the file that is read")
} else {
format!(
" {} is read first, so it wins over this one",
read.path.display()
)
};
out(&message);
}
Ok(())
}
fn out(text: &str) {
let stdout = std::io::stdout();
let mut handle = stdout.lock();
if writeln!(handle, "{text}").is_err() {
std::process::exit(0);
}
}
fn note(text: &str) {
let stderr = std::io::stderr();
let mut handle = stderr.lock();
if writeln!(handle, "{text}").is_err() {
std::process::exit(1);
}
}