#![allow(clippy::print_literal)]
use std::env;
use std::io;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use std::process;
use anyhow::{anyhow, Context, Result};
use colored::*;
use regex::Regex;
pub use cli::Command;
pub use config::Config;
use cli::SubCommand;
use files::PatchGroup;
use git::GitGroup;
use hooks::HooksGroup;
pub mod cli;
pub mod config;
mod files;
mod git;
mod hooks;
#[derive(Debug)]
pub struct BumpContext {
new_version: String,
dry_run: bool,
interactive: bool,
patch_only: bool,
project_path: PathBuf,
config: Config,
git_state: git::State,
}
impl BumpContext {
pub fn from_command(cmd: &Command) -> Result<Self> {
let project_path = match &cmd.cwd {
None => env::current_dir()
.with_context(|| "Could not get current working directory".to_string())?,
Some(p) => p.to_path_buf(),
};
let cfg_path = project_path.join("tbump.toml");
let config = crate::config::parse(&cfg_path)
.with_context(|| format!("Could not parse configuration in {:?}", &cfg_path))?;
let new_version = cmd.new_version.as_deref().unwrap();
let git_state = git::get_state(&project_path)?;
Ok(BumpContext {
project_path,
new_version: new_version.to_string(),
dry_run: cmd.dry_run,
interactive: !cmd.non_interactive,
patch_only: cmd.patch_only,
config,
git_state,
})
}
fn current_version(&self) -> &str {
&self.config.current_version
}
fn new_version(&self) -> &str {
&self.new_version
}
fn project_path(&self) -> &Path {
&self.project_path
}
fn version_regex(&self) -> &Regex {
&self.config.version_regex
}
}
fn ask_for_confirmation() -> Result<bool> {
println!("{} Looking good? (y/N)", "::".yellow());
print!("{}", "> ".yellow());
io::stdout().flush()?;
let mut answer = String::new();
io::stdin().read_line(&mut answer)?;
match answer.to_ascii_lowercase().trim_end() {
"yes" | "y" => Ok(true),
_ => Ok(false),
}
}
fn init(current_version: &str) -> Result<()> {
let template = include_str!("tbump.in.toml");
let to_write = template.replace("@current_version@", current_version);
std::fs::write("tbump.toml", to_write)
.with_context(|| "Could not write file tbump.toml".to_string())?;
println!("Generated tbump.toml");
Ok(())
}
pub fn run(command: &Command) -> Result<()> {
if command.sub_cmd.is_none() && command.new_version.is_none() {
eprintln!("Usage:\n tbump init <current-version>\n tbump <new-version>");
process::exit(1);
};
if let Some(sub_cmd) = &command.sub_cmd {
let SubCommand::Init { current_version } = sub_cmd;
init(current_version)
} else {
let context = BumpContext::from_command(&command)?;
bump(&context)
}
}
pub fn bump(context: &BumpContext) -> Result<()> {
let current_version = &context.current_version();
let new_version = &context.new_version();
println!(
"{} {} {} {} {}",
"::".blue(),
"Bumping from",
current_version.bold(),
"to",
new_version.bold(),
);
let groups = get_groups(&context)?;
if context.interactive {
for group in &groups {
group.dry_run();
}
}
if context.dry_run {
return Ok(());
}
if context.interactive {
let confirmed = ask_for_confirmation()?;
if !confirmed {
return Err(anyhow!("Cancelled by user"));
}
}
for group in &groups {
group.run()?;
}
Ok(())
}
fn get_groups(context: &BumpContext) -> Result<Vec<Box<dyn Group>>> {
let mut groups: Vec<Box<dyn Group>> = vec![];
let add_git_ops = !context.patch_only;
let patch_group = PatchGroup::new(context)?;
groups.push(Box::new(patch_group));
if add_git_ops {
let before_commit = HooksGroup::before_commit(&context);
if !before_commit.is_empty() {
groups.push(Box::new(before_commit));
}
}
if add_git_ops {
let git_group = GitGroup::new(context)?;
groups.push(Box::new(git_group));
}
let after_push = HooksGroup::after_push(context);
if !after_push.is_empty() {
groups.push(Box::new(after_push));
}
Ok(groups)
}
pub trait Operation {
fn run(&self) -> Result<()>;
fn dry_run(&self);
}
trait Group {
fn operations(&self) -> &[Box<dyn Operation>];
fn description(&self) -> String;
}
impl dyn Group {
fn dry_run(&self) {
println!(
"{} {} {}",
"=>".blue(),
&self.description(),
"(dry run)".dimmed()
);
for operation in self.operations() {
operation.dry_run();
}
}
fn run(&self) -> Result<()> {
println!("{} {}", "=>".blue(), &self.description());
for operation in self.operations() {
operation.run()?;
}
Ok(())
}
}