//! CLI errors and exit codes (PLAN 6.15: 0 ok, 1 error, 2 usage, 3 conflicts, 4 dirty refusal). use scion_core::{ErrorCode, ScionError}; use scion_wc::WorkingCopy; /// Exit code: success. pub(crate) const EXIT_OK: i32 = 0; /// Exit code: any other error. pub(crate) const EXIT_ERROR: i32 = 1; /// Exit code: usage error. pub(crate) const EXIT_USAGE: i32 = 2; /// Exit code: merge conflicts (rebase paused, unresolved conflicts). pub(crate) const EXIT_CONFLICTS: i32 = 3; /// Exit code: refused because of dirty files. pub(crate) const EXIT_DIRTY: i32 = 4; /// A failed command. #[derive(Debug, thiserror::Error)] pub(crate) enum CliError { /// A Scion error (from the server or the working-copy library). #[error("{0}")] Scion(#[from] ScionError), /// Bad command-line usage detected after parsing. #[error("{0}")] Usage(String), /// A client-side failure that has no SPEC error code. #[error("{0}")] Local(String), } pub(crate) type CliResult = Result; impl CliError { /// PLAN 6.15 exit code. pub(crate) fn exit_code(&self) -> i32 { match self { CliError::Usage(_) => EXIT_USAGE, CliError::Local(_) => EXIT_ERROR, CliError::Scion(e) => scion_exit_code(e), } } /// `(code, subcode)` for the error envelope. pub(crate) fn code(&self) -> (String, Option) { match self { CliError::Usage(_) => ("USAGE".to_owned(), None), CliError::Local(_) => ("LOCAL".to_owned(), None), CliError::Scion(e) => (e.code.as_str().to_owned(), e.subcode().map(str::to_owned)), } } /// Human message: `CODE[/subcode]: message`. pub(crate) fn human(&self) -> String { match self { CliError::Usage(m) => format!("usage: {m}"), CliError::Local(m) => m.clone(), CliError::Scion(e) => match e.subcode() { Some(s) => format!("{}/{s}: {}", e.code.as_str(), e.message), None => format!("{}: {}", e.code.as_str(), e.message), }, } } /// The message alone. pub(crate) fn message(&self) -> String { match self { CliError::Usage(m) | CliError::Local(m) => m.clone(), CliError::Scion(e) => e.message.clone(), } } } /// Exit code of a Scion error (PLAN 6.15). pub(crate) fn scion_exit_code(e: &ScionError) -> i32 { if e.code == ErrorCode::MergeConflict { return EXIT_CONFLICTS; } match e.subcode() { Some(s) if s == WorkingCopy::SUBCODE_DIRTY => EXIT_DIRTY, Some(s) if s == WorkingCopy::SUBCODE_UNRESOLVED || s == WorkingCopy::SUBCODE_MARKERS_REMAIN => { EXIT_CONFLICTS } _ => EXIT_ERROR, } } /// A local error. pub(crate) fn local(msg: impl Into) -> CliError { CliError::Local(msg.into()) } /// A usage error. pub(crate) fn usage(msg: impl Into) -> CliError { CliError::Usage(msg.into()) } #[cfg(test)] mod tests { use super::*; #[test] fn exit_codes() { assert_eq!(usage("x").exit_code(), EXIT_USAGE); assert_eq!(local("x").exit_code(), EXIT_ERROR); let c = CliError::from(ScionError::new(ErrorCode::MergeConflict, "c")); assert_eq!(c.exit_code(), EXIT_CONFLICTS); let d = CliError::from(ScionError::invalid(WorkingCopy::SUBCODE_DIRTY, "d")); assert_eq!(d.exit_code(), EXIT_DIRTY); assert_eq!( d.code(), ( "INVALID_ARGUMENT".into(), Some(WorkingCopy::SUBCODE_DIRTY.into()) ) ); let m = CliError::from(ScionError::invalid( WorkingCopy::SUBCODE_MARKERS_REMAIN, "m", )); assert_eq!(m.exit_code(), EXIT_CONFLICTS); let u = CliError::from(ScionError::invalid(WorkingCopy::SUBCODE_UNRESOLVED, "u")); assert_eq!(u.exit_code(), EXIT_CONFLICTS); let n = CliError::from(ScionError::not_found("n")); assert_eq!(n.exit_code(), EXIT_ERROR); assert!(n.human().starts_with("NOT_FOUND: ")); } }