use std::path::PathBuf;
use thiserror::Error;
pub type OutpostResult<T> = Result<T, OutpostError>;
#[derive(Debug, Error)]
pub enum OutpostError {
#[error("not inside a Git repository: {}", .0.display())]
NotARepo(PathBuf),
#[error("not inside a managed outpost: {}", .0.display())]
NotAnOutpost(PathBuf),
#[error("source repository not found at {}", .0.display())]
SourceMissing(PathBuf),
#[error("{command} must be run from {expected}; effective cwd is {}", .cwd.display())]
WrongContext {
command: &'static str,
expected: &'static str,
cwd: PathBuf,
},
#[error("{command} requires <outpost> when run from source repository {}", .cwd.display())]
MissingOutpostPath { command: &'static str, cwd: PathBuf },
#[error("destination already exists: {}", .0.display())]
DestinationExists(PathBuf),
#[error("destination {} is inside an existing Git repository", .0.display())]
DestinationInsideRepo(PathBuf),
#[error("working tree is dirty in {}; {hint}", .repo.display())]
DirtyTree { repo: PathBuf, hint: &'static str },
#[error("branch {branch} has unpushed commits in {}; {hint}", .repo.display())]
UnpushedCommits {
repo: PathBuf,
branch: String,
hint: &'static str,
},
#[error("history diverges from source repository on branch {branch}")]
Divergence { branch: String },
#[error("branch not found: {branch} in {}", .repo.display())]
BranchNotFound { branch: String, repo: PathBuf },
#[error("no upstream tracking configured for branch {branch}")]
NoUpstreamTracking { branch: String },
#[error(
"upstream is not a branch ref (got {merge_ref}); cannot synchronize from a non-branch upstream"
)]
UpstreamNotABranch { merge_ref: String },
#[error("invalid ref name: {name}")]
InvalidRefName { name: String },
#[error(
"source repository {} has {branch} checked out; cannot push to a non-bare checked-out branch (configure receive.denyCurrentBranch=updateInstead on the source, or check out a different branch in the source)",
.r#source.display()
)]
PushIntoCheckedOutBranch { r#source: PathBuf, branch: String },
#[error("branch {branch} does not exist on the source repository")]
AmbiguousBranchCreation { branch: String },
#[error("outpost is locked: {}{reason}", .path.display())]
OutpostLocked { path: PathBuf, reason: String },
#[error("registry entry path is not a managed outpost of this source: {}", .0.display())]
RegistryEntryNotManaged(PathBuf),
#[error("registry entry not found: {}", .0.display())]
RegistryEntryNotFound(PathBuf),
#[error("outpost id prefix not found: {0}")]
OutpostIdPrefixNotFound(String),
#[error("outpost id prefix is ambiguous: {0}")]
OutpostIdPrefixAmbiguous(String),
#[error("outpost selector is ambiguous: {0}")]
OutpostSelectorAmbiguous(String),
#[error("invalid registry file at {}: {reason}", .path.display())]
BadRegistry { path: PathBuf, reason: String },
#[error("invalid outpost metadata at {}: {reason}", .outpost.display())]
BadMetadata { outpost: PathBuf, reason: String },
#[error("git command failed: `git {args}` (exit {code}): {stderr}")]
GitFailed {
args: String,
code: i32,
stderr: String,
},
#[error("git command terminated by signal: `git {args}`{signal_str}")]
GitTerminatedBySignal { args: String, signal_str: String },
#[error("io error at {}: {source}", .path.display())]
IoAt {
path: PathBuf,
source: std::io::Error,
},
}
impl OutpostError {
pub fn exit_code(&self) -> u8 {
use OutpostError::*;
match self {
NotARepo(_)
| NotAnOutpost(_)
| SourceMissing(_)
| WrongContext { .. }
| MissingOutpostPath { .. } => 2,
DestinationExists(_)
| DestinationInsideRepo(_)
| DirtyTree { .. }
| UnpushedCommits { .. }
| OutpostLocked { .. } => 3,
Divergence { .. }
| PushIntoCheckedOutBranch { .. }
| AmbiguousBranchCreation { .. } => 4,
BranchNotFound { .. }
| NoUpstreamTracking { .. }
| InvalidRefName { .. }
| UpstreamNotABranch { .. } => 5,
BadRegistry { .. }
| BadMetadata { .. }
| OutpostIdPrefixNotFound(_)
| OutpostIdPrefixAmbiguous(_)
| OutpostSelectorAmbiguous(_)
| RegistryEntryNotManaged(_)
| RegistryEntryNotFound(_) => 6,
GitFailed { code, .. } => (*code).clamp(1, 125) as u8,
GitTerminatedBySignal { .. } => 137,
IoAt { .. } => 70,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn path(value: &str) -> PathBuf {
PathBuf::from(value)
}
#[test]
fn display_strings_match_snapshot() {
let cases = [
(
OutpostError::NotARepo(path("/repo")),
"not inside a Git repository: /repo",
),
(
OutpostError::NotAnOutpost(path("/outpost")),
"not inside a managed outpost: /outpost",
),
(
OutpostError::SourceMissing(path("/source")),
"source repository not found at /source",
),
(
OutpostError::WrongContext {
command: "pull",
expected: "a managed outpost",
cwd: path("/source"),
},
"pull must be run from a managed outpost; effective cwd is /source",
),
(
OutpostError::MissingOutpostPath {
command: "lock",
cwd: path("/source"),
},
"lock requires <outpost> when run from source repository /source",
),
(
OutpostError::DestinationExists(path("/dest")),
"destination already exists: /dest",
),
(
OutpostError::DestinationInsideRepo(path("/dest")),
"destination /dest is inside an existing Git repository",
),
(
OutpostError::DirtyTree {
repo: path("/repo"),
hint: "pass --force",
},
"working tree is dirty in /repo; pass --force",
),
(
OutpostError::UnpushedCommits {
repo: path("/repo"),
branch: "main".to_owned(),
hint: "push first",
},
"branch main has unpushed commits in /repo; push first",
),
(
OutpostError::Divergence {
branch: "main".to_owned(),
},
"history diverges from source repository on branch main",
),
(
OutpostError::BranchNotFound {
branch: "feature".to_owned(),
repo: path("/repo"),
},
"branch not found: feature in /repo",
),
(
OutpostError::NoUpstreamTracking {
branch: "feature".to_owned(),
},
"no upstream tracking configured for branch feature",
),
(
OutpostError::UpstreamNotABranch {
merge_ref: "refs/tags/v1".to_owned(),
},
"upstream is not a branch ref (got refs/tags/v1); cannot synchronize from a non-branch upstream",
),
(
OutpostError::InvalidRefName {
name: "-evil".to_owned(),
},
"invalid ref name: -evil",
),
(
OutpostError::PushIntoCheckedOutBranch {
source: path("/source"),
branch: "main".to_owned(),
},
"source repository /source has main checked out; cannot push to a non-bare checked-out branch (configure receive.denyCurrentBranch=updateInstead on the source, or check out a different branch in the source)",
),
(
OutpostError::AmbiguousBranchCreation {
branch: "feature".to_owned(),
},
"branch feature does not exist on the source repository",
),
(
OutpostError::OutpostLocked {
path: path("/outpost"),
reason: ": release".to_owned(),
},
"outpost is locked: /outpost: release",
),
(
OutpostError::RegistryEntryNotManaged(path("/outpost")),
"registry entry path is not a managed outpost of this source: /outpost",
),
(
OutpostError::RegistryEntryNotFound(path("/missing")),
"registry entry not found: /missing",
),
(
OutpostError::OutpostIdPrefixNotFound("abcde".to_owned()),
"outpost id prefix not found: abcde",
),
(
OutpostError::OutpostIdPrefixAmbiguous("abcde".to_owned()),
"outpost id prefix is ambiguous: abcde",
),
(
OutpostError::OutpostSelectorAmbiguous("abcde".to_owned()),
"outpost selector is ambiguous: abcde",
),
(
OutpostError::BadRegistry {
path: path("/repo/.outpost/registry.json"),
reason: "invalid json".to_owned(),
},
"invalid registry file at /repo/.outpost/registry.json: invalid json",
),
(
OutpostError::BadMetadata {
outpost: path("/outpost"),
reason: "missing source".to_owned(),
},
"invalid outpost metadata at /outpost: missing source",
),
(
OutpostError::GitFailed {
args: "status --short".to_owned(),
code: 1,
stderr: "fatal".to_owned(),
},
"git command failed: `git status --short` (exit 1): fatal",
),
(
OutpostError::GitTerminatedBySignal {
args: "fetch".to_owned(),
signal_str: " (signal 9)".to_owned(),
},
"git command terminated by signal: `git fetch` (signal 9)",
),
(
OutpostError::IoAt {
path: path("/repo/.outpost/registry.json"),
source: std::io::Error::new(std::io::ErrorKind::NotFound, "missing"),
},
"io error at /repo/.outpost/registry.json: missing",
),
];
for (error, expected) in cases {
assert_eq!(error.to_string(), expected);
}
}
#[test]
fn exit_code_maps_each_variant() {
let cases = [
(OutpostError::NotARepo(path("/repo")), 2),
(OutpostError::NotAnOutpost(path("/outpost")), 2),
(OutpostError::SourceMissing(path("/source")), 2),
(
OutpostError::WrongContext {
command: "pull",
expected: "a managed outpost",
cwd: path("/source"),
},
2,
),
(
OutpostError::MissingOutpostPath {
command: "lock",
cwd: path("/source"),
},
2,
),
(OutpostError::DestinationExists(path("/dest")), 3),
(OutpostError::DestinationInsideRepo(path("/dest")), 3),
(
OutpostError::DirtyTree {
repo: path("/repo"),
hint: "pass --force",
},
3,
),
(
OutpostError::UnpushedCommits {
repo: path("/repo"),
branch: "main".to_owned(),
hint: "push first",
},
3,
),
(
OutpostError::Divergence {
branch: "main".to_owned(),
},
4,
),
(
OutpostError::BranchNotFound {
branch: "feature".to_owned(),
repo: path("/repo"),
},
5,
),
(
OutpostError::NoUpstreamTracking {
branch: "feature".to_owned(),
},
5,
),
(
OutpostError::UpstreamNotABranch {
merge_ref: "refs/tags/v1".to_owned(),
},
5,
),
(
OutpostError::InvalidRefName {
name: "-evil".to_owned(),
},
5,
),
(
OutpostError::PushIntoCheckedOutBranch {
source: path("/source"),
branch: "main".to_owned(),
},
4,
),
(
OutpostError::AmbiguousBranchCreation {
branch: "feature".to_owned(),
},
4,
),
(
OutpostError::OutpostLocked {
path: path("/outpost"),
reason: ": release".to_owned(),
},
3,
),
(OutpostError::RegistryEntryNotManaged(path("/outpost")), 6),
(OutpostError::RegistryEntryNotFound(path("/missing")), 6),
(OutpostError::OutpostIdPrefixNotFound("abcde".to_owned()), 6),
(
OutpostError::OutpostIdPrefixAmbiguous("abcde".to_owned()),
6,
),
(
OutpostError::OutpostSelectorAmbiguous("abcde".to_owned()),
6,
),
(
OutpostError::BadRegistry {
path: path("/repo/.outpost/registry.json"),
reason: "invalid json".to_owned(),
},
6,
),
(
OutpostError::BadMetadata {
outpost: path("/outpost"),
reason: "missing source".to_owned(),
},
6,
),
(
OutpostError::GitFailed {
args: "status".to_owned(),
code: 42,
stderr: "fatal".to_owned(),
},
42,
),
(
OutpostError::GitTerminatedBySignal {
args: "fetch".to_owned(),
signal_str: " (signal 9)".to_owned(),
},
137,
),
(
OutpostError::IoAt {
path: path("/repo/.outpost/registry.json"),
source: std::io::Error::new(std::io::ErrorKind::NotFound, "missing"),
},
70,
),
];
for (error, expected) in cases {
assert_eq!(error.exit_code(), expected);
}
assert_eq!(
OutpostError::GitFailed {
args: "status".to_owned(),
code: -1,
stderr: "fatal".to_owned(),
}
.exit_code(),
1
);
assert_eq!(
OutpostError::GitFailed {
args: "status".to_owned(),
code: 256,
stderr: "fatal".to_owned(),
}
.exit_code(),
125
);
}
}