1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
//! Error and result types.
use std::path::PathBuf;
use thiserror::Error;
/// Errors produced by prov.
#[derive(Debug, Error)]
pub enum Error {
/// The embedded-metadata backend (`fig`) failed to parse or serialize.
#[error("metadata error: {0}")]
Meta(#[from] fig::Error),
/// A structural invariant was violated (e.g. malformed frontmatter fence).
#[error("{0}")]
Structure(String),
/// A document a workspace operation names is not on disk — the typed form of
/// the many "X does not exist" guards the mutation ops make before touching a
/// document (`reparent`, `rename`, `duplicate`, `register`, …). A caller can
/// tell a genuinely-missing target from a malformed one by matching the
/// variant, rather than sniffing the message text.
#[error("{0} does not exist")]
NotFound(PathBuf),
/// A workspace operation would create a document where one already exists, and
/// refused rather than overwrite it — the typed form of the "X already exists"
/// guards in `create`/`rename`/`attach`. A destination collision is a distinct
/// outcome from a missing source, and now distinguishable as one.
#[error("{0} already exists")]
AlreadyExists(PathBuf),
/// The storage backend failed.
#[error("io error: {0}")]
Io(#[from] std::io::Error),
/// The `twig` body parser failed — see `content.rs`.
#[error("content error: {0}")]
Content(String),
/// A record store — the id registry, the recycle-bin index, or a flat
/// vocabulary — was found in a **markdown** carrier (fenced frontmatter)
/// rather than a whole-file config document (`.yaml`/`.json`/`.figl`). prov
/// imposes a sorted, one-record-per-line layout on these stores (DESIGN §5),
/// so a prose carrier has no stable home for its records and is refused. Make
/// it a bare config file. See [`crate::document::require_whole_file`].
#[error(
"record store must be a whole-file config document (.yaml/.json/.figl), \
not markdown frontmatter: {0}"
)]
MarkdownStore(PathBuf),
/// A path handed to a workspace read or write resolved *outside* the
/// workspace root — an absolute path, or one that climbs above the root with
/// `..`. prov clamps every I/O to the tree it was pointed at (a link
/// target is data, and data must never be able to name `/etc/passwd` or a
/// sibling repo), so such a path is refused rather than followed. See
/// [`crate::link::escapes_root`], the guard at `prov`'s `Workspace`'s `load`
/// and `prov`'s `ChangeSet::apply`.
#[error("path escapes the workspace root: {0}")]
Escape(PathBuf),
/// A `prov`'s `ChangeSet` was applied while a previous change's
/// write-ahead journal was still on disk — an earlier mutation was
/// interrupted (a crash) and never recovered. Landing this set would
/// overwrite that journal and lose the record needed to complete the
/// interrupted change, so the apply refuses: run recovery
/// (`prov`'s `journal::recover`, which `prov check` performs) first, then
/// retry.
#[error(
"a previous change was interrupted and not yet recovered (found {0}); \
recover it first (run `prov check`), then retry"
)]
StaleJournal(PathBuf),
/// A staged write failed *and* the rollback that should have undone it also
/// failed — see `prov`'s `ChangeSet::apply`. The one case where
/// prov cannot say what is on disk, so it says exactly that instead of
/// reporting the original failure as if the workspace were untouched.
#[error(
"{cause}; and rolling back failed too: {rollback}. \
The workspace may be partially written — run `prov check`."
)]
Torn {
/// The failure that triggered the rollback.
cause: String,
/// The failure the rollback itself hit.
rollback: String,
},
/// An operation would have registered an ID across a registration the index
/// already holds — see [`Collision`](crate::index::Collision). Refused rather
/// than resolved, because the displaced document still spells the ID in its
/// own frontmatter and only its author can say which one should keep it.
#[error("{0}; refusing to displace it")]
Collision(crate::index::Collision),
}
impl From<crate::index::Collision> for Error {
fn from(collision: crate::index::Collision) -> Self {
Error::Collision(collision)
}
}
/// Convenience alias for results in this crate.
pub type Result<T> = std::result::Result<T, Error>;
/// Carry a transaction failure into prov's own error vocabulary.
///
/// [`fs_transaction`] phrases its errors for a generic tree of files, since
/// it knows nothing about workspaces. The variants map one-to-one onto prov's,
/// which restate them in terms a prov user can act on — naming `prov check` as
/// the recovery step, and a workspace root as the boundary that was crossed.
impl From<fs_transaction::Error> for Error {
fn from(e: fs_transaction::Error) -> Self {
use fs_transaction::Error as Tx;
match e {
Tx::Io(e) => Error::Io(e),
Tx::Escape(path) => Error::Escape(path),
Tx::StaleJournal(path) => Error::StaleJournal(path),
Tx::Torn { cause, rollback } => Error::Torn { cause, rollback },
// The three that have no prov-level counterpart: a journal prov
// cannot read, a replay it cannot finish, and a path it could not
// encode. All are structural failures of the on-disk state, which
// is what `Structure` names.
Tx::NonUtf8Path(path) => {
Error::Structure(format!("cannot journal non-UTF-8 path: {}", path.display()))
}
Tx::Corrupt(what) => Error::Structure(format!("journal is corrupt: {what}")),
Tx::Recovery(what) => Error::Structure(format!("journal replay: {what}")),
// `fs_transaction::Error` is `#[non_exhaustive]`: a variant added
// upstream must not silently become a compile error here, but it
// must not be mistaken for a prov-level failure either.
other => Error::Structure(other.to_string()),
}
}
}