use std::path::PathBuf;
use thiserror::Error;
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum DodotError {
#[error("filesystem error at {path}: {source}")]
Fs {
path: PathBuf,
source: std::io::Error,
},
#[error("filesystem error {verb} {from} to {to}: {source}")]
FsBetween {
verb: &'static str,
from: PathBuf,
to: PathBuf,
source: std::io::Error,
},
#[error("symlink conflict: {path} already exists and is not managed by dodot")]
SymlinkConflict { path: PathBuf },
#[error("protected path: {path} cannot be symlinked")]
ProtectedPath { path: PathBuf },
#[error("pack not found: {name}")]
PackNotFound { name: String },
#[error("pack is invalid: {name}: {reason}")]
PackInvalid { name: String, reason: String },
#[error("pack ordering collision: display name `{display_name}` resolves to multiple packs:\n{}", .paths.iter().map(|p| format!(" - {}", p.display())).collect::<Vec<_>>().join("\n"))]
PackOrderingCollision {
display_name: String,
paths: Vec<PathBuf>,
},
#[error("handler not found: {name}")]
HandlerNotFound { name: String },
#[error("config error: {0}")]
Config(String),
#[error("command failed: {command} (exit code {exit_code})\n{stderr}")]
CommandFailed {
command: String,
exit_code: i32,
stderr: String,
},
#[error("invalid pattern {pattern}: {reason}")]
InvalidPattern { pattern: String, reason: String },
#[error("cross-pack deployment conflict detected (--force does not override this):\n{}", crate::conflicts::format_conflicts(.conflicts))]
CrossPackConflict {
conflicts: Vec<crate::conflicts::Conflict>,
},
#[error(
"cannot check for cross-pack deployment conflicts yet (--force does not override this):\n{}\n \
each of these files declares the paths it deploys to inside its own contents, and \
dodot has not rendered those contents — either never, or not since they last \
changed. Rendering them here would resolve their secrets and write their output \
for a run you have not agreed to yet.\n \
run `dodot up` for the pack(s) above, then re-run this command.",
.unresolved
.iter()
.map(|u| format!(" - pack '{}' ({} handler): {}", u.pack, u.handler, u.source))
.collect::<Vec<_>>()
.join("\n")
)]
ConflictCheckIncomplete {
unresolved: Vec<crate::packs::orchestration::UnresolvedClaim>,
},
#[error(
"adopt could not finish publishing into pack `{pack}`: {reason}\n \
the entries it had already published are back where they were, so the pack \
holds its pre-adopt content: {}\n \
no source file was changed — nothing was adopted.",
if .restored.is_empty() {
"the failure came before the first entry was published".to_string()
} else {
format!("restored {}", .restored.join(", "))
}
)]
PublicationRolledBack {
pack: String,
reason: String,
restored: Vec<String>,
},
#[error(
"adopt could not finish publishing into pack `{pack}`: {reason}\n \
and could not put the pack back the way it was:\n{}\n \
nothing was deleted — that content is still on disk at the paths above, and \
this run has left `{preparation}` in place rather than cleaning it up. Move \
what you need back by hand, then remove that directory.{}\n \
no source file was changed — nothing was adopted.",
.stranded
.iter()
.map(|s| format!(" - `{}` is at `{}`", s.in_pack, s.at))
.collect::<Vec<_>>()
.join("\n"),
if .restored.is_empty() {
String::new()
} else {
format!("\n the rest of the pack is back as it was: {}", .restored.join(", "))
}
)]
PublicationRollbackIncomplete {
pack: String,
reason: String,
restored: Vec<String>,
stranded: Vec<crate::commands::adopt::StrandedEntry>,
preparation: String,
},
#[error(
"routing override conflict in pack `{pack}` for `{rel_path}`:\n \
filename routes via its prefix, and `[symlink.targets]` declares `{config_target}`.\n \
pick one — either rename the file (drop the `home.`/`app.`/`xdg.`/`lib.` or `_home/`/`_xdg/`/`_app/`/`_lib/` prefix) \
or remove the `[symlink.targets]` entry."
)]
RoutingOverrideConflict {
pack: String,
rel_path: String,
config_target: String,
},
#[error("preprocessing failed for {source_file} ({preprocessor}): {message}")]
PreprocessorError {
preprocessor: String,
source_file: PathBuf,
message: String,
},
#[error("preprocessing collision in pack \"{pack}\": {source_file} expands to {expanded_name}, which conflicts with an existing pack file or another preprocessor's output")]
PreprocessorCollision {
pack: String,
source_file: String,
expanded_name: String,
},
#[error("template render failed for {}:\n {message}", source_file.display())]
TemplateRender {
source_file: PathBuf,
message: String,
},
#[error("template variable name \"{name}\" is reserved (dodot and env are built-in namespaces); choose a different name in [preprocessor.template.vars]")]
TemplateReservedVar { name: String },
#[error("unresolved dodot-conflict markers in {} at line{} {}\n resolve the conflict block(s) with `git diff -- '{}'` and remove the dodot-conflict marker lines, then re-run.", source_file.display(), if line_numbers.len() == 1 { "" } else { "s" }, line_numbers.iter().map(|n| n.to_string()).collect::<Vec<_>>().join(", "), source_file.display())]
UnresolvedConflictMarker {
source_file: PathBuf,
line_numbers: Vec<usize>,
},
#[error("{0}")]
Other(String),
}
pub type Result<T> = std::result::Result<T, DodotError>;
pub(crate) fn fs_err(path: impl Into<PathBuf>, source: std::io::Error) -> DodotError {
DodotError::Fs {
path: path.into(),
source,
}
}
pub(crate) fn fs_between_err(
verb: &'static str,
from: impl Into<PathBuf>,
to: impl Into<PathBuf>,
source: std::io::Error,
) -> DodotError {
DodotError::FsBetween {
verb,
from: from.into(),
to: to.into(),
source,
}
}