mod commit;
mod helpers;
mod init;
mod update;
use std::error::Error as _;
use clap::{ArgAction, Parser, Subcommand};
use commit::backend::CustomCommandBackendError;
use eyre::{Report, Result};
use inquire::InquireError;
use tracing_subscriber::fmt::format::FmtSpan;
use self::{
commit::{Commit, CommitError, backend::BackendError},
helpers::NotInGitWorktree,
init::{Init, InitError},
update::{Update, UpdateError},
};
use crate::{
config::{self, CONFIG_FILE_NAME, FromTomlError, updater},
error, hint,
};
const LONG_VERSION: &str = concat!(
env!("CARGO_PKG_VERSION"),
"\nrevision: ",
env!("REVISION"),
"\nfeatures: ",
env!("FEATURES"),
"\ntarget: ",
env!("TARGET"),
"\nprofile: ",
env!("PROFILE"),
"\nbuilt by: ",
env!("BUILT_BY"),
);
#[derive(Debug, Parser)]
#[command(
bin_name = "git z",
author,
version = env!("VERSION_WITH_GIT"),
long_version = LONG_VERSION,
)]
pub struct GitZ {
#[command(subcommand)]
command: GitZCommand,
#[arg(short = 'v', action = ArgAction::Count, global = true)]
verbosity: u8,
}
#[derive(Debug, Subcommand)]
pub enum GitZCommand {
Init(Init),
Commit(Commit),
Update(Update),
}
trait Command {
fn run(&self) -> Result<()>;
}
impl GitZ {
pub fn run() -> Result<()> {
let args = Self::parse();
setup_tracing(args.verbosity);
let result = match args.command {
GitZCommand::Init(init) => init.run(),
GitZCommand::Commit(commit) => commit.run(),
GitZCommand::Update(update) => update.run(),
};
match result {
Err(error) => handle_errors(error),
Ok(()) => Ok(()),
}
}
}
fn setup_tracing(verbosity: u8) {
tracing_subscriber::fmt()
.with_env_filter(env_filter(verbosity))
.with_span_events(span_events(verbosity))
.init();
}
fn env_filter(verbosity: u8) -> &'static str {
match verbosity {
0 => "off",
1 => "git_z=info",
2 => "git_z=debug",
3_u8..=u8::MAX => "git_z=trace",
}
}
fn span_events(verbosity: u8) -> FmtSpan {
match verbosity {
0..=3 => FmtSpan::NONE,
4..=u8::MAX => FmtSpan::ACTIVE,
}
}
enum ErrorHandling {
Return(Report),
Exit(i32),
}
fn handle_errors(error: Report) -> Result<()> {
let handling = if let Some(error) = error.downcast_ref::<NotInGitWorktree>()
{
handle_not_in_git_worktree(error)
} else if let Some(config::LoadError::InvalidConfig(error)) =
error.downcast_ref::<config::LoadError>()
{
handle_from_toml_error(error)
} else if let Some(updater::LoadError::InvalidConfig(error)) =
error.downcast_ref::<updater::LoadError>()
{
handle_from_toml_error(error)
} else if let Some(error) =
error.downcast_ref::<CustomCommandBackendError>()
{
handle_custom_command_backend_error(error)
} else if let Some(error) = error.downcast_ref::<InitError>() {
handle_init_error(error)
} else if let Some(error) = error.downcast_ref::<CommitError>() {
handle_commit_error(error)
} else if let Some(error) = error.downcast_ref::<UpdateError>() {
handle_update_error(error)
} else if let Some(InquireError::OperationCanceled) =
error.downcast_ref::<InquireError>()
{
ErrorHandling::Exit(exitcode::TEMPFAIL)
} else if let Some(InquireError::OperationInterrupted) =
error.downcast_ref::<InquireError>()
{
ErrorHandling::Exit(exitcode::TEMPFAIL)
} else {
ErrorHandling::Return(error)
};
match handling {
ErrorHandling::Return(error) => Err(error),
ErrorHandling::Exit(code) => {
#[expect(
clippy::exit,
reason = "This function is purposefully written to handle \
errors, write a useful message and exit with an error \
code. This is the only place in the code where it is done."
)]
std::process::exit(code);
}
}
}
fn handle_not_in_git_worktree(error: &NotInGitWorktree) -> ErrorHandling {
match error {
NotInGitWorktree::CannotRunGit(os_error) => {
error!("{error}.");
hint!("The OS reports: {os_error}.");
ErrorHandling::Exit(exitcode::UNAVAILABLE)
}
NotInGitWorktree::NotInRepo => {
error!("{error}.");
hint!("You can initialise a Git repository by running `git init`.");
ErrorHandling::Exit(exitcode::USAGE)
}
NotInGitWorktree::NotInWorktree => {
error!("{error}.");
hint!(
"You seem to be inside a Git repository, but not in a worktree."
);
ErrorHandling::Exit(exitcode::USAGE)
}
}
}
fn handle_from_toml_error(error: &FromTomlError) -> ErrorHandling {
match error {
FromTomlError::UnsupportedVersion { .. } => {
error!("{error}.");
hint!(
"Your {CONFIG_FILE_NAME} may have been created by a newer version of git-z."
);
}
FromTomlError::UnsupportedDevelopmentVersion {
gitz_version, ..
} => {
error!("{error}.");
hint! {"
Your {CONFIG_FILE_NAME} has been created by a development version of git-z.
However, configurations produced by a development version are only
supported by the immediately following release.
To update from this version, you can install git-z {gitz_version}
run `git z update`, then update to the latest version and run
`git z update` again.\
"};
}
FromTomlError::ParseError(parse_error) => {
error!("Invalid configuration in {CONFIG_FILE_NAME}.");
hint!("\n{parse_error}");
}
}
ErrorHandling::Exit(exitcode::CONFIG)
}
fn handle_custom_command_backend_error(
error: &CustomCommandBackendError,
) -> ErrorHandling {
match error {
CustomCommandBackendError::Syntax { parse_error, .. } => {
error!("{error}.");
hint!("Hint: {parse_error}.");
ErrorHandling::Exit(exitcode::USAGE)
}
}
}
fn handle_init_error(error: &InitError) -> ErrorHandling {
match error {
InitError::ExistingConfig => {
error!("{error}.");
hint!("You can force the command by running `git z init -f`.");
}
}
ErrorHandling::Exit(exitcode::CANTCREAT)
}
fn handle_commit_error(error: &CommitError) -> ErrorHandling {
match error {
#[cfg(feature = "unstable-pre-commit")]
CommitError::CannotRunPreCommit(os_error) => {
error!("{error}.");
hint!("The OS reports: {os_error}.");
ErrorHandling::Exit(exitcode::UNAVAILABLE)
}
#[cfg(feature = "unstable-pre-commit")]
CommitError::PreCommitFailed => {
error!("{error}.");
ErrorHandling::Exit(1)
}
CommitError::Backend(backend_error) => {
handle_commit_backend_error(backend_error)
}
CommitError::Template(tera_error) => {
error!("{tera_error} from the configuration.");
if let Some(parse_error) = tera_error.source() {
hint!("\n{parse_error}\n");
}
ErrorHandling::Exit(exitcode::CONFIG)
}
}
}
fn handle_commit_backend_error(error: &BackendError) -> ErrorHandling {
match error {
BackendError::CannotRun {
os_error: source, ..
} => {
error!("{error}.");
hint!("The OS reports: {source}.");
ErrorHandling::Exit(exitcode::UNAVAILABLE)
}
BackendError::ExecutionError { status_code } => {
ErrorHandling::Exit(status_code.unwrap_or(1_i32))
}
}
}
fn handle_update_error(error: &UpdateError) -> ErrorHandling {
match error {
UpdateError::UnsupportedVersion { .. } => {
error!("{error}.");
hint!(
"Your {CONFIG_FILE_NAME} may have been created by a newer version of git-z."
);
}
UpdateError::UnsupportedDevelopmentVersion { gitz_version, .. } => {
error!("{error}.");
hint! {"
`git z update` can update a configuration from any previous release.
However, configurations produced by a development version can only be
updated by the immediately following release.
To update from this version, you can install git-z {gitz_version},
run `git z update`, then update to the latest version and run
`git z update` again.\
"};
}
}
ErrorHandling::Exit(exitcode::CONFIG)
}